StdTable.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. <script setup lang="ts">
  2. import { message } from 'ant-design-vue'
  3. import { HolderOutlined } from '@ant-design/icons-vue'
  4. import type { ComputedRef, Ref } from 'vue'
  5. import type { SorterResult, TablePaginationConfig } from 'ant-design-vue/lib/table/interface'
  6. import type { FilterValue } from 'ant-design-vue/es/table/interface'
  7. import type { Key } from 'ant-design-vue/es/_util/type'
  8. import type { RouteParams } from 'vue-router'
  9. import _ from 'lodash'
  10. import StdPagination from './StdPagination.vue'
  11. import StdDataEntry from '@/components/StdDesign/StdDataEntry'
  12. import type { Pagination } from '@/api/curd'
  13. import type { Column } from '@/components/StdDesign/types'
  14. import useSortable from '@/components/StdDesign/StdDataDisplay/methods/sortable'
  15. import type Curd from '@/api/curd'
  16. export interface StdTableProps {
  17. title?: string
  18. mode?: string
  19. rowKey?: string
  20. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  21. api: Curd<any>
  22. columns: Column[]
  23. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  24. getParams?: Record<string, any>
  25. size?: string
  26. disableQueryParams?: boolean
  27. disableSearch?: boolean
  28. pithy?: boolean
  29. exportExcel?: boolean
  30. exportMaterial?: boolean
  31. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  32. overwriteParams?: Record<string, any>
  33. disabledView?: boolean
  34. disabledModify?: boolean
  35. selectionType?: string
  36. sortable?: boolean
  37. disableDelete?: boolean
  38. disablePagination?: boolean
  39. sortableMoveHook?: (oldRow: number[], newRow: number[]) => boolean
  40. scrollX?: string | number
  41. }
  42. const props = withDefaults(defineProps<StdTableProps>(), {
  43. rowKey: 'id',
  44. })
  45. const emit = defineEmits(['clickEdit', 'clickView', 'clickBatchModify', 'update:selectedRowKeys'])
  46. const route = useRoute()
  47. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  48. const dataSource: Ref<any[]> = ref([])
  49. const expandKeysList: Ref<Key[]> = ref([])
  50. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  51. const rowsKeyIndexMap: Ref<Record<number, any>> = ref({})
  52. const loading = ref(true)
  53. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  54. const selectedRecords: Ref<Record<any, any>> = ref({})
  55. // This can be useful if there are more than one StdTable in the same page.
  56. const randomId = ref(Math.random().toString(36).substring(2, 8))
  57. const updateFilter = ref(0)
  58. const init = ref(false)
  59. const pagination: Pagination = reactive({
  60. total: 1,
  61. per_page: 10,
  62. current_page: 1,
  63. total_pages: 1,
  64. })
  65. const params = reactive({
  66. ...props.getParams,
  67. })
  68. const get_list = _.debounce(_get_list, 200, {
  69. leading: true,
  70. trailing: false,
  71. })
  72. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  73. const selectedRowKeys = defineModel<any[]>('selectedRowKeys', {
  74. default: () => [],
  75. })
  76. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  77. const selectedRows = defineModel<any[]>('selectedRows', {
  78. type: Array,
  79. default: () => [],
  80. })
  81. onMounted(() => {
  82. selectedRows.value.forEach(v => {
  83. selectedRecords.value[v[props.rowKey]] = v
  84. })
  85. })
  86. const searchColumns = computed(() => {
  87. const _searchColumns: Column[] = []
  88. props.columns.forEach((column: Column) => {
  89. if (column.search) {
  90. if (typeof column.search === 'object') {
  91. _searchColumns.push({
  92. ...column,
  93. edit: column.search,
  94. })
  95. }
  96. else {
  97. _searchColumns.push({ ...column })
  98. }
  99. }
  100. })
  101. return _searchColumns
  102. })
  103. const pithyColumns = computed<Column[]>(() => {
  104. if (props.pithy) {
  105. return props.columns?.filter(c => {
  106. return c.pithy === true && !c.hiddenInTable
  107. })
  108. }
  109. return props.columns?.filter(c => {
  110. return !c.hiddenInTable
  111. })
  112. })
  113. const batchColumns = computed(() => {
  114. const batch: Column[] = []
  115. props.columns?.forEach(column => {
  116. if (column.batch)
  117. batch.push(column)
  118. })
  119. return batch
  120. })
  121. watch(route, () => {
  122. params.trash = route.query.trash === 'true'
  123. })
  124. const filterParams = reactive({})
  125. watch(filterParams, () => {
  126. Object.assign(params, {
  127. ...filterParams,
  128. page: 1,
  129. })
  130. })
  131. onMounted(() => {
  132. if (!props.disableQueryParams) {
  133. Object.assign(params, {
  134. ...route.query,
  135. trash: route.query.trash === 'true',
  136. })
  137. Object.assign(filterParams, {
  138. ...route.query,
  139. trash: route.query.trash === 'true',
  140. })
  141. }
  142. get_list()
  143. if (props.sortable)
  144. initSortable()
  145. if (!selectedRowKeys.value?.length)
  146. selectedRowKeys.value = []
  147. init.value = true
  148. })
  149. defineExpose({
  150. get_list,
  151. pagination,
  152. })
  153. function destroy(id: number | string) {
  154. props.api!.destroy(id, { permanent: params.trash }).then(() => {
  155. get_list()
  156. message.success($gettext('Deleted successfully'))
  157. }).catch(e => {
  158. message.error($gettext(e?.message ?? 'Server error'))
  159. })
  160. }
  161. function recover(id: number | string) {
  162. props.api.recover(id).then(() => {
  163. message.success($gettext('Recovered Successfully'))
  164. get_list()
  165. }).catch(e => {
  166. message.error(e?.message ?? $gettext('Server error'))
  167. })
  168. }
  169. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  170. function buildIndexMap(data: any, level: number = 0, index: number = 0, total: number[] = []) {
  171. if (data && data.length > 0) {
  172. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  173. data.forEach((v: any) => {
  174. v.level = level
  175. const current_indexes = [...total, index++]
  176. rowsKeyIndexMap.value[v.id] = current_indexes
  177. if (v.children)
  178. buildIndexMap(v.children, level + 1, 0, current_indexes)
  179. })
  180. }
  181. }
  182. async function _get_list(page_num = null, page_size = 20) {
  183. loading.value = true
  184. if (page_num) {
  185. params.page = page_num
  186. params.page_size = page_size
  187. }
  188. props.api?.get_list({ ...route.query, ...params, ...props.overwriteParams }).then(async r => {
  189. dataSource.value = r.data
  190. rowsKeyIndexMap.value = {}
  191. if (props.sortable)
  192. buildIndexMap(r.data)
  193. if (r.pagination)
  194. Object.assign(pagination, r.pagination)
  195. setTimeout(() => {
  196. loading.value = false
  197. }, 200)
  198. }).catch(e => {
  199. message.error(e?.message ?? $gettext('Server error'))
  200. })
  201. }
  202. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  203. function onTableChange(_pagination: TablePaginationConfig, filters: Record<string, FilterValue>, sorter: SorterResult | SorterResult<any>[]) {
  204. if (sorter) {
  205. sorter = sorter as SorterResult
  206. selectedRowKeys.value = []
  207. params.sort_by = sorter.field
  208. params.order = sorter.order === 'ascend' ? 'asc' : 'desc'
  209. switch (sorter.order) {
  210. case 'ascend':
  211. params.sort = 'asc'
  212. break
  213. case 'descend':
  214. params.sort = 'desc'
  215. break
  216. default:
  217. params.sort = null
  218. break
  219. }
  220. }
  221. if (filters) {
  222. Object.keys(filters).forEach((v: string) => {
  223. params[v] = filters[v]
  224. })
  225. }
  226. if (_pagination)
  227. selectedRowKeys.value = []
  228. }
  229. function expandedTable(keys: Key[]) {
  230. expandKeysList.value = keys
  231. }
  232. // eslint-disable-next-line @typescript-eslint/no-explicit-any,sonarjs/cognitive-complexity
  233. async function onSelect(record: any, selected: boolean, _selectedRows: any[]) {
  234. if (props.selectionType === 'checkbox' || props.exportExcel) {
  235. if (selected) {
  236. _selectedRows.forEach(v => {
  237. if (v) {
  238. if (selectedRecords.value[v[props.rowKey]] === undefined)
  239. selectedRowKeys.value.push(v[props.rowKey])
  240. selectedRecords.value[v[props.rowKey]] = v
  241. }
  242. })
  243. }
  244. else {
  245. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  246. selectedRowKeys.value = selectedRowKeys.value.filter((v: any) => v !== record[props.rowKey])
  247. delete selectedRecords.value[record[props.rowKey]]
  248. }
  249. await nextTick(async () => {
  250. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  251. const filteredRows: any[] = []
  252. selectedRowKeys.value.forEach(v => {
  253. filteredRows.push(selectedRecords.value[v])
  254. })
  255. selectedRows.value = filteredRows
  256. })
  257. }
  258. else {
  259. if (selected) {
  260. selectedRowKeys.value = record[props.rowKey]
  261. selectedRows.value = [record]
  262. }
  263. else {
  264. selectedRowKeys.value = []
  265. selectedRows.value = []
  266. }
  267. }
  268. }
  269. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  270. async function onSelectAll(selected: boolean, _selectedRows: any[], changeRows: any[]) {
  271. // console.log(selected, selectedRows, changeRows)
  272. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  273. changeRows.forEach((v: any) => {
  274. if (v) {
  275. if (selected) {
  276. selectedRowKeys.value.push(v[props.rowKey])
  277. selectedRecords.value[v[props.rowKey]] = v
  278. }
  279. else {
  280. delete selectedRecords.value[v[props.rowKey]]
  281. }
  282. }
  283. })
  284. if (!selected) {
  285. selectedRowKeys.value = selectedRowKeys.value.filter(v => {
  286. return selectedRecords.value[v]
  287. })
  288. }
  289. // console.log(selectedRowKeysBuffer.value, selectedRecords.value)
  290. await nextTick(async () => {
  291. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  292. const filteredRows: any[] = []
  293. selectedRowKeys.value.forEach(v => {
  294. filteredRows.push(selectedRecords.value[v])
  295. })
  296. selectedRows.value = filteredRows
  297. })
  298. }
  299. const router = useRouter()
  300. const resetSearch = async () => {
  301. Object.keys(params).forEach(v => {
  302. delete params[v]
  303. })
  304. Object.assign(params, {
  305. ...props.getParams,
  306. })
  307. router.push({ query: {} }).catch(() => {
  308. })
  309. updateFilter.value++
  310. }
  311. watch(params, async v => {
  312. if (init.value) {
  313. await nextTick(() => {
  314. get_list()
  315. })
  316. if (!props.disableQueryParams)
  317. await router.push({ query: v as RouteParams })
  318. }
  319. })
  320. if (props.getParams) {
  321. const getParams = computed(() => props.getParams)
  322. watch(getParams, () => {
  323. Object.assign(params, {
  324. ...props.getParams,
  325. page: 1,
  326. })
  327. }, { deep: true })
  328. }
  329. if (props.overwriteParams) {
  330. const overwriteParams = computed(() => props.overwriteParams)
  331. watch(overwriteParams, () => {
  332. Object.assign(params, {
  333. page: 1,
  334. })
  335. if (params.page === 1)
  336. get_list()
  337. }, { deep: true })
  338. }
  339. const rowSelection = computed(() => {
  340. if (batchColumns.value.length > 0 || props.selectionType || props.exportExcel) {
  341. return {
  342. selectedRowKeys: selectedRowKeys.value,
  343. onSelect,
  344. onSelectAll,
  345. type: (batchColumns.value.length > 0 || props.exportExcel) ? 'checkbox' : props.selectionType,
  346. }
  347. }
  348. else {
  349. return null
  350. }
  351. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  352. }) as ComputedRef<any>
  353. const hasSelectedRow = computed(() => {
  354. return batchColumns.value.length > 0 && selectedRowKeys.value.length > 0
  355. })
  356. function clickBatchEdit() {
  357. emit('clickBatchModify', batchColumns.value, selectedRowKeys.value)
  358. }
  359. function initSortable() {
  360. useSortable(props, randomId, dataSource, rowsKeyIndexMap, expandKeysList)
  361. }
  362. function changePage(page: number, page_size: number) {
  363. Object.assign(params, {
  364. page,
  365. page_size,
  366. })
  367. }
  368. const paginationSize = computed(() => {
  369. if (props.size === 'small')
  370. return 'small'
  371. else
  372. return 'default'
  373. })
  374. </script>
  375. <template>
  376. <div class="std-table">
  377. <StdDataEntry
  378. v-if="!disableSearch && searchColumns.length"
  379. :key="updateFilter"
  380. :data-list="searchColumns"
  381. :data-source="filterParams"
  382. type="search"
  383. layout="inline"
  384. >
  385. <template #action>
  386. <ASpace class="action-btn">
  387. <AButton @click="resetSearch">
  388. {{ $gettext('Reset') }}
  389. </AButton>
  390. <AButton
  391. v-if="hasSelectedRow"
  392. @click="clickBatchEdit"
  393. >
  394. {{ $gettext('Batch Modify') }}
  395. </AButton>
  396. <slot name="append-search" />
  397. </ASpace>
  398. </template>
  399. </StdDataEntry>
  400. <ATable
  401. :id="`std-table-${randomId}`"
  402. :columns="pithyColumns"
  403. :data-source="dataSource"
  404. :loading="loading"
  405. :pagination="false"
  406. :row-key="rowKey"
  407. :row-selection="rowSelection"
  408. :scroll="{ x: scrollX }"
  409. :size="size as any"
  410. :expanded-row-keys="expandKeysList"
  411. @change="onTableChange"
  412. @expanded-rows-change="expandedTable"
  413. >
  414. <template #bodyCell="{ text, record, column }: {text: any, record: Record<string, any>, column: any}">
  415. <template v-if="column.handle === true">
  416. <span class="ant-table-drag-icon"><HolderOutlined /></span>
  417. {{ text }}
  418. </template>
  419. <template v-if="column.dataIndex === 'action'">
  420. <template v-if="!props.disabledView && !params.trash">
  421. <AButton
  422. type="link"
  423. size="small"
  424. @click="$emit('clickView', record[props.rowKey], record)"
  425. >
  426. {{ $gettext('View') }}
  427. </AButton>
  428. <ADivider
  429. v-if="!props.disabledModify"
  430. type="vertical"
  431. />
  432. </template>
  433. <template v-if="!props.disabledModify && !params.trash">
  434. <AButton
  435. type="link"
  436. size="small"
  437. @click="$emit('clickEdit', record[props.rowKey], record)"
  438. >
  439. {{ $gettext('Modify') }}
  440. </AButton>
  441. <ADivider
  442. v-if="!props.disableDelete"
  443. type="vertical"
  444. />
  445. </template>
  446. <slot
  447. name="actions"
  448. :record="record"
  449. />
  450. <template v-if="!props.disableDelete">
  451. <APopconfirm
  452. v-if="!params.trash"
  453. :cancel-text="$gettext('No')"
  454. :ok-text="$gettext('OK')"
  455. :title="$gettext('Are you sure you want to delete this item?')"
  456. @confirm="destroy(record[rowKey])"
  457. >
  458. <AButton
  459. type="link"
  460. size="small"
  461. >
  462. {{ $gettext('Delete') }}
  463. </AButton>
  464. </APopconfirm>
  465. <APopconfirm
  466. v-else
  467. :cancel-text="$gettext('No')"
  468. :ok-text="$gettext('OK')"
  469. :title="$gettext('Are you sure you want to recover this item?')"
  470. @confirm="recover(record[rowKey])"
  471. >
  472. <AButton
  473. type="link"
  474. size="small"
  475. >
  476. {{ $gettext('Recover') }}
  477. </AButton>
  478. </APopconfirm>
  479. <APopconfirm
  480. v-if="params.trash"
  481. :cancel-text="$gettext('No')"
  482. :ok-text="$gettext('OK')"
  483. :title="$gettext('Are you sure you want to delete this item permanently?')"
  484. @confirm="destroy(record[rowKey])"
  485. >
  486. <AButton
  487. type="link"
  488. size="small"
  489. >
  490. {{ $gettext('Delete Permanently') }}
  491. </AButton>
  492. </APopconfirm>
  493. </template>
  494. </template>
  495. </template>
  496. </ATable>
  497. <StdPagination
  498. :size="paginationSize"
  499. :loading="loading"
  500. :pagination="pagination"
  501. @change="changePage"
  502. @change-page-size="onTableChange"
  503. />
  504. </div>
  505. </template>
  506. <style lang="less">
  507. .ant-table-scroll {
  508. .ant-table-body {
  509. overflow-x: auto !important;
  510. }
  511. }
  512. </style>
  513. <style lang="less" scoped>
  514. .ant-form {
  515. margin: 10px 0 20px 0;
  516. }
  517. .ant-slider {
  518. min-width: 90px;
  519. }
  520. .action-btn {
  521. // min-height: 50px;
  522. height: 100%;
  523. display: flex;
  524. align-items: flex-start;
  525. }
  526. :deep(.ant-form-inline .ant-form-item) {
  527. margin-bottom: 10px;
  528. }
  529. </style>
  530. <style lang="less">
  531. .ant-table-drag-icon {
  532. float: left;
  533. margin-right: 16px;
  534. cursor: grab;
  535. }
  536. .sortable-ghost *, .sortable-chosen * {
  537. cursor: grabbing !important;
  538. }
  539. </style>