PageContent.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953
  1. <template>
  2. <div
  3. class="rounded bg-[var(--el-bg-color)] border border-[var(--el-border-color)] p-5 h-full md:flex flex-1 flex-col md:overflow-auto"
  4. >
  5. <!-- 表格工具栏 -->
  6. <div class="flex flex-col md:flex-row justify-between gap-y-2.5 mb-2.5">
  7. <!-- 左侧工具栏 -->
  8. <div class="toolbar-left flex gap-y-2.5 gap-x-2 md:gap-x-3 flex-wrap">
  9. <template v-for="(btn, index) in toolbarLeftBtn" :key="index">
  10. <el-button
  11. v-hasPerm="btn.perm ?? '*:*:*'"
  12. v-bind="btn.attrs"
  13. :disabled="btn.name === 'delete' && removeIds.length === 0"
  14. @click="handleToolbar(btn.name)"
  15. >
  16. {{ btn.text }}
  17. </el-button>
  18. </template>
  19. </div>
  20. <!-- 右侧工具栏 -->
  21. <div class="toolbar-right flex gap-y-2.5 gap-x-2 md:gap-x-3 flex-wrap">
  22. <template v-for="(btn, index) in toolbarRightBtn" :key="index">
  23. <el-popover v-if="btn.name === 'filter'" placement="bottom" trigger="click">
  24. <template #reference>
  25. <el-button v-bind="btn.attrs"></el-button>
  26. </template>
  27. <el-scrollbar max-height="350px">
  28. <template v-for="col in cols" :key="col.prop">
  29. <el-checkbox v-if="col.prop" v-model="col.show" :label="col.label" />
  30. </template>
  31. </el-scrollbar>
  32. </el-popover>
  33. <el-button
  34. v-else
  35. v-hasPerm="btn.perm ?? '*:*:*'"
  36. v-bind="btn.attrs"
  37. @click="handleToolbar(btn.name)"
  38. ></el-button>
  39. </template>
  40. </div>
  41. </div>
  42. <!-- 列表 -->
  43. <el-table
  44. ref="tableRef"
  45. v-loading="loading"
  46. v-bind="contentConfig.table"
  47. :data="pageData"
  48. :row-key="pk"
  49. class="flex-1"
  50. @selection-change="handleSelectionChange"
  51. @filter-change="handleFilterChange"
  52. >
  53. <template v-for="col in cols" :key="col.prop">
  54. <el-table-column v-if="col.show" v-bind="col">
  55. <template #default="scope">
  56. <!-- 显示图片 -->
  57. <template v-if="col.templet === 'image'">
  58. <template v-if="col.prop">
  59. <template v-if="Array.isArray(scope.row[col.prop])">
  60. <template v-for="(item, index) in scope.row[col.prop]" :key="item">
  61. <el-image
  62. :src="getFullImageUrl(item)"
  63. :preview-src-list="scope.row[col.prop].map((url: string) => getFullImageUrl(url))"
  64. :initial-index="index"
  65. :preview-teleported="true"
  66. :style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px`"
  67. />
  68. </template>
  69. </template>
  70. <template v-else-if="typeof scope.row[col.prop] === 'string' && scope.row[col.prop].includes(',')">
  71. <!-- 处理逗号分隔的图片字符串 -->
  72. <template v-for="(item, index) in scope.row[col.prop].split(',')" :key="index">
  73. <el-image
  74. :src="getFullImageUrl(item.trim())"
  75. :preview-src-list="scope.row[col.prop].split(',').map((url: string) => getFullImageUrl(url.trim()))"
  76. :initial-index="index"
  77. :preview-teleported="true"
  78. :style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px`"
  79. />
  80. </template>
  81. </template>
  82. <template v-else>
  83. <el-image
  84. :src="getFullImageUrl(scope.row[col.prop])"
  85. :preview-src-list="[getFullImageUrl(scope.row[col.prop])]"
  86. :preview-teleported="true"
  87. :style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px`"
  88. />
  89. </template>
  90. </template>
  91. </template>
  92. <!-- 根据行的selectList属性返回对应列表值 -->
  93. <template v-else-if="col.templet === 'list'">
  94. <template v-if="col.prop">
  95. {{ (col.selectList ?? {})[scope.row[col.prop]] }}
  96. </template>
  97. </template>
  98. <!-- 格式化显示链接 -->
  99. <template v-else-if="col.templet === 'url'">
  100. <template v-if="col.prop">
  101. <el-link type="primary" :href="scope.row[col.prop]" target="_blank">
  102. {{ scope.row[col.prop] }}
  103. </el-link>
  104. </template>
  105. </template>
  106. <!-- 生成开关组件 -->
  107. <template v-else-if="col.templet === 'switch'">
  108. <template v-if="col.prop">
  109. <!-- pageData.length>0: 解决el-switch组件会在表格初始化的时候触发一次change事件 -->
  110. <el-switch
  111. v-model="scope.row[col.prop]"
  112. :active-value="col.activeValue ?? 1"
  113. :inactive-value="col.inactiveValue ?? 0"
  114. :inline-prompt="true"
  115. :active-text="col.activeText ?? ''"
  116. :inactive-text="col.inactiveText ?? ''"
  117. :validate-event="false"
  118. :disabled="!hasButtonPerm(col.prop)"
  119. @change="
  120. pageData.length > 0 && handleModify(col.prop, scope.row[col.prop], scope.row)
  121. "
  122. />
  123. </template>
  124. </template>
  125. <!-- 生成输入框组件 -->
  126. <template v-else-if="col.templet === 'input'">
  127. <template v-if="col.prop">
  128. <el-input
  129. v-model="scope.row[col.prop]"
  130. :type="col.inputType ?? 'text'"
  131. :disabled="!hasButtonPerm(col.prop)"
  132. @blur="handleModify(col.prop, scope.row[col.prop], scope.row)"
  133. />
  134. </template>
  135. </template>
  136. <!-- 格式化为价格 -->
  137. <template v-else-if="col.templet === 'price'">
  138. <template v-if="col.prop">
  139. {{ `${col.priceFormat ?? "¥"}${scope.row[col.prop]}` }}
  140. </template>
  141. </template>
  142. <!-- 格式化为百分比 -->
  143. <template v-else-if="col.templet === 'percent'">
  144. <template v-if="col.prop">{{ scope.row[col.prop] }}%</template>
  145. </template>
  146. <!-- 显示图标 -->
  147. <template v-else-if="col.templet === 'icon'">
  148. <template v-if="col.prop">
  149. <template v-if="scope.row[col.prop].startsWith('el-icon-')">
  150. <el-icon>
  151. <component :is="scope.row[col.prop].replace('el-icon-', '')" />
  152. </el-icon>
  153. </template>
  154. <template v-else>
  155. <div class="i-svg:{{ scope.row[col.prop] }}" />
  156. </template>
  157. </template>
  158. </template>
  159. <!-- 格式化时间 -->
  160. <template v-else-if="col.templet === 'date'">
  161. <template v-if="col.prop">
  162. {{
  163. scope.row[col.prop]
  164. ? useDateFormat(scope.row[col.prop], col.dateFormat ?? "YYYY-MM-DD HH:mm:ss")
  165. .value
  166. : ""
  167. }}
  168. </template>
  169. </template>
  170. <!-- 列操作栏 -->
  171. <template v-else-if="col.templet === 'tool'">
  172. <template v-for="(btn, index) in tableToolbarBtn" :key="index">
  173. <el-button
  174. v-if="btn.render === undefined || btn.render(scope.row)"
  175. v-hasPerm="btn.perm ?? '*:*:*'"
  176. v-bind="typeof btn.attrs === 'function' ? btn.attrs(scope.row) : btn.attrs"
  177. @click="
  178. handleOperate({
  179. name: btn.name,
  180. row: scope.row,
  181. column: scope.column,
  182. $index: scope.$index,
  183. })
  184. "
  185. >
  186. {{ typeof btn.text === 'function' ? btn.text(scope.row) : btn.text }}
  187. </el-button>
  188. </template>
  189. </template>
  190. <!-- 自定义 -->
  191. <template v-else-if="col.templet === 'custom'">
  192. <slot :name="col.slotName ?? col.prop" :prop="col.prop" v-bind="scope" />
  193. </template>
  194. </template>
  195. </el-table-column>
  196. </template>
  197. </el-table>
  198. <!-- 分页 -->
  199. <div v-if="showPagination" class="mt-4">
  200. <el-scrollbar :class="['h-8!', { 'flex-x-end': contentConfig?.pagePosition === 'right' }]">
  201. <el-pagination
  202. v-bind="pagination"
  203. @size-change="handleSizeChange"
  204. @current-change="handleCurrentChange"
  205. />
  206. </el-scrollbar>
  207. </div>
  208. <!-- 导出弹窗 -->
  209. <el-dialog
  210. v-model="exportsModalVisible"
  211. :align-center="true"
  212. title="导出数据"
  213. width="600px"
  214. style="padding-right: 0"
  215. @close="handleCloseExportsModal"
  216. >
  217. <!-- 滚动 -->
  218. <el-scrollbar max-height="60vh">
  219. <!-- 表单 -->
  220. <el-form
  221. ref="exportsFormRef"
  222. style="padding-right: var(--el-dialog-padding-primary)"
  223. :model="exportsFormData"
  224. :rules="exportsFormRules"
  225. >
  226. <el-form-item label="文件名" prop="filename">
  227. <el-input v-model="exportsFormData.filename" clearable />
  228. </el-form-item>
  229. <el-form-item label="工作表名" prop="sheetname">
  230. <el-input v-model="exportsFormData.sheetname" clearable />
  231. </el-form-item>
  232. <el-form-item label="数据源" prop="origin">
  233. <el-select v-model="exportsFormData.origin">
  234. <el-option label="当前数据 (当前页的数据)" :value="ExportsOriginEnum.CURRENT" />
  235. <el-option
  236. label="选中数据 (所有选中的数据)"
  237. :value="ExportsOriginEnum.SELECTED"
  238. :disabled="selectionData.length <= 0"
  239. />
  240. <el-option
  241. label="全量数据 (所有分页的数据)"
  242. :value="ExportsOriginEnum.REMOTE"
  243. :disabled="contentConfig.exportsAction === undefined"
  244. />
  245. </el-select>
  246. </el-form-item>
  247. <el-form-item label="字段" prop="fields">
  248. <el-checkbox-group v-model="exportsFormData.fields">
  249. <template v-for="col in cols" :key="col.prop">
  250. <el-checkbox v-if="col.prop" :value="col.prop" :label="col.label" />
  251. </template>
  252. </el-checkbox-group>
  253. </el-form-item>
  254. </el-form>
  255. </el-scrollbar>
  256. <!-- 弹窗底部操作按钮 -->
  257. <template #footer>
  258. <div style="padding-right: var(--el-dialog-padding-primary)">
  259. <el-button type="primary" @click="handleExportsSubmit">确 定</el-button>
  260. <el-button @click="handleCloseExportsModal">取 消</el-button>
  261. </div>
  262. </template>
  263. </el-dialog>
  264. <!-- 导入弹窗 -->
  265. <el-dialog
  266. v-model="importModalVisible"
  267. :align-center="true"
  268. title="导入数据"
  269. width="600px"
  270. style="padding-right: 0"
  271. @close="handleCloseImportModal"
  272. >
  273. <!-- 滚动 -->
  274. <el-scrollbar max-height="60vh">
  275. <!-- 表单 -->
  276. <el-form
  277. ref="importFormRef"
  278. style="padding-right: var(--el-dialog-padding-primary)"
  279. :model="importFormData"
  280. :rules="importFormRules"
  281. >
  282. <el-form-item label="文件名" prop="files">
  283. <el-upload
  284. ref="uploadRef"
  285. v-model:file-list="importFormData.files"
  286. class="w-full"
  287. accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
  288. :drag="true"
  289. :limit="1"
  290. :auto-upload="false"
  291. :on-exceed="handleFileExceed"
  292. >
  293. <el-icon class="el-icon--upload"><upload-filled /></el-icon>
  294. <div class="el-upload__text">
  295. <span>将文件拖到此处,或</span>
  296. <em>点击上传</em>
  297. </div>
  298. <template #tip>
  299. <div class="el-upload__tip">
  300. *.xlsx / *.xls
  301. <el-link
  302. v-if="contentConfig.importTemplate"
  303. type="primary"
  304. icon="download"
  305. underline="never"
  306. @click="handleDownloadTemplate"
  307. >
  308. 下载模板
  309. </el-link>
  310. </div>
  311. </template>
  312. </el-upload>
  313. </el-form-item>
  314. </el-form>
  315. </el-scrollbar>
  316. <!-- 弹窗底部操作按钮 -->
  317. <template #footer>
  318. <div style="padding-right: var(--el-dialog-padding-primary)">
  319. <el-button
  320. type="primary"
  321. :disabled="importFormData.files.length === 0"
  322. @click="handleImportSubmit"
  323. >
  324. 确 定
  325. </el-button>
  326. <el-button @click="handleCloseImportModal">取 消</el-button>
  327. </div>
  328. </template>
  329. </el-dialog>
  330. </div>
  331. </template>
  332. <script setup lang="ts">
  333. import { hasPerm } from "@/utils/auth";
  334. import { useDateFormat, useThrottleFn } from "@vueuse/core";
  335. import { getFullImageUrl } from "@/utils";
  336. import {
  337. genFileId,
  338. type FormInstance,
  339. type FormRules,
  340. type UploadInstance,
  341. type UploadRawFile,
  342. type UploadUserFile,
  343. type TableInstance,
  344. } from "element-plus";
  345. import ExcelJS from "exceljs";
  346. import { reactive, ref, computed } from "vue";
  347. import type { IContentConfig, IObject, IOperateData } from "./types";
  348. import type { IToolsButton } from "./types";
  349. // 定义接收的属性
  350. const props = defineProps<{ contentConfig: IContentConfig }>();
  351. // 定义自定义事件
  352. const emit = defineEmits<{
  353. addClick: [];
  354. exportClick: [];
  355. searchClick: [];
  356. toolbarClick: [name: string];
  357. editClick: [row: IObject];
  358. filterChange: [data: IObject];
  359. operateClick: [data: IOperateData];
  360. }>();
  361. // 表格工具栏按钮配置
  362. const config = computed(() => props.contentConfig);
  363. const buttonConfig = reactive<Record<string, IObject>>({
  364. add: { text: "新增", attrs: { icon: "plus", type: "success" }, perm: "add" },
  365. delete: { text: "删除", attrs: { icon: "delete", type: "danger" }, perm: "delete" },
  366. import: { text: "导入", attrs: { icon: "upload", type: "" }, perm: "import" },
  367. export: { text: "导出", attrs: { icon: "download", type: "" }, perm: "export" },
  368. refresh: { text: "刷新", attrs: { icon: "refresh", type: "" }, perm: "*:*:*" },
  369. filter: { text: "筛选列", attrs: { icon: "operation", type: "" }, perm: "*:*:*" },
  370. search: { text: "搜索", attrs: { icon: "search", type: "" }, perm: "search" },
  371. imports: { text: "批量导入", attrs: { icon: "upload", type: "" }, perm: "imports" },
  372. exports: { text: "批量导出", attrs: { icon: "download", type: "" }, perm: "exports" },
  373. view: { text: "查看", attrs: { icon: "view", type: "primary" }, perm: "view" },
  374. edit: { text: "编辑", attrs: { icon: "edit", type: "primary" }, perm: "edit" },
  375. });
  376. // 主键
  377. const pk = props.contentConfig.pk ?? "id";
  378. // 权限名称前缀
  379. const authPrefix = computed(() => props.contentConfig.permPrefix);
  380. // 获取按钮权限标识
  381. function getButtonPerm(action: string): string | null {
  382. // 如果action已经包含完整路径(包含冒号),则直接使用
  383. if (action.includes(":")) {
  384. return action;
  385. }
  386. // 否则使用权限前缀组合
  387. return authPrefix.value ? `${authPrefix.value}:${action}` : null;
  388. }
  389. // 检查是否有权限
  390. function hasButtonPerm(action: string): boolean {
  391. const perm = getButtonPerm(action);
  392. // 如果没有设置权限标识,则默认具有权限
  393. if (!perm) return true;
  394. return hasPerm(perm);
  395. }
  396. // 创建工具栏按钮
  397. function createToolbar(toolbar: Array<string | IToolsButton>, attr = {}) {
  398. return toolbar.map((item) => {
  399. const isString = typeof item === "string";
  400. const baseItem = isString ? buttonConfig[item] : item;
  401. // 如果 item 是函数形式的 attrs,我们不应该将 attr 合并进去,因为这会覆盖函数
  402. const mergedAttrs = typeof item !== "string" && typeof item?.attrs === 'function'
  403. ? item.attrs
  404. : {
  405. ...attr,
  406. ...(isString ? baseItem.attrs : (item?.attrs || baseItem?.attrs || {})),
  407. };
  408. return {
  409. name: isString ? item : item?.name || "",
  410. text: isString ? baseItem.text : (item?.text || baseItem?.text || ""),
  411. attrs: mergedAttrs,
  412. render: isString ? undefined : (item?.render ?? undefined),
  413. perm: isString
  414. ? getButtonPerm(baseItem.perm)
  415. : baseItem?.perm
  416. ? getButtonPerm(baseItem.perm as string)
  417. : "*:*:*",
  418. };
  419. });
  420. }
  421. // 左侧工具栏按钮
  422. const toolbarLeftBtn = computed(() => {
  423. if (!config.value.toolbar || config.value.toolbar.length === 0) return [];
  424. return createToolbar(config.value.toolbar, {});
  425. });
  426. // 右侧工具栏按钮
  427. const toolbarRightBtn = computed(() => {
  428. if (!config.value.defaultToolbar || config.value.defaultToolbar.length === 0) return [];
  429. return createToolbar(config.value.defaultToolbar, { circle: true });
  430. });
  431. // 表格操作工具栏
  432. const tableToolbar = config.value.cols[config.value.cols.length - 1].operat ?? ["edit", "delete"];
  433. const tableToolbarBtn = createToolbar(tableToolbar, { link: true, size: "small" });
  434. // 表格列
  435. const cols = ref(
  436. props.contentConfig.cols.map((col) => {
  437. if (col.initFn) {
  438. col.initFn(col);
  439. }
  440. if (col.show === undefined) {
  441. col.show = true;
  442. }
  443. if (col.prop !== undefined && col.columnKey === undefined && col["column-key"] === undefined) {
  444. col.columnKey = col.prop;
  445. }
  446. if (
  447. col.type === "selection" &&
  448. col.reserveSelection === undefined &&
  449. col["reserve-selection"] === undefined
  450. ) {
  451. // 配合表格row-key实现跨页多选
  452. col.reserveSelection = true;
  453. }
  454. return col;
  455. })
  456. );
  457. // 加载状态
  458. const loading = ref(false);
  459. // 列表数据
  460. const pageData = ref<IObject[]>([]);
  461. // 显示分页
  462. const showPagination = props.contentConfig.pagination !== false;
  463. // 分页配置
  464. const defaultPagination = {
  465. background: true,
  466. layout: "total, sizes, prev, pager, next, jumper",
  467. pageSize: 20,
  468. pageSizes: [10, 20, 30, 50],
  469. total: 0,
  470. currentPage: 1,
  471. };
  472. const pagination = reactive(
  473. typeof props.contentConfig.pagination === "object"
  474. ? { ...defaultPagination, ...props.contentConfig.pagination }
  475. : defaultPagination
  476. );
  477. // 分页相关的请求参数
  478. const request = props.contentConfig.request ?? {
  479. pageName: "pageNum",
  480. limitName: "pageSize",
  481. };
  482. const tableRef = ref<TableInstance>();
  483. // 行选中
  484. const selectionData = ref<IObject[]>([]);
  485. // 删除ID集合 用于批量删除
  486. const removeIds = ref<(number | string)[]>([]);
  487. function handleSelectionChange(selection: any[]) {
  488. selectionData.value = selection;
  489. removeIds.value = selection.map((item) => item[pk]);
  490. }
  491. // 获取行选中
  492. function getSelectionData() {
  493. return selectionData.value;
  494. }
  495. // 刷新
  496. function handleRefresh(isRestart = false) {
  497. fetchPageData(lastFormData, isRestart);
  498. }
  499. // 删除
  500. function handleDelete(id?: number | string) {
  501. const ids = [id || removeIds.value].join(",");
  502. if (!ids) {
  503. ElMessage.warning("请勾选删除项");
  504. return;
  505. }
  506. ElMessageBox.confirm("确认删除?", "警告", {
  507. confirmButtonText: "确定",
  508. cancelButtonText: "取消",
  509. type: "warning",
  510. })
  511. .then(function () {
  512. if (props.contentConfig.deleteAction) {
  513. props.contentConfig
  514. .deleteAction(ids)
  515. .then(() => {
  516. ElMessage.success("删除成功");
  517. removeIds.value = [];
  518. //清空选中项
  519. tableRef.value?.clearSelection();
  520. handleRefresh(true);
  521. })
  522. .catch(() => {});
  523. } else {
  524. ElMessage.error("未配置deleteAction");
  525. }
  526. })
  527. .catch(() => {});
  528. }
  529. // 导出表单
  530. const fields: string[] = [];
  531. cols.value.forEach((item) => {
  532. if (item.prop !== undefined) {
  533. fields.push(item.prop);
  534. }
  535. });
  536. const enum ExportsOriginEnum {
  537. CURRENT = "current",
  538. SELECTED = "selected",
  539. REMOTE = "remote",
  540. }
  541. const exportsModalVisible = ref(false);
  542. const exportsFormRef = ref<FormInstance>();
  543. const exportsFormData = reactive({
  544. filename: "",
  545. sheetname: "",
  546. fields,
  547. origin: ExportsOriginEnum.CURRENT,
  548. });
  549. const exportsFormRules: FormRules = {
  550. fields: [{ required: true, message: "请选择字段" }],
  551. origin: [{ required: true, message: "请选择数据源" }],
  552. };
  553. // 打开导出弹窗
  554. function handleOpenExportsModal() {
  555. exportsModalVisible.value = true;
  556. }
  557. // 导出确认
  558. const handleExportsSubmit = useThrottleFn(() => {
  559. exportsFormRef.value?.validate((valid: boolean) => {
  560. if (valid) {
  561. handleExports();
  562. handleCloseExportsModal();
  563. }
  564. });
  565. }, 3000);
  566. // 关闭导出弹窗
  567. function handleCloseExportsModal() {
  568. exportsModalVisible.value = false;
  569. exportsFormRef.value?.resetFields();
  570. nextTick(() => {
  571. exportsFormRef.value?.clearValidate();
  572. });
  573. }
  574. // 导出
  575. function handleExports() {
  576. const filename = exportsFormData.filename
  577. ? exportsFormData.filename
  578. : props.contentConfig.permPrefix || "export";
  579. const sheetname = exportsFormData.sheetname ? exportsFormData.sheetname : "sheet";
  580. const workbook = new ExcelJS.Workbook();
  581. const worksheet = workbook.addWorksheet(sheetname);
  582. const columns: Partial<ExcelJS.Column>[] = [];
  583. cols.value.forEach((col) => {
  584. if (col.label && col.prop && exportsFormData.fields.includes(col.prop)) {
  585. columns.push({ header: col.label, key: col.prop });
  586. }
  587. });
  588. worksheet.columns = columns;
  589. if (exportsFormData.origin === ExportsOriginEnum.REMOTE) {
  590. if (props.contentConfig.exportsAction) {
  591. props.contentConfig.exportsAction(lastFormData).then((res) => {
  592. worksheet.addRows(res);
  593. workbook.xlsx
  594. .writeBuffer()
  595. .then((buffer) => {
  596. saveXlsx(buffer, filename as string);
  597. })
  598. .catch((error) => console.log(error));
  599. });
  600. } else {
  601. ElMessage.error("未配置exportsAction");
  602. }
  603. } else {
  604. worksheet.addRows(
  605. exportsFormData.origin === ExportsOriginEnum.SELECTED ? selectionData.value : pageData.value
  606. );
  607. workbook.xlsx
  608. .writeBuffer()
  609. .then((buffer) => {
  610. saveXlsx(buffer, filename as string);
  611. })
  612. .catch((error) => console.log(error));
  613. }
  614. }
  615. // 导入表单
  616. let isFileImport = false;
  617. const uploadRef = ref<UploadInstance>();
  618. const importModalVisible = ref(false);
  619. const importFormRef = ref<FormInstance>();
  620. const importFormData = reactive<{
  621. files: UploadUserFile[];
  622. }>({
  623. files: [],
  624. });
  625. const importFormRules: FormRules = {
  626. files: [{ required: true, message: "请选择文件" }],
  627. };
  628. // 打开导入弹窗
  629. function handleOpenImportModal(isFile: boolean = false) {
  630. importModalVisible.value = true;
  631. isFileImport = isFile;
  632. }
  633. // 覆盖前一个文件
  634. function handleFileExceed(files: File[]) {
  635. uploadRef.value!.clearFiles();
  636. const file = files[0] as UploadRawFile;
  637. file.uid = genFileId();
  638. uploadRef.value!.handleStart(file);
  639. }
  640. // 下载导入模板
  641. function handleDownloadTemplate() {
  642. const importTemplate = props.contentConfig.importTemplate;
  643. if (typeof importTemplate === "string") {
  644. window.open(importTemplate);
  645. } else if (typeof importTemplate === "function") {
  646. importTemplate().then((response) => {
  647. const fileData = response.data;
  648. const fileName = decodeURI(
  649. response.headers["content-disposition"].split(";")[1].split("=")[1]
  650. );
  651. saveXlsx(fileData, fileName);
  652. });
  653. } else {
  654. ElMessage.error("未配置importTemplate");
  655. }
  656. }
  657. // 导入确认
  658. const handleImportSubmit = useThrottleFn(() => {
  659. importFormRef.value?.validate((valid: boolean) => {
  660. if (valid) {
  661. if (isFileImport) {
  662. handleImport();
  663. } else {
  664. handleImports();
  665. }
  666. }
  667. });
  668. }, 3000);
  669. // 关闭导入弹窗
  670. function handleCloseImportModal() {
  671. importModalVisible.value = false;
  672. importFormRef.value?.resetFields();
  673. nextTick(() => {
  674. importFormRef.value?.clearValidate();
  675. });
  676. }
  677. // 文件导入
  678. function handleImport() {
  679. const importAction = props.contentConfig.importAction;
  680. if (importAction === undefined) {
  681. ElMessage.error("未配置importAction");
  682. return;
  683. }
  684. importAction(importFormData.files[0].raw as File).then(() => {
  685. ElMessage.success("导入数据成功");
  686. handleCloseImportModal();
  687. handleRefresh(true);
  688. });
  689. }
  690. // 导入
  691. function handleImports() {
  692. const importsAction = props.contentConfig.importsAction;
  693. if (importsAction === undefined) {
  694. ElMessage.error("未配置importsAction");
  695. return;
  696. }
  697. // 获取选择的文件
  698. const file = importFormData.files[0].raw as File;
  699. // 创建Workbook实例
  700. const workbook = new ExcelJS.Workbook();
  701. // 使用FileReader对象来读取文件内容
  702. const fileReader = new FileReader();
  703. // 二进制字符串的形式加载文件
  704. fileReader.readAsArrayBuffer(file);
  705. fileReader.onload = (ev) => {
  706. if (ev.target !== null && ev.target.result !== null) {
  707. const result = ev.target.result as ArrayBuffer;
  708. // 从 buffer中加载数据解析
  709. workbook.xlsx
  710. .load(result)
  711. .then((workbook) => {
  712. // 解析后的数据
  713. const data = [];
  714. // 获取第一个worksheet内容
  715. const worksheet = workbook.getWorksheet(1);
  716. if (worksheet) {
  717. // 获取第一行的标题
  718. const fields: any[] = [];
  719. worksheet.getRow(1).eachCell((cell) => {
  720. fields.push(cell.value);
  721. });
  722. // 遍历工作表的每一行(从第二行开始,因为第一行通常是标题行)
  723. for (let rowNumber = 2; rowNumber <= worksheet.rowCount; rowNumber++) {
  724. const rowData: IObject = {};
  725. const row = worksheet.getRow(rowNumber);
  726. // 遍历当前行的每个单元格
  727. row.eachCell((cell, colNumber) => {
  728. // 获取标题对应的键,并将当前单元格的值存储到相应的属性名中
  729. rowData[fields[colNumber - 1]] = cell.value;
  730. });
  731. // 将当前行的数据对象添加到数组中
  732. data.push(rowData);
  733. }
  734. }
  735. if (data.length === 0) {
  736. ElMessage.error("未解析到数据");
  737. return;
  738. }
  739. importsAction(data).then(() => {
  740. ElMessage.success("导入数据成功");
  741. handleCloseImportModal();
  742. handleRefresh(true);
  743. });
  744. })
  745. .catch((error) => console.log(error));
  746. } else {
  747. ElMessage.error("读取文件失败");
  748. }
  749. };
  750. }
  751. // 操作栏
  752. function handleToolbar(name: string) {
  753. switch (name) {
  754. case "refresh":
  755. handleRefresh();
  756. break;
  757. case "exports":
  758. handleOpenExportsModal();
  759. break;
  760. case "imports":
  761. handleOpenImportModal();
  762. break;
  763. case "search":
  764. emit("searchClick");
  765. break;
  766. case "add":
  767. emit("addClick");
  768. break;
  769. case "delete":
  770. handleDelete();
  771. break;
  772. case "import":
  773. handleOpenImportModal(true);
  774. break;
  775. case "export":
  776. emit("exportClick");
  777. break;
  778. default:
  779. emit("toolbarClick", name);
  780. break;
  781. }
  782. }
  783. // 操作列
  784. function handleOperate(data: IOperateData) {
  785. switch (data.name) {
  786. case "delete":
  787. if (props.contentConfig?.deleteAction) {
  788. handleDelete(data.row[pk]);
  789. } else {
  790. emit("operateClick", data);
  791. }
  792. break;
  793. default:
  794. emit("operateClick", data);
  795. break;
  796. }
  797. }
  798. // 属性修改
  799. function handleModify(field: string, value: boolean | string | number, row: Record<string, any>) {
  800. if (props.contentConfig.modifyAction) {
  801. props.contentConfig.modifyAction({
  802. [pk]: row[pk],
  803. field,
  804. value,
  805. });
  806. } else {
  807. ElMessage.error("未配置modifyAction");
  808. }
  809. }
  810. // 分页切换
  811. function handleSizeChange(value: number) {
  812. pagination.pageSize = value;
  813. handleRefresh();
  814. }
  815. function handleCurrentChange(value: number) {
  816. pagination.currentPage = value;
  817. handleRefresh();
  818. }
  819. // 远程数据筛选
  820. let filterParams: IObject = {};
  821. function handleFilterChange(newFilters: any) {
  822. const filters: IObject = {};
  823. for (const key in newFilters) {
  824. const col = cols.value.find((col) => {
  825. return col.columnKey === key || col["column-key"] === key;
  826. });
  827. if (col && col.filterJoin !== undefined) {
  828. filters[key] = newFilters[key].join(col.filterJoin);
  829. } else {
  830. filters[key] = newFilters[key];
  831. }
  832. }
  833. filterParams = { ...filterParams, ...filters };
  834. emit("filterChange", filterParams);
  835. }
  836. // 获取筛选条件
  837. function getFilterParams() {
  838. return filterParams;
  839. }
  840. // 获取分页数据
  841. let lastFormData = {};
  842. function fetchPageData(formData: IObject = {}, isRestart = false) {
  843. loading.value = true;
  844. // 上一次搜索条件
  845. lastFormData = formData;
  846. // 重置页码
  847. if (isRestart) {
  848. pagination.currentPage = 1;
  849. }
  850. props.contentConfig
  851. .indexAction(
  852. showPagination
  853. ? {
  854. [request.pageName]: pagination.currentPage,
  855. [request.limitName]: pagination.pageSize,
  856. ...formData,
  857. }
  858. : formData
  859. )
  860. .then((data) => {
  861. if (showPagination) {
  862. if (props.contentConfig.parseData) {
  863. data = props.contentConfig.parseData(data);
  864. }
  865. pagination.total = data.total;
  866. pageData.value = data.list;
  867. } else {
  868. pageData.value = data;
  869. }
  870. })
  871. .finally(() => {
  872. loading.value = false;
  873. });
  874. }
  875. fetchPageData();
  876. // 导出Excel
  877. function exportPageData(formData: IObject = {}) {
  878. if (props.contentConfig.exportAction) {
  879. props.contentConfig.exportAction(formData).then((response) => {
  880. const fileData = response.data;
  881. const fileName = decodeURI(
  882. response.headers["content-disposition"].split(";")[1].split("=")[1]
  883. );
  884. saveXlsx(fileData, fileName);
  885. });
  886. } else {
  887. ElMessage.error("未配置exportAction");
  888. }
  889. }
  890. // 浏览器保存文件
  891. function saveXlsx(fileData: any, fileName: string) {
  892. const fileType =
  893. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
  894. const blob = new Blob([fileData], { type: fileType });
  895. const downloadUrl = window.URL.createObjectURL(blob);
  896. const downloadLink = document.createElement("a");
  897. downloadLink.href = downloadUrl;
  898. downloadLink.download = fileName;
  899. document.body.appendChild(downloadLink);
  900. downloadLink.click();
  901. document.body.removeChild(downloadLink);
  902. window.URL.revokeObjectURL(downloadUrl);
  903. }
  904. // 暴露的属性和方法
  905. defineExpose({ fetchPageData, exportPageData, getFilterParams, getSelectionData, handleRefresh });
  906. </script>
  907. <style lang="scss" scoped>
  908. .toolbar-left,
  909. .toolbar-right {
  910. .el-button {
  911. margin-right: 0 !important;
  912. margin-left: 0 !important;
  913. }
  914. }
  915. </style>