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.
Files changed (86) hide show
  1. package/package.json +12 -0
  2. package/src/App.vue +3 -0
  3. package/src/api/index.ts +19 -0
  4. package/src/api/modules/gen-ai/gen-entry/index.ts +30 -0
  5. package/src/api/modules/gen-ai/model-manager/index.ts +42 -0
  6. package/src/api/modules/gen-ai/model-manager/mockApi.ts +33 -0
  7. package/src/api/modules/index.ts +98 -0
  8. package/src/api/modules/user/index.ts +4 -0
  9. package/src/api/request.ts +102 -0
  10. package/src/assets/sample-access-icon.png +0 -0
  11. package/src/assets/sample-pie-chart.png +0 -0
  12. package/src/assets/vue.svg +1 -0
  13. package/src/components/CapsuleScrollbar.vue +93 -0
  14. package/src/components/Export/ExcelExport.vue +592 -0
  15. package/src/components/Export/ExcelExport2.vue +494 -0
  16. package/src/components/Export/ExcelExport3.vue +342 -0
  17. package/src/components/Export/ExcelExport4.vue +665 -0
  18. package/src/components/Export/excelExport.js +547 -0
  19. package/src/components/Export/excelExport.ts +551 -0
  20. package/src/components/GEN-AI/index.vue +142 -0
  21. package/src/components/GEN-AI/index1.vue +456 -0
  22. package/src/components/GEN-AI/index10.vue +5 -0
  23. package/src/components/GEN-AI/index2.vue +568 -0
  24. package/src/components/GEN-AI/index3.vue +623 -0
  25. package/src/components/GEN-AI/index4.vue +629 -0
  26. package/src/components/GEN-AI/index5.vue +578 -0
  27. package/src/components/GEN-AI/index6.vue +656 -0
  28. package/src/components/GEN-AI/index7.vue +717 -0
  29. package/src/components/GEN-AI/index8.vue +405 -0
  30. package/src/components/GEN-AI/index9.vue +1065 -0
  31. package/src/components/GEN-AI/types.ts +12 -0
  32. package/src/components/GEN-AI/utils.ts +42 -0
  33. package/src/components/HelloWorld.vue +41 -0
  34. package/src/components/PageCard.vue +7 -0
  35. package/src/components/PageHeader.vue +32 -0
  36. package/src/components/backup/index5 copy.vue +556 -0
  37. package/src/components/backup/index5.vue +620 -0
  38. package/src/components/backup/index9 copy.vue +1029 -0
  39. package/src/components/backup/index9-pro.vue +1065 -0
  40. package/src/components/backup/index9.vue +1057 -0
  41. package/src/components/el-date-picker.vue +64 -0
  42. package/src/directives/btnLoading.ts +427 -0
  43. package/src/directives/debounce copy.ts +670 -0
  44. package/src/directives/debounce.ts +98 -0
  45. package/src/directives/index.ts +25 -0
  46. package/src/layouts/MainLayout.vue +101 -0
  47. package/src/main.ts +85 -0
  48. package/src/router/index.ts +76 -0
  49. package/src/router/menus.ts +28 -0
  50. package/src/style.css +79 -0
  51. package/src/styles/_variables.scss +24 -0
  52. package/src/styles/app-button.css +26 -0
  53. package/src/styles/element-overrides.css +23 -0
  54. package/src/styles/global.css +44 -0
  55. package/src/styles/index.scss +1 -0
  56. package/src/styles/page-card.css +21 -0
  57. package/src/styles/variables.css +26 -0
  58. package/src/test/mock.ts +101 -0
  59. package/src/test/test1.vue +402 -0
  60. package/src/test/test2.vue +1689 -0
  61. package/src/types/gen-ai/gen-entry/index.ts +17 -0
  62. package/src/types/gen-ai/model-manager/index.ts +19 -0
  63. package/src/utils/docxExport.ts +1610 -0
  64. package/src/utils/gen-ai-navigation.ts +37 -0
  65. package/src/utils/gen-ai-scroll.ts +455 -0
  66. package/src/utils/openDataLoaderWordExport.ts +33 -0
  67. package/src/utils/pageScrollbar.ts +115 -0
  68. package/src/utils/randomTranscode.ts +87 -0
  69. package/src/utils/reportPdfExport.ts +44 -0
  70. package/src/views/AdminCenter/index.vue +817 -0
  71. package/src/views/Blank.vue +68 -0
  72. package/src/views/Home.vue +29 -0
  73. package/src/views/ReportCenter/index.vue +1380 -0
  74. package/src/views/TemplateCenter/Knowledge.ts +83 -0
  75. package/src/views/TemplateCenter/data.d.ts +10 -0
  76. package/src/views/TemplateCenter/index.vue +1205 -0
  77. package/src/views/TemplateCenter/service.ts +69 -0
  78. package/src/views/gen-ai/components/RecentReportsTable.vue +193 -0
  79. package/src/views/gen-ai/gen-entry/index.vue +309 -0
  80. package/src/views/gen-ai/gen-entry/mockData.ts +160 -0
  81. package/src/views/gen-ai/management-center/index.vue +53 -0
  82. package/src/views/gen-ai/model-manager/ChapterTitleScroll.vue +275 -0
  83. package/src/views/gen-ai/model-manager/index.vue +1205 -0
  84. package/src/views/gen-ai/model-manager/mockData.ts +122 -0
  85. package/src/views/gen-ai/report-center/index.vue +158 -0
  86. package/src/vite-env.d.ts +38 -0
