index.vue 14 KB

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