index.vue 11 KB

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