@@ -0,0 +1,623 @@
1
+ <template>
2
+ <div class="flex h-screen bg-[#F3F4F6] text-[#333]">
3
+ <main class="flex-1 overflow-y-auto p-12">
4
+ <div
5
+ class="max-w-5xl mx-auto bg-white shadow-sm border border-gray-100 rounded-sm p-10 min-h-screen relative"
6
+ id="report-container"
7
+ >
8
+ <!-- 导出按钮 -->
9
+ <div class="absolute top-6 right-8 flex gap-4 no-print">
10
+ <el-button
11
+ @click="handleExportPDF"
12
+ size="small"
13
+ plain
14
+ type="danger"
15
+ :loading="isExporting"
16
+ >
17
+ 匯出為PDF
18
+ </el-button>
19
+ <el-button
20
+ @click="handleExportWord"
21
+ size="small"
22
+ plain
23
+ type="danger"
24
+ :loading="isExporting"
25
+ >
26
+ 匯出為Word
27
+ </el-button>
28
+ </div>
29
+
30
+ <div class="prose prose-slate max-w-none">
31
+ <div class="font-bold flex items-center mb-5 text-lg border-t pt-6">
32
+ 正文:
33
+ <Edit3Icon :size="18" class="ml-2 text-[#C54E5E] cursor-pointer" />
34
+ </div>
35
+
36
+ <!-- 按顺序展示每个项目 -->
37
+ <div class="content-wrapper space-y-4">
38
+ <template v-for="(item, idx) in displaySequence" :key="idx">
39
+ <!-- 文本段落:使用独立打字机组件,仅当索引小于 activeIndex 时才渲染 -->
40
+ <TypewriterText
41
+ v-if="item.type === 'text' && idx < activeIndex"
42
+ :text="item.content!"
43
+ :auto-start="
44
+ idx === activeIndex - 1 && idx === lastStartedTextIndex
45
+ "
46
+ @completed="onTextCompleted(idx)"
47
+ class="text-paragraph"
48
+ />
49
+ <!-- 普通组件:同样在索引小于 activeIndex 时渲染 -->
50
+ <component
51
+ v-else-if="item.type === 'component' && idx < activeIndex"
52
+ :is="item.component"
53
+ class="my-4"
54
+ />
55
+ </template>
56
+ </div>
57
+
58
+ <!-- 全局打字光标(仅当还有未显示的项目时显示) -->
59
+ <span
60
+ v-if="activeIndex < displaySequence.length"
61
+ class="inline-block w-[2px] h-[18px] bg-[#C54E5E] animate-pulse ml-1 align-middle"
62
+ ></span>
63
+ </div>
64
+ </div>
65
+ </main>
66
+
67
+ <aside class="w-80 bg-white border-l border-gray-200 p-6 no-print">
68
+ <h3 class="font-bold text-gray-700 mb-6">生成報告設置</h3>
69
+ <p class="text-sm text-gray-500">✨ ECharts 折柱图 + 打字机效果</p>
70
+ <p class="text-xs text-gray-400 mt-2">
71
+ 包含:带状态表格 / 按钮组 / 图标集 / ECharts混合图表<br />
72
+ 房地产报告全文 + 逐字打字效果
73
+ </p>
74
+ <el-button @click="restartDemo" type="danger" plain class="w-full mt-6">
75
+ 重新播放
76
+ </el-button>
77
+ <div class="mt-6 text-xs text-gray-400 border-t pt-4">
78
+ 💡 提示:<br />
79
+ • 表格行按状态高亮<br />
80
+ • ECharts 图表支持缩放/保存/切换类型<br />
81
+ • 图标、表格、图表会随打字顺序逐步出现
82
+ </div>
83
+ </aside>
84
+ </div>
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ import {
89
+ ref,
90
+ onMounted,
91
+ onUnmounted,
92
+ defineComponent,
93
+ h,
94
+ markRaw,
95
+ type Component,
96
+ nextTick,
97
+ } from "vue";
98
+ import { Edit3Icon } from "lucide-vue-next";
99
+ import { ElButton, ElTable, ElTableColumn, ElIcon } from "element-plus";
100
+ import { Platform, Eleme, DeleteFilled, Delete } from "@element-plus/icons-vue";
101
+ import * as echarts from "echarts";
102
+ import html2canvas from "html2canvas";
103
+ import jsPDF from "jspdf";
104
+ import { saveAs } from "file-saver";
105
+
106
+ // ---------- 导出工具函数(修复版)----------
107
+ const isExporting = ref(false);
108
+
109
+ // 等待所有内容完全显示(若未完成则自动完成)
110
+ const waitForFullDisplay = async () => {
111
+ if (activeIndex.value < displaySequence.value.length) {
112
+ // 强制完成所有项目:将 activeIndex 设为末尾,并等待下一轮渲染
113
+ activeIndex.value = displaySequence.value.length;
114
+ // 等待 Vue 更新 DOM 和 ECharts 图表渲染
115
+ await nextTick();
116
+ // 给 ECharts 额外时间渲染(如果有图表组件)
117
+ await new Promise((resolve) => setTimeout(resolve, 500));
118
+ }
119
+ };
120
+
121
+ const exportToPDF = async (containerId: string, filename: string) => {
122
+ const element = document.getElementById(containerId);
123
+ if (!element) {
124
+ console.error("Container not found");
125
+ return;
126
+ }
127
+
128
+ try {
129
+ isExporting.value = true;
130
+ // 确保所有内容已完全显示
131
+ await waitForFullDisplay();
132
+
133
+ const loadingEl = document.createElement("div");
134
+ loadingEl.innerText = "正在生成PDF,请稍候...";
135
+ loadingEl.style.cssText = `
136
+ position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
137
+ background: rgba(0,0,0,0.7); color: white; padding: 10px 20px;
138
+ border-radius: 8px; z-index: 9999; font-size: 14px;
139
+ `;
140
+ document.body.appendChild(loadingEl);
141
+
142
+ // 临时移除可能干扰导出的元素(如动画光标)
143
+ const cursor = document.querySelector(".animate-pulse");
144
+ if (cursor) (cursor as HTMLElement).style.display = "none";
145
+
146
+ const canvas = await html2canvas(element, {
147
+ scale: 2,
148
+ useCORS: true,
149
+ logging: false,
150
+ backgroundColor: "#ffffff",
151
+ onclone: (clonedDoc, _element) => {
152
+ // 在克隆文档中确保图表样式正确
153
+ const charts = clonedDoc.querySelectorAll(".echarts-container");
154
+ charts.forEach((chart) => {
155
+ (chart as HTMLElement).style.height = "400px";
156
+ });
157
+ },
158
+ });
159
+ const imgData = canvas.toDataURL("image/png");
160
+ const pdf = new jsPDF("p", "mm", "a4");
161
+ const imgWidth = 210;
162
+ const pageHeight = 297;
163
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
164
+ let heightLeft = imgHeight;
165
+ let position = 0;
166
+
167
+ pdf.addImage(imgData, "PNG", 0, position, imgWidth, imgHeight);
168
+ heightLeft -= pageHeight;
169
+
170
+ while (heightLeft > 0) {
171
+ position = heightLeft - imgHeight;
172
+ pdf.addPage();
173
+ pdf.addImage(imgData, "PNG", 0, position, imgWidth, imgHeight);
174
+ heightLeft -= pageHeight;
175
+ }
176
+ pdf.save(`${filename}.pdf`);
177
+ document.body.removeChild(loadingEl);
178
+ if (cursor) (cursor as HTMLElement).style.display = "";
179
+ } catch (error) {
180
+ console.error("PDF导出失败:", error);
181
+ alert("PDF导出失败,请重试");
182
+ } finally {
183
+ isExporting.value = false;
184
+ }
185
+ };
186
+
187
+ const exportToWord = async (containerId: string, filename: string) => {
188
+ const element = document.getElementById(containerId);
189
+ if (!element) {
190
+ console.error("Container not found");
191
+ return;
192
+ }
193
+
194
+ try {
195
+ isExporting.value = true;
196
+ await waitForFullDisplay();
197
+
198
+ const cloneElement = element.cloneNode(true) as HTMLElement;
199
+ // 移除所有按钮和交互元素(包括导出按钮、侧边栏等)
200
+ const noPrintElements = cloneElement.querySelectorAll(".no-print");
201
+ noPrintElements.forEach((el) => el.remove());
202
+ // 移除打字光标
203
+ const cursors = cloneElement.querySelectorAll(".animate-pulse");
204
+ cursors.forEach((el) => el.remove());
205
+
206
+ // 获取样式
207
+ const styles = document.querySelectorAll("style");
208
+ let styleHtml = "";
209
+ styles.forEach((style) => {
210
+ styleHtml += style.innerHTML;
211
+ });
212
+
213
+ const docHtml = `
214
+ <!DOCTYPE html>
215
+ <html>
216
+ <head>
217
+ <meta charset="UTF-8">
218
+ <title>${filename}</title>
219
+ <style>
220
+ body {
221
+ font-family: 'Segoe UI', 'Roboto', 'Noto Sans', sans-serif;
222
+ padding: 20px;
223
+ margin: 0 auto;
224
+ max-width: 1000px;
225
+ }
226
+ .el-table {
227
+ width: 100%;
228
+ border-collapse: collapse;
229
+ }
230
+ .el-table th, .el-table td {
231
+ border: 1px solid #ddd;
232
+ padding: 8px;
233
+ }
234
+ .success-row { background-color: #f0f9eb; }
235
+ .info-row { background-color: #e6f7ff; }
236
+ .warning-row { background-color: #fdf6ec; }
237
+ .danger-row { background-color: #fef0f0; }
238
+ .text-paragraph {
239
+ line-height: 1.8;
240
+ margin-bottom: 1rem;
241
+ }
242
+ ${styleHtml}
243
+ </style>
244
+ </head>
245
+ <body>
246
+ ${cloneElement.outerHTML}
247
+ </body>
248
+ </html>
249
+ `;
250
+
251
+ const blob = new Blob([docHtml], { type: "application/msword" });
252
+ saveAs(blob, `${filename}.doc`);
253
+ } catch (error) {
254
+ console.error("Word导出失败:", error);
255
+ alert("Word导出失败,请重试");
256
+ } finally {
257
+ isExporting.value = false;
258
+ }
259
+ };
260
+
261
+ const handleExportPDF = () =>
262
+ exportToPDF("report-container", "图文报告_房地产整合");
263
+ const handleExportWord = () =>
264
+ exportToWord("report-container", "图文报告_房地产整合");
265
+
266
+ // ---------- 报告文本 ----------
267
+ const rawReport = `2024年香港房地產市場整體呈現「價跌量穩、租金偏強、商業不振」的分化局面,全年各類物業價格普遍下調,但成交及租賃需求在政策放寬和人口流入支持下較前兩年有所改善。住宅方面,撤銷所有樓市「辣招」、放寬按揭成數及利率見頂後回落,帶動一手主導的成交回升,但無礙樓價全年再跌約中個百分比,反映經濟及供應壓力仍然主導價格走勢。
268
+
269
+ 在住宅市場,本年特點包括:
270
+ 一、銷售量回升、價格續調:2024年在全面撤辣及按揭寬鬆後,全年住宅買賣宗數按年錄得逾兩成增長,但全年樓價仍錄得約6%至7%的跌幅,延續過去兩年的調整趨勢。
271
+ 二、供應顯著增加:私樓落成量約2.4萬個單位,較2023年大幅增加約七成,主要為中小型單位,為後市價格帶來持續壓力。
272
+ 三、租金走勢優於樓價:在人口及人才計劃帶動下,住宅租金全年錄得中低個位升幅,租金指數距歷史高位僅約數個百分點,令小型單位的平均回報率升至近十多年高位水平。
273
+
274
+ 在非住宅方面,2024年為商廈及舖位市場的「谷底年」,寫字樓空置率創逾十年新高,商業成交金額及宗數均跌至有紀錄以來低位。`;
275
+
276
+ const rawParagraphs = rawReport
277
+ .split(/\n\s*\n/)
278
+ .filter((p) => p.trim().length > 0);
279
+ const [para1 = "", para2 = "", para3 = ""] = rawParagraphs;
280
+
281
+ // ---------- 组件定义(markRaw)----------
282
+ const ElStatusTable = markRaw(
283
+ defineComponent({
284
+ name: "ElStatusTable",
285
+ setup() {
286
+ const tableData = [
287
+ {
288
+ date: "2016-05-02",
289
+ name: "王小虎",
290
+ address: "上海市普陀区金沙江路1518弄",
291
+ status: "success",
292
+ },
293
+ {
294
+ date: "2016-05-04",
295
+ name: "王小虎",
296
+ address: "上海市普陀区金沙江路1518弄",
297
+ status: "info",
298
+ },
299
+ {
300
+ date: "2016-05-01",
301
+ name: "王小虎",
302
+ address: "上海市普陀区金沙江路1518弄",
303
+ status: "warning",
304
+ },
305
+ {
306
+ date: "2016-05-03",
307
+ name: "王小虎",
308
+ address: "上海市普陀区金沙江路1518弄",
309
+ status: "danger",
310
+ },
311
+ ];
312
+ const rowClassName = ({ row }: { row: any }) => {
313
+ if (row.status === "success") return "success-row";
314
+ if (row.status === "info") return "info-row";
315
+ if (row.status === "warning") return "warning-row";
316
+ if (row.status === "danger") return "danger-row";
317
+ return "";
318
+ };
319
+ return () =>
320
+ h(
321
+ ElTable,
322
+ {
323
+ data: tableData,
324
+ style: { width: "100%" },
325
+ rowClassName,
326
+ border: true,
327
+ },
328
+ () => [
329
+ h(ElTableColumn, { prop: "date", label: "日期", width: "120" }),
330
+ h(ElTableColumn, { prop: "name", label: "姓名", width: "100" }),
331
+ h(ElTableColumn, { prop: "address", label: "地址" }),
332
+ h(
333
+ ElTableColumn,
334
+ { label: "状态", width: "100" },
335
+ {
336
+ default: ({ row }: any) => {
337
+ let type: "success" | "info" | "warning" | "danger" = "success";
338
+ if (row.status === "info") type = "info";
339
+ if (row.status === "warning") type = "warning";
340
+ if (row.status === "danger") type = "danger";
341
+ return h(
342
+ ElButton,
343
+ { size: "small", type, plain: true },
344
+ () => row.status
345
+ );
346
+ },
347
+ }
348
+ ),
349
+ ]
350
+ );
351
+ },
352
+ })
353
+ );
354
+
355
+ const ButtonGroup = markRaw(
356
+ defineComponent({
357
+ setup() {
358
+ return () =>
359
+ h("div", { class: "flex flex-wrap gap-3 my-2" }, [
360
+ h(ElButton, { type: "primary" }, () => "主要按钮"),
361
+ h(ElButton, { type: "success" }, () => "成功按钮"),
362
+ h(ElButton, { type: "info" }, () => "信息按钮"),
363
+ h(ElButton, { type: "warning" }, () => "警告按钮"),
364
+ h(ElButton, { type: "danger" }, () => "危险按钮"),
365
+ h(ElButton, {}, () => "默认按钮"),
366
+ h(ElButton, { round: true }, () => "圆角按钮"),
367
+ ]);
368
+ },
369
+ })
370
+ );
371
+
372
+ const IconShowcase = markRaw(
373
+ defineComponent({
374
+ setup() {
375
+ return () =>
376
+ h(
377
+ "div",
378
+ { class: "flex gap-4 items-center py-3 border-y border-gray-100" },
379
+ [
380
+ h(ElIcon, { size: 24, color: "#409EFF" }, () => h(Platform)),
381
+ h(ElIcon, { size: 24, color: "#67C23A" }, () => h(Eleme)),
382
+ h(ElIcon, { size: 24, color: "#F56C6C" }, () => h(DeleteFilled)),
383
+ h(ElIcon, { size: 24, color: "#E6A23C" }, () => h(Delete)),
384
+ h(
385
+ "span",
386
+ { class: "text-sm text-gray-500 ml-2" },
387
+ "el-icon-platform-eleme / eleme / delete-filled / delete"
388
+ ),
389
+ ]
390
+ );
391
+ },
392
+ })
393
+ );
394
+
395
+ const EChartsMixed = markRaw(
396
+ defineComponent({
397
+ name: "EChartsMixed",
398
+ setup() {
399
+ const chartRef = ref<HTMLDivElement | null>(null);
400
+ let chartInstance: echarts.ECharts | null = null;
401
+
402
+ const initChart = () => {
403
+ if (!chartRef.value) return;
404
+ chartInstance = echarts.init(chartRef.value);
405
+ const option = {
406
+ tooltip: {
407
+ trigger: "axis",
408
+ axisPointer: { type: "cross", crossStyle: { color: "#999" } },
409
+ },
410
+ toolbox: {
411
+ feature: {
412
+ dataView: { show: true, readOnly: false },
413
+ magicType: { show: true, type: ["line", "bar"] },
414
+ restore: { show: true },
415
+ saveAsImage: { show: true },
416
+ },
417
+ },
418
+ legend: { data: ["Evaporation", "Precipitation", "Temperature"] },
419
+ xAxis: [
420
+ {
421
+ type: "category",
422
+ data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
423
+ axisPointer: { type: "shadow" },
424
+ },
425
+ ],
426
+ yAxis: [
427
+ {
428
+ type: "value",
429
+ name: "Precipitation",
430
+ min: 0,
431
+ max: 250,
432
+ interval: 50,
433
+ axisLabel: { formatter: "{value} ml" },
434
+ },
435
+ {
436
+ type: "value",
437
+ name: "Temperature",
438
+ min: 0,
439
+ max: 25,
440
+ interval: 5,
441
+ axisLabel: { formatter: "{value} °C" },
442
+ },
443
+ ],
444
+ series: [
445
+ {
446
+ name: "Evaporation",
447
+ type: "bar",
448
+ tooltip: { valueFormatter: (value: any) => value + " ml" },
449
+ data: [2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6],
450
+ },
451
+ {
452
+ name: "Precipitation",
453
+ type: "bar",
454
+ tooltip: { valueFormatter: (value: any) => value + " ml" },
455
+ data: [2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6],
456
+ },
457
+ {
458
+ name: "Temperature",
459
+ type: "line",
460
+ yAxisIndex: 1,
461
+ tooltip: { valueFormatter: (value: any) => value + " °C" },
462
+ data: [2.0, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3],
463
+ },
464
+ ],
465
+ };
466
+ chartInstance.setOption(option);
467
+ window.addEventListener("resize", () => chartInstance?.resize());
468
+ };
469
+
470
+ onMounted(() => {
471
+ nextTick(() => initChart());
472
+ });
473
+
474
+ onUnmounted(() => {
475
+ chartInstance?.dispose();
476
+ });
477
+
478
+ return () =>
479
+ h("div", {
480
+ ref: chartRef,
481
+ style: { width: "100%", height: "400px" },
482
+ class: "echarts-container",
483
+ });
484
+ },
485
+ })
486
+ );
487
+
488
+ // ---------- 构建显示序列 ----------
489
+ type DisplayItem =
490
+ | { type: "text"; content: string }
491
+ | { type: "component"; component: Component };
492
+
493
+ const displaySequence = ref<DisplayItem[]>([
494
+ { type: "text", content: para1 },
495
+ { type: "component", component: ElStatusTable },
496
+ { type: "text", content: "📌 带状态表格(高亮区分成功/信息/警告/危险)" },
497
+ { type: "text", content: para2 },
498
+ { type: "component", component: ButtonGroup },
499
+ { type: "component", component: IconShowcase },
500
+ { type: "text", content: para3 },
501
+ { type: "component", component: EChartsMixed },
502
+ {
503
+ type: "text",
504
+ content: "✅ 以上为整合 ECharts 图表与完整房地产报告的图文混排效果。",
505
+ },
506
+ ]);
507
+
508
+ // ---------- 顺序显示控制 ----------
509
+ const activeIndex = ref(0); // 当前已显示到第几个项目(从0开始,0表示未显示任何项目)
510
+ const lastStartedTextIndex = ref(-1); // 记录当前正在打字的文本索引,用于 auto-start
511
+
512
+ // 文本段落打字完成回调
513
+ const onTextCompleted = (idx: number) => {
514
+ // 当前文本完成,显示下一个项目
515
+ if (idx === activeIndex.value - 1) {
516
+ // 注意:文本完成时 activeIndex 已经指向了该项目之后?不,逻辑是:当 activeIndex 增加时,会渲染新的项目。
517
+ // 此处需要将 activeIndex 增加1,以显示下一个项目
518
+ if (activeIndex.value < displaySequence.value.length) {
519
+ activeIndex.value++;
520
+ // 如果下一个项目是文本,则需要标记它开始打字
521
+ const nextItem = displaySequence.value[activeIndex.value - 1]; // 因为 activeIndex 刚增加,指向新显示的项目
522
+ if (nextItem?.type === "text") {
523
+ lastStartedTextIndex.value = activeIndex.value - 1;
524
+ }
525
+ }
526
+ }
527
+ };
528
+
529
+ // 启动顺序展示
530
+ const startSequence = () => {
531
+ if (displaySequence.value.length === 0) return;
532
+ // 先显示第一个项目
533
+ activeIndex.value = 1;
534
+ const firstItem = displaySequence.value[0];
535
+ if (firstItem?.type === "text") {
536
+ lastStartedTextIndex.value = 0;
537
+ }
538
+ };
539
+
540
+ // 重置
541
+ const restartDemo = () => {
542
+ activeIndex.value = 0;
543
+ lastStartedTextIndex.value = -1;
544
+ // 等待 Vue 更新后重新开始
545
+ nextTick(() => {
546
+ startSequence();
547
+ });
548
+ };
549
+
550
+ // 挂载时启动
551
+ onMounted(() => {
552
+ startSequence();
553
+ });
554
+
555
+ // ---------- 打字机组件(独立控制每个文本段落的逐字显示)----------
556
+ const TypewriterText = defineComponent({
557
+ name: "TypewriterText",
558
+ props: {
559
+ text: { type: String, required: true },
560
+ autoStart: { type: Boolean, default: false },
561
+ },
562
+ emits: ["completed"],
563
+ setup(props, { emit }) {
564
+ const displayed = ref("");
565
+ let interval: number | null = null;
566
+ let isStarted = false;
567
+
568
+ const startTyping = () => {
569
+ if (isStarted) return;
570
+ isStarted = true;
571
+ displayed.value = "";
572
+ let i = 0;
573
+ interval = window.setInterval(() => {
574
+ if (i < props.text.length) {
575
+ displayed.value += props.text[i];
576
+ i++;
577
+ } else {
578
+ if (interval) clearInterval(interval);
579
+ interval = null;
580
+ emit("completed");
581
+ }
582
+ }, 28);
583
+ };
584
+
585
+ if (props.autoStart) {
586
+ startTyping();
587
+ }
588
+
589
+ return () => h("div", { class: "typewriter-text" }, displayed.value);
590
+ },
591
+ });
592
+ </script>
593
+
594
+ <style scoped>
595
+ :deep(.el-table .success-row) {
596
+ background-color: #f0f9eb;
597
+ }
598
+ :deep(.el-table .info-row) {
599
+ background-color: #e6f7ff;
600
+ }
601
+ :deep(.el-table .warning-row) {
602
+ background-color: #fdf6ec;
603
+ }
604
+ :deep(.el-table .danger-row) {
605
+ background-color: #fef0f0;
606
+ }
607
+ .text-paragraph {
608
+ line-height: 1.8;
609
+ font-size: 15px;
610
+ letter-spacing: 0.01em;
611
+ color: #374151;
612
+ margin-bottom: 1rem;
613
+ white-space: pre-wrap;
614
+ }
615
+ .typewriter-text {
616
+ white-space: pre-wrap;
617
+ }
618
+ @media print {
619
+ .no-print {
620
+ display: none !important;
621
+ }
622
+ }
623
+ </style>