@hbdlzy/ui-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/components.manifest.json +50 -0
- package/dist/index.cjs +1 -0
- package/dist/index.js +308 -0
- package/package.json +47 -0
- package/src/components/BaseExportButton/BaseExportButton.types.ts +29 -0
- package/src/components/BaseExportButton/BaseExportButton.utils.ts +54 -0
- package/src/components/BaseExportButton/BaseExportButton.vue +182 -0
- package/src/components/BaseExportButton/README.md +25 -0
- package/src/components/BaseExportButton/index.ts +20 -0
- package/src/excel/exportExcel.ts +170 -0
- package/src/index.ts +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @hbdlzy/ui-core
|
|
2
|
+
|
|
3
|
+
`@hbdlzy/ui-core` 负责沉淀跨项目复用的基础 UI 组件和无业务依赖的前端工具。
|
|
4
|
+
|
|
5
|
+
## 当前内容
|
|
6
|
+
|
|
7
|
+
- `BaseExportButton`
|
|
8
|
+
- `exportExcel`
|
|
9
|
+
|
|
10
|
+
## 使用方式
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { BaseExportButton, exportExcel } from '@hbdlzy/ui-core'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 设计原则
|
|
17
|
+
|
|
18
|
+
- 只做基础 UI 和交互
|
|
19
|
+
- 不直接依赖接口、路由、store
|
|
20
|
+
- 每个组件必须配套 README 和 AI manifest
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"package": "@hbdlzy/ui-core",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"updatedAt": "2026-04-21",
|
|
5
|
+
"components": [
|
|
6
|
+
{
|
|
7
|
+
"name": "BaseExportButton",
|
|
8
|
+
"category": "ui-core",
|
|
9
|
+
"entry": "@hbdlzy/ui-core",
|
|
10
|
+
"importName": "BaseExportButton",
|
|
11
|
+
"docs": "packages/ui-core/src/components/BaseExportButton/README.md",
|
|
12
|
+
"description": "统一处理前端 Excel 导出和后�?blob 下载的基础导出按钮�?,
|
|
13
|
+
"props": [
|
|
14
|
+
{
|
|
15
|
+
"name": "label",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"default": "导出"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "fileName",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"default": ""
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "excelOptions",
|
|
26
|
+
"type": "ExcelExportOptions",
|
|
27
|
+
"required": false
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "requestHandler",
|
|
31
|
+
"type": "() => Promise<ExportResponseLike>",
|
|
32
|
+
"required": false
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "beforeExport",
|
|
36
|
+
"type": "() => boolean | Promise<boolean>",
|
|
37
|
+
"required": false
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"emits": ["start", "success", "error"]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"utilities": [
|
|
44
|
+
{
|
|
45
|
+
"name": "exportExcel",
|
|
46
|
+
"entry": "@hbdlzy/ui-core",
|
|
47
|
+
"description": "支持多级表头和自动列宽的纯前�?Excel 导出工具�?
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});var c=require("vue"),g=require("element-plus"),w=require("xlsx"),O=require("file-saver");function D(e){if(e&&e.__esModule)return e;var r=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});return e&&Object.keys(e).forEach(function(t){if(t!=="default"){var n=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(r,t,n.get?n:{enumerable:!0,get:function(){return e[t]}})}}),r.default=e,Object.freeze(r)}var x=D(w);const S="Sheet1",A=16;function h(e){var r;return(r=e.children)!=null&&r.length?e.children.reduce((t,n)=>t+h(n),0):1}function y(e,r=1){return e.reduce((t,n)=>{var o;return(o=n.children)!=null&&o.length?Math.max(t,y(n.children,r+1)):Math.max(t,r)},r)}function b(e){const r=`${e!=null?e:""}`;return Array.from(r).reduce((t,n)=>t+(n.charCodeAt(0)>255?2:1),0)}function _(e,r){return r?r.split(".").reduce((t,n)=>{if(t!=null)return t[n]},e):""}function N(e,r,t){return typeof t.formatter=="function"?t.formatter(e,r,t):_(e,t.key)}function q(e){const r=y(e),t=e.reduce((u,s)=>u+h(s),0),n=Array.from({length:r},()=>Array.from({length:t}).fill("")),o=[],l=[];let f=0;const d=(u,s)=>{var p;const a=f,i=h(u);if(n[s][a]=u.label,(p=u.children)!=null&&p.length){u.children.forEach(m=>d(m,s+1)),i>1&&o.push({s:{r:s,c:a},e:{r:s,c:a+i-1}});return}l.push(u),f+=1,s<r-1&&o.push({s:{r:s,c:a},e:{r:r-1,c:a}})};return e.forEach(u=>d(u,0)),{headerRows:n,leafColumns:l,merges:o}}function H(e,r,t,n){return e.map((o,l)=>{var s;const f=b(o.label),d=r.reduce((a,i)=>Math.max(a,b(i[l])),0);return{wch:(s=o.width)!=null?s:n?Math.max(t,f,d):t}})}function k(e){return e.endsWith(".xlsx")?e:`${e}.xlsx`}function F(e){const{fileName:r,sheetName:t=S,data:n,autoWidth:o=!0,defaultColumnWidth:l=A}=e,f=e.columns.filter(m=>!!m);if(!f.length)throw new Error("Excel export columns can not be empty.");const{headerRows:d,leafColumns:u,merges:s}=q(f),a=n.map((m,B)=>u.map(M=>N(m,B,M))),i=x.utils.aoa_to_sheet([...d,...a]);i["!merges"]=s,i["!cols"]=H(u,a,l,o);const p=x.utils.book_new();x.utils.book_append_sheet(p,i,t),x.writeFile(p,k(r))}const L="export-file";function z(e,r){var o;if(!e)return"";const t=r.toLowerCase(),n=Object.keys(e).find(l=>l.toLowerCase()===t);return n&&(o=e[n])!=null?o:""}function C(e){if(!e)return"";const r=e.match(/filename\*=UTF-8''([^;]+)/i);if(r!=null&&r[1])return decodeURIComponent(r[1].replace(/["']/g,""));const t=e.match(/filename="?([^";]+)"?/i);return t!=null&&t[1]?decodeURIComponent(t[1].replace(/["']/g,"")):""}function E(e){return`${e!=null?e:""}`.trim()||L}function T(e){return e instanceof Blob?e:new Blob([e])}function v(e,r=""){const t=z(e.headers,"content-disposition"),n=E(C(t)||r);return O.saveAs(T(e.data),n),n}const j=c.defineComponent({__name:"BaseExportButton",props:{label:{type:String,default:"\u5BFC\u51FA"},type:{type:String,default:"primary"},size:{type:String,default:"default"},plain:{type:Boolean,default:!1},link:{type:Boolean,default:!1},text:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},fileName:{type:String,default:""},excelOptions:Object,requestHandler:Function,beforeExport:Function,successMessage:{type:String,default:"\u5BFC\u51FA\u6210\u529F"},errorMessage:{type:String,default:"\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"},autoMessage:{type:Boolean,default:!0}},emits:["start","success","error"],setup(e,{emit:r}){const t=e,n=r,o=c.ref(!1),l=c.computed(()=>t.excelOptions&&t.requestHandler?"invalid":t.excelOptions?"excel":t.requestHandler?"request":"none"),f=c.computed(()=>t.disabled||o.value||l.value==="none");async function d(){return t.beforeExport?await t.beforeExport()!==!1:!0}async function u(){if(o.value||t.disabled)return;if(l.value==="none"){g.ElMessage.warning("\u8BF7\u5148\u914D\u7F6E\u5BFC\u51FA\u903B\u8F91");return}if(l.value==="invalid"){const a=new Error("BaseExportButton \u4E0D\u80FD\u540C\u65F6\u63A5\u6536 excelOptions \u548C requestHandler");n("error",a),g.ElMessage.error(a.message);return}if(!!await d()){o.value=!0,n("start");try{if(l.value==="excel"&&t.excelOptions){F(t.excelOptions),t.autoMessage&&g.ElMessage.success(t.successMessage),n("success",{mode:"excel",fileName:E(t.excelOptions.fileName)});return}if(l.value==="request"&&t.requestHandler){const a=await t.requestHandler(),i=v(a,t.fileName);t.autoMessage&&g.ElMessage.success(t.successMessage),n("success",{mode:"request",fileName:i})}}catch(a){console.error(a),t.autoMessage&&g.ElMessage.error(t.errorMessage),n("error",a)}finally{o.value=!1}}}return(s,a)=>{const i=c.resolveComponent("el-button");return c.openBlock(),c.createBlock(i,{class:"base-export-button",type:e.type,size:e.size,plain:e.plain,link:e.link,text:e.text,disabled:f.value,loading:o.value,onClick:u},{default:c.withCtx(()=>[c.renderSlot(s.$slots,"default",{},()=>[c.createTextVNode(c.toDisplayString(e.label),1)])]),_:3},8,["type","size","plain","link","text","disabled","loading"])}}});exports.BaseExportButton=j;exports.downloadExportResponse=v;exports.exportExcel=F;exports.extractFileNameFromDisposition=C;exports.normalizeExportFileName=E;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { defineComponent, ref, computed, resolveComponent, openBlock, createBlock, withCtx, renderSlot, createTextVNode, toDisplayString } from "vue";
|
|
2
|
+
import { ElMessage } from "element-plus";
|
|
3
|
+
import * as XLSX from "xlsx";
|
|
4
|
+
import { saveAs } from "file-saver";
|
|
5
|
+
const DEFAULT_SHEET_NAME = "Sheet1";
|
|
6
|
+
const DEFAULT_COLUMN_WIDTH = 16;
|
|
7
|
+
function getLeafCount(column) {
|
|
8
|
+
var _a;
|
|
9
|
+
if (!((_a = column.children) == null ? void 0 : _a.length)) {
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
12
|
+
return column.children.reduce((count, child) => count + getLeafCount(child), 0);
|
|
13
|
+
}
|
|
14
|
+
function getMaxDepth(columns, depth = 1) {
|
|
15
|
+
return columns.reduce((maxDepth, column) => {
|
|
16
|
+
var _a;
|
|
17
|
+
if (!((_a = column.children) == null ? void 0 : _a.length)) {
|
|
18
|
+
return Math.max(maxDepth, depth);
|
|
19
|
+
}
|
|
20
|
+
return Math.max(maxDepth, getMaxDepth(column.children, depth + 1));
|
|
21
|
+
}, depth);
|
|
22
|
+
}
|
|
23
|
+
function getDisplayLength(value) {
|
|
24
|
+
const text = `${value != null ? value : ""}`;
|
|
25
|
+
return Array.from(text).reduce((length, char) => {
|
|
26
|
+
return length + (char.charCodeAt(0) > 255 ? 2 : 1);
|
|
27
|
+
}, 0);
|
|
28
|
+
}
|
|
29
|
+
function getValueByPath(source, path) {
|
|
30
|
+
if (!path) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
return path.split(".").reduce((current, key) => {
|
|
34
|
+
if (current === null || current === void 0) {
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
return current[key];
|
|
38
|
+
}, source);
|
|
39
|
+
}
|
|
40
|
+
function getCellValue(row, rowIndex, column) {
|
|
41
|
+
if (typeof column.formatter === "function") {
|
|
42
|
+
return column.formatter(row, rowIndex, column);
|
|
43
|
+
}
|
|
44
|
+
return getValueByPath(row, column.key);
|
|
45
|
+
}
|
|
46
|
+
function buildHeaderMatrix(columns) {
|
|
47
|
+
const maxDepth = getMaxDepth(columns);
|
|
48
|
+
const totalLeafCount = columns.reduce((count, column) => count + getLeafCount(column), 0);
|
|
49
|
+
const headerRows = Array.from({ length: maxDepth }, () => Array.from({ length: totalLeafCount }).fill(""));
|
|
50
|
+
const merges = [];
|
|
51
|
+
const leafColumns = [];
|
|
52
|
+
let currentColumnIndex = 0;
|
|
53
|
+
const fillHeaders = (column, depth) => {
|
|
54
|
+
var _a;
|
|
55
|
+
const startColumnIndex = currentColumnIndex;
|
|
56
|
+
const leafCount = getLeafCount(column);
|
|
57
|
+
headerRows[depth][startColumnIndex] = column.label;
|
|
58
|
+
if ((_a = column.children) == null ? void 0 : _a.length) {
|
|
59
|
+
column.children.forEach((child) => fillHeaders(child, depth + 1));
|
|
60
|
+
if (leafCount > 1) {
|
|
61
|
+
merges.push({
|
|
62
|
+
s: { r: depth, c: startColumnIndex },
|
|
63
|
+
e: { r: depth, c: startColumnIndex + leafCount - 1 }
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
leafColumns.push(column);
|
|
69
|
+
currentColumnIndex += 1;
|
|
70
|
+
if (depth < maxDepth - 1) {
|
|
71
|
+
merges.push({
|
|
72
|
+
s: { r: depth, c: startColumnIndex },
|
|
73
|
+
e: { r: maxDepth - 1, c: startColumnIndex }
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
columns.forEach((column) => fillHeaders(column, 0));
|
|
78
|
+
return {
|
|
79
|
+
headerRows,
|
|
80
|
+
leafColumns,
|
|
81
|
+
merges
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function buildColumnWidths(leafColumns, dataRows, defaultColumnWidth, autoWidth) {
|
|
85
|
+
return leafColumns.map((column, columnIndex) => {
|
|
86
|
+
var _a;
|
|
87
|
+
const headerWidth = getDisplayLength(column.label);
|
|
88
|
+
const dataWidth = dataRows.reduce((maxWidth, row) => {
|
|
89
|
+
return Math.max(maxWidth, getDisplayLength(row[columnIndex]));
|
|
90
|
+
}, 0);
|
|
91
|
+
const targetWidth = (_a = column.width) != null ? _a : autoWidth ? Math.max(defaultColumnWidth, headerWidth, dataWidth) : defaultColumnWidth;
|
|
92
|
+
return { wch: targetWidth };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function normalizeFileName(fileName) {
|
|
96
|
+
return fileName.endsWith(".xlsx") ? fileName : `${fileName}.xlsx`;
|
|
97
|
+
}
|
|
98
|
+
function exportExcel(options) {
|
|
99
|
+
const {
|
|
100
|
+
fileName,
|
|
101
|
+
sheetName = DEFAULT_SHEET_NAME,
|
|
102
|
+
data,
|
|
103
|
+
autoWidth = true,
|
|
104
|
+
defaultColumnWidth = DEFAULT_COLUMN_WIDTH
|
|
105
|
+
} = options;
|
|
106
|
+
const columns = options.columns.filter((column) => !!column);
|
|
107
|
+
if (!columns.length) {
|
|
108
|
+
throw new Error("Excel export columns can not be empty.");
|
|
109
|
+
}
|
|
110
|
+
const { headerRows, leafColumns, merges } = buildHeaderMatrix(columns);
|
|
111
|
+
const dataRows = data.map((row, rowIndex) => leafColumns.map((column) => getCellValue(row, rowIndex, column)));
|
|
112
|
+
const worksheet = XLSX.utils.aoa_to_sheet([...headerRows, ...dataRows]);
|
|
113
|
+
worksheet["!merges"] = merges;
|
|
114
|
+
worksheet["!cols"] = buildColumnWidths(leafColumns, dataRows, defaultColumnWidth, autoWidth);
|
|
115
|
+
const workbook = XLSX.utils.book_new();
|
|
116
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
117
|
+
XLSX.writeFile(workbook, normalizeFileName(fileName));
|
|
118
|
+
}
|
|
119
|
+
const DEFAULT_EXPORT_FILE_NAME = "export-file";
|
|
120
|
+
function getHeaderValue(headers, headerName) {
|
|
121
|
+
var _a;
|
|
122
|
+
if (!headers) {
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
const targetHeaderName = headerName.toLowerCase();
|
|
126
|
+
const matchedHeaderKey = Object.keys(headers).find((key) => key.toLowerCase() === targetHeaderName);
|
|
127
|
+
return matchedHeaderKey ? (_a = headers[matchedHeaderKey]) != null ? _a : "" : "";
|
|
128
|
+
}
|
|
129
|
+
function extractFileNameFromDisposition(contentDisposition) {
|
|
130
|
+
if (!contentDisposition) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
|
|
134
|
+
if (utf8Match == null ? void 0 : utf8Match[1]) {
|
|
135
|
+
return decodeURIComponent(utf8Match[1].replace(/["']/g, ""));
|
|
136
|
+
}
|
|
137
|
+
const basicMatch = contentDisposition.match(/filename="?([^";]+)"?/i);
|
|
138
|
+
if (basicMatch == null ? void 0 : basicMatch[1]) {
|
|
139
|
+
return decodeURIComponent(basicMatch[1].replace(/["']/g, ""));
|
|
140
|
+
}
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
function normalizeExportFileName(fileName) {
|
|
144
|
+
const normalizedFileName = `${fileName != null ? fileName : ""}`.trim();
|
|
145
|
+
return normalizedFileName || DEFAULT_EXPORT_FILE_NAME;
|
|
146
|
+
}
|
|
147
|
+
function toBlob(payload) {
|
|
148
|
+
return payload instanceof Blob ? payload : new Blob([payload]);
|
|
149
|
+
}
|
|
150
|
+
function downloadExportResponse(response, fallbackFileName = "") {
|
|
151
|
+
const contentDisposition = getHeaderValue(response.headers, "content-disposition");
|
|
152
|
+
const fileName = normalizeExportFileName(extractFileNameFromDisposition(contentDisposition) || fallbackFileName);
|
|
153
|
+
saveAs(toBlob(response.data), fileName);
|
|
154
|
+
return fileName;
|
|
155
|
+
}
|
|
156
|
+
const _sfc_main = defineComponent({
|
|
157
|
+
__name: "BaseExportButton",
|
|
158
|
+
props: {
|
|
159
|
+
label: {
|
|
160
|
+
type: String,
|
|
161
|
+
default: "\u5BFC\u51FA"
|
|
162
|
+
},
|
|
163
|
+
type: {
|
|
164
|
+
type: String,
|
|
165
|
+
default: "primary"
|
|
166
|
+
},
|
|
167
|
+
size: {
|
|
168
|
+
type: String,
|
|
169
|
+
default: "default"
|
|
170
|
+
},
|
|
171
|
+
plain: {
|
|
172
|
+
type: Boolean,
|
|
173
|
+
default: false
|
|
174
|
+
},
|
|
175
|
+
link: {
|
|
176
|
+
type: Boolean,
|
|
177
|
+
default: false
|
|
178
|
+
},
|
|
179
|
+
text: {
|
|
180
|
+
type: Boolean,
|
|
181
|
+
default: false
|
|
182
|
+
},
|
|
183
|
+
disabled: {
|
|
184
|
+
type: Boolean,
|
|
185
|
+
default: false
|
|
186
|
+
},
|
|
187
|
+
fileName: {
|
|
188
|
+
type: String,
|
|
189
|
+
default: ""
|
|
190
|
+
},
|
|
191
|
+
excelOptions: Object,
|
|
192
|
+
requestHandler: Function,
|
|
193
|
+
beforeExport: Function,
|
|
194
|
+
successMessage: {
|
|
195
|
+
type: String,
|
|
196
|
+
default: "\u5BFC\u51FA\u6210\u529F"
|
|
197
|
+
},
|
|
198
|
+
errorMessage: {
|
|
199
|
+
type: String,
|
|
200
|
+
default: "\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"
|
|
201
|
+
},
|
|
202
|
+
autoMessage: {
|
|
203
|
+
type: Boolean,
|
|
204
|
+
default: true
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
emits: ["start", "success", "error"],
|
|
208
|
+
setup(__props, { emit: __emit }) {
|
|
209
|
+
const props = __props;
|
|
210
|
+
const emit = __emit;
|
|
211
|
+
const loading = ref(false);
|
|
212
|
+
const exportMode = computed(() => {
|
|
213
|
+
if (props.excelOptions && props.requestHandler) {
|
|
214
|
+
return "invalid";
|
|
215
|
+
}
|
|
216
|
+
if (props.excelOptions) {
|
|
217
|
+
return "excel";
|
|
218
|
+
}
|
|
219
|
+
if (props.requestHandler) {
|
|
220
|
+
return "request";
|
|
221
|
+
}
|
|
222
|
+
return "none";
|
|
223
|
+
});
|
|
224
|
+
const isDisabled = computed(() => props.disabled || loading.value || exportMode.value === "none");
|
|
225
|
+
async function runBeforeExport() {
|
|
226
|
+
if (!props.beforeExport) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
const result = await props.beforeExport();
|
|
230
|
+
return result !== false;
|
|
231
|
+
}
|
|
232
|
+
async function handleExport() {
|
|
233
|
+
if (loading.value || props.disabled) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (exportMode.value === "none") {
|
|
237
|
+
ElMessage.warning("\u8BF7\u5148\u914D\u7F6E\u5BFC\u51FA\u903B\u8F91");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (exportMode.value === "invalid") {
|
|
241
|
+
const conflictError = new Error("BaseExportButton \u4E0D\u80FD\u540C\u65F6\u63A5\u6536 excelOptions \u548C requestHandler");
|
|
242
|
+
emit("error", conflictError);
|
|
243
|
+
ElMessage.error(conflictError.message);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const canContinue = await runBeforeExport();
|
|
247
|
+
if (!canContinue) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
loading.value = true;
|
|
251
|
+
emit("start");
|
|
252
|
+
try {
|
|
253
|
+
if (exportMode.value === "excel" && props.excelOptions) {
|
|
254
|
+
exportExcel(props.excelOptions);
|
|
255
|
+
if (props.autoMessage) {
|
|
256
|
+
ElMessage.success(props.successMessage);
|
|
257
|
+
}
|
|
258
|
+
emit("success", {
|
|
259
|
+
mode: "excel",
|
|
260
|
+
fileName: normalizeExportFileName(props.excelOptions.fileName)
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (exportMode.value === "request" && props.requestHandler) {
|
|
265
|
+
const response = await props.requestHandler();
|
|
266
|
+
const fileName = downloadExportResponse(response, props.fileName);
|
|
267
|
+
if (props.autoMessage) {
|
|
268
|
+
ElMessage.success(props.successMessage);
|
|
269
|
+
}
|
|
270
|
+
emit("success", {
|
|
271
|
+
mode: "request",
|
|
272
|
+
fileName
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error(error);
|
|
277
|
+
if (props.autoMessage) {
|
|
278
|
+
ElMessage.error(props.errorMessage);
|
|
279
|
+
}
|
|
280
|
+
emit("error", error);
|
|
281
|
+
} finally {
|
|
282
|
+
loading.value = false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return (_ctx, _cache) => {
|
|
286
|
+
const _component_el_button = resolveComponent("el-button");
|
|
287
|
+
return openBlock(), createBlock(_component_el_button, {
|
|
288
|
+
class: "base-export-button",
|
|
289
|
+
type: __props.type,
|
|
290
|
+
size: __props.size,
|
|
291
|
+
plain: __props.plain,
|
|
292
|
+
link: __props.link,
|
|
293
|
+
text: __props.text,
|
|
294
|
+
disabled: isDisabled.value,
|
|
295
|
+
loading: loading.value,
|
|
296
|
+
onClick: handleExport
|
|
297
|
+
}, {
|
|
298
|
+
default: withCtx(() => [
|
|
299
|
+
renderSlot(_ctx.$slots, "default", {}, () => [
|
|
300
|
+
createTextVNode(toDisplayString(__props.label), 1)
|
|
301
|
+
])
|
|
302
|
+
]),
|
|
303
|
+
_: 3
|
|
304
|
+
}, 8, ["type", "size", "plain", "link", "text", "disabled", "loading"]);
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
export { _sfc_main as BaseExportButton, downloadExportResponse, exportExcel, extractFileNameFromDisposition, normalizeExportFileName };
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hbdlzy/ui-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Company shared Vue UI core package.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/hbdlzy/componentized-library.git",
|
|
9
|
+
"directory": "packages/ui-core"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.cjs",
|
|
12
|
+
"module": "dist/index.js",
|
|
13
|
+
"types": "src/index.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"require": "./dist/index.cjs",
|
|
18
|
+
"types": "./src/index.ts"
|
|
19
|
+
},
|
|
20
|
+
"./components.manifest.json": "./components.manifest.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"README.md",
|
|
26
|
+
"components.manifest.json"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"element-plus": "^2.13.7",
|
|
30
|
+
"vue": "^3.5.14"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"file-saver": "2.0.5",
|
|
34
|
+
"xlsx": "^0.18.5"
|
|
35
|
+
},
|
|
36
|
+
"sideEffects": [
|
|
37
|
+
"**/*.css",
|
|
38
|
+
"**/*.vue"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"registry": "https://registry.npmjs.org/"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "vite build"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ExcelExportOptions } from '../../excel/exportExcel'
|
|
2
|
+
|
|
3
|
+
export interface ExportResponseHeaders {
|
|
4
|
+
[key: string]: string | undefined
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ExportResponseLike {
|
|
8
|
+
data: Blob | BlobPart | ArrayBuffer
|
|
9
|
+
headers?: ExportResponseHeaders
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ExportRequestHandler = () => Promise<ExportResponseLike>
|
|
13
|
+
|
|
14
|
+
export type ExportMode = 'excel' | 'request' | 'none' | 'invalid'
|
|
15
|
+
|
|
16
|
+
export type ExportBeforeHandler = () => boolean | void | Promise<boolean | void>
|
|
17
|
+
|
|
18
|
+
export interface ExportSuccessPayload {
|
|
19
|
+
mode: Exclude<ExportMode, 'none' | 'invalid'>
|
|
20
|
+
fileName?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BaseExportExcelConfig<Row = Record<string, any>> {
|
|
24
|
+
excelOptions: ExcelExportOptions<Row>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BaseExportRequestConfig {
|
|
28
|
+
requestHandler: ExportRequestHandler
|
|
29
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { saveAs } from 'file-saver'
|
|
2
|
+
import type { ExportResponseHeaders, ExportResponseLike } from './BaseExportButton.types'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_EXPORT_FILE_NAME = 'export-file'
|
|
5
|
+
|
|
6
|
+
function getHeaderValue(headers: ExportResponseHeaders | undefined, headerName: string) {
|
|
7
|
+
if (!headers) {
|
|
8
|
+
return ''
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const targetHeaderName = headerName.toLowerCase()
|
|
12
|
+
const matchedHeaderKey = Object.keys(headers).find(key => key.toLowerCase() === targetHeaderName)
|
|
13
|
+
|
|
14
|
+
return matchedHeaderKey ? headers[matchedHeaderKey] ?? '' : ''
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function extractFileNameFromDisposition(contentDisposition?: string) {
|
|
18
|
+
if (!contentDisposition) {
|
|
19
|
+
return ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)
|
|
23
|
+
|
|
24
|
+
if (utf8Match?.[1]) {
|
|
25
|
+
return decodeURIComponent(utf8Match[1].replace(/["']/g, ''))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const basicMatch = contentDisposition.match(/filename="?([^";]+)"?/i)
|
|
29
|
+
|
|
30
|
+
if (basicMatch?.[1]) {
|
|
31
|
+
return decodeURIComponent(basicMatch[1].replace(/["']/g, ''))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return ''
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizeExportFileName(fileName?: string) {
|
|
38
|
+
const normalizedFileName = `${fileName ?? ''}`.trim()
|
|
39
|
+
|
|
40
|
+
return normalizedFileName || DEFAULT_EXPORT_FILE_NAME
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toBlob(payload: ExportResponseLike['data']) {
|
|
44
|
+
return payload instanceof Blob ? payload : new Blob([payload])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function downloadExportResponse(response: ExportResponseLike, fallbackFileName = '') {
|
|
48
|
+
const contentDisposition = getHeaderValue(response.headers, 'content-disposition')
|
|
49
|
+
const fileName = normalizeExportFileName(extractFileNameFromDisposition(contentDisposition) || fallbackFileName)
|
|
50
|
+
|
|
51
|
+
saveAs(toBlob(response.data), fileName)
|
|
52
|
+
|
|
53
|
+
return fileName
|
|
54
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-button
|
|
3
|
+
class="base-export-button"
|
|
4
|
+
:type="type"
|
|
5
|
+
:size="size"
|
|
6
|
+
:plain="plain"
|
|
7
|
+
:link="link"
|
|
8
|
+
:text="text"
|
|
9
|
+
:disabled="isDisabled"
|
|
10
|
+
:loading="loading"
|
|
11
|
+
@click="handleExport"
|
|
12
|
+
>
|
|
13
|
+
<slot>{{ label }}</slot>
|
|
14
|
+
</el-button>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { computed, ref } from 'vue'
|
|
19
|
+
import type { PropType } from 'vue'
|
|
20
|
+
import { ElMessage } from 'element-plus'
|
|
21
|
+
import { exportExcel, type ExcelExportOptions } from '../../excel/exportExcel'
|
|
22
|
+
import { downloadExportResponse, normalizeExportFileName } from './BaseExportButton.utils'
|
|
23
|
+
import type {
|
|
24
|
+
ExportBeforeHandler,
|
|
25
|
+
ExportMode,
|
|
26
|
+
ExportRequestHandler,
|
|
27
|
+
ExportSuccessPayload
|
|
28
|
+
} from './BaseExportButton.types'
|
|
29
|
+
|
|
30
|
+
const props = defineProps({
|
|
31
|
+
label: {
|
|
32
|
+
type: String,
|
|
33
|
+
default: '导出'
|
|
34
|
+
},
|
|
35
|
+
type: {
|
|
36
|
+
type: String as PropType<'primary' | 'success' | 'warning' | 'danger' | 'info' | ''>,
|
|
37
|
+
default: 'primary'
|
|
38
|
+
},
|
|
39
|
+
size: {
|
|
40
|
+
type: String as PropType<'large' | 'default' | 'small'>,
|
|
41
|
+
default: 'default'
|
|
42
|
+
},
|
|
43
|
+
plain: {
|
|
44
|
+
type: Boolean,
|
|
45
|
+
default: false
|
|
46
|
+
},
|
|
47
|
+
link: {
|
|
48
|
+
type: Boolean,
|
|
49
|
+
default: false
|
|
50
|
+
},
|
|
51
|
+
text: {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: false
|
|
54
|
+
},
|
|
55
|
+
disabled: {
|
|
56
|
+
type: Boolean,
|
|
57
|
+
default: false
|
|
58
|
+
},
|
|
59
|
+
fileName: {
|
|
60
|
+
type: String,
|
|
61
|
+
default: ''
|
|
62
|
+
},
|
|
63
|
+
excelOptions: Object as PropType<ExcelExportOptions<Record<string, any>>>,
|
|
64
|
+
requestHandler: Function as PropType<ExportRequestHandler>,
|
|
65
|
+
beforeExport: Function as PropType<ExportBeforeHandler>,
|
|
66
|
+
successMessage: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: '导出成功'
|
|
69
|
+
},
|
|
70
|
+
errorMessage: {
|
|
71
|
+
type: String,
|
|
72
|
+
default: '导出失败,请稍后重试'
|
|
73
|
+
},
|
|
74
|
+
autoMessage: {
|
|
75
|
+
type: Boolean,
|
|
76
|
+
default: true
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const emit = defineEmits<{
|
|
81
|
+
(event: 'start'): void
|
|
82
|
+
(event: 'success', payload: ExportSuccessPayload): void
|
|
83
|
+
(event: 'error', error: unknown): void
|
|
84
|
+
}>()
|
|
85
|
+
|
|
86
|
+
const loading = ref(false)
|
|
87
|
+
|
|
88
|
+
const exportMode = computed<ExportMode>(() => {
|
|
89
|
+
if (props.excelOptions && props.requestHandler) {
|
|
90
|
+
return 'invalid'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (props.excelOptions) {
|
|
94
|
+
return 'excel'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (props.requestHandler) {
|
|
98
|
+
return 'request'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return 'none'
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const isDisabled = computed(() => props.disabled || loading.value || exportMode.value === 'none')
|
|
105
|
+
|
|
106
|
+
async function runBeforeExport() {
|
|
107
|
+
if (!props.beforeExport) {
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = await props.beforeExport()
|
|
112
|
+
return result !== false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function handleExport() {
|
|
116
|
+
if (loading.value || props.disabled) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (exportMode.value === 'none') {
|
|
121
|
+
ElMessage.warning('请先配置导出逻辑')
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (exportMode.value === 'invalid') {
|
|
126
|
+
const conflictError = new Error('BaseExportButton 不能同时接收 excelOptions 和 requestHandler')
|
|
127
|
+
emit('error', conflictError)
|
|
128
|
+
ElMessage.error(conflictError.message)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const canContinue = await runBeforeExport()
|
|
133
|
+
|
|
134
|
+
if (!canContinue) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
loading.value = true
|
|
139
|
+
emit('start')
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
if (exportMode.value === 'excel' && props.excelOptions) {
|
|
143
|
+
exportExcel(props.excelOptions)
|
|
144
|
+
|
|
145
|
+
if (props.autoMessage) {
|
|
146
|
+
ElMessage.success(props.successMessage)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
emit('success', {
|
|
150
|
+
mode: 'excel',
|
|
151
|
+
fileName: normalizeExportFileName(props.excelOptions.fileName)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (exportMode.value === 'request' && props.requestHandler) {
|
|
158
|
+
const response = await props.requestHandler()
|
|
159
|
+
const fileName = downloadExportResponse(response, props.fileName)
|
|
160
|
+
|
|
161
|
+
if (props.autoMessage) {
|
|
162
|
+
ElMessage.success(props.successMessage)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
emit('success', {
|
|
166
|
+
mode: 'request',
|
|
167
|
+
fileName
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(error)
|
|
172
|
+
|
|
173
|
+
if (props.autoMessage) {
|
|
174
|
+
ElMessage.error(props.errorMessage)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
emit('error', error)
|
|
178
|
+
} finally {
|
|
179
|
+
loading.value = false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
</script>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# BaseExportButton
|
|
2
|
+
|
|
3
|
+
`BaseExportButton` 是一个基础导出按钮组件,用来统一项目里的导出入口。
|
|
4
|
+
|
|
5
|
+
它支持两类场景:
|
|
6
|
+
|
|
7
|
+
- 纯前端 Excel 导出
|
|
8
|
+
- 调接口返回 blob 文件下载
|
|
9
|
+
|
|
10
|
+
## 使用方式
|
|
11
|
+
|
|
12
|
+
```vue
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { BaseExportButton } from '@hbdlzy/ui-core'
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<BaseExportButton :excel-options="excelOptions" />
|
|
19
|
+
</template>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 约束
|
|
23
|
+
|
|
24
|
+
- `excelOptions` 和 `requestHandler` 二选一
|
|
25
|
+
- 组件只负责触发导出,不负责业务数据查询
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import BaseExportButton from './BaseExportButton.vue'
|
|
2
|
+
|
|
3
|
+
export { BaseExportButton }
|
|
4
|
+
export {
|
|
5
|
+
downloadExportResponse,
|
|
6
|
+
extractFileNameFromDisposition,
|
|
7
|
+
normalizeExportFileName
|
|
8
|
+
} from './BaseExportButton.utils'
|
|
9
|
+
export type {
|
|
10
|
+
BaseExportExcelConfig,
|
|
11
|
+
BaseExportRequestConfig,
|
|
12
|
+
ExportBeforeHandler,
|
|
13
|
+
ExportMode,
|
|
14
|
+
ExportRequestHandler,
|
|
15
|
+
ExportResponseHeaders,
|
|
16
|
+
ExportResponseLike,
|
|
17
|
+
ExportSuccessPayload
|
|
18
|
+
} from './BaseExportButton.types'
|
|
19
|
+
|
|
20
|
+
export default BaseExportButton
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as XLSX from 'xlsx'
|
|
2
|
+
|
|
3
|
+
export type ExcelCellValue = string | number | boolean | null | undefined | Date
|
|
4
|
+
|
|
5
|
+
export interface ExcelExportColumn<Row = Record<string, any>> {
|
|
6
|
+
label: string
|
|
7
|
+
key?: string
|
|
8
|
+
width?: number
|
|
9
|
+
formatter?: (row: Row, rowIndex: number, column: ExcelExportColumn<Row>) => ExcelCellValue
|
|
10
|
+
children?: Array<ExcelExportColumn<Row>>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ExcelExportOptions<Row = Record<string, any>> {
|
|
14
|
+
fileName: string
|
|
15
|
+
sheetName?: string
|
|
16
|
+
columns: Array<ExcelExportColumn<Row>>
|
|
17
|
+
data: Array<Row>
|
|
18
|
+
autoWidth?: boolean
|
|
19
|
+
defaultColumnWidth?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_SHEET_NAME = 'Sheet1'
|
|
23
|
+
const DEFAULT_COLUMN_WIDTH = 16
|
|
24
|
+
|
|
25
|
+
function getLeafCount<Row>(column: ExcelExportColumn<Row>): number {
|
|
26
|
+
if (!column.children?.length) {
|
|
27
|
+
return 1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return column.children.reduce((count, child) => count + getLeafCount(child), 0)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getMaxDepth<Row>(columns: Array<ExcelExportColumn<Row>>, depth = 1): number {
|
|
34
|
+
return columns.reduce((maxDepth, column) => {
|
|
35
|
+
if (!column.children?.length) {
|
|
36
|
+
return Math.max(maxDepth, depth)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Math.max(maxDepth, getMaxDepth(column.children, depth + 1))
|
|
40
|
+
}, depth)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getDisplayLength(value: ExcelCellValue): number {
|
|
44
|
+
const text = `${value ?? ''}`
|
|
45
|
+
|
|
46
|
+
return Array.from(text).reduce((length, char) => {
|
|
47
|
+
return length + (char.charCodeAt(0) > 255 ? 2 : 1)
|
|
48
|
+
}, 0)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getValueByPath(source: Record<string, any>, path?: string): ExcelCellValue {
|
|
52
|
+
if (!path) {
|
|
53
|
+
return ''
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return path.split('.').reduce<any>((current, key) => {
|
|
57
|
+
if (current === null || current === undefined) {
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return current[key]
|
|
62
|
+
}, source)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getCellValue<Row>(row: Row, rowIndex: number, column: ExcelExportColumn<Row>): ExcelCellValue {
|
|
66
|
+
if (typeof column.formatter === 'function') {
|
|
67
|
+
return column.formatter(row, rowIndex, column)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return getValueByPath(row as Record<string, any>, column.key)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildHeaderMatrix<Row>(columns: Array<ExcelExportColumn<Row>>) {
|
|
74
|
+
const maxDepth = getMaxDepth(columns)
|
|
75
|
+
const totalLeafCount = columns.reduce((count, column) => count + getLeafCount(column), 0)
|
|
76
|
+
const headerRows = Array.from({ length: maxDepth }, () => Array.from({ length: totalLeafCount }).fill(''))
|
|
77
|
+
const merges: XLSX.Range[] = []
|
|
78
|
+
const leafColumns: Array<ExcelExportColumn<Row>> = []
|
|
79
|
+
|
|
80
|
+
let currentColumnIndex = 0
|
|
81
|
+
|
|
82
|
+
const fillHeaders = (column: ExcelExportColumn<Row>, depth: number) => {
|
|
83
|
+
const startColumnIndex = currentColumnIndex
|
|
84
|
+
const leafCount = getLeafCount(column)
|
|
85
|
+
|
|
86
|
+
headerRows[depth][startColumnIndex] = column.label
|
|
87
|
+
|
|
88
|
+
if (column.children?.length) {
|
|
89
|
+
column.children.forEach(child => fillHeaders(child, depth + 1))
|
|
90
|
+
|
|
91
|
+
if (leafCount > 1) {
|
|
92
|
+
merges.push({
|
|
93
|
+
s: { r: depth, c: startColumnIndex },
|
|
94
|
+
e: { r: depth, c: startColumnIndex + leafCount - 1 }
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
leafColumns.push(column)
|
|
102
|
+
currentColumnIndex += 1
|
|
103
|
+
|
|
104
|
+
if (depth < maxDepth - 1) {
|
|
105
|
+
merges.push({
|
|
106
|
+
s: { r: depth, c: startColumnIndex },
|
|
107
|
+
e: { r: maxDepth - 1, c: startColumnIndex }
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
columns.forEach(column => fillHeaders(column, 0))
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
headerRows,
|
|
116
|
+
leafColumns,
|
|
117
|
+
merges
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildColumnWidths<Row>(
|
|
122
|
+
leafColumns: Array<ExcelExportColumn<Row>>,
|
|
123
|
+
dataRows: Array<Array<ExcelCellValue>>,
|
|
124
|
+
defaultColumnWidth: number,
|
|
125
|
+
autoWidth: boolean
|
|
126
|
+
) {
|
|
127
|
+
return leafColumns.map((column, columnIndex) => {
|
|
128
|
+
const headerWidth = getDisplayLength(column.label)
|
|
129
|
+
const dataWidth = dataRows.reduce((maxWidth, row) => {
|
|
130
|
+
return Math.max(maxWidth, getDisplayLength(row[columnIndex]))
|
|
131
|
+
}, 0)
|
|
132
|
+
|
|
133
|
+
const targetWidth =
|
|
134
|
+
column.width ?? (autoWidth ? Math.max(defaultColumnWidth, headerWidth, dataWidth) : defaultColumnWidth)
|
|
135
|
+
|
|
136
|
+
return { wch: targetWidth }
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizeFileName(fileName: string) {
|
|
141
|
+
return fileName.endsWith('.xlsx') ? fileName : `${fileName}.xlsx`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function exportExcel<Row = Record<string, any>>(options: ExcelExportOptions<Row>) {
|
|
145
|
+
const {
|
|
146
|
+
fileName,
|
|
147
|
+
sheetName = DEFAULT_SHEET_NAME,
|
|
148
|
+
data,
|
|
149
|
+
autoWidth = true,
|
|
150
|
+
defaultColumnWidth = DEFAULT_COLUMN_WIDTH
|
|
151
|
+
} = options
|
|
152
|
+
const columns = options.columns.filter(column => !!column)
|
|
153
|
+
|
|
154
|
+
if (!columns.length) {
|
|
155
|
+
throw new Error('Excel export columns can not be empty.')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { headerRows, leafColumns, merges } = buildHeaderMatrix(columns)
|
|
159
|
+
const dataRows = data.map((row, rowIndex) => leafColumns.map(column => getCellValue(row, rowIndex, column)))
|
|
160
|
+
|
|
161
|
+
const worksheet = XLSX.utils.aoa_to_sheet([...headerRows, ...dataRows])
|
|
162
|
+
worksheet['!merges'] = merges
|
|
163
|
+
worksheet['!cols'] = buildColumnWidths(leafColumns, dataRows, defaultColumnWidth, autoWidth)
|
|
164
|
+
|
|
165
|
+
const workbook = XLSX.utils.book_new()
|
|
166
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
|
|
167
|
+
XLSX.writeFile(workbook, normalizeFileName(fileName))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default exportExcel
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as BaseExportButton } from './components/BaseExportButton'
|
|
2
|
+
export * from './components/BaseExportButton'
|
|
3
|
+
export { exportExcel } from './excel/exportExcel'
|
|
4
|
+
export type { ExcelCellValue, ExcelExportColumn, ExcelExportOptions } from './excel/exportExcel'
|