index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <template>
  2. <el-table
  3. ref="table"
  4. v-bind="tableProps"
  5. v-el-table-infinite-scroll="scrollOver"
  6. :data="dealedTableData"
  7. @cell-click="(...args:any[]) => { emit('cellClick', ...args) }"
  8. @cell-contextmenu="(...args:any[]) => { emit('cellContextmenu', ...args) }"
  9. @cell-dblclick="(...args:any[]) => { emit('cellDblclick', ...args) }"
  10. @cell-mouse-enter="(...args:any[]) => { emit('cellMouseEnter', ...args) }"
  11. @cell-mouse-leave="(...args:any[]) => { emit('cellMouseLeave', ...args) }"
  12. @current-change="(...args:any[]) => { emit('currentChange', ...args) }"
  13. @expand-change="(...args:any[]) => { emit('expandChange', ...args) }"
  14. @filter-change="(...args:any[]) => { emit('filterChange', ...args) }"
  15. @header-click="(...args:any[]) => { emit('headerClick', ...args) }"
  16. @header-contextmenu="(...args:any[]) => { emit('headerContextmenu', ...args) }"
  17. @header-dragend="(...args:any[]) => { emit('headerDragend', ...args) }"
  18. @row-click="(...args:any[]) => { emit('rowClick', ...args) }"
  19. @row-contextmenu="(...args:any[]) => { emit('rowContextmenu', ...args) }"
  20. @row-dblclick="(...args:any[]) => { emit('rowDblclick', ...args) }"
  21. @select="(...args:any[]) => { emit('select', ...args) }"
  22. @select-all="(...args:any[]) => { emit('selectAll', ...args) }"
  23. @selection-change="(...args:any[]) => { emit('selectionChange', ...args) }"
  24. @sort-change="(...args:any[]) => { emit('sortChange', ...args) }"
  25. >
  26. <el-table-column
  27. v-if="sequence || customSequence"
  28. :prop="customSequence ? 'index' : undefined"
  29. :type="customSequence ? undefined : 'index'"
  30. :fixed="hasFixedColumn"
  31. :width="50"
  32. align="center"
  33. >
  34. <template #header>
  35. <TableHeaderCell label="序号" />
  36. </template>
  37. </el-table-column>
  38. <el-table-column
  39. v-for="column in tableColumns"
  40. :key="column.columnName"
  41. v-bind="computedColumnProps(column)"
  42. >
  43. <template #header>
  44. <TableHeaderCell
  45. v-model:filter-values="filterValueMap[column.columnName]"
  46. v-model:sort-rule="sortRuleMap[column.columnName]"
  47. :label="labelFormatter(column.columnLabel)"
  48. :desc="column.columnDescribe"
  49. :show-desc="column.showDesc"
  50. :filter-options="filterOptionMap[column.columnName]"
  51. :sortable="!!column.needSort"
  52. filter-style="arrow"
  53. @update:sort-rule="
  54. sortRule => {
  55. sortRuleChangeHandler(column.columnName, sortRule)
  56. }
  57. "
  58. />
  59. </template>
  60. <template v-if="column.customRender" #default="scope">
  61. <component :is="column.customRender(scope)" />
  62. </template>
  63. </el-table-column>
  64. </el-table>
  65. </template>
  66. <script setup lang="ts">
  67. import TableHeaderCell from '@/components/TableHeaderCell/index.vue'
  68. import type { CSSProperties, VNode } from 'vue'
  69. import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
  70. import { CommonData, CommonTableColumn } from '~/common'
  71. import { Options, useTableFilterAndSort } from '@/hooks/useTableFilterAndSort'
  72. import { ElTable } from 'element-plus'
  73. import { useTableSettingsStore } from '@/store/tableSettings'
  74. type SummaryMethod<T> = (data: {
  75. columns: TableColumnCtx<T>[]
  76. data: T[]
  77. }) => string[]
  78. type ColumnCls<T> = string | ((data: { row: T; rowIndex: number }) => string)
  79. type ColumnStyle<T> =
  80. | CSSProperties
  81. | ((data: { row: T; rowIndex: number }) => CSSProperties)
  82. type CellCls<T> =
  83. | string
  84. | ((data: {
  85. row: T
  86. rowIndex: number
  87. column: TableColumnCtx<T>
  88. columnIndex: number
  89. }) => string)
  90. type CellStyle<T> =
  91. | CSSProperties
  92. | ((data: {
  93. row: T
  94. rowIndex: number
  95. column: TableColumnCtx<T>
  96. columnIndex: number
  97. }) => CSSProperties)
  98. type Sort = {
  99. prop: string
  100. order: 'ascending' | 'descending'
  101. init?: any
  102. silent?: any
  103. }
  104. type TreeNode = {
  105. expanded?: boolean
  106. loading?: boolean
  107. noLazyChildren?: boolean
  108. indent?: number
  109. level?: number
  110. display?: boolean
  111. }
  112. type Layout = 'fixed' | 'auto'
  113. type TableColumnProps<T> = {
  114. type?: string
  115. index?: number | ((index: number) => number)
  116. columnKey?: string
  117. width?: string | number
  118. minWidth?: string | number
  119. fixed?: boolean | string
  120. renderHeader?: (data: { column: TableColumnCtx<T>; $index: number }) => VNode
  121. resizable?: boolean
  122. formatter?: (
  123. row: T,
  124. column: TableColumnCtx<T>,
  125. cellValue: any,
  126. index: number
  127. ) => VNode | string
  128. showOverflowTooltip?: boolean
  129. align?: string
  130. headerAlign?: string
  131. className?: string
  132. labelClassName?: string
  133. selectable?: (row: T, index: number) => boolean
  134. reserveSelection?: boolean
  135. }
  136. const props = withDefaults(
  137. defineProps<{
  138. data: CommonData[]
  139. size?: string
  140. width?: string | number
  141. height?: string | number
  142. maxHeight?: string | number
  143. fit?: boolean
  144. stripe?: boolean
  145. border?: boolean
  146. rowKey?: string | ((row: CommonData) => string)
  147. showHeader?: boolean
  148. showSummary?: boolean
  149. sumText?: string
  150. summaryMethod?: SummaryMethod<CommonData>
  151. rowClassName?: ColumnCls<CommonData>
  152. rowStyle?: ColumnStyle<CommonData>
  153. cellClassName?: CellCls<CommonData>
  154. cellStyle?: CellStyle<CommonData>
  155. headerRowClassName?: ColumnCls<CommonData>
  156. headerRowStyle?: ColumnStyle<CommonData>
  157. headerCellClassName?: CellCls<CommonData>
  158. headerCellStyle?: CellStyle<CommonData>
  159. highlightCurrentRow?: boolean
  160. currentRowKey?: string | number
  161. emptyText?: string
  162. expandRowKeys?: any[]
  163. defaultExpandAll?: boolean
  164. defaultSort?: Sort
  165. tooltipEffect?: string
  166. spanMethod?: (data: {
  167. row: CommonData
  168. rowIndex: number
  169. column: TableColumnCtx<CommonData>
  170. columnIndex: number
  171. }) =>
  172. | number[]
  173. | {
  174. rowspan: number
  175. colspan: number
  176. }
  177. | undefined
  178. selectOnIndeterminate?: boolean
  179. indent?: number
  180. treeProps?: {
  181. hasChildren?: string
  182. children?: string
  183. }
  184. lazy?: boolean
  185. load?: (
  186. row: CommonData,
  187. treeNode: TreeNode,
  188. resolve: (data: CommonData[]) => void
  189. ) => void
  190. className?: string
  191. style?: CSSProperties
  192. tableLayout?: Layout
  193. flexible?: boolean
  194. scrollbarAlwaysOn?: boolean
  195. columnProps?: TableColumnProps<CommonData>
  196. columns: (CommonTableColumn & TableColumnProps<CommonData>)[]
  197. sequence?: boolean
  198. customSequence?: boolean
  199. filterSortOptions?: Options
  200. cacheKeys?: string[]
  201. labelFormatter?: (label: string) => string
  202. }>(),
  203. {
  204. size: 'default',
  205. height: '100%',
  206. maxHeight: '100%',
  207. stripe: true,
  208. border: true,
  209. fit: true,
  210. showHeader: true,
  211. labelFormatter: (label: string) => label,
  212. }
  213. )
  214. const defaultSummaryMethod: SummaryMethod<CommonData> = ({ columns, data }) => {
  215. const sums: string[] = []
  216. columns.forEach((column, index) => {
  217. const countColumn = tableColumns.value.find(
  218. col => column.property === col.columnName && col.needCount
  219. )
  220. if (countColumn) {
  221. const sumNumber = data.reduce((prev: number, curr: CommonData) => {
  222. const cellData = curr[column.property]
  223. if (countColumn.countMode === 'all') {
  224. return prev + 1
  225. }
  226. if (countColumn.countMode === 'notNull') {
  227. return cellData ? prev + 1 : prev
  228. }
  229. const value = Number(cellData)
  230. if (!Number.isNaN(value)) {
  231. prev += value
  232. }
  233. return prev
  234. }, 0)
  235. sums[index] = sumNumber.toString()
  236. }
  237. })
  238. sums[0] = '合计:' + (sums[0] ?? '')
  239. return sums
  240. }
  241. const tableProps = computed(() => {
  242. const rawProps = toRaw(props)
  243. const result: { [x: string]: any } = {}
  244. Object.entries(rawProps).forEach(([key, value]) => {
  245. if (
  246. ![
  247. 'columnProps',
  248. 'columns',
  249. 'sequence',
  250. 'customSequence',
  251. 'filterSortOptions',
  252. 'cacheKeys',
  253. 'labelFormatter',
  254. ].includes(key) &&
  255. (value ?? '') !== ''
  256. ) {
  257. result[key] = value
  258. }
  259. if (props.columns.some(column => column.needCount)) {
  260. result.showSummary = true
  261. }
  262. if (!result.summaryMethod) {
  263. result.summaryMethod = defaultSummaryMethod
  264. }
  265. })
  266. return result
  267. })
  268. const computedColumnProps = computed(() => {
  269. const defaultColumnProps: TableColumnProps<CommonData> = {
  270. align: 'center',
  271. }
  272. return (column: CommonTableColumn & TableColumnProps<CommonData>) => ({
  273. ...defaultColumnProps,
  274. ...props.columnProps,
  275. ...column,
  276. })
  277. })
  278. const tableColumns = ref<CommonTableColumn[]>([])
  279. const tableData = ref<CommonData[]>([])
  280. watchEffect(() => {
  281. tableColumns.value = props.columns.reduce(
  282. (prevColumns: CommonTableColumn[], column) => {
  283. if (!column.hidden) {
  284. prevColumns.push({
  285. label: column.columnLabel,
  286. prop: column.columnName,
  287. ...column,
  288. })
  289. }
  290. return prevColumns
  291. },
  292. []
  293. )
  294. tableData.value = props.data
  295. })
  296. const hasFixedColumn = computed(() =>
  297. tableColumns.value.some(column => column.fixed)
  298. )
  299. const {
  300. filterOptionMap,
  301. filterValueMap,
  302. sortRuleMap,
  303. dealedTableData,
  304. sortChangeHandler,
  305. } = useTableFilterAndSort(tableColumns, tableData, props.filterSortOptions)
  306. const { saveTableFilterValues } = useTableSettingsStore()
  307. watch(
  308. sortRuleMap,
  309. map => {
  310. emit('sortRuleChange', map)
  311. },
  312. { deep: true }
  313. )
  314. const sortRuleChangeHandler = (columnName: string, sortRule: string) => {
  315. sortRuleMap[columnName] = sortRule
  316. sortChangeHandler(columnName, sortRule)
  317. }
  318. if (props.cacheKeys?.length) {
  319. watch(filterValueMap, map => {
  320. const values: { [x: string]: string[] } = {}
  321. props.cacheKeys!.forEach(columnName => {
  322. values[columnName] = map[columnName]
  323. })
  324. saveTableFilterValues(values)
  325. })
  326. }
  327. const emit = defineEmits([
  328. 'select',
  329. 'selectAll',
  330. 'selectionChange',
  331. 'cellMouseEnter',
  332. 'cellMouseLeave',
  333. 'cellClick',
  334. 'cellDblclick',
  335. 'cellContextmenu',
  336. 'rowClick',
  337. 'rowContextmenu',
  338. 'rowDblclick',
  339. 'headerClick',
  340. 'headerContextmenu',
  341. 'sortChange',
  342. 'filterChange',
  343. 'currentChange',
  344. 'headerDragend',
  345. 'expandChange',
  346. 'sortRuleChange',
  347. 'scrollOver',
  348. ])
  349. const scrollOver = () => {
  350. emit('scrollOver')
  351. }
  352. const table = ref<InstanceType<typeof ElTable> | null>(null)
  353. defineExpose({
  354. table,
  355. })
  356. </script>
  357. <style scoped lang="scss">
  358. .el-table :deep {
  359. .el-table__cell {
  360. padding: 0;
  361. height: 40px;
  362. &.cell-filter {
  363. position: relative;
  364. &::before {
  365. content: '';
  366. position: absolute;
  367. width: 100%;
  368. height: 100%;
  369. top: 0;
  370. left: 0;
  371. z-index: 1;
  372. }
  373. &.cell-filter-yellow::before {
  374. opacity: 0.47;
  375. background-color: #eef3d6;
  376. }
  377. &.cell-filter-green::before {
  378. opacity: 0.73;
  379. background-color: #eef3d6;
  380. }
  381. &.cell-filter-cyan::before {
  382. opacity: 0.73;
  383. background-color: #d6e6f3;
  384. }
  385. .cell {
  386. position: relative;
  387. z-index: 2;
  388. }
  389. }
  390. .cell {
  391. padding: 0;
  392. font-size: 14px;
  393. color: #101116;
  394. font-family: DIN, Microsoft YaHei;
  395. &:not(.el-tooltip) {
  396. white-space: pre-line;
  397. }
  398. }
  399. }
  400. .el-table__header .el-table__cell {
  401. background: #ffffff;
  402. font-weight: bold;
  403. .cell {
  404. height: 100%;
  405. }
  406. }
  407. .el-table__body {
  408. .el-table__row--striped .el-table__cell {
  409. background-color: #f0f3f7;
  410. }
  411. .el-table__cell.cell-click .cell {
  412. color: #2d67e3;
  413. cursor: pointer;
  414. }
  415. }
  416. .el-scrollbar__bar {
  417. &.is-horizontal {
  418. height: 15px;
  419. }
  420. &.is-vertical {
  421. width: 15px;
  422. }
  423. }
  424. }
  425. </style>