VbDataTable.vue 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049
  1. <script setup lang="ts">
  2. import { VbUtil } from "@@/vb-dom"
  3. import apis from "@a"
  4. import symbolKeys from "@@@/table/symbol"
  5. import { getAssetPath } from "@@/utils"
  6. import Toolbar from "@@@/table/partials/toolbar/TableToolbar.vue"
  7. import Content from "@@@/table/partials/TableContent.vue"
  8. import Footer from "@@@/table/partials/footer/TableFooter.vue"
  9. import type { Sort, Header, Scroll, ToolBtn } from "@@@/table/models"
  10. import type { VbFormItem, VbFormRowItem } from "@@@/form/models"
  11. import type { AxiosRequestConfig } from "axios"
  12. import type { WritableComputedRef } from "vue"
  13. const formSlotSuffix = "tool-form_"
  14. const props = withDefaults(
  15. defineProps<{
  16. columns: Header[]
  17. columnsFun?: () => Header[]
  18. tableTitle?: string
  19. emptyTableText?: string
  20. keyField?: string
  21. initSearch?: boolean // 页面加载时自动查询
  22. autoSearch?: boolean // 参数变动时自动查询(深监听)
  23. data?: any
  24. selectedRows?: any[]
  25. /* remote */
  26. remoteFun?: (q: any) => Promise<any>
  27. url?: string
  28. method?: string
  29. configs?: AxiosRequestConfig
  30. queryParams?: any
  31. loading?: boolean
  32. loadingText?: string
  33. sortField?: string // 排序字段
  34. sortOrder?: "asc" | "desc"
  35. /* modal form */
  36. modal?: any
  37. formData?: any
  38. exportUrl?: string
  39. exportName?: string
  40. getEntityFun?: (v: any) => Promise<any>
  41. deleteEntityFun?: (v: any) => Promise<any>
  42. resetFormFun?: () => void
  43. /* toolbar */
  44. showToolbar?: boolean // 是否显示toolbar
  45. /* searchbar */
  46. showSearchBar?: boolean // 是否显示Searchbar
  47. searchFormRowItems?: VbFormRowItem[]
  48. searchFormItems?: VbFormItem[]
  49. searchFormSpan?: number
  50. customSearchFun?: (query: any) => void
  51. resetSearchFormFun?: (query: any) => void
  52. /* left toolbar */
  53. handleBtns?: ToolBtn[]
  54. handlePerm?: string
  55. handleFuns?: object
  56. handleDisabledFuns?: object
  57. useCustomBtns?: boolean // 不从远程获取,直接使用自定义的Btns
  58. customBtns?: ToolBtn[]
  59. btnIconPrefix?: string
  60. /* right toolbar */
  61. showRightToolbar?: boolean // 是否显示右边的toolbar
  62. showRightSearchBtn?: boolean // 初始是否显示搜索栏
  63. showRightColumnBtn?: boolean // 是否显示右边toolbar的选择列按钮
  64. rightToolbarBtnGutter?: number // 右边toolbar按钮间距
  65. /* checkbox */
  66. checkMultiple?: boolean // 是否多选
  67. checkPageMultiple?: boolean // 是否跨页多选
  68. hasCheckbox?: boolean // 是否有选择框 (checkMultiple 和 checkPageMultiple 为true 时不起作用)
  69. /* pagination */
  70. pagination?: boolean
  71. noPage?: boolean // 查询不带分页参数
  72. total?: number
  73. currentPage?: number
  74. maxPageBtnCount?: number
  75. pageSize?: number
  76. pageSizeArray?: number[]
  77. pageSizeChange?: boolean
  78. /* fixed */
  79. fixedHeader?: boolean // 固定列头
  80. fixedNumber?: number // 左边固定列数
  81. fixedRightNumber?: number // 右边固定列数
  82. scroll?: Scroll
  83. /* rowspan */
  84. rowSpanSuffix?: string // rowspan 的字段后缀 后台应该组成 `${字段}${后缀}`
  85. /* tree */
  86. isTree?: boolean
  87. expandDepth?: number
  88. intervalLeft?: number
  89. parentField?: string
  90. iconField?: string
  91. childrenField?: string
  92. /* tree lazy */
  93. isLazy?: boolean
  94. isLazySearch?: boolean
  95. rootId?: string
  96. leafField?: string
  97. /* class & style */
  98. tableBoxClass?: string
  99. tableBoxStyle?: any
  100. tableBoxHeight?: number | string
  101. tableBoxWidth?: number | string
  102. tableClass?: string
  103. headerClass?: string
  104. bodyClass?: string
  105. thTrClass?: string
  106. tdTrClass?: string
  107. rowClick?: (row: any) => void
  108. rowDbClick?: (row: any) => void
  109. }>(),
  110. {
  111. tableTitle: "",
  112. emptyTableText: "未查询到数据",
  113. loadingText: "正在加载中,请稍后...",
  114. initSearch: true,
  115. autoSearch: false,
  116. selectedRows: () => [],
  117. method: "get",
  118. sortOrder: "asc",
  119. showToolbar: true,
  120. showSearchBar: true,
  121. searchFormRowItems: () => [],
  122. searchFormItems: () => [],
  123. searchFormSpan: 4,
  124. showRightToolbar: true,
  125. showRightColumnBtn: true,
  126. showRightSearchBtn: true,
  127. rightToolbarBtnGutter: 10,
  128. pagination: true,
  129. noPage: false,
  130. total: 0,
  131. currentPage: 1,
  132. pageSizeArray: () => {
  133. return [15, 25, 50]
  134. },
  135. pageSizeChange: true,
  136. maxPageBtnCount: 5,
  137. hasCheckbox: false,
  138. checkMultiple: false,
  139. checkPageMultiple: false,
  140. fixedHeader: false,
  141. fixedNumber: 0,
  142. fixedRightNumber: 0,
  143. rowSpanSuffix: "_rowSpan",
  144. isTree: false,
  145. parentField: "parent_id",
  146. childrenField: "children",
  147. iconField: "name",
  148. leafField: "isLeaf",
  149. expandDepth: 0,
  150. intervalLeft: 10,
  151. btnIconPrefix: "me-1 bi bi-",
  152. tableBoxClass: "w-100 p-5",
  153. tableClass: "table-bordered table-rounded",
  154. headerClass: "",
  155. bodyClass: "fw-semibold text-gray-600",
  156. thTrClass: "text-center text-gray-800 fw-bold fs-7",
  157. tdTrClass: "text-center fs-7 "
  158. }
  159. )
  160. const emits = defineEmits<{
  161. (e: "update:columns", v: Header[]): void
  162. (e: "update:selectedRows", v: any[]): void
  163. (e: "update:currentPage", v: number): void
  164. (e: "update:pageSize", v: number): void
  165. (e: "update:loading", v: boolean): void
  166. (e: "update:queryParams", v: any): void
  167. (e: "page-change", v: number): void
  168. (e: "update:formData", v: any): void
  169. (e: "sort", v: Sort[]): void
  170. (e: "row-click", row: any): void
  171. (e: "row-db-click", row: any): void
  172. (e: "checkbox-change", isChecked: boolean, row: any): void
  173. (e: "select", row: any): void
  174. (e: "unSelect", row: any): void
  175. (e: "checkbox-all", isChecked: boolean, rows: any[]): void
  176. (e: "select-all", rows: any[]): void
  177. (e: "unSelect-all", rows: any[]): void
  178. }>()
  179. const tableBoxRef = ref()
  180. const tableContentRef = ref()
  181. const id = ref("")
  182. const { data, searchFormRowItems, searchFormItems } = toRefs(props)
  183. const leftFixedRef = ref()
  184. const tableResponsiveRef = ref()
  185. const rightFixedRef = ref()
  186. const emptyQueryParams = JSON.stringify(props.queryParams ? props.queryParams : {})
  187. let treeData: any = []
  188. let treeAllData: any = []
  189. const remoteTotal = ref<number>(0)
  190. const remoteData = ref<any[]>([])
  191. const checkAll = ref<boolean>(false)
  192. const toolbarHandleBtns = ref<ToolBtn[]>([])
  193. const sortParams = ref<Sort[]>([
  194. {
  195. label: props.sortField ?? "",
  196. order: props.sortOrder == "asc" ? "asc" : "desc"
  197. }
  198. ])
  199. const _columns = ref<Header[]>([])
  200. const columns: WritableComputedRef<Header[]> = computed({
  201. get(): Header[] {
  202. if (_columns.value && _columns.value.length > 0) {
  203. if (props.columnsFun) {
  204. const cols = props.columnsFun() ?? []
  205. const newCols: Header[] = []
  206. cols.forEach((v: Header) => {
  207. const col = _columns.value.find((vv: Header) => vv.field == v.field)
  208. if (col) {
  209. v.visible = col.visible
  210. }
  211. newCols.push(v)
  212. })
  213. _columns.value = newCols
  214. }
  215. } else {
  216. _columns.value = props.columnsFun ? props.columnsFun() : props.columns ?? []
  217. }
  218. return _columns.value
  219. },
  220. set(value: Header[]): void {
  221. _columns.value = value
  222. emits("update:columns", value)
  223. }
  224. })
  225. const _selectedRows = ref<any[]>()
  226. const selectedRows: WritableComputedRef<any[]> = computed({
  227. get(): any[] {
  228. return _selectedRows.value ?? props.selectedRows ?? []
  229. },
  230. set(value: any[]): void {
  231. _selectedRows.value = value
  232. emits("update:selectedRows", value)
  233. }
  234. })
  235. const _currentPage = ref<number>()
  236. const currentPage: WritableComputedRef<number> = computed({
  237. get(): number {
  238. return _currentPage.value ?? props.currentPage
  239. },
  240. set(value: number): void {
  241. if (_currentPage.value != value) {
  242. _currentPage.value = value
  243. emits("update:currentPage", value)
  244. emits("page-change", value)
  245. }
  246. }
  247. })
  248. const _pageSize = ref<number>()
  249. const pageSize: WritableComputedRef<number> = computed({
  250. get(): number {
  251. return _pageSize.value ?? props.pageSize ?? props.pageSizeArray[0]
  252. },
  253. set(value: number): void {
  254. _pageSize.value = value
  255. emits("update:pageSize", value)
  256. }
  257. })
  258. const _loading = ref<boolean>()
  259. const loading: WritableComputedRef<boolean> = computed({
  260. get(): boolean {
  261. return _loading.value ?? props.loading
  262. },
  263. set(value: boolean): void {
  264. _loading.value = value
  265. emits("update:loading", value)
  266. }
  267. })
  268. const _innerQueryParams = ref<any>()
  269. const innerQueryParams: WritableComputedRef<any> = computed({
  270. get(): boolean {
  271. return _innerQueryParams.value ?? props.queryParams
  272. },
  273. set(value: any): void {
  274. _innerQueryParams.value = value
  275. }
  276. })
  277. const rowColumns = computed(() => {
  278. const rowCols: Header[] = []
  279. function calcRowColumns(list: Header[], isVisible: boolean) {
  280. list.forEach((v) => {
  281. const visible = isVisible && (v.visible === undefined || v.visible)
  282. if (v.children && v.children.length > 0) {
  283. calcRowColumns(v.children, visible)
  284. } else {
  285. rowCols.push(Object.assign({}, v, { visible }))
  286. }
  287. })
  288. }
  289. calcRowColumns(columns.value, true)
  290. return rowCols
  291. })
  292. const keyField = ref(props.keyField ? props.keyField : rowColumns.value[0].field)
  293. const displayData = computed(() => {
  294. let tableData: any = []
  295. if (!!props.remoteFun || !!props.url) {
  296. tableData = remoteData.value || []
  297. } else if (data?.value) {
  298. tableData = data.value
  299. }
  300. if (tableData.length <= pageSize.value) {
  301. return tableData
  302. } else {
  303. const sliceFrom = (currentPage.value - 1) * pageSize.value
  304. return tableData.slice(sliceFrom, sliceFrom + pageSize.value)
  305. }
  306. })
  307. const isMultipleCheck = computed(() => {
  308. return props.checkMultiple || props.checkPageMultiple
  309. })
  310. const dataTotalCount = computed(() => {
  311. return !!props.remoteFun || !!props.url ? remoteTotal.value : props.total
  312. })
  313. const selectedIds = computed(() => {
  314. return selectedRows.value.map((v) => v[keyField.value])
  315. })
  316. const selectedRowCount = computed(() => {
  317. return selectedRows.value.length
  318. })
  319. const tableBoxClass = computed(() => {
  320. let classStr = props.tableBoxClass
  321. if (props.isTree) {
  322. classStr += " vb-tree-table"
  323. }
  324. return classStr
  325. })
  326. const _tableBoxStyle = ref(calcTableBoxStyle())
  327. const fixedColumn = computed(() => props.fixedNumber > 0 || props.fixedRightNumber > 0)
  328. const tableStyle = computed(() => {
  329. let style = ""
  330. let x = props.scroll?.x
  331. const y = props.scroll?.y
  332. if (fixedColumn.value) {
  333. style += "overflow:auto;"
  334. x = x ?? 1920
  335. }
  336. if (x) {
  337. style += `width:${Number(x) ? x + "px" : x};`
  338. }
  339. if (y) {
  340. style += `height:${Number(y) ? y + "px" : y};`
  341. }
  342. return style
  343. })
  344. const tableRef = computed(() => {
  345. return tableContentRef.value.tableRef
  346. })
  347. const defaultHandleFuns = {
  348. handleCreate: () => {
  349. props.resetFormFun && props.resetFormFun()
  350. if (props.modal) {
  351. props.modal.show()
  352. props.modal.changePrefixTitle("添加")
  353. }
  354. },
  355. handleUpdate: (e?: any, row?: any) => {
  356. props.resetFormFun && props.resetFormFun()
  357. row = row || getSelected()
  358. if (!row) {
  359. return
  360. }
  361. if (props.getEntityFun) {
  362. props.getEntityFun(row[keyField.value]).then((res) => {
  363. emits("update:formData", res.data)
  364. if (props.modal) {
  365. props.modal.show()
  366. props.modal.changePrefixTitle("修改")
  367. }
  368. })
  369. } else {
  370. const _row = Object.assign({}, row)
  371. emits("update:formData", _row)
  372. if (props.modal) {
  373. props.modal.show()
  374. props.modal.changePrefixTitle("修改")
  375. }
  376. }
  377. },
  378. handleDelete: (e?: any, rows?: any) => {
  379. rows = rows || getSelecteds()
  380. if (!rows) {
  381. return
  382. }
  383. if (!props.deleteEntityFun) {
  384. message.alertError("删除接口方法未配置,请联系管理员")
  385. return
  386. }
  387. const ids = rows.map((v: any) => v[keyField.value])
  388. message
  389. .confirm('是否确认删除编号为"' + ids.join(",") + '"的数据项?')
  390. .then(() => {
  391. props.deleteEntityFun &&
  392. props.deleteEntityFun(ids).then(() => {
  393. customSearch()
  394. })
  395. })
  396. .catch(() => {
  397. //
  398. })
  399. },
  400. handleExport: () => {
  401. console.log("TE")
  402. if (props.exportUrl) {
  403. download(
  404. props.exportUrl,
  405. `${props.exportName ? props.exportName + "_" : ""}${new Date().getTime()}.xlsx`,
  406. { method: "POST", params: { ...innerQueryParams.value } }
  407. )
  408. } else {
  409. message.alertError("导出接口方法未配置,请联系管理员")
  410. }
  411. }
  412. }
  413. const resetData = () => {
  414. currentPage.value = 1
  415. selectedRows.value = []
  416. }
  417. const remote = (id?: string) => {
  418. const params = props.noPage
  419. ? innerQueryParams.value
  420. : Object.assign(
  421. { pageNum: currentPage.value, pageSize: pageSize.value },
  422. innerQueryParams.value
  423. )
  424. if (sortParams.value.length) {
  425. params.orderByColumn = ""
  426. params.isAsc = ""
  427. sortParams.value.forEach((v: any) => {
  428. if (v.order && v.label) {
  429. params.orderByColumn += (params.orderByColumn ? "," : "") + v.label
  430. params.isAsc += (params.isAsc ? "," : "") + v.order
  431. }
  432. })
  433. }
  434. let curData: any = {}
  435. if (props.isLazy) {
  436. curData = treeAllData.find((v: any) => {
  437. return v[keyField.value] == id
  438. })
  439. if (!id && !curData) {
  440. throw new Error("懒加载模式,未获取到 parentId ,请检查")
  441. }
  442. if (curData?.children && curData?.children.length) {
  443. if (props.isLazySearch) {
  444. curData.children = []
  445. } else {
  446. return Promise.resolve(0)
  447. }
  448. }
  449. if (!props.keyField) {
  450. throw new Error("懒加载模式,请配置 keyField")
  451. }
  452. params[props.keyField] = id
  453. }
  454. loading.value = true
  455. return new Promise((resolve, reject) => {
  456. if (props.remoteFun) {
  457. props
  458. .remoteFun(params)
  459. .then((res: any) => {
  460. processData(res.rows || res.data, res.total)
  461. })
  462. .catch((err) => {
  463. loading.value = false
  464. reject(err)
  465. })
  466. } else if (props.url) {
  467. const qParam = props.method.toLocaleLowerCase() == "get" ? { params } : { data: params }
  468. const configs = Object.assign(
  469. { url: props.url, method: props.method, successAlert: false },
  470. props.configs,
  471. qParam
  472. )
  473. Rs.request(configs)
  474. .then((res: any) => {
  475. processData(res.rows || res.data, res.total)
  476. })
  477. .catch(() => {
  478. loading.value = false
  479. })
  480. }
  481. function processData(data: any, total: number) {
  482. remoteTotal.value = total || 0
  483. loading.value = false
  484. if (props.isLazy) {
  485. if (props.isLazySearch) {
  486. Object.keys(innerQueryParams.value).forEach((v) => {
  487. innerQueryParams.value[v] = undefined
  488. })
  489. if (!data.length) {
  490. const item = {}
  491. item[props.iconField ?? ""] = "没有查询到字节点"
  492. item[props.leafField ?? ""] = true
  493. item[props.parentField ?? ""] = id
  494. data.push(item)
  495. }
  496. resolve(1)
  497. }
  498. if (curData) {
  499. curData.children = []
  500. data.forEach((v: any) => {
  501. const index = treeAllData.findIndex((vv: any) => {
  502. return vv[keyField.value] == v[keyField.value]
  503. })
  504. if (index < 0) {
  505. treeAllData.push(v)
  506. curData.children.push(treeAllData[treeAllData.length - 1])
  507. } else {
  508. curData.children.push(treeAllData[index])
  509. }
  510. })
  511. } else {
  512. treeData.push(...data)
  513. treeAllData.push(...treeData)
  514. }
  515. remoteData.value = JSON.parse(JSON.stringify(treeData))
  516. } else {
  517. remoteData.value = data
  518. }
  519. }
  520. })
  521. }
  522. const isContentSlots = (name: string | number): boolean => {
  523. const str = name.toString()
  524. return (
  525. rowColumns.value.find((v) => {
  526. return v.field == str || v.field + "_header" == str
  527. }) != null
  528. )
  529. }
  530. const loadHandleBtns = () => {
  531. toolbarHandleBtns.value = []
  532. if (!props.useCustomBtns && props.handlePerm) {
  533. const _handleFuns = Object.assign({}, defaultHandleFuns, props.handleFuns ?? {})
  534. apis.system.menuApi.menuChildrenByPerms(props.handlePerm).then((res) => {
  535. if (res.data && res.data.length > 0) {
  536. res.data.forEach((v: any) => {
  537. if (v.btnScript && v.btnScript != "hide") {
  538. const scripts = v.btnScript.split("@")
  539. const _btn = props.handleBtns?.find((v) => {
  540. return v.key == scripts[0]
  541. })
  542. const btn: ToolBtn = {
  543. key: scripts[0],
  544. show: _btn?.show == undefined ? true : _btn?.show,
  545. name: _btn?.name ?? v.menuName,
  546. permission: _btn?.permission ?? v.perms,
  547. clickFun: _btn?.clickFun ?? _handleFuns[scripts[0]],
  548. icon: _btn?.icon ?? `${props.btnIconPrefix}${v.icon}`,
  549. iconType: _btn?.iconType ?? "class",
  550. btnClass: _btn?.btnClass ?? v.btnClass,
  551. disabledFun: _btn?.disabledFun
  552. }
  553. if (scripts.length > 1) {
  554. btn.disabledFun = (r: number) => {
  555. const temp = Number(scripts[1])
  556. if (temp == 0) {
  557. return r == 0
  558. }
  559. return r != Number(scripts[1])
  560. }
  561. }
  562. if (props.handleDisabledFuns && props.handleDisabledFuns[scripts[0]]) {
  563. btn.disabledFun = props.handleDisabledFuns[scripts[0]]
  564. }
  565. toolbarHandleBtns.value.push(btn)
  566. }
  567. })
  568. }
  569. toolbarHandleBtns.value.push(...(props.customBtns ?? []))
  570. })
  571. } else {
  572. toolbarHandleBtns.value = props.customBtns ?? []
  573. }
  574. }
  575. const customSearch = () => {
  576. if (props.customSearchFun) {
  577. props.customSearchFun(innerQueryParams.value)
  578. } else {
  579. search()
  580. }
  581. }
  582. function calcTableBoxStyle() {
  583. const style = props.tableBoxStyle ?? {}
  584. if (props.tableBoxHeight) {
  585. style.height = Number(props.tableBoxHeight) ? props.tableBoxHeight + "px" : props.tableBoxHeight
  586. } else {
  587. nextTick(() => {
  588. const tableBoxAutoHeight =
  589. document.documentElement.clientHeight -
  590. (document.querySelector(".app-header")?.clientHeight ?? 0) -
  591. (document.querySelector(".app-footer")?.clientHeight ?? 0) -
  592. 5
  593. style.height = tableBoxAutoHeight + "px"
  594. })
  595. }
  596. if (props.tableBoxWidth) {
  597. style.width = Number(props.tableBoxWidth) ? props.tableBoxWidth + "px" : props.tableBoxWidth
  598. } else {
  599. style.width = "100%"
  600. }
  601. return style
  602. }
  603. function query(query?: any) {
  604. if (query) {
  605. innerQueryParams.value = Object.assign({}, query)
  606. }
  607. search()
  608. }
  609. function search(isReset = true) {
  610. if (isReset) {
  611. resetData()
  612. }
  613. if (!!props.remoteFun || !!props.url) {
  614. remoteData.value = []
  615. treeData = []
  616. treeAllData = []
  617. remote(props.isLazy ? props.rootId : undefined)
  618. }
  619. }
  620. function getQueryParams() {
  621. return innerQueryParams.value
  622. }
  623. function getSelected() {
  624. return getSelecteds()[0]
  625. }
  626. function getSelecteds() {
  627. return selectedRows.value
  628. }
  629. function getSelectedIds() {
  630. return selectedIds.value
  631. }
  632. function getData() {
  633. return remoteData.value
  634. }
  635. function setSelecteds(rows: any[] | any, isSelected: boolean) {
  636. if (!Array.isArray(rows)) {
  637. rows = [rows]
  638. }
  639. if (isSelected) {
  640. selectedRows.value = [...new Set([...selectedRows.value, ...rows])]
  641. } else {
  642. selectedRows.value = selectedRows.value.filter((item: any) => !rows.includes(item))
  643. }
  644. }
  645. function clearSelecteds() {
  646. selectedRows.value = []
  647. }
  648. provide(symbolKeys.tableBox, tableBoxRef)
  649. provide(symbolKeys.searchFrom, {
  650. queryParams: innerQueryParams,
  651. searchFormRowItems,
  652. searchFormItems,
  653. searchFormSpan: props.searchFormSpan
  654. })
  655. provide(symbolKeys.toolBtns, toolbarHandleBtns)
  656. provide(symbolKeys.selectedRowCount, selectedRowCount)
  657. provide(symbolKeys.rightToolbar, {
  658. columns,
  659. showSearchBtn:
  660. props.showRightSearchBtn && (!!props.searchFormItems || !!props.searchFormRowItems),
  661. showColumnBtn: props.showRightColumnBtn,
  662. gutter: props.rightToolbarBtnGutter
  663. })
  664. provide(symbolKeys.content, {
  665. loading,
  666. tableClass: props.tableClass,
  667. tableStyle: tableStyle.value
  668. })
  669. provide(symbolKeys.checkAll, checkAll)
  670. provide(symbolKeys.header, {
  671. columns,
  672. hasCheckbox: isMultipleCheck.value || props.hasCheckbox,
  673. isMultipleCheck: isMultipleCheck.value,
  674. sortField: props.sortField ?? "",
  675. sortOrder: props.sortOrder,
  676. headerClass: props.fixedHeader ? props.headerClass + " fixed" : props.headerClass,
  677. thTrClass: props.thTrClass
  678. })
  679. provide(symbolKeys.body, {
  680. isTree: props.isTree,
  681. childrenField: props.childrenField,
  682. bodyClass: props.bodyClass
  683. })
  684. provide(symbolKeys.bodyTr, {
  685. selectedIds,
  686. isTree: props.isTree,
  687. keyField: keyField.value,
  688. hasCheckbox: isMultipleCheck.value || props.hasCheckbox,
  689. isMultipleCheck: isMultipleCheck.value,
  690. expandDepth: props.expandDepth,
  691. intervalLeft: props.intervalLeft,
  692. iconField: props.iconField,
  693. parentField: props.parentField,
  694. childrenField: props.childrenField,
  695. leafField: props.leafField,
  696. isLazy: props.isLazy,
  697. lazySearch: props.isLazySearch,
  698. tdTrClass: props.tdTrClass
  699. })
  700. provide(symbolKeys.bodyTds, {
  701. columns: rowColumns,
  702. rowSpanSuffix: props.rowSpanSuffix,
  703. expandDepth: props.expandDepth,
  704. intervalLeft: props.intervalLeft,
  705. iconField: props.iconField,
  706. parentField: props.parentField,
  707. childrenField: props.childrenField,
  708. leafField: props.leafField,
  709. isLazy: props.isLazy
  710. })
  711. provide(symbolKeys.displayData, displayData)
  712. provide(symbolKeys.pagination, {
  713. total: dataTotalCount,
  714. currentPage,
  715. pageSize,
  716. maxPageBtnCount: props.maxPageBtnCount
  717. })
  718. provide(symbolKeys.pagePerItems, {
  719. total: dataTotalCount,
  720. currentPage,
  721. pageSize,
  722. pageSizeArray: props.pageSizeArray,
  723. pageSizeChange: props.pageSizeChange
  724. })
  725. provide(symbolKeys.formSlotSuffix, formSlotSuffix)
  726. const onPageChange = (page: number) => {
  727. currentPage.value = page
  728. search(false)
  729. }
  730. const onPageSizeChange = (count: number) => {
  731. pageSize.value = count
  732. search()
  733. }
  734. const onColumnsChange = (headers: Header[]) => {
  735. columns.value = headers
  736. }
  737. const onSort = (v: Sort[]) => {
  738. const sorts = toValue(v)
  739. sortParams.value = sorts
  740. emits("sort", sorts)
  741. search(false)
  742. }
  743. const onSelectAll = (isChecked: boolean) => {
  744. let sRows: any = []
  745. if (isChecked) {
  746. sRows = [...new Set([...selectedRows.value, ...displayData.value])]
  747. selectedRows.value = sRows
  748. emits("select-all", sRows)
  749. emits("checkbox-all", true, sRows)
  750. } else {
  751. if (props.checkPageMultiple) {
  752. sRows = selectedRows.value.filter(
  753. (v: any) => !displayData.value.find((vv: any) => v[keyField.value] == vv[keyField.value])
  754. )
  755. } else {
  756. sRows = []
  757. }
  758. selectedRows.value = sRows
  759. emits("unSelect-all", sRows)
  760. emits("checkbox-all", false, displayData.value)
  761. }
  762. }
  763. const onSelect = (v: { isChecked: boolean; row: any }) => {
  764. let sRows: any = []
  765. const row = toValue(v.row)
  766. if (v.isChecked) {
  767. sRows = isMultipleCheck.value ? [...new Set([...selectedRows.value, row])] : [row]
  768. selectedRows.value = sRows
  769. emits("select", row)
  770. emits("checkbox-change", true, row)
  771. } else {
  772. sRows = isMultipleCheck.value
  773. ? selectedRows.value.filter((vv) => vv[keyField.value] != row[keyField.value])
  774. : []
  775. selectedRows.value = sRows
  776. emits("unSelect", row)
  777. emits("checkbox-change", false, row)
  778. }
  779. }
  780. const onQuery = () => {
  781. emits("update:queryParams", innerQueryParams.value)
  782. }
  783. const onRowClick = (v: any) => {
  784. const row = toValue(v)
  785. props.rowClick && props.rowClick(row)
  786. emits("row-click", row)
  787. }
  788. const onRowDbClick = (v: any) => {
  789. const row = toValue(v)
  790. props.rowDbClick && props.rowDbClick(row)
  791. emits("row-db-click", row)
  792. }
  793. const onReset = () => {
  794. innerQueryParams.value = JSON.parse(emptyQueryParams)
  795. props.resetSearchFormFun && props.resetSearchFormFun(innerQueryParams.value)
  796. emits("update:queryParams", innerQueryParams.value)
  797. }
  798. const onTreeToggle = (v: any) => {
  799. const row = toValue(v)
  800. const id = "tr_" + row[keyField.value]
  801. const tr = tableBoxRef.value?.querySelector(`#${id}`)
  802. const icon = tr?.querySelector(".icon")
  803. if (!tr || !icon) {
  804. return
  805. }
  806. if (props.isLazy) {
  807. // 已有children可展开的直接展开,不在调用懒加载
  808. if (tr.className.search("hasChildren") >= 0) {
  809. if (tr.className.search("tr-expand") >= 0 || !props.isLazySearch) {
  810. toggle(tr, icon, id)
  811. return
  812. }
  813. }
  814. remote(id).then((res) => {
  815. if (res == 1 && props.isLazySearch) {
  816. nextTick(() => {
  817. showChildren(id)
  818. })
  819. }
  820. })
  821. tr.className = tr.className.replace("tr-collapse", "tr-expand")
  822. icon.className = icon.className.replace("ki-add-folder", "ki-minus-folder")
  823. } else {
  824. toggle(tr, icon, id)
  825. }
  826. function toggle(_tr: HTMLElement, _icon: HTMLElement, id: string) {
  827. if (_tr.className.search("tr-expand") >= 0) {
  828. _tr.className = _tr.className.replace("tr-expand", "tr-collapse")
  829. _icon.className = _icon.className.replace("ki-minus-folder", "ki-add-folder")
  830. hideChildren(id)
  831. } else if (_tr.className.search("tr-collapse") >= 0) {
  832. _tr.className = _tr.className.replace("tr-collapse", "tr-expand")
  833. _icon.className = _icon.className.replace("ki-add-folder", "ki-minus-folder")
  834. showChildren(id)
  835. }
  836. }
  837. function hideChildren(parentId: string) {
  838. const children = document.querySelectorAll(`[data-parent="${parentId}"]`)
  839. if (children && children.length) {
  840. children.forEach((v) => {
  841. v.className = v.className.replace("show", "hide")
  842. v.className = v.className.replace("tr-expand", "tr-collapse")
  843. const iconEl = v.querySelector(".icon")
  844. if (iconEl) {
  845. iconEl.className = iconEl.className.replace("ki-minus-folder", "ki-add-folder")
  846. }
  847. hideChildren(v.id)
  848. })
  849. }
  850. }
  851. function showChildren(parentId: string) {
  852. const children = document.querySelectorAll(`[data-parent="${parentId}"]`)
  853. if (children && children.length) {
  854. children.forEach((v) => {
  855. v.className = v.className.replace("hide", "show")
  856. })
  857. }
  858. }
  859. }
  860. const onFixedScrollX = () => {
  861. leftFixedRef.value.className = leftFixedRef.value.className.replace("no-shadow", "")
  862. rightFixedRef.value.className = rightFixedRef.value.className.replace("no-shadow", "")
  863. if (tableResponsiveRef.value) {
  864. const left = tableResponsiveRef.value.scrollLeft
  865. if (left == 0) {
  866. leftFixedRef.value.className += " no-shadow"
  867. }
  868. if (left >= tableRef.value.clientWidth - tableResponsiveRef.value.clientWidth) {
  869. rightFixedRef.value.className += " no-shadow"
  870. }
  871. }
  872. }
  873. function BindInterEvent() {
  874. VbUtil.EventHandlerUtil.on(tableBoxRef.value, "vbtable.row.click", onRowClick)
  875. VbUtil.EventHandlerUtil.on(tableBoxRef.value, "vbtable.row.dbclick", onRowDbClick)
  876. VbUtil.EventHandlerUtil.on(tableBoxRef.value, "vbtable.query", onQuery)
  877. VbUtil.EventHandlerUtil.on(tableBoxRef.value, "vbtable.reset", onReset)
  878. VbUtil.EventHandlerUtil.on(tableBoxRef.value, "vbtable.sort", onSort)
  879. VbUtil.EventHandlerUtil.on(tableBoxRef.value, "vbtable.select-all", onSelectAll)
  880. VbUtil.EventHandlerUtil.on(tableBoxRef.value, "vbtable.select", onSelect)
  881. VbUtil.EventHandlerUtil.on(tableBoxRef.value, "vbtable.page-size-change", onPageSizeChange)
  882. VbUtil.EventHandlerUtil.on(tableBoxRef.value, "vbtable.page-change", onPageChange)
  883. VbUtil.EventHandlerUtil.on(tableBoxRef.value, "vbtable.columns-change", onColumnsChange)
  884. VbUtil.EventHandlerUtil.on(tableBoxRef.value, "vbtable.row.tree-toggle", onTreeToggle)
  885. if (fixedColumn.value && tableResponsiveRef.value) {
  886. tableResponsiveRef.value.addEventListener("scroll", onFixedScrollX, true)
  887. }
  888. }
  889. function initFixedColumns() {
  890. if (fixedColumn.value) {
  891. const tr = tableRef.value?.querySelector("tr")
  892. if (tr && tr.children) {
  893. let width = 0
  894. if (leftFixedRef.value) {
  895. let num = props.fixedNumber
  896. if (props.hasCheckbox || props.checkMultiple || props.checkPageMultiple) {
  897. num += 1
  898. }
  899. for (let i = 0; i < num; i++) {
  900. width += tr.children[i]?.clientWidth ?? 0
  901. }
  902. leftFixedRef.value.style.width = width + "px"
  903. leftFixedRef.value.firstElementChild.style.width = tableRef.value.clientWidth + "px"
  904. }
  905. if (rightFixedRef.value) {
  906. width = 0
  907. const trLength = tr.children.length
  908. for (let i = trLength - 1; i >= trLength - props.fixedRightNumber; i--) {
  909. width += tr.children[i]?.clientWidth ?? 0
  910. }
  911. rightFixedRef.value.style.width = width + "px"
  912. rightFixedRef.value.firstElementChild.style.width = tableRef.value.clientWidth + "px"
  913. rightFixedRef.value.firstElementChild.style.transform = `translateX(-${
  914. tableRef.value.clientWidth - width
  915. }px)`
  916. if (tableRef.value.clientWidth <= tableResponsiveRef.value.clientWidth) {
  917. rightFixedRef.value.className += " no-shadow"
  918. }
  919. }
  920. }
  921. }
  922. }
  923. function init() {
  924. id.value = VbUtil.getUniqueIdWithPrefix("vb-table")
  925. if (props.initSearch) {
  926. customSearch()
  927. }
  928. loadHandleBtns()
  929. nextTick(() => {
  930. initFixedColumns()
  931. BindInterEvent()
  932. })
  933. window.addEventListener("resize", () => {
  934. _tableBoxStyle.value = calcTableBoxStyle()
  935. })
  936. }
  937. onMounted(init)
  938. onUnmounted(() => {
  939. tableResponsiveRef.value?.removeEventListener("scroll", onFixedScrollX, true)
  940. })
  941. watch(selectedRows, (val: any) => {
  942. if (val.length > 0 && val.length == displayData.value.length) {
  943. let flag = true
  944. displayData.value.forEach((item: any) => {
  945. if (
  946. selectedRows.value.find((row: any) => row[keyField.value] == item[keyField.value]) == null
  947. ) {
  948. flag = false
  949. return
  950. }
  951. })
  952. checkAll.value = flag
  953. } else {
  954. checkAll.value = false
  955. }
  956. })
  957. watch(
  958. () => props.queryParams,
  959. (val: any) => {
  960. innerQueryParams.value = val
  961. customSearch()
  962. },
  963. { deep: props.autoSearch }
  964. )
  965. defineExpose({
  966. tableRef: tableBoxRef,
  967. query,
  968. getSelected,
  969. getSelectedIds,
  970. getSelecteds,
  971. getData,
  972. getQueryParams,
  973. setSelecteds,
  974. clearSelecteds,
  975. defaultHandleFuns
  976. })
  977. </script>
  978. <template>
  979. <div ref="tableBoxRef" class="vb-table" :id="id" :class="tableBoxClass" :style="_tableBoxStyle">
  980. <Toolbar
  981. v-if="showToolbar"
  982. :tableTitle="tableTitle"
  983. :showSearchBar="showSearchBar"
  984. :showRightToolbar="showRightToolbar">
  985. <template v-for="(_, name) in $slots" #[name]>
  986. <slot v-if="name.toString().search(formSlotSuffix) == 0" :name="name" />
  987. </template>
  988. </Toolbar>
  989. <div class="table-box">
  990. <div
  991. v-if="props.fixedNumber > 0"
  992. ref="leftFixedRef"
  993. class="fixed-columns fixed-columns-left no-shadow">
  994. <Content>
  995. <template v-for="(_, name) in $slots" #[name]="{ row }">
  996. <slot v-if="isContentSlots(name)" :name="name" :row="row" />
  997. </template>
  998. </Content>
  999. </div>
  1000. <div ref="tableResponsiveRef" class="table-responsive">
  1001. <Content ref="tableContentRef">
  1002. <template v-for="(_, name) in $slots" #[name]="{ row }">
  1003. <slot v-if="isContentSlots(name)" :name="name" :row="row" />
  1004. </template>
  1005. </Content>
  1006. <template v-if="loading">
  1007. <div class="h-100px d-flex justify-content-center flex-column align-items-center">
  1008. <div class="spinner-border text-gray-700 mb-5"></div>
  1009. <span class="text-gray-700">{{ loadingText }}</span>
  1010. </div>
  1011. </template>
  1012. <div
  1013. v-else-if="displayData.length == 0 && !loading"
  1014. class="h-100px d-flex justify-content-center flex-column align-items-center">
  1015. <img class="mb-2" :src="getAssetPath('media/table/empty.svg')" />
  1016. <span class="text-gray-700">{{ emptyTableText }}</span>
  1017. </div>
  1018. </div>
  1019. <div v-if="props.fixedRightNumber > 0" ref="rightFixedRef" class="fixed-columns-right">
  1020. <Content>
  1021. <template v-for="(_, name) in $slots" #[name]="{ row }">
  1022. <slot v-if="isContentSlots(name)" :name="name" :row="row" />
  1023. </template>
  1024. </Content>
  1025. </div>
  1026. </div>
  1027. <Footer :loading="loading" :pagination="pagination && !noPage" />
  1028. </div>
  1029. </template>