aegon-gen 1.0.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/package.json +12 -0
- package/src/App.vue +3 -0
- package/src/api/index.ts +19 -0
- package/src/api/modules/gen-ai/gen-entry/index.ts +30 -0
- package/src/api/modules/gen-ai/model-manager/index.ts +42 -0
- package/src/api/modules/gen-ai/model-manager/mockApi.ts +33 -0
- package/src/api/modules/index.ts +98 -0
- package/src/api/modules/user/index.ts +4 -0
- package/src/api/request.ts +102 -0
- package/src/assets/sample-access-icon.png +0 -0
- package/src/assets/sample-pie-chart.png +0 -0
- package/src/assets/vue.svg +1 -0
- package/src/components/CapsuleScrollbar.vue +93 -0
- package/src/components/Export/ExcelExport.vue +592 -0
- package/src/components/Export/ExcelExport2.vue +494 -0
- package/src/components/Export/ExcelExport3.vue +342 -0
- package/src/components/Export/ExcelExport4.vue +665 -0
- package/src/components/Export/excelExport.js +547 -0
- package/src/components/Export/excelExport.ts +551 -0
- package/src/components/GEN-AI/index.vue +142 -0
- package/src/components/GEN-AI/index1.vue +456 -0
- package/src/components/GEN-AI/index10.vue +5 -0
- package/src/components/GEN-AI/index2.vue +568 -0
- package/src/components/GEN-AI/index3.vue +623 -0
- package/src/components/GEN-AI/index4.vue +629 -0
- package/src/components/GEN-AI/index5.vue +578 -0
- package/src/components/GEN-AI/index6.vue +656 -0
- package/src/components/GEN-AI/index7.vue +717 -0
- package/src/components/GEN-AI/index8.vue +405 -0
- package/src/components/GEN-AI/index9.vue +1065 -0
- package/src/components/GEN-AI/types.ts +12 -0
- package/src/components/GEN-AI/utils.ts +42 -0
- package/src/components/HelloWorld.vue +41 -0
- package/src/components/PageCard.vue +7 -0
- package/src/components/PageHeader.vue +32 -0
- package/src/components/backup/index5 copy.vue +556 -0
- package/src/components/backup/index5.vue +620 -0
- package/src/components/backup/index9 copy.vue +1029 -0
- package/src/components/backup/index9-pro.vue +1065 -0
- package/src/components/backup/index9.vue +1057 -0
- package/src/components/el-date-picker.vue +64 -0
- package/src/directives/btnLoading.ts +427 -0
- package/src/directives/debounce copy.ts +670 -0
- package/src/directives/debounce.ts +98 -0
- package/src/directives/index.ts +25 -0
- package/src/layouts/MainLayout.vue +101 -0
- package/src/main.ts +85 -0
- package/src/router/index.ts +76 -0
- package/src/router/menus.ts +28 -0
- package/src/style.css +79 -0
- package/src/styles/_variables.scss +24 -0
- package/src/styles/app-button.css +26 -0
- package/src/styles/element-overrides.css +23 -0
- package/src/styles/global.css +44 -0
- package/src/styles/index.scss +1 -0
- package/src/styles/page-card.css +21 -0
- package/src/styles/variables.css +26 -0
- package/src/test/mock.ts +101 -0
- package/src/test/test1.vue +402 -0
- package/src/test/test2.vue +1689 -0
- package/src/types/gen-ai/gen-entry/index.ts +17 -0
- package/src/types/gen-ai/model-manager/index.ts +19 -0
- package/src/utils/docxExport.ts +1610 -0
- package/src/utils/gen-ai-navigation.ts +37 -0
- package/src/utils/gen-ai-scroll.ts +455 -0
- package/src/utils/openDataLoaderWordExport.ts +33 -0
- package/src/utils/pageScrollbar.ts +115 -0
- package/src/utils/randomTranscode.ts +87 -0
- package/src/utils/reportPdfExport.ts +44 -0
- package/src/views/AdminCenter/index.vue +817 -0
- package/src/views/Blank.vue +68 -0
- package/src/views/Home.vue +29 -0
- package/src/views/ReportCenter/index.vue +1380 -0
- package/src/views/TemplateCenter/Knowledge.ts +83 -0
- package/src/views/TemplateCenter/data.d.ts +10 -0
- package/src/views/TemplateCenter/index.vue +1205 -0
- package/src/views/TemplateCenter/service.ts +69 -0
- package/src/views/gen-ai/components/RecentReportsTable.vue +193 -0
- package/src/views/gen-ai/gen-entry/index.vue +309 -0
- package/src/views/gen-ai/gen-entry/mockData.ts +160 -0
- package/src/views/gen-ai/management-center/index.vue +53 -0
- package/src/views/gen-ai/model-manager/ChapterTitleScroll.vue +275 -0
- package/src/views/gen-ai/model-manager/index.vue +1205 -0
- package/src/views/gen-ai/model-manager/mockData.ts +122 -0
- package/src/views/gen-ai/report-center/index.vue +158 -0
- package/src/vite-env.d.ts +38 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
// excelExport.ts
|
|
2
|
+
import ExcelJS from "exceljs";
|
|
3
|
+
import type { CellValue, Worksheet, Cell } from "exceljs";
|
|
4
|
+
|
|
5
|
+
export interface ExportOptions {
|
|
6
|
+
excelName?: string;
|
|
7
|
+
sheetName?: string;
|
|
8
|
+
dateColumnName?: string;
|
|
9
|
+
groupColumnName?: string;
|
|
10
|
+
columnsToMerge?: string[];
|
|
11
|
+
rightAlignedColumns?: string[];
|
|
12
|
+
centerAlignedColumns?: string[];
|
|
13
|
+
leftAlignedColumns?: string[];
|
|
14
|
+
exportType?: 'single' | 'multi-sheet';
|
|
15
|
+
headerStyle?: {
|
|
16
|
+
font?: any;
|
|
17
|
+
fill?: any;
|
|
18
|
+
alignment?: any;
|
|
19
|
+
border?: any;
|
|
20
|
+
};
|
|
21
|
+
dataRowStyle?: {
|
|
22
|
+
font?: any;
|
|
23
|
+
alignment?: any;
|
|
24
|
+
border?: any;
|
|
25
|
+
};
|
|
26
|
+
columnWidthLimit?: number;
|
|
27
|
+
customFileName?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const getCellValueLength = (cellValue: CellValue | undefined | null): number => {
|
|
31
|
+
if (cellValue === null || cellValue === undefined) return 0;
|
|
32
|
+
let strValue = "";
|
|
33
|
+
if (typeof cellValue === "string") {
|
|
34
|
+
strValue = cellValue;
|
|
35
|
+
} else if (typeof cellValue === "number" || typeof cellValue === "boolean") {
|
|
36
|
+
strValue = cellValue.toString();
|
|
37
|
+
} else if (cellValue instanceof Date) {
|
|
38
|
+
strValue = cellValue.toISOString();
|
|
39
|
+
} else {
|
|
40
|
+
strValue = String(cellValue);
|
|
41
|
+
}
|
|
42
|
+
return strValue.length;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const isNumberValue = (value: any): boolean => {
|
|
46
|
+
if (value === null || value === undefined) return false;
|
|
47
|
+
if (typeof value === "number") return true;
|
|
48
|
+
if (typeof value !== "string") return false;
|
|
49
|
+
|
|
50
|
+
if (/[a-zA-Z\-_:]/.test(value)) return false;
|
|
51
|
+
|
|
52
|
+
const cleanedValue = value.replace(/[,$]/g, "").replace(/\s/g, "");
|
|
53
|
+
|
|
54
|
+
return /^-?\d+(\.\d+)?$/.test(cleanedValue);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isCodeValue = (value: any): boolean => {
|
|
58
|
+
if (value === null || value === undefined) return false;
|
|
59
|
+
if (typeof value !== "string") return false;
|
|
60
|
+
|
|
61
|
+
return /^\d{3}-[A-Z]\d-\d{8}-\d{3}$/.test(value) ||
|
|
62
|
+
/^[A-Za-z0-9\-_.:]+$/.test(value);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const shouldLeftAlign = (cellValue: any, columnName?: string, leftAlignedColumns: string[] = []): boolean => {
|
|
66
|
+
if (columnName && leftAlignedColumns.includes(columnName)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isCodeValue(cellValue)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof cellValue === "string" && !isNumberValue(cellValue)) {
|
|
75
|
+
const strValue = cellValue.toString();
|
|
76
|
+
if (strValue.includes("/") || strValue.includes("-") || strValue.includes(":")) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return false;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const groupDataByDate = (data: string[][], dateColumnName: string = ""): Record<string, string[][]> => {
|
|
85
|
+
if (data.length <= 1 || !dateColumnName) return {};
|
|
86
|
+
const header: string[] = data[0] || [];
|
|
87
|
+
const dateGroups: Record<string, string[][]> = {};
|
|
88
|
+
|
|
89
|
+
if (!header || !Array.isArray(header)) {
|
|
90
|
+
dateGroups["全部数据"] = data;
|
|
91
|
+
return dateGroups;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const dateColumnIndex = header.indexOf(dateColumnName);
|
|
95
|
+
if (dateColumnIndex === -1) {
|
|
96
|
+
dateGroups["全部数据"] = data;
|
|
97
|
+
return dateGroups;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (let i = 1; i < data.length; i++) {
|
|
101
|
+
const row: string[] = data[i] || [];
|
|
102
|
+
const date = row[dateColumnIndex];
|
|
103
|
+
if (!date) continue;
|
|
104
|
+
|
|
105
|
+
if (!dateGroups[date]) {
|
|
106
|
+
dateGroups[date] = [header];
|
|
107
|
+
}
|
|
108
|
+
dateGroups[date].push(row);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return dateGroups;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const configureWorksheetStyle = (
|
|
115
|
+
worksheet: Worksheet,
|
|
116
|
+
header: string[],
|
|
117
|
+
options: {
|
|
118
|
+
rightAlignedColumns?: string[];
|
|
119
|
+
centerAlignedColumns?: string[];
|
|
120
|
+
leftAlignedColumns?: string[];
|
|
121
|
+
headerStyle?: any;
|
|
122
|
+
dataRowStyle?: any;
|
|
123
|
+
columnWidthLimit?: number;
|
|
124
|
+
} = {}
|
|
125
|
+
): void => {
|
|
126
|
+
const {
|
|
127
|
+
rightAlignedColumns = [],
|
|
128
|
+
centerAlignedColumns = [],
|
|
129
|
+
leftAlignedColumns = [],
|
|
130
|
+
headerStyle = {},
|
|
131
|
+
dataRowStyle = {},
|
|
132
|
+
columnWidthLimit = 50
|
|
133
|
+
} = options;
|
|
134
|
+
|
|
135
|
+
if (!header || !Array.isArray(header)) return;
|
|
136
|
+
|
|
137
|
+
// 设置表头样式(修改:确保所有表头单元格都有边框)
|
|
138
|
+
const headerRow = worksheet.getRow(1);
|
|
139
|
+
if (headerRow) {
|
|
140
|
+
// 使用 eachCell 遍历所有有数据的表头单元格
|
|
141
|
+
headerRow.eachCell((cell: Cell) => {
|
|
142
|
+
cell.font = {
|
|
143
|
+
name: "Calibri",
|
|
144
|
+
size: 11,
|
|
145
|
+
bold: true,
|
|
146
|
+
color: { argb: "FF000000" },
|
|
147
|
+
...headerStyle.font
|
|
148
|
+
};
|
|
149
|
+
cell.fill = {
|
|
150
|
+
type: "pattern",
|
|
151
|
+
pattern: "solid",
|
|
152
|
+
fgColor: { argb: "FFFFFFFF" },
|
|
153
|
+
...headerStyle.fill
|
|
154
|
+
};
|
|
155
|
+
cell.alignment = {
|
|
156
|
+
vertical: "middle",
|
|
157
|
+
horizontal: "left",
|
|
158
|
+
wrapText: true,
|
|
159
|
+
...headerStyle.alignment
|
|
160
|
+
};
|
|
161
|
+
cell.border = {
|
|
162
|
+
top: { style: "thin", color: { argb: "FF000000" } },
|
|
163
|
+
left: { style: "thin", color: { argb: "FF000000" } },
|
|
164
|
+
bottom: { style: "thin", color: { argb: "FF000000" } },
|
|
165
|
+
right: { style: "thin", color: { argb: "FF000000" } },
|
|
166
|
+
...headerStyle.border
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// 确保所有列都有边框(包括空单元格)
|
|
171
|
+
for (let i = 1; i <= header.length; i++) {
|
|
172
|
+
const cell = headerRow.getCell(i);
|
|
173
|
+
if (!cell.border) {
|
|
174
|
+
cell.border = {
|
|
175
|
+
top: { style: "thin", color: { argb: "FF000000" } },
|
|
176
|
+
left: { style: "thin", color: { argb: "FF000000" } },
|
|
177
|
+
bottom: { style: "thin", color: { argb: "FF000000" } },
|
|
178
|
+
right: { style: "thin", color: { argb: "FF000000" } },
|
|
179
|
+
...headerStyle.border
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 修改:遍历所有行和列,确保所有单元格都有边框
|
|
186
|
+
const totalRows = worksheet.rowCount;
|
|
187
|
+
const totalCols = header.length;
|
|
188
|
+
|
|
189
|
+
for (let rowIndex = 1; rowIndex <= totalRows; rowIndex++) {
|
|
190
|
+
const row = worksheet.getRow(rowIndex);
|
|
191
|
+
|
|
192
|
+
if (!row) continue;
|
|
193
|
+
|
|
194
|
+
for (let colIndex = 1; colIndex <= totalCols; colIndex++) {
|
|
195
|
+
const cell = row.getCell(colIndex);
|
|
196
|
+
|
|
197
|
+
// 跳过表头行(已经设置过)
|
|
198
|
+
if (rowIndex === 1) continue;
|
|
199
|
+
|
|
200
|
+
// 修复:将 undefined 值转换为空字符串
|
|
201
|
+
if (cell.value === undefined) {
|
|
202
|
+
cell.value = '';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 为所有单元格设置字体
|
|
206
|
+
cell.font = {
|
|
207
|
+
name: "Calibri",
|
|
208
|
+
size: 11,
|
|
209
|
+
color: { argb: "FF000000" },
|
|
210
|
+
...dataRowStyle.font
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const columnName = colIndex <= header.length ? header[colIndex - 1] : "";
|
|
214
|
+
|
|
215
|
+
// 设置对齐方式
|
|
216
|
+
let horizontalAlignment = "left";
|
|
217
|
+
|
|
218
|
+
if (columnName && centerAlignedColumns.includes(columnName)) {
|
|
219
|
+
horizontalAlignment = "center";
|
|
220
|
+
}
|
|
221
|
+
else if (columnName && rightAlignedColumns.includes(columnName)) {
|
|
222
|
+
horizontalAlignment = "right";
|
|
223
|
+
}
|
|
224
|
+
else if (columnName && leftAlignedColumns.includes(columnName)) {
|
|
225
|
+
horizontalAlignment = "left";
|
|
226
|
+
}
|
|
227
|
+
else if (shouldLeftAlign(cell.value, columnName, leftAlignedColumns)) {
|
|
228
|
+
horizontalAlignment = "left";
|
|
229
|
+
}
|
|
230
|
+
else if (isNumberValue(cell.value)) {
|
|
231
|
+
horizontalAlignment = "right";
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
horizontalAlignment = "left";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
cell.alignment = {
|
|
238
|
+
vertical: "middle",
|
|
239
|
+
horizontal: horizontalAlignment,
|
|
240
|
+
wrapText: true,
|
|
241
|
+
...dataRowStyle.alignment
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// 确保所有数据单元格都有边框
|
|
245
|
+
cell.border = {
|
|
246
|
+
top: { style: "thin", color: { argb: "FF000000" } },
|
|
247
|
+
left: { style: "thin", color: { argb: "FF000000" } },
|
|
248
|
+
bottom: { style: "thin", color: { argb: "FF000000" } },
|
|
249
|
+
right: { style: "thin", color: { argb: "FF000000" } },
|
|
250
|
+
...dataRowStyle.border
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// 修复:防止数值自动转换为科学计数法 - 对数值列设置文本格式
|
|
254
|
+
if (cell.value !== null && cell.value !== undefined && cell.value !== '') {
|
|
255
|
+
const cellStr = String(cell.value);
|
|
256
|
+
// 检查是否是纯数字(无字母和特殊字符,除了-、$、,)
|
|
257
|
+
if (isNumberValue(cellStr) && cellStr.length > 10) {
|
|
258
|
+
// 对于较长的数字字符串,设置为文本格式避免科学计数法
|
|
259
|
+
cell.numFmt = '@';
|
|
260
|
+
// 确保值为字符串格式,避免Excel自动转换
|
|
261
|
+
if (typeof cell.value === 'number') {
|
|
262
|
+
cell.value = String(cell.value);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
worksheet.columns?.forEach((column) => {
|
|
270
|
+
let maxWidth = 10;
|
|
271
|
+
if (column.values && column.values.length > 0) {
|
|
272
|
+
maxWidth = column.values.reduce<number>((w: number, cellValue: CellValue) => {
|
|
273
|
+
const cellLength = getCellValueLength(cellValue);
|
|
274
|
+
return Math.max(w, cellLength);
|
|
275
|
+
}, 10);
|
|
276
|
+
}
|
|
277
|
+
column.width = Math.min(maxWidth + 2, columnWidthLimit);
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const groupDataByColumn = (data: any[][], groupColumnName: string = ""): any[] => {
|
|
282
|
+
const groups: any[] = [];
|
|
283
|
+
let currentGroup: any = null;
|
|
284
|
+
const groupColumnIndex = groupColumnName ? data[0]?.indexOf(groupColumnName) : 0;
|
|
285
|
+
|
|
286
|
+
data.forEach((row, index) => {
|
|
287
|
+
const groupValue = row[groupColumnIndex || 0];
|
|
288
|
+
if (!currentGroup || currentGroup.groupValue !== groupValue) {
|
|
289
|
+
if (currentGroup) {
|
|
290
|
+
currentGroup.endRow = index - 1;
|
|
291
|
+
groups.push(currentGroup);
|
|
292
|
+
}
|
|
293
|
+
currentGroup = {
|
|
294
|
+
groupValue,
|
|
295
|
+
startRow: index,
|
|
296
|
+
endRow: index,
|
|
297
|
+
rows: [row],
|
|
298
|
+
};
|
|
299
|
+
} else {
|
|
300
|
+
currentGroup.endRow = index;
|
|
301
|
+
currentGroup.rows.push(row);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (currentGroup) {
|
|
306
|
+
groups.push(currentGroup);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return groups;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const mergeCellsByGroup = (
|
|
313
|
+
worksheet: Worksheet,
|
|
314
|
+
data: any[][],
|
|
315
|
+
columnsToMerge: string[],
|
|
316
|
+
headers: string[]
|
|
317
|
+
): void => {
|
|
318
|
+
if (!data || data.length === 0 || !columnsToMerge || columnsToMerge.length === 0) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const groups = groupDataByColumn(data);
|
|
323
|
+
const mergedRanges = new Set<string>();
|
|
324
|
+
|
|
325
|
+
groups.forEach((group) => {
|
|
326
|
+
if (group.startRow === group.endRow) return;
|
|
327
|
+
|
|
328
|
+
columnsToMerge.forEach((columnName) => {
|
|
329
|
+
const columnIndexes: number[] = [];
|
|
330
|
+
headers.forEach((header, index) => {
|
|
331
|
+
if (header === columnName) {
|
|
332
|
+
columnIndexes.push(index);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
columnIndexes.forEach((columnIndex) => {
|
|
337
|
+
if (columnIndex === -1) return;
|
|
338
|
+
const startRow = group.startRow + 2;
|
|
339
|
+
const endRow = group.endRow + 2;
|
|
340
|
+
const rangeKey = `${startRow}-${endRow}-${columnIndex + 1}`;
|
|
341
|
+
|
|
342
|
+
if (mergedRanges.has(rangeKey)) return;
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
worksheet.mergeCells(startRow, columnIndex + 1, endRow, columnIndex + 1);
|
|
346
|
+
mergedRanges.add(rangeKey);
|
|
347
|
+
|
|
348
|
+
let mergedValue = "";
|
|
349
|
+
if (group.rows && group.rows.length > 0) {
|
|
350
|
+
for (let i = 0; i < group.rows.length; i++) {
|
|
351
|
+
const rowData = group.rows[i];
|
|
352
|
+
if (rowData && rowData[columnIndex] !== undefined) {
|
|
353
|
+
const value = rowData[columnIndex];
|
|
354
|
+
if (
|
|
355
|
+
value !== null &&
|
|
356
|
+
value !== undefined &&
|
|
357
|
+
String(value).trim() !== ""
|
|
358
|
+
) {
|
|
359
|
+
mergedValue = String(value);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const cell = worksheet.getCell(startRow, columnIndex + 1);
|
|
367
|
+
cell.value = mergedValue;
|
|
368
|
+
|
|
369
|
+
// 修复:如果合并的值是长数字,确保它不被转换为科学计数法
|
|
370
|
+
if (mergedValue && isNumberValue(mergedValue) && mergedValue.length > 10) {
|
|
371
|
+
cell.numFmt = '@';
|
|
372
|
+
}
|
|
373
|
+
} catch (error: any) {
|
|
374
|
+
console.warn(`合并单元格失败: ${error.message}`, {
|
|
375
|
+
startRow,
|
|
376
|
+
columnIndex: columnIndex + 1,
|
|
377
|
+
endRow,
|
|
378
|
+
columnName,
|
|
379
|
+
groupValue: group.groupValue,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
export const getDateRangeString = (data: string[][], dateColumnName: string = ""): string => {
|
|
388
|
+
if (data.length <= 1 || !dateColumnName) return "";
|
|
389
|
+
|
|
390
|
+
const header: string[] = data[0] || [];
|
|
391
|
+
const dateColumnIndex = header.indexOf(dateColumnName);
|
|
392
|
+
|
|
393
|
+
if (dateColumnIndex === -1) return "";
|
|
394
|
+
|
|
395
|
+
const dates: string[] = [];
|
|
396
|
+
for (let i = 1; i < data.length; i++) {
|
|
397
|
+
const row: string[] = data[i] || [];
|
|
398
|
+
const date = row[dateColumnIndex];
|
|
399
|
+
if (date && !dates.includes(date)) {
|
|
400
|
+
dates.push(date);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (dates.length === 0) return "";
|
|
405
|
+
dates.sort();
|
|
406
|
+
|
|
407
|
+
if (dates.length === 1) {
|
|
408
|
+
return dates[0] as any;
|
|
409
|
+
} else {
|
|
410
|
+
return `${dates[0]}~${dates[dates.length - 1]}`;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
export const guiExportToExcel = async (
|
|
415
|
+
data: string[][],
|
|
416
|
+
options: ExportOptions = {}
|
|
417
|
+
): Promise<void> => {
|
|
418
|
+
const {
|
|
419
|
+
excelName = "", // 修改:默认值设置为空字符串
|
|
420
|
+
sheetName = "", // 修改:默认值设置为空字符串
|
|
421
|
+
dateColumnName = "",
|
|
422
|
+
groupColumnName = "",
|
|
423
|
+
columnsToMerge = [],
|
|
424
|
+
rightAlignedColumns = [],
|
|
425
|
+
centerAlignedColumns = [],
|
|
426
|
+
leftAlignedColumns = [],
|
|
427
|
+
exportType = "single",
|
|
428
|
+
headerStyle = {},
|
|
429
|
+
dataRowStyle = {},
|
|
430
|
+
columnWidthLimit = 50,
|
|
431
|
+
customFileName = ""
|
|
432
|
+
} = options;
|
|
433
|
+
|
|
434
|
+
const workbook = new ExcelJS.Workbook();
|
|
435
|
+
|
|
436
|
+
// 处理数据,将 undefined 值替换为空字符串
|
|
437
|
+
const finalData = data.map((row, rowIndex) => {
|
|
438
|
+
if (rowIndex === 0) return row; // 表头行不变
|
|
439
|
+
|
|
440
|
+
return row.map(cell => {
|
|
441
|
+
// 将 undefined 值转换为空字符串
|
|
442
|
+
if (cell === undefined || cell === null) {
|
|
443
|
+
return '';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const cellStr = String(cell);
|
|
447
|
+
|
|
448
|
+
// 修复:检查是否是可能被转换为科学计数法的长数字
|
|
449
|
+
if (isNumberValue(cellStr) && cellStr.length > 10) {
|
|
450
|
+
// 在 configureWorksheetStyle 中会为这些单元格设置文本格式
|
|
451
|
+
return cellStr;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return cellStr;
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
if (exportType === "multi-sheet" && dateColumnName) {
|
|
459
|
+
const dateGroups = groupDataByDate(finalData, dateColumnName);
|
|
460
|
+
|
|
461
|
+
if (Object.keys(dateGroups).length === 0) {
|
|
462
|
+
const worksheet = workbook.addWorksheet(sheetName || "数据");
|
|
463
|
+
const header = finalData[0] || [];
|
|
464
|
+
worksheet.addRows(finalData.length > 0 ? finalData : [header]);
|
|
465
|
+
configureWorksheetStyle(worksheet, header, {
|
|
466
|
+
rightAlignedColumns,
|
|
467
|
+
centerAlignedColumns,
|
|
468
|
+
leftAlignedColumns,
|
|
469
|
+
headerStyle,
|
|
470
|
+
dataRowStyle,
|
|
471
|
+
columnWidthLimit
|
|
472
|
+
});
|
|
473
|
+
} else {
|
|
474
|
+
Object.keys(dateGroups).forEach((date) => {
|
|
475
|
+
const dateData = dateGroups[date] || [];
|
|
476
|
+
let sheetName = date;
|
|
477
|
+
if (sheetName === "全部数据") {
|
|
478
|
+
sheetName = "汇总";
|
|
479
|
+
} else {
|
|
480
|
+
sheetName = sheetName.replace(/[\/\\\*\?\[\]:]/g, "-");
|
|
481
|
+
if (sheetName.length > 31) {
|
|
482
|
+
sheetName = sheetName.substring(0, 31);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const worksheet = workbook.addWorksheet(sheetName);
|
|
487
|
+
if (dateData.length > 0) {
|
|
488
|
+
worksheet.addRows(dateData);
|
|
489
|
+
const header = dateData[0] || [];
|
|
490
|
+
configureWorksheetStyle(worksheet, header, {
|
|
491
|
+
rightAlignedColumns,
|
|
492
|
+
centerAlignedColumns,
|
|
493
|
+
leftAlignedColumns,
|
|
494
|
+
headerStyle,
|
|
495
|
+
dataRowStyle,
|
|
496
|
+
columnWidthLimit
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
if (columnsToMerge.length > 0 && groupColumnName) {
|
|
500
|
+
const dataWithoutHeader = dateData.slice(1);
|
|
501
|
+
mergeCellsByGroup(worksheet, dataWithoutHeader, columnsToMerge, header);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
const worksheet = workbook.addWorksheet(sheetName || "数据");
|
|
508
|
+
const header = finalData[0] || [];
|
|
509
|
+
worksheet.addRows(finalData);
|
|
510
|
+
configureWorksheetStyle(worksheet, header, {
|
|
511
|
+
rightAlignedColumns,
|
|
512
|
+
centerAlignedColumns,
|
|
513
|
+
leftAlignedColumns,
|
|
514
|
+
headerStyle,
|
|
515
|
+
dataRowStyle,
|
|
516
|
+
columnWidthLimit
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
if (columnsToMerge.length > 0 && groupColumnName) {
|
|
520
|
+
const dataWithoutHeader = finalData.slice(1);
|
|
521
|
+
mergeCellsByGroup(worksheet, dataWithoutHeader, columnsToMerge, header);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
const buffer = await workbook.xlsx.writeBuffer();
|
|
527
|
+
const blob = new Blob([buffer], {
|
|
528
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
529
|
+
});
|
|
530
|
+
const url = URL.createObjectURL(blob);
|
|
531
|
+
const a = document.createElement("a");
|
|
532
|
+
|
|
533
|
+
let fileName = customFileName || excelName;
|
|
534
|
+
if (exportType === "multi-sheet" && dateColumnName) {
|
|
535
|
+
const dateRange = getDateRangeString(finalData, dateColumnName);
|
|
536
|
+
if (dateRange) {
|
|
537
|
+
fileName = `${excelName}${excelName && dateRange ? '_' : ''}${dateRange}`;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
a.href = url;
|
|
542
|
+
a.download = `${fileName || "导出数据"}.xlsx`; // 添加默认文件名
|
|
543
|
+
document.body.appendChild(a);
|
|
544
|
+
a.click();
|
|
545
|
+
document.body.removeChild(a);
|
|
546
|
+
URL.revokeObjectURL(url);
|
|
547
|
+
return Promise.resolve();
|
|
548
|
+
} catch (error) {
|
|
549
|
+
return Promise.reject(error);
|
|
550
|
+
}
|
|
551
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, reactive, computed, onMounted } from "vue";
|
|
3
|
+
import { Edit3Icon } from "lucide-vue-next";
|
|
4
|
+
import { marked } from "marked";
|
|
5
|
+
import { exportToPDF, exportToWord } from "./utils";
|
|
6
|
+
|
|
7
|
+
defineOptions({
|
|
8
|
+
name: "GenAI",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// 1. 状态管理
|
|
12
|
+
const isTyping = ref(false);
|
|
13
|
+
const displayedText = ref("");
|
|
14
|
+
const report = reactive({
|
|
15
|
+
title: "行業風險年報 | 房地產 | 2024",
|
|
16
|
+
// 按照 UI 图精确还原的文本内容
|
|
17
|
+
rawContent: `2024年香港房地產市場整體呈現「價跌量穩、租金偏強、商業不振」的分化局面,全年各類物業價格普遍下調,但成交及租賃需求在政策放寬和人口流入支持下較前兩年有所改善。住宅方面,撤銷所有樓市「辣招」、放寬按揭成數及利率見頂後回落,帶動一手主導的成交回升,但無礙樓價全年再跌約中個位百分比,反映經濟及供應壓力仍然主導價格走勢。
|
|
18
|
+
在住宅市場,本年特點包括:
|
|
19
|
+
一、銷售量回升、價格續調:2024年在全面撤辣及按揭寬鬆後,全年住宅買賣宗數按年錄得逾兩成增長,但全年樓價仍錄得約6%至7%的跌幅,延續過去兩年的調整趨勢。
|
|
20
|
+
二、供應顯著增加:私樓落成量約2.4萬個單位,較2023年大幅增加約七成,主要為中小型單位,為後市價格帶來持續壓力。
|
|
21
|
+
三、租金走勢優於樓價:在人口及人才計劃帶動下,住宅租金全年錄得中低個位升幅,租金指數距歷史高位僅約數個百分點,令小型單位的平均回報率升至近十多年高位水平。
|
|
22
|
+
在非住宅方面,2024年為商廈及舖位市場的「谷底年」,寫字樓空置率創逾十年新高,商業成交金額及宗數均跌至有紀錄以來低位。`,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// 2. 打字机效果逻辑 - 0.5s 一个字
|
|
26
|
+
const startTypewriter = async () => {
|
|
27
|
+
if (isTyping.value) return;
|
|
28
|
+
displayedText.value = "";
|
|
29
|
+
isTyping.value = true;
|
|
30
|
+
|
|
31
|
+
const chars = report.rawContent.split("");
|
|
32
|
+
for (const char of chars) {
|
|
33
|
+
displayedText.value += char;
|
|
34
|
+
// 间隔 0.5s 一个字
|
|
35
|
+
await new Promise((res) => setTimeout(res, 30));
|
|
36
|
+
}
|
|
37
|
+
isTyping.value = false;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// 3. Markdown 转换
|
|
41
|
+
const renderedMarkdown = computed(() => marked.parse(displayedText.value));
|
|
42
|
+
|
|
43
|
+
onMounted(() => {
|
|
44
|
+
startTypewriter();
|
|
45
|
+
});
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div class="flex h-screen bg-[#F3F4F6] text-[#333]">
|
|
50
|
+
<main class="flex-1 overflow-y-auto p-12">
|
|
51
|
+
<div
|
|
52
|
+
class="max-w-5xl mx-auto bg-white shadow-sm border border-gray-100 rounded-sm p-16 min-h-screen relative"
|
|
53
|
+
id="report-container"
|
|
54
|
+
>
|
|
55
|
+
<div class="absolute top-8 right-8 flex gap-4 no-print">
|
|
56
|
+
<button
|
|
57
|
+
@click="exportToPDF('report-container', report.title)"
|
|
58
|
+
style="padding-right: 5px"
|
|
59
|
+
class="flex items-center border border-[#C54E5E] text-[#C54E5E] px-6 py-2 rounded-md hover:bg-red-50 transition-colors text-sm font-medium"
|
|
60
|
+
>
|
|
61
|
+
匯出為PDF
|
|
62
|
+
</button>
|
|
63
|
+
<button
|
|
64
|
+
@click="exportToWord('report-container', report.title)"
|
|
65
|
+
class="flex items-center border border-[#C54E5E] text-[#C54E5E] px-6 py-2 rounded-md hover:bg-red-50 transition-colors text-sm font-medium"
|
|
66
|
+
>
|
|
67
|
+
匯出為Word
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="mb-10 group relative">
|
|
72
|
+
<div class="flex items-center justify-between">
|
|
73
|
+
<h1 class="text-[22px] font-bold text-[#333]">
|
|
74
|
+
報告標題:{{ report.title }}
|
|
75
|
+
</h1>
|
|
76
|
+
<!-- <Edit3Icon
|
|
77
|
+
:size="20"
|
|
78
|
+
class="text-[#C54E5E] cursor-pointer opacity-80"
|
|
79
|
+
/> -->
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="prose prose-slate max-w-none">
|
|
84
|
+
<div class="font-bold flex items-center mb-6 text-lg border-t pt-8">
|
|
85
|
+
正文:
|
|
86
|
+
<Edit3Icon :size="18" class="ml-2 text-[#C54E5E] cursor-pointer" />
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="content-wrapper">
|
|
90
|
+
<div
|
|
91
|
+
v-html="renderedMarkdown"
|
|
92
|
+
class="markdown-body inline text-[#444] leading-[2] text-[15px]"
|
|
93
|
+
></div>
|
|
94
|
+
<span
|
|
95
|
+
v-if="isTyping"
|
|
96
|
+
class="inline-block w-[2px] h-[18px] bg-[#C54E5E] animate-pulse ml-1 align-middle"
|
|
97
|
+
></span>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</main>
|
|
102
|
+
|
|
103
|
+
<aside class="w-80 bg-white border-l border-gray-200 p-6 no-print">
|
|
104
|
+
<h3 class="font-bold text-gray-700 mb-6">生成報告設置</h3>
|
|
105
|
+
</aside>
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
108
|
+
|
|
109
|
+
<style scoped>
|
|
110
|
+
/* 1:1 还原设计图的 Markdown 样式 */
|
|
111
|
+
.markdown-body :deep(p) {
|
|
112
|
+
line-height: 2;
|
|
113
|
+
margin-bottom: 1.5rem;
|
|
114
|
+
letter-spacing: 0.02em;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.markdown-body :deep(h1),
|
|
118
|
+
.markdown-body :deep(h2),
|
|
119
|
+
.markdown-body :deep(h3) {
|
|
120
|
+
color: #333;
|
|
121
|
+
margin-top: 1.5rem;
|
|
122
|
+
margin-bottom: 1rem;
|
|
123
|
+
font-weight: 700;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.content-wrapper {
|
|
127
|
+
position: relative;
|
|
128
|
+
white-space: pre-wrap;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* 打印优化 */
|
|
132
|
+
@media print {
|
|
133
|
+
.no-print {
|
|
134
|
+
display: none !important;
|
|
135
|
+
}
|
|
136
|
+
#report-container {
|
|
137
|
+
box-shadow: none !important;
|
|
138
|
+
border: none !important;
|
|
139
|
+
padding: 0 !important;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
</style>
|