index.vue 11 KB

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