index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. <template>
  2. <div class="table-container">
  3. <div v-if="!hideTool" class="table-header mb8">
  4. <div>
  5. <slot name="command"></slot>
  6. </div>
  7. <div v-loading="state.exportLoading" class="table-footer-tool">
  8. <SvgIcon v-if="!config.hideRefresh" name="iconfont icon-shuaxin" :size="22" title="刷新" @click="() => onRefreshTable()" class="tool-icon" />
  9. <el-tooltip effect="light" :content="state.switchFixedContent" placement="bottom-start" :show-after="200" v-if="state.haveFixed" >
  10. <el-icon :style="{ color: state.fixedIconColor }" @click="switchFixed" class="tool-icon"><ele-Switch /></el-icon>
  11. </el-tooltip>
  12. <el-dropdown v-if="!config.hideExport" trigger="click">
  13. <SvgIcon name="iconfont icon-yunxiazai_o" :size="22" title="导出" class="tool-icon" />
  14. <template #dropdown>
  15. <el-dropdown-menu>
  16. <el-dropdown-item @click="onExportTable">导出本页数据</el-dropdown-item>
  17. <el-dropdown-item @click="onExportTableAll">导出全部数据</el-dropdown-item>
  18. </el-dropdown-menu>
  19. </template>
  20. </el-dropdown>
  21. <SvgIcon v-if="!config.hidePrint" name="iconfont icon-dayin" :size="19" title="打印" @click="onPrintTable" class="tool-icon" />
  22. <el-popover v-if="!config.hideSet" placement="bottom-end" trigger="click" transition="el-zoom-in-top" popper-class="table-tool-popper" :width="180" :persistent="false" @show="onSetTable">
  23. <template #reference>
  24. <SvgIcon name="iconfont icon-quanjushezhi_o" class="tool-icon" :size="22" title="设置" />
  25. </template>
  26. <template #default>
  27. <div class="tool-box">
  28. <el-checkbox v-model="state.checkListAll" :indeterminate="state.checkListIndeterminate" class="ml10 mr1" label="列显示" @change="onCheckAllChange" />
  29. <el-checkbox v-model="getConfig.isSerialNo" class="ml12 mr1" label="序号" />
  30. <el-checkbox v-if="getConfig.showSelection" v-model="getConfig.isSelection" class="ml12 mr1" label="多选" />
  31. <el-tooltip content="拖动进行排序" placement="top-start">
  32. <SvgIcon style="float: right; margin-right: 5px; margin-top: 2px" name="fa fa-question-circle-o" :size="17" class="ml11" color="#909399" />
  33. </el-tooltip>
  34. </div>
  35. <el-divider style="margin: 10px 0 10px -5px" />
  36. <el-scrollbar>
  37. <div ref="toolSetRef" class="tool-sortable">
  38. <div class="tool-sortable-item" v-for="v in columns" :key="v.prop" v-show="!v.hideCheck" :data-key="v.prop">
  39. <i class="fa fa-arrows-alt handle cursor-pointer"></i>
  40. <el-checkbox v-model="v.isCheck" size="default" class="ml12 mr8" :label="v.label" @change="onCheckChange" />
  41. </div>
  42. </div>
  43. </el-scrollbar>
  44. </template>
  45. </el-popover>
  46. </div>
  47. </div>
  48. <el-table
  49. ref="tableRef"
  50. :data="state.data"
  51. :border="setBorder"
  52. :stripe="setStripe"
  53. v-bind="$attrs"
  54. row-key="id"
  55. default-expand-all
  56. style="width: 100%"
  57. v-loading="state.loading"
  58. :default-sort="defaultSort"
  59. @selection-change="onSelectionChange"
  60. @sort-change="sortChange"
  61. >
  62. <el-table-column type="selection" :reserve-selection="true" :width="30" v-if="config.isSelection && config.showSelection" />
  63. <el-table-column type="index" :fixed="state.currentFixed && state.serialNoFixed" label="序号" align="center" :width="60" v-if="config.isSerialNo" />
  64. <el-table-column v-for="(item, index) in setHeader" :key="index" v-bind="item">
  65. <template #header v-if="!item.children && $slots[item.prop]">
  66. <slot :name="`${item.prop}header`" />
  67. </template>
  68. <!-- 自定义列插槽,插槽名为columns属性的prop -->
  69. <template #default="scope" v-if="!item.children && $slots[item.prop]">
  70. <formatter v-if="item.formatter" :fn="item.formatter(scope.row, scope.column, scope.cellValue, scope.index)"> </formatter>
  71. <slot v-else :name="item.prop" v-bind="scope"></slot>
  72. </template>
  73. <template v-else-if="!item.children" v-slot="scope">
  74. <formatter v-if="item.formatter" :fn="item.formatter(scope.row, scope.column, scope.cellValue, scope.index)"> </formatter>
  75. <template v-else-if="item.type === 'image'">
  76. <el-image
  77. :style="{ width: `${item.width}px`, height: `${item.height}px` }"
  78. :src="scope.row[item.prop]"
  79. :zoom-rate="1.2"
  80. :preview-src-list="[scope.row[item.prop]]"
  81. preview-teleported
  82. fit="cover"
  83. />
  84. </template>
  85. <template v-else>
  86. {{ getProperty(scope.row, item.prop) }}
  87. </template>
  88. </template>
  89. <el-table-column v-for="(childrenItem, childrenIndex) in item.children" :key="childrenIndex" v-bind="childrenItem">
  90. <!-- 自定义列插槽,插槽名为columns属性的prop -->
  91. <template #default="scope" v-if="$slots[childrenItem.prop]">
  92. <formatter v-if="childrenItem.formatter" :fn="childrenItem.formatter(scope.row, scope.column, scope.cellValue, scope.index)"> </formatter>
  93. <slot v-else :name="childrenItem.prop" v-bind="scope"></slot>
  94. </template>
  95. <template v-else v-slot="scope">
  96. <formatter v-if="childrenItem.formatter" :fn="childrenItem.formatter(scope.row, scope.column, scope.cellValue, scope.index)"> </formatter>
  97. <template v-else-if="childrenItem.type === 'image'">
  98. <el-image
  99. :style="{ width: `${childrenItem.width}px`, height: `${childrenItem.height}px` }"
  100. :src="scope.row[childrenItem.prop]"
  101. :zoom-rate="1.2"
  102. :preview-src-list="[scope.row[childrenItem.prop]]"
  103. preview-teleported
  104. fit="cover"
  105. />
  106. </template>
  107. <template v-else>
  108. {{ getProperty(scope.row, childrenItem.prop) }}
  109. </template>
  110. </template>
  111. </el-table-column>
  112. </el-table-column>
  113. <template #empty>
  114. <el-empty description="暂无数据" />
  115. </template>
  116. </el-table>
  117. <div v-if="!config.hidePagination && state.showPagination" class="table-footer mt15">
  118. <el-pagination
  119. v-model:current-page="state.page.page"
  120. v-model:page-size="state.page.pageSize"
  121. size="small"
  122. :pager-count="5"
  123. :page-sizes="config.pageSizes"
  124. :total="state.total"
  125. layout="total, sizes, prev, pager, next, jumper"
  126. background
  127. @size-change="onHandleSizeChange"
  128. @current-change="onHandleCurrentChange"
  129. >
  130. </el-pagination>
  131. </div>
  132. </div>
  133. </template>
  134. <script setup lang="ts" name="netxTable">
  135. import { reactive, computed, nextTick, ref, onMounted } from 'vue';
  136. import { ElMessage } from 'element-plus';
  137. import Sortable from 'sortablejs';
  138. import { storeToRefs } from 'pinia';
  139. import printJs from 'print-js';
  140. //import { EmptyObjectType } from "/@/types/global";
  141. import formatter from '/@/components/table/formatter.vue';
  142. import { useThemeConfig } from '/@/stores/themeConfig';
  143. import { exportExcel } from '/@/utils/exportExcel'; //TODO: 此包会引起浏览器控制台报 Module "stream" has been externalized for browser compatibility. Cannot access "stream.Readable" in client code. 警告,建议替换
  144. // 定义父组件传过来的值
  145. const props = defineProps({
  146. // 获取数据的方法,由父组件传递
  147. getData: {
  148. type: Function,
  149. required: true,
  150. },
  151. // 列属性,和elementUI的Table-column 属性相同,附加属性:isCheck-是否默认勾选展示,hideCheck-是否隐藏该列的可勾选和拖拽
  152. columns: {
  153. type: Array<any>,
  154. default: () => [],
  155. },
  156. // 配置项:isBorder-是否显示表格边框,isSerialNo-是否显示表格序号,showSelection-是否显示表格可多选,isSelection-是否默认选中表格多选,pageSize-每页条数,hideExport-是否隐藏导出按钮,exportFileName-导出表格的文件名,空值默认用应用名称作为文件名
  157. config: {
  158. type: Object,
  159. default: () => ({}),
  160. },
  161. // 筛选参数
  162. param: {
  163. type: Object,
  164. default: () => ({}),
  165. },
  166. // 默认排序方式,{prop:"排序字段",order:"ascending or descending"}
  167. defaultSort: {
  168. type: Object,
  169. default: () => ({}),
  170. },
  171. // 导出报表自定义数据转换方法,不传按字段值导出
  172. exportChangeData: {
  173. type: Function,
  174. },
  175. // 打印标题
  176. printName: {
  177. type: String,
  178. default: () => '',
  179. },
  180. });
  181. // 定义子组件向父组件传值/事件,pageChange-翻页事件,selectionChange-表格多选事件,可以在父组件处理批量删除/修改等功能,sortHeader-拖拽列顺序事件
  182. const emit = defineEmits(['pageChange', 'selectionChange', 'sortHeader']);
  183. // 定义变量内容
  184. const toolSetRef = ref();
  185. const tableRef = ref();
  186. const storesThemeConfig = useThemeConfig();
  187. const { themeConfig } = storeToRefs(storesThemeConfig);
  188. const state = reactive({
  189. data: [] as Array<EmptyObjectType>,
  190. loading: false,
  191. exportLoading: false,
  192. total: 0,
  193. page: {
  194. page: 1,
  195. pageSize: 50,
  196. field: '',
  197. order: '',
  198. },
  199. showPagination: true,
  200. selectlist: [] as EmptyObjectType[],
  201. checkListAll: true,
  202. checkListIndeterminate: false,
  203. oldColumns: [] as EmptyObjectType[],
  204. columns: [] as EmptyObjectType[],
  205. haveFixed: false,
  206. currentFixed: false,
  207. serialNoFixed: false,
  208. switchFixedContent: '取消固定列',
  209. fixedIconColor: themeConfig.value.primary,
  210. });
  211. const hideTool = computed(() => {
  212. return props.config.hideTool ?? false;
  213. });
  214. const getProperty = (obj: any, property: any) => {
  215. const keys = property.split('.');
  216. let value = obj;
  217. for (const key of keys) {
  218. value = value[key];
  219. }
  220. return value;
  221. };
  222. // 设置边框显示/隐藏
  223. const setBorder = computed(() => {
  224. return props.config.isBorder ? true : false;
  225. });
  226. // 设置斑马纹显示/隐藏
  227. const setStripe = computed(() => {
  228. return props.config.isStripe ? true : false;
  229. });
  230. // 获取父组件 配置项(必传)
  231. const getConfig = computed(() => {
  232. return props.config;
  233. });
  234. // 设置 tool header 数据
  235. const setHeader = computed(() => {
  236. return state.columns.filter((v) => v.isCheck);
  237. });
  238. // tool 列显示全选改变时
  239. const onCheckAllChange = <T,>(val: T) => {
  240. if (val) state.columns.forEach((v) => (v.isCheck = true));
  241. else state.columns.forEach((v) => (v.isCheck = false));
  242. state.checkListIndeterminate = false;
  243. };
  244. // tool 列显示当前项改变时
  245. const onCheckChange = () => {
  246. const headers = state.columns.filter((v) => v.isCheck).length;
  247. state.checkListAll = headers === state.columns.length;
  248. state.checkListIndeterminate = headers > 0 && headers < state.columns.length;
  249. };
  250. // 表格多选改变时
  251. const onSelectionChange = (val: EmptyObjectType[]) => {
  252. state.selectlist = val;
  253. emit('selectionChange', state.selectlist);
  254. };
  255. // 分页改变
  256. const onHandleSizeChange = (val: number) => {
  257. state.page.pageSize = val;
  258. onRefreshTable();
  259. emit('pageChange', state.page);
  260. };
  261. // 改变当前页
  262. const onHandleCurrentChange = (val: number) => {
  263. state.page.page = val;
  264. onRefreshTable();
  265. emit('pageChange', state.page);
  266. };
  267. // 列排序
  268. const sortChange = (column: any) => {
  269. state.page.field = column.prop;
  270. state.page.order = column.order;
  271. onRefreshTable();
  272. };
  273. // 重置列表
  274. const pageReset = () => {
  275. tableRef.value.clearSelection();
  276. state.page.page = 1;
  277. onRefreshTable();
  278. };
  279. // 导出当前页
  280. const onExportTable = () => {
  281. if (setHeader.value.length <= 0) return ElMessage.error('没有勾选要导出的列');
  282. exportData(state.data);
  283. };
  284. // 全部导出
  285. const onExportTableAll = async () => {
  286. if (setHeader.value.length <= 0) return ElMessage.error('没有勾选要导出的列');
  287. state.exportLoading = true;
  288. const param = Object.assign({}, props.param, { page: 1, pageSize: 9999999 });
  289. const res = await props.getData(param);
  290. state.exportLoading = false;
  291. const data = res.result?.items ?? [];
  292. exportData(data);
  293. };
  294. // 导出方法
  295. const exportData = (data: Array<EmptyObjectType>) => {
  296. if (data.length <= 0) return ElMessage.error('没有数据可以导出');
  297. state.exportLoading = true;
  298. let exportData = JSON.parse(JSON.stringify(data));
  299. if (props.exportChangeData) {
  300. exportData = props.exportChangeData(exportData);
  301. }
  302. exportExcel(
  303. exportData,
  304. `${props.config.exportFileName ? props.config.exportFileName : themeConfig.value.globalTitle}_${new Date().toLocaleString()}`,
  305. setHeader.value.filter((item) => {
  306. return item.type != 'action';
  307. }),
  308. '导出数据'
  309. );
  310. state.exportLoading = false;
  311. };
  312. // 打印
  313. const onPrintTable = () => {
  314. // https://printjs.crabbly.com/#documentation
  315. // 自定义打印
  316. let tableTh = '';
  317. let tableTrTd = '';
  318. let tableTd: any = {};
  319. // 表头
  320. setHeader.value.forEach((v: any) => {
  321. if (v.prop === 'action') {
  322. return;
  323. }
  324. tableTh += `<th class="table-th">${v.label}</th>`;
  325. });
  326. // 表格内容
  327. state.data.forEach((val: any, key: any) => {
  328. if (!tableTd[key]) tableTd[key] = [];
  329. setHeader.value.forEach((v: any) => {
  330. if (v.prop === 'action') {
  331. return;
  332. }
  333. if (v.type === 'text') {
  334. tableTd[key].push(`<td class="table-th table-center">${val[v.prop]}</td>`);
  335. } else if (v.type === 'image') {
  336. tableTd[key].push(`<td class="table-th table-center"><img src="${val[v.prop]}" style="width:${v.width}px;height:${v.height}px;"/></td>`);
  337. } else {
  338. tableTd[key].push(`<td class="table-th table-center">${val[v.prop]}</td>`);
  339. }
  340. });
  341. tableTrTd += `<tr>${tableTd[key].join('')}</tr>`;
  342. });
  343. // 打印
  344. printJs({
  345. printable: `<div style=display:flex;flex-direction:column;text-align:center><h3>${props.printName}</h3></div><table border=1 cellspacing=0><tr>${tableTh}${tableTrTd}</table>`,
  346. type: 'raw-html',
  347. css: ['//at.alicdn.com/t/c/font_2298093_rnp72ifj3ba.css', '//unpkg.com/element-plus/dist/index.css'],
  348. style: `@media print{.mb15{margin-bottom:15px;}.el-button--small i.iconfont{font-size: 12px !important;margin-right: 5px;}}; .table-th{word-break: break-all;white-space: pre-wrap;}.table-center{text-align: center;}`,
  349. });
  350. };
  351. // 拖拽设置
  352. const onSetTable = () => {
  353. nextTick(() => {
  354. const sortable = Sortable.create(toolSetRef.value, {
  355. handle: '.handle',
  356. dataIdAttr: 'data-key',
  357. animation: 150,
  358. onEnd: () => {
  359. const headerList: EmptyObjectType[] = [];
  360. sortable.toArray().forEach((val: any) => {
  361. state.columns.forEach((v) => {
  362. if (v.prop === val) headerList.push({ ...v });
  363. });
  364. });
  365. emit('sortHeader', headerList);
  366. },
  367. });
  368. });
  369. };
  370. const onRefreshTable = async () => {
  371. state.loading = true;
  372. let param = Object.assign({}, props.param, { ...state.page });
  373. Object.keys(param).forEach((key) => param[key] === undefined && delete param[key]);
  374. const res = await props.getData(param);
  375. state.loading = false;
  376. if (res && res.result && res.result.items) {
  377. state.showPagination = true;
  378. state.data = res.result?.items ?? [];
  379. state.total = res.result?.total ?? 0;
  380. } else {
  381. state.showPagination = false;
  382. state.data = res && res.result ? res.result : [];
  383. }
  384. };
  385. const toggleSelection = (row: any, statu?: boolean) => {
  386. tableRef.value!.toggleRowSelection(row, statu);
  387. };
  388. const getTableData = () => {
  389. return state.data;
  390. };
  391. const setTableData = (data: Array<EmptyObjectType>, add: boolean = false) => {
  392. if (add) {
  393. // 追加, 去重
  394. var repeat = false;
  395. for (let newItem of data) {
  396. repeat = false;
  397. for (let item of state.data) {
  398. if (newItem.id === item.id) {
  399. repeat = true;
  400. break;
  401. }
  402. }
  403. if (!repeat) {
  404. state.data.push(newItem);
  405. }
  406. }
  407. } else {
  408. state.data = data;
  409. }
  410. };
  411. const clearFixed = () => {
  412. for (let item of state.columns) delete item['fixed'];
  413. };
  414. const switchFixed = () => {
  415. state.currentFixed = !state.currentFixed;
  416. state.switchFixedContent = state.currentFixed ? '取消固定列' : '启用固定列';
  417. if (state.currentFixed) {
  418. state.fixedIconColor = themeConfig.value.primary;
  419. state.columns = JSON.parse(JSON.stringify(state.oldColumns));
  420. } else {
  421. state.fixedIconColor = '';
  422. clearFixed();
  423. }
  424. };
  425. const refreshColumns = () => {
  426. state.oldColumns = JSON.parse(JSON.stringify(props.columns));
  427. state.columns = props.columns;
  428. for (let item of state.columns) {
  429. if (item.fixed !== undefined) {
  430. state.haveFixed = true;
  431. state.currentFixed = true;
  432. if (item.fixed == 'left') {
  433. state.serialNoFixed = true;
  434. break;
  435. }
  436. }
  437. }
  438. };
  439. onMounted(() => {
  440. if (props.defaultSort) {
  441. state.page.field = props.defaultSort.prop;
  442. state.page.order = props.defaultSort.order;
  443. }
  444. state.page.pageSize = props.config.pageSize ?? 10;
  445. refreshColumns();
  446. onRefreshTable();
  447. });
  448. const handleList = onRefreshTable;
  449. // 暴露变量
  450. defineExpose({
  451. pageReset,
  452. handleList,
  453. toggleSelection,
  454. getTableData,
  455. setTableData,
  456. refreshColumns,
  457. });
  458. </script>
  459. <style scoped lang="scss">
  460. .table-container {
  461. display: flex !important;
  462. flex-direction: column;
  463. height: 100%;
  464. .el-table {
  465. flex: 1;
  466. }
  467. .table-footer {
  468. display: flex;
  469. justify-content: flex-end;
  470. }
  471. .table-header {
  472. display: flex;
  473. .table-footer-tool {
  474. flex: 1;
  475. display: flex;
  476. align-items: center;
  477. justify-content: flex-end;
  478. i {
  479. margin-right: 5px !important;
  480. cursor: pointer;
  481. color: var(--el-text-color-regular);
  482. &:last-of-type {
  483. margin-right: 0;
  484. }
  485. }
  486. .el-dropdown {
  487. i {
  488. margin-right: 10px;
  489. color: var(--el-text-color-regular);
  490. }
  491. }
  492. .tool-icon {
  493. border: 1px solid #a7a7a7;
  494. border-radius: 20%;
  495. padding: 1px;
  496. display: inline-flex;
  497. align-items: center;
  498. justify-content: center;
  499. }
  500. .el-icon.tool-icon {
  501. font-size: 25px;
  502. border: 1px solid #a7a7a7;
  503. padding: 4px;
  504. }
  505. }
  506. }
  507. }
  508. </style>