From 7f2f3bd08855ead9d6e463dadedf4e7844fe42d8 Mon Sep 17 00:00:00 2001 From: xlsea Date: Mon, 18 Aug 2025 16:14:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(utils):=20=E6=96=B0=E5=A2=9E=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=20Excel=20=E5=AF=BC=E5=87=BA=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- pnpm-lock.yaml | 72 +++++++++++++++++++++++++++++++++++++++++++++ src/utils/export.ts | 71 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/utils/export.ts diff --git a/package.json b/package.json index 7ec81fff..9cc8759c 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,8 @@ "vue-advanced-cropper": "^2.8.9", "vue-draggable-plus": "0.6.0", "vue-i18n": "11.1.9", - "vue-router": "4.5.1" + "vue-router": "4.5.1", + "xlsx": "0.18.5" }, "devDependencies": { "@elegant-router/vue": "0.3.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf55d46a..5197dd23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: vue-router: specifier: 4.5.1 version: 4.5.1(vue@3.5.17(typescript@5.8.3)) + xlsx: + specifier: 0.18.5 + version: 0.18.5 devDependencies: '@elegant-router/vue': specifier: 0.3.8 @@ -1547,6 +1550,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1752,6 +1759,10 @@ packages: caniuse-lite@1.0.30001727: resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + chalk@1.1.3: resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} engines: {node: '>=0.10.0'} @@ -1801,6 +1812,10 @@ packages: resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} engines: {node: '>=0.8'} + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + collection-visit@1.0.0: resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} engines: {node: '>=0.10.0'} @@ -1871,6 +1886,11 @@ packages: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2393,6 +2413,10 @@ packages: resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} engines: {node: '>= 6'} + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + fragment-cache@0.2.1: resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} engines: {node: '>=0.10.0'} @@ -3652,6 +3676,10 @@ packages: resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} engines: {node: '>=0.10.0'} + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -4168,10 +4196,18 @@ packages: engines: {node: '>= 8'} hasBin: true + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -4184,6 +4220,11 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -5465,6 +5506,8 @@ snapshots: acorn@8.15.0: {} + adler-32@1.3.1: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -5691,6 +5734,11 @@ snapshots: caniuse-lite@1.0.30001727: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + chalk@1.1.3: dependencies: ansi-styles: 2.2.1 @@ -5757,6 +5805,8 @@ snapshots: clone@2.1.2: {} + codepage@1.15.0: {} + collection-visit@1.0.0: dependencies: map-visit: 1.0.0 @@ -5811,6 +5861,8 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + crc-32@1.2.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -6469,6 +6521,8 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + frac@1.1.2: {} + fragment-cache@0.2.1: dependencies: map-cache: 0.2.2 @@ -7742,6 +7796,10 @@ snapshots: dependencies: extend-shallow: 3.0.2 + ssf@0.11.2: + dependencies: + frac: 1.1.2 + stable-hash-x@0.2.0: {} stable@0.1.8: {} @@ -8363,8 +8421,12 @@ snapshots: dependencies: isexe: 2.0.0 + wmf@1.0.2: {} + word-wrap@1.2.5: {} + word@0.3.0: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -8381,6 +8443,16 @@ snapshots: dependencies: is-wsl: 3.1.0 + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + xml-name-validator@4.0.0: {} y18n@5.0.8: {} diff --git a/src/utils/export.ts b/src/utils/export.ts new file mode 100644 index 00000000..e8cb4630 --- /dev/null +++ b/src/utils/export.ts @@ -0,0 +1,71 @@ +import { utils, writeFile } from 'xlsx'; +import { isNotNull } from '@/utils/common'; +import { $t } from '@/locales'; + +export interface ExportExcelProps { + columns: NaiveUI.TableColumn>[]; + data: NaiveUI.TableDataWithIndex[]; + filename: string; + ignoreKeys?: (keyof NaiveUI.TableDataWithIndex | NaiveUI.CustomColumnKey)[]; + dicts?: Record, string>; +} + +export function exportExcel({ + columns, + data, + filename, + dicts, + ignoreKeys = ['index', 'operate'] +}: ExportExcelProps) { + const exportColumns = columns.filter(col => isTableColumnHasKey(col) && !ignoreKeys?.includes(col.key)); + + const excelList = data.map(item => exportColumns.map(col => getTableValue(col, item, dicts))); + + const titleList = exportColumns.map(col => (isTableColumnHasTitle(col) && col.title) || null); + + excelList.unshift(titleList); + + const workBook = utils.book_new(); + + const workSheet = utils.aoa_to_sheet(excelList); + + workSheet['!cols'] = exportColumns.map(item => ({ + width: Math.round(Number(item.width) / 10 || 20) + })); + + utils.book_append_sheet(workBook, workSheet, filename); + + writeFile(workBook, `${filename}.xlsx`); +} + +function getTableValue( + col: NaiveUI.TableColumn>, + item: NaiveUI.TableDataWithIndex, + dicts?: Record, string> +) { + if (!isTableColumnHasKey(col)) { + return null; + } + + const { key } = col; + + if (key === 'operate') { + return null; + } + + if (isNotNull(dicts?.[key]) && isNotNull(item[key])) { + return $t(item[key] as App.I18n.I18nKey); + } + + return item[key]; +} + +function isTableColumnHasKey(column: NaiveUI.TableColumn): column is NaiveUI.TableColumnWithKey { + return Boolean((column as NaiveUI.TableColumnWithKey).key); +} + +function isTableColumnHasTitle(column: NaiveUI.TableColumn): column is NaiveUI.TableColumnWithKey & { + title: string; +} { + return Boolean((column as NaiveUI.TableColumnWithKey).title); +}