aegon-lv97 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/gen-ai/build-report/chartStreamRefs.ts +227 -0
- package/gen-ai/build-report/coherence-check/index.vue +559 -0
- package/gen-ai/build-report/components/ReportChapterWorkspace.vue +4571 -0
- package/gen-ai/build-report/components/ReportConfigPanel.vue +727 -0
- package/gen-ai/build-report/components/ReportGenerateSettingsPanel.vue +2361 -0
- package/gen-ai/build-report/components/ReportGeneratingPanel.vue +1765 -0
- package/gen-ai/build-report/components/ReportPromptPanel.vue +625 -0
- package/gen-ai/build-report/components/ReportSourceFileSummary.vue +665 -0
- package/gen-ai/build-report/components/ReportSourcesPanel.vue +1327 -0
- package/gen-ai/build-report/components/ReportTemplateDetail.vue +191 -0
- package/gen-ai/build-report/components/ReportTemplateEditorFormPanel.vue +667 -0
- package/gen-ai/build-report/components/ReportTemplatePicker.vue +302 -0
- package/gen-ai/build-report/components/ReportWorkflowSidebar.vue +148 -0
- package/gen-ai/build-report/components/paramsSetting.vue +886 -0
- package/gen-ai/build-report/components/reportEdit.vue +2283 -0
- package/gen-ai/build-report/index.vue +856 -0
- package/gen-ai/build-report/report-domain.ts +1133 -0
- package/gen-ai/build-report/report-template-data.ts +295 -0
- package/gen-ai/build-report/useReportWorkflow.ts +1386 -0
- package/gen-ai/common/assets/NotoSans-SC.woff2 +0 -0
- package/gen-ai/common/assets/NotoSans-TC.woff2 +0 -0
- package/gen-ai/common/assets/NotoSans.woff2 +0 -0
- package/gen-ai/common/assets/fonts.css +278 -0
- package/gen-ai/common/assets/pagination.css +163 -0
- package/gen-ai/common/directives/btnLoading.ts +455 -0
- package/gen-ai/common/directives/debounce.ts +98 -0
- package/gen-ai/common/directives/index.ts +30 -0
- package/gen-ai/common/en.md +417 -0
- package/gen-ai/common/index.ts +36 -0
- package/gen-ai/common/types/component-expose.ts +9 -0
- package/gen-ai/common/utils/api-res-status.ts +7 -0
- package/gen-ai/common/utils/navigation.ts +59 -0
- package/gen-ai/common/utils/remote-request.ts +45 -0
- package/gen-ai/common/utils/scroll.ts +577 -0
- package/gen-ai/components/AIExport/docx-export.ts +950 -0
- package/gen-ai/components/AIExport/docxExport.ts +2566 -0
- package/gen-ai/components/AIExport/index.ts +52 -0
- package/gen-ai/components/AIExport/parseMarkdownHtml.ts +653 -0
- package/gen-ai/components/CapsuleScrollbar.vue +149 -0
- package/gen-ai/components/ChapterTitleScroll.vue +468 -0
- package/gen-ai/components/EntryHeroNavCards.vue +379 -0
- package/gen-ai/components/GenAiPagination.vue +52 -0
- package/gen-ai/components/ModelCard.vue +1114 -0
- package/gen-ai/components/Preview/FilePreviewPage.vue +949 -0
- package/gen-ai/components/Preview/PreviewCapsuleScroll.vue +205 -0
- package/gen-ai/components/Preview/PreviewOutlineTree.vue +148 -0
- package/gen-ai/components/Preview/index.ts +11 -0
- package/gen-ai/components/Preview/preview.ts +724 -0
- package/gen-ai/components/PromptField.vue +505 -0
- package/gen-ai/components/RecentReportsTable.vue +974 -0
- package/gen-ai/components/ReportCollapsibleSection.vue +172 -0
- package/gen-ai/components/ReportHistoryList.vue +637 -0
- package/gen-ai/components/ReportSectionConfirmPopover.vue +251 -0
- package/gen-ai/components/editor/ChapterTiptapEditor.vue +3718 -0
- package/gen-ai/components/editor/StructureBlockView.vue +441 -0
- package/gen-ai/components/editor/TiptapToolbar.vue +1036 -0
- package/gen-ai/components/editor/TypewriterDocEditor.vue +422 -0
- package/gen-ai/components/editor/_StructureOrgNode.vue +326 -0
- package/gen-ai/components/editor/index.ts +28 -0
- package/gen-ai/components/editor/parseMarkdownHtml.ts +754 -0
- package/gen-ai/components/editor/reviewCitationTip.ts +163 -0
- package/gen-ai/gen-entry/api/index.ts +32 -0
- package/gen-ai/gen-entry/index.vue +287 -0
- package/gen-ai/gen-entry/types/index.ts +4 -0
- package/gen-ai/i.mark +2 -0
- package/gen-ai/management-center/index.vue +1815 -0
- package/gen-ai/management-center/mockData.ts +248 -0
- package/gen-ai/management-center/types/index.ts +37 -0
- package/gen-ai/model-manager/TemplateEditorFormPanel.vue +661 -0
- package/gen-ai/model-manager/api/index.ts +32 -0
- package/gen-ai/model-manager/api/mockApi.ts +33 -0
- package/gen-ai/model-manager/index.vue +1583 -0
- package/gen-ai/model-manager/mockData.ts +177 -0
- package/gen-ai/model-manager/template-detail-shared.ts +154 -0
- package/gen-ai/model-manager/template-editor.vue +345 -0
- package/gen-ai/model-manager/types/index.ts +40 -0
- package/gen-ai/model-manager/utils.ts +25 -0
- package/package.json +12 -0
|
@@ -0,0 +1,2566 @@
|
|
|
1
|
+
import JSZip from 'jszip'
|
|
2
|
+
import html2canvas from 'html2canvas'
|
|
3
|
+
|
|
4
|
+
/** 純 text/html altChunk,不再使用 MHT(避免 --------tDocumentpart-- 殘片) */
|
|
5
|
+
|
|
6
|
+
/** OOXML 根關係(與 html-docx-js 一致) */
|
|
7
|
+
const PACKAGE_RELS_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
8
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
9
|
+
<Relationship
|
|
10
|
+
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
|
|
11
|
+
Target="/word/document.xml" Id="R09c83fafc067488e" />
|
|
12
|
+
</Relationships>
|
|
13
|
+
`
|
|
14
|
+
|
|
15
|
+
/** 與 html-docx-js defaultMargins 數值對齊(twips) */
|
|
16
|
+
export const defaultMargins = {
|
|
17
|
+
top: 1440,
|
|
18
|
+
right: 1440,
|
|
19
|
+
bottom: 1440,
|
|
20
|
+
left: 1440,
|
|
21
|
+
header: 720,
|
|
22
|
+
footer: 720,
|
|
23
|
+
gutter: 0,
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
export type DocxMargins = {
|
|
27
|
+
top: number
|
|
28
|
+
right: number
|
|
29
|
+
bottom: number
|
|
30
|
+
left: number
|
|
31
|
+
header: number
|
|
32
|
+
footer: number
|
|
33
|
+
gutter: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DocxHeaderFooterOptions {
|
|
37
|
+
/** 页眉右上角文字,默认「商密三级」(对齐行业调研导出模板) */
|
|
38
|
+
headerText?: string
|
|
39
|
+
/** 页脚是否显示「当前页 / 总页数」,默认 true */
|
|
40
|
+
showPageNumbers?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DocxExportOptions {
|
|
44
|
+
orientation?: 'portrait' | 'landscape'
|
|
45
|
+
margins?: Partial<DocxMargins>
|
|
46
|
+
/** 页面宽度(twips),默认 A4 竖向 */
|
|
47
|
+
pageWidth?: number
|
|
48
|
+
/** 页面高度(twips),默认 A4 竖向 */
|
|
49
|
+
pageHeight?: number
|
|
50
|
+
headerFooter?: DocxHeaderFooterOptions
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A4 竖向:210mm × 297mm(Word twips,1 inch = 1440 twips) */
|
|
54
|
+
export const A4_PORTRAIT = {
|
|
55
|
+
width: 11906,
|
|
56
|
+
height: 16838,
|
|
57
|
+
} as const
|
|
58
|
+
|
|
59
|
+
/** A4 常用 2cm 页边距 ≈ 1134 twips;页眉靠近页顶(约 0.75cm) */
|
|
60
|
+
export const A4_MARGINS_2CM = {
|
|
61
|
+
top: 1134,
|
|
62
|
+
right: 1134,
|
|
63
|
+
bottom: 1134,
|
|
64
|
+
left: 1134,
|
|
65
|
+
header: 425,
|
|
66
|
+
footer: 720,
|
|
67
|
+
gutter: 0,
|
|
68
|
+
} as const
|
|
69
|
+
|
|
70
|
+
/** 可打印区域宽度(px@96dpi):210mm - 左右各 2cm ≈ 643px */
|
|
71
|
+
export const A4_CONTENT_WIDTH_PX = Math.round(((210 - 40) / 25.4) * 96)
|
|
72
|
+
|
|
73
|
+
/** Word 可打印区宽度(pt):210mm - 左右各 2cm ≈ 482pt */
|
|
74
|
+
export const A4_CONTENT_WIDTH_PT = Math.round(((210 - 40) / 25.4) * 72)
|
|
75
|
+
|
|
76
|
+
/** 图表导出高度(与页面 400px 比例协调,且可落在单页 A4 内) */
|
|
77
|
+
export const A4_CHART_HEIGHT_PX = 380
|
|
78
|
+
|
|
79
|
+
/** 单张图表在 A4 可打印区域内的最大高度 */
|
|
80
|
+
export const A4_CHART_MAX_HEIGHT_PX = Math.round(((297 - 4) / 25.4) * 96 * 0.42)
|
|
81
|
+
|
|
82
|
+
/** 左浮动无障碍图标(固定尺寸,不超出页边距) */
|
|
83
|
+
export const DOCX_FLOAT_ICON_SIZE_PX = 72
|
|
84
|
+
|
|
85
|
+
/** 饼图左栏最大宽度:A4 可打印区 42% 减去间距 */
|
|
86
|
+
export const DOCX_PIE_CHART_MAX_WIDTH_PX = Math.min(
|
|
87
|
+
220,
|
|
88
|
+
Math.floor(A4_CONTENT_WIDTH_PX * 0.42) - 16
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
/** 饼图最大高度(避免 Word 单页溢出) */
|
|
92
|
+
export const DOCX_PIE_CHART_MAX_HEIGHT_PX = Math.min(
|
|
93
|
+
120,
|
|
94
|
+
Math.floor(A4_CHART_MAX_HEIGHT_PX * 0.32)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
export const DEFAULT_DOCX_EXPORT_OPTIONS: DocxExportOptions = {
|
|
98
|
+
orientation: 'portrait',
|
|
99
|
+
pageWidth: A4_PORTRAIT.width,
|
|
100
|
+
pageHeight: A4_PORTRAIT.height,
|
|
101
|
+
margins: { ...A4_MARGINS_2CM },
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function mergeMargins(margins?: Partial<DocxMargins>): DocxMargins {
|
|
105
|
+
return { ...defaultMargins, ...margins }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolvePageSize(options: DocxExportOptions) {
|
|
109
|
+
const orientation = options.orientation ?? 'portrait'
|
|
110
|
+
if (options.pageWidth && options.pageHeight) {
|
|
111
|
+
return { width: options.pageWidth, height: options.pageHeight, orientation }
|
|
112
|
+
}
|
|
113
|
+
if (orientation === 'landscape') {
|
|
114
|
+
return { width: A4_PORTRAIT.height, height: A4_PORTRAIT.width, orientation }
|
|
115
|
+
}
|
|
116
|
+
return { width: A4_PORTRAIT.width, height: A4_PORTRAIT.height, orientation }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 不再組 MHT。Word 官方支持 altChunk + text/html;
|
|
121
|
+
* MHT(message/rfc822)在遷移環境極易導入失敗,正文只剩 --------tDocumentpart--。
|
|
122
|
+
*/
|
|
123
|
+
function encodeHtmlAltChunk(htmlSource: string): Uint8Array {
|
|
124
|
+
const html = String(htmlSource || '').trim()
|
|
125
|
+
if (!html) throw new Error('Word 導出失敗:HTML 為空')
|
|
126
|
+
// UTF-8 BOM:提升 Word / 部分環境對 charset 的識別
|
|
127
|
+
const bom = new Uint8Array([0xef, 0xbb, 0xbf])
|
|
128
|
+
const body = new TextEncoder().encode(html)
|
|
129
|
+
const out = new Uint8Array(bom.length + body.length)
|
|
130
|
+
out.set(bom, 0)
|
|
131
|
+
out.set(body, bom.length)
|
|
132
|
+
return out
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function loadImageNaturalSize(
|
|
136
|
+
dataUrl: string
|
|
137
|
+
): Promise<{ width: number; height: number }> {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const probe = new Image()
|
|
140
|
+
probe.onload = () =>
|
|
141
|
+
resolve({ width: probe.naturalWidth, height: probe.naturalHeight })
|
|
142
|
+
probe.onerror = () => reject(new Error('chart image load failed'))
|
|
143
|
+
probe.src = dataUrl
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 将图表缩放到 A4 页边距内,避免 Word 裁切或撑出页面 */
|
|
148
|
+
function fitChartToA4Content(naturalWidth: number, naturalHeight: number) {
|
|
149
|
+
const maxW = A4_CONTENT_WIDTH_PX
|
|
150
|
+
const maxH = Math.min(A4_CHART_HEIGHT_PX, A4_CHART_MAX_HEIGHT_PX)
|
|
151
|
+
|
|
152
|
+
let width = maxW
|
|
153
|
+
let height = Math.round((naturalHeight / naturalWidth) * width)
|
|
154
|
+
|
|
155
|
+
if (height > maxH) {
|
|
156
|
+
height = maxH
|
|
157
|
+
width = Math.round((naturalWidth / naturalHeight) * height)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { width, height }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 图表容器:Word 需使用数值 width/height(勿用 100% 属性,否则会只显示一半) */
|
|
164
|
+
async function createChartBlock(dataUrl: string): Promise<HTMLDivElement> {
|
|
165
|
+
const natural = await loadImageNaturalSize(dataUrl)
|
|
166
|
+
const { width, height } = fitChartToA4Content(natural.width, natural.height)
|
|
167
|
+
|
|
168
|
+
const wrap = document.createElement('div')
|
|
169
|
+
wrap.className = 'docx-chart-wrap'
|
|
170
|
+
wrap.style.cssText = [
|
|
171
|
+
`width:${width}px`,
|
|
172
|
+
`max-width:${A4_CONTENT_WIDTH_PX}px`,
|
|
173
|
+
'margin:12pt auto',
|
|
174
|
+
'text-align:center',
|
|
175
|
+
'page-break-inside:avoid',
|
|
176
|
+
'box-sizing:border-box',
|
|
177
|
+
'overflow:visible',
|
|
178
|
+
].join(';')
|
|
179
|
+
|
|
180
|
+
const img = document.createElement('img')
|
|
181
|
+
img.className = 'docx-chart-img'
|
|
182
|
+
img.src = dataUrl
|
|
183
|
+
img.setAttribute('width', String(width))
|
|
184
|
+
img.setAttribute('height', String(height))
|
|
185
|
+
img.style.cssText = [
|
|
186
|
+
`width:${width}px`,
|
|
187
|
+
`height:${height}px`,
|
|
188
|
+
`max-width:${A4_CONTENT_WIDTH_PX}px`,
|
|
189
|
+
'display:block',
|
|
190
|
+
'margin:0 auto',
|
|
191
|
+
'border:0',
|
|
192
|
+
].join(';')
|
|
193
|
+
|
|
194
|
+
wrap.appendChild(img)
|
|
195
|
+
return wrap
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** ECharts:按 A4 可打印区导出完整图表,避免裁切与溢出 */
|
|
199
|
+
export async function rasterizeEchartsForDocx(
|
|
200
|
+
sourceRoot: HTMLElement,
|
|
201
|
+
targetRoot: HTMLElement,
|
|
202
|
+
echartsApi: {
|
|
203
|
+
getInstanceByDom: (el: HTMLElement) => {
|
|
204
|
+
resize: (opts?: { width?: number; height?: number }) => void
|
|
205
|
+
getDataURL: (opts: object) => string
|
|
206
|
+
} | undefined
|
|
207
|
+
}
|
|
208
|
+
) {
|
|
209
|
+
const sourceCharts = sourceRoot.querySelectorAll('.echarts-container')
|
|
210
|
+
const targetCharts = targetRoot.querySelectorAll('.echarts-container')
|
|
211
|
+
const exportW = A4_CONTENT_WIDTH_PX
|
|
212
|
+
const exportH = A4_CHART_HEIGHT_PX
|
|
213
|
+
|
|
214
|
+
for (let i = 0; i < sourceCharts.length; i++) {
|
|
215
|
+
const sourceEl = sourceCharts[i] as HTMLElement
|
|
216
|
+
const targetEl = targetCharts[i] as HTMLElement
|
|
217
|
+
if (!targetEl) continue
|
|
218
|
+
|
|
219
|
+
const prevWidth = sourceEl.style.width
|
|
220
|
+
const prevHeight = sourceEl.style.height
|
|
221
|
+
const prevMaxWidth = sourceEl.style.maxWidth
|
|
222
|
+
|
|
223
|
+
let dataUrl = ''
|
|
224
|
+
const inst = echartsApi.getInstanceByDom(sourceEl)
|
|
225
|
+
if (inst) {
|
|
226
|
+
try {
|
|
227
|
+
sourceEl.style.width = `${exportW}px`
|
|
228
|
+
sourceEl.style.height = `${exportH}px`
|
|
229
|
+
sourceEl.style.maxWidth = `${exportW}px`
|
|
230
|
+
inst.resize({ width: exportW, height: exportH })
|
|
231
|
+
await new Promise((r) => setTimeout(r, 320))
|
|
232
|
+
dataUrl = inst.getDataURL({
|
|
233
|
+
type: 'png',
|
|
234
|
+
pixelRatio: 1,
|
|
235
|
+
backgroundColor: '#ffffff',
|
|
236
|
+
excludeComponents: ['toolbox', 'dataZoom'],
|
|
237
|
+
})
|
|
238
|
+
} finally {
|
|
239
|
+
sourceEl.style.width = prevWidth
|
|
240
|
+
sourceEl.style.height = prevHeight
|
|
241
|
+
sourceEl.style.maxWidth = prevMaxWidth
|
|
242
|
+
inst.resize()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!dataUrl) {
|
|
247
|
+
const canvas = sourceEl.querySelector('canvas')
|
|
248
|
+
if (canvas instanceof HTMLCanvasElement) {
|
|
249
|
+
dataUrl = canvas.toDataURL('image/png')
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!dataUrl) continue
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const chartBlock = await createChartBlock(dataUrl)
|
|
257
|
+
chartBlock.classList.add('echarts-container')
|
|
258
|
+
targetEl.replaceWith(chartBlock)
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.warn('Failed to rasterize echarts for docx:', e)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function svgToPngImg(svg: SVGSVGElement): Promise<HTMLImageElement> {
|
|
266
|
+
const clone = svg.cloneNode(true) as SVGSVGElement
|
|
267
|
+
if (!clone.getAttribute('xmlns')) {
|
|
268
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
|
|
269
|
+
}
|
|
270
|
+
const rect = svg.getBoundingClientRect()
|
|
271
|
+
const w = Math.max(rect.width, 16)
|
|
272
|
+
const h = Math.max(rect.height, 16)
|
|
273
|
+
clone.setAttribute('width', String(w))
|
|
274
|
+
clone.setAttribute('height', String(h))
|
|
275
|
+
|
|
276
|
+
const svgData = new XMLSerializer().serializeToString(clone)
|
|
277
|
+
const url = URL.createObjectURL(
|
|
278
|
+
new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
return await new Promise((resolve, reject) => {
|
|
283
|
+
const img = new Image()
|
|
284
|
+
img.onload = () => {
|
|
285
|
+
const canvas = document.createElement('canvas')
|
|
286
|
+
canvas.width = w * 2
|
|
287
|
+
canvas.height = h * 2
|
|
288
|
+
const ctx = canvas.getContext('2d')
|
|
289
|
+
ctx?.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
290
|
+
const wordImg = document.createElement('img')
|
|
291
|
+
wordImg.src = canvas.toDataURL('image/png')
|
|
292
|
+
wordImg.style.width = `${w}px`
|
|
293
|
+
wordImg.style.height = `${h}px`
|
|
294
|
+
wordImg.style.verticalAlign = 'middle'
|
|
295
|
+
wordImg.style.display = 'inline-block'
|
|
296
|
+
resolve(wordImg)
|
|
297
|
+
}
|
|
298
|
+
img.onerror = () => reject(new Error('SVG load failed'))
|
|
299
|
+
img.src = url
|
|
300
|
+
})
|
|
301
|
+
} finally {
|
|
302
|
+
URL.revokeObjectURL(url)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Element Plus 图标等:html2canvas 栅格化,避免 <use> 外链丢失 */
|
|
307
|
+
export async function rasterizeIconsForDocx(
|
|
308
|
+
sourceRoot: HTMLElement,
|
|
309
|
+
targetRoot: HTMLElement
|
|
310
|
+
) {
|
|
311
|
+
const sourceIcons = sourceRoot.querySelectorAll('.el-icon')
|
|
312
|
+
const targetIcons = targetRoot.querySelectorAll('.el-icon')
|
|
313
|
+
|
|
314
|
+
for (let i = 0; i < sourceIcons.length; i++) {
|
|
315
|
+
const src = sourceIcons[i] as HTMLElement
|
|
316
|
+
const tgt = targetIcons[i] as HTMLElement
|
|
317
|
+
if (!tgt) continue
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const canvas = await html2canvas(src, {
|
|
321
|
+
backgroundColor: null,
|
|
322
|
+
scale: 2,
|
|
323
|
+
logging: false,
|
|
324
|
+
})
|
|
325
|
+
const img = document.createElement('img')
|
|
326
|
+
img.src = canvas.toDataURL('image/png')
|
|
327
|
+
img.setAttribute('width', '24')
|
|
328
|
+
img.setAttribute('height', '24')
|
|
329
|
+
img.style.width = '24px'
|
|
330
|
+
img.style.height = '24px'
|
|
331
|
+
img.style.verticalAlign = 'middle'
|
|
332
|
+
img.style.display = 'inline-block'
|
|
333
|
+
tgt.replaceWith(img)
|
|
334
|
+
} catch {
|
|
335
|
+
const svg = src.querySelector('svg')
|
|
336
|
+
if (svg) {
|
|
337
|
+
try {
|
|
338
|
+
const img = await svgToPngImg(svg)
|
|
339
|
+
tgt.replaceWith(img)
|
|
340
|
+
} catch {
|
|
341
|
+
/* keep original */
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 图标替换后剩余的独立 SVG(如 Lucide)转 PNG */
|
|
349
|
+
export async function rasterizeStandaloneSvgsForDocx(targetRoot: HTMLElement) {
|
|
350
|
+
const svgs = Array.from(targetRoot.querySelectorAll('svg'))
|
|
351
|
+
for (const svg of svgs) {
|
|
352
|
+
try {
|
|
353
|
+
const img = await svgToPngImg(svg)
|
|
354
|
+
svg.replaceWith(img)
|
|
355
|
+
} catch {
|
|
356
|
+
/* keep */
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function fetchImageAsDataUrl(url: string): Promise<string> {
|
|
362
|
+
const res = await fetch(url)
|
|
363
|
+
if (!res.ok) throw new Error(`fetch image failed: ${res.status}`)
|
|
364
|
+
const blob = await res.blob()
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
const reader = new FileReader()
|
|
367
|
+
reader.onload = () => resolve(reader.result as string)
|
|
368
|
+
reader.onerror = () => reject(new Error('read image blob failed'))
|
|
369
|
+
reader.readAsDataURL(blob)
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function getDocxImageTargetSize(img: HTMLImageElement): {
|
|
374
|
+
width: number
|
|
375
|
+
height: number
|
|
376
|
+
} {
|
|
377
|
+
const naturalW = img.naturalWidth || Number.parseInt(img.getAttribute('width') || '0', 10) || 100
|
|
378
|
+
const naturalH = img.naturalHeight || Number.parseInt(img.getAttribute('height') || '0', 10) || 100
|
|
379
|
+
|
|
380
|
+
if (img.classList.contains('doc-float-icon')) {
|
|
381
|
+
return {
|
|
382
|
+
width: DOCX_FLOAT_ICON_SIZE_PX,
|
|
383
|
+
height: DOCX_FLOAT_ICON_SIZE_PX,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (img.classList.contains('doc-pie-chart-img')) {
|
|
388
|
+
let width = Math.min(
|
|
389
|
+
DOCX_PIE_CHART_MAX_WIDTH_PX,
|
|
390
|
+
Number.parseInt(img.getAttribute('width') || '0', 10) || DOCX_PIE_CHART_MAX_WIDTH_PX,
|
|
391
|
+
A4_CONTENT_WIDTH_PX
|
|
392
|
+
)
|
|
393
|
+
let height = Math.round((naturalH / naturalW) * width)
|
|
394
|
+
if (height > DOCX_PIE_CHART_MAX_HEIGHT_PX) {
|
|
395
|
+
height = DOCX_PIE_CHART_MAX_HEIGHT_PX
|
|
396
|
+
width = Math.round((naturalW / naturalH) * height)
|
|
397
|
+
}
|
|
398
|
+
return { width, height }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (naturalW > A4_CONTENT_WIDTH_PX) {
|
|
402
|
+
return {
|
|
403
|
+
width: A4_CONTENT_WIDTH_PX,
|
|
404
|
+
height: Math.round((naturalH / naturalW) * A4_CONTENT_WIDTH_PX),
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return { width: naturalW, height: naturalH }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function imgElementToDataUrl(
|
|
412
|
+
img: HTMLImageElement,
|
|
413
|
+
targetW: number,
|
|
414
|
+
targetH: number
|
|
415
|
+
): Promise<string> {
|
|
416
|
+
return new Promise((resolve, reject) => {
|
|
417
|
+
const draw = () => {
|
|
418
|
+
if (!targetW || !targetH) {
|
|
419
|
+
reject(new Error('invalid image dimensions'))
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
const canvas = document.createElement('canvas')
|
|
423
|
+
canvas.width = targetW
|
|
424
|
+
canvas.height = targetH
|
|
425
|
+
const ctx = canvas.getContext('2d')
|
|
426
|
+
if (!ctx) {
|
|
427
|
+
reject(new Error('no canvas context'))
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
ctx.drawImage(img, 0, 0, targetW, targetH)
|
|
431
|
+
resolve(canvas.toDataURL('image/png'))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (img.complete && img.naturalWidth > 0) {
|
|
435
|
+
draw()
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const prevOnload = img.onload
|
|
440
|
+
const prevOnerror = img.onerror
|
|
441
|
+
img.onload = () => {
|
|
442
|
+
img.onload = prevOnload
|
|
443
|
+
img.onerror = prevOnerror
|
|
444
|
+
draw()
|
|
445
|
+
}
|
|
446
|
+
img.onerror = () => {
|
|
447
|
+
img.onload = prevOnload
|
|
448
|
+
img.onerror = prevOnerror
|
|
449
|
+
reject(new Error('image load failed'))
|
|
450
|
+
}
|
|
451
|
+
if (!img.complete) {
|
|
452
|
+
const src = img.getAttribute('src')
|
|
453
|
+
if (src) img.src = src
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** 将页面中的 <img>(Vite 资源 URL 等)转为 data URL,避免 Word MHT 丢失图片 */
|
|
459
|
+
export async function rasterizeImagesForDocx(
|
|
460
|
+
root: HTMLElement,
|
|
461
|
+
sourceRoot?: HTMLElement
|
|
462
|
+
) {
|
|
463
|
+
const images = Array.from(root.querySelectorAll('img')) as HTMLImageElement[]
|
|
464
|
+
const sourceImages = sourceRoot
|
|
465
|
+
? (Array.from(sourceRoot.querySelectorAll('img')) as HTMLImageElement[])
|
|
466
|
+
: images
|
|
467
|
+
|
|
468
|
+
await Promise.all(
|
|
469
|
+
images.map(async (img, index) => {
|
|
470
|
+
const src = img.getAttribute('src') || img.src
|
|
471
|
+
if (!src) return
|
|
472
|
+
|
|
473
|
+
const liveImg = sourceImages[index] ?? img
|
|
474
|
+
const { width, height } = getDocxImageTargetSize(liveImg)
|
|
475
|
+
|
|
476
|
+
if (src.startsWith('data:')) {
|
|
477
|
+
img.setAttribute('width', String(width))
|
|
478
|
+
img.setAttribute('height', String(height))
|
|
479
|
+
img.style.width = `${width}px`
|
|
480
|
+
img.style.height = `${height}px`
|
|
481
|
+
img.style.maxWidth = `${width}px`
|
|
482
|
+
img.style.maxHeight = `${height}px`
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const applyRasterized = (dataUrl: string) => {
|
|
487
|
+
img.setAttribute('src', dataUrl)
|
|
488
|
+
img.src = dataUrl
|
|
489
|
+
img.setAttribute('width', String(width))
|
|
490
|
+
img.setAttribute('height', String(height))
|
|
491
|
+
img.style.width = `${width}px`
|
|
492
|
+
img.style.height = `${height}px`
|
|
493
|
+
img.style.maxWidth = `${width}px`
|
|
494
|
+
img.style.maxHeight = `${height}px`
|
|
495
|
+
img.style.display = 'block'
|
|
496
|
+
img.style.border = '0'
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
applyRasterized(await imgElementToDataUrl(liveImg, width, height))
|
|
501
|
+
return
|
|
502
|
+
} catch {
|
|
503
|
+
/* fall through to fetch */
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const fetchUrl = liveImg.currentSrc || liveImg.src || src
|
|
508
|
+
const probe = new Image()
|
|
509
|
+
probe.src = await fetchImageAsDataUrl(fetchUrl)
|
|
510
|
+
await new Promise<void>((resolve, reject) => {
|
|
511
|
+
probe.onload = () => resolve()
|
|
512
|
+
probe.onerror = () => reject(new Error('probe load failed'))
|
|
513
|
+
})
|
|
514
|
+
applyRasterized(await imgElementToDataUrl(probe, width, height))
|
|
515
|
+
} catch (e) {
|
|
516
|
+
console.warn('Failed to rasterize image for docx:', src, e)
|
|
517
|
+
}
|
|
518
|
+
})
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export async function rasterizeMediaForDocx(
|
|
523
|
+
sourceRoot: HTMLElement,
|
|
524
|
+
targetRoot: HTMLElement,
|
|
525
|
+
echartsApi?: {
|
|
526
|
+
getInstanceByDom: (el: HTMLElement) => {
|
|
527
|
+
resize: () => void
|
|
528
|
+
getDataURL: (opts: object) => string
|
|
529
|
+
} | undefined
|
|
530
|
+
}
|
|
531
|
+
) {
|
|
532
|
+
if (echartsApi) {
|
|
533
|
+
await rasterizeEchartsForDocx(sourceRoot, targetRoot, echartsApi)
|
|
534
|
+
}
|
|
535
|
+
await rasterizeIconsForDocx(sourceRoot, targetRoot)
|
|
536
|
+
await rasterizeStandaloneSvgsForDocx(targetRoot)
|
|
537
|
+
await rasterizeImagesForDocx(targetRoot)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const DOCX_XML_NS =
|
|
541
|
+
'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ' +
|
|
542
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"'
|
|
543
|
+
|
|
544
|
+
function escapeXmlText(text: string): string {
|
|
545
|
+
return text
|
|
546
|
+
.replace(/&/g, '&')
|
|
547
|
+
.replace(/</g, '<')
|
|
548
|
+
.replace(/>/g, '>')
|
|
549
|
+
.replace(/"/g, '"')
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** 页眉密级:文字 #606266;文字下方双蓝线 #2262E0(仅跟文字等宽,不通栏) */
|
|
553
|
+
const HEADER_TEXT_COLOR = '606266'
|
|
554
|
+
const HEADER_UNDERLINE_COLOR = '2262E0'
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* 頁腳頁碼域:用 fldSimple,避免 complex fldChar+instrText 偶發把 PAGE/NUMPAGES
|
|
558
|
+
* 當正文英文字母顯示(WPS / 部分 Word 打開 altChunk 文檔時)。
|
|
559
|
+
*/
|
|
560
|
+
function pageFieldRun(instr: string, placeholder = '1'): string {
|
|
561
|
+
const rPr = `<w:rPr>
|
|
562
|
+
<w:rFonts w:ascii="Times New Roman" w:eastAsia="宋体" w:hAnsi="Times New Roman"/>
|
|
563
|
+
<w:sz w:val="18"/>
|
|
564
|
+
<w:szCs w:val="18"/>
|
|
565
|
+
<w:color w:val="000000"/>
|
|
566
|
+
</w:rPr>`
|
|
567
|
+
return `<w:fldSimple w:instr=" ${instr} ">
|
|
568
|
+
<w:r>
|
|
569
|
+
${rPr}
|
|
570
|
+
<w:t>${placeholder}</w:t>
|
|
571
|
+
</w:r>
|
|
572
|
+
</w:fldSimple>`
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* 页眉「商密三级」:文字 #606266 + 文字等宽双蓝线 #2262E0(同图)。
|
|
577
|
+
* 仅单元格底边 double;表格底边必须 nil,否则会叠出两套双线。
|
|
578
|
+
*/
|
|
579
|
+
function buildHeaderXml(headerText: string): string {
|
|
580
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
581
|
+
<w:hdr ${DOCX_XML_NS}>
|
|
582
|
+
<w:tbl>
|
|
583
|
+
<w:tblPr>
|
|
584
|
+
<w:tblW w:w="0" w:type="auto"/>
|
|
585
|
+
<w:jc w:val="right"/>
|
|
586
|
+
<w:tblBorders>
|
|
587
|
+
<w:top w:val="nil"/>
|
|
588
|
+
<w:left w:val="nil"/>
|
|
589
|
+
<w:bottom w:val="nil"/>
|
|
590
|
+
<w:right w:val="nil"/>
|
|
591
|
+
<w:insideH w:val="nil"/>
|
|
592
|
+
<w:insideV w:val="nil"/>
|
|
593
|
+
</w:tblBorders>
|
|
594
|
+
<w:tblCellMar>
|
|
595
|
+
<w:top w:w="0" w:type="dxa"/>
|
|
596
|
+
<w:left w:w="0" w:type="dxa"/>
|
|
597
|
+
<w:bottom w:w="0" w:type="dxa"/>
|
|
598
|
+
<w:right w:w="0" w:type="dxa"/>
|
|
599
|
+
</w:tblCellMar>
|
|
600
|
+
</w:tblPr>
|
|
601
|
+
<w:tr>
|
|
602
|
+
<w:tc>
|
|
603
|
+
<w:tcPr>
|
|
604
|
+
<w:tcW w:w="0" w:type="auto"/>
|
|
605
|
+
<w:tcBorders>
|
|
606
|
+
<w:top w:val="nil"/>
|
|
607
|
+
<w:left w:val="nil"/>
|
|
608
|
+
<w:bottom w:val="double" w:sz="6" w:space="1" w:color="${HEADER_UNDERLINE_COLOR}"/>
|
|
609
|
+
<w:right w:val="nil"/>
|
|
610
|
+
</w:tcBorders>
|
|
611
|
+
</w:tcPr>
|
|
612
|
+
<w:p>
|
|
613
|
+
<w:pPr>
|
|
614
|
+
<w:jc w:val="right"/>
|
|
615
|
+
<w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/>
|
|
616
|
+
</w:pPr>
|
|
617
|
+
<w:r>
|
|
618
|
+
<w:rPr>
|
|
619
|
+
<w:rFonts w:ascii="Microsoft YaHei" w:eastAsia="微软雅黑" w:hAnsi="Microsoft YaHei"/>
|
|
620
|
+
<w:sz w:val="18"/>
|
|
621
|
+
<w:szCs w:val="18"/>
|
|
622
|
+
<w:color w:val="${HEADER_TEXT_COLOR}"/>
|
|
623
|
+
</w:rPr>
|
|
624
|
+
<w:t>${escapeXmlText(headerText)}</w:t>
|
|
625
|
+
</w:r>
|
|
626
|
+
</w:p>
|
|
627
|
+
</w:tc>
|
|
628
|
+
</w:tr>
|
|
629
|
+
</w:tbl>
|
|
630
|
+
</w:hdr>`
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function buildFooterXml(): string {
|
|
634
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
635
|
+
<w:ftr ${DOCX_XML_NS}>
|
|
636
|
+
<w:p>
|
|
637
|
+
<w:pPr><w:jc w:val="center"/></w:pPr>
|
|
638
|
+
${pageFieldRun('PAGE')}
|
|
639
|
+
<w:r>
|
|
640
|
+
<w:rPr>
|
|
641
|
+
<w:rFonts w:ascii="Times New Roman" w:eastAsia="宋体" w:hAnsi="Times New Roman"/>
|
|
642
|
+
<w:sz w:val="18"/>
|
|
643
|
+
<w:szCs w:val="18"/>
|
|
644
|
+
<w:color w:val="000000"/>
|
|
645
|
+
</w:rPr>
|
|
646
|
+
<w:t xml:space="preserve"> / </w:t>
|
|
647
|
+
</w:r>
|
|
648
|
+
${pageFieldRun('NUMPAGES')}
|
|
649
|
+
</w:p>
|
|
650
|
+
</w:ftr>`
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function buildDocumentXmlWithHeaderFooter(
|
|
654
|
+
width: number,
|
|
655
|
+
height: number,
|
|
656
|
+
orient: string,
|
|
657
|
+
margins: ReturnType<typeof mergeMargins>
|
|
658
|
+
): string {
|
|
659
|
+
// 單一 altChunk + text/html(禁止 MHT / 禁止多段 afchunk)
|
|
660
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
661
|
+
<w:document
|
|
662
|
+
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
663
|
+
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
|
|
664
|
+
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
665
|
+
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
|
666
|
+
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
|
667
|
+
xmlns:ns6="http://schemas.openxmlformats.org/schemaLibrary/2006/main"
|
|
668
|
+
xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"
|
|
669
|
+
xmlns:ns8="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
|
|
670
|
+
xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram"
|
|
671
|
+
xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"
|
|
672
|
+
xmlns:ns11="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
|
|
673
|
+
xmlns:dsp="http://schemas.microsoft.com/office/drawing/2008/diagram"
|
|
674
|
+
xmlns:ns13="urn:schemas-microsoft-com:office:excel"
|
|
675
|
+
xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
676
|
+
xmlns:v="urn:schemas-microsoft-com:vml"
|
|
677
|
+
xmlns:w10="urn:schemas-microsoft-com:office:word"
|
|
678
|
+
xmlns:ns17="urn:schemas-microsoft-com:office:powerpoint"
|
|
679
|
+
xmlns:odx="http://opendope.org/xpaths"
|
|
680
|
+
xmlns:odc="http://opendope.org/conditions"
|
|
681
|
+
xmlns:odq="http://opendope.org/questions"
|
|
682
|
+
xmlns:odi="http://opendope.org/components"
|
|
683
|
+
xmlns:odgm="http://opendope.org/SmartArt/DataHierarchy"
|
|
684
|
+
xmlns:ns24="http://schemas.openxmlformats.org/officeDocument/2006/bibliography"
|
|
685
|
+
xmlns:ns25="http://schemas.openxmlformats.org/drawingml/2006/compatibility"
|
|
686
|
+
xmlns:ns26="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas">
|
|
687
|
+
<w:body>
|
|
688
|
+
<w:altChunk r:id="htmlChunk" />
|
|
689
|
+
<w:sectPr>
|
|
690
|
+
<w:headerReference w:type="default" r:id="rIdHeader"/>
|
|
691
|
+
<w:footerReference w:type="default" r:id="rIdFooter"/>
|
|
692
|
+
<w:pgSz w:w="${width}" w:h="${height}" w:orient="${orient}" />
|
|
693
|
+
<w:pgMar w:top="${margins.top}"
|
|
694
|
+
w:right="${margins.right}"
|
|
695
|
+
w:bottom="${margins.bottom}"
|
|
696
|
+
w:left="${margins.left}"
|
|
697
|
+
w:header="${margins.header}"
|
|
698
|
+
w:footer="${margins.footer}"
|
|
699
|
+
w:gutter="${margins.gutter}"/>
|
|
700
|
+
</w:sectPr>
|
|
701
|
+
</w:body>
|
|
702
|
+
</w:document>`
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function buildContentTypesXml(): string {
|
|
706
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
707
|
+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
708
|
+
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
|
|
709
|
+
<Default Extension="html" ContentType="text/html" />
|
|
710
|
+
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
|
711
|
+
<Override PartName="/word/afchunk.html" ContentType="text/html"/>
|
|
712
|
+
<Override PartName="/word/header1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"/>
|
|
713
|
+
<Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>
|
|
714
|
+
<Override PartName="/word/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"/>
|
|
715
|
+
</Types>
|
|
716
|
+
`
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function buildDocumentRelsXml(): string {
|
|
720
|
+
// 相對路徑(相對 word/_rels/ → word/afchunk.html)。比絕對 /word/... 更兼容遷移環境 Word
|
|
721
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
722
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
723
|
+
<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk"
|
|
724
|
+
Target="afchunk.html" Id="htmlChunk" />
|
|
725
|
+
<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
|
|
726
|
+
Target="header1.xml" Id="rIdHeader" />
|
|
727
|
+
<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
|
|
728
|
+
Target="footer1.xml" Id="rIdFooter" />
|
|
729
|
+
<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings"
|
|
730
|
+
Target="settings.xml" Id="rIdSettings" />
|
|
731
|
+
</Relationships>
|
|
732
|
+
`
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** 多段完整 HTML 無法直接拼接;僅在「已是同一文檔殼」時由上層合併 body 後傳入單一字串 */
|
|
736
|
+
function resolveSingleHtml(htmlOrParts: string | string[]): string {
|
|
737
|
+
if (typeof htmlOrParts === 'string') {
|
|
738
|
+
const html = htmlOrParts.trim()
|
|
739
|
+
if (!html) throw new Error('Word 導出失敗:無可打包的 HTML 內容')
|
|
740
|
+
return html
|
|
741
|
+
}
|
|
742
|
+
const parts = htmlOrParts.map((p) => String(p || '').trim()).filter(Boolean)
|
|
743
|
+
if (!parts.length) throw new Error('Word 導出失敗:無可打包的 HTML 內容')
|
|
744
|
+
if (parts.length === 1) return parts[0] as string
|
|
745
|
+
throw new Error(
|
|
746
|
+
'Word 導出失敗:請傳入單一完整 HTML(封面/目錄/正文應在 HTML 內分頁後一次打包)'
|
|
747
|
+
)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** 打包後自檢:禁止 .mht / 多段 afchunk */
|
|
751
|
+
async function assertPackerIntegrity(zipBytes: Uint8Array, sourceHtml: string) {
|
|
752
|
+
const zip = await JSZip.loadAsync(zipBytes)
|
|
753
|
+
const names = Object.keys(zip.files).filter((n) => !n.endsWith('/'))
|
|
754
|
+
|
|
755
|
+
const mhtFiles = names.filter((n) => /\.mht$/i.test(n))
|
|
756
|
+
if (mhtFiles.length) {
|
|
757
|
+
throw new Error(
|
|
758
|
+
`Word 導出失敗:檢測到舊版 MHT(${mhtFiles.join(', ')}),請完整覆蓋 AIExport 並移除 asBlob 舊入口`
|
|
759
|
+
)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const multiHtml = names.filter((n) => /word\/afchunk\d+\.html$/i.test(n))
|
|
763
|
+
if (multiHtml.length) {
|
|
764
|
+
throw new Error(
|
|
765
|
+
`Word 導出失敗:檢測到多段 afchunk(${multiHtml.join(', ')}),必須為單一 word/afchunk.html`
|
|
766
|
+
)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const htmlFile = zip.file('word/afchunk.html')
|
|
770
|
+
if (!htmlFile) {
|
|
771
|
+
throw new Error('Word 導出失敗:缺少 word/afchunk.html')
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const docRels = zip.file('word/_rels/document.xml.rels')
|
|
775
|
+
if (docRels) {
|
|
776
|
+
const relsXml = await docRels.async('string')
|
|
777
|
+
if (!/Target="afchunk\.html"/i.test(relsXml) || !/Id="htmlChunk"/i.test(relsXml)) {
|
|
778
|
+
throw new Error('Word 導出失敗:document.xml.rels 未正確指向 afchunk.html')
|
|
779
|
+
}
|
|
780
|
+
if (/\.mht/i.test(relsXml)) {
|
|
781
|
+
throw new Error('Word 導出失敗:rels 仍指向 .mht,請覆蓋最新 AIExport')
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const embedded = await htmlFile.async('string')
|
|
786
|
+
if (/mhtDocumentPart|aiexportMhtPart|Content-Transfer-Encoding:\s*quoted-printable/i.test(embedded)) {
|
|
787
|
+
throw new Error('Word 導出失敗:HTML 塊內含 MHT 殘片')
|
|
788
|
+
}
|
|
789
|
+
if (!/<html[\s>]/i.test(embedded) && !/<body[\s>]/i.test(embedded)) {
|
|
790
|
+
throw new Error('Word 導出失敗:afchunk.html 不是有效 HTML')
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const plainSrc = String(sourceHtml || '')
|
|
794
|
+
.replace(/<[^>]+>/g, ' ')
|
|
795
|
+
.replace(/ /gi, ' ')
|
|
796
|
+
.replace(/\s+/g, '')
|
|
797
|
+
const plainEmb = embedded.replace(/<[^>]+>/g, ' ').replace(/\s+/g, '')
|
|
798
|
+
if (plainSrc.length >= 8) {
|
|
799
|
+
const probe = plainSrc.slice(0, 8)
|
|
800
|
+
if (!plainEmb.includes(probe) && !plainEmb.includes(probe.slice(0, 4))) {
|
|
801
|
+
console.warn('[AIExport] embedded HTML probe mismatch (non-fatal)')
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function buildDocxZip(
|
|
807
|
+
htmlOrParts: string | string[],
|
|
808
|
+
options: DocxExportOptions = DEFAULT_DOCX_EXPORT_OPTIONS
|
|
809
|
+
): Promise<Blob> {
|
|
810
|
+
const html = resolveSingleHtml(htmlOrParts)
|
|
811
|
+
|
|
812
|
+
const { width, height, orientation } = resolvePageSize(options)
|
|
813
|
+
const margins = mergeMargins({ ...A4_MARGINS_2CM, ...options.margins })
|
|
814
|
+
const headerFooter = {
|
|
815
|
+
headerText: options.headerFooter?.headerText ?? '商密三级',
|
|
816
|
+
showPageNumbers: options.headerFooter?.showPageNumbers ?? true,
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const zip = new JSZip()
|
|
820
|
+
zip.file('[Content_Types].xml', buildContentTypesXml())
|
|
821
|
+
zip.folder('_rels')!.file('.rels', PACKAGE_RELS_XML)
|
|
822
|
+
|
|
823
|
+
const wordFolder = zip.folder('word')!
|
|
824
|
+
wordFolder.file(
|
|
825
|
+
'document.xml',
|
|
826
|
+
buildDocumentXmlWithHeaderFooter(width, height, orientation, margins)
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
// 純 HTML altChunk(UTF-8 BOM)——永不生成 afchunk.mht
|
|
830
|
+
wordFolder.file('afchunk.html', encodeHtmlAltChunk(html))
|
|
831
|
+
|
|
832
|
+
wordFolder.file('header1.xml', buildHeaderXml(headerFooter.headerText))
|
|
833
|
+
|
|
834
|
+
if (headerFooter.showPageNumbers) {
|
|
835
|
+
wordFolder.file('footer1.xml', buildFooterXml())
|
|
836
|
+
} else {
|
|
837
|
+
wordFolder.file(
|
|
838
|
+
'footer1.xml',
|
|
839
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:ftr ${DOCX_XML_NS}><w:p/></w:ftr>`
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
wordFolder.folder('_rels')!.file('document.xml.rels', buildDocumentRelsXml())
|
|
844
|
+
|
|
845
|
+
wordFolder.file(
|
|
846
|
+
'settings.xml',
|
|
847
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
848
|
+
<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
849
|
+
<w:zoom w:percent="100"/>
|
|
850
|
+
<w:defaultTabStop w:val="420"/>
|
|
851
|
+
<w:updateFields w:val="true"/>
|
|
852
|
+
<w:hideSpellingErrors/>
|
|
853
|
+
<w:hideGrammaticalErrors/>
|
|
854
|
+
</w:settings>`
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
const buffer = await zip.generateAsync({
|
|
858
|
+
type: 'arraybuffer',
|
|
859
|
+
compression: 'DEFLATE',
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
const bytes = new Uint8Array(buffer.slice(0))
|
|
863
|
+
if (bytes.length < 4 || bytes[0] !== 0x50 || bytes[1] !== 0x4b) {
|
|
864
|
+
throw new Error('Word 導出失敗:ZIP 打包結果異常,請檢查 jszip 版本(建議 3.10.x)')
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
await assertPackerIntegrity(bytes, html)
|
|
868
|
+
|
|
869
|
+
return new Blob([bytes], {
|
|
870
|
+
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
871
|
+
})
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export function buildWordDocumentHtml(bodyHtml: string): string {
|
|
875
|
+
const safeBody = sanitizeExportBodyHtml(bodyHtml)
|
|
876
|
+
return `<!DOCTYPE html>
|
|
877
|
+
<html xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
878
|
+
xmlns:w="urn:schemas-microsoft-com:office:word"
|
|
879
|
+
xmlns="http://www.w3.org/TR/REC-html40"
|
|
880
|
+
lang="zh-Hant">
|
|
881
|
+
<head>
|
|
882
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
883
|
+
<meta charset="UTF-8">
|
|
884
|
+
<!--[if gte mso 9]>
|
|
885
|
+
<xml>
|
|
886
|
+
<w:WordDocument>
|
|
887
|
+
<w:View>Print</w:View>
|
|
888
|
+
<w:Zoom>100</w:Zoom>
|
|
889
|
+
<w:DoNotOptimizeForBrowser/>
|
|
890
|
+
<w:HideSpellingErrors/>
|
|
891
|
+
<w:HideGrammaticalErrors/>
|
|
892
|
+
</w:WordDocument>
|
|
893
|
+
</xml>
|
|
894
|
+
<![endif]-->
|
|
895
|
+
<style>
|
|
896
|
+
/* 固定 A4 pt;與 document.xml sectPr 一致,避免節間落到 Letter */
|
|
897
|
+
@page {
|
|
898
|
+
size: 595.3pt 841.9pt;
|
|
899
|
+
margin: 56.7pt;
|
|
900
|
+
mso-page-orientation: portrait;
|
|
901
|
+
mso-header-margin: 21.3pt;
|
|
902
|
+
mso-footer-margin: 35.4pt;
|
|
903
|
+
mso-paper-source: 0;
|
|
904
|
+
}
|
|
905
|
+
@page WordSection1 {
|
|
906
|
+
size: 595.3pt 841.9pt;
|
|
907
|
+
margin: 56.7pt;
|
|
908
|
+
mso-page-orientation: portrait;
|
|
909
|
+
mso-header-margin: 21.3pt;
|
|
910
|
+
mso-footer-margin: 35.4pt;
|
|
911
|
+
mso-paper-source: 0;
|
|
912
|
+
}
|
|
913
|
+
div.WordSection1 {
|
|
914
|
+
page: WordSection1;
|
|
915
|
+
width: 100%;
|
|
916
|
+
max-width: 100%;
|
|
917
|
+
box-sizing: border-box;
|
|
918
|
+
}
|
|
919
|
+
html, body {
|
|
920
|
+
font-family: "Times New Roman", "宋体", SimSun, serif;
|
|
921
|
+
font-size: 11pt;
|
|
922
|
+
line-height: 1.5;
|
|
923
|
+
color: #000000;
|
|
924
|
+
margin: 0;
|
|
925
|
+
padding: 0;
|
|
926
|
+
width: 100%;
|
|
927
|
+
max-width: 100%;
|
|
928
|
+
box-sizing: border-box;
|
|
929
|
+
word-wrap: break-word;
|
|
930
|
+
overflow-wrap: break-word;
|
|
931
|
+
}
|
|
932
|
+
.docx-page-root,
|
|
933
|
+
.doc-sample-root,
|
|
934
|
+
.doc-sample-body {
|
|
935
|
+
width: 100% !important;
|
|
936
|
+
max-width: 100% !important;
|
|
937
|
+
margin: 0;
|
|
938
|
+
padding: 0;
|
|
939
|
+
box-sizing: border-box;
|
|
940
|
+
font-family: "Times New Roman", "宋体", SimSun, serif;
|
|
941
|
+
font-size: 11pt;
|
|
942
|
+
line-height: 1.5;
|
|
943
|
+
color: #000000;
|
|
944
|
+
text-align: left;
|
|
945
|
+
overflow: hidden;
|
|
946
|
+
word-wrap: break-word;
|
|
947
|
+
overflow-wrap: break-word;
|
|
948
|
+
}
|
|
949
|
+
/* 正文禁止文字下劃線(勿傷表格邊框;頁眉雙藍線在 OOXML) */
|
|
950
|
+
.docx-chapters-body,
|
|
951
|
+
.docx-toc-page-root,
|
|
952
|
+
.docx-cover-page :not(.docx-members-label):not(.docx-members-label *) {
|
|
953
|
+
text-decoration: none !important;
|
|
954
|
+
text-decoration-line: none !important;
|
|
955
|
+
}
|
|
956
|
+
.docx-chapters-body u,
|
|
957
|
+
.docx-chapters-body a,
|
|
958
|
+
.docx-chapters-body span,
|
|
959
|
+
.docx-chapters-body p,
|
|
960
|
+
.docx-toc-page-root u,
|
|
961
|
+
.docx-toc-page-root a,
|
|
962
|
+
.docx-toc-page-root span,
|
|
963
|
+
.docx-toc-page-root p,
|
|
964
|
+
.docx-cover-page u {
|
|
965
|
+
text-decoration: none !important;
|
|
966
|
+
text-decoration-line: none !important;
|
|
967
|
+
border-bottom: none !important;
|
|
968
|
+
}
|
|
969
|
+
.docx-members-label,
|
|
970
|
+
.docx-members-label u {
|
|
971
|
+
text-decoration: underline !important;
|
|
972
|
+
text-decoration-color: #000000 !important;
|
|
973
|
+
-webkit-text-decoration-color: #000000 !important;
|
|
974
|
+
border-bottom: none !important;
|
|
975
|
+
}
|
|
976
|
+
.docx-chapters-body h1,
|
|
977
|
+
.docx-chapters-body h2,
|
|
978
|
+
.docx-chapters-body h3,
|
|
979
|
+
.docx-chapters-body p,
|
|
980
|
+
.docx-chapter-title {
|
|
981
|
+
border-bottom: none !important;
|
|
982
|
+
text-decoration: none !important;
|
|
983
|
+
text-decoration-color: #000000 !important;
|
|
984
|
+
}
|
|
985
|
+
/* 表格邊框必須保留(蓋過上文文字去線規則) */
|
|
986
|
+
.docx-chapters-body table,
|
|
987
|
+
.docx-chapters-body .doc-table,
|
|
988
|
+
.docx-chapters-body .doc-table-word {
|
|
989
|
+
border: 1px solid #000000 !important;
|
|
990
|
+
border-collapse: collapse !important;
|
|
991
|
+
}
|
|
992
|
+
.docx-chapters-body table td,
|
|
993
|
+
.docx-chapters-body table th,
|
|
994
|
+
.docx-chapters-body .doc-table td,
|
|
995
|
+
.docx-chapters-body .doc-table th,
|
|
996
|
+
.docx-chapters-body .doc-table-word td,
|
|
997
|
+
.docx-chapters-body .doc-table-word th {
|
|
998
|
+
border: 1px solid #000000 !important;
|
|
999
|
+
border-bottom: 1px solid #000000 !important;
|
|
1000
|
+
border-top: 1px solid #000000 !important;
|
|
1001
|
+
border-left: 1px solid #000000 !important;
|
|
1002
|
+
border-right: 1px solid #000000 !important;
|
|
1003
|
+
}
|
|
1004
|
+
.docx-page-break {
|
|
1005
|
+
page-break-before: always !important;
|
|
1006
|
+
height: 0;
|
|
1007
|
+
margin: 0 !important;
|
|
1008
|
+
padding: 0 !important;
|
|
1009
|
+
border: 0 !important;
|
|
1010
|
+
font-size: 1pt !important;
|
|
1011
|
+
line-height: 0 !important;
|
|
1012
|
+
}
|
|
1013
|
+
.docx-toc-page-root,
|
|
1014
|
+
.docx-cover-page,
|
|
1015
|
+
.docx-chapters-body,
|
|
1016
|
+
.docx-members-block {
|
|
1017
|
+
width: 100% !important;
|
|
1018
|
+
max-width: 100% !important;
|
|
1019
|
+
box-sizing: border-box;
|
|
1020
|
+
overflow: hidden;
|
|
1021
|
+
}
|
|
1022
|
+
/* 目錄整體左移:去掉多餘左邊距,右制表位收回頁邊距內 */
|
|
1023
|
+
.docx-toc-page-root {
|
|
1024
|
+
margin: 0 !important;
|
|
1025
|
+
padding: 0 !important;
|
|
1026
|
+
text-align: left !important;
|
|
1027
|
+
page-break-after: auto;
|
|
1028
|
+
}
|
|
1029
|
+
.docx-chapters-body {
|
|
1030
|
+
page-break-before: auto;
|
|
1031
|
+
text-align: left;
|
|
1032
|
+
}
|
|
1033
|
+
/* 圖二正文標準 */
|
|
1034
|
+
.docx-chapters-body h1.docx-chapter-title,
|
|
1035
|
+
.docx-chapters-body .docx-chapter-title {
|
|
1036
|
+
font-size: 14pt !important;
|
|
1037
|
+
font-weight: bold !important;
|
|
1038
|
+
color: #000000 !important;
|
|
1039
|
+
text-align: left !important;
|
|
1040
|
+
margin: 14pt 0 8pt 0 !important;
|
|
1041
|
+
page-break-after: avoid;
|
|
1042
|
+
max-width: 100% !important;
|
|
1043
|
+
word-wrap: break-word;
|
|
1044
|
+
}
|
|
1045
|
+
.docx-chapters-body p,
|
|
1046
|
+
.docx-chapters-body li {
|
|
1047
|
+
font-size: 11pt !important;
|
|
1048
|
+
line-height: 1.5 !important;
|
|
1049
|
+
color: #000000 !important;
|
|
1050
|
+
margin: 0 0 10pt 0 !important;
|
|
1051
|
+
text-align: left !important;
|
|
1052
|
+
max-width: 100% !important;
|
|
1053
|
+
word-wrap: break-word;
|
|
1054
|
+
overflow-wrap: break-word;
|
|
1055
|
+
}
|
|
1056
|
+
.docx-chapters-body h2,
|
|
1057
|
+
.docx-chapters-body h3 {
|
|
1058
|
+
font-weight: bold !important;
|
|
1059
|
+
color: #000000 !important;
|
|
1060
|
+
text-align: left !important;
|
|
1061
|
+
margin: 10pt 0 6pt 0 !important;
|
|
1062
|
+
max-width: 100% !important;
|
|
1063
|
+
}
|
|
1064
|
+
.docx-cover-page {
|
|
1065
|
+
width: 100%;
|
|
1066
|
+
}
|
|
1067
|
+
.docx-cover {
|
|
1068
|
+
margin: 36pt 0 0;
|
|
1069
|
+
text-align: center;
|
|
1070
|
+
}
|
|
1071
|
+
.docx-cover-title {
|
|
1072
|
+
font-family: "PMingLiU", "新細明體", "新细明体", serif !important;
|
|
1073
|
+
mso-ascii-font-family: "PMingLiU";
|
|
1074
|
+
mso-fareast-font-family: "新細明體";
|
|
1075
|
+
mso-hansi-font-family: "PMingLiU";
|
|
1076
|
+
font-size: 18pt !important;
|
|
1077
|
+
font-weight: bold !important;
|
|
1078
|
+
color: #000000 !important;
|
|
1079
|
+
text-align: center !important;
|
|
1080
|
+
margin: 0 0 8pt !important;
|
|
1081
|
+
line-height: 1.45 !important;
|
|
1082
|
+
}
|
|
1083
|
+
.docx-cover-year {
|
|
1084
|
+
font-family: "FangSong", "仿宋", STFangsong, serif !important;
|
|
1085
|
+
mso-ascii-font-family: "FangSong";
|
|
1086
|
+
mso-fareast-font-family: "仿宋";
|
|
1087
|
+
mso-hansi-font-family: "FangSong";
|
|
1088
|
+
font-size: 18pt !important;
|
|
1089
|
+
font-weight: bold !important;
|
|
1090
|
+
text-align: center !important;
|
|
1091
|
+
margin: 0 0 0 !important;
|
|
1092
|
+
line-height: 1.45 !important;
|
|
1093
|
+
}
|
|
1094
|
+
/* 固定行高撑开空白,把「小組成員」推到首頁約下 1/3 */
|
|
1095
|
+
.docx-cover-spacer {
|
|
1096
|
+
margin: 0 !important;
|
|
1097
|
+
padding: 0 !important;
|
|
1098
|
+
font-size: 1pt !important;
|
|
1099
|
+
line-height: 260pt !important;
|
|
1100
|
+
mso-line-height-rule: exactly;
|
|
1101
|
+
}
|
|
1102
|
+
.docx-members-block {
|
|
1103
|
+
margin-top: 0 !important;
|
|
1104
|
+
page-break-inside: avoid;
|
|
1105
|
+
page-break-before: auto;
|
|
1106
|
+
text-align: left;
|
|
1107
|
+
}
|
|
1108
|
+
.docx-members-block--cont {
|
|
1109
|
+
margin-top: 24pt !important;
|
|
1110
|
+
}
|
|
1111
|
+
.docx-members-label {
|
|
1112
|
+
font-family: "FangSong", "仿宋", STFangsong, serif !important;
|
|
1113
|
+
mso-ascii-font-family: "FangSong";
|
|
1114
|
+
mso-fareast-font-family: "仿宋";
|
|
1115
|
+
mso-hansi-font-family: "FangSong";
|
|
1116
|
+
font-size: 18pt !important;
|
|
1117
|
+
font-weight: bold !important;
|
|
1118
|
+
text-align: left !important;
|
|
1119
|
+
text-decoration: none !important;
|
|
1120
|
+
margin: 0 0 10pt !important;
|
|
1121
|
+
color: #000000 !important;
|
|
1122
|
+
border: none !important;
|
|
1123
|
+
border-bottom: none !important;
|
|
1124
|
+
}
|
|
1125
|
+
.docx-members-label u {
|
|
1126
|
+
text-decoration: underline !important;
|
|
1127
|
+
text-decoration-color: #000000 !important;
|
|
1128
|
+
-webkit-text-decoration-color: #000000 !important;
|
|
1129
|
+
}
|
|
1130
|
+
.docx-member-line {
|
|
1131
|
+
font-family: "FangSong", "仿宋", STFangsong, serif !important;
|
|
1132
|
+
mso-ascii-font-family: "FangSong";
|
|
1133
|
+
mso-fareast-font-family: "仿宋";
|
|
1134
|
+
mso-hansi-font-family: "FangSong";
|
|
1135
|
+
font-size: 16pt !important;
|
|
1136
|
+
font-weight: normal !important;
|
|
1137
|
+
text-align: left !important;
|
|
1138
|
+
margin: 0 0 6pt !important;
|
|
1139
|
+
line-height: 1.4 !important;
|
|
1140
|
+
color: #000000 !important;
|
|
1141
|
+
white-space: normal !important;
|
|
1142
|
+
word-wrap: break-word !important;
|
|
1143
|
+
overflow-wrap: anywhere !important;
|
|
1144
|
+
word-break: break-word !important;
|
|
1145
|
+
}
|
|
1146
|
+
.docx-member-group-title {
|
|
1147
|
+
font-size: 14pt !important;
|
|
1148
|
+
font-weight: bold !important;
|
|
1149
|
+
text-align: left !important;
|
|
1150
|
+
margin: 0 0 10pt !important;
|
|
1151
|
+
}
|
|
1152
|
+
.docx-toc-title {
|
|
1153
|
+
font-family: "FangSong", "仿宋", STFangsong, serif !important;
|
|
1154
|
+
mso-ascii-font-family: "FangSong";
|
|
1155
|
+
mso-fareast-font-family: "仿宋";
|
|
1156
|
+
mso-hansi-font-family: "FangSong";
|
|
1157
|
+
font-size: 12pt !important;
|
|
1158
|
+
font-weight: normal !important;
|
|
1159
|
+
color: #2262E0 !important;
|
|
1160
|
+
margin: 12pt 0 18pt 0 !important;
|
|
1161
|
+
padding: 0 !important;
|
|
1162
|
+
text-align: left !important;
|
|
1163
|
+
text-decoration: none !important;
|
|
1164
|
+
border: none !important;
|
|
1165
|
+
border-bottom: none !important;
|
|
1166
|
+
}
|
|
1167
|
+
.docx-toc-entry {
|
|
1168
|
+
/* 中文:新細明體;英文:Times New Roman;字號 12 */
|
|
1169
|
+
font-family: "Times New Roman", "PMingLiU", "新細明體", "新细明体", serif !important;
|
|
1170
|
+
mso-ascii-font-family: "Times New Roman";
|
|
1171
|
+
mso-fareast-font-family: "新細明體";
|
|
1172
|
+
mso-hansi-font-family: "Times New Roman";
|
|
1173
|
+
font-size: 12pt !important;
|
|
1174
|
+
color: #000000 !important;
|
|
1175
|
+
margin: 0 0 6pt 0 !important;
|
|
1176
|
+
padding: 0 !important;
|
|
1177
|
+
line-height: 1.5 !important;
|
|
1178
|
+
text-align: left !important;
|
|
1179
|
+
text-indent: 0 !important;
|
|
1180
|
+
/* 右制表位對齊右頁邊距 */
|
|
1181
|
+
tab-stops: right dotted ${A4_CONTENT_WIDTH_PT}pt;
|
|
1182
|
+
max-width: 100% !important;
|
|
1183
|
+
word-wrap: break-word;
|
|
1184
|
+
}
|
|
1185
|
+
h1.docx-chapter-title,
|
|
1186
|
+
.docx-chapter-title {
|
|
1187
|
+
font-family: "Times New Roman", "宋体", SimSun, serif !important;
|
|
1188
|
+
font-size: 14pt !important;
|
|
1189
|
+
font-weight: bold !important;
|
|
1190
|
+
color: #000000 !important;
|
|
1191
|
+
text-align: left !important;
|
|
1192
|
+
margin: 14pt 0 8pt !important;
|
|
1193
|
+
padding: 0 !important;
|
|
1194
|
+
border: none !important;
|
|
1195
|
+
page-break-after: avoid;
|
|
1196
|
+
max-width: 100% !important;
|
|
1197
|
+
word-wrap: break-word;
|
|
1198
|
+
}
|
|
1199
|
+
.docx-refs-title {
|
|
1200
|
+
font-size: 12pt !important;
|
|
1201
|
+
font-weight: bold !important;
|
|
1202
|
+
margin: 12pt 0 8pt !important;
|
|
1203
|
+
}
|
|
1204
|
+
.docx-ref-line {
|
|
1205
|
+
font-size: 11pt !important;
|
|
1206
|
+
margin: 0 0 4pt !important;
|
|
1207
|
+
line-height: 1.35 !important;
|
|
1208
|
+
max-width: 100% !important;
|
|
1209
|
+
word-wrap: break-word;
|
|
1210
|
+
border: none !important;
|
|
1211
|
+
border-width: 0 !important;
|
|
1212
|
+
outline: none !important;
|
|
1213
|
+
mso-border-alt: none !important;
|
|
1214
|
+
}
|
|
1215
|
+
/* 章節末「參考資料:」——有 [ref:…] 時追加 */
|
|
1216
|
+
.docx-chapter-refs-label {
|
|
1217
|
+
font-family: "FangSong", "仿宋", STFangsong, serif !important;
|
|
1218
|
+
font-size: 11pt !important;
|
|
1219
|
+
font-weight: normal !important;
|
|
1220
|
+
color: #000000 !important;
|
|
1221
|
+
margin: 14pt 0 6pt 0 !important;
|
|
1222
|
+
line-height: 1.5 !important;
|
|
1223
|
+
text-align: left !important;
|
|
1224
|
+
}
|
|
1225
|
+
.docx-chapter-refs-item {
|
|
1226
|
+
font-family: "Times New Roman", "PMingLiU", "新細明體", serif !important;
|
|
1227
|
+
mso-ascii-font-family: "Times New Roman";
|
|
1228
|
+
mso-fareast-font-family: "新細明體";
|
|
1229
|
+
font-size: 11pt !important;
|
|
1230
|
+
font-weight: normal !important;
|
|
1231
|
+
color: #000000 !important;
|
|
1232
|
+
margin: 0 0 4pt 0 !important;
|
|
1233
|
+
line-height: 1.5 !important;
|
|
1234
|
+
text-align: left !important;
|
|
1235
|
+
max-width: 100% !important;
|
|
1236
|
+
word-wrap: break-word;
|
|
1237
|
+
}
|
|
1238
|
+
.docx-appendix-title {
|
|
1239
|
+
font-size: 14pt !important;
|
|
1240
|
+
font-weight: bold !important;
|
|
1241
|
+
margin: 0 0 12pt !important;
|
|
1242
|
+
border: none !important;
|
|
1243
|
+
outline: none !important;
|
|
1244
|
+
mso-border-alt: none !important;
|
|
1245
|
+
}
|
|
1246
|
+
/* 參考文獻清單:禁止黑框 / 表格邊(Word 導入時易誤加) */
|
|
1247
|
+
.docx-refs-appendix,
|
|
1248
|
+
.docx-refs-appendix p,
|
|
1249
|
+
.docx-refs-appendix div,
|
|
1250
|
+
.docx-refs-appendix table,
|
|
1251
|
+
.docx-refs-appendix td,
|
|
1252
|
+
.docx-refs-appendix th {
|
|
1253
|
+
border: none !important;
|
|
1254
|
+
border-width: 0 !important;
|
|
1255
|
+
outline: none !important;
|
|
1256
|
+
mso-border-alt: none !important;
|
|
1257
|
+
background: transparent !important;
|
|
1258
|
+
}
|
|
1259
|
+
.docx-table-wrap {
|
|
1260
|
+
width: ${A4_CONTENT_WIDTH_PT}pt !important;
|
|
1261
|
+
max-width: ${A4_CONTENT_WIDTH_PT}pt !important;
|
|
1262
|
+
height: auto !important;
|
|
1263
|
+
min-height: 0 !important;
|
|
1264
|
+
margin: 6pt 0 12pt;
|
|
1265
|
+
padding: 0;
|
|
1266
|
+
overflow: hidden;
|
|
1267
|
+
box-sizing: border-box;
|
|
1268
|
+
}
|
|
1269
|
+
.docx-table-wrap--nested {
|
|
1270
|
+
width: auto !important;
|
|
1271
|
+
max-width: 100% !important;
|
|
1272
|
+
margin: 4pt 0 !important;
|
|
1273
|
+
}
|
|
1274
|
+
table, .doc-table, .doc-table-word {
|
|
1275
|
+
border-collapse: collapse !important;
|
|
1276
|
+
height: auto !important;
|
|
1277
|
+
min-height: 0 !important;
|
|
1278
|
+
table-layout: fixed !important;
|
|
1279
|
+
mso-table-layout-alt: fixed !important;
|
|
1280
|
+
word-wrap: break-word !important;
|
|
1281
|
+
overflow-wrap: break-word !important;
|
|
1282
|
+
margin: 0 !important;
|
|
1283
|
+
border: 1px solid #000000;
|
|
1284
|
+
box-sizing: border-box;
|
|
1285
|
+
page-break-inside: auto !important;
|
|
1286
|
+
}
|
|
1287
|
+
/* 頂層表鋪滿正文區;嵌套表寬度由內聯 pt 決定,勿再強制整頁寬 */
|
|
1288
|
+
.docx-table-wrap:not(.docx-table-wrap--nested) > table {
|
|
1289
|
+
width: ${A4_CONTENT_WIDTH_PT}pt !important;
|
|
1290
|
+
max-width: ${A4_CONTENT_WIDTH_PT}pt !important;
|
|
1291
|
+
}
|
|
1292
|
+
.docx-table-wrap--nested > table {
|
|
1293
|
+
max-width: 100% !important;
|
|
1294
|
+
height: auto !important;
|
|
1295
|
+
}
|
|
1296
|
+
table tr,
|
|
1297
|
+
table td,
|
|
1298
|
+
table th,
|
|
1299
|
+
.doc-table tr, .doc-table td, .doc-table th,
|
|
1300
|
+
.doc-table-word tr, .doc-table-word td, .doc-table-word th {
|
|
1301
|
+
height: auto !important;
|
|
1302
|
+
min-height: 0 !important;
|
|
1303
|
+
page-break-inside: auto !important;
|
|
1304
|
+
}
|
|
1305
|
+
table td, table th,
|
|
1306
|
+
.doc-table td, .doc-table th,
|
|
1307
|
+
.doc-table-word td, .doc-table-word th {
|
|
1308
|
+
word-wrap: break-word !important;
|
|
1309
|
+
overflow-wrap: break-word !important;
|
|
1310
|
+
overflow: hidden !important;
|
|
1311
|
+
vertical-align: top !important;
|
|
1312
|
+
line-height: 1.35 !important;
|
|
1313
|
+
}
|
|
1314
|
+
.doc-table-complex th,
|
|
1315
|
+
.doc-table-complex td,
|
|
1316
|
+
.doc-table-word th,
|
|
1317
|
+
.doc-table-word td {
|
|
1318
|
+
font-size: 9pt !important;
|
|
1319
|
+
padding: 2pt 3pt !important;
|
|
1320
|
+
}
|
|
1321
|
+
.doc-h1, h1.doc-h1 {
|
|
1322
|
+
font-size: 20pt !important;
|
|
1323
|
+
font-weight: bold !important;
|
|
1324
|
+
color: #c00000 !important;
|
|
1325
|
+
text-align: left !important;
|
|
1326
|
+
margin: 0 0 12pt !important;
|
|
1327
|
+
padding: 0 !important;
|
|
1328
|
+
border: none !important;
|
|
1329
|
+
}
|
|
1330
|
+
.doc-h2, h2.doc-h2 {
|
|
1331
|
+
font-size: 14pt !important;
|
|
1332
|
+
font-weight: bold !important;
|
|
1333
|
+
color: #000000 !important;
|
|
1334
|
+
margin: 12pt 0 6pt !important;
|
|
1335
|
+
padding: 0 !important;
|
|
1336
|
+
border: none !important;
|
|
1337
|
+
}
|
|
1338
|
+
.doc-h3, h3.doc-h3 {
|
|
1339
|
+
font-size: 12pt !important;
|
|
1340
|
+
font-weight: bold !important;
|
|
1341
|
+
color: #000000 !important;
|
|
1342
|
+
margin: 10pt 0 4pt !important;
|
|
1343
|
+
}
|
|
1344
|
+
.doc-p, p.doc-p {
|
|
1345
|
+
font-size: 11pt !important;
|
|
1346
|
+
line-height: 1.15 !important;
|
|
1347
|
+
color: #000000 !important;
|
|
1348
|
+
margin: 0 0 10pt !important;
|
|
1349
|
+
text-align: left !important;
|
|
1350
|
+
}
|
|
1351
|
+
.doc-link-line, p.doc-link-line {
|
|
1352
|
+
font-size: 11pt !important;
|
|
1353
|
+
margin: 0 0 2pt !important;
|
|
1354
|
+
padding: 0 !important;
|
|
1355
|
+
}
|
|
1356
|
+
.doc-link-anchor, a.doc-link-anchor {
|
|
1357
|
+
color: #000000 !important;
|
|
1358
|
+
text-decoration: none !important;
|
|
1359
|
+
}
|
|
1360
|
+
.typewriter-paragraph {
|
|
1361
|
+
white-space: pre-wrap;
|
|
1362
|
+
word-break: break-word;
|
|
1363
|
+
font-size: inherit !important;
|
|
1364
|
+
line-height: inherit !important;
|
|
1365
|
+
color: inherit !important;
|
|
1366
|
+
}
|
|
1367
|
+
/* 列表:双列无边框表——标记 | 正文;换行只在右列,禁止负缩进顶出页边 */
|
|
1368
|
+
.doc-ol-export { margin: 6pt 0 10pt; padding: 0; font-size: 11pt; color: #000000; max-width: 100%; box-sizing: border-box; }
|
|
1369
|
+
table.doc-list-item-table,
|
|
1370
|
+
.doc-list-item-table {
|
|
1371
|
+
width: 100% !important;
|
|
1372
|
+
max-width: 100% !important;
|
|
1373
|
+
border: none !important;
|
|
1374
|
+
border-collapse: collapse !important;
|
|
1375
|
+
border-spacing: 0 !important;
|
|
1376
|
+
margin: 0 0 2pt 0 !important;
|
|
1377
|
+
padding: 0 !important;
|
|
1378
|
+
table-layout: fixed !important;
|
|
1379
|
+
mso-table-layout-alt: fixed !important;
|
|
1380
|
+
box-sizing: border-box !important;
|
|
1381
|
+
}
|
|
1382
|
+
.doc-list-item-table td,
|
|
1383
|
+
table.doc-list-item-table td {
|
|
1384
|
+
border: none !important;
|
|
1385
|
+
border-width: 0 !important;
|
|
1386
|
+
mso-border-alt: none !important;
|
|
1387
|
+
background: transparent !important;
|
|
1388
|
+
padding: 0 !important;
|
|
1389
|
+
margin: 0 !important;
|
|
1390
|
+
font-size: 11pt !important;
|
|
1391
|
+
line-height: 1.15 !important;
|
|
1392
|
+
color: #000000 !important;
|
|
1393
|
+
vertical-align: top !important;
|
|
1394
|
+
word-wrap: break-word !important;
|
|
1395
|
+
overflow-wrap: break-word !important;
|
|
1396
|
+
}
|
|
1397
|
+
.doc-list-item-table .doc-list-marker {
|
|
1398
|
+
width: 22pt !important;
|
|
1399
|
+
max-width: 22pt !important;
|
|
1400
|
+
white-space: nowrap !important;
|
|
1401
|
+
padding-right: 4pt !important;
|
|
1402
|
+
}
|
|
1403
|
+
.doc-list-item-table .doc-list-text {
|
|
1404
|
+
width: auto !important;
|
|
1405
|
+
}
|
|
1406
|
+
.docx-chapters-body ul,
|
|
1407
|
+
.docx-chapters-body ol {
|
|
1408
|
+
margin: 0 0 10pt 0 !important;
|
|
1409
|
+
padding-left: 18pt !important;
|
|
1410
|
+
max-width: 100% !important;
|
|
1411
|
+
box-sizing: border-box !important;
|
|
1412
|
+
}
|
|
1413
|
+
.docx-chapters-body li {
|
|
1414
|
+
margin: 0 0 2pt 0 !important;
|
|
1415
|
+
max-width: 100% !important;
|
|
1416
|
+
box-sizing: border-box !important;
|
|
1417
|
+
word-wrap: break-word !important;
|
|
1418
|
+
}
|
|
1419
|
+
th, td {
|
|
1420
|
+
border: 1px solid #000000;
|
|
1421
|
+
padding: 3pt 4pt !important;
|
|
1422
|
+
font-size: 10pt !important;
|
|
1423
|
+
color: #000000;
|
|
1424
|
+
height: auto !important;
|
|
1425
|
+
min-height: 0 !important;
|
|
1426
|
+
word-wrap: break-word !important;
|
|
1427
|
+
overflow-wrap: break-word !important;
|
|
1428
|
+
box-sizing: border-box;
|
|
1429
|
+
vertical-align: top !important;
|
|
1430
|
+
}
|
|
1431
|
+
th { font-weight: bold; background-color: #ffffff; }
|
|
1432
|
+
.doc-th-green, th.doc-th-green { background-color: #c6efce !important; color: #000000 !important; }
|
|
1433
|
+
.doc-th-red, th.doc-th-red { background-color: #ffc7ce !important; color: #000000 !important; }
|
|
1434
|
+
.doc-chart-intro {
|
|
1435
|
+
display: block;
|
|
1436
|
+
width: 100% !important;
|
|
1437
|
+
max-width: 100% !important;
|
|
1438
|
+
margin: 0 0 10pt;
|
|
1439
|
+
overflow: hidden;
|
|
1440
|
+
box-sizing: border-box;
|
|
1441
|
+
}
|
|
1442
|
+
.doc-chart-intro__left,
|
|
1443
|
+
.doc-chart-intro__right {
|
|
1444
|
+
display: block;
|
|
1445
|
+
float: none !important;
|
|
1446
|
+
width: 100% !important;
|
|
1447
|
+
max-width: 100% !important;
|
|
1448
|
+
margin: 0 0 8pt 0 !important;
|
|
1449
|
+
overflow: visible;
|
|
1450
|
+
box-sizing: border-box;
|
|
1451
|
+
}
|
|
1452
|
+
.doc-pie-chart-img { display: block; width: ${DOCX_PIE_CHART_MAX_WIDTH_PX}px; height: auto; max-width: 100%; max-height: ${DOCX_PIE_CHART_MAX_HEIGHT_PX}px; box-sizing: border-box; }
|
|
1453
|
+
.doc-chart-intro-text { margin: 0; text-align: left; font-size: 11pt !important; }
|
|
1454
|
+
.doc-images-block { overflow: hidden; margin: 0 0 10pt; max-width: 100%; width: 100%; box-sizing: border-box; }
|
|
1455
|
+
.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; }
|
|
1456
|
+
.doc-images-p { margin: 0 0 10pt; text-align: left; font-size: 11pt; line-height: 1.15; }
|
|
1457
|
+
.success-row td { background-color: #f0f9eb; }
|
|
1458
|
+
.info-row td { background-color: #e6f7ff; }
|
|
1459
|
+
.warning-row td { background-color: #fdf6ec; }
|
|
1460
|
+
.danger-row td { background-color: #fef0f0; }
|
|
1461
|
+
img { max-width: 100%; height: auto; }
|
|
1462
|
+
.docx-chart-wrap,
|
|
1463
|
+
.echarts-container.docx-chart-wrap {
|
|
1464
|
+
max-width: 100%;
|
|
1465
|
+
width: 100%;
|
|
1466
|
+
margin: 12pt 0 !important;
|
|
1467
|
+
text-align: center !important;
|
|
1468
|
+
page-break-inside: avoid;
|
|
1469
|
+
page-break-before: auto;
|
|
1470
|
+
box-sizing: border-box;
|
|
1471
|
+
overflow: hidden;
|
|
1472
|
+
}
|
|
1473
|
+
.docx-chart-wrap .docx-chart-img,
|
|
1474
|
+
.docx-chart-wrap img {
|
|
1475
|
+
display: block !important;
|
|
1476
|
+
margin: 0 auto !important;
|
|
1477
|
+
border: 0;
|
|
1478
|
+
max-width: 100%;
|
|
1479
|
+
}
|
|
1480
|
+
button, .el-button {
|
|
1481
|
+
display: inline-block !important;
|
|
1482
|
+
padding: 4px 12px;
|
|
1483
|
+
margin: 0 6px 6px 0;
|
|
1484
|
+
border: 1px solid #dcdfe6;
|
|
1485
|
+
border-radius: 4px;
|
|
1486
|
+
font-size: 12px;
|
|
1487
|
+
background: #fff;
|
|
1488
|
+
color: #606266;
|
|
1489
|
+
}
|
|
1490
|
+
.el-button--danger { background: #fef0f0 !important; color: #f56c6c !important; border-color: #fbc4c4 !important; }
|
|
1491
|
+
.el-button--success { background: #f0f9eb !important; color: #67c23a !important; }
|
|
1492
|
+
.el-button--primary { background: #ecf5ff !important; color: #409eff !important; }
|
|
1493
|
+
.section-title {
|
|
1494
|
+
font-size: 18px;
|
|
1495
|
+
font-weight: bold;
|
|
1496
|
+
color: #1a1a1a;
|
|
1497
|
+
border-top: 1px solid #e5e7eb;
|
|
1498
|
+
padding-top: 24px;
|
|
1499
|
+
margin-bottom: 20px;
|
|
1500
|
+
}
|
|
1501
|
+
</style>
|
|
1502
|
+
</head>
|
|
1503
|
+
<body>
|
|
1504
|
+
<div class="WordSection1">
|
|
1505
|
+
<div class="docx-page-root">${safeBody}</div>
|
|
1506
|
+
</div>
|
|
1507
|
+
</body>
|
|
1508
|
+
</html>`
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
const STYLE_PROPS = [
|
|
1512
|
+
'color',
|
|
1513
|
+
'fontSize',
|
|
1514
|
+
'fontFamily',
|
|
1515
|
+
'fontWeight',
|
|
1516
|
+
'fontStyle',
|
|
1517
|
+
'backgroundColor',
|
|
1518
|
+
'textAlign',
|
|
1519
|
+
'lineHeight',
|
|
1520
|
+
'marginTop',
|
|
1521
|
+
'marginBottom',
|
|
1522
|
+
'marginLeft',
|
|
1523
|
+
'marginRight',
|
|
1524
|
+
'paddingTop',
|
|
1525
|
+
'paddingBottom',
|
|
1526
|
+
'paddingLeft',
|
|
1527
|
+
'paddingRight',
|
|
1528
|
+
'borderTop',
|
|
1529
|
+
'borderBottom',
|
|
1530
|
+
'borderLeft',
|
|
1531
|
+
'borderRight',
|
|
1532
|
+
'display',
|
|
1533
|
+
'width',
|
|
1534
|
+
'height',
|
|
1535
|
+
'maxWidth',
|
|
1536
|
+
'verticalAlign',
|
|
1537
|
+
'whiteSpace',
|
|
1538
|
+
'letterSpacing',
|
|
1539
|
+
] as const
|
|
1540
|
+
|
|
1541
|
+
type InlineStyleMap = Record<string, string>
|
|
1542
|
+
|
|
1543
|
+
const DOC_CLASS_INLINE_STYLES: Record<string, InlineStyleMap> = {
|
|
1544
|
+
'doc-h1': {
|
|
1545
|
+
fontSize: '20pt',
|
|
1546
|
+
fontWeight: 'bold',
|
|
1547
|
+
color: '#c00000',
|
|
1548
|
+
textAlign: 'left',
|
|
1549
|
+
margin: '0 0 12pt',
|
|
1550
|
+
padding: '0',
|
|
1551
|
+
border: 'none',
|
|
1552
|
+
},
|
|
1553
|
+
'doc-h2': {
|
|
1554
|
+
fontSize: '14pt',
|
|
1555
|
+
fontWeight: 'bold',
|
|
1556
|
+
color: '#000000',
|
|
1557
|
+
margin: '12pt 0 6pt',
|
|
1558
|
+
padding: '0',
|
|
1559
|
+
border: 'none',
|
|
1560
|
+
},
|
|
1561
|
+
'doc-h3': {
|
|
1562
|
+
fontSize: '12pt',
|
|
1563
|
+
fontWeight: 'bold',
|
|
1564
|
+
color: '#000000',
|
|
1565
|
+
margin: '10pt 0 4pt',
|
|
1566
|
+
},
|
|
1567
|
+
'doc-p': {
|
|
1568
|
+
fontSize: '11pt',
|
|
1569
|
+
lineHeight: '1.15',
|
|
1570
|
+
color: '#000000',
|
|
1571
|
+
margin: '0 0 10pt',
|
|
1572
|
+
textAlign: 'left',
|
|
1573
|
+
},
|
|
1574
|
+
'doc-link-line': {
|
|
1575
|
+
fontSize: '11pt',
|
|
1576
|
+
margin: '0 0 2pt',
|
|
1577
|
+
padding: '0',
|
|
1578
|
+
},
|
|
1579
|
+
'doc-link-anchor': {
|
|
1580
|
+
color: '#000000',
|
|
1581
|
+
textDecoration: 'none',
|
|
1582
|
+
},
|
|
1583
|
+
'doc-th-green': {
|
|
1584
|
+
backgroundColor: '#c6efce',
|
|
1585
|
+
color: '#000000',
|
|
1586
|
+
fontWeight: 'bold',
|
|
1587
|
+
},
|
|
1588
|
+
'doc-th-red': {
|
|
1589
|
+
backgroundColor: '#ffc7ce',
|
|
1590
|
+
color: '#000000',
|
|
1591
|
+
fontWeight: 'bold',
|
|
1592
|
+
},
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function applyInlineStyles(el: HTMLElement, styles: InlineStyleMap) {
|
|
1596
|
+
for (const [prop, val] of Object.entries(styles)) {
|
|
1597
|
+
;(el.style as unknown as Record<string, string>)[prop] = val
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function applyDocClassInlineStyles(el: HTMLElement) {
|
|
1602
|
+
Array.from(el.classList).forEach((cls) => {
|
|
1603
|
+
const styles = DOC_CLASS_INLINE_STYLES[cls]
|
|
1604
|
+
if (styles) applyInlineStyles(el, styles)
|
|
1605
|
+
})
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function mountCloneForStyleRead(clone: HTMLElement): () => void {
|
|
1609
|
+
const host = document.createElement('div')
|
|
1610
|
+
host.setAttribute('data-docx-export-host', 'true')
|
|
1611
|
+
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;`
|
|
1612
|
+
host.appendChild(clone)
|
|
1613
|
+
document.body.appendChild(host)
|
|
1614
|
+
return () => {
|
|
1615
|
+
host.remove()
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function clampAllImagesForDocx(root: HTMLElement) {
|
|
1620
|
+
root.querySelectorAll('img').forEach((node) => {
|
|
1621
|
+
if (!(node instanceof HTMLImageElement)) return
|
|
1622
|
+
const { width, height } = getDocxImageTargetSize(node)
|
|
1623
|
+
node.setAttribute('width', String(width))
|
|
1624
|
+
node.setAttribute('height', String(height))
|
|
1625
|
+
node.style.maxWidth = `${width}px`
|
|
1626
|
+
node.style.maxHeight = `${height}px`
|
|
1627
|
+
node.style.width = `${width}px`
|
|
1628
|
+
node.style.height = `${height}px`
|
|
1629
|
+
node.style.boxSizing = 'border-box'
|
|
1630
|
+
})
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function flattenChartIntroForDocx(root: HTMLElement) {
|
|
1634
|
+
root.querySelectorAll('.doc-chart-intro').forEach((intro) => {
|
|
1635
|
+
if (!(intro instanceof HTMLElement)) return
|
|
1636
|
+
intro.style.cssText = [
|
|
1637
|
+
'display:block',
|
|
1638
|
+
'width:100%',
|
|
1639
|
+
'max-width:100%',
|
|
1640
|
+
'margin:0 0 10pt',
|
|
1641
|
+
'overflow:hidden',
|
|
1642
|
+
'box-sizing:border-box',
|
|
1643
|
+
].join(';')
|
|
1644
|
+
|
|
1645
|
+
const left = intro.querySelector('.doc-chart-intro__left')
|
|
1646
|
+
const right = intro.querySelector('.doc-chart-intro__right')
|
|
1647
|
+
const img = intro.querySelector('.doc-pie-chart-img')
|
|
1648
|
+
|
|
1649
|
+
if (left instanceof HTMLElement) {
|
|
1650
|
+
left.style.cssText = [
|
|
1651
|
+
'display:block',
|
|
1652
|
+
'float:none',
|
|
1653
|
+
'width:100%',
|
|
1654
|
+
'max-width:100%',
|
|
1655
|
+
'margin:0 0 8pt 0',
|
|
1656
|
+
'box-sizing:border-box',
|
|
1657
|
+
].join(';')
|
|
1658
|
+
}
|
|
1659
|
+
if (img instanceof HTMLImageElement) {
|
|
1660
|
+
const { width, height } = getDocxImageTargetSize(img)
|
|
1661
|
+
const maxW = Math.min(width, Math.floor(A4_CONTENT_WIDTH_PX * 0.55))
|
|
1662
|
+
const maxH = DOCX_PIE_CHART_MAX_HEIGHT_PX
|
|
1663
|
+
img.style.cssText = [
|
|
1664
|
+
'display:block',
|
|
1665
|
+
`width:${maxW}px`,
|
|
1666
|
+
`height:${height}px`,
|
|
1667
|
+
'max-width:100%',
|
|
1668
|
+
`max-height:${maxH}px`,
|
|
1669
|
+
'border:0',
|
|
1670
|
+
'box-sizing:border-box',
|
|
1671
|
+
].join(';')
|
|
1672
|
+
img.setAttribute('width', String(maxW))
|
|
1673
|
+
img.setAttribute('height', String(height))
|
|
1674
|
+
}
|
|
1675
|
+
if (right instanceof HTMLElement) {
|
|
1676
|
+
right.style.cssText = [
|
|
1677
|
+
'display:block',
|
|
1678
|
+
'float:none',
|
|
1679
|
+
'width:100%',
|
|
1680
|
+
'max-width:100%',
|
|
1681
|
+
'margin:0',
|
|
1682
|
+
'overflow:hidden',
|
|
1683
|
+
'box-sizing:border-box',
|
|
1684
|
+
].join(';')
|
|
1685
|
+
}
|
|
1686
|
+
})
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function flattenImagesBlockForDocx(root: HTMLElement) {
|
|
1690
|
+
root.querySelectorAll('.doc-images-block').forEach((block) => {
|
|
1691
|
+
if (!(block instanceof HTMLElement)) return
|
|
1692
|
+
block.style.overflow = 'hidden'
|
|
1693
|
+
block.style.maxWidth = '100%'
|
|
1694
|
+
block.style.width = '100%'
|
|
1695
|
+
block.style.boxSizing = 'border-box'
|
|
1696
|
+
})
|
|
1697
|
+
|
|
1698
|
+
root.querySelectorAll('.doc-float-icon').forEach((img) => {
|
|
1699
|
+
if (!(img instanceof HTMLImageElement)) return
|
|
1700
|
+
const size = DOCX_FLOAT_ICON_SIZE_PX
|
|
1701
|
+
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;`
|
|
1702
|
+
img.setAttribute('width', String(size))
|
|
1703
|
+
img.setAttribute('height', String(size))
|
|
1704
|
+
})
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* 列表展平为「标记 | 正文」无边框双列表:
|
|
1709
|
+
* - 换行只发生在右列,不会越过编号/圆点
|
|
1710
|
+
* - 绝不用负 text-indent / 负 margin,避免顶出页边距
|
|
1711
|
+
*/
|
|
1712
|
+
const DOC_LIST_MARKER_W_OL_PT = 22
|
|
1713
|
+
const DOC_LIST_MARKER_W_UL_PT = 16
|
|
1714
|
+
const DOC_LIST_INDENT_STEP_PT = 12
|
|
1715
|
+
|
|
1716
|
+
function getListItemLabel(li: HTMLLIElement): string {
|
|
1717
|
+
const label = li.querySelector(':scope > .doc-ol-label')
|
|
1718
|
+
if (label?.textContent?.trim()) return label.textContent.trim()
|
|
1719
|
+
|
|
1720
|
+
const parts: string[] = []
|
|
1721
|
+
li.childNodes.forEach((node) => {
|
|
1722
|
+
if (node instanceof HTMLElement && (node.tagName === 'UL' || node.tagName === 'OL')) return
|
|
1723
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1724
|
+
const text = node.textContent?.trim()
|
|
1725
|
+
if (text) parts.push(text)
|
|
1726
|
+
} else if (node instanceof HTMLElement) {
|
|
1727
|
+
const text = node.textContent?.trim()
|
|
1728
|
+
if (text) parts.push(text)
|
|
1729
|
+
}
|
|
1730
|
+
})
|
|
1731
|
+
if (parts.length) return parts.join(' ')
|
|
1732
|
+
return li.textContent?.trim() ?? ''
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function appendFlattenedList(
|
|
1736
|
+
list: HTMLOListElement | HTMLUListElement,
|
|
1737
|
+
container: HTMLElement,
|
|
1738
|
+
depth: number
|
|
1739
|
+
) {
|
|
1740
|
+
const ordered = list.tagName === 'OL'
|
|
1741
|
+
const markerW = ordered ? DOC_LIST_MARKER_W_OL_PT : DOC_LIST_MARKER_W_UL_PT
|
|
1742
|
+
const indent = depth * DOC_LIST_INDENT_STEP_PT
|
|
1743
|
+
let index = 0
|
|
1744
|
+
list.querySelectorAll(':scope > li').forEach((li) => {
|
|
1745
|
+
if (!(li instanceof HTMLLIElement)) return
|
|
1746
|
+
index++
|
|
1747
|
+
const mainText = getListItemLabel(li)
|
|
1748
|
+
const marker = ordered ? `${index}.` : '•'
|
|
1749
|
+
|
|
1750
|
+
const table = document.createElement('table')
|
|
1751
|
+
table.className = 'doc-list-item-table'
|
|
1752
|
+
table.setAttribute('border', '0')
|
|
1753
|
+
table.setAttribute('cellpadding', '0')
|
|
1754
|
+
table.setAttribute('cellspacing', '0')
|
|
1755
|
+
table.style.cssText = [
|
|
1756
|
+
'width:100%',
|
|
1757
|
+
'max-width:100%',
|
|
1758
|
+
'border:none',
|
|
1759
|
+
'border-collapse:collapse',
|
|
1760
|
+
'margin:0 0 2pt 0',
|
|
1761
|
+
'padding:0',
|
|
1762
|
+
'table-layout:fixed',
|
|
1763
|
+
'box-sizing:border-box',
|
|
1764
|
+
].join(';')
|
|
1765
|
+
|
|
1766
|
+
const tr = document.createElement('tr')
|
|
1767
|
+
const tdMarker = document.createElement('td')
|
|
1768
|
+
tdMarker.className = 'doc-list-marker'
|
|
1769
|
+
tdMarker.style.cssText = [
|
|
1770
|
+
'border:none',
|
|
1771
|
+
`width:${markerW + indent}pt`,
|
|
1772
|
+
`max-width:${markerW + indent}pt`,
|
|
1773
|
+
indent > 0 ? `padding-left:${indent}pt` : 'padding:0',
|
|
1774
|
+
'padding-right:4pt',
|
|
1775
|
+
'padding-top:0',
|
|
1776
|
+
'padding-bottom:0',
|
|
1777
|
+
'vertical-align:top',
|
|
1778
|
+
'white-space:nowrap',
|
|
1779
|
+
'font-size:11pt',
|
|
1780
|
+
'line-height:1.15',
|
|
1781
|
+
'color:#000000',
|
|
1782
|
+
].join(';')
|
|
1783
|
+
tdMarker.textContent = marker
|
|
1784
|
+
|
|
1785
|
+
const tdText = document.createElement('td')
|
|
1786
|
+
tdText.className = 'doc-list-text'
|
|
1787
|
+
tdText.style.cssText = [
|
|
1788
|
+
'border:none',
|
|
1789
|
+
'width:auto',
|
|
1790
|
+
'padding:0',
|
|
1791
|
+
'vertical-align:top',
|
|
1792
|
+
'font-size:11pt',
|
|
1793
|
+
'line-height:1.15',
|
|
1794
|
+
'color:#000000',
|
|
1795
|
+
'word-wrap:break-word',
|
|
1796
|
+
'overflow-wrap:break-word',
|
|
1797
|
+
].join(';')
|
|
1798
|
+
tdText.textContent = mainText
|
|
1799
|
+
|
|
1800
|
+
tr.appendChild(tdMarker)
|
|
1801
|
+
tr.appendChild(tdText)
|
|
1802
|
+
table.appendChild(tr)
|
|
1803
|
+
container.appendChild(table)
|
|
1804
|
+
|
|
1805
|
+
li.querySelectorAll(':scope > ul, :scope > ol').forEach((nested) => {
|
|
1806
|
+
if (nested instanceof HTMLOListElement || nested instanceof HTMLUListElement) {
|
|
1807
|
+
appendFlattenedList(nested, container, depth + 1)
|
|
1808
|
+
}
|
|
1809
|
+
})
|
|
1810
|
+
})
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/** Word altChunk 无法可靠处理原生 ol/ul;展平为无边框双列表(换行不越标记、不顶页边) */
|
|
1814
|
+
function normalizeListsForDocx(root: HTMLElement) {
|
|
1815
|
+
const topLists = Array.from(root.querySelectorAll('ol, ul')).filter((el) => {
|
|
1816
|
+
return !el.parentElement?.closest('ol, ul')
|
|
1817
|
+
})
|
|
1818
|
+
|
|
1819
|
+
topLists.forEach((list) => {
|
|
1820
|
+
if (!(list instanceof HTMLOListElement || list instanceof HTMLUListElement)) return
|
|
1821
|
+
const container = document.createElement('div')
|
|
1822
|
+
container.className = 'doc-ol-export'
|
|
1823
|
+
container.style.cssText =
|
|
1824
|
+
'margin:6pt 0 10pt;padding:0;font-size:11pt;color:#000000;max-width:100%;box-sizing:border-box;'
|
|
1825
|
+
appendFlattenedList(list, container, 0)
|
|
1826
|
+
list.replaceWith(container)
|
|
1827
|
+
})
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
/** 统计表格逻辑列数(含 colspan) */
|
|
1831
|
+
function getTableColumnCount(table: HTMLTableElement): number {
|
|
1832
|
+
let maxCols = 0
|
|
1833
|
+
Array.from(table.rows).forEach((row) => {
|
|
1834
|
+
let cols = 0
|
|
1835
|
+
Array.from(row.cells).forEach((cell) => {
|
|
1836
|
+
cols += cell.colSpan || 1
|
|
1837
|
+
})
|
|
1838
|
+
maxCols = Math.max(maxCols, cols)
|
|
1839
|
+
})
|
|
1840
|
+
return maxCols
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function getColumnWidthsPt(colCount: number, total = A4_CONTENT_WIDTH_PT): number[] {
|
|
1844
|
+
const safeTotal = Math.max(40, Math.round(total))
|
|
1845
|
+
if (colCount <= 0) return [safeTotal]
|
|
1846
|
+
if (colCount === 3) {
|
|
1847
|
+
const c0 = Math.round(safeTotal * 0.28)
|
|
1848
|
+
const c1 = Math.round(safeTotal * 0.36)
|
|
1849
|
+
return [c0, c1, safeTotal - c0 - c1]
|
|
1850
|
+
}
|
|
1851
|
+
if (colCount === 5) {
|
|
1852
|
+
const c0 = Math.round(safeTotal * 0.26)
|
|
1853
|
+
const rest = Math.floor((safeTotal - c0) / 4)
|
|
1854
|
+
return [c0, rest, rest, rest, safeTotal - c0 - rest * 3]
|
|
1855
|
+
}
|
|
1856
|
+
const each = Math.floor(safeTotal / colCount)
|
|
1857
|
+
return Array.from({ length: colCount }, (_, index) =>
|
|
1858
|
+
index === colCount - 1 ? safeTotal - each * (colCount - 1) : each
|
|
1859
|
+
)
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/** 列寬用絕對 pt,總和 = 正文區滿寬,對齊右頁邊距(Word 對 width:100% 常收縮) */
|
|
1863
|
+
function applyTableColgroupPt(table: HTMLTableElement, widthsPt: number[]) {
|
|
1864
|
+
table.querySelector('colgroup')?.remove()
|
|
1865
|
+
const colgroup = document.createElement('colgroup')
|
|
1866
|
+
widthsPt.forEach((width) => {
|
|
1867
|
+
const col = document.createElement('col')
|
|
1868
|
+
col.setAttribute('width', String(width))
|
|
1869
|
+
col.setAttribute(
|
|
1870
|
+
'style',
|
|
1871
|
+
`width:${width}pt;mso-width-source:userset;mso-width-alt:${width * 20}`
|
|
1872
|
+
)
|
|
1873
|
+
colgroup.appendChild(col)
|
|
1874
|
+
})
|
|
1875
|
+
table.insertBefore(colgroup, table.firstChild)
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
type DocxCellInfo = {
|
|
1879
|
+
text: string
|
|
1880
|
+
tag: 'th' | 'td'
|
|
1881
|
+
className: string
|
|
1882
|
+
textAlign: string
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
/** 将含 colspan/rowspan 的表格展开为逻辑网格 */
|
|
1886
|
+
function expandTableToGrid(table: HTMLTableElement): DocxCellInfo[][] {
|
|
1887
|
+
const colCount = getTableColumnCount(table)
|
|
1888
|
+
const grid: (DocxCellInfo | null)[][] = []
|
|
1889
|
+
|
|
1890
|
+
Array.from(table.rows).forEach((row, rowIndex) => {
|
|
1891
|
+
if (!grid[rowIndex]) grid[rowIndex] = Array(colCount).fill(null)
|
|
1892
|
+
let colIndex = 0
|
|
1893
|
+
|
|
1894
|
+
Array.from(row.cells).forEach((cell) => {
|
|
1895
|
+
while (colIndex < colCount && grid[rowIndex]![colIndex] !== null) colIndex++
|
|
1896
|
+
|
|
1897
|
+
const info: DocxCellInfo = {
|
|
1898
|
+
text: cell.textContent?.trim() ?? '',
|
|
1899
|
+
tag: cell.tagName === 'TH' ? 'th' : 'td',
|
|
1900
|
+
className: cell.className,
|
|
1901
|
+
textAlign: (cell as HTMLElement).style.textAlign || '',
|
|
1902
|
+
}
|
|
1903
|
+
const colspan = cell.colSpan || 1
|
|
1904
|
+
const rowspan = cell.rowSpan || 1
|
|
1905
|
+
|
|
1906
|
+
for (let r = 0; r < rowspan; r++) {
|
|
1907
|
+
for (let c = 0; c < colspan; c++) {
|
|
1908
|
+
const targetRow = rowIndex + r
|
|
1909
|
+
if (!grid[targetRow]) grid[targetRow] = Array(colCount).fill(null)
|
|
1910
|
+
grid[targetRow]![colIndex + c] =
|
|
1911
|
+
r === 0 && c === 0 ? info : { ...info, text: '' }
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
colIndex += colspan
|
|
1915
|
+
})
|
|
1916
|
+
})
|
|
1917
|
+
|
|
1918
|
+
return grid.filter((row) => row.some((cell) => cell !== null)) as DocxCellInfo[][]
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
function applyWordTableStyles(
|
|
1922
|
+
table: HTMLTableElement,
|
|
1923
|
+
colCount: number,
|
|
1924
|
+
tableWidthPt = A4_CONTENT_WIDTH_PT
|
|
1925
|
+
) {
|
|
1926
|
+
const isComplex = table.classList.contains('doc-table-complex') || colCount >= 5
|
|
1927
|
+
const cellFontSize = isComplex ? '9pt' : '10pt'
|
|
1928
|
+
const cellPadding = isComplex ? '2pt 3pt' : '3pt 4pt'
|
|
1929
|
+
const colWidthsPt = getColumnWidthsPt(colCount, tableWidthPt)
|
|
1930
|
+
|
|
1931
|
+
applyTableColgroupPt(table, colWidthsPt)
|
|
1932
|
+
|
|
1933
|
+
// 清掉編輯器 / getComputedStyle 寫死的 height,避免 Word 把末行拉滿頁面
|
|
1934
|
+
table.removeAttribute('width')
|
|
1935
|
+
table.removeAttribute('height')
|
|
1936
|
+
table.removeAttribute('style')
|
|
1937
|
+
table.setAttribute('width', String(tableWidthPt))
|
|
1938
|
+
table.style.cssText = [
|
|
1939
|
+
'border-collapse:collapse',
|
|
1940
|
+
`width:${tableWidthPt}pt`,
|
|
1941
|
+
`max-width:${tableWidthPt}pt`,
|
|
1942
|
+
'height:auto',
|
|
1943
|
+
'min-height:0',
|
|
1944
|
+
'table-layout:fixed',
|
|
1945
|
+
'mso-table-layout-alt:fixed',
|
|
1946
|
+
'word-wrap:break-word',
|
|
1947
|
+
'overflow-wrap:break-word',
|
|
1948
|
+
'margin:0',
|
|
1949
|
+
'border:1px solid #000000',
|
|
1950
|
+
'box-sizing:border-box',
|
|
1951
|
+
'page-break-inside:auto',
|
|
1952
|
+
].join(';')
|
|
1953
|
+
|
|
1954
|
+
table.querySelectorAll('tr').forEach((row) => {
|
|
1955
|
+
const tr = row as HTMLTableRowElement
|
|
1956
|
+
tr.removeAttribute('height')
|
|
1957
|
+
tr.style.height = 'auto'
|
|
1958
|
+
tr.style.minHeight = '0'
|
|
1959
|
+
tr.style.pageBreakInside = 'auto'
|
|
1960
|
+
Array.from(tr.cells).forEach((cell, cellIndex) => {
|
|
1961
|
+
cell.removeAttribute('colspan')
|
|
1962
|
+
cell.removeAttribute('rowspan')
|
|
1963
|
+
cell.removeAttribute('height')
|
|
1964
|
+
const colW = colWidthsPt[cellIndex] ?? colWidthsPt[colWidthsPt.length - 1]
|
|
1965
|
+
if (colW != null) {
|
|
1966
|
+
cell.setAttribute('width', String(colW))
|
|
1967
|
+
cell.style.width = `${colW}pt`
|
|
1968
|
+
}
|
|
1969
|
+
cell.style.height = 'auto'
|
|
1970
|
+
cell.style.minHeight = '0'
|
|
1971
|
+
cell.style.minWidth = ''
|
|
1972
|
+
cell.style.maxWidth = ''
|
|
1973
|
+
cell.style.border = '1px solid #000000'
|
|
1974
|
+
cell.style.padding = cellPadding
|
|
1975
|
+
cell.style.fontSize = cellFontSize
|
|
1976
|
+
cell.style.lineHeight = '1.35'
|
|
1977
|
+
cell.style.wordWrap = 'break-word'
|
|
1978
|
+
cell.style.overflowWrap = 'break-word'
|
|
1979
|
+
cell.style.boxSizing = 'border-box'
|
|
1980
|
+
cell.style.verticalAlign = 'top'
|
|
1981
|
+
cell.style.pageBreakInside = 'auto'
|
|
1982
|
+
cell.style.overflow = 'hidden'
|
|
1983
|
+
|
|
1984
|
+
if (!cell.classList.contains('doc-th-green') && !cell.classList.contains('doc-th-red')) {
|
|
1985
|
+
cell.style.color = '#000000'
|
|
1986
|
+
}
|
|
1987
|
+
})
|
|
1988
|
+
})
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function createWordTableCell(info: DocxCellInfo, cellFontSize: string, cellPadding: string) {
|
|
1992
|
+
const cell = document.createElement(info.tag)
|
|
1993
|
+
cell.className = info.className
|
|
1994
|
+
cell.textContent = info.text
|
|
1995
|
+
cell.style.border = '1px solid #000000'
|
|
1996
|
+
cell.style.padding = cellPadding
|
|
1997
|
+
cell.style.fontSize = cellFontSize
|
|
1998
|
+
cell.style.lineHeight = '1.35'
|
|
1999
|
+
cell.style.height = 'auto'
|
|
2000
|
+
cell.style.minHeight = '0'
|
|
2001
|
+
cell.style.wordWrap = 'break-word'
|
|
2002
|
+
cell.style.overflowWrap = 'break-word'
|
|
2003
|
+
cell.style.boxSizing = 'border-box'
|
|
2004
|
+
cell.style.verticalAlign = 'top'
|
|
2005
|
+
cell.style.pageBreakInside = 'auto'
|
|
2006
|
+
if (info.textAlign) cell.style.textAlign = info.textAlign
|
|
2007
|
+
if (!cell.classList.contains('doc-th-green') && !cell.classList.contains('doc-th-red')) {
|
|
2008
|
+
cell.style.color = '#000000'
|
|
2009
|
+
}
|
|
2010
|
+
return cell
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
function rebuildFlatWordTable(
|
|
2014
|
+
grid: DocxCellInfo[][],
|
|
2015
|
+
tableClasses: string,
|
|
2016
|
+
tableWidthPt = A4_CONTENT_WIDTH_PT
|
|
2017
|
+
): HTMLDivElement {
|
|
2018
|
+
const colCount = grid[0]?.length ?? 0
|
|
2019
|
+
const isComplex = tableClasses.includes('doc-table-complex') || colCount >= 5
|
|
2020
|
+
const cellFontSize = isComplex ? '9pt' : '10pt'
|
|
2021
|
+
const cellPadding = isComplex ? '2pt 3pt' : '3pt 4pt'
|
|
2022
|
+
const nested = tableWidthPt < A4_CONTENT_WIDTH_PT - 1
|
|
2023
|
+
|
|
2024
|
+
const wrap = document.createElement('div')
|
|
2025
|
+
wrap.className = nested ? 'docx-table-wrap docx-table-wrap--nested' : 'docx-table-wrap'
|
|
2026
|
+
wrap.style.cssText = [
|
|
2027
|
+
`width:${tableWidthPt}pt`,
|
|
2028
|
+
`max-width:${tableWidthPt}pt`,
|
|
2029
|
+
'height:auto',
|
|
2030
|
+
'margin:6pt 0 12pt',
|
|
2031
|
+
'padding:0',
|
|
2032
|
+
'overflow:hidden',
|
|
2033
|
+
'box-sizing:border-box',
|
|
2034
|
+
].join(';')
|
|
2035
|
+
|
|
2036
|
+
const table = document.createElement('table')
|
|
2037
|
+
table.className = `${tableClasses} doc-table-word`.trim()
|
|
2038
|
+
applyWordTableStyles(table, colCount, tableWidthPt)
|
|
2039
|
+
|
|
2040
|
+
grid.forEach((row) => {
|
|
2041
|
+
const tr = document.createElement('tr')
|
|
2042
|
+
tr.style.height = 'auto'
|
|
2043
|
+
tr.style.minHeight = '0'
|
|
2044
|
+
tr.style.pageBreakInside = 'auto'
|
|
2045
|
+
row.forEach((info) => {
|
|
2046
|
+
tr.appendChild(createWordTableCell(info, cellFontSize, cellPadding))
|
|
2047
|
+
})
|
|
2048
|
+
table.appendChild(tr)
|
|
2049
|
+
})
|
|
2050
|
+
|
|
2051
|
+
wrap.appendChild(table)
|
|
2052
|
+
return wrap
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function tableHasSpanningCells(table: HTMLTableElement): boolean {
|
|
2056
|
+
return Array.from(table.querySelectorAll('th, td')).some((cell) => {
|
|
2057
|
+
const el = cell as HTMLTableCellElement
|
|
2058
|
+
return (el.colSpan || 1) > 1 || (el.rowSpan || 1) > 1
|
|
2059
|
+
})
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
function getTableNestingDepth(table: HTMLTableElement): number {
|
|
2063
|
+
let depth = 0
|
|
2064
|
+
let el: HTMLElement | null = table.parentElement
|
|
2065
|
+
while (el) {
|
|
2066
|
+
if (el instanceof HTMLTableElement) depth += 1
|
|
2067
|
+
el = el.parentElement
|
|
2068
|
+
}
|
|
2069
|
+
return depth
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
/** 嵌套表寬 = 父單元格可用寬(等比例,勿再拉滿整頁) */
|
|
2073
|
+
function resolveTableWidthPt(table: HTMLTableElement): number {
|
|
2074
|
+
const parentCell = table.parentElement?.closest('td, th') as HTMLTableCellElement | null
|
|
2075
|
+
if (!parentCell) return A4_CONTENT_WIDTH_PT
|
|
2076
|
+
|
|
2077
|
+
const raw =
|
|
2078
|
+
Number.parseFloat(parentCell.getAttribute('width') || '') ||
|
|
2079
|
+
Number.parseFloat(String(parentCell.style.width || '').replace(/pt$/i, '')) ||
|
|
2080
|
+
0
|
|
2081
|
+
// 減去單元格左右 padding,避免嵌套表撐破父格
|
|
2082
|
+
const inner = raw > 0 ? Math.max(36, Math.round(raw - 8)) : Math.round(A4_CONTENT_WIDTH_PT * 0.48)
|
|
2083
|
+
return Math.min(inner, A4_CONTENT_WIDTH_PT)
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
/** 去掉全空行,避免 Word 把空白行拉高 */
|
|
2087
|
+
function removeBlankTableRows(table: HTMLTableElement) {
|
|
2088
|
+
Array.from(table.rows).forEach((row) => {
|
|
2089
|
+
const hasText = Array.from(row.cells).some((cell) => (cell.textContent || '').trim().length > 0)
|
|
2090
|
+
const hasMedia = Array.from(row.cells).some((cell) => cell.querySelector('img, table, svg'))
|
|
2091
|
+
if (!hasText && !hasMedia) row.remove()
|
|
2092
|
+
})
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
/** 重建 Word 安全表格:頂層鋪滿正文區;嵌套表按父格寬等比例;禁止行高被拉長 */
|
|
2096
|
+
function rebuildTablesForWord(root: HTMLElement) {
|
|
2097
|
+
const tables = Array.from(root.querySelectorAll('table')).filter((node) => {
|
|
2098
|
+
if (!(node instanceof HTMLTableElement)) return false
|
|
2099
|
+
if (node.classList.contains('docx-toc-row')) return false
|
|
2100
|
+
// 列表双列表:勿重建、勿加黑框
|
|
2101
|
+
if (node.classList.contains('doc-list-item-table')) return false
|
|
2102
|
+
if (node.closest('.doc-ol-export')) return false
|
|
2103
|
+
return true
|
|
2104
|
+
}) as HTMLTableElement[]
|
|
2105
|
+
|
|
2106
|
+
// 外層先處理,嵌套表才能讀到父 td 的 pt 寬
|
|
2107
|
+
tables.sort((a, b) => getTableNestingDepth(a) - getTableNestingDepth(b))
|
|
2108
|
+
|
|
2109
|
+
tables.forEach((node) => {
|
|
2110
|
+
// 已被包進 wrap 後替換掉的節點可能脫離文檔
|
|
2111
|
+
if (!root.contains(node)) return
|
|
2112
|
+
|
|
2113
|
+
removeBlankTableRows(node)
|
|
2114
|
+
if (!node.rows.length) {
|
|
2115
|
+
node.remove()
|
|
2116
|
+
return
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
const tableWidthPt = resolveTableWidthPt(node)
|
|
2120
|
+
const nested = tableWidthPt < A4_CONTENT_WIDTH_PT - 1
|
|
2121
|
+
const tableClasses = Array.from(node.classList)
|
|
2122
|
+
.filter((c) => c !== 'doc-table-word')
|
|
2123
|
+
.join(' ')
|
|
2124
|
+
const colCount = getTableColumnCount(node)
|
|
2125
|
+
|
|
2126
|
+
if (tableHasSpanningCells(node)) {
|
|
2127
|
+
const grid = expandTableToGrid(node)
|
|
2128
|
+
// 展開後再剔空行
|
|
2129
|
+
const compact = grid.filter((row) => row.some((cell) => (cell.text || '').trim().length > 0))
|
|
2130
|
+
node.replaceWith(
|
|
2131
|
+
rebuildFlatWordTable(compact.length ? compact : grid, tableClasses, tableWidthPt)
|
|
2132
|
+
)
|
|
2133
|
+
return
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
const wrap = document.createElement('div')
|
|
2137
|
+
wrap.className = nested ? 'docx-table-wrap docx-table-wrap--nested' : 'docx-table-wrap'
|
|
2138
|
+
wrap.style.cssText = [
|
|
2139
|
+
`width:${tableWidthPt}pt`,
|
|
2140
|
+
`max-width:${tableWidthPt}pt`,
|
|
2141
|
+
'height:auto',
|
|
2142
|
+
'margin:6pt 0 12pt',
|
|
2143
|
+
'padding:0',
|
|
2144
|
+
'overflow:hidden',
|
|
2145
|
+
'box-sizing:border-box',
|
|
2146
|
+
].join(';')
|
|
2147
|
+
|
|
2148
|
+
node.classList.add('doc-table-word')
|
|
2149
|
+
applyWordTableStyles(node, colCount, tableWidthPt)
|
|
2150
|
+
node.parentNode?.insertBefore(wrap, node)
|
|
2151
|
+
wrap.appendChild(node)
|
|
2152
|
+
})
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
/** 導出正文去掉 [ref:…](含全角冒號/括號) */
|
|
2156
|
+
export function stripRefMarkersFromHtml(value: string) {
|
|
2157
|
+
return String(value || '')
|
|
2158
|
+
.replace(/\[\s*ref\s*[::]\s*[^\]]+\]/gi, '')
|
|
2159
|
+
.replace(/[\s*ref\s*[::][^]]+]/gi, '')
|
|
2160
|
+
.replace(
|
|
2161
|
+
/<p\b[^>]*>\s*參考資料\s*[::]?\s*<\/p>\s*(?:<p\b[^>]*>\s*(?:\[\d+\]\s*)*(?:\[\s*ref\s*[::][^\]]*\])?(?:\s*\[\d+\])*\s*<\/p>\s*)+/gi,
|
|
2162
|
+
''
|
|
2163
|
+
)
|
|
2164
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/** 章節書籤:目錄 PAGEREF 目標(name + id,兼容 Word HTML altChunk) */
|
|
2168
|
+
function isChapterBookmarkName(value: string): boolean {
|
|
2169
|
+
return /^_Ch\d+$/i.test(String(value || '').trim())
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/** 表格重建 / 清裝飾後重新掛 _ChN 書籤(掛在章標題上,避免空錨被 Word 丟掉) */
|
|
2173
|
+
export function reannotateChapterBookmarksInDom(root: HTMLElement): void {
|
|
2174
|
+
const sections = Array.from(root.querySelectorAll('.docx-export-chapter'))
|
|
2175
|
+
const targets: Element[] = sections.length
|
|
2176
|
+
? sections
|
|
2177
|
+
: Array.from(root.querySelectorAll('h1.docx-chapter-title'))
|
|
2178
|
+
|
|
2179
|
+
targets.forEach((target, i) => {
|
|
2180
|
+
const name = `_Ch${i + 1}`
|
|
2181
|
+
const heading =
|
|
2182
|
+
target.matches?.('h1, h2')
|
|
2183
|
+
? target
|
|
2184
|
+
: target.querySelector('h1.docx-chapter-title, h1, h2') || target
|
|
2185
|
+
|
|
2186
|
+
if (heading instanceof HTMLElement) {
|
|
2187
|
+
heading.setAttribute('id', name)
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
let anchor =
|
|
2191
|
+
(heading.querySelector(`:scope > a[name="${name}"]`) as HTMLElement | null) ||
|
|
2192
|
+
(heading.querySelector(`a[name="${name}"]`) as HTMLElement | null)
|
|
2193
|
+
if (!anchor) {
|
|
2194
|
+
anchor = document.createElement('a')
|
|
2195
|
+
heading.insertBefore(anchor, heading.firstChild)
|
|
2196
|
+
}
|
|
2197
|
+
anchor.setAttribute('name', name)
|
|
2198
|
+
anchor.removeAttribute('id')
|
|
2199
|
+
anchor.removeAttribute('href')
|
|
2200
|
+
})
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
/**
|
|
2204
|
+
* 最終打包前清正文紅藍下標:
|
|
2205
|
+
* - 刪 [ref:…]
|
|
2206
|
+
* - 展開正文 <u>(保留小組成員標題)
|
|
2207
|
+
* - 去掉 underline / 藍紅底邊
|
|
2208
|
+
* - 保留目錄 PAGEREF 用的章節書籤 a[name=_ChN] / id=_ChN
|
|
2209
|
+
*/
|
|
2210
|
+
export function sanitizeExportBodyHtml(html: string): string {
|
|
2211
|
+
let out = stripRefMarkersFromHtml(html)
|
|
2212
|
+
|
|
2213
|
+
out = out.replace(
|
|
2214
|
+
/(<p\b[^>]*class="[^"]*docx-members-label[^"]*"[^>]*>[\s\S]*?<\/p>)|<u\b[^>]*>([\s\S]*?)<\/u>/gi,
|
|
2215
|
+
(_full, membersLabel, inner) => (membersLabel ? membersLabel : String(inner ?? ''))
|
|
2216
|
+
)
|
|
2217
|
+
|
|
2218
|
+
out = out.replace(/\btext-decoration\s*:\s*[^;"']*underline[^;"']*;?/gi, 'text-decoration:none;')
|
|
2219
|
+
out = out.replace(/\btext-decoration-line\s*:\s*[^;"']*;?/gi, '')
|
|
2220
|
+
out = out.replace(/\btext-decoration-color\s*:\s*[^;"']*;?/gi, '')
|
|
2221
|
+
// 不在此處動 border-bottom:會誤傷表格下邊線
|
|
2222
|
+
// 去掉普通鏈接,但保留 _ChN 書籤錨點(目錄 PAGEREF 依賴)
|
|
2223
|
+
out = out.replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi, (full, attrs, inner) => {
|
|
2224
|
+
if (/\b(?:name|id)\s*=\s*["']_Ch\d+["']/i.test(String(attrs || ''))) return full
|
|
2225
|
+
return String(inner ?? '')
|
|
2226
|
+
})
|
|
2227
|
+
|
|
2228
|
+
return out
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
/** 正文去掉引用標記 / 藍紅下劃線等裝飾(頁眉 OOXML 雙藍線不受影響) */
|
|
2232
|
+
function stripBodyExportDecorations(root: HTMLElement) {
|
|
2233
|
+
const isMembersLabel = (el: Element) =>
|
|
2234
|
+
el.classList.contains('docx-members-label') || !!el.closest('.docx-members-label')
|
|
2235
|
+
|
|
2236
|
+
// 1) 刪除 citation 圓標(導出不需要)
|
|
2237
|
+
root.querySelectorAll('.report-tiptap-citation, [data-citation]').forEach((el) => el.remove())
|
|
2238
|
+
|
|
2239
|
+
// 2) 章節末「參考資料:[n] [ref:…]」整段去掉(佔位下標)
|
|
2240
|
+
root.querySelectorAll('.docx-chapter-refs-label, .docx-chapter-refs-item').forEach((el) => {
|
|
2241
|
+
el.remove()
|
|
2242
|
+
})
|
|
2243
|
+
|
|
2244
|
+
// 3) 鏈接去掉藍字下劃線,只留純文字;章節書籤 a[name=_ChN] 必須保留(目錄 PAGEREF)
|
|
2245
|
+
root.querySelectorAll('a').forEach((el) => {
|
|
2246
|
+
if (!(el instanceof HTMLElement)) return
|
|
2247
|
+
if (isMembersLabel(el)) return
|
|
2248
|
+
const name = String(el.getAttribute('name') || '')
|
|
2249
|
+
const id = String(el.getAttribute('id') || '')
|
|
2250
|
+
if (isChapterBookmarkName(name) || isChapterBookmarkName(id)) {
|
|
2251
|
+
el.removeAttribute('href')
|
|
2252
|
+
el.style.textDecoration = 'none'
|
|
2253
|
+
el.style.color = 'inherit'
|
|
2254
|
+
el.style.borderBottom = 'none'
|
|
2255
|
+
return
|
|
2256
|
+
}
|
|
2257
|
+
const text = el.textContent ?? ''
|
|
2258
|
+
const span = document.createElement('span')
|
|
2259
|
+
span.textContent = text
|
|
2260
|
+
el.replaceWith(span)
|
|
2261
|
+
})
|
|
2262
|
+
|
|
2263
|
+
// 4) <u> 展開為純文字(小組成員標題保留下劃線)
|
|
2264
|
+
root.querySelectorAll('u').forEach((el) => {
|
|
2265
|
+
if (isMembersLabel(el)) return
|
|
2266
|
+
const parent = el.parentNode
|
|
2267
|
+
if (!parent) return
|
|
2268
|
+
while (el.firstChild) parent.insertBefore(el.firstChild, el)
|
|
2269
|
+
el.remove()
|
|
2270
|
+
})
|
|
2271
|
+
|
|
2272
|
+
// 5) 去掉正文內聯 [ref:…](含全角冒號;跨文本節點先併父級再清)
|
|
2273
|
+
const refRe = /\[\s*ref\s*[::]\s*[^\]]+\]/gi
|
|
2274
|
+
root.querySelectorAll('p, li, td, th, h1, h2, h3, h4, span, div').forEach((el) => {
|
|
2275
|
+
if (!(el instanceof HTMLElement)) return
|
|
2276
|
+
if (isMembersLabel(el)) return
|
|
2277
|
+
const raw = el.innerHTML
|
|
2278
|
+
if (!/ref\s*[::]/i.test(raw) && !/[\s*ref/i.test(raw)) return
|
|
2279
|
+
const cleaned = stripRefMarkersFromHtml(raw)
|
|
2280
|
+
if (cleaned !== raw) el.innerHTML = cleaned
|
|
2281
|
+
})
|
|
2282
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
2283
|
+
const textNodes: Text[] = []
|
|
2284
|
+
while (walker.nextNode()) textNodes.push(walker.currentNode as Text)
|
|
2285
|
+
for (const node of textNodes) {
|
|
2286
|
+
if (!node.nodeValue || !refRe.test(node.nodeValue)) continue
|
|
2287
|
+
refRe.lastIndex = 0
|
|
2288
|
+
const next = node.nodeValue.replace(refRe, '').replace(/[ \t]{2,}/g, ' ')
|
|
2289
|
+
node.nodeValue = next
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// 5b) 刪掉只剩「參考資料:」或空編號的段落
|
|
2293
|
+
root.querySelectorAll('p').forEach((el) => {
|
|
2294
|
+
const t = (el.textContent || '').replace(/\s+/g, ' ').trim()
|
|
2295
|
+
if (/^參考資料\s*[::]?$/.test(t) || /^\[\d+\]$/.test(t)) {
|
|
2296
|
+
el.remove()
|
|
2297
|
+
}
|
|
2298
|
+
})
|
|
2299
|
+
|
|
2300
|
+
const isBlue = (c: string) => {
|
|
2301
|
+
const s = String(c || '').toLowerCase().replace(/\s+/g, '')
|
|
2302
|
+
if (!s || s === 'transparent' || s === 'inherit') return false
|
|
2303
|
+
if (s.includes('2262') || s.includes('0563c1') || s.includes('409eff') || s.includes('0070c0')) {
|
|
2304
|
+
return true
|
|
2305
|
+
}
|
|
2306
|
+
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(s)) {
|
|
2307
|
+
const hex =
|
|
2308
|
+
s.length === 4 ? `#${s[1]}${s[1]}${s[2]}${s[2]}${s[3]}${s[3]}` : s
|
|
2309
|
+
const r = Number.parseInt(hex.slice(1, 3), 16)
|
|
2310
|
+
const g = Number.parseInt(hex.slice(3, 5), 16)
|
|
2311
|
+
const b = Number.parseInt(hex.slice(5, 7), 16)
|
|
2312
|
+
return b > 140 && b > r + 30 && b > g + 20
|
|
2313
|
+
}
|
|
2314
|
+
return /rgb\(\s*\d+\s*,\s*\d+\s*,\s*(1[5-9]\d|2\d{2})\s*\)/i.test(s)
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
const isRed = (c: string) => {
|
|
2318
|
+
const s = String(c || '').toLowerCase().replace(/\s+/g, '')
|
|
2319
|
+
if (!s || s === 'transparent' || s === 'inherit') return false
|
|
2320
|
+
if (s.includes('ff0000') || s.includes('e74c3c') || s.includes('f56c6c') || s.includes('c00000')) {
|
|
2321
|
+
return true
|
|
2322
|
+
}
|
|
2323
|
+
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(s)) {
|
|
2324
|
+
const hex =
|
|
2325
|
+
s.length === 4 ? `#${s[1]}${s[1]}${s[2]}${s[2]}${s[3]}${s[3]}` : s
|
|
2326
|
+
const r = Number.parseInt(hex.slice(1, 3), 16)
|
|
2327
|
+
const g = Number.parseInt(hex.slice(3, 5), 16)
|
|
2328
|
+
const b = Number.parseInt(hex.slice(5, 7), 16)
|
|
2329
|
+
return r > 160 && r > g + 40 && r > b + 40
|
|
2330
|
+
}
|
|
2331
|
+
return /rgb\(\s*(1[5-9]\d|2\d{2})\s*,\s*\d+\s*,\s*\d+\s*\)/i.test(s)
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// 6) 去掉正文文字下劃線 / 藍紅裝飾底邊(絕對不碰表格)
|
|
2335
|
+
const isTablePart = (el: Element) =>
|
|
2336
|
+
/^(TABLE|THEAD|TBODY|TFOOT|TR|TD|TH|COLGROUP|COL)$/i.test(el.tagName) ||
|
|
2337
|
+
!!el.closest('table')
|
|
2338
|
+
|
|
2339
|
+
root.querySelectorAll('*').forEach((node) => {
|
|
2340
|
+
if (!(node instanceof HTMLElement)) return
|
|
2341
|
+
if (isMembersLabel(node)) return
|
|
2342
|
+
if (isTablePart(node)) return
|
|
2343
|
+
const style = node.style
|
|
2344
|
+
|
|
2345
|
+
// 僅清文字裝飾用底邊(藍/紅),勿動黑框表格
|
|
2346
|
+
if (style.borderBottom && style.borderBottom !== 'none' && style.borderBottom !== '0px') {
|
|
2347
|
+
if (
|
|
2348
|
+
isBlue(style.borderBottomColor) ||
|
|
2349
|
+
isRed(style.borderBottomColor) ||
|
|
2350
|
+
isBlue(style.borderColor) ||
|
|
2351
|
+
isRed(style.borderColor)
|
|
2352
|
+
) {
|
|
2353
|
+
style.borderBottom = 'none'
|
|
2354
|
+
style.borderBottomColor = ''
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
const deco = String(style.textDecoration || style.textDecorationLine || '')
|
|
2359
|
+
const decoColor = String(style.textDecorationColor || '')
|
|
2360
|
+
if (
|
|
2361
|
+
/underline/i.test(deco) ||
|
|
2362
|
+
isBlue(decoColor) ||
|
|
2363
|
+
isRed(decoColor) ||
|
|
2364
|
+
node.classList.contains('report-tiptap-link') ||
|
|
2365
|
+
node.classList.contains('doc-link-anchor')
|
|
2366
|
+
) {
|
|
2367
|
+
style.textDecoration = 'none'
|
|
2368
|
+
style.textDecorationColor = ''
|
|
2369
|
+
style.textDecorationLine = 'none'
|
|
2370
|
+
if (isBlue(style.color) || isRed(style.color)) {
|
|
2371
|
+
style.color = '#000000'
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
if (/^H[1-6]$/.test(node.tagName) || node.classList.contains('docx-chapter-title')) {
|
|
2376
|
+
style.borderBottom = 'none'
|
|
2377
|
+
style.textDecoration = 'none'
|
|
2378
|
+
}
|
|
2379
|
+
})
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
/** Word 导出移除预览分页标记,避免 MHT 中产生整页空白 */
|
|
2383
|
+
export function stripWordPageBreaks(root: HTMLElement) {
|
|
2384
|
+
root.querySelectorAll('[data-export-page-break]').forEach((el) => el.remove())
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
/** @deprecated 使用 stripWordPageBreaks,注入分页符会在 Word 中产生大量空白 */
|
|
2388
|
+
export function injectWordPageBreaks(root: HTMLElement) {
|
|
2389
|
+
stripWordPageBreaks(root)
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
/** 剥离 Vue / 无用属性,内联计算样式,尽量贴近页面 1:1 */
|
|
2393
|
+
export function prepareDomForDocx(root: HTMLElement, inPlace = false): HTMLElement {
|
|
2394
|
+
const clone = inPlace ? root : (root.cloneNode(true) as HTMLElement)
|
|
2395
|
+
|
|
2396
|
+
clone.querySelectorAll('.no-print, .animate-pulse, .typewriter-cursor').forEach((el) =>
|
|
2397
|
+
el.remove()
|
|
2398
|
+
)
|
|
2399
|
+
|
|
2400
|
+
const unmount = mountCloneForStyleRead(clone)
|
|
2401
|
+
|
|
2402
|
+
try {
|
|
2403
|
+
clone.querySelectorAll('*').forEach((el) => {
|
|
2404
|
+
if (!(el instanceof HTMLElement)) return
|
|
2405
|
+
|
|
2406
|
+
applyDocClassInlineStyles(el)
|
|
2407
|
+
|
|
2408
|
+
const computed = window.getComputedStyle(el)
|
|
2409
|
+
if (computed.display === 'flex' || computed.display === 'inline-flex') {
|
|
2410
|
+
el.style.display = 'block'
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
for (const prop of STYLE_PROPS) {
|
|
2414
|
+
// 表格行高絕不能內聯像素 height,否則 Word 會把末行拉長變形
|
|
2415
|
+
if (prop === 'height' && /^(TABLE|TR|TD|TH|TBODY|THEAD|TFOOT|COLGROUP|COL)$/i.test(el.tagName)) {
|
|
2416
|
+
continue
|
|
2417
|
+
}
|
|
2418
|
+
const val = computed[prop as keyof CSSStyleDeclaration]
|
|
2419
|
+
if (!val || val === 'initial' || val === 'normal' || val === 'auto' || val === 'none') {
|
|
2420
|
+
continue
|
|
2421
|
+
}
|
|
2422
|
+
// TipTap / 滾動容器常算出 height:0,內聯後 Word 正文被裁成空白
|
|
2423
|
+
if (
|
|
2424
|
+
(prop === 'height' || prop === 'width' || prop === 'maxWidth') &&
|
|
2425
|
+
(val === '0px' || val === '0' || val === '0%')
|
|
2426
|
+
) {
|
|
2427
|
+
continue
|
|
2428
|
+
}
|
|
2429
|
+
const styleRecord = el.style as unknown as Record<string, string>
|
|
2430
|
+
if (!styleRecord[prop]) {
|
|
2431
|
+
styleRecord[prop] = String(val)
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
if (el.tagName === 'TABLE') {
|
|
2436
|
+
el.style.borderCollapse = 'collapse'
|
|
2437
|
+
el.style.tableLayout = 'fixed'
|
|
2438
|
+
el.style.width = '100%'
|
|
2439
|
+
el.style.maxWidth = '100%'
|
|
2440
|
+
el.style.height = 'auto'
|
|
2441
|
+
el.style.minHeight = '0'
|
|
2442
|
+
el.style.pageBreakInside = 'auto'
|
|
2443
|
+
el.removeAttribute('height')
|
|
2444
|
+
}
|
|
2445
|
+
if (el.tagName === 'TR') {
|
|
2446
|
+
el.style.height = 'auto'
|
|
2447
|
+
el.style.minHeight = '0'
|
|
2448
|
+
el.removeAttribute('height')
|
|
2449
|
+
}
|
|
2450
|
+
if (el.tagName === 'TD' || el.tagName === 'TH') {
|
|
2451
|
+
el.style.border = '1px solid #000000'
|
|
2452
|
+
el.style.padding = '4pt 6pt'
|
|
2453
|
+
el.style.fontSize = '11pt'
|
|
2454
|
+
el.style.height = 'auto'
|
|
2455
|
+
el.style.minHeight = '0'
|
|
2456
|
+
el.style.verticalAlign = 'top'
|
|
2457
|
+
el.removeAttribute('height')
|
|
2458
|
+
if (!el.classList.contains('doc-th-green') && !el.classList.contains('doc-th-red')) {
|
|
2459
|
+
el.style.color = '#000000'
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
if (el.tagName === 'IMG') {
|
|
2463
|
+
const img = el as HTMLImageElement
|
|
2464
|
+
el.style.display = el.style.display || 'block'
|
|
2465
|
+
el.style.maxWidth = el.style.maxWidth || '100%'
|
|
2466
|
+
if (!img.getAttribute('width') && img.naturalWidth > 0) {
|
|
2467
|
+
img.setAttribute('width', String(img.naturalWidth))
|
|
2468
|
+
}
|
|
2469
|
+
if (!img.getAttribute('height') && img.naturalHeight > 0) {
|
|
2470
|
+
img.setAttribute('height', String(img.naturalHeight))
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
})
|
|
2474
|
+
} finally {
|
|
2475
|
+
unmount()
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
clone.querySelectorAll('*').forEach((el) => {
|
|
2479
|
+
if (!(el instanceof HTMLElement)) return
|
|
2480
|
+
Array.from(el.attributes).forEach((attr) => {
|
|
2481
|
+
if (
|
|
2482
|
+
attr.name.startsWith('data-v-') ||
|
|
2483
|
+
attr.name === 'data-v-app' ||
|
|
2484
|
+
attr.name.startsWith('data-cy') ||
|
|
2485
|
+
attr.name === 'data-docx-export-host'
|
|
2486
|
+
) {
|
|
2487
|
+
el.removeAttribute(attr.name)
|
|
2488
|
+
}
|
|
2489
|
+
})
|
|
2490
|
+
})
|
|
2491
|
+
|
|
2492
|
+
clampAllImagesForDocx(clone)
|
|
2493
|
+
flattenChartIntroForDocx(clone)
|
|
2494
|
+
flattenImagesBlockForDocx(clone)
|
|
2495
|
+
normalizeListsForDocx(clone)
|
|
2496
|
+
rebuildTablesForWord(clone)
|
|
2497
|
+
stripBodyExportDecorations(clone)
|
|
2498
|
+
// 表格重建 / 去裝飾後重新掛章節書籤,供目錄 PAGEREF(必須在 strip 之後)
|
|
2499
|
+
reannotateChapterBookmarksInDom(clone)
|
|
2500
|
+
// 去紅藍下標後強制還原表格邊框(防止誤清 border-bottom);列表雙列表保持無邊框
|
|
2501
|
+
clone.querySelectorAll('table').forEach((table) => {
|
|
2502
|
+
if (!(table instanceof HTMLElement)) return
|
|
2503
|
+
if (table.classList.contains('doc-list-item-table') || table.closest('.doc-ol-export')) {
|
|
2504
|
+
table.style.border = 'none'
|
|
2505
|
+
table.style.borderCollapse = 'collapse'
|
|
2506
|
+
return
|
|
2507
|
+
}
|
|
2508
|
+
table.style.borderCollapse = 'collapse'
|
|
2509
|
+
table.style.border = table.style.border || '1px solid #000000'
|
|
2510
|
+
})
|
|
2511
|
+
clone.querySelectorAll('td, th').forEach((cell) => {
|
|
2512
|
+
if (!(cell instanceof HTMLElement)) return
|
|
2513
|
+
if (cell.closest('.doc-list-item-table') || cell.closest('.doc-ol-export')) {
|
|
2514
|
+
cell.style.border = 'none'
|
|
2515
|
+
cell.style.borderBottom = 'none'
|
|
2516
|
+
return
|
|
2517
|
+
}
|
|
2518
|
+
cell.style.border = '1px solid #000000'
|
|
2519
|
+
cell.style.borderBottom = '1px solid #000000'
|
|
2520
|
+
})
|
|
2521
|
+
|
|
2522
|
+
clone.querySelectorAll('.typewriter-card__body, .content-wrapper, .doc-sample-body').forEach((el) => {
|
|
2523
|
+
if (!(el instanceof HTMLElement)) return
|
|
2524
|
+
el.style.minHeight = '0'
|
|
2525
|
+
el.style.height = 'auto'
|
|
2526
|
+
el.style.padding = '0'
|
|
2527
|
+
el.style.maxWidth = '100%'
|
|
2528
|
+
el.style.width = '100%'
|
|
2529
|
+
el.style.boxSizing = 'border-box'
|
|
2530
|
+
})
|
|
2531
|
+
|
|
2532
|
+
clone.classList.add('docx-page-root')
|
|
2533
|
+
clone.style.maxHeight = 'none'
|
|
2534
|
+
clone.style.overflow = 'hidden'
|
|
2535
|
+
clone.style.border = 'none'
|
|
2536
|
+
clone.style.padding = '0'
|
|
2537
|
+
clone.style.minHeight = '0'
|
|
2538
|
+
clone.style.backgroundColor = '#ffffff'
|
|
2539
|
+
clone.style.boxSizing = 'border-box'
|
|
2540
|
+
clone.style.width = '100%'
|
|
2541
|
+
clone.style.maxWidth = '100%'
|
|
2542
|
+
clone.style.margin = '0'
|
|
2543
|
+
clone.style.fontFamily = '"Times New Roman", "宋体", SimSun, serif'
|
|
2544
|
+
clone.style.fontSize = '11pt'
|
|
2545
|
+
clone.style.lineHeight = '1.5'
|
|
2546
|
+
clone.style.color = '#000000'
|
|
2547
|
+
clone.style.wordWrap = 'break-word'
|
|
2548
|
+
|
|
2549
|
+
clone.querySelectorAll('.echarts-container').forEach((el) => {
|
|
2550
|
+
if (!(el instanceof HTMLElement)) return
|
|
2551
|
+
if (el.querySelector('.docx-chart-wrap')) return
|
|
2552
|
+
el.style.width = '100%'
|
|
2553
|
+
el.style.maxWidth = '100%'
|
|
2554
|
+
el.style.textAlign = 'center'
|
|
2555
|
+
el.style.margin = '12pt auto'
|
|
2556
|
+
})
|
|
2557
|
+
|
|
2558
|
+
return clone
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
export async function exportHtmlToDocx(
|
|
2562
|
+
fullHtmlOrParts: string | string[],
|
|
2563
|
+
options: DocxExportOptions = DEFAULT_DOCX_EXPORT_OPTIONS
|
|
2564
|
+
): Promise<Blob> {
|
|
2565
|
+
return buildDocxZip(fullHtmlOrParts, { ...DEFAULT_DOCX_EXPORT_OPTIONS, ...options })
|
|
2566
|
+
}
|