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,1610 @@
1
+ import JSZip from 'jszip'
2
+ import html2canvas from 'html2canvas'
3
+ import { relsXml } from 'html-docx-js-typescript/dist/assets'
4
+ import { defaultMargins, mhtPartTemplate } from 'html-docx-js-typescript/dist/templates'
5
+
6
+ export interface DocxHeaderFooterOptions {
7
+ /** 页眉右上角文字,默认「商业机密」 */
8
+ headerText?: string
9
+ /** 页脚是否显示「当前页 / 总页数」,默认 true */
10
+ showPageNumbers?: boolean
11
+ }
12
+
13
+ export interface DocxExportOptions {
14
+ orientation?: 'portrait' | 'landscape'
15
+ margins?: Partial<typeof defaultMargins>
16
+ /** 页面宽度(twips),默认 A4 竖向 */
17
+ pageWidth?: number
18
+ /** 页面高度(twips),默认 A4 竖向 */
19
+ pageHeight?: number
20
+ headerFooter?: DocxHeaderFooterOptions
21
+ }
22
+
23
+ /** A4 竖向:210mm × 297mm(Word twips,1 inch = 1440 twips) */
24
+ export const A4_PORTRAIT = {
25
+ width: 11906,
26
+ height: 16838,
27
+ } as const
28
+
29
+ /** A4 常用 2cm 页边距 ≈ 1134 twips */
30
+ export const A4_MARGINS_2CM = {
31
+ top: 1134,
32
+ right: 1134,
33
+ bottom: 1134,
34
+ left: 1134,
35
+ header: 720,
36
+ footer: 720,
37
+ gutter: 0,
38
+ } as const
39
+
40
+ /** 可打印区域宽度(px@96dpi):210mm - 左右各 2cm ≈ 642px */
41
+ export const A4_CONTENT_WIDTH_PX = Math.round(((210 - 4) / 25.4) * 96)
42
+
43
+ /** Word 可打印区宽度(pt):210mm - 左右各 2cm ≈ 482pt */
44
+ export const A4_CONTENT_WIDTH_PT = Math.round(((210 - 40) / 25.4) * 72)
45
+
46
+ /** 图表导出高度(与页面 400px 比例协调,且可落在单页 A4 内) */
47
+ export const A4_CHART_HEIGHT_PX = 380
48
+
49
+ /** 单张图表在 A4 可打印区域内的最大高度 */
50
+ export const A4_CHART_MAX_HEIGHT_PX = Math.round(((297 - 4) / 25.4) * 96 * 0.42)
51
+
52
+ /** 左浮动无障碍图标(固定尺寸,不超出页边距) */
53
+ export const DOCX_FLOAT_ICON_SIZE_PX = 72
54
+
55
+ /** 饼图左栏最大宽度:A4 可打印区 42% 减去间距 */
56
+ export const DOCX_PIE_CHART_MAX_WIDTH_PX = Math.min(
57
+ 220,
58
+ Math.floor(A4_CONTENT_WIDTH_PX * 0.42) - 16
59
+ )
60
+
61
+ /** 饼图最大高度(避免 Word 单页溢出) */
62
+ export const DOCX_PIE_CHART_MAX_HEIGHT_PX = Math.min(
63
+ 120,
64
+ Math.floor(A4_CHART_MAX_HEIGHT_PX * 0.32)
65
+ )
66
+
67
+ export const DEFAULT_DOCX_EXPORT_OPTIONS: DocxExportOptions = {
68
+ orientation: 'portrait',
69
+ pageWidth: A4_PORTRAIT.width,
70
+ pageHeight: A4_PORTRAIT.height,
71
+ margins: { ...A4_MARGINS_2CM },
72
+ }
73
+
74
+ function mergeMargins(margins?: Partial<typeof defaultMargins>) {
75
+ return { ...defaultMargins, ...margins }
76
+ }
77
+
78
+ function resolvePageSize(options: DocxExportOptions) {
79
+ const orientation = options.orientation ?? 'portrait'
80
+ if (options.pageWidth && options.pageHeight) {
81
+ return { width: options.pageWidth, height: options.pageHeight, orientation }
82
+ }
83
+ if (orientation === 'landscape') {
84
+ return { width: A4_PORTRAIT.height, height: A4_PORTRAIT.width, orientation }
85
+ }
86
+ return { width: A4_PORTRAIT.width, height: A4_PORTRAIT.height, orientation }
87
+ }
88
+
89
+ /** UTF-8 正确 quoted-printable 编码(修复中文乱码,且保留 MHT 图片关联) */
90
+ function encodeQuotedPrintableUtf8(input: string): string {
91
+ const bytes = new TextEncoder().encode(input)
92
+ let out = ''
93
+ let lineLen = 0
94
+
95
+ const softBreak = () => {
96
+ out += '=\r\n'
97
+ lineLen = 0
98
+ }
99
+
100
+ const appendChar = (ch: string, encodedLen: number) => {
101
+ if (lineLen + encodedLen > 75) softBreak()
102
+ out += ch
103
+ lineLen += encodedLen
104
+ }
105
+
106
+ for (let i = 0; i < bytes.length; i++) {
107
+ const b = bytes[i]!
108
+ const isPrintable =
109
+ (b >= 33 && b <= 60) || (b >= 62 && b <= 126) || b === 9 || b === 32
110
+
111
+ if (isPrintable) {
112
+ appendChar(String.fromCharCode(b), 1)
113
+ } else {
114
+ const enc = `=${b.toString(16).toUpperCase().padStart(2, '0')}`
115
+ appendChar(enc, 3)
116
+ }
117
+ }
118
+
119
+ return out
120
+ }
121
+
122
+ function prepareImageParts(htmlSource: string) {
123
+ const imageContentParts: string[] = []
124
+ const html = htmlSource.replace(
125
+ /src=(["'])data:([^"']+)\1/gi,
126
+ (_match, _quote: string, dataUrl: string) => {
127
+ const comma = dataUrl.indexOf(',')
128
+ const meta = dataUrl.slice(0, comma)
129
+ const encodedContent = dataUrl.slice(comma + 1)
130
+ const semi = meta.indexOf(';')
131
+ const contentType = meta.slice(0, semi)
132
+ const contentEncoding = meta.slice(semi + 1)
133
+ if (!contentType || !encodedContent) return _match
134
+
135
+ const ext = contentType.split('/')[1]?.replace(/[^a-z0-9]/gi, '') || 'png'
136
+ const index = imageContentParts.length
137
+ const contentLocation = `file:///C:/fake/image${index}.${ext}`
138
+ imageContentParts.push(
139
+ mhtPartTemplate(contentType, contentEncoding, contentLocation, encodedContent)
140
+ )
141
+ return `src="${contentLocation}"`
142
+ }
143
+ )
144
+ return { htmlSource: html, imageContentParts }
145
+ }
146
+
147
+ function buildMhtDocument(htmlSource: string): string {
148
+ const { htmlSource: htmlWithRefs, imageContentParts } = prepareImageParts(htmlSource)
149
+ const encodedBody = encodeQuotedPrintableUtf8(htmlWithRefs)
150
+ const parts = imageContentParts.join('\n')
151
+
152
+ return `MIME-Version: 1.0
153
+ Content-Type: multipart/related;
154
+ type="text/html";
155
+ boundary="----=mhtDocumentPart"
156
+
157
+
158
+ ------=mhtDocumentPart
159
+ Content-Type: text/html;
160
+ charset="utf-8"
161
+ Content-Transfer-Encoding: quoted-printable
162
+ Content-Location: file:///C:/fake/document.html
163
+
164
+ ${encodedBody}
165
+
166
+ ${parts}
167
+
168
+ ------=mhtDocumentPart--
169
+ `
170
+ }
171
+
172
+ function loadImageNaturalSize(
173
+ dataUrl: string
174
+ ): Promise<{ width: number; height: number }> {
175
+ return new Promise((resolve, reject) => {
176
+ const probe = new Image()
177
+ probe.onload = () =>
178
+ resolve({ width: probe.naturalWidth, height: probe.naturalHeight })
179
+ probe.onerror = () => reject(new Error('chart image load failed'))
180
+ probe.src = dataUrl
181
+ })
182
+ }
183
+
184
+ /** 将图表缩放到 A4 页边距内,避免 Word 裁切或撑出页面 */
185
+ function fitChartToA4Content(naturalWidth: number, naturalHeight: number) {
186
+ const maxW = A4_CONTENT_WIDTH_PX
187
+ const maxH = Math.min(A4_CHART_HEIGHT_PX, A4_CHART_MAX_HEIGHT_PX)
188
+
189
+ let width = maxW
190
+ let height = Math.round((naturalHeight / naturalWidth) * width)
191
+
192
+ if (height > maxH) {
193
+ height = maxH
194
+ width = Math.round((naturalWidth / naturalHeight) * height)
195
+ }
196
+
197
+ return { width, height }
198
+ }
199
+
200
+ /** 图表容器:Word 需使用数值 width/height(勿用 100% 属性,否则会只显示一半) */
201
+ async function createChartBlock(dataUrl: string): Promise<HTMLDivElement> {
202
+ const natural = await loadImageNaturalSize(dataUrl)
203
+ const { width, height } = fitChartToA4Content(natural.width, natural.height)
204
+
205
+ const wrap = document.createElement('div')
206
+ wrap.className = 'docx-chart-wrap'
207
+ wrap.style.cssText = [
208
+ `width:${width}px`,
209
+ `max-width:${A4_CONTENT_WIDTH_PX}px`,
210
+ 'margin:12pt auto',
211
+ 'text-align:center',
212
+ 'page-break-inside:avoid',
213
+ 'box-sizing:border-box',
214
+ 'overflow:visible',
215
+ ].join(';')
216
+
217
+ const img = document.createElement('img')
218
+ img.className = 'docx-chart-img'
219
+ img.src = dataUrl
220
+ img.setAttribute('width', String(width))
221
+ img.setAttribute('height', String(height))
222
+ img.style.cssText = [
223
+ `width:${width}px`,
224
+ `height:${height}px`,
225
+ `max-width:${A4_CONTENT_WIDTH_PX}px`,
226
+ 'display:block',
227
+ 'margin:0 auto',
228
+ 'border:0',
229
+ ].join(';')
230
+
231
+ wrap.appendChild(img)
232
+ return wrap
233
+ }
234
+
235
+ /** ECharts:按 A4 可打印区导出完整图表,避免裁切与溢出 */
236
+ export async function rasterizeEchartsForDocx(
237
+ sourceRoot: HTMLElement,
238
+ targetRoot: HTMLElement,
239
+ echartsApi: {
240
+ getInstanceByDom: (el: HTMLElement) => {
241
+ resize: (opts?: { width?: number; height?: number }) => void
242
+ getDataURL: (opts: object) => string
243
+ } | undefined
244
+ }
245
+ ) {
246
+ const sourceCharts = sourceRoot.querySelectorAll('.echarts-container')
247
+ const targetCharts = targetRoot.querySelectorAll('.echarts-container')
248
+ const exportW = A4_CONTENT_WIDTH_PX
249
+ const exportH = A4_CHART_HEIGHT_PX
250
+
251
+ for (let i = 0; i < sourceCharts.length; i++) {
252
+ const sourceEl = sourceCharts[i] as HTMLElement
253
+ const targetEl = targetCharts[i] as HTMLElement
254
+ if (!targetEl) continue
255
+
256
+ const prevWidth = sourceEl.style.width
257
+ const prevHeight = sourceEl.style.height
258
+ const prevMaxWidth = sourceEl.style.maxWidth
259
+
260
+ let dataUrl = ''
261
+ const inst = echartsApi.getInstanceByDom(sourceEl)
262
+ if (inst) {
263
+ try {
264
+ sourceEl.style.width = `${exportW}px`
265
+ sourceEl.style.height = `${exportH}px`
266
+ sourceEl.style.maxWidth = `${exportW}px`
267
+ inst.resize({ width: exportW, height: exportH })
268
+ await new Promise((r) => setTimeout(r, 320))
269
+ dataUrl = inst.getDataURL({
270
+ type: 'png',
271
+ pixelRatio: 1,
272
+ backgroundColor: '#ffffff',
273
+ excludeComponents: ['toolbox', 'dataZoom'],
274
+ })
275
+ } finally {
276
+ sourceEl.style.width = prevWidth
277
+ sourceEl.style.height = prevHeight
278
+ sourceEl.style.maxWidth = prevMaxWidth
279
+ inst.resize()
280
+ }
281
+ }
282
+
283
+ if (!dataUrl) {
284
+ const canvas = sourceEl.querySelector('canvas')
285
+ if (canvas instanceof HTMLCanvasElement) {
286
+ dataUrl = canvas.toDataURL('image/png')
287
+ }
288
+ }
289
+
290
+ if (!dataUrl) continue
291
+
292
+ const chartBlock = await createChartBlock(dataUrl)
293
+ chartBlock.classList.add('echarts-container')
294
+ targetEl.replaceWith(chartBlock)
295
+ }
296
+ }
297
+
298
+ async function svgToPngImg(svg: SVGSVGElement): Promise<HTMLImageElement> {
299
+ const clone = svg.cloneNode(true) as SVGSVGElement
300
+ if (!clone.getAttribute('xmlns')) {
301
+ clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
302
+ }
303
+ const rect = svg.getBoundingClientRect()
304
+ const w = Math.max(rect.width, 16)
305
+ const h = Math.max(rect.height, 16)
306
+ clone.setAttribute('width', String(w))
307
+ clone.setAttribute('height', String(h))
308
+
309
+ const svgData = new XMLSerializer().serializeToString(clone)
310
+ const url = URL.createObjectURL(
311
+ new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
312
+ )
313
+
314
+ try {
315
+ return await new Promise((resolve, reject) => {
316
+ const img = new Image()
317
+ img.onload = () => {
318
+ const canvas = document.createElement('canvas')
319
+ canvas.width = w * 2
320
+ canvas.height = h * 2
321
+ const ctx = canvas.getContext('2d')
322
+ ctx?.drawImage(img, 0, 0, canvas.width, canvas.height)
323
+ const wordImg = document.createElement('img')
324
+ wordImg.src = canvas.toDataURL('image/png')
325
+ wordImg.style.width = `${w}px`
326
+ wordImg.style.height = `${h}px`
327
+ wordImg.style.verticalAlign = 'middle'
328
+ wordImg.style.display = 'inline-block'
329
+ resolve(wordImg)
330
+ }
331
+ img.onerror = () => reject(new Error('SVG load failed'))
332
+ img.src = url
333
+ })
334
+ } finally {
335
+ URL.revokeObjectURL(url)
336
+ }
337
+ }
338
+
339
+ /** Element Plus 图标等:html2canvas 栅格化,避免 <use> 外链丢失 */
340
+ export async function rasterizeIconsForDocx(
341
+ sourceRoot: HTMLElement,
342
+ targetRoot: HTMLElement
343
+ ) {
344
+ const sourceIcons = sourceRoot.querySelectorAll('.el-icon')
345
+ const targetIcons = targetRoot.querySelectorAll('.el-icon')
346
+
347
+ for (let i = 0; i < sourceIcons.length; i++) {
348
+ const src = sourceIcons[i] as HTMLElement
349
+ const tgt = targetIcons[i] as HTMLElement
350
+ if (!tgt) continue
351
+
352
+ try {
353
+ const canvas = await html2canvas(src, {
354
+ backgroundColor: null,
355
+ scale: 2,
356
+ logging: false,
357
+ })
358
+ const img = document.createElement('img')
359
+ img.src = canvas.toDataURL('image/png')
360
+ img.setAttribute('width', '24')
361
+ img.setAttribute('height', '24')
362
+ img.style.width = '24px'
363
+ img.style.height = '24px'
364
+ img.style.verticalAlign = 'middle'
365
+ img.style.display = 'inline-block'
366
+ tgt.replaceWith(img)
367
+ } catch {
368
+ const svg = src.querySelector('svg')
369
+ if (svg) {
370
+ try {
371
+ const img = await svgToPngImg(svg)
372
+ tgt.replaceWith(img)
373
+ } catch {
374
+ /* keep original */
375
+ }
376
+ }
377
+ }
378
+ }
379
+ }
380
+
381
+ /** 图标替换后剩余的独立 SVG(如 Lucide)转 PNG */
382
+ export async function rasterizeStandaloneSvgsForDocx(targetRoot: HTMLElement) {
383
+ const svgs = [...targetRoot.querySelectorAll('svg')]
384
+ for (const svg of svgs) {
385
+ try {
386
+ const img = await svgToPngImg(svg)
387
+ svg.replaceWith(img)
388
+ } catch {
389
+ /* keep */
390
+ }
391
+ }
392
+ }
393
+
394
+ async function fetchImageAsDataUrl(url: string): Promise<string> {
395
+ const res = await fetch(url)
396
+ if (!res.ok) throw new Error(`fetch image failed: ${res.status}`)
397
+ const blob = await res.blob()
398
+ return new Promise((resolve, reject) => {
399
+ const reader = new FileReader()
400
+ reader.onload = () => resolve(reader.result as string)
401
+ reader.onerror = () => reject(new Error('read image blob failed'))
402
+ reader.readAsDataURL(blob)
403
+ })
404
+ }
405
+
406
+ export function getDocxImageTargetSize(img: HTMLImageElement): {
407
+ width: number
408
+ height: number
409
+ } {
410
+ const naturalW = img.naturalWidth || Number.parseInt(img.getAttribute('width') || '0', 10) || 100
411
+ const naturalH = img.naturalHeight || Number.parseInt(img.getAttribute('height') || '0', 10) || 100
412
+
413
+ if (img.classList.contains('doc-float-icon')) {
414
+ return {
415
+ width: DOCX_FLOAT_ICON_SIZE_PX,
416
+ height: DOCX_FLOAT_ICON_SIZE_PX,
417
+ }
418
+ }
419
+
420
+ if (img.classList.contains('doc-pie-chart-img')) {
421
+ let width = Math.min(
422
+ DOCX_PIE_CHART_MAX_WIDTH_PX,
423
+ Number.parseInt(img.getAttribute('width') || '0', 10) || DOCX_PIE_CHART_MAX_WIDTH_PX,
424
+ A4_CONTENT_WIDTH_PX
425
+ )
426
+ let height = Math.round((naturalH / naturalW) * width)
427
+ if (height > DOCX_PIE_CHART_MAX_HEIGHT_PX) {
428
+ height = DOCX_PIE_CHART_MAX_HEIGHT_PX
429
+ width = Math.round((naturalW / naturalH) * height)
430
+ }
431
+ return { width, height }
432
+ }
433
+
434
+ if (naturalW > A4_CONTENT_WIDTH_PX) {
435
+ return {
436
+ width: A4_CONTENT_WIDTH_PX,
437
+ height: Math.round((naturalH / naturalW) * A4_CONTENT_WIDTH_PX),
438
+ }
439
+ }
440
+
441
+ return { width: naturalW, height: naturalH }
442
+ }
443
+
444
+ function imgElementToDataUrl(
445
+ img: HTMLImageElement,
446
+ targetW: number,
447
+ targetH: number
448
+ ): Promise<string> {
449
+ return new Promise((resolve, reject) => {
450
+ const draw = () => {
451
+ if (!targetW || !targetH) {
452
+ reject(new Error('invalid image dimensions'))
453
+ return
454
+ }
455
+ const canvas = document.createElement('canvas')
456
+ canvas.width = targetW
457
+ canvas.height = targetH
458
+ const ctx = canvas.getContext('2d')
459
+ if (!ctx) {
460
+ reject(new Error('no canvas context'))
461
+ return
462
+ }
463
+ ctx.drawImage(img, 0, 0, targetW, targetH)
464
+ resolve(canvas.toDataURL('image/png'))
465
+ }
466
+
467
+ if (img.complete && img.naturalWidth > 0) {
468
+ draw()
469
+ return
470
+ }
471
+
472
+ const prevOnload = img.onload
473
+ const prevOnerror = img.onerror
474
+ img.onload = () => {
475
+ img.onload = prevOnload
476
+ img.onerror = prevOnerror
477
+ draw()
478
+ }
479
+ img.onerror = () => {
480
+ img.onload = prevOnload
481
+ img.onerror = prevOnerror
482
+ reject(new Error('image load failed'))
483
+ }
484
+ if (!img.complete) {
485
+ const src = img.getAttribute('src')
486
+ if (src) img.src = src
487
+ }
488
+ })
489
+ }
490
+
491
+ /** 将页面中的 <img>(Vite 资源 URL 等)转为 data URL,避免 Word MHT 丢失图片 */
492
+ export async function rasterizeImagesForDocx(
493
+ root: HTMLElement,
494
+ sourceRoot?: HTMLElement
495
+ ) {
496
+ const images = [...root.querySelectorAll('img')] as HTMLImageElement[]
497
+ const sourceImages = sourceRoot
498
+ ? ([...sourceRoot.querySelectorAll('img')] as HTMLImageElement[])
499
+ : images
500
+
501
+ await Promise.all(
502
+ images.map(async (img, index) => {
503
+ const src = img.getAttribute('src') || img.src
504
+ if (!src) return
505
+
506
+ const liveImg = sourceImages[index] ?? img
507
+ const { width, height } = getDocxImageTargetSize(liveImg)
508
+
509
+ if (src.startsWith('data:')) {
510
+ img.setAttribute('width', String(width))
511
+ img.setAttribute('height', String(height))
512
+ img.style.width = `${width}px`
513
+ img.style.height = `${height}px`
514
+ img.style.maxWidth = `${width}px`
515
+ img.style.maxHeight = `${height}px`
516
+ return
517
+ }
518
+
519
+ const applyRasterized = (dataUrl: string) => {
520
+ img.setAttribute('src', dataUrl)
521
+ img.src = dataUrl
522
+ img.setAttribute('width', String(width))
523
+ img.setAttribute('height', String(height))
524
+ img.style.width = `${width}px`
525
+ img.style.height = `${height}px`
526
+ img.style.maxWidth = `${width}px`
527
+ img.style.maxHeight = `${height}px`
528
+ img.style.display = 'block'
529
+ img.style.border = '0'
530
+ }
531
+
532
+ try {
533
+ applyRasterized(await imgElementToDataUrl(liveImg, width, height))
534
+ return
535
+ } catch {
536
+ /* fall through to fetch */
537
+ }
538
+
539
+ try {
540
+ const fetchUrl = liveImg.currentSrc || liveImg.src || src
541
+ const probe = new Image()
542
+ probe.src = await fetchImageAsDataUrl(fetchUrl)
543
+ await new Promise<void>((resolve, reject) => {
544
+ probe.onload = () => resolve()
545
+ probe.onerror = () => reject(new Error('probe load failed'))
546
+ })
547
+ applyRasterized(await imgElementToDataUrl(probe, width, height))
548
+ } catch (e) {
549
+ console.warn('Failed to rasterize image for docx:', src, e)
550
+ }
551
+ })
552
+ )
553
+ }
554
+
555
+ export async function rasterizeMediaForDocx(
556
+ sourceRoot: HTMLElement,
557
+ targetRoot: HTMLElement,
558
+ echartsApi?: {
559
+ getInstanceByDom: (el: HTMLElement) => {
560
+ resize: () => void
561
+ getDataURL: (opts: object) => string
562
+ } | undefined
563
+ }
564
+ ) {
565
+ if (echartsApi) {
566
+ await rasterizeEchartsForDocx(sourceRoot, targetRoot, echartsApi)
567
+ }
568
+ await rasterizeIconsForDocx(sourceRoot, targetRoot)
569
+ await rasterizeStandaloneSvgsForDocx(targetRoot)
570
+ await rasterizeImagesForDocx(targetRoot)
571
+ }
572
+
573
+ const DOCX_XML_NS =
574
+ 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ' +
575
+ 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"'
576
+
577
+ function escapeXmlText(text: string): string {
578
+ return text
579
+ .replace(/&/g, '&amp;')
580
+ .replace(/</g, '&lt;')
581
+ .replace(/>/g, '&gt;')
582
+ .replace(/"/g, '&quot;')
583
+ }
584
+
585
+ function pageFieldRun(instr: string, placeholder = '1'): string {
586
+ return `<w:r>
587
+ <w:rPr><w:sz w:val="18"/><w:color w:val="666666"/></w:rPr>
588
+ <w:fldChar w:fldCharType="begin"/>
589
+ </w:r>
590
+ <w:r>
591
+ <w:rPr><w:sz w:val="18"/><w:color w:val="666666"/></w:rPr>
592
+ <w:instrText xml:space="preserve"> ${instr} </w:instrText>
593
+ </w:r>
594
+ <w:r>
595
+ <w:rPr><w:sz w:val="18"/><w:color w:val="666666"/></w:rPr>
596
+ <w:fldChar w:fldCharType="separate"/>
597
+ </w:r>
598
+ <w:r>
599
+ <w:rPr><w:sz w:val="18"/><w:color w:val="666666"/></w:rPr>
600
+ <w:t>${placeholder}</w:t>
601
+ </w:r>
602
+ <w:r>
603
+ <w:rPr><w:sz w:val="18"/><w:color w:val="666666"/></w:rPr>
604
+ <w:fldChar w:fldCharType="end"/>
605
+ </w:r>`
606
+ }
607
+
608
+ function buildHeaderXml(headerText: string): string {
609
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
610
+ <w:hdr ${DOCX_XML_NS}>
611
+ <w:p>
612
+ <w:pPr><w:jc w:val="right"/></w:pPr>
613
+ <w:r>
614
+ <w:rPr>
615
+ <w:rFonts w:ascii="Microsoft YaHei" w:eastAsia="微软雅黑" w:hAnsi="Microsoft YaHei"/>
616
+ <w:sz w:val="18"/>
617
+ <w:color w:val="C00000"/>
618
+ </w:rPr>
619
+ <w:t>${escapeXmlText(headerText)}</w:t>
620
+ </w:r>
621
+ </w:p>
622
+ </w:hdr>`
623
+ }
624
+
625
+ function buildFooterXml(): string {
626
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
627
+ <w:ftr ${DOCX_XML_NS}>
628
+ <w:p>
629
+ <w:pPr><w:jc w:val="center"/></w:pPr>
630
+ ${pageFieldRun('PAGE')}
631
+ <w:r>
632
+ <w:rPr><w:sz w:val="18"/><w:color w:val="666666"/></w:rPr>
633
+ <w:t xml:space="preserve"> / </w:t>
634
+ </w:r>
635
+ ${pageFieldRun('NUMPAGES')}
636
+ </w:p>
637
+ </w:ftr>`
638
+ }
639
+
640
+ function buildDocumentXmlWithHeaderFooter(
641
+ width: number,
642
+ height: number,
643
+ orient: string,
644
+ margins: ReturnType<typeof mergeMargins>
645
+ ): string {
646
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
647
+ <w:document
648
+ xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
649
+ xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
650
+ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
651
+ xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
652
+ xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
653
+ xmlns:ns6="http://schemas.openxmlformats.org/schemaLibrary/2006/main"
654
+ xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"
655
+ xmlns:ns8="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
656
+ xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram"
657
+ xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"
658
+ xmlns:ns11="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
659
+ xmlns:dsp="http://schemas.microsoft.com/office/drawing/2008/diagram"
660
+ xmlns:ns13="urn:schemas-microsoft-com:office:excel"
661
+ xmlns:o="urn:schemas-microsoft-com:office:office"
662
+ xmlns:v="urn:schemas-microsoft-com:vml"
663
+ xmlns:w10="urn:schemas-microsoft-com:office:word"
664
+ xmlns:ns17="urn:schemas-microsoft-com:office:powerpoint"
665
+ xmlns:odx="http://opendope.org/xpaths"
666
+ xmlns:odc="http://opendope.org/conditions"
667
+ xmlns:odq="http://opendope.org/questions"
668
+ xmlns:odi="http://opendope.org/components"
669
+ xmlns:odgm="http://opendope.org/SmartArt/DataHierarchy"
670
+ xmlns:ns24="http://schemas.openxmlformats.org/officeDocument/2006/bibliography"
671
+ xmlns:ns25="http://schemas.openxmlformats.org/drawingml/2006/compatibility"
672
+ xmlns:ns26="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas">
673
+ <w:body>
674
+ <w:altChunk r:id="htmlChunk" />
675
+ <w:sectPr>
676
+ <w:headerReference w:type="default" r:id="rIdHeader"/>
677
+ <w:footerReference w:type="default" r:id="rIdFooter"/>
678
+ <w:pgSz w:w="${width}" w:h="${height}" w:orient="${orient}" />
679
+ <w:pgMar w:top="${margins.top}"
680
+ w:right="${margins.right}"
681
+ w:bottom="${margins.bottom}"
682
+ w:left="${margins.left}"
683
+ w:header="${margins.header}"
684
+ w:footer="${margins.footer}"
685
+ w:gutter="${margins.gutter}"/>
686
+ </w:sectPr>
687
+ </w:body>
688
+ </w:document>`
689
+ }
690
+
691
+ const CONTENT_TYPES_WITH_HEADER_FOOTER = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
692
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
693
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
694
+ <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
695
+ <Override PartName="/word/afchunk.mht" ContentType="message/rfc822"/>
696
+ <Override PartName="/word/header1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"/>
697
+ <Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>
698
+ </Types>
699
+ `
700
+
701
+ const DOCUMENT_RELS_WITH_HEADER_FOOTER = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
702
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
703
+ <Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk"
704
+ Target="/word/afchunk.mht" Id="htmlChunk" />
705
+ <Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
706
+ Target="header1.xml" Id="rIdHeader" />
707
+ <Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
708
+ Target="footer1.xml" Id="rIdFooter" />
709
+ </Relationships>
710
+ `
711
+
712
+ async function buildDocxZip(
713
+ html: string,
714
+ options: DocxExportOptions = DEFAULT_DOCX_EXPORT_OPTIONS
715
+ ): Promise<Blob> {
716
+ const { width, height, orientation } = resolvePageSize(options)
717
+ const margins = mergeMargins({ ...A4_MARGINS_2CM, ...options.margins })
718
+ const headerFooter = {
719
+ headerText: options.headerFooter?.headerText ?? '商业机密',
720
+ showPageNumbers: options.headerFooter?.showPageNumbers ?? true,
721
+ }
722
+
723
+ const zip = new JSZip()
724
+ zip.file('[Content_Types].xml', CONTENT_TYPES_WITH_HEADER_FOOTER)
725
+ zip.folder('_rels')!.file('.rels', relsXml)
726
+
727
+ const wordFolder = zip.folder('word')!
728
+ wordFolder
729
+ .file(
730
+ 'document.xml',
731
+ buildDocumentXmlWithHeaderFooter(width, height, orientation, margins)
732
+ )
733
+ .file('afchunk.mht', buildMhtDocument(html))
734
+ .file('header1.xml', buildHeaderXml(headerFooter.headerText))
735
+
736
+ if (headerFooter.showPageNumbers) {
737
+ wordFolder.file('footer1.xml', buildFooterXml())
738
+ } else {
739
+ wordFolder.file(
740
+ 'footer1.xml',
741
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:ftr ${DOCX_XML_NS}><w:p/></w:ftr>`
742
+ )
743
+ }
744
+
745
+ wordFolder.folder('_rels')!.file('document.xml.rels', DOCUMENT_RELS_WITH_HEADER_FOOTER)
746
+
747
+ const buffer = await zip.generateAsync({
748
+ type: 'arraybuffer',
749
+ compression: 'DEFLATE',
750
+ })
751
+
752
+ return new Blob([buffer], {
753
+ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
754
+ })
755
+ }
756
+
757
+ export function buildWordDocumentHtml(bodyHtml: string): string {
758
+ return `<!DOCTYPE html>
759
+ <html xmlns:o="urn:schemas-microsoft-com:office:office"
760
+ xmlns:w="urn:schemas-microsoft-com:office:word"
761
+ xmlns="http://www.w3.org/TR/REC-html40"
762
+ lang="zh-Hant">
763
+ <head>
764
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
765
+ <meta charset="UTF-8">
766
+ <!--[if gte mso 9]>
767
+ <xml>
768
+ <w:WordDocument>
769
+ <w:View>Print</w:View>
770
+ <w:Zoom>100</w:Zoom>
771
+ <w:DoNotOptimizeForBrowser/>
772
+ </w:WordDocument>
773
+ </xml>
774
+ <![endif]-->
775
+ <style>
776
+ @page {
777
+ size: A4 portrait;
778
+ margin: 2cm;
779
+ mso-page-orientation: portrait;
780
+ }
781
+ body {
782
+ font-family: Calibri, "Segoe UI", Arial, Helvetica, sans-serif;
783
+ font-size: 11pt;
784
+ line-height: 1.15;
785
+ color: #000000;
786
+ margin: 0;
787
+ padding: 0;
788
+ width: 100%;
789
+ max-width: 100%;
790
+ box-sizing: border-box;
791
+ }
792
+ .docx-page-root,
793
+ .doc-sample-root,
794
+ .doc-sample-body {
795
+ width: ${A4_CONTENT_WIDTH_PT}pt !important;
796
+ max-width: ${A4_CONTENT_WIDTH_PT}pt !important;
797
+ margin: 0;
798
+ padding: 0;
799
+ box-sizing: border-box;
800
+ font-family: Calibri, "Segoe UI", Arial, Helvetica, sans-serif;
801
+ font-size: 11pt;
802
+ line-height: 1.15;
803
+ color: #000000;
804
+ text-align: left;
805
+ }
806
+ .docx-table-wrap {
807
+ width: ${A4_CONTENT_WIDTH_PT}pt !important;
808
+ max-width: ${A4_CONTENT_WIDTH_PT}pt !important;
809
+ margin: 6pt 0 12pt;
810
+ padding: 0;
811
+ overflow: hidden;
812
+ }
813
+ table, .doc-table, .doc-table-word {
814
+ border-collapse: collapse !important;
815
+ width: ${A4_CONTENT_WIDTH_PT}pt !important;
816
+ max-width: ${A4_CONTENT_WIDTH_PT}pt !important;
817
+ table-layout: fixed !important;
818
+ mso-table-layout-alt: fixed !important;
819
+ word-wrap: break-word !important;
820
+ overflow-wrap: break-word !important;
821
+ margin: 0 !important;
822
+ border: 1px solid #000000;
823
+ box-sizing: border-box;
824
+ page-break-inside: auto !important;
825
+ }
826
+ tr, td, th {
827
+ page-break-inside: auto !important;
828
+ }
829
+ .doc-table-complex th,
830
+ .doc-table-complex td,
831
+ .doc-table-word th,
832
+ .doc-table-word td {
833
+ font-size: 9pt !important;
834
+ padding: 2pt 3pt !important;
835
+ }
836
+ .doc-h1, h1.doc-h1 {
837
+ font-size: 20pt !important;
838
+ font-weight: bold !important;
839
+ color: #c00000 !important;
840
+ text-align: left !important;
841
+ margin: 0 0 12pt !important;
842
+ padding: 0 !important;
843
+ border: none !important;
844
+ }
845
+ .doc-h2, h2.doc-h2 {
846
+ font-size: 14pt !important;
847
+ font-weight: bold !important;
848
+ color: #000000 !important;
849
+ margin: 12pt 0 6pt !important;
850
+ padding: 0 !important;
851
+ border: none !important;
852
+ }
853
+ .doc-h3, h3.doc-h3 {
854
+ font-size: 12pt !important;
855
+ font-weight: bold !important;
856
+ color: #000000 !important;
857
+ margin: 10pt 0 4pt !important;
858
+ }
859
+ .doc-p, p.doc-p {
860
+ font-size: 11pt !important;
861
+ line-height: 1.15 !important;
862
+ color: #000000 !important;
863
+ margin: 0 0 10pt !important;
864
+ text-align: left !important;
865
+ }
866
+ .doc-link-line, p.doc-link-line {
867
+ font-size: 11pt !important;
868
+ margin: 0 0 2pt !important;
869
+ padding: 0 !important;
870
+ }
871
+ .doc-link-anchor, a.doc-link-anchor {
872
+ color: #0563c1 !important;
873
+ text-decoration: underline !important;
874
+ }
875
+ .typewriter-paragraph {
876
+ white-space: pre-wrap;
877
+ word-break: break-word;
878
+ font-size: inherit !important;
879
+ line-height: inherit !important;
880
+ color: inherit !important;
881
+ }
882
+ .doc-ol-export { margin: 6pt 0 10pt; padding: 0; font-size: 11pt; color: #000000; }
883
+ .doc-ol-export-item {
884
+ margin: 0 0 2pt 0.5in !important;
885
+ text-indent: -0.25in !important;
886
+ font-size: 11pt !important;
887
+ line-height: 1.15 !important;
888
+ font-weight: normal !important;
889
+ color: #000000 !important;
890
+ }
891
+ .doc-ul-export-item {
892
+ margin: 0 0 2pt 0.75in !important;
893
+ text-indent: -0.25in !important;
894
+ font-size: 11pt !important;
895
+ line-height: 1.15 !important;
896
+ font-weight: normal !important;
897
+ color: #000000 !important;
898
+ }
899
+ th, td {
900
+ border: 1px solid #000000;
901
+ padding: 3pt 4pt !important;
902
+ font-size: 10pt !important;
903
+ color: #000000;
904
+ word-wrap: break-word !important;
905
+ overflow-wrap: break-word !important;
906
+ box-sizing: border-box;
907
+ }
908
+ th { font-weight: bold; background-color: #ffffff; }
909
+ .doc-th-green, th.doc-th-green { background-color: #c6efce !important; color: #000000 !important; }
910
+ .doc-th-red, th.doc-th-red { background-color: #ffc7ce !important; color: #000000 !important; }
911
+ .doc-chart-intro {
912
+ display: block;
913
+ width: ${A4_CONTENT_WIDTH_PT}pt !important;
914
+ max-width: ${A4_CONTENT_WIDTH_PT}pt !important;
915
+ margin: 0 0 10pt;
916
+ overflow: hidden;
917
+ box-sizing: border-box;
918
+ }
919
+ .doc-chart-intro__left,
920
+ .doc-chart-intro__right {
921
+ display: block;
922
+ float: none !important;
923
+ width: 100% !important;
924
+ max-width: ${A4_CONTENT_WIDTH_PT}pt !important;
925
+ margin: 0 0 8pt 0 !important;
926
+ overflow: visible;
927
+ box-sizing: border-box;
928
+ }
929
+ .doc-pie-chart-img { display: block; width: ${DOCX_PIE_CHART_MAX_WIDTH_PX}px; height: auto; max-width: ${DOCX_PIE_CHART_MAX_WIDTH_PX}px; max-height: ${DOCX_PIE_CHART_MAX_HEIGHT_PX}px; box-sizing: border-box; }
930
+ .doc-chart-intro-text { margin: 0; text-align: left; font-size: 11pt !important; }
931
+ .doc-images-block { overflow: hidden; margin: 0 0 10pt; max-width: ${A4_CONTENT_WIDTH_PX}px; width: 100%; box-sizing: border-box; }
932
+ .doc-float-icon { float: left; width: ${DOCX_FLOAT_ICON_SIZE_PX}px; height: ${DOCX_FLOAT_ICON_SIZE_PX}px; max-width: ${DOCX_FLOAT_ICON_SIZE_PX}px; max-height: ${DOCX_FLOAT_ICON_SIZE_PX}px; margin: 0 12pt 8pt 0; display: block; box-sizing: border-box; }
933
+ .doc-images-p { margin: 0 0 10pt; text-align: left; font-size: 11pt; line-height: 1.15; }
934
+ .success-row td { background-color: #f0f9eb; }
935
+ .info-row td { background-color: #e6f7ff; }
936
+ .warning-row td { background-color: #fdf6ec; }
937
+ .danger-row td { background-color: #fef0f0; }
938
+ img { max-width: 100%; height: auto; }
939
+ .docx-chart-wrap,
940
+ .echarts-container.docx-chart-wrap {
941
+ max-width: ${A4_CONTENT_WIDTH_PX}px;
942
+ margin: 12pt auto !important;
943
+ text-align: center !important;
944
+ page-break-inside: avoid;
945
+ page-break-before: auto;
946
+ box-sizing: border-box;
947
+ overflow: visible;
948
+ }
949
+ .docx-chart-wrap .docx-chart-img,
950
+ .docx-chart-wrap img {
951
+ display: block !important;
952
+ margin: 0 auto !important;
953
+ border: 0;
954
+ max-width: ${A4_CONTENT_WIDTH_PX}px;
955
+ }
956
+ button, .el-button {
957
+ display: inline-block !important;
958
+ padding: 4px 12px;
959
+ margin: 0 6px 6px 0;
960
+ border: 1px solid #dcdfe6;
961
+ border-radius: 4px;
962
+ font-size: 12px;
963
+ background: #fff;
964
+ color: #606266;
965
+ }
966
+ .el-button--danger { background: #fef0f0 !important; color: #f56c6c !important; border-color: #fbc4c4 !important; }
967
+ .el-button--success { background: #f0f9eb !important; color: #67c23a !important; }
968
+ .el-button--primary { background: #ecf5ff !important; color: #409eff !important; }
969
+ .section-title {
970
+ font-size: 18px;
971
+ font-weight: bold;
972
+ color: #1a1a1a;
973
+ border-top: 1px solid #e5e7eb;
974
+ padding-top: 24px;
975
+ margin-bottom: 20px;
976
+ }
977
+ </style>
978
+ </head>
979
+ <body><div class="docx-page-root">${bodyHtml}</div></body>
980
+ </html>`
981
+ }
982
+
983
+ const STYLE_PROPS = [
984
+ 'color',
985
+ 'fontSize',
986
+ 'fontFamily',
987
+ 'fontWeight',
988
+ 'fontStyle',
989
+ 'backgroundColor',
990
+ 'textAlign',
991
+ 'lineHeight',
992
+ 'marginTop',
993
+ 'marginBottom',
994
+ 'marginLeft',
995
+ 'marginRight',
996
+ 'paddingTop',
997
+ 'paddingBottom',
998
+ 'paddingLeft',
999
+ 'paddingRight',
1000
+ 'borderTop',
1001
+ 'borderBottom',
1002
+ 'borderLeft',
1003
+ 'borderRight',
1004
+ 'display',
1005
+ 'width',
1006
+ 'height',
1007
+ 'maxWidth',
1008
+ 'verticalAlign',
1009
+ 'whiteSpace',
1010
+ 'letterSpacing',
1011
+ ] as const
1012
+
1013
+ type InlineStyleMap = Record<string, string>
1014
+
1015
+ const DOC_CLASS_INLINE_STYLES: Record<string, InlineStyleMap> = {
1016
+ 'doc-h1': {
1017
+ fontSize: '20pt',
1018
+ fontWeight: 'bold',
1019
+ color: '#c00000',
1020
+ textAlign: 'left',
1021
+ margin: '0 0 12pt',
1022
+ padding: '0',
1023
+ border: 'none',
1024
+ },
1025
+ 'doc-h2': {
1026
+ fontSize: '14pt',
1027
+ fontWeight: 'bold',
1028
+ color: '#000000',
1029
+ margin: '12pt 0 6pt',
1030
+ padding: '0',
1031
+ border: 'none',
1032
+ },
1033
+ 'doc-h3': {
1034
+ fontSize: '12pt',
1035
+ fontWeight: 'bold',
1036
+ color: '#000000',
1037
+ margin: '10pt 0 4pt',
1038
+ },
1039
+ 'doc-p': {
1040
+ fontSize: '11pt',
1041
+ lineHeight: '1.15',
1042
+ color: '#000000',
1043
+ margin: '0 0 10pt',
1044
+ textAlign: 'left',
1045
+ },
1046
+ 'doc-link-line': {
1047
+ fontSize: '11pt',
1048
+ margin: '0 0 2pt',
1049
+ padding: '0',
1050
+ },
1051
+ 'doc-link-anchor': {
1052
+ color: '#0563c1',
1053
+ textDecoration: 'underline',
1054
+ },
1055
+ 'doc-th-green': {
1056
+ backgroundColor: '#c6efce',
1057
+ color: '#000000',
1058
+ fontWeight: 'bold',
1059
+ },
1060
+ 'doc-th-red': {
1061
+ backgroundColor: '#ffc7ce',
1062
+ color: '#000000',
1063
+ fontWeight: 'bold',
1064
+ },
1065
+ }
1066
+
1067
+ function applyInlineStyles(el: HTMLElement, styles: InlineStyleMap) {
1068
+ for (const [prop, val] of Object.entries(styles)) {
1069
+ ;(el.style as unknown as Record<string, string>)[prop] = val
1070
+ }
1071
+ }
1072
+
1073
+ function applyDocClassInlineStyles(el: HTMLElement) {
1074
+ for (const cls of el.classList) {
1075
+ const styles = DOC_CLASS_INLINE_STYLES[cls]
1076
+ if (styles) applyInlineStyles(el, styles)
1077
+ }
1078
+ }
1079
+
1080
+ function mountCloneForStyleRead(clone: HTMLElement): () => void {
1081
+ const host = document.createElement('div')
1082
+ host.setAttribute('data-docx-export-host', 'true')
1083
+ host.style.cssText = `position:fixed;left:-100000px;top:0;width:${A4_CONTENT_WIDTH_PX}px;max-width:100%;visibility:hidden;pointer-events:none;box-sizing:border-box;`
1084
+ host.appendChild(clone)
1085
+ document.body.appendChild(host)
1086
+ return () => {
1087
+ host.remove()
1088
+ }
1089
+ }
1090
+
1091
+ function clampAllImagesForDocx(root: HTMLElement) {
1092
+ root.querySelectorAll('img').forEach((node) => {
1093
+ if (!(node instanceof HTMLImageElement)) return
1094
+ const { width, height } = getDocxImageTargetSize(node)
1095
+ node.setAttribute('width', String(width))
1096
+ node.setAttribute('height', String(height))
1097
+ node.style.maxWidth = `${width}px`
1098
+ node.style.maxHeight = `${height}px`
1099
+ node.style.width = `${width}px`
1100
+ node.style.height = `${height}px`
1101
+ node.style.boxSizing = 'border-box'
1102
+ })
1103
+ }
1104
+
1105
+ function flattenChartIntroForDocx(root: HTMLElement) {
1106
+ root.querySelectorAll('.doc-chart-intro').forEach((intro) => {
1107
+ if (!(intro instanceof HTMLElement)) return
1108
+ intro.style.cssText = [
1109
+ 'display:block',
1110
+ `width:${A4_CONTENT_WIDTH_PT}pt`,
1111
+ `max-width:${A4_CONTENT_WIDTH_PT}pt`,
1112
+ 'margin:0 0 10pt',
1113
+ 'overflow:visible',
1114
+ 'box-sizing:border-box',
1115
+ ].join(';')
1116
+
1117
+ const left = intro.querySelector('.doc-chart-intro__left')
1118
+ const right = intro.querySelector('.doc-chart-intro__right')
1119
+ const img = intro.querySelector('.doc-pie-chart-img')
1120
+
1121
+ if (left instanceof HTMLElement) {
1122
+ left.style.cssText = [
1123
+ 'display:block',
1124
+ 'float:none',
1125
+ 'width:100%',
1126
+ `max-width:${A4_CONTENT_WIDTH_PT}pt`,
1127
+ 'margin:0 0 8pt 0',
1128
+ 'box-sizing:border-box',
1129
+ ].join(';')
1130
+ }
1131
+ if (img instanceof HTMLImageElement) {
1132
+ const { width, height } = getDocxImageTargetSize(img)
1133
+ const maxW = Math.min(width, Math.floor(A4_CONTENT_WIDTH_PT * 0.55))
1134
+ const maxH = DOCX_PIE_CHART_MAX_HEIGHT_PX
1135
+ img.style.cssText = [
1136
+ 'display:block',
1137
+ `width:${maxW}px`,
1138
+ `height:${height}px`,
1139
+ `max-width:${maxW}px`,
1140
+ `max-height:${maxH}px`,
1141
+ 'border:0',
1142
+ 'box-sizing:border-box',
1143
+ ].join(';')
1144
+ img.setAttribute('width', String(maxW))
1145
+ img.setAttribute('height', String(height))
1146
+ }
1147
+ if (right instanceof HTMLElement) {
1148
+ right.style.cssText = [
1149
+ 'display:block',
1150
+ 'float:none',
1151
+ 'width:100%',
1152
+ `max-width:${A4_CONTENT_WIDTH_PT}pt`,
1153
+ 'margin:0',
1154
+ 'overflow:visible',
1155
+ 'box-sizing:border-box',
1156
+ ].join(';')
1157
+ }
1158
+ })
1159
+ }
1160
+
1161
+ function flattenImagesBlockForDocx(root: HTMLElement) {
1162
+ root.querySelectorAll('.doc-images-block').forEach((block) => {
1163
+ if (!(block instanceof HTMLElement)) return
1164
+ block.style.overflow = 'hidden'
1165
+ block.style.maxWidth = `${A4_CONTENT_WIDTH_PX}px`
1166
+ block.style.width = '100%'
1167
+ block.style.boxSizing = 'border-box'
1168
+ })
1169
+
1170
+ root.querySelectorAll('.doc-float-icon').forEach((img) => {
1171
+ if (!(img instanceof HTMLImageElement)) return
1172
+ const size = DOCX_FLOAT_ICON_SIZE_PX
1173
+ img.style.cssText = `float:left;width:${size}px;height:${size}px;max-width:${size}px;max-height:${size}px;margin:0 12pt 8pt 0;display:block;border:0;box-sizing:border-box;`
1174
+ img.setAttribute('width', String(size))
1175
+ img.setAttribute('height', String(size))
1176
+ })
1177
+ }
1178
+
1179
+ const DOC_LIST_ITEM_STYLE =
1180
+ 'margin:0 0 2pt 0.5in;text-indent:-0.25in;font-size:11pt;line-height:1.15;font-weight:normal;color:#000000;'
1181
+ const DOC_BULLET_ITEM_STYLE =
1182
+ 'margin:0 0 2pt 0.75in;text-indent:-0.25in;font-size:11pt;line-height:1.15;font-weight:normal;color:#000000;'
1183
+
1184
+ function getListItemLabel(li: HTMLLIElement): string {
1185
+ const label = li.querySelector(':scope > .doc-ol-label')
1186
+ if (label?.textContent?.trim()) return label.textContent.trim()
1187
+
1188
+ const nestedUl = li.querySelector(':scope > ul.doc-ul')
1189
+ if (nestedUl) {
1190
+ const parts: string[] = []
1191
+ li.childNodes.forEach((node) => {
1192
+ if (node === nestedUl) return
1193
+ if (node.nodeType === Node.TEXT_NODE) {
1194
+ const text = node.textContent?.trim()
1195
+ if (text) parts.push(text)
1196
+ } else if (node instanceof HTMLElement && node.tagName !== 'UL') {
1197
+ const text = node.textContent?.trim()
1198
+ if (text) parts.push(text)
1199
+ }
1200
+ })
1201
+ if (parts.length) return parts.join(' ')
1202
+ }
1203
+
1204
+ return li.textContent?.trim() ?? ''
1205
+ }
1206
+
1207
+ /** Word MHT 无法正确处理嵌套 ol/ul,整表展平为手动编号/圆点段落 */
1208
+ function normalizeListsForDocx(root: HTMLElement) {
1209
+ root.querySelectorAll('ol.doc-ol').forEach((ol) => {
1210
+ if (!(ol instanceof HTMLOListElement)) return
1211
+
1212
+ const container = document.createElement('div')
1213
+ container.className = 'doc-ol-export'
1214
+ container.style.cssText = 'margin:6pt 0 10pt;padding:0;font-size:11pt;color:#000000;'
1215
+
1216
+ let index = 0
1217
+ ol.querySelectorAll(':scope > li').forEach((li) => {
1218
+ if (!(li instanceof HTMLLIElement)) return
1219
+ index++
1220
+
1221
+ const nestedUl = li.querySelector(':scope > ul.doc-ul')
1222
+ const mainText = getListItemLabel(li)
1223
+
1224
+ const numP = document.createElement('p')
1225
+ numP.className = 'doc-ol-export-item'
1226
+ numP.style.cssText = DOC_LIST_ITEM_STYLE
1227
+ numP.textContent = `${index}. ${mainText}`
1228
+ container.appendChild(numP)
1229
+
1230
+ if (nestedUl) {
1231
+ [...nestedUl.querySelectorAll(':scope > li')].forEach((item) => {
1232
+ const bulletP = document.createElement('p')
1233
+ bulletP.className = 'doc-ul-export-item'
1234
+ bulletP.style.cssText = DOC_BULLET_ITEM_STYLE
1235
+ bulletP.textContent = `• ${item.textContent?.trim() ?? ''}`
1236
+ container.appendChild(bulletP)
1237
+ })
1238
+ }
1239
+ })
1240
+
1241
+ ol.replaceWith(container)
1242
+ })
1243
+ }
1244
+
1245
+ /** 统计表格逻辑列数(含 colspan) */
1246
+ function getTableColumnCount(table: HTMLTableElement): number {
1247
+ let maxCols = 0
1248
+ Array.from(table.rows).forEach((row) => {
1249
+ let cols = 0
1250
+ Array.from(row.cells).forEach((cell) => {
1251
+ cols += cell.colSpan || 1
1252
+ })
1253
+ maxCols = Math.max(maxCols, cols)
1254
+ })
1255
+ return maxCols
1256
+ }
1257
+
1258
+ function getColumnWidthsPt(colCount: number): number[] {
1259
+ const total = A4_CONTENT_WIDTH_PT
1260
+ if (colCount === 3) {
1261
+ const c0 = Math.round(total * 0.42)
1262
+ const c1 = Math.round(total * 0.29)
1263
+ return [c0, c1, total - c0 - c1]
1264
+ }
1265
+ if (colCount === 5) {
1266
+ const c0 = Math.round(total * 0.26)
1267
+ const rest = Math.floor((total - c0) / 4)
1268
+ return [c0, rest, rest, rest, total - c0 - rest * 3]
1269
+ }
1270
+ const each = Math.floor(total / colCount)
1271
+ return Array.from({ length: colCount }, (_, index) =>
1272
+ index === colCount - 1 ? total - each * (colCount - 1) : each
1273
+ )
1274
+ }
1275
+
1276
+ function applyTableColgroupPt(table: HTMLTableElement, widthsPt: number[]) {
1277
+ table.querySelector('colgroup')?.remove()
1278
+ const colgroup = document.createElement('colgroup')
1279
+ widthsPt.forEach((width) => {
1280
+ const col = document.createElement('col')
1281
+ col.setAttribute('width', String(width))
1282
+ col.setAttribute(
1283
+ 'style',
1284
+ `width:${width}pt;mso-width-source:userset;mso-width-alt:${width * 20}`
1285
+ )
1286
+ colgroup.appendChild(col)
1287
+ })
1288
+ table.insertBefore(colgroup, table.firstChild)
1289
+ }
1290
+
1291
+ type DocxCellInfo = {
1292
+ text: string
1293
+ tag: 'th' | 'td'
1294
+ className: string
1295
+ textAlign: string
1296
+ }
1297
+
1298
+ /** 将含 colspan/rowspan 的表格展开为逻辑网格 */
1299
+ function expandTableToGrid(table: HTMLTableElement): DocxCellInfo[][] {
1300
+ const colCount = getTableColumnCount(table)
1301
+ const grid: (DocxCellInfo | null)[][] = []
1302
+
1303
+ Array.from(table.rows).forEach((row, rowIndex) => {
1304
+ if (!grid[rowIndex]) grid[rowIndex] = Array(colCount).fill(null)
1305
+ let colIndex = 0
1306
+
1307
+ Array.from(row.cells).forEach((cell) => {
1308
+ while (colIndex < colCount && grid[rowIndex]![colIndex] !== null) colIndex++
1309
+
1310
+ const info: DocxCellInfo = {
1311
+ text: cell.textContent?.trim() ?? '',
1312
+ tag: cell.tagName === 'TH' ? 'th' : 'td',
1313
+ className: cell.className,
1314
+ textAlign: (cell as HTMLElement).style.textAlign || '',
1315
+ }
1316
+ const colspan = cell.colSpan || 1
1317
+ const rowspan = cell.rowSpan || 1
1318
+
1319
+ for (let r = 0; r < rowspan; r++) {
1320
+ for (let c = 0; c < colspan; c++) {
1321
+ const targetRow = rowIndex + r
1322
+ if (!grid[targetRow]) grid[targetRow] = Array(colCount).fill(null)
1323
+ grid[targetRow]![colIndex + c] =
1324
+ r === 0 && c === 0 ? info : { ...info, text: '' }
1325
+ }
1326
+ }
1327
+ colIndex += colspan
1328
+ })
1329
+ })
1330
+
1331
+ return grid.filter((row) => row.some((cell) => cell !== null)) as DocxCellInfo[][]
1332
+ }
1333
+
1334
+ function applyWordTableStyles(table: HTMLTableElement, colCount: number) {
1335
+ const isComplex = table.classList.contains('doc-table-complex') || colCount >= 5
1336
+ const cellFontSize = isComplex ? '9pt' : '10pt'
1337
+ const cellPadding = isComplex ? '2pt 3pt' : '3pt 4pt'
1338
+ const colWidthsPt = getColumnWidthsPt(colCount)
1339
+
1340
+ applyTableColgroupPt(table, colWidthsPt)
1341
+
1342
+ table.setAttribute('width', String(A4_CONTENT_WIDTH_PT))
1343
+ table.style.cssText = [
1344
+ 'border-collapse:collapse',
1345
+ `width:${A4_CONTENT_WIDTH_PT}pt`,
1346
+ `max-width:${A4_CONTENT_WIDTH_PT}pt`,
1347
+ 'table-layout:fixed',
1348
+ 'mso-table-layout-alt:fixed',
1349
+ 'word-wrap:break-word',
1350
+ 'overflow-wrap:break-word',
1351
+ 'margin:0',
1352
+ 'border:1px solid #000000',
1353
+ 'box-sizing:border-box',
1354
+ 'page-break-inside:auto',
1355
+ ].join(';')
1356
+
1357
+ table.querySelectorAll('tr').forEach((row) => {
1358
+ ;(row as HTMLElement).style.pageBreakInside = 'auto'
1359
+ Array.from(row.cells).forEach((cell) => {
1360
+ cell.removeAttribute('colspan')
1361
+ cell.removeAttribute('rowspan')
1362
+ cell.style.border = '1px solid #000000'
1363
+ cell.style.padding = cellPadding
1364
+ cell.style.fontSize = cellFontSize
1365
+ cell.style.wordWrap = 'break-word'
1366
+ cell.style.overflowWrap = 'break-word'
1367
+ cell.style.boxSizing = 'border-box'
1368
+ cell.style.verticalAlign = 'top'
1369
+ cell.style.pageBreakInside = 'auto'
1370
+
1371
+ if (!cell.classList.contains('doc-th-green') && !cell.classList.contains('doc-th-red')) {
1372
+ cell.style.color = '#000000'
1373
+ }
1374
+ })
1375
+ })
1376
+ }
1377
+
1378
+ function createWordTableCell(info: DocxCellInfo, cellFontSize: string, cellPadding: string) {
1379
+ const cell = document.createElement(info.tag)
1380
+ cell.className = info.className
1381
+ cell.textContent = info.text
1382
+ cell.style.border = '1px solid #000000'
1383
+ cell.style.padding = cellPadding
1384
+ cell.style.fontSize = cellFontSize
1385
+ cell.style.wordWrap = 'break-word'
1386
+ cell.style.overflowWrap = 'break-word'
1387
+ cell.style.boxSizing = 'border-box'
1388
+ cell.style.verticalAlign = 'top'
1389
+ cell.style.pageBreakInside = 'auto'
1390
+ if (info.textAlign) cell.style.textAlign = info.textAlign
1391
+ if (!cell.classList.contains('doc-th-green') && !cell.classList.contains('doc-th-red')) {
1392
+ cell.style.color = '#000000'
1393
+ }
1394
+ return cell
1395
+ }
1396
+
1397
+ function rebuildFlatWordTable(
1398
+ grid: DocxCellInfo[][],
1399
+ tableClasses: string
1400
+ ): HTMLDivElement {
1401
+ const colCount = grid[0]?.length ?? 0
1402
+ const isComplex = tableClasses.includes('doc-table-complex') || colCount >= 5
1403
+ const cellFontSize = isComplex ? '9pt' : '10pt'
1404
+ const cellPadding = isComplex ? '2pt 3pt' : '3pt 4pt'
1405
+
1406
+ const wrap = document.createElement('div')
1407
+ wrap.className = 'docx-table-wrap'
1408
+ wrap.style.cssText = [
1409
+ `width:${A4_CONTENT_WIDTH_PT}pt`,
1410
+ `max-width:${A4_CONTENT_WIDTH_PT}pt`,
1411
+ 'margin:6pt 0 12pt',
1412
+ 'padding:0',
1413
+ 'overflow:hidden',
1414
+ 'box-sizing:border-box',
1415
+ ].join(';')
1416
+
1417
+ const table = document.createElement('table')
1418
+ table.className = `${tableClasses} doc-table-word`.trim()
1419
+ applyWordTableStyles(table, colCount)
1420
+
1421
+ grid.forEach((row) => {
1422
+ const tr = document.createElement('tr')
1423
+ tr.style.pageBreakInside = 'auto'
1424
+ row.forEach((info) => {
1425
+ tr.appendChild(createWordTableCell(info, cellFontSize, cellPadding))
1426
+ })
1427
+ table.appendChild(tr)
1428
+ })
1429
+
1430
+ wrap.appendChild(table)
1431
+ return wrap
1432
+ }
1433
+
1434
+ function tableHasSpanningCells(table: HTMLTableElement): boolean {
1435
+ return Array.from(table.querySelectorAll('th, td')).some((cell) => {
1436
+ const el = cell as HTMLTableCellElement
1437
+ return (el.colSpan || 1) > 1 || (el.rowSpan || 1) > 1
1438
+ })
1439
+ }
1440
+
1441
+ /** 重建 Word 安全表格:pt 宽度 + 无 colspan/rowspan + 外层 wrap 防溢出 */
1442
+ function rebuildTablesForWord(root: HTMLElement) {
1443
+ root.querySelectorAll('table.doc-table').forEach((node) => {
1444
+ if (!(node instanceof HTMLTableElement)) return
1445
+
1446
+ const tableClasses = [...node.classList]
1447
+ .filter((c) => c !== 'doc-table-word')
1448
+ .join(' ')
1449
+ const colCount = getTableColumnCount(node)
1450
+
1451
+ if (tableHasSpanningCells(node)) {
1452
+ const grid = expandTableToGrid(node)
1453
+ node.replaceWith(rebuildFlatWordTable(grid, tableClasses))
1454
+ return
1455
+ }
1456
+
1457
+ const wrap = document.createElement('div')
1458
+ wrap.className = 'docx-table-wrap'
1459
+ wrap.style.cssText = [
1460
+ `width:${A4_CONTENT_WIDTH_PT}pt`,
1461
+ `max-width:${A4_CONTENT_WIDTH_PT}pt`,
1462
+ 'margin:6pt 0 12pt',
1463
+ 'padding:0',
1464
+ 'overflow:hidden',
1465
+ 'box-sizing:border-box',
1466
+ ].join(';')
1467
+
1468
+ node.classList.add('doc-table-word')
1469
+ applyWordTableStyles(node, colCount)
1470
+ node.parentNode?.insertBefore(wrap, node)
1471
+ wrap.appendChild(node)
1472
+ })
1473
+ }
1474
+
1475
+ /** Word 导出移除预览分页标记,避免 MHT 中产生整页空白 */
1476
+ export function stripWordPageBreaks(root: HTMLElement) {
1477
+ root.querySelectorAll('[data-export-page-break]').forEach((el) => el.remove())
1478
+ }
1479
+
1480
+ /** @deprecated 使用 stripWordPageBreaks,注入分页符会在 Word 中产生大量空白 */
1481
+ export function injectWordPageBreaks(root: HTMLElement) {
1482
+ stripWordPageBreaks(root)
1483
+ }
1484
+
1485
+ /** 剥离 Vue / 无用属性,内联计算样式,尽量贴近页面 1:1 */
1486
+ export function prepareDomForDocx(root: HTMLElement, inPlace = false): HTMLElement {
1487
+ const clone = inPlace ? root : (root.cloneNode(true) as HTMLElement)
1488
+
1489
+ clone.querySelectorAll('.no-print, .animate-pulse, .typewriter-cursor').forEach((el) =>
1490
+ el.remove()
1491
+ )
1492
+
1493
+ const unmount = mountCloneForStyleRead(clone)
1494
+
1495
+ try {
1496
+ clone.querySelectorAll('*').forEach((el) => {
1497
+ if (!(el instanceof HTMLElement)) return
1498
+
1499
+ applyDocClassInlineStyles(el)
1500
+
1501
+ const computed = window.getComputedStyle(el)
1502
+ if (computed.display === 'flex' || computed.display === 'inline-flex') {
1503
+ el.style.display = 'block'
1504
+ }
1505
+
1506
+ for (const prop of STYLE_PROPS) {
1507
+ const val = computed[prop]
1508
+ if (val && val !== 'initial' && val !== 'normal' && val !== 'auto' && val !== 'none') {
1509
+ const styleRecord = el.style as unknown as Record<string, string>
1510
+ if (!styleRecord[prop]) {
1511
+ styleRecord[prop] = val
1512
+ }
1513
+ }
1514
+ }
1515
+
1516
+ if (el.tagName === 'TABLE') {
1517
+ el.style.borderCollapse = 'collapse'
1518
+ el.style.tableLayout = 'fixed'
1519
+ el.style.width = `${A4_CONTENT_WIDTH_PT}pt`
1520
+ el.style.maxWidth = `${A4_CONTENT_WIDTH_PT}pt`
1521
+ el.style.pageBreakInside = 'auto'
1522
+ }
1523
+ if (el.tagName === 'TD' || el.tagName === 'TH') {
1524
+ el.style.border = '1px solid #000000'
1525
+ el.style.padding = '4pt 6pt'
1526
+ el.style.fontSize = '11pt'
1527
+ if (!el.classList.contains('doc-th-green') && !el.classList.contains('doc-th-red')) {
1528
+ el.style.color = '#000000'
1529
+ }
1530
+ }
1531
+ if (el.tagName === 'IMG') {
1532
+ const img = el as HTMLImageElement
1533
+ el.style.display = el.style.display || 'block'
1534
+ el.style.maxWidth = el.style.maxWidth || '100%'
1535
+ if (!img.getAttribute('width') && img.naturalWidth > 0) {
1536
+ img.setAttribute('width', String(img.naturalWidth))
1537
+ }
1538
+ if (!img.getAttribute('height') && img.naturalHeight > 0) {
1539
+ img.setAttribute('height', String(img.naturalHeight))
1540
+ }
1541
+ }
1542
+ })
1543
+ } finally {
1544
+ unmount()
1545
+ }
1546
+
1547
+ clone.querySelectorAll('*').forEach((el) => {
1548
+ if (!(el instanceof HTMLElement)) return
1549
+ ;[...el.attributes].forEach((attr) => {
1550
+ if (
1551
+ attr.name.startsWith('data-v-') ||
1552
+ attr.name === 'data-v-app' ||
1553
+ attr.name.startsWith('data-cy') ||
1554
+ attr.name === 'data-docx-export-host'
1555
+ ) {
1556
+ el.removeAttribute(attr.name)
1557
+ }
1558
+ })
1559
+ })
1560
+
1561
+ clampAllImagesForDocx(clone)
1562
+ flattenChartIntroForDocx(clone)
1563
+ flattenImagesBlockForDocx(clone)
1564
+ normalizeListsForDocx(clone)
1565
+ rebuildTablesForWord(clone)
1566
+
1567
+ clone.querySelectorAll('.typewriter-card__body, .content-wrapper, .doc-sample-body').forEach((el) => {
1568
+ if (!(el instanceof HTMLElement)) return
1569
+ el.style.minHeight = '0'
1570
+ el.style.height = 'auto'
1571
+ el.style.padding = '0'
1572
+ el.style.maxWidth = `${A4_CONTENT_WIDTH_PT}pt`
1573
+ el.style.width = `${A4_CONTENT_WIDTH_PT}pt`
1574
+ el.style.boxSizing = 'border-box'
1575
+ })
1576
+
1577
+ clone.classList.add('docx-page-root')
1578
+ clone.style.maxHeight = 'none'
1579
+ clone.style.overflow = 'visible'
1580
+ clone.style.border = 'none'
1581
+ clone.style.padding = '0'
1582
+ clone.style.minHeight = '0'
1583
+ clone.style.backgroundColor = '#ffffff'
1584
+ clone.style.boxSizing = 'border-box'
1585
+ clone.style.width = `${A4_CONTENT_WIDTH_PT}pt`
1586
+ clone.style.maxWidth = `${A4_CONTENT_WIDTH_PT}pt`
1587
+ clone.style.margin = '0'
1588
+ clone.style.fontFamily = 'Calibri, "Segoe UI", Arial, Helvetica, sans-serif'
1589
+ clone.style.fontSize = '11pt'
1590
+ clone.style.lineHeight = '1.15'
1591
+ clone.style.color = '#000000'
1592
+
1593
+ clone.querySelectorAll('.echarts-container').forEach((el) => {
1594
+ if (!(el instanceof HTMLElement)) return
1595
+ if (el.querySelector('.docx-chart-wrap')) return
1596
+ el.style.width = '100%'
1597
+ el.style.maxWidth = '100%'
1598
+ el.style.textAlign = 'center'
1599
+ el.style.margin = '12pt auto'
1600
+ })
1601
+
1602
+ return clone
1603
+ }
1604
+
1605
+ export async function exportHtmlToDocx(
1606
+ fullHtml: string,
1607
+ options: DocxExportOptions = DEFAULT_DOCX_EXPORT_OPTIONS
1608
+ ): Promise<Blob> {
1609
+ return buildDocxZip(fullHtml, { ...DEFAULT_DOCX_EXPORT_OPTIONS, ...options })
1610
+ }