f-docx-editor 0.1.1 → 0.1.3

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.
@@ -1,4613 +0,0 @@
1
- <template>
2
- <div class="view-page" :class="{ 'has-outline': documentOutline.length }">
3
- <aside v-if="documentOutline.length" class="document-outline">
4
- <div class="outline-title">文档目录</div>
5
- <button
6
- v-for="item in documentOutline"
7
- :key="item.id"
8
- type="button"
9
- class="outline-item"
10
- :class="[`level-${item.level}`, { 'is-active': activeOutlineId === item.id }]"
11
- @click="scrollToOutlineItem(item)"
12
- >
13
- <span class="outline-text">{{ item.text }}</span>
14
- </button>
15
- </aside>
16
-
17
- <div
18
- ref="docxScrollRef"
19
- :class="renderType === 'view' ? 'view-box' : 'edit-box'"
20
- @scroll="handleDocumentScroll"
21
- >
22
- <div v-if="isLoading" class="status-box">正在生成文档预览...</div>
23
- <div v-else-if="errorMessage" class="status-box is-error">{{ errorMessage }}</div>
24
- <div
25
- v-show="!isLoading && !errorMessage"
26
- ref="previewRef"
27
- class="docx-preview-container"
28
- ></div>
29
- </div>
30
-
31
- <el-dialog
32
- v-model="editDialogVisible"
33
- title="编辑变量内容"
34
- width="70%"
35
- class="editDialog-dialog"
36
- destroy-on-close
37
- @closed="onDialogClosed"
38
- >
39
- <div class="edit-dialog">
40
- <div class="edit-dialog-tip">
41
- 当前变量:{{ activeEditableField?.key || '--' }}
42
- <span v-if="activeEditableField?.path" class="edit-dialog-path">
43
- {{ activeEditableField.path }}
44
- </span>
45
- </div>
46
-
47
- <div class="editor-toolbar">
48
- <el-tooltip
49
- v-for="item in headingActions"
50
- :key="item.level"
51
- :content="`标题 ${item.level}`"
52
- placement="top"
53
- >
54
- <button
55
- type="button"
56
- class="toolbar-btn"
57
- :class="{ 'is-active': isHeadingActive(item.level) }"
58
- @mousedown.prevent
59
- @click="setHeading(item.level)"
60
- >
61
- <svg class="toolbar-svg heading-svg" viewBox="0 0 28 28" aria-hidden="true">
62
- <text x="5" y="19" class="svg-heading-main">H</text>
63
- <text x="18" y="24" class="svg-heading-level">{{ item.level }}</text>
64
- </svg>
65
- </button>
66
- </el-tooltip>
67
-
68
- <el-tooltip content="正文" placement="top">
69
- <button
70
- type="button"
71
- class="toolbar-btn"
72
- :class="{ 'is-active': isParagraphActive }"
73
- @mousedown.prevent
74
- @click="setParagraph"
75
- >
76
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
77
- <path d="M6 7h16M6 12h16M6 17h13M6 22h10" class="svg-stroke round" />
78
- </svg>
79
- </button>
80
- </el-tooltip>
81
-
82
- <div class="toolbar-divider"></div>
83
-
84
- <el-tooltip content="加粗" placement="top">
85
- <button
86
- type="button"
87
- class="toolbar-btn"
88
- :class="{ 'is-active': isBoldActive }"
89
- @mousedown.prevent
90
- @click="toggleBold"
91
- >
92
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
93
- <text x="8" y="20" class="svg-bold">B</text>
94
- </svg>
95
- </button>
96
- </el-tooltip>
97
-
98
- <el-popover
99
- v-model:visible="tablePickerVisible"
100
- placement="bottom-start"
101
- trigger="click"
102
- :width="340"
103
- popper-class="table-picker-popper"
104
- >
105
- <template #reference>
106
- <button
107
- type="button"
108
- class="toolbar-btn table-trigger-btn"
109
- :class="{ 'is-active': isTableActive }"
110
- title="插入表格"
111
- @mousedown.prevent
112
- >
113
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
114
- <rect x="5" y="5" width="18" height="18" rx="2" class="svg-stroke" />
115
- <path d="M5 11.5h18M5 17.5h18M11.5 5v18M17.5 5v18" class="svg-stroke thin" />
116
- <rect x="6.5" y="6.5" width="15" height="4" class="svg-soft-fill" />
117
- </svg>
118
- </button>
119
- </template>
120
-
121
- <div class="table-picker">
122
- <div class="table-picker-title">插入表格</div>
123
- <div class="table-picker-grid">
124
- <button
125
- v-for="cell in tablePickerCells"
126
- :key="cell.key"
127
- type="button"
128
- class="table-picker-cell"
129
- :class="{ 'is-active': cell.row <= tablePickerHover.rows && cell.col <= tablePickerHover.cols }"
130
- @mouseenter="onTablePickerHover(cell.row, cell.col)"
131
- @click="insertTable(cell.row, cell.col)"
132
- ></button>
133
- </div>
134
- <div class="table-picker-tip">
135
- {{ tablePickerHover.rows }} x {{ tablePickerHover.cols }}
136
- </div>
137
- </div>
138
- </el-popover>
139
-
140
- <el-tooltip content="左对齐" placement="top">
141
- <button
142
- type="button"
143
- class="toolbar-btn"
144
- :class="{ 'is-active': isTextAlignActive('left') }"
145
- @mousedown.prevent
146
- @click="setTextAlign('left')"
147
- >
148
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
149
- <path d="M6 7h16M6 12h11M6 17h16M6 22h9" class="svg-stroke round" />
150
- </svg>
151
- </button>
152
- </el-tooltip>
153
-
154
- <el-tooltip content="居中对齐" placement="top">
155
- <button
156
- type="button"
157
- class="toolbar-btn"
158
- :class="{ 'is-active': isTextAlignActive('center') }"
159
- @mousedown.prevent
160
- @click="setTextAlign('center')"
161
- >
162
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
163
- <path d="M6 7h16M9 12h10M6 17h16M10 22h8" class="svg-stroke round" />
164
- </svg>
165
- </button>
166
- </el-tooltip>
167
-
168
- <el-tooltip content="右对齐" placement="top">
169
- <button
170
- type="button"
171
- class="toolbar-btn"
172
- :class="{ 'is-active': isTextAlignActive('right') }"
173
- @mousedown.prevent
174
- @click="setTextAlign('right')"
175
- >
176
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
177
- <path d="M6 7h16M11 12h11M6 17h16M13 22h9" class="svg-stroke round" />
178
- </svg>
179
- </button>
180
- </el-tooltip>
181
-
182
- <div class="toolbar-divider"></div>
183
-
184
- <el-tooltip content="下方插入行" placement="top">
185
- <span class="toolbar-tip-wrap">
186
- <button
187
- type="button"
188
- class="toolbar-btn table-op-btn"
189
- :disabled="!isTableActive"
190
- @mousedown.prevent
191
- @click="addTableRow"
192
- >
193
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
194
- <rect x="5" y="4" width="18" height="16" rx="1.5" class="svg-stroke muted" />
195
- <path d="M5 9.5h18M5 14.5h18M11 4v16M17 4v16" class="svg-stroke thin muted" />
196
- <path d="M14 23v4M12 25h4" class="svg-stroke accent round" />
197
- </svg>
198
- </button>
199
- </span>
200
- </el-tooltip>
201
-
202
- <el-tooltip content="删除当前行" placement="top">
203
- <span class="toolbar-tip-wrap">
204
- <button
205
- type="button"
206
- class="toolbar-btn table-op-btn"
207
- :disabled="!isTableActive"
208
- @mousedown.prevent
209
- @click="deleteTableRow"
210
- >
211
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
212
- <rect x="5" y="4" width="18" height="16" rx="1.5" class="svg-stroke muted" />
213
- <path d="M5 9.5h18M5 14.5h18M11 4v16M17 4v16" class="svg-stroke thin muted" />
214
- <path d="M11.5 25h5" class="svg-stroke danger round" />
215
- </svg>
216
- </button>
217
- </span>
218
- </el-tooltip>
219
-
220
- <el-tooltip content="右侧插入列" placement="top">
221
- <span class="toolbar-tip-wrap">
222
- <button
223
- type="button"
224
- class="toolbar-btn table-op-btn"
225
- :disabled="!isTableActive"
226
- @mousedown.prevent
227
- @click="addTableColumn"
228
- >
229
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
230
- <rect x="4" y="5" width="16" height="18" rx="1.5" class="svg-stroke muted" />
231
- <path d="M4 11h16M4 17h16M9.5 5v18M15 5v18" class="svg-stroke thin muted" />
232
- <path d="M24 12v4M22 14h4" class="svg-stroke accent round" />
233
- </svg>
234
- </button>
235
- </span>
236
- </el-tooltip>
237
-
238
- <el-tooltip content="删除当前列" placement="top">
239
- <span class="toolbar-tip-wrap">
240
- <button
241
- type="button"
242
- class="toolbar-btn table-op-btn"
243
- :disabled="!isTableActive"
244
- @mousedown.prevent
245
- @click="deleteTableColumn"
246
- >
247
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
248
- <rect x="4" y="5" width="16" height="18" rx="1.5" class="svg-stroke muted" />
249
- <path d="M4 11h16M4 17h16M9.5 5v18M15 5v18" class="svg-stroke thin muted" />
250
- <path d="M22 14h5" class="svg-stroke danger round" />
251
- </svg>
252
- </button>
253
- </span>
254
- </el-tooltip>
255
-
256
- <el-tooltip content="删除表格" placement="top">
257
- <span class="toolbar-tip-wrap">
258
- <button
259
- type="button"
260
- class="toolbar-btn table-op-btn"
261
- :disabled="!isTableActive"
262
- @mousedown.prevent
263
- @click="removeTable"
264
- >
265
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
266
- <rect x="6" y="6" width="16" height="16" rx="2" class="svg-stroke muted" />
267
- <path d="M11 11l6 6M17 11l-6 6" class="svg-stroke danger round" />
268
- </svg>
269
- </button>
270
- </span>
271
- </el-tooltip>
272
-
273
- <div class="toolbar-divider"></div>
274
-
275
- <el-tooltip content="字体颜色" placement="top">
276
- <button type="button" class="toolbar-btn color-btn" @mousedown.prevent @click="openTextColorPicker">
277
- <svg class="toolbar-svg color-svg" viewBox="0 0 28 28" aria-hidden="true">
278
- <path d="M8 19l6-14 6 14M10.5 14.5h7" class="svg-stroke round" />
279
- <path d="M6 23h16" class="svg-stroke color-preview-stroke round" :style="{ color: currentTextColor }" />
280
- </svg>
281
- </button>
282
- </el-tooltip>
283
- <input
284
- ref="textColorInputRef"
285
- class="hidden-color-input"
286
- type="color"
287
- :value="currentTextColor"
288
- @input="onTextColorChange"
289
- />
290
-
291
- <el-tooltip content="背景色" placement="top">
292
- <button type="button" class="toolbar-btn color-btn" @mousedown.prevent @click="openHighlightColorPicker">
293
- <svg class="toolbar-svg color-svg" viewBox="0 0 28 28" aria-hidden="true">
294
- <path d="M8 16.5l7.5-7.5 3.5 3.5-7.5 7.5H8v-3.5z" class="svg-stroke round" />
295
- <path d="M17 7l4 4" class="svg-stroke thin round" />
296
- <rect x="6" y="22" width="16" height="3" rx="1.5" class="svg-color-fill" :style="{ color: currentHighlightColor }" />
297
- </svg>
298
- </button>
299
- </el-tooltip>
300
-
301
- <el-tooltip content="清除背景色" placement="top">
302
- <button
303
- type="button"
304
- class="toolbar-btn clear-highlight-btn"
305
- @mousedown.prevent
306
- @click="clearHighlightColor"
307
- >
308
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
309
- <path d="M8 16.5l7.5-7.5 3.5 3.5-7.5 7.5H8v-3.5z" class="svg-stroke muted round" />
310
- <path d="M17 7l4 4" class="svg-stroke thin muted round" />
311
- <path d="M7 23h14" class="svg-stroke danger round" />
312
- <path d="M21 6L6 21" class="svg-stroke danger round" />
313
- </svg>
314
- </button>
315
- </el-tooltip>
316
-
317
- <el-tooltip content="格式刷" placement="top">
318
- <button
319
- type="button"
320
- class="toolbar-btn"
321
- :class="{ 'is-active': Boolean(copiedFormat) }"
322
- @mousedown.prevent
323
- @click="toggleFormatPainter"
324
- >
325
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
326
- <path d="M8 5h12v6H8zM10 11v4h8v-4M13 15h2v7" class="svg-stroke round" />
327
- </svg>
328
- </button>
329
- </el-tooltip>
330
-
331
- <el-tooltip content="清除格式" placement="top">
332
- <button
333
- type="button"
334
- class="toolbar-btn"
335
- @mousedown.prevent
336
- @click="clearEditorFormat"
337
- >
338
- <svg class="toolbar-svg" viewBox="0 0 28 28" aria-hidden="true">
339
- <path d="M7 20l5-12h4l5 12M9 16h10" class="svg-stroke round" />
340
- <path d="M21 6L6 21" class="svg-stroke danger round" />
341
- </svg>
342
- </button>
343
- </el-tooltip>
344
- <input
345
- ref="highlightColorInputRef"
346
- class="hidden-color-input"
347
- type="color"
348
- :value="currentHighlightColor"
349
- @input="onHighlightColorChange"
350
- />
351
-
352
- <div class="toolbar-divider"></div>
353
-
354
- <el-tooltip content="撤销" placement="top">
355
- <span class="toolbar-tip-wrap">
356
- <button
357
- type="button"
358
- class="toolbar-btn"
359
- :disabled="!canUndo"
360
- @mousedown.prevent
361
- @click="undoEditor"
362
- >
363
- <RefreshLeft />
364
- </button>
365
- </span>
366
- </el-tooltip>
367
-
368
- <el-tooltip content="恢复" placement="top">
369
- <span class="toolbar-tip-wrap">
370
- <button
371
- type="button"
372
- class="toolbar-btn"
373
- :disabled="!canRedo"
374
- @mousedown.prevent
375
- @click="redoEditor"
376
- >
377
- <RefreshRight />
378
- </button>
379
- </span>
380
- </el-tooltip>
381
- </div>
382
-
383
- <div
384
- class="editor-content-shell"
385
- @click="hideTableContextMenu"
386
- @mousedown.capture="onEditorMouseDownCapture"
387
- @contextmenu="onEditorContextMenu"
388
- >
389
- <EditorContent v-if="editor" :editor="editor" class="editor-content" />
390
- </div>
391
-
392
- <div
393
- v-if="tableContextMenuVisible"
394
- class="table-context-menu"
395
- :style="{ left: `${tableContextMenuPosition.x}px`, top: `${tableContextMenuPosition.y}px` }"
396
- @mousedown.prevent
397
- >
398
- <button type="button" @click="runTableMenuCommand('mergeCells')">合并单元格</button>
399
- <button type="button" @click="runTableMenuCommand('splitCell')">拆分单元格</button>
400
- <div class="table-context-divider"></div>
401
- <button type="button" @click="runTableMenuCommand('addRowBefore')">上方插入行</button>
402
- <button type="button" @click="runTableMenuCommand('addRowAfter')">下方插入行</button>
403
- <button type="button" @click="runTableMenuCommand('deleteRow')">删除当前行</button>
404
- <div class="table-context-divider"></div>
405
- <button type="button" @click="runTableMenuCommand('addColumnBefore')">左侧插入列</button>
406
- <button type="button" @click="runTableMenuCommand('addColumnAfter')">右侧插入列</button>
407
- <button type="button" @click="runTableMenuCommand('deleteColumn')">删除当前列</button>
408
- <div class="table-context-divider"></div>
409
- <button type="button" @click="runTableMenuCommand('toggleHeaderRow')">切换表头行</button>
410
- <button type="button" @click="runTableMenuCommand('toggleHeaderColumn')">切换表头列</button>
411
- <button type="button" class="is-danger" @click="runTableMenuCommand('deleteTable')">删除表格</button>
412
- </div>
413
- </div>
414
-
415
- <template #footer>
416
- <div class="dialog-footer">
417
- <el-button @click="editDialogVisible = false">取消</el-button>
418
- <el-button type="primary" @click="saveActiveField">保存当前内容</el-button>
419
- </div>
420
- </template>
421
- </el-dialog>
422
-
423
- </div>
424
- </template>
425
-
426
- <script>
427
- export default {
428
- name: "DocxEditor"
429
- }
430
- </script>
431
-
432
- <script setup>
433
- import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue'
434
- import { ElMessage } from 'element-plus'
435
- import {
436
- RefreshLeft,
437
- RefreshRight
438
- } from '@element-plus/icons-vue'
439
- import Docxtemplater from 'docxtemplater'
440
- import ImageModule from 'docxtemplater-image-module-free'
441
- import PizZip from 'pizzip'
442
- import * as docxPreview from 'docx-preview'
443
- import * as echarts from 'echarts'
444
- import { Extension } from '@tiptap/core'
445
- import Color from '@tiptap/extension-color'
446
- import Highlight from '@tiptap/extension-highlight'
447
- import Placeholder from '@tiptap/extension-placeholder'
448
- import { CellSelection } from '@tiptap/pm/tables'
449
- import { Table } from '@tiptap/extension-table'
450
- import TableCell from '@tiptap/extension-table-cell'
451
- import TableHeader from '@tiptap/extension-table-header'
452
- import TableRow from '@tiptap/extension-table-row'
453
- import TextAlign from '@tiptap/extension-text-align'
454
- import { TextStyle } from '@tiptap/extension-text-style'
455
- import StarterKit from '@tiptap/starter-kit'
456
- import { EditorContent, useEditor } from '@tiptap/vue-3'
457
-
458
- const DEFAULT_TEMPLATE_FILE_NAME = '运营报告.docx'
459
- const DOCX_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
460
- const DEFAULT_TEXT_COLOR = '#2b3345'
461
- const DEFAULT_HIGHLIGHT_COLOR = '#e8f3ff'
462
- const EDITOR_MARKER_START = '[EDITOR_START]'
463
- const EDITOR_MARKER_END = '[EDITOR_END]'
464
- const LEGACY_EDITOR_MARKER_START = '【'
465
- const LEGACY_EDITOR_MARKER_END = '】'
466
- const WATER_LEVEL_CHART_TAGS = ['cpuWaterLevelChart', 'memoryWaterLevelChart', 'diskWaterLevelChart']
467
- const WATER_LEVEL_CHART_CANVAS_WIDTH = 720
468
- const WATER_LEVEL_CHART_CANVAS_HEIGHT = 360
469
- const WATER_LEVEL_CHART_DOCX_WIDTH = 554
470
- const WATER_LEVEL_CHART_DOCX_HEIGHT = 277
471
- const WATER_LEVEL_CHART_GRID = {
472
- top: 88,
473
- left: 48,
474
- right: 36,
475
- bottom: 42
476
- }
477
- const DOCX_TABLE_WIDTH_DXA = 8306
478
- const DOCX_TABLE_WIDTH_PCT = 5000
479
- const DOCX_TABLE_FONT_NAME = '方正仿宋_GB2312'
480
- const DOCX_TABLE_HEADER_FONT_SIZE_HALF_POINTS = '28'
481
- const DOCX_TABLE_BODY_FONT_SIZE_HALF_POINTS = '22'
482
- const EDITOR_TABLE_FONT_FAMILY = "'方正仿宋_GB2312', '仿宋_GB2312', FangSong, STFangsong, SimSun, '宋体', serif"
483
- const EDITOR_TABLE_HEADER_FONT_SIZE = '14pt'
484
- const EDITOR_TABLE_BODY_FONT_SIZE = '11pt'
485
- const TABLE_PICKER_ROWS = 8
486
- const TABLE_PICKER_COLS = 10
487
- const RICH_TEXT_TOKEN_PREFIX = '__DOCX_RICH_TEXT_FIELD__'
488
- const EDITABLE_EMPTY_PLACEHOLDER = '请输入内容...'
489
- const EDITOR_EMPTY_PLACEHOLDER = '请输入内容,支持 Ctrl/Command + A/C/Z/Y 等常用快捷键...'
490
- const EDITOR_INDENT_STEP_EM = 2
491
- const EDITOR_MAX_INDENT_LEVEL = 6
492
-
493
- const props = defineProps({
494
- renderData: {
495
- type: Object,
496
- default: () => ({})
497
- },
498
- renderType: {
499
- type: String,
500
- default: 'view'
501
- },
502
- templateUrl: {
503
- type: String,
504
- default: ''
505
- },
506
- templateFileName: {
507
- type: String,
508
- default: '运营报告.docx'
509
- },
510
- exportFileName: {
511
- type: String,
512
- default: ''
513
- }
514
- })
515
-
516
- const emit = defineEmits([
517
- 'content-change',
518
- 'render-data-change',
519
- 'save-payload-change',
520
- 'update:editorHtml',
521
- 'update:editorJson'
522
- ])
523
-
524
- const previewRef = ref(null)
525
- const docxScrollRef = ref(null)
526
- const isLoading = ref(false)
527
- const errorMessage = ref('')
528
- const editDialogVisible = ref(false)
529
- const activeEditableField = ref(null)
530
- const textColorInputRef = ref(null)
531
- const highlightColorInputRef = ref(null)
532
- const currentTextColor = ref(DEFAULT_TEXT_COLOR)
533
- const currentHighlightColor = ref(DEFAULT_HIGHLIGHT_COLOR)
534
- const pendingEditorSelection = ref(null)
535
- const copiedFormat = ref(null)
536
- const tablePickerVisible = ref(false)
537
- const tablePickerHover = ref({
538
- rows: 3,
539
- cols: 3
540
- })
541
- const tableContextMenuVisible = ref(false)
542
- const tableContextMenuPosition = ref({
543
- x: 0,
544
- y: 0
545
- })
546
- const documentOutline = ref([])
547
- const activeOutlineId = ref('')
548
- const editableFieldKeys = ref([])
549
- const editableMarkerDescriptors = ref([])
550
- const editableFieldPathMap = ref({})
551
- const editableFieldContentMap = reactive({})
552
- const editableFieldTemplateHtmlMap = reactive({})
553
- const headingStyleLevelMap = ref({})
554
- const documentHeadingList = ref([])
555
- const wordStyleContext = ref({
556
- headingStyleIds: {},
557
- bodyStyleId: '',
558
- editableHeadingCounters: {}
559
- })
560
- const headingActions = [
561
- { label: 'H1', level: 1 },
562
- { label: 'H2', level: 2 },
563
- { label: 'H3', level: 3 }
564
- ]
565
- const tablePickerCells = Array.from({ length: TABLE_PICKER_ROWS * TABLE_PICKER_COLS }, (_, index) => ({
566
- key: index,
567
- row: Math.floor(index / TABLE_PICKER_COLS) + 1,
568
- col: (index % TABLE_PICKER_COLS) + 1
569
- }))
570
-
571
- let renderToken = 0
572
- let editableDomCleanupList = []
573
- let outlineScrollRaf = 0
574
- const editableRenderData = ref(createEditableRenderData(props.renderData))
575
- const DOM_TEXT_NODE = 3
576
- const DOM_ELEMENT_NODE = 1
577
-
578
- function getEditorIndentLevel(editorInstance) {
579
- const attrs = editorInstance.isActive('heading')
580
- ? editorInstance.getAttributes('heading')
581
- : editorInstance.getAttributes('paragraph')
582
-
583
- return Number(attrs.textIndentLevel || 0)
584
- }
585
-
586
- function updateEditorIndent(editorInstance, delta) {
587
- if (!editorInstance || editorInstance.isActive('table')) {
588
- return false
589
- }
590
-
591
- const nodeType = editorInstance.isActive('heading') ? 'heading' : 'paragraph'
592
- const nextLevel = Math.min(
593
- EDITOR_MAX_INDENT_LEVEL,
594
- Math.max(0, getEditorIndentLevel(editorInstance) + delta)
595
- )
596
-
597
- return editorInstance
598
- .chain()
599
- .focus()
600
- .updateAttributes(nodeType, {
601
- textIndentLevel: nextLevel
602
- })
603
- .run()
604
- }
605
-
606
- const EditorIndent = Extension.create({
607
- name: 'editorIndent',
608
-
609
- addGlobalAttributes() {
610
- return [
611
- {
612
- types: ['paragraph', 'heading'],
613
- attributes: {
614
- textIndentLevel: {
615
- default: 0,
616
- parseHTML: (element) => {
617
- const indent = element.style?.textIndent || ''
618
- const match = indent.match(/^([\d.]+)em$/)
619
-
620
- if (!match) {
621
- return 0
622
- }
623
-
624
- return Math.min(
625
- EDITOR_MAX_INDENT_LEVEL,
626
- Math.max(0, Math.round(Number(match[1]) / EDITOR_INDENT_STEP_EM))
627
- )
628
- },
629
- renderHTML: (attributes) => {
630
- const level = Number(attributes.textIndentLevel || 0)
631
-
632
- if (!level) {
633
- return {}
634
- }
635
-
636
- return {
637
- style: `text-indent: ${level * EDITOR_INDENT_STEP_EM}em;`
638
- }
639
- }
640
- }
641
- }
642
- }
643
- ]
644
- },
645
-
646
- addKeyboardShortcuts() {
647
- return {
648
- Tab: () => updateEditorIndent(this.editor, 1),
649
- 'Shift-Tab': () => updateEditorIndent(this.editor, -1),
650
- 'Mod-y': () => this.editor.commands.redo(),
651
- 'Mod-Shift-z': () => this.editor.commands.redo(),
652
- 'Mod-z': () => this.editor.commands.undo()
653
- }
654
- }
655
- })
656
-
657
- const editor = useEditor({
658
- content: '<p></p>',
659
- extensions: [
660
- StarterKit.configure({
661
- bulletList: false,
662
- orderedList: false,
663
- blockquote: false,
664
- codeBlock: false,
665
- horizontalRule: false
666
- }),
667
- Table.configure({
668
- resizable: true
669
- }),
670
- TableRow,
671
- TableHeader,
672
- TableCell,
673
- TextStyle,
674
- Color.configure({
675
- types: ['textStyle']
676
- }),
677
- Highlight.configure({
678
- multicolor: true
679
- }),
680
- TextAlign.configure({
681
- types: ['heading', 'paragraph']
682
- }),
683
- EditorIndent,
684
- Placeholder.configure({
685
- placeholder: EDITOR_EMPTY_PLACEHOLDER,
686
- showOnlyWhenEditable: true,
687
- includeChildren: true
688
- })
689
- ],
690
- onSelectionUpdate({ editor: instance }) {
691
- currentTextColor.value = instance.getAttributes('textStyle').color || DEFAULT_TEXT_COLOR
692
- currentHighlightColor.value = instance.getAttributes('highlight').color || DEFAULT_HIGHLIGHT_COLOR
693
- },
694
- onUpdate({ editor: instance }) {
695
- emit('content-change', {
696
- html: instance.getHTML(),
697
- json: instance.getJSON()
698
- })
699
- emit('update:editorHtml', instance.getHTML())
700
- emit('update:editorJson', instance.getJSON())
701
- }
702
- })
703
-
704
- const isBoldActive = computed(() => editor.value?.isActive('bold'))
705
- const isTableActive = computed(() => editor.value?.isActive('table'))
706
- const isParagraphActive = computed(() => editor.value?.isActive('paragraph'))
707
- const canUndo = computed(() => editor.value?.can().chain().focus().undo().run())
708
- const canRedo = computed(() => editor.value?.can().chain().focus().redo().run())
709
-
710
- function deepClone(value) {
711
- return JSON.parse(JSON.stringify(value || {}))
712
- }
713
-
714
- function createEditableRenderData(source) {
715
- const cloned = deepClone(source)
716
-
717
- if (!cloned.__editableRichTextMap) {
718
- cloned.__editableRichTextMap = {}
719
- }
720
-
721
- return cloned
722
- }
723
-
724
- function getChartTagDataAliases() {
725
- return WATER_LEVEL_CHART_TAGS.reduce((result, key) => {
726
- result[key] = getValueByPath(editableRenderData.value, `overallData.${key}`) || editableRenderData.value[key] || {}
727
- return result
728
- }, {})
729
- }
730
-
731
- function getChartTagNames() {
732
- return WATER_LEVEL_CHART_TAGS.reduce((result, key) => {
733
- result[`{%${key}}`] = true
734
- result[key] = true
735
- return result
736
- }, {})
737
- }
738
-
739
- function getExportFileName() {
740
- if (props.exportFileName) {
741
- return props.exportFileName
742
- }
743
-
744
- const startTime = editableRenderData.value?.startTime || ''
745
- const endTime = editableRenderData.value?.endTime || ''
746
-
747
- if (startTime && endTime) {
748
- return `云平台运维周报_${startTime}_${endTime}.docx`
749
- }
750
-
751
- return '云平台运维周报.docx'
752
- }
753
-
754
- function clearPreview() {
755
- clearEditableDecorations()
756
- documentOutline.value = []
757
- activeOutlineId.value = ''
758
- Object.keys(editableFieldTemplateHtmlMap).forEach((key) => {
759
- delete editableFieldTemplateHtmlMap[key]
760
- })
761
- if (previewRef.value) {
762
- previewRef.value.innerHTML = ''
763
- }
764
- }
765
-
766
- function clearEditableDecorations() {
767
- editableDomCleanupList.forEach((cleanup) => cleanup())
768
- editableDomCleanupList = []
769
- }
770
-
771
- function normalizeValue(value) {
772
- if (Array.isArray(value)) {
773
- return value.map((item) => {
774
- if (item && typeof item === 'object') {
775
- return normalizeObject(item)
776
- }
777
- return item ?? ''
778
- })
779
- }
780
-
781
- if (value && typeof value === 'object') {
782
- return normalizeObject(value)
783
- }
784
-
785
- return value ?? ''
786
- }
787
-
788
- function normalizeObject(source = {}) {
789
- return Object.keys(source).reduce((result, key) => {
790
- result[key] = normalizeValue(source[key])
791
- return result
792
- }, {})
793
- }
794
-
795
- function buildTemplateData(data = {}) {
796
- const normalizedData = normalizeObject(data)
797
- const overallData = normalizedData.overallData || {}
798
- const operationsData = normalizedData.operationsData || {}
799
- const maintenanceData = normalizedData.maintenanceData || {}
800
- const workThisWeek = normalizedData.workThisWeek || {}
801
- const workNextWeek = normalizedData.workNextWeek || {}
802
-
803
- return {
804
- ...normalizedData,
805
- ...overallData,
806
- ...operationsData,
807
- ...maintenanceData,
808
- ...workThisWeek,
809
- ...workNextWeek,
810
- intranet_resource_list: overallData.intranet_resource_list || []
811
- }
812
- }
813
-
814
- function getRichTextToken(fieldKey) {
815
- return `${RICH_TEXT_TOKEN_PREFIX}${fieldKey}__`
816
- }
817
-
818
- function hasRichTextContent(richItem) {
819
- return Boolean(richItem?.html || richItem?.json || richItem?.blocks?.length)
820
- }
821
-
822
- function getStoredRichTextItem(fieldKey) {
823
- return editableRenderData.value.__editableRichTextMap?.[fieldKey] || null
824
- }
825
-
826
- function buildTemplateDataForRender(options = {}) {
827
- const { useRichTextTokens = true, useRichTextPlainText = true } = options
828
- const source = deepClone(editableRenderData.value)
829
- const richTextMap = source.__editableRichTextMap || {}
830
-
831
- editableFieldKeys.value.forEach((fieldKey) => {
832
- if (getValueByPath(source, fieldKey) === '') {
833
- setValueByPath(source, fieldKey, '')
834
- }
835
- })
836
-
837
- Object.keys(richTextMap).forEach((key) => {
838
- const richItem = richTextMap[key]
839
- if (richItem?.path) {
840
- const shouldUseToken = useRichTextTokens && hasRichTextContent(richItem)
841
- const fallbackText = useRichTextPlainText ? (richItem.plainText || '') : ''
842
- setValueByPath(source, richItem.path, shouldUseToken
843
- ? getRichTextToken(key)
844
- : fallbackText)
845
- }
846
- })
847
-
848
- return {
849
- ...buildTemplateData(source),
850
- ...getChartTagDataAliases()
851
- }
852
- }
853
-
854
- function getTemplateTextParts(xmlText) {
855
- return [...xmlText.matchAll(/<w:t[^>]*>([\s\S]*?)<\/w:t>/g)].map((match) => decodeXmlText(match[1]))
856
- }
857
-
858
- function escapeRegExp(text) {
859
- return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
860
- }
861
-
862
- function getEditableMarkerRegExp() {
863
- return new RegExp(
864
- `(?:${escapeRegExp(LEGACY_EDITOR_MARKER_START)}\\s*)?${escapeRegExp(EDITOR_MARKER_START)}\\s*(\\{([^#/{][^}]*)\\})[\\s\\S]*?${escapeRegExp(EDITOR_MARKER_END)}(?:\\s*${escapeRegExp(LEGACY_EDITOR_MARKER_END)})?`,
865
- 'g'
866
- )
867
- }
868
-
869
- function getRenderedEditableMarkerRegExp() {
870
- return new RegExp(
871
- `(?:${escapeRegExp(LEGACY_EDITOR_MARKER_START)}\\s*)?${escapeRegExp(EDITOR_MARKER_START)}\\s*([\\s\\S]*?)\\s*${escapeRegExp(EDITOR_MARKER_END)}(?:\\s*${escapeRegExp(LEGACY_EDITOR_MARKER_END)})?`,
872
- 'g'
873
- )
874
- }
875
-
876
- function decodeXmlText(text) {
877
- return text
878
- .replace(/&lt;/g, '<')
879
- .replace(/&gt;/g, '>')
880
- .replace(/&amp;/g, '&')
881
- .replace(/&quot;/g, '"')
882
- .replace(/&#39;/g, "'")
883
- }
884
-
885
- function extractEditableMarkerDescriptors(xmlText) {
886
- const text = getTemplateTextParts(xmlText).join('')
887
- const validMarkerRegExp = getEditableMarkerRegExp()
888
-
889
- return [...text.matchAll(getRenderedEditableMarkerRegExp())]
890
- .map((match) => {
891
- const markerText = match[0] || ''
892
- const validMatch = validMarkerRegExp.exec(markerText)
893
- validMarkerRegExp.lastIndex = 0
894
-
895
- return {
896
- markerText,
897
- fieldKey: validMatch?.[2] || '',
898
- isValid: Boolean(validMatch?.[2])
899
- }
900
- })
901
- }
902
-
903
- function buildScalarPathMap(source, currentPath = '', result = {}) {
904
- if (!source || typeof source !== 'object') {
905
- return result
906
- }
907
-
908
- Object.keys(source).forEach((key) => {
909
- if (key.startsWith('__')) {
910
- return
911
- }
912
-
913
- const value = source[key]
914
- const nextPath = currentPath ? `${currentPath}.${key}` : key
915
-
916
- if (Array.isArray(value)) {
917
- return
918
- }
919
-
920
- if (value && typeof value === 'object') {
921
- buildScalarPathMap(value, nextPath, result)
922
- return
923
- }
924
-
925
- if (!result[key]) {
926
- result[key] = nextPath
927
- }
928
- })
929
-
930
- return result
931
- }
932
-
933
- function setValueByPath(target, path, value) {
934
- if (!path) {
935
- return
936
- }
937
-
938
- const pathList = path.split('.')
939
- let current = target
940
-
941
- for (let index = 0; index < pathList.length - 1; index += 1) {
942
- const pathKey = pathList[index]
943
- if (!current[pathKey] || typeof current[pathKey] !== 'object') {
944
- current[pathKey] = {}
945
- }
946
- current = current[pathKey]
947
- }
948
-
949
- current[pathList[pathList.length - 1]] = value
950
- }
951
-
952
- function getValueByPath(target, path) {
953
- if (!path) {
954
- return ''
955
- }
956
-
957
- return path.split('.').reduce((result, key) => {
958
- if (result == null) {
959
- return ''
960
- }
961
- return result[key]
962
- }, target)
963
- }
964
-
965
- function htmlToDocxText(html) {
966
- const container = document.createElement('div')
967
- container.innerHTML = html
968
-
969
- function walk(node) {
970
- if (!node) {
971
- return ''
972
- }
973
-
974
- if (node.nodeType === DOM_TEXT_NODE) {
975
- return node.textContent || ''
976
- }
977
-
978
- if (node.nodeType !== DOM_ELEMENT_NODE) {
979
- return ''
980
- }
981
-
982
- const tagName = node.tagName?.toLowerCase?.() || ''
983
-
984
- if (tagName === 'br') {
985
- return '\n'
986
- }
987
-
988
- const childText = Array.from(node.childNodes).map((childNode) => walk(childNode)).join('')
989
-
990
- if (['p', 'div', 'h1', 'h2', 'h3', 'li'].includes(tagName)) {
991
- return `${childText}\n`
992
- }
993
-
994
- if (tagName === 'tr') {
995
- return `${childText}\n`
996
- }
997
-
998
- if (tagName === 'td' || tagName === 'th') {
999
- return `${childText}\t`
1000
- }
1001
-
1002
- return childText
1003
- }
1004
-
1005
- return Array.from(container.childNodes)
1006
- .map((childNode) => walk(childNode))
1007
- .join('')
1008
- .replace(/\t+$/gm, '')
1009
- .replace(/\n{3,}/g, '\n\n')
1010
- .trim()
1011
- }
1012
-
1013
- function hasVisibleHtmlContent(html = '') {
1014
- if (!html) {
1015
- return false
1016
- }
1017
-
1018
- const container = document.createElement('div')
1019
- container.innerHTML = html
1020
-
1021
- if (container.querySelector('table, img, svg, canvas')) {
1022
- return true
1023
- }
1024
-
1025
- return Boolean((container.textContent || '').replace(/\u00a0/g, ' ').trim())
1026
- }
1027
-
1028
- function hasEditableDisplayContent(key, fallbackText = '') {
1029
- const richTextItem = getStoredRichTextItem(key)
1030
-
1031
- if (richTextItem?.html !== undefined) {
1032
- return hasVisibleHtmlContent(richTextItem.html)
1033
- }
1034
-
1035
- if (richTextItem?.plainText !== undefined) {
1036
- return Boolean(String(richTextItem.plainText || '').trim())
1037
- }
1038
-
1039
- if (editableFieldTemplateHtmlMap[key]) {
1040
- return hasVisibleHtmlContent(editableFieldTemplateHtmlMap[key])
1041
- }
1042
-
1043
- return Boolean(String(fallbackText || '').trim())
1044
- }
1045
-
1046
- function getFieldStoredHtml(key, fallbackText = '') {
1047
- const richTextItem = getStoredRichTextItem(key)
1048
-
1049
- if (richTextItem?.html) {
1050
- return normalizeRichTextHtmlForStorage(richTextItem.html)
1051
- }
1052
-
1053
- if (editableFieldTemplateHtmlMap[key]) {
1054
- return normalizeRichTextHtmlForStorage(editableFieldTemplateHtmlMap[key])
1055
- }
1056
-
1057
- return `<p>${escapeHtml(String(fallbackText || ''))}</p>`
1058
- }
1059
-
1060
- function getFieldRenderableHtml(key, fallbackText = '') {
1061
- const richTextItem = getStoredRichTextItem(key)
1062
-
1063
- if (richTextItem?.html) {
1064
- return normalizeRichTextHtmlForStorage(richTextItem.html)
1065
- }
1066
-
1067
- if (editableFieldTemplateHtmlMap[key]) {
1068
- return normalizeRichTextHtmlForStorage(editableFieldTemplateHtmlMap[key])
1069
- }
1070
-
1071
- return `<p>${escapeHtml(String(fallbackText || ''))}</p>`
1072
- }
1073
-
1074
- function normalizeRichTextHtmlForStorage(html = '') {
1075
- if (!html) {
1076
- return ''
1077
- }
1078
-
1079
- const container = document.createElement('div')
1080
- container.innerHTML = html
1081
-
1082
- container.querySelectorAll('.tableWrapper').forEach((wrapper) => {
1083
- wrapper.style.width = '100%'
1084
- wrapper.style.maxWidth = '100%'
1085
- })
1086
-
1087
- container.querySelectorAll('table').forEach((table) => {
1088
- table.removeAttribute('width')
1089
- table.style.width = '100%'
1090
- table.style.minWidth = '100%'
1091
- table.style.maxWidth = '100%'
1092
- table.style.tableLayout = 'fixed'
1093
- })
1094
-
1095
- container.querySelectorAll('col').forEach((col) => {
1096
- col.removeAttribute('width')
1097
- col.style.width = ''
1098
- })
1099
-
1100
- container.querySelectorAll('td, th').forEach((cell) => {
1101
- cell.style.textAlign = cell.style.textAlign || 'center'
1102
- cell.style.verticalAlign = 'middle'
1103
- })
1104
-
1105
- return container.innerHTML
1106
- }
1107
-
1108
- function buildRenderableSegments(html, fallbackText = '') {
1109
- if (!html) {
1110
- return [{ type: 'text', text: String(fallbackText || '') }]
1111
- }
1112
-
1113
- const container = document.createElement('div')
1114
- container.innerHTML = html
1115
- const segments = []
1116
-
1117
- function walk(node) {
1118
- if (!node) {
1119
- return
1120
- }
1121
-
1122
- if (node.nodeType === DOM_TEXT_NODE) {
1123
- const text = node.textContent || ''
1124
- if (text) {
1125
- segments.push({ type: 'text', text })
1126
- }
1127
- return
1128
- }
1129
-
1130
- if (node.nodeType !== DOM_ELEMENT_NODE) {
1131
- return
1132
- }
1133
-
1134
- const tagName = node.tagName?.toLowerCase?.() || ''
1135
-
1136
- if (tagName === 'br') {
1137
- segments.push({ type: 'break' })
1138
- return
1139
- }
1140
-
1141
- Array.from(node.childNodes).forEach((childNode) => walk(childNode))
1142
-
1143
- if (['p', 'div', 'h1', 'h2', 'h3', 'li'].includes(tagName)) {
1144
- segments.push({ type: 'break' })
1145
- }
1146
- }
1147
-
1148
- Array.from(container.childNodes).forEach((childNode) => walk(childNode))
1149
-
1150
- while (segments.length && segments[segments.length - 1].type === 'break') {
1151
- segments.pop()
1152
- }
1153
-
1154
- return segments.length ? segments : [{ type: 'text', text: String(fallbackText || '') }]
1155
- }
1156
-
1157
- function getClosestParagraphElement(node) {
1158
- if (!node) {
1159
- return null
1160
- }
1161
-
1162
- const parentElement = node.nodeType === DOM_ELEMENT_NODE ? node : node.parentElement
1163
- return parentElement?.closest?.('p') || null
1164
- }
1165
-
1166
- function normalizeEditableText(text = '') {
1167
- return text.replace(/(\s|\u00a0|\u200b|\u200c|\u200d|\ufeff)/g, '')
1168
- }
1169
-
1170
- function getRangeText(startNode, startOffset, endNode, endOffset) {
1171
- const range = document.createRange()
1172
- range.setStart(startNode, startOffset)
1173
- range.setEnd(endNode, endOffset)
1174
- return range.toString()
1175
- }
1176
-
1177
- function getRangeFragment(startNode, startOffset, endNode, endOffset) {
1178
- const range = document.createRange()
1179
- range.setStart(startNode, startOffset)
1180
- range.setEnd(endNode, endOffset)
1181
- return range.cloneContents()
1182
- }
1183
-
1184
- function isBlockBoundaryCharacter(char = '') {
1185
- return !char || ['\n', '\r', '\f'].includes(char)
1186
- }
1187
-
1188
- function isBlockByTextBoundary(combinedText = '', rangeStart = 0, rangeEnd = 0) {
1189
- return isBlockBoundaryCharacter(combinedText[rangeStart - 1]) && isBlockBoundaryCharacter(combinedText[rangeEnd])
1190
- }
1191
-
1192
- function isDomBlockBoundary(node) {
1193
- if (!node || node.nodeType !== DOM_ELEMENT_NODE) {
1194
- return false
1195
- }
1196
-
1197
- const tagName = node.tagName?.toLowerCase?.() || ''
1198
-
1199
- if (tagName === 'br') {
1200
- return true
1201
- }
1202
-
1203
- const styleText = node.getAttribute('style') || ''
1204
- return /(?:page-)?break-(?:before|after)\s*:\s*(?:page|always)/i.test(styleText)
1205
- }
1206
-
1207
- function getFragmentBoundaryText(fragment) {
1208
- const parts = []
1209
-
1210
- function walk(node) {
1211
- if (!node) {
1212
- return
1213
- }
1214
-
1215
- if (node.nodeType === DOM_TEXT_NODE) {
1216
- parts.push(node.textContent || '')
1217
- return
1218
- }
1219
-
1220
- if (node.nodeType !== DOM_ELEMENT_NODE) {
1221
- return
1222
- }
1223
-
1224
- if (isDomBlockBoundary(node)) {
1225
- parts.push('\n')
1226
- return
1227
- }
1228
-
1229
- Array.from(node.childNodes).forEach((childNode) => walk(childNode))
1230
- }
1231
-
1232
- Array.from(fragment.childNodes).forEach((childNode) => walk(childNode))
1233
- return parts.join('')
1234
- }
1235
-
1236
- function isCleanBoundaryFragment(fragment, direction) {
1237
- const boundaryText = getFragmentBoundaryText(fragment)
1238
-
1239
- if (!normalizeEditableText(boundaryText)) {
1240
- return true
1241
- }
1242
-
1243
- const textParts = boundaryText.split(/[\n\r\f]/)
1244
-
1245
- if (direction === 'before') {
1246
- return !normalizeEditableText(textParts[textParts.length - 1] || '')
1247
- }
1248
-
1249
- return !normalizeEditableText(textParts[0] || '')
1250
- }
1251
-
1252
- function isStandaloneParagraphPlaceholder(startPosition, endPosition, fullText = '', combinedText = '', rangeStart = 0, rangeEnd = 0) {
1253
- const { node: startNode, offset: startOffset } = startPosition
1254
- const { node: endNode, offset: endOffset } = endPosition
1255
- const startParagraph = getClosestParagraphElement(startNode)
1256
- const endParagraph = getClosestParagraphElement(endNode)
1257
-
1258
- if (!startParagraph || !endParagraph) {
1259
- return isBlockByTextBoundary(combinedText, rangeStart, rangeEnd)
1260
- }
1261
-
1262
- if (startParagraph !== endParagraph) {
1263
- return true
1264
- }
1265
-
1266
- const paragraphStartText = getRangeText(startParagraph, 0, startNode, startOffset)
1267
- const paragraphEndOffset = startParagraph.childNodes.length
1268
- const paragraphEndText = getRangeText(endNode, endOffset, startParagraph, paragraphEndOffset)
1269
- const paragraphStartFragment = getRangeFragment(startParagraph, 0, startNode, startOffset)
1270
- const paragraphEndFragment = getRangeFragment(endNode, endOffset, startParagraph, paragraphEndOffset)
1271
- const markerText = normalizeEditableText(fullText)
1272
- const paragraphText = normalizeEditableText(startParagraph.textContent || '')
1273
- const hasOnlyWhitespaceAroundMarker =
1274
- !normalizeEditableText(paragraphStartText) && !normalizeEditableText(paragraphEndText)
1275
- const hasVisualBlockBoundary =
1276
- isCleanBoundaryFragment(paragraphStartFragment, 'before') &&
1277
- isCleanBoundaryFragment(paragraphEndFragment, 'after')
1278
-
1279
- return hasOnlyWhitespaceAroundMarker || hasVisualBlockBoundary || paragraphText === markerText
1280
- }
1281
-
1282
- function applySourceParagraphLayout(target, sourceParagraph) {
1283
- if (!sourceParagraph) {
1284
- return
1285
- }
1286
-
1287
- const computedStyle = window.getComputedStyle(sourceParagraph)
1288
- const inheritedProperties = [
1289
- 'textAlign',
1290
- 'marginTop',
1291
- 'marginBottom',
1292
- 'paddingTop',
1293
- 'paddingBottom',
1294
- 'textIndent',
1295
- 'lineHeight'
1296
- ]
1297
-
1298
- inheritedProperties.forEach((property) => {
1299
- target.style[property] = computedStyle[property]
1300
- })
1301
- }
1302
-
1303
- function escapeHtml(text) {
1304
- return text
1305
- .replace(/&/g, '&amp;')
1306
- .replace(/</g, '&lt;')
1307
- .replace(/>/g, '&gt;')
1308
- .replace(/"/g, '&quot;')
1309
- .replace(/'/g, '&#39;')
1310
- }
1311
-
1312
- function escapeXml(text = '') {
1313
- return String(text)
1314
- .replace(/&/g, '&amp;')
1315
- .replace(/</g, '&lt;')
1316
- .replace(/>/g, '&gt;')
1317
- .replace(/"/g, '&quot;')
1318
- }
1319
-
1320
- function normalizeDocxColor(color = '') {
1321
- const trimmedColor = String(color || '').trim()
1322
-
1323
- if (/^#[0-9a-f]{6}$/i.test(trimmedColor)) {
1324
- return trimmedColor.slice(1).toUpperCase()
1325
- }
1326
-
1327
- const rgbMatch = trimmedColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i)
1328
-
1329
- if (rgbMatch) {
1330
- return [rgbMatch[1], rgbMatch[2], rgbMatch[3]]
1331
- .map((value) => Number(value).toString(16).padStart(2, '0'))
1332
- .join('')
1333
- .toUpperCase()
1334
- }
1335
-
1336
- return ''
1337
- }
1338
-
1339
- function stripConflictingRunProperties(runProperties = '') {
1340
- return runProperties
1341
- .replace(/<w:b\b[^>]*\/>/g, '')
1342
- .replace(/<w:b\b[\s\S]*?<\/w:b>/g, '')
1343
- .replace(/<w:color\b[^>]*\/>/g, '')
1344
- .replace(/<w:color\b[\s\S]*?<\/w:color>/g, '')
1345
- .replace(/<w:highlight\b[^>]*\/>/g, '')
1346
- .replace(/<w:highlight\b[\s\S]*?<\/w:highlight>/g, '')
1347
- .replace(/<w:shd\b[^>]*\/>/g, '')
1348
- .replace(/<w:shd\b[\s\S]*?<\/w:shd>/g, '')
1349
- }
1350
-
1351
- function stripRunFontProperties(runProperties = '') {
1352
- return runProperties
1353
- .replace(/<w:rFonts\b[^>]*\/>/g, '')
1354
- .replace(/<w:rFonts\b[\s\S]*?<\/w:rFonts>/g, '')
1355
- }
1356
-
1357
- function stripRunSizeProperties(runProperties = '') {
1358
- return runProperties
1359
- .replace(/<w:sz\b[^>]*\/>/g, '')
1360
- .replace(/<w:sz\b[\s\S]*?<\/w:sz>/g, '')
1361
- .replace(/<w:szCs\b[^>]*\/>/g, '')
1362
- .replace(/<w:szCs\b[\s\S]*?<\/w:szCs>/g, '')
1363
- }
1364
-
1365
- function buildDocxRunFontProperties(fontName = DOCX_TABLE_FONT_NAME) {
1366
- const escapedFontName = escapeXml(fontName)
1367
- return `<w:rFonts w:hint="eastAsia" w:ascii="${escapedFontName}" w:hAnsi="${escapedFontName}" w:eastAsia="${escapedFontName}" w:cs="${escapedFontName}"/>`
1368
- }
1369
-
1370
- function buildDocxRunSizeProperties(fontSizeHalfPoints = DOCX_TABLE_BODY_FONT_SIZE_HALF_POINTS) {
1371
- return `<w:sz w:val="${fontSizeHalfPoints}"/><w:szCs w:val="${fontSizeHalfPoints}"/>`
1372
- }
1373
-
1374
- function withDocxTableRunStyle(baseRunProperties = '', isHeader = false) {
1375
- const cleanRunProperties = stripRunSizeProperties(stripRunFontProperties(baseRunProperties))
1376
- const fontSize = isHeader
1377
- ? DOCX_TABLE_HEADER_FONT_SIZE_HALF_POINTS
1378
- : DOCX_TABLE_BODY_FONT_SIZE_HALF_POINTS
1379
-
1380
- return `${cleanRunProperties}${buildDocxRunFontProperties()}${buildDocxRunSizeProperties(fontSize)}`
1381
- }
1382
-
1383
- function buildDocxRunProperties(baseRunProperties = '', marks = {}) {
1384
- const properties = [stripConflictingRunProperties(baseRunProperties)]
1385
- const textColor = normalizeDocxColor(marks.color)
1386
- const backgroundColor = normalizeDocxColor(marks.backgroundColor)
1387
-
1388
- if (marks.bold) {
1389
- properties.push('<w:b/>')
1390
- }
1391
-
1392
- if (textColor) {
1393
- properties.push(`<w:color w:val="${textColor}"/>`)
1394
- }
1395
-
1396
- if (backgroundColor) {
1397
- properties.push(`<w:shd w:val="clear" w:color="auto" w:fill="${backgroundColor}"/>`)
1398
- }
1399
-
1400
- const content = properties.join('')
1401
- return content ? `<w:rPr>${content}</w:rPr>` : ''
1402
- }
1403
-
1404
- function buildDocxTextRuns(text = '', marks = {}, baseRunProperties = '') {
1405
- const parts = String(text).split('\n')
1406
- const runProperties = buildDocxRunProperties(baseRunProperties, marks)
1407
- const runs = []
1408
-
1409
- parts.forEach((part, index) => {
1410
- if (index > 0) {
1411
- runs.push('<w:r><w:br/></w:r>')
1412
- }
1413
-
1414
- if (part) {
1415
- runs.push(`<w:r>${runProperties}<w:t xml:space="preserve">${escapeXml(part)}</w:t></w:r>`)
1416
- }
1417
- })
1418
-
1419
- return runs.join('')
1420
- }
1421
-
1422
- function buildDocxEmptyTextRun(baseRunProperties = '') {
1423
- return `<w:r>${buildDocxRunProperties(baseRunProperties, {})}<w:t xml:space="preserve"></w:t></w:r>`
1424
- }
1425
-
1426
- function stripParagraphJustification(paragraphProperties = '') {
1427
- return paragraphProperties
1428
- .replace(/<w:jc\b[^>]*\/>/g, '')
1429
- .replace(/<w:jc\b[\s\S]*?<\/w:jc>/g, '')
1430
- }
1431
-
1432
- function stripParagraphIndent(paragraphProperties = '') {
1433
- return paragraphProperties
1434
- .replace(/<w:ind\b[^>]*\/>/g, '')
1435
- .replace(/<w:ind\b[\s\S]*?<\/w:ind>/g, '')
1436
- }
1437
-
1438
- function stripParagraphStyle(paragraphProperties = '') {
1439
- return paragraphProperties
1440
- .replace(/<w:pStyle\b[^>]*\/>/g, '')
1441
- .replace(/<w:pStyle\b[\s\S]*?<\/w:pStyle>/g, '')
1442
- }
1443
-
1444
- function normalizeDocxAlign(align = '') {
1445
- const alignMap = {
1446
- left: 'left',
1447
- center: 'center',
1448
- right: 'right'
1449
- }
1450
-
1451
- return alignMap[align] || 'left'
1452
- }
1453
-
1454
- function buildDocxParagraphProperties(baseParagraphProperties = '', align = 'left', textIndentLevel = 0, styleId = '') {
1455
- const paragraphProperties = stripParagraphStyle(stripParagraphIndent(stripParagraphJustification(baseParagraphProperties)))
1456
- const level = Math.max(0, Number(textIndentLevel || 0))
1457
- const indentXml = level ? `<w:ind w:firstLine="${Math.round(level * 420)}"/>` : ''
1458
- const styleXml = styleId ? `<w:pStyle w:val="${escapeXml(styleId)}"/>` : ''
1459
- return `<w:pPr>${styleXml}${paragraphProperties}<w:jc w:val="${normalizeDocxAlign(align)}"/>${indentXml}</w:pPr>`
1460
- }
1461
-
1462
- function getXmlText(xml = '') {
1463
- return [...xml.matchAll(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g)]
1464
- .map((match) => decodeXmlText(match[1]))
1465
- .join('')
1466
- }
1467
-
1468
- function getParagraphProperties(paragraphXml = '') {
1469
- return paragraphXml.match(/<w:pPr>([\s\S]*?)<\/w:pPr>/)?.[1] || ''
1470
- }
1471
-
1472
- function getFirstRunProperties(paragraphXml = '') {
1473
- return paragraphXml.match(/<w:rPr>([\s\S]*?)<\/w:rPr>/)?.[1] || ''
1474
- }
1475
-
1476
- function buildDocxParagraph(runs = '', align = 'left', baseParagraphProperties = '', textIndentLevel = 0, styleId = '') {
1477
- return `<w:p>${buildDocxParagraphProperties(baseParagraphProperties, align, textIndentLevel, styleId)}${runs || '<w:r><w:t xml:space="preserve"></w:t></w:r>'}</w:p>`
1478
- }
1479
-
1480
- function getTableColumnCount(tableBlock = {}) {
1481
- return Math.max(
1482
- 1,
1483
- ...(tableBlock.rows || []).map((row) => (row.cells || []).reduce((result, cell) => {
1484
- return result + Number(cell.colSpan || 1)
1485
- }, 0))
1486
- )
1487
- }
1488
-
1489
- function normalizeTableRowsForDocx(tableBlock = {}) {
1490
- const columnCount = getTableColumnCount(tableBlock)
1491
- const activeRowSpans = []
1492
-
1493
- return (tableBlock.rows || []).map((row) => {
1494
- const normalizedCells = []
1495
- let columnIndex = 0
1496
-
1497
- function appendVerticalContinuation() {
1498
- const activeSpan = activeRowSpans[columnIndex]
1499
-
1500
- if (!activeSpan?.remaining) {
1501
- return false
1502
- }
1503
-
1504
- normalizedCells.push({
1505
- vMerge: 'continue',
1506
- colSpan: activeSpan.colSpan || 1,
1507
- blocks: []
1508
- })
1509
- activeSpan.remaining -= 1
1510
- columnIndex += activeSpan.colSpan || 1
1511
- return true
1512
- }
1513
-
1514
- const rowCells = row.cells || []
1515
-
1516
- rowCells.forEach((cell) => {
1517
- while (appendVerticalContinuation()) {
1518
- // Fill cells occupied by vertical merges from previous rows before placing the next real cell.
1519
- }
1520
-
1521
- const colSpan = Number(cell.colSpan || 1)
1522
- const rowSpan = Number(cell.rowSpan || 1)
1523
-
1524
- normalizedCells.push({
1525
- ...cell,
1526
- colSpan,
1527
- rowSpan,
1528
- vMerge: rowSpan > 1 ? 'restart' : ''
1529
- })
1530
-
1531
- if (rowSpan > 1) {
1532
- activeRowSpans[columnIndex] = {
1533
- remaining: rowSpan - 1,
1534
- colSpan
1535
- }
1536
- }
1537
-
1538
- columnIndex += colSpan
1539
- })
1540
-
1541
- while (columnIndex < columnCount) {
1542
- if (!appendVerticalContinuation()) {
1543
- normalizedCells.push({
1544
- colSpan: 1,
1545
- blocks: []
1546
- })
1547
- columnIndex += 1
1548
- }
1549
- }
1550
-
1551
- return {
1552
- cells: normalizedCells
1553
- }
1554
- })
1555
- }
1556
-
1557
- function buildDocxTableCell(cell = {}, columnWidthPct = 0, baseParagraphProperties = '', baseRunProperties = '') {
1558
- const tableRunProperties = withDocxTableRunStyle(baseRunProperties, cell.header)
1559
- const paragraphBlocks = (cell.blocks || []).filter((block) => block.type === 'paragraph' || block.type === 'heading')
1560
- const cellXml = paragraphBlocks.length
1561
- ? paragraphBlocks.map((block) => {
1562
- const runs = (block.runs || []).map((run) => {
1563
- return buildDocxTextRuns(run.text || '', cell.header ? { ...(run.marks || {}), bold: true } : run.marks || {}, tableRunProperties)
1564
- }).join('')
1565
-
1566
- return buildDocxParagraph(runs, block.align || 'center', baseParagraphProperties)
1567
- }).join('')
1568
- : buildDocxParagraph(buildDocxEmptyTextRun(tableRunProperties), 'center', baseParagraphProperties)
1569
-
1570
- const gridSpan = Number(cell.colSpan || 1) > 1
1571
- ? `<w:gridSpan w:val="${Number(cell.colSpan || 1)}"/>`
1572
- : ''
1573
- const shading = cell.header
1574
- ? '<w:shd w:val="clear" w:color="auto" w:fill="BFBFBF"/>'
1575
- : ''
1576
- const verticalMerge = cell.vMerge
1577
- ? `<w:vMerge${cell.vMerge === 'restart' ? ' w:val="restart"' : ''}/>`
1578
- : ''
1579
-
1580
- return `<w:tc><w:tcPr><w:tcW w:w="${columnWidthPct * Number(cell.colSpan || 1)}" w:type="pct"/>${gridSpan}${verticalMerge}${shading}<w:vAlign w:val="center"/><w:tcMar><w:top w:w="80" w:type="dxa"/><w:left w:w="80" w:type="dxa"/><w:bottom w:w="80" w:type="dxa"/><w:right w:w="80" w:type="dxa"/></w:tcMar></w:tcPr>${cellXml}</w:tc>`
1581
- }
1582
-
1583
- function buildDocxTable(tableBlock = {}, baseParagraphProperties = '', baseRunProperties = '') {
1584
- const columnCount = getTableColumnCount(tableBlock)
1585
- const columnWidth = Math.max(1, Math.floor(DOCX_TABLE_WIDTH_DXA / columnCount))
1586
- const columnWidthPct = Math.max(1, Math.floor(DOCX_TABLE_WIDTH_PCT / columnCount))
1587
- const tableGrid = Array.from({ length: columnCount }, () => `<w:gridCol w:w="${columnWidth}"/>`).join('')
1588
- const rowsXml = normalizeTableRowsForDocx(tableBlock).map((row) => {
1589
- const cellsXml = (row.cells || []).map((cell) => {
1590
- return buildDocxTableCell(cell, columnWidthPct, baseParagraphProperties, baseRunProperties)
1591
- }).join('')
1592
-
1593
- return `<w:tr>${cellsXml}</w:tr>`
1594
- }).join('')
1595
-
1596
- return `<w:tbl><w:tblPr><w:tblW w:w="${DOCX_TABLE_WIDTH_PCT}" w:type="pct"/><w:jc w:val="center"/><w:tblLayout w:type="fixed"/><w:tblBorders><w:top w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:left w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:bottom w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:right w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:insideH w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:insideV w:val="single" w:sz="8" w:space="0" w:color="000000"/></w:tblBorders></w:tblPr><w:tblGrid>${tableGrid}</w:tblGrid>${rowsXml}</w:tbl>`
1597
- }
1598
-
1599
- function createInlineMarks(marks = []) {
1600
- return marks.reduce((result, mark) => {
1601
- if (mark.type === 'bold') {
1602
- result.bold = true
1603
- }
1604
-
1605
- if (mark.type === 'textStyle' && mark.attrs?.color) {
1606
- result.color = mark.attrs.color
1607
- }
1608
-
1609
- if (mark.type === 'highlight' && mark.attrs?.color) {
1610
- result.backgroundColor = mark.attrs.color
1611
- }
1612
-
1613
- return result
1614
- }, {})
1615
- }
1616
-
1617
- function getHtmlNodeTextAlign(node) {
1618
- return getHtmlNodeTextAlignWithDefault(node)
1619
- }
1620
-
1621
- function getHtmlNodeTextIndentLevel(node) {
1622
- if (!node || node.nodeType !== DOM_ELEMENT_NODE) {
1623
- return 0
1624
- }
1625
-
1626
- const indent = node.style?.textIndent || ''
1627
- const match = indent.match(/^([\d.]+)em$/)
1628
-
1629
- if (!match) {
1630
- return 0
1631
- }
1632
-
1633
- return Math.min(
1634
- EDITOR_MAX_INDENT_LEVEL,
1635
- Math.max(0, Math.round(Number(match[1]) / EDITOR_INDENT_STEP_EM))
1636
- )
1637
- }
1638
-
1639
- function getHtmlNodeTextAlignWithDefault(node, defaultAlign = 'left') {
1640
- if (!node || node.nodeType !== DOM_ELEMENT_NODE) {
1641
- return defaultAlign
1642
- }
1643
-
1644
- const inlineAlign = node.style?.textAlign || ''
1645
-
1646
- if (['left', 'center', 'right'].includes(inlineAlign)) {
1647
- return inlineAlign
1648
- }
1649
-
1650
- const alignAttr = node.getAttribute?.('align') || ''
1651
-
1652
- if (['left', 'center', 'right'].includes(alignAttr)) {
1653
- return alignAttr
1654
- }
1655
-
1656
- return defaultAlign
1657
- }
1658
-
1659
- function getHtmlNodeMarks(node, inheritedMarks = {}) {
1660
- if (!node || node.nodeType !== DOM_ELEMENT_NODE) {
1661
- return inheritedMarks
1662
- }
1663
-
1664
- const tagName = node.tagName?.toLowerCase?.() || ''
1665
- const nextMarks = { ...inheritedMarks }
1666
-
1667
- if (tagName === 'strong' || tagName === 'b') {
1668
- nextMarks.bold = true
1669
- }
1670
-
1671
- const textColor = node.style?.color ? normalizeDocxColor(node.style.color) : ''
1672
- const backgroundColor = node.style?.backgroundColor ? normalizeDocxColor(node.style.backgroundColor) : ''
1673
-
1674
- if (textColor) {
1675
- nextMarks.color = `#${textColor}`
1676
- }
1677
-
1678
- if (backgroundColor) {
1679
- nextMarks.backgroundColor = `#${backgroundColor}`
1680
- }
1681
-
1682
- return nextMarks
1683
- }
1684
-
1685
- function extractRunsFromHtmlNode(node, inheritedMarks = {}) {
1686
- if (!node) {
1687
- return []
1688
- }
1689
-
1690
- if (node.nodeType === DOM_TEXT_NODE) {
1691
- return node.textContent
1692
- ? [{
1693
- text: node.textContent,
1694
- marks: inheritedMarks
1695
- }]
1696
- : []
1697
- }
1698
-
1699
- if (node.nodeType !== DOM_ELEMENT_NODE) {
1700
- return []
1701
- }
1702
-
1703
- const tagName = node.tagName?.toLowerCase?.() || ''
1704
- const nextMarks = getHtmlNodeMarks(node, inheritedMarks)
1705
-
1706
- if (tagName === 'br') {
1707
- return [{
1708
- text: '\n',
1709
- marks: nextMarks
1710
- }]
1711
- }
1712
-
1713
- return Array.from(node.childNodes).flatMap((childNode) => extractRunsFromHtmlNode(childNode, nextMarks))
1714
- }
1715
-
1716
- function normalizeRuns(runs = []) {
1717
- return runs.reduce((result, run) => {
1718
- if (!run?.text) {
1719
- return result
1720
- }
1721
-
1722
- const previousRun = result[result.length - 1]
1723
- const marksKey = JSON.stringify(run.marks || {})
1724
- const previousMarksKey = JSON.stringify(previousRun?.marks || {})
1725
-
1726
- if (previousRun && marksKey === previousMarksKey) {
1727
- previousRun.text += run.text
1728
- return result
1729
- }
1730
-
1731
- result.push({
1732
- text: run.text,
1733
- marks: run.marks || {}
1734
- })
1735
- return result
1736
- }, [])
1737
- }
1738
-
1739
- function extractBlocksFromHtml(html = '') {
1740
- if (!html) {
1741
- return []
1742
- }
1743
-
1744
- const container = document.createElement('div')
1745
- container.innerHTML = html
1746
-
1747
- function extractBlockFromElement(element) {
1748
- const tagName = element.tagName?.toLowerCase?.() || ''
1749
-
1750
- if (tagName === 'table') {
1751
- return {
1752
- type: 'table',
1753
- rows: Array.from(element.rows || []).map((row) => ({
1754
- cells: Array.from(row.cells || []).map((cell) => {
1755
- const childBlocks = Array.from(cell.childNodes).flatMap((childNode) => {
1756
- if (childNode.nodeType === DOM_TEXT_NODE) {
1757
- const text = childNode.textContent?.trim?.() || ''
1758
- return text
1759
- ? [{
1760
- type: 'paragraph',
1761
- align: getHtmlNodeTextAlignWithDefault(cell, 'center'),
1762
- textIndentLevel: 0,
1763
- runs: [{ text, marks: {} }]
1764
- }]
1765
- : []
1766
- }
1767
-
1768
- if (childNode.nodeType !== DOM_ELEMENT_NODE) {
1769
- return []
1770
- }
1771
-
1772
- const childTagName = childNode.tagName?.toLowerCase?.() || ''
1773
-
1774
- if (['p', 'div', 'h1', 'h2', 'h3'].includes(childTagName)) {
1775
- const childBlock = extractBlockFromElement(childNode)
1776
-
1777
- if (childBlock && !childNode.style?.textAlign && !childNode.getAttribute?.('align')) {
1778
- childBlock.align = getHtmlNodeTextAlignWithDefault(cell, 'center')
1779
- }
1780
-
1781
- return childBlock ? [childBlock] : []
1782
- }
1783
-
1784
- return []
1785
- })
1786
-
1787
- return {
1788
- header: cell.tagName?.toLowerCase?.() === 'th',
1789
- colSpan: Number(cell.colSpan || 1),
1790
- rowSpan: Number(cell.rowSpan || 1),
1791
- blocks: childBlocks.length
1792
- ? childBlocks
1793
- : [{
1794
- type: 'paragraph',
1795
- align: getHtmlNodeTextAlignWithDefault(cell, 'center'),
1796
- textIndentLevel: 0,
1797
- runs: []
1798
- }]
1799
- }
1800
- })
1801
- }))
1802
- }
1803
- }
1804
-
1805
- if (!['p', 'div', 'h1', 'h2', 'h3'].includes(tagName)) {
1806
- return null
1807
- }
1808
-
1809
- return {
1810
- type: tagName.startsWith('h') ? 'heading' : 'paragraph',
1811
- level: tagName.startsWith('h') ? Number(tagName.slice(1)) : null,
1812
- align: getHtmlNodeTextAlign(element),
1813
- textIndentLevel: getHtmlNodeTextIndentLevel(element),
1814
- runs: normalizeRuns(extractRunsFromHtmlNode(element))
1815
- }
1816
- }
1817
-
1818
- const blocks = Array.from(container.childNodes).flatMap((node) => {
1819
- if (node.nodeType === DOM_TEXT_NODE) {
1820
- const text = node.textContent?.trim?.() || ''
1821
- return text
1822
- ? [{
1823
- type: 'paragraph',
1824
- align: 'left',
1825
- textIndentLevel: 0,
1826
- runs: [{ text, marks: {} }]
1827
- }]
1828
- : []
1829
- }
1830
-
1831
- if (node.nodeType !== DOM_ELEMENT_NODE) {
1832
- return []
1833
- }
1834
-
1835
- const tagName = node.tagName?.toLowerCase?.() || ''
1836
-
1837
- if (!['p', 'div', 'h1', 'h2', 'h3', 'table'].includes(tagName)) {
1838
- return []
1839
- }
1840
-
1841
- const block = extractBlockFromElement(node)
1842
- return block ? [block] : []
1843
- })
1844
-
1845
- return blocks.filter((block) => {
1846
- if (block.type === 'table') {
1847
- return block.rows?.length
1848
- }
1849
-
1850
- return block.runs?.some((run) => run.text)
1851
- })
1852
- }
1853
-
1854
- function extractTextRuns(contentList = []) {
1855
- return contentList.reduce((result, item) => {
1856
- if (!item) {
1857
- return result
1858
- }
1859
-
1860
- if (item.type === 'text') {
1861
- result.push({
1862
- text: item.text || '',
1863
- marks: createInlineMarks(item.marks)
1864
- })
1865
- return result
1866
- }
1867
-
1868
- if (Array.isArray(item.content)) {
1869
- result.push(...extractTextRuns(item.content))
1870
- }
1871
-
1872
- return result
1873
- }, [])
1874
- }
1875
-
1876
- function convertRichTextNodeToBlock(node) {
1877
- if (!node) {
1878
- return null
1879
- }
1880
-
1881
- if (node.type === 'paragraph' || node.type === 'heading') {
1882
- return {
1883
- type: node.type,
1884
- level: node.attrs?.level || null,
1885
- align: node.attrs?.textAlign || 'left',
1886
- textIndentLevel: Number(node.attrs?.textIndentLevel || 0),
1887
- runs: extractTextRuns(node.content || [])
1888
- }
1889
- }
1890
-
1891
- return {
1892
- type: node.type,
1893
- runs: extractTextRuns(node.content || [])
1894
- }
1895
- }
1896
-
1897
- function buildBackendRichTextValue(fieldKey, path, html, json, plainText) {
1898
- const normalizedHtml = normalizeRichTextHtmlForStorage(html)
1899
- const htmlBlocks = extractBlocksFromHtml(normalizedHtml)
1900
- const jsonBlocks = (json?.content || [])
1901
- .map((node) => convertRichTextNodeToBlock(node))
1902
- .filter(Boolean)
1903
-
1904
- return {
1905
- version: '1.0',
1906
- fieldKey,
1907
- path,
1908
- docxRenderText: plainText,
1909
- plainText,
1910
- html: normalizedHtml,
1911
- json,
1912
- blocks: htmlBlocks.length ? htmlBlocks : jsonBlocks,
1913
- updatedAt: new Date().toISOString()
1914
- }
1915
- }
1916
-
1917
- function getRichTextBlocks(richItem = {}) {
1918
- if (!richItem) {
1919
- return []
1920
- }
1921
-
1922
- if (Array.isArray(richItem.blocks) && richItem.blocks.length) {
1923
- return richItem.blocks
1924
- }
1925
-
1926
- return (richItem.json?.content || [])
1927
- .map((node) => convertRichTextNodeToBlock(node))
1928
- .filter(Boolean)
1929
- }
1930
-
1931
- function buildDocxRichTextRuns(richItem = {}, baseRunProperties = '') {
1932
- const normalizedRichItem = richItem || {}
1933
- const blocks = getRichTextBlocks(richItem).filter((block) => block.type !== 'table')
1934
-
1935
- if (!blocks.length && normalizedRichItem.plainText) {
1936
- return buildDocxTextRuns(normalizedRichItem.plainText, {}, baseRunProperties)
1937
- }
1938
-
1939
- return blocks.map((block, blockIndex) => {
1940
- const blockRuns = (block.runs || []).map((run) => {
1941
- return buildDocxTextRuns(run.text || '', run.marks || {}, baseRunProperties)
1942
- }).join('')
1943
-
1944
- return `${blockIndex > 0 ? '<w:r><w:br/></w:r>' : ''}${blockRuns}`
1945
- }).join('')
1946
- }
1947
-
1948
- function formatHeadingNumber(level, counters) {
1949
- if (level === 1) {
1950
- const chineseNumbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
1951
- return `${chineseNumbers[counters[0] - 1] || counters[0]}、`
1952
- }
1953
-
1954
- if (level === 2) {
1955
- const chineseNumbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
1956
- return `(${chineseNumbers[counters[1] - 1] || counters[1]})`
1957
- }
1958
-
1959
- return `(${counters[2]})`
1960
- }
1961
-
1962
- function stripExistingHeadingNumber(text = '') {
1963
- return String(text || '')
1964
- .replace(/^([一二三四五六七八九十]+、|([一二三四五六七八九十\d]+)|\(\d+\)|\d+[.、])\s*/, '')
1965
- }
1966
-
1967
- function applyAutoHeadingNumber(block, counters) {
1968
- if (block.type !== 'heading' || !block.level || block.__numberApplied) {
1969
- return block
1970
- }
1971
-
1972
- const level = Math.min(3, Math.max(1, Number(block.level || 1)))
1973
- counters[level - 1] += 1
1974
-
1975
- for (let index = level; index < counters.length; index += 1) {
1976
- counters[index] = 0
1977
- }
1978
-
1979
- const runs = block.runs || []
1980
- const firstRun = runs.find((run) => run.text)
1981
- const prefix = formatHeadingNumber(level, counters)
1982
-
1983
- if (firstRun) {
1984
- firstRun.text = `${prefix}${stripExistingHeadingNumber(firstRun.text)}`
1985
- } else {
1986
- runs.unshift({ text: prefix, marks: {} })
1987
- }
1988
-
1989
- block.runs = runs
1990
- block.__numberApplied = true
1991
- return block
1992
- }
1993
-
1994
- function getBlockParagraphStyleId(block, renderContext = {}) {
1995
- if (block.type === 'heading') {
1996
- return renderContext.headingStyleIds?.[block.level] || ''
1997
- }
1998
-
1999
- return renderContext.bodyStyleId || ''
2000
- }
2001
-
2002
- function buildDocxRichTextParagraphs(richItem = {}, baseParagraphProperties = '', baseRunProperties = '', beforeText = '', afterText = '', renderContext = {}) {
2003
- const normalizedRichItem = richItem || {}
2004
- const blocks = getRichTextBlocks(richItem)
2005
- const renderBlocks = blocks.length
2006
- ? blocks
2007
- : [{
2008
- type: 'paragraph',
2009
- align: 'left',
2010
- textIndentLevel: 0,
2011
- runs: [{ text: normalizedRichItem.plainText || '', marks: {} }]
2012
- }]
2013
-
2014
- const normalizedBlocks = deepClone(renderBlocks)
2015
- const headingCounters = [...(renderContext.headingCounters || [0, 0, 0])]
2016
-
2017
- if (beforeText) {
2018
- if (normalizedBlocks[0]?.type === 'paragraph' || normalizedBlocks[0]?.type === 'heading') {
2019
- normalizedBlocks[0].runs = [{ text: beforeText, marks: {} }, ...(normalizedBlocks[0].runs || [])]
2020
- } else {
2021
- normalizedBlocks.unshift({
2022
- type: 'paragraph',
2023
- align: 'left',
2024
- textIndentLevel: 0,
2025
- runs: [{ text: beforeText, marks: {} }]
2026
- })
2027
- }
2028
- }
2029
-
2030
- if (afterText) {
2031
- const lastBlock = normalizedBlocks[normalizedBlocks.length - 1]
2032
-
2033
- if (lastBlock?.type === 'paragraph' || lastBlock?.type === 'heading') {
2034
- lastBlock.runs = [...(lastBlock.runs || []), { text: afterText, marks: {} }]
2035
- } else {
2036
- normalizedBlocks.push({
2037
- type: 'paragraph',
2038
- align: 'left',
2039
- textIndentLevel: 0,
2040
- runs: [{ text: afterText, marks: {} }]
2041
- })
2042
- }
2043
- }
2044
-
2045
- return normalizedBlocks.map((sourceBlock) => {
2046
- const block = applyAutoHeadingNumber(sourceBlock, headingCounters)
2047
-
2048
- if (block.type === 'table') {
2049
- return buildDocxTable(block, baseParagraphProperties, baseRunProperties)
2050
- }
2051
-
2052
- const runs = (block.runs || []).map((run) => {
2053
- return buildDocxTextRuns(run.text || '', run.marks || {}, baseRunProperties)
2054
- }).join('')
2055
-
2056
- return buildDocxParagraph(
2057
- runs,
2058
- block.align || 'left',
2059
- baseParagraphProperties,
2060
- block.textIndentLevel || 0,
2061
- getBlockParagraphStyleId(block, renderContext)
2062
- )
2063
- }).join('')
2064
- }
2065
-
2066
- function hasRichTextTableBlock(richItem = {}) {
2067
- return getRichTextBlocks(richItem).some((block) => block.type === 'table')
2068
- }
2069
-
2070
- function getRichTextFirstTableHtml(richItem = {}) {
2071
- if (!richItem?.html) {
2072
- return ''
2073
- }
2074
-
2075
- const container = document.createElement('div')
2076
- container.innerHTML = richItem.html
2077
- const table = container.querySelector('table')
2078
-
2079
- return table?.outerHTML || ''
2080
- }
2081
-
2082
- function replaceTokenParagraphAndFollowingTable(xmlText = '', token = '', richItem = {}, renderContext = {}) {
2083
- const escapedToken = escapeRegExp(token)
2084
- const tokenParagraphAndTableRegExp = new RegExp(
2085
- `<w:p\\b(?:(?!<w:p\\b)[\\s\\S])*?${escapedToken}(?:(?!<w:p\\b)[\\s\\S])*?<\\/w:p>\\s*<w:tbl\\b[\\s\\S]*?<\\/w:tbl>`,
2086
- 'g'
2087
- )
2088
-
2089
- return xmlText.replace(tokenParagraphAndTableRegExp, (matchedXml) => {
2090
- const baseParagraphProperties = getParagraphProperties(matchedXml)
2091
- const baseRunProperties = getFirstRunProperties(matchedXml)
2092
- return buildDocxRichTextParagraphs(
2093
- richItem,
2094
- baseParagraphProperties,
2095
- baseRunProperties,
2096
- '',
2097
- '',
2098
- renderContext
2099
- )
2100
- })
2101
- }
2102
-
2103
- function getRichTextRenderContext(fieldKey, options = {}) {
2104
- const context = options.wordStyleContext || {}
2105
-
2106
- return {
2107
- headingStyleIds: context.headingStyleIds || {},
2108
- bodyStyleId: context.bodyStyleId || '',
2109
- headingCounters: context.editableHeadingCounters?.[fieldKey] || [0, 0, 0]
2110
- }
2111
- }
2112
-
2113
- function replaceRichTextTokensInXml(xmlText = '', richTextMap = {}, options = {}) {
2114
- return Object.keys(richTextMap).reduce((result, fieldKey) => {
2115
- const richItem = richTextMap[fieldKey]
2116
- const renderContext = getRichTextRenderContext(fieldKey, options)
2117
-
2118
- if (!hasRichTextContent(richItem)) {
2119
- return result
2120
- }
2121
-
2122
- const token = getRichTextToken(fieldKey)
2123
-
2124
- if (!result.includes(token)) {
2125
- return result
2126
- }
2127
-
2128
- if (hasRichTextTableBlock(richItem)) {
2129
- result = replaceTokenParagraphAndFollowingTable(result, token, richItem, renderContext)
2130
-
2131
- if (!result.includes(token)) {
2132
- return result
2133
- }
2134
-
2135
- result = result.replace(
2136
- /<w:tbl\b[\s\S]*?<\/w:tbl>/g,
2137
- (tableXml) => {
2138
- if (!tableXml.includes(token)) {
2139
- return tableXml
2140
- }
2141
-
2142
- return buildDocxRichTextParagraphs(
2143
- richItem,
2144
- getParagraphProperties(tableXml),
2145
- getFirstRunProperties(tableXml),
2146
- '',
2147
- '',
2148
- renderContext
2149
- )
2150
- }
2151
- )
2152
- }
2153
-
2154
- if (!result.includes(token)) {
2155
- return result
2156
- }
2157
-
2158
- const paragraphPatchedXml = result.replace(
2159
- /<w:p\b[^>]*>[\s\S]*?<\/w:p>/g,
2160
- (paragraphXml) => {
2161
- if (!paragraphXml.includes(token)) {
2162
- return paragraphXml
2163
- }
2164
-
2165
- const paragraphText = getXmlText(paragraphXml)
2166
-
2167
- if (!paragraphText.includes(token)) {
2168
- return paragraphXml
2169
- }
2170
-
2171
- const [beforeText, afterText] = paragraphText.split(token)
2172
- const blocks = getRichTextBlocks(richItem)
2173
- const isStandaloneToken = !normalizeEditableText(beforeText) && !normalizeEditableText(afterText)
2174
- const hasExplicitParagraphAlign = blocks.some((block) => ['center', 'right'].includes(block.align))
2175
-
2176
- if (!isStandaloneToken && !hasExplicitParagraphAlign && blocks.length <= 1) {
2177
- return paragraphXml
2178
- }
2179
-
2180
- return buildDocxRichTextParagraphs(
2181
- richItem,
2182
- getParagraphProperties(paragraphXml),
2183
- getFirstRunProperties(paragraphXml),
2184
- beforeText,
2185
- afterText,
2186
- renderContext
2187
- )
2188
- }
2189
- )
2190
-
2191
- if (!paragraphPatchedXml.includes(token)) {
2192
- return paragraphPatchedXml
2193
- }
2194
-
2195
- return paragraphPatchedXml.replace(
2196
- /<w:r\b[\s\S]*?<\/w:r>/g,
2197
- (runXml) => {
2198
- if (!runXml.includes(token)) {
2199
- return runXml
2200
- }
2201
-
2202
- const runProperties = runXml.match(/<w:rPr>([\s\S]*?)<\/w:rPr>/)?.[1] || ''
2203
- const textMatches = [...runXml.matchAll(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g)]
2204
- const runText = textMatches.map((match) => decodeXmlText(match[1])).join('')
2205
-
2206
- if (!runText.includes(token)) {
2207
- return runXml
2208
- }
2209
-
2210
- const [beforeText, afterText] = runText.split(token)
2211
- const beforeRuns = buildDocxTextRuns(beforeText, {}, runProperties)
2212
- const richRuns = buildDocxRichTextRuns(richItem, runProperties)
2213
- const afterRuns = buildDocxTextRuns(afterText, {}, runProperties)
2214
-
2215
- return `${beforeRuns}${richRuns}${afterRuns}`
2216
- }
2217
- )
2218
- }, xmlText)
2219
- }
2220
-
2221
- function applyRichTextTokensToDocx(zip, richTextMap = {}, options = {}) {
2222
- const documentXmlFile = zip.file('word/document.xml')
2223
-
2224
- if (!documentXmlFile) {
2225
- return
2226
- }
2227
-
2228
- const documentXml = documentXmlFile.asText()
2229
- const patchedXml = replaceRichTextTokensInXml(documentXml, richTextMap, options)
2230
-
2231
- if (patchedXml !== documentXml) {
2232
- zip.file('word/document.xml', patchedXml)
2233
- }
2234
- }
2235
-
2236
- function buildSavePayload() {
2237
- const richTextMap = editableRenderData.value.__editableRichTextMap || {}
2238
-
2239
- return {
2240
- renderData: deepClone(editableRenderData.value),
2241
- richTextFields: deepClone(richTextMap),
2242
- editableFields: editableFieldKeys.value.map((fieldKey) => ({
2243
- fieldKey,
2244
- path: editableFieldPathMap.value[fieldKey] || '',
2245
- hasRichText: Boolean(richTextMap[fieldKey])
2246
- }))
2247
- }
2248
- }
2249
-
2250
- function getRenderableFieldHtml(key, fallbackText = '') {
2251
- const richTextMap = editableRenderData.value.__editableRichTextMap || {}
2252
- const richTextItem = richTextMap[key]
2253
-
2254
- return buildRenderableSegments(richTextItem?.html, fallbackText)
2255
- }
2256
-
2257
- function cleanupEditorMarkers() {
2258
- if (!previewRef.value) {
2259
- return
2260
- }
2261
-
2262
- const textNodes = getAllTextNodes(previewRef.value)
2263
-
2264
- textNodes.forEach((node) => {
2265
- const text = node.textContent || ''
2266
-
2267
- if (!text) {
2268
- return
2269
- }
2270
-
2271
- const cleanedText = text
2272
- .replaceAll(`${LEGACY_EDITOR_MARKER_START}${EDITOR_MARKER_START}`, '')
2273
- .replaceAll(`${EDITOR_MARKER_END}${LEGACY_EDITOR_MARKER_END}`, '')
2274
- .replaceAll(`${LEGACY_EDITOR_MARKER_START} ${EDITOR_MARKER_START}`, '')
2275
- .replaceAll(`${EDITOR_MARKER_END} ${LEGACY_EDITOR_MARKER_END}`, '')
2276
- .replaceAll(EDITOR_MARKER_START, '')
2277
- .replaceAll(EDITOR_MARKER_END, '')
2278
- .replaceAll(LEGACY_EDITOR_MARKER_START, '')
2279
- .replaceAll(LEGACY_EDITOR_MARKER_END, '')
2280
-
2281
- if (cleanedText !== text) {
2282
- node.textContent = cleanedText
2283
- }
2284
- })
2285
- }
2286
-
2287
- function normalizeWaterLevelChartConfig(config = {}) {
2288
- const xAxisData = config.xAxisData || config.xAxis || config.categories || []
2289
- const yAxisData = config.yAxisData || config.yAxis || config.values || []
2290
- const upperLimit = Number(
2291
- config.upperLimit ?? config.reasonableUpperLimit ?? config.maxReasonableValue ?? 80
2292
- )
2293
- const lowerLimit = Number(
2294
- config.lowerLimit ?? config.reasonableLowerLimit ?? config.minReasonableValue ?? 30
2295
- )
2296
- const regionList = config.regionList || config.regions || config.areas || []
2297
-
2298
- return {
2299
- title: config.title || '',
2300
- xAxisData,
2301
- yAxisData,
2302
- upperLimit,
2303
- lowerLimit,
2304
- regionList
2305
- }
2306
- }
2307
-
2308
- function buildWaterLevelChartOption(config = {}, tagName = '') {
2309
- const chartConfig = normalizeWaterLevelChartConfig(config, tagName)
2310
- const total = chartConfig.xAxisData.length || 1
2311
- const plotWidth = WATER_LEVEL_CHART_CANVAS_WIDTH - WATER_LEVEL_CHART_GRID.left - WATER_LEVEL_CHART_GRID.right
2312
- const plotBottom = WATER_LEVEL_CHART_CANVAS_HEIGHT - WATER_LEVEL_CHART_GRID.bottom
2313
- const regionGraphics = []
2314
- let consumedCount = 0
2315
-
2316
- chartConfig.regionList.forEach((region, index) => {
2317
- const itemCount = Number(region.count || region.length || 0)
2318
- if (!itemCount) {
2319
- return
2320
- }
2321
-
2322
- const startRatio = consumedCount / total
2323
- const endRatio = (consumedCount + itemCount) / total
2324
- const centerRatio = (startRatio + endRatio) / 2
2325
- const centerX = WATER_LEVEL_CHART_GRID.left + plotWidth * centerRatio
2326
- const splitX = WATER_LEVEL_CHART_GRID.left + plotWidth * endRatio
2327
-
2328
- regionGraphics.push({
2329
- type: 'group',
2330
- left: centerX - 32,
2331
- top: 18,
2332
- children: [
2333
- {
2334
- type: 'rect',
2335
- shape: {
2336
- x: 0,
2337
- y: 0,
2338
- width: 64,
2339
- height: 18
2340
- },
2341
- style: {
2342
- fill: '#efefef',
2343
- stroke: '#999',
2344
- lineWidth: 1
2345
- }
2346
- },
2347
- {
2348
- type: 'text',
2349
- style: {
2350
- x: 32,
2351
- y: 9,
2352
- text: region.name || `区域${index + 1}`,
2353
- fill: '#333',
2354
- fontSize: 12,
2355
- fontFamily: 'Microsoft YaHei',
2356
- textAlign: 'center',
2357
- textVerticalAlign: 'middle'
2358
- }
2359
- }
2360
- ]
2361
- })
2362
-
2363
- if (index < chartConfig.regionList.length - 1) {
2364
- regionGraphics.push({
2365
- type: 'line',
2366
- shape: {
2367
- x1: splitX,
2368
- y1: 38,
2369
- x2: splitX,
2370
- y2: plotBottom
2371
- },
2372
- style: {
2373
- stroke: '#777',
2374
- lineWidth: 1
2375
- }
2376
- })
2377
- }
2378
-
2379
- consumedCount += itemCount
2380
- })
2381
-
2382
- return {
2383
- animation: false,
2384
- backgroundColor: '#fff',
2385
- title: {
2386
- text: chartConfig.title,
2387
- left: 'center',
2388
- top: 0,
2389
- textStyle: {
2390
- color: '#333',
2391
- fontSize: 14,
2392
- fontWeight: 400
2393
- }
2394
- },
2395
- legend: {
2396
- top: 58,
2397
- right: 18,
2398
- itemWidth: 18,
2399
- itemHeight: 2,
2400
- textStyle: {
2401
- color: '#666',
2402
- fontSize: 11
2403
- },
2404
- data: ['合理区间上限', '合理区间下限']
2405
- },
2406
- grid: {
2407
- ...WATER_LEVEL_CHART_GRID
2408
- },
2409
- tooltip: {
2410
- trigger: 'axis'
2411
- },
2412
- xAxis: {
2413
- type: 'category',
2414
- data: chartConfig.xAxisData,
2415
- axisTick: {
2416
- show: false
2417
- },
2418
- axisLine: {
2419
- lineStyle: {
2420
- color: '#888'
2421
- }
2422
- },
2423
- axisLabel: {
2424
- interval: 0,
2425
- rotate: 90,
2426
- color: '#333',
2427
- fontSize: 11
2428
- }
2429
- },
2430
- yAxis: {
2431
- type: 'value',
2432
- min: 0,
2433
- max: 100,
2434
- axisLine: {
2435
- show: true
2436
- },
2437
- splitLine: {
2438
- lineStyle: {
2439
- color: '#e8e8e8'
2440
- }
2441
- }
2442
- },
2443
- series: [
2444
- {
2445
- name: '水位值',
2446
- type: 'bar',
2447
- barWidth: 26,
2448
- data: chartConfig.yAxisData.map((value) => {
2449
- const numericValue = Number(value)
2450
- let color = '#86c5e3'
2451
-
2452
- if (numericValue >= chartConfig.upperLimit) {
2453
- color = '#ff2a23'
2454
- } else if (numericValue >= chartConfig.upperLimit * 0.85) {
2455
- color = '#ffef1f'
2456
- }
2457
-
2458
- return {
2459
- value: numericValue,
2460
- itemStyle: {
2461
- color
2462
- }
2463
- }
2464
- }),
2465
- label: {
2466
- show: true,
2467
- position: 'top',
2468
- color: '#333',
2469
- fontSize: 11,
2470
- formatter: ({ value }) => Number(value).toFixed(2)
2471
- },
2472
- markLine: {
2473
- silent: true,
2474
- symbol: 'none',
2475
- lineStyle: {
2476
- width: 1
2477
- },
2478
- data: [
2479
- {
2480
- name: '合理区间上限',
2481
- yAxis: chartConfig.upperLimit,
2482
- lineStyle: {
2483
- color: '#ff6b6b',
2484
- type: 'dashed'
2485
- }
2486
- },
2487
- {
2488
- name: '合理区间下限',
2489
- yAxis: chartConfig.lowerLimit,
2490
- lineStyle: {
2491
- color: '#77c48b',
2492
- type: 'dashed'
2493
- }
2494
- }
2495
- ]
2496
- }
2497
- }
2498
- ],
2499
- graphic: regionGraphics
2500
- }
2501
- }
2502
-
2503
- async function renderChartToImageBuffer(config = {}, tagName = '') {
2504
- const chartDom = document.createElement('div')
2505
- chartDom.style.cssText = `position:fixed;left:-10000px;top:-10000px;width:${WATER_LEVEL_CHART_CANVAS_WIDTH}px;height:${WATER_LEVEL_CHART_CANVAS_HEIGHT}px;background:#fff;`
2506
- document.body.appendChild(chartDom)
2507
-
2508
- try {
2509
- const chart = echarts.init(chartDom, null, {
2510
- renderer: 'canvas',
2511
- width: WATER_LEVEL_CHART_CANVAS_WIDTH,
2512
- height: WATER_LEVEL_CHART_CANVAS_HEIGHT
2513
- })
2514
- chart.setOption(buildWaterLevelChartOption(config, tagName))
2515
- const dataUrl = chart.getDataURL({
2516
- type: 'png',
2517
- pixelRatio: 2,
2518
- backgroundColor: '#fff'
2519
- })
2520
- chart.dispose()
2521
- const response = await fetch(dataUrl)
2522
- return response.arrayBuffer()
2523
- } finally {
2524
- chartDom.remove()
2525
- }
2526
- }
2527
-
2528
- async function loadTemplate() {
2529
- const templateUrl = props.templateUrl || `/${encodeURIComponent(props.templateFileName || DEFAULT_TEMPLATE_FILE_NAME)}`
2530
- const response = await fetch(templateUrl)
2531
-
2532
- if (!response.ok) {
2533
- throw new Error(`模板加载失败: ${response.status}`)
2534
- }
2535
-
2536
- return response.arrayBuffer()
2537
- }
2538
-
2539
- function stripEditorMarkersFromXml(xmlText = '') {
2540
- return xmlText
2541
- .replaceAll(EDITOR_MARKER_START, '')
2542
- .replaceAll(EDITOR_MARKER_END, '')
2543
- }
2544
-
2545
- function createTemplateZip(templateBuffer, options = {}) {
2546
- const { stripEditorMarkers = false } = options
2547
- const zip = new PizZip(templateBuffer)
2548
- const documentXml = zip.file('word/document.xml')?.asText?.() || ''
2549
- let patchedXml = WATER_LEVEL_CHART_TAGS.reduce((result, key) => {
2550
- return result.replaceAll(`{${key}}`, `{%${key}}`)
2551
- }, documentXml)
2552
-
2553
- if (stripEditorMarkers) {
2554
- patchedXml = stripEditorMarkersFromXml(patchedXml)
2555
- }
2556
-
2557
- if (patchedXml !== documentXml) {
2558
- zip.file('word/document.xml', patchedXml)
2559
- }
2560
-
2561
- return {
2562
- zip,
2563
- documentXml: patchedXml
2564
- }
2565
- }
2566
-
2567
- async function createRenderedDocxBlob(options = {}) {
2568
- const { stripEditorMarkers = true, applyRichText = true } = options
2569
- const templateBuffer = await loadTemplate()
2570
- const { zip: templateContextZip, documentXml: templateContextXml } = createTemplateZip(templateBuffer, {
2571
- stripEditorMarkers: false
2572
- })
2573
- const { zip } = createTemplateZip(templateBuffer, { stripEditorMarkers })
2574
- const currentWordStyleContext = extractWordStyleContext(templateContextZip, templateContextXml)
2575
- const imageModule = new ImageModule({
2576
- centered: true,
2577
- fileType: 'docx',
2578
- getImage(tagValue, tagName) {
2579
- if (getChartTagNames()[tagName]) {
2580
- return renderChartToImageBuffer(tagValue, tagName)
2581
- }
2582
- return tagValue
2583
- },
2584
- getSize(img, tagValue, tagName) {
2585
- if (getChartTagNames()[tagName]) {
2586
- return [WATER_LEVEL_CHART_DOCX_WIDTH, WATER_LEVEL_CHART_DOCX_HEIGHT]
2587
- }
2588
- return [120, 120]
2589
- }
2590
- })
2591
- const doc = new Docxtemplater(zip, {
2592
- modules: [imageModule],
2593
- paragraphLoop: true,
2594
- linebreaks: true
2595
- })
2596
- const renderData = buildTemplateDataForRender({
2597
- useRichTextTokens: applyRichText,
2598
- useRichTextPlainText: applyRichText
2599
- })
2600
-
2601
- if (typeof doc.resolveData === 'function') {
2602
- await doc.resolveData(renderData)
2603
- } else if (typeof doc.setData === 'function') {
2604
- doc.setData(renderData)
2605
- }
2606
-
2607
- doc.render()
2608
- if (applyRichText) {
2609
- applyRichTextTokensToDocx(doc.getZip(), editableRenderData.value.__editableRichTextMap || {}, {
2610
- wordStyleContext: currentWordStyleContext
2611
- })
2612
- }
2613
-
2614
- return doc.getZip().generate({
2615
- type: 'blob',
2616
- mimeType: DOCX_MIME_TYPE
2617
- })
2618
- }
2619
-
2620
- function downloadBlob(blob, fileName) {
2621
- const downloadUrl = URL.createObjectURL(blob)
2622
- const anchor = document.createElement('a')
2623
- anchor.href = downloadUrl
2624
- anchor.download = fileName
2625
- document.body.appendChild(anchor)
2626
- anchor.click()
2627
- document.body.removeChild(anchor)
2628
- URL.revokeObjectURL(downloadUrl)
2629
- }
2630
-
2631
- function getAllTextNodes(container) {
2632
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT)
2633
- const textNodes = []
2634
-
2635
- while (walker.nextNode()) {
2636
- const currentNode = walker.currentNode
2637
- textNodes.push(currentNode)
2638
- }
2639
-
2640
- return textNodes
2641
- }
2642
-
2643
- function getNodePositionByOffset(textNodes, absoluteOffset, bias = 'end') {
2644
- let currentOffset = 0
2645
-
2646
- for (const node of textNodes) {
2647
- const textLength = (node.textContent || '').length
2648
- const nextOffset = currentOffset + textLength
2649
-
2650
- const isTargetNode = bias === 'start'
2651
- ? absoluteOffset < nextOffset
2652
- : absoluteOffset <= nextOffset
2653
-
2654
- if (isTargetNode) {
2655
- return {
2656
- node,
2657
- offset: Math.max(0, absoluteOffset - currentOffset)
2658
- }
2659
- }
2660
-
2661
- currentOffset = nextOffset
2662
- }
2663
-
2664
- const lastNode = textNodes[textNodes.length - 1]
2665
-
2666
- return {
2667
- node: lastNode,
2668
- offset: (lastNode?.textContent || '').length
2669
- }
2670
- }
2671
-
2672
- function bindEditableAnchorClick(wrapper, fieldKey) {
2673
- const onClick = () => openFieldEditor(fieldKey)
2674
- wrapper.addEventListener('click', onClick)
2675
-
2676
- editableDomCleanupList.push(() => {
2677
- wrapper.removeEventListener('click', onClick)
2678
- })
2679
- }
2680
-
2681
- function createInlineEditableAnchor(fieldKey, fallbackText = '') {
2682
- const wrapper = document.createElement('span')
2683
- wrapper.className = 'editable-field-anchor'
2684
- wrapper.dataset.fieldKey = fieldKey
2685
- const hasContent = hasEditableDisplayContent(fieldKey, fallbackText)
2686
-
2687
- if (!hasContent) {
2688
- wrapper.classList.add('is-placeholder')
2689
- wrapper.textContent = EDITABLE_EMPTY_PLACEHOLDER
2690
- bindEditableAnchorClick(wrapper, fieldKey)
2691
- return wrapper
2692
- }
2693
-
2694
- const segments = getRenderableFieldHtml(fieldKey, fallbackText)
2695
-
2696
- segments.forEach((segment) => {
2697
- if (segment.type === 'break') {
2698
- wrapper.appendChild(document.createElement('br'))
2699
- return
2700
- }
2701
-
2702
- wrapper.appendChild(document.createTextNode(segment.text || ''))
2703
- })
2704
-
2705
- bindEditableAnchorClick(wrapper, fieldKey)
2706
- return wrapper
2707
- }
2708
-
2709
- function createBlockEditableAnchor(fieldKey, fallbackText = '', sourceParagraph = null) {
2710
- const wrapper = document.createElement('div')
2711
- wrapper.className = 'editable-field-block'
2712
- wrapper.dataset.fieldKey = fieldKey
2713
- const hasContent = hasEditableDisplayContent(fieldKey, fallbackText)
2714
-
2715
- if (hasContent) {
2716
- wrapper.innerHTML = getFieldRenderableHtml(fieldKey, fallbackText)
2717
- } else {
2718
- wrapper.classList.add('is-placeholder')
2719
- wrapper.textContent = EDITABLE_EMPTY_PLACEHOLDER
2720
- }
2721
-
2722
- if (wrapper.querySelector('table')) {
2723
- wrapper.classList.add('is-table-field')
2724
- }
2725
- applySourceParagraphLayout(wrapper, sourceParagraph)
2726
- bindEditableAnchorClick(wrapper, fieldKey)
2727
- return wrapper
2728
- }
2729
-
2730
- function stripEditorMarkersFromElement(element) {
2731
- if (!element) {
2732
- return
2733
- }
2734
-
2735
- getAllTextNodes(element).forEach((node) => {
2736
- const text = node.textContent || ''
2737
- const cleanedText = text
2738
- .replaceAll(`${LEGACY_EDITOR_MARKER_START}${EDITOR_MARKER_START}`, '')
2739
- .replaceAll(`${EDITOR_MARKER_END}${LEGACY_EDITOR_MARKER_END}`, '')
2740
- .replaceAll(`${LEGACY_EDITOR_MARKER_START} ${EDITOR_MARKER_START}`, '')
2741
- .replaceAll(`${EDITOR_MARKER_END} ${LEGACY_EDITOR_MARKER_END}`, '')
2742
- .replaceAll(EDITOR_MARKER_START, '')
2743
- .replaceAll(EDITOR_MARKER_END, '')
2744
- .replaceAll(LEGACY_EDITOR_MARKER_START, '')
2745
- .replaceAll(LEGACY_EDITOR_MARKER_END, '')
2746
-
2747
- if (cleanedText !== text) {
2748
- node.textContent = cleanedText
2749
- }
2750
- })
2751
- }
2752
-
2753
- function getCleanOuterHtml(element) {
2754
- if (!element) {
2755
- return ''
2756
- }
2757
-
2758
- const clonedElement = element.cloneNode(true)
2759
- stripEditorMarkersFromElement(clonedElement)
2760
- return clonedElement.outerHTML || ''
2761
- }
2762
-
2763
- function getSharedClosestTable(startNode, endNode) {
2764
- const startElement = startNode?.nodeType === DOM_ELEMENT_NODE ? startNode : startNode?.parentElement
2765
- const endElement = endNode?.nodeType === DOM_ELEMENT_NODE ? endNode : endNode?.parentElement
2766
- const startTable = startElement?.closest?.('table') || null
2767
- const endTable = endElement?.closest?.('table') || null
2768
-
2769
- return startTable && startTable === endTable ? startTable : null
2770
- }
2771
-
2772
- function getRangeContainedTableHtml(startPosition, endPosition) {
2773
- if (!startPosition.node || !endPosition.node) {
2774
- return ''
2775
- }
2776
-
2777
- const range = document.createRange()
2778
- range.setStart(startPosition.node, startPosition.offset)
2779
- range.setEnd(endPosition.node, endPosition.offset)
2780
-
2781
- const fragment = range.cloneContents()
2782
- const table = fragment.querySelector?.('table')
2783
-
2784
- return table ? getCleanOuterHtml(table) : ''
2785
- }
2786
-
2787
- function replaceEditableParagraphRange(startNode, endNode, replacement) {
2788
- const startParagraph = getClosestParagraphElement(startNode)
2789
- const endParagraph = getClosestParagraphElement(endNode)
2790
-
2791
- if (!startParagraph || !endParagraph) {
2792
- return false
2793
- }
2794
-
2795
- if (startParagraph === endParagraph) {
2796
- startParagraph.replaceWith(replacement)
2797
- return true
2798
- }
2799
-
2800
- const range = document.createRange()
2801
- range.setStartBefore(startParagraph)
2802
- range.setEndAfter(endParagraph)
2803
- range.deleteContents()
2804
- range.insertNode(replacement)
2805
- return true
2806
- }
2807
-
2808
- function getDocxPreviewClassName(styleId = '') {
2809
- return `docx_${String(styleId || '').replace(/[ .]+/g, '-').replace(/[&]+/g, 'and').toLowerCase()}`
2810
- }
2811
-
2812
- function extractHeadingStyleLevelMap(zip) {
2813
- const stylesXml = zip.file('word/styles.xml')?.asText?.() || ''
2814
- const result = {}
2815
-
2816
- for (const match of stylesXml.matchAll(/<w:style\b[\s\S]*?<\/w:style>/g)) {
2817
- const styleXml = match[0]
2818
- const type = styleXml.match(/w:type="([^"]+)"/)?.[1] || ''
2819
-
2820
- if (type !== 'paragraph') {
2821
- continue
2822
- }
2823
-
2824
- const styleId = styleXml.match(/w:styleId="([^"]+)"/)?.[1] || ''
2825
- const outlineLevel = styleXml.match(/<w:outlineLvl w:val="([^"]+)"/)?.[1]
2826
-
2827
- if (!styleId || outlineLevel === undefined) {
2828
- continue
2829
- }
2830
-
2831
- const level = Number(outlineLevel) + 1
2832
-
2833
- if (level >= 1 && level <= 3) {
2834
- result[getDocxPreviewClassName(styleId)] = level
2835
- }
2836
- }
2837
-
2838
- return result
2839
- }
2840
-
2841
- function extractHeadingStyleLevelById(zip) {
2842
- const stylesXml = zip.file('word/styles.xml')?.asText?.() || ''
2843
- const result = {}
2844
-
2845
- for (const match of stylesXml.matchAll(/<w:style\b[\s\S]*?<\/w:style>/g)) {
2846
- const styleXml = match[0]
2847
- const type = styleXml.match(/w:type="([^"]+)"/)?.[1] || ''
2848
-
2849
- if (type !== 'paragraph') {
2850
- continue
2851
- }
2852
-
2853
- const styleId = styleXml.match(/w:styleId="([^"]+)"/)?.[1] || ''
2854
- const outlineLevel = styleXml.match(/<w:outlineLvl w:val="([^"]+)"/)?.[1]
2855
-
2856
- if (!styleId || outlineLevel === undefined) {
2857
- continue
2858
- }
2859
-
2860
- const level = Number(outlineLevel) + 1
2861
-
2862
- if (level >= 1 && level <= 3) {
2863
- result[styleId] = level
2864
- }
2865
- }
2866
-
2867
- return result
2868
- }
2869
-
2870
- function extractWordStyleContext(zip, documentXml = '') {
2871
- const stylesXml = zip.file('word/styles.xml')?.asText?.() || ''
2872
- const headingStyleIds = {}
2873
- let bodyStyleId = ''
2874
-
2875
- for (const match of stylesXml.matchAll(/<w:style\b[\s\S]*?<\/w:style>/g)) {
2876
- const styleXml = match[0]
2877
- const type = styleXml.match(/w:type="([^"]+)"/)?.[1] || ''
2878
-
2879
- if (type !== 'paragraph') {
2880
- continue
2881
- }
2882
-
2883
- const styleId = styleXml.match(/w:styleId="([^"]+)"/)?.[1] || ''
2884
- const styleName = styleXml.match(/<w:name w:val="([^"]+)"/)?.[1] || ''
2885
- const outlineLevel = styleXml.match(/<w:outlineLvl w:val="([^"]+)"/)?.[1]
2886
-
2887
- if (styleId && outlineLevel !== undefined) {
2888
- const level = Number(outlineLevel) + 1
2889
-
2890
- if (level >= 1 && level <= 3) {
2891
- headingStyleIds[level] = styleId
2892
- }
2893
- }
2894
-
2895
- if (!bodyStyleId && /^normal$/i.test(styleName)) {
2896
- bodyStyleId = styleId
2897
- }
2898
- }
2899
-
2900
- const headingStyleLevelById = extractHeadingStyleLevelById(zip)
2901
- const editableHeadingCounters = {}
2902
- const counters = [0, 0, 0]
2903
-
2904
- for (const match of documentXml.matchAll(/<w:p\b[\s\S]*?<\/w:p>/g)) {
2905
- const paragraphXml = match[0]
2906
- const styleId = paragraphXml.match(/<w:pStyle w:val="([^"]+)"/)?.[1] || ''
2907
- const level = headingStyleLevelById[styleId]
2908
- const paragraphText = getParagraphText(paragraphXml)
2909
-
2910
- if (paragraphText.includes(EDITOR_MARKER_START)) {
2911
- const descriptorList = extractEditableMarkerDescriptors(paragraphXml)
2912
-
2913
- descriptorList.forEach((descriptor) => {
2914
- if (descriptor.isValid && descriptor.fieldKey && !editableHeadingCounters[descriptor.fieldKey]) {
2915
- editableHeadingCounters[descriptor.fieldKey] = [...counters]
2916
- }
2917
- })
2918
- }
2919
-
2920
- if (level >= 1 && level <= 3) {
2921
- counters[level - 1] += 1
2922
-
2923
- for (let index = level; index < counters.length; index += 1) {
2924
- counters[index] = 0
2925
- }
2926
- }
2927
- }
2928
-
2929
- return {
2930
- headingStyleIds,
2931
- bodyStyleId,
2932
- editableHeadingCounters
2933
- }
2934
- }
2935
-
2936
- function normalizeOutlineText(text = '') {
2937
- return String(text || '').replace(/\s+/g, '')
2938
- }
2939
-
2940
- function getParagraphText(paragraphXml = '') {
2941
- return [...paragraphXml.matchAll(/<w:t[^>]*>([\s\S]*?)<\/w:t>/g)]
2942
- .map((match) => decodeXmlText(match[1]))
2943
- .join('')
2944
- .trim()
2945
- .replace(/\s+/g, ' ')
2946
- }
2947
-
2948
- function extractDocumentHeadingList(zip, documentXml = '') {
2949
- const styleLevelMap = extractHeadingStyleLevelById(zip)
2950
- const result = []
2951
-
2952
- for (const match of documentXml.matchAll(/<w:p\b[\s\S]*?<\/w:p>/g)) {
2953
- const paragraphXml = match[0]
2954
- const styleId = paragraphXml.match(/<w:pStyle w:val="([^"]+)"/)?.[1] || ''
2955
- const level = styleLevelMap[styleId]
2956
-
2957
- if (!level) {
2958
- continue
2959
- }
2960
-
2961
- const text = getParagraphText(paragraphXml)
2962
-
2963
- if (!text || text.includes(EDITOR_MARKER_START) || text.includes(EDITOR_MARKER_END)) {
2964
- continue
2965
- }
2966
-
2967
- result.push({
2968
- level,
2969
- text,
2970
- normalizedText: normalizeOutlineText(text)
2971
- })
2972
- }
2973
-
2974
- return result
2975
- }
2976
-
2977
- function getOutlineLevel(element) {
2978
- if (!element) {
2979
- return 0
2980
- }
2981
-
2982
- for (const className of Array.from(element.classList || [])) {
2983
- const level = headingStyleLevelMap.value[className]
2984
-
2985
- if (level >= 1 && level <= 3) {
2986
- return level
2987
- }
2988
- }
2989
-
2990
- const tagName = element.tagName?.toLowerCase?.() || ''
2991
-
2992
- if (/^h[1-3]$/.test(tagName)) {
2993
- return Number(tagName.slice(1))
2994
- }
2995
-
2996
- return 0
2997
- }
2998
-
2999
- function isVisibleOutlineCandidate(element) {
3000
- const style = window.getComputedStyle(element)
3001
- const rect = element.getBoundingClientRect()
3002
- return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
3003
- }
3004
-
3005
- function getCandidateNormalizedText(element) {
3006
- return normalizeOutlineText(element?.textContent || '')
3007
- }
3008
-
3009
- function findHeadingElement(heading, candidates, searchStartIndex = 0) {
3010
- const headingText = heading.normalizedText
3011
-
3012
- if (!headingText) {
3013
- return { element: null, index: searchStartIndex }
3014
- }
3015
-
3016
- const exactMatchedIndex = candidates.findIndex((element, index) => {
3017
- return index >= searchStartIndex && getCandidateNormalizedText(element) === headingText
3018
- })
3019
-
3020
- if (exactMatchedIndex >= 0) {
3021
- return {
3022
- element: candidates[exactMatchedIndex],
3023
- index: exactMatchedIndex
3024
- }
3025
- }
3026
-
3027
- const containingMatches = candidates
3028
- .map((element, index) => ({
3029
- element,
3030
- index,
3031
- text: getCandidateNormalizedText(element)
3032
- }))
3033
- .filter((item) => {
3034
- return item.index >= searchStartIndex
3035
- && item.text.includes(headingText)
3036
- && item.text.length <= headingText.length + 12
3037
- })
3038
- .sort((first, second) => {
3039
- return first.text.length - second.text.length || first.index - second.index
3040
- })
3041
-
3042
- if (containingMatches.length) {
3043
- return {
3044
- element: containingMatches[0].element,
3045
- index: containingMatches[0].index
3046
- }
3047
- }
3048
-
3049
- const looseMatches = candidates
3050
- .map((element, index) => ({
3051
- element,
3052
- index,
3053
- text: getCandidateNormalizedText(element)
3054
- }))
3055
- .filter((item) => {
3056
- return item.index >= searchStartIndex && item.text.includes(headingText)
3057
- })
3058
- .sort((first, second) => {
3059
- return first.text.length - second.text.length || first.index - second.index
3060
- })
3061
-
3062
- if (looseMatches.length) {
3063
- return {
3064
- element: looseMatches[0].element,
3065
- index: looseMatches[0].index
3066
- }
3067
- }
3068
-
3069
- return { element: null, index: searchStartIndex }
3070
- }
3071
-
3072
- function getOutlineCandidateElements() {
3073
- const root = previewRef.value?.querySelector('.docx') || previewRef.value
3074
-
3075
- if (!root) {
3076
- return []
3077
- }
3078
-
3079
- const candidates = Array.from(root.querySelectorAll('h1, h2, h3, p, div, span'))
3080
- .filter((element) => {
3081
- if (element === root || element.closest('table') || !isVisibleOutlineCandidate(element)) {
3082
- return false
3083
- }
3084
-
3085
- const text = getCandidateNormalizedText(element)
3086
- return text.length > 0 && text.length <= 120
3087
- })
3088
-
3089
- return candidates.filter((element) => {
3090
- const text = getCandidateNormalizedText(element)
3091
- const childWithSameText = Array.from(element.children || []).some((child) => {
3092
- return candidates.includes(child) && getCandidateNormalizedText(child) === text
3093
- })
3094
-
3095
- return !childWithSameText
3096
- })
3097
- }
3098
-
3099
- function buildDocumentOutline() {
3100
- if (!previewRef.value) {
3101
- documentOutline.value = []
3102
- return
3103
- }
3104
-
3105
- const candidates = getOutlineCandidateElements()
3106
- const seenTexts = new Set()
3107
- const outlineList = []
3108
- const templateHeadings = documentHeadingList.value || []
3109
-
3110
- if (templateHeadings.length) {
3111
- let searchStartIndex = 0
3112
-
3113
- templateHeadings.forEach((heading) => {
3114
- const seenKey = `${heading.level}-${heading.normalizedText}`
3115
-
3116
- if (seenTexts.has(seenKey)) {
3117
- return
3118
- }
3119
-
3120
- const { element, index } = findHeadingElement(heading, candidates, searchStartIndex)
3121
-
3122
- if (element) {
3123
- searchStartIndex = index + 1
3124
- }
3125
-
3126
- seenTexts.add(seenKey)
3127
- const id = `docx-outline-${outlineList.length + 1}`
3128
-
3129
- if (element) {
3130
- element.dataset.outlineId = id
3131
- }
3132
-
3133
- outlineList.push({
3134
- id,
3135
- level: heading.level,
3136
- text: heading.text,
3137
- element
3138
- })
3139
- })
3140
-
3141
- documentOutline.value = outlineList
3142
- activeOutlineId.value = outlineList[0]?.id || ''
3143
- updateActiveOutlineByScroll()
3144
- return
3145
- }
3146
-
3147
- candidates.forEach((element) => {
3148
- const text = (element.textContent || '').trim().replace(/\s+/g, ' ')
3149
-
3150
- if (!text || text.length > 80 || text.includes(EDITOR_MARKER_START) || text.includes(EDITOR_MARKER_END)) {
3151
- return
3152
- }
3153
-
3154
- const level = getOutlineLevel(element)
3155
-
3156
- if (!level) {
3157
- return
3158
- }
3159
-
3160
- const seenKey = `${level}-${text}`
3161
-
3162
- if (seenTexts.has(seenKey)) {
3163
- return
3164
- }
3165
-
3166
- seenTexts.add(seenKey)
3167
- const id = `docx-outline-${outlineList.length + 1}`
3168
- element.dataset.outlineId = id
3169
- outlineList.push({
3170
- id,
3171
- level,
3172
- text,
3173
- element
3174
- })
3175
- })
3176
-
3177
- documentOutline.value = outlineList
3178
- activeOutlineId.value = outlineList[0]?.id || ''
3179
- updateActiveOutlineByScroll()
3180
- }
3181
-
3182
- function getDocumentScrollContainer() {
3183
- return docxScrollRef.value || previewRef.value?.parentElement || null
3184
- }
3185
-
3186
- function resolveOutlineElement(item) {
3187
- if (!item || !previewRef.value) {
3188
- return null
3189
- }
3190
-
3191
- if (item.element?.isConnected) {
3192
- return item.element
3193
- }
3194
-
3195
- const elementById = previewRef.value.querySelector(`[data-outline-id="${item.id}"]`)
3196
-
3197
- if (elementById) {
3198
- item.element = elementById
3199
- return elementById
3200
- }
3201
-
3202
- const candidates = getOutlineCandidateElements()
3203
- const matched = findHeadingElement({
3204
- normalizedText: normalizeOutlineText(item.text)
3205
- }, candidates, 0).element
3206
-
3207
- if (matched) {
3208
- matched.dataset.outlineId = item.id
3209
- item.element = matched
3210
- }
3211
-
3212
- return matched
3213
- }
3214
-
3215
- function getElementTopInScrollContainer(element, container) {
3216
- const elementRect = element.getBoundingClientRect()
3217
- const containerRect = container.getBoundingClientRect()
3218
- return elementRect.top - containerRect.top + container.scrollTop
3219
- }
3220
-
3221
- function scrollToOutlineItem(item) {
3222
- const container = getDocumentScrollContainer()
3223
- const element = resolveOutlineElement(item)
3224
-
3225
- if (!element || !container) {
3226
- return
3227
- }
3228
-
3229
- activeOutlineId.value = item.id
3230
- container.scrollTo({
3231
- top: Math.max(getElementTopInScrollContainer(element, container) - 24, 0),
3232
- behavior: 'smooth'
3233
- })
3234
- }
3235
-
3236
- function updateActiveOutlineByScroll() {
3237
- const container = getDocumentScrollContainer()
3238
-
3239
- if (!container || !documentOutline.value.length) {
3240
- return
3241
- }
3242
-
3243
- const containerRect = container.getBoundingClientRect()
3244
- const anchorTop = containerRect.top + 96
3245
- let activeItem = null
3246
- let firstBelowItem = null
3247
-
3248
- documentOutline.value.forEach((item) => {
3249
- const element = resolveOutlineElement(item)
3250
-
3251
- if (!element) {
3252
- return
3253
- }
3254
-
3255
- const rect = element.getBoundingClientRect()
3256
-
3257
- if (rect.top <= anchorTop) {
3258
- activeItem = item
3259
- return
3260
- }
3261
-
3262
- if (!firstBelowItem) {
3263
- firstBelowItem = item
3264
- }
3265
- })
3266
-
3267
- const nextActiveId = activeItem?.id || firstBelowItem?.id || documentOutline.value[0]?.id || ''
3268
-
3269
- if (nextActiveId && activeOutlineId.value !== nextActiveId) {
3270
- activeOutlineId.value = nextActiveId
3271
- }
3272
- }
3273
-
3274
- function handleDocumentScroll() {
3275
- if (outlineScrollRaf) {
3276
- cancelAnimationFrame(outlineScrollRaf)
3277
- }
3278
-
3279
- outlineScrollRaf = requestAnimationFrame(() => {
3280
- outlineScrollRaf = 0
3281
- updateActiveOutlineByScroll()
3282
- })
3283
- }
3284
-
3285
- function decorateEditableRegions() {
3286
- if (props.renderType !== 'edit' || !previewRef.value || !editableMarkerDescriptors.value.length) {
3287
- return
3288
- }
3289
-
3290
- clearEditableDecorations()
3291
-
3292
- const textNodes = getAllTextNodes(previewRef.value)
3293
- const combinedText = textNodes.map((node) => node.textContent || '').join('')
3294
- const markerPairs = [...combinedText.matchAll(getRenderedEditableMarkerRegExp())]
3295
- .map((match, index) => ({
3296
- match,
3297
- descriptor: editableMarkerDescriptors.value[index]
3298
- }))
3299
- .filter((item) => item.descriptor?.isValid && item.descriptor?.fieldKey)
3300
- const replacedTables = new WeakSet()
3301
-
3302
- for (let index = markerPairs.length - 1; index >= 0; index -= 1) {
3303
- const { match, descriptor } = markerPairs[index]
3304
- const fullText = match[0] || ''
3305
- const fieldKey = descriptor.fieldKey
3306
-
3307
- if (!fieldKey) {
3308
- continue
3309
- }
3310
-
3311
- const rangeStart = match.index || 0
3312
- const rangeEnd = rangeStart + fullText.length
3313
- const fallbackText = (match[1] || '').trim()
3314
-
3315
- const startPosition = getNodePositionByOffset(textNodes, rangeStart, 'start')
3316
- const endPosition = getNodePositionByOffset(textNodes, rangeEnd, 'end')
3317
-
3318
- if (!startPosition.node || !endPosition.node) {
3319
- continue
3320
- }
3321
-
3322
- if (!startPosition.node.isConnected || !endPosition.node.isConnected) {
3323
- continue
3324
- }
3325
-
3326
- const storedRichItem = getStoredRichTextItem(fieldKey)
3327
- const storedTableHtml = hasRichTextTableBlock(storedRichItem)
3328
- ? getRichTextFirstTableHtml(storedRichItem)
3329
- : ''
3330
-
3331
- if (storedTableHtml) {
3332
- editableFieldTemplateHtmlMap[fieldKey] = storedTableHtml
3333
- const range = document.createRange()
3334
- range.setStart(startPosition.node, startPosition.offset)
3335
- range.setEnd(endPosition.node, endPosition.offset)
3336
- range.deleteContents()
3337
- range.insertNode(createBlockEditableAnchor(fieldKey, fallbackText, getClosestParagraphElement(startPosition.node)))
3338
- continue
3339
- }
3340
-
3341
- const containedTableHtml = getRangeContainedTableHtml(startPosition, endPosition)
3342
-
3343
- if (containedTableHtml) {
3344
- editableFieldTemplateHtmlMap[fieldKey] = containedTableHtml
3345
- const range = document.createRange()
3346
- range.setStart(startPosition.node, startPosition.offset)
3347
- range.setEnd(endPosition.node, endPosition.offset)
3348
- range.deleteContents()
3349
- range.insertNode(createBlockEditableAnchor(fieldKey, fallbackText, getClosestParagraphElement(startPosition.node)))
3350
- continue
3351
- }
3352
-
3353
- const sharedTable = getSharedClosestTable(startPosition.node, endPosition.node)
3354
-
3355
- if (sharedTable && !replacedTables.has(sharedTable)) {
3356
- editableFieldTemplateHtmlMap[fieldKey] = getCleanOuterHtml(sharedTable)
3357
- sharedTable.replaceWith(createBlockEditableAnchor(fieldKey, fallbackText, getClosestParagraphElement(startPosition.node)))
3358
- replacedTables.add(sharedTable)
3359
- continue
3360
- }
3361
-
3362
- const range = document.createRange()
3363
- const isStandaloneBlock = isStandaloneParagraphPlaceholder(
3364
- startPosition,
3365
- endPosition,
3366
- fullText,
3367
- combinedText,
3368
- rangeStart,
3369
- rangeEnd
3370
- )
3371
-
3372
- if (isStandaloneBlock) {
3373
- const paragraphElement = getClosestParagraphElement(startPosition.node)
3374
- replaceEditableParagraphRange(
3375
- startPosition.node,
3376
- endPosition.node,
3377
- createBlockEditableAnchor(fieldKey, fallbackText, paragraphElement)
3378
- )
3379
- continue
3380
- }
3381
-
3382
- range.setStart(startPosition.node, startPosition.offset)
3383
- range.setEnd(endPosition.node, endPosition.offset)
3384
- range.deleteContents()
3385
- range.insertNode(createInlineEditableAnchor(fieldKey, fallbackText))
3386
- }
3387
- }
3388
-
3389
- function openFieldEditor(fieldKey) {
3390
- const path = editableFieldPathMap.value[fieldKey] || ''
3391
- const plainText = getValueByPath(editableRenderData.value, path) ?? ''
3392
-
3393
- activeEditableField.value = {
3394
- key: fieldKey,
3395
- path
3396
- }
3397
-
3398
- editor.value?.commands.setContent(getFieldStoredHtml(fieldKey, plainText), false)
3399
- editDialogVisible.value = true
3400
- }
3401
-
3402
- function saveActiveField() {
3403
- if (!activeEditableField.value || !editor.value) {
3404
- return
3405
- }
3406
-
3407
- const fieldKey = activeEditableField.value.key
3408
- const html = normalizeRichTextHtmlForStorage(editor.value.getHTML())
3409
- const json = editor.value.getJSON()
3410
- const plainText = htmlToDocxText(html)
3411
- const path = activeEditableField.value.path
3412
-
3413
- editableRenderData.value.__editableRichTextMap[fieldKey] = buildBackendRichTextValue(
3414
- fieldKey,
3415
- path,
3416
- html,
3417
- json,
3418
- plainText
3419
- )
3420
-
3421
- setValueByPath(editableRenderData.value, path, plainText)
3422
- editableFieldContentMap[fieldKey] = html
3423
-
3424
- emit('render-data-change', deepClone(editableRenderData.value))
3425
- emit('save-payload-change', buildSavePayload())
3426
- editDialogVisible.value = false
3427
- renderPreview()
3428
- }
3429
-
3430
- function onDialogClosed() {
3431
- activeEditableField.value = null
3432
- }
3433
-
3434
- async function renderPreview() {
3435
- const currentToken = ++renderToken
3436
- isLoading.value = true
3437
- errorMessage.value = ''
3438
-
3439
- try {
3440
- const templateBuffer = await loadTemplate()
3441
- const { zip, documentXml } = createTemplateZip(templateBuffer, {
3442
- stripEditorMarkers: props.renderType !== 'edit'
3443
- })
3444
-
3445
- headingStyleLevelMap.value = extractHeadingStyleLevelMap(zip)
3446
- documentHeadingList.value = extractDocumentHeadingList(zip, documentXml)
3447
- wordStyleContext.value = extractWordStyleContext(zip, documentXml)
3448
- editableMarkerDescriptors.value = extractEditableMarkerDescriptors(documentXml)
3449
- editableFieldKeys.value = editableMarkerDescriptors.value
3450
- .filter((descriptor) => descriptor.isValid)
3451
- .map((descriptor) => descriptor.fieldKey)
3452
- editableFieldPathMap.value = buildScalarPathMap(editableRenderData.value)
3453
- editableFieldKeys.value.forEach((fieldKey) => {
3454
- if (!editableFieldPathMap.value[fieldKey]) {
3455
- editableFieldPathMap.value[fieldKey] = fieldKey
3456
- }
3457
- })
3458
- const renderedBlob = await createRenderedDocxBlob({
3459
- stripEditorMarkers: props.renderType !== 'edit',
3460
- applyRichText: props.renderType !== 'edit'
3461
- })
3462
-
3463
- await nextTick()
3464
-
3465
- if (!previewRef.value || currentToken !== renderToken) {
3466
- return
3467
- }
3468
-
3469
- clearPreview()
3470
-
3471
- await docxPreview.renderAsync(renderedBlob, previewRef.value, previewRef.value, {
3472
- className: 'docx-preview',
3473
- inWrapper: true,
3474
- breakPages: true,
3475
- ignoreWidth: false,
3476
- ignoreHeight: false,
3477
- ignoreLastRenderedPageBreak: false,
3478
- renderHeaders: true,
3479
- renderFooters: true,
3480
- useBase64URL: true
3481
- })
3482
-
3483
- await nextTick()
3484
-
3485
- if (props.renderType === 'edit') {
3486
- decorateEditableRegions()
3487
- } else {
3488
- cleanupEditorMarkers()
3489
- }
3490
-
3491
- buildDocumentOutline()
3492
- } catch (error) {
3493
- if (currentToken !== renderToken) {
3494
- return
3495
- }
3496
-
3497
- console.error('Docx preview render failed:', error)
3498
- errorMessage.value = '文档预览生成失败,请检查模板变量或模板文件。'
3499
- ElMessage.error(errorMessage.value)
3500
- } finally {
3501
- if (currentToken === renderToken) {
3502
- isLoading.value = false
3503
- }
3504
- }
3505
- }
3506
-
3507
- async function exportDocx() {
3508
- try {
3509
- const renderedBlob = await createRenderedDocxBlob({
3510
- stripEditorMarkers: true
3511
- })
3512
- downloadBlob(renderedBlob, getExportFileName())
3513
- return {
3514
- success: true,
3515
- fileName: getExportFileName()
3516
- }
3517
- } catch (error) {
3518
- console.error('Docx export failed:', error)
3519
- ElMessage.error('导出失败,请检查模板变量或模板文件。')
3520
- return {
3521
- success: false,
3522
- error
3523
- }
3524
- }
3525
- }
3526
-
3527
- function isHeadingActive(level) {
3528
- return !!editor.value?.isActive('heading', { level })
3529
- }
3530
-
3531
- function isTextAlignActive(alignment) {
3532
- return !!editor.value?.isActive({ textAlign: alignment })
3533
- }
3534
-
3535
- function setHeading(level) {
3536
- editor.value?.chain().focus().toggleHeading({ level }).run()
3537
- }
3538
-
3539
- function setParagraph() {
3540
- editor.value?.chain().focus().setParagraph().updateAttributes('paragraph', {
3541
- textIndentLevel: 0
3542
- }).setTextAlign('left').run()
3543
- }
3544
-
3545
- function toggleBold() {
3546
- editor.value?.chain().focus().toggleBold().run()
3547
- }
3548
-
3549
- function getCurrentEditorFormat() {
3550
- if (!editor.value) {
3551
- return null
3552
- }
3553
-
3554
- const headingLevel = headingActions.find((item) => editor.value.isActive('heading', { level: item.level }))?.level || 0
3555
-
3556
- return {
3557
- headingLevel,
3558
- bold: Boolean(editor.value.isActive('bold')),
3559
- textColor: editor.value.getAttributes('textStyle').color || '',
3560
- backgroundColor: editor.value.getAttributes('highlight').color || '',
3561
- align: ['center', 'right'].find((alignment) => editor.value.isActive({ textAlign: alignment })) || 'left',
3562
- textIndentLevel: getEditorIndentLevel(editor.value)
3563
- }
3564
- }
3565
-
3566
- function applyEditorFormat(format) {
3567
- if (!editor.value || !format) {
3568
- return
3569
- }
3570
-
3571
- let command = editor.value.chain().focus().unsetAllMarks()
3572
-
3573
- if (format.headingLevel) {
3574
- command = command.setHeading({ level: format.headingLevel })
3575
- } else {
3576
- command = command.setParagraph()
3577
- }
3578
-
3579
- command = command
3580
- .setTextAlign(format.align || 'left')
3581
- .updateAttributes(format.headingLevel ? 'heading' : 'paragraph', {
3582
- textIndentLevel: Number(format.textIndentLevel || 0)
3583
- })
3584
-
3585
- if (format.bold) {
3586
- command = command.toggleBold()
3587
- }
3588
-
3589
- if (format.textColor) {
3590
- command = command.setColor(format.textColor)
3591
- }
3592
-
3593
- if (format.backgroundColor) {
3594
- command = command.setHighlight({ color: format.backgroundColor })
3595
- }
3596
-
3597
- command.run()
3598
- }
3599
-
3600
- function toggleFormatPainter() {
3601
- if (!copiedFormat.value) {
3602
- copiedFormat.value = getCurrentEditorFormat()
3603
- ElMessage.success('已复制当前格式,请选中目标内容后再次点击格式刷应用。')
3604
- return
3605
- }
3606
-
3607
- applyEditorFormat(copiedFormat.value)
3608
- copiedFormat.value = null
3609
- }
3610
-
3611
- function clearEditorFormat() {
3612
- if (!editor.value) {
3613
- return
3614
- }
3615
-
3616
- editor.value
3617
- .chain()
3618
- .focus()
3619
- .unsetAllMarks()
3620
- .setParagraph()
3621
- .setTextAlign('left')
3622
- .updateAttributes('paragraph', {
3623
- textIndentLevel: 0
3624
- })
3625
- .run()
3626
- }
3627
-
3628
- function onTablePickerHover(rows, cols) {
3629
- tablePickerHover.value = { rows, cols }
3630
- }
3631
-
3632
- function insertTable(rows, cols) {
3633
- editor.value?.chain().focus().insertTable({
3634
- rows,
3635
- cols,
3636
- withHeaderRow: true
3637
- }).run()
3638
- tablePickerVisible.value = false
3639
- }
3640
-
3641
- function addTableRow() {
3642
- editor.value?.chain().focus().addRowAfter().run()
3643
- }
3644
-
3645
- function deleteTableRow() {
3646
- editor.value?.chain().focus().deleteRow().run()
3647
- }
3648
-
3649
- function addTableColumn() {
3650
- editor.value?.chain().focus().addColumnAfter().run()
3651
- }
3652
-
3653
- function deleteTableColumn() {
3654
- editor.value?.chain().focus().deleteColumn().run()
3655
- }
3656
-
3657
- function removeTable() {
3658
- editor.value?.chain().focus().deleteTable().run()
3659
- }
3660
-
3661
- function hideTableContextMenu() {
3662
- tableContextMenuVisible.value = false
3663
- }
3664
-
3665
- function isTableCellSelection(selection) {
3666
- return selection instanceof CellSelection
3667
- }
3668
-
3669
- function shouldKeepTableCellSelection(tableCell) {
3670
- const selection = editor.value?.state.selection
3671
-
3672
- return isTableCellSelection(selection) && tableCell.classList.contains('selectedCell')
3673
- }
3674
-
3675
- function setEditorSelectionFromEvent(event) {
3676
- const view = editor.value?.view
3677
- const position = view?.posAtCoords({
3678
- left: event.clientX,
3679
- top: event.clientY
3680
- })
3681
-
3682
- if (position?.pos) {
3683
- editor.value.chain().focus().setTextSelection(position.pos).run()
3684
- return
3685
- }
3686
-
3687
- editor.value?.commands.focus()
3688
- }
3689
-
3690
- function onEditorMouseDownCapture(event) {
3691
- if (event.button !== 2) {
3692
- return
3693
- }
3694
-
3695
- const tableCell = event.target?.closest?.('td, th')
3696
-
3697
- if (!tableCell || !shouldKeepTableCellSelection(tableCell)) {
3698
- return
3699
- }
3700
-
3701
- event.preventDefault()
3702
- event.stopPropagation()
3703
- event.stopImmediatePropagation?.()
3704
- }
3705
-
3706
- function onEditorContextMenu(event) {
3707
- const tableCell = event.target?.closest?.('td, th')
3708
-
3709
- if (!tableCell || !editor.value) {
3710
- hideTableContextMenu()
3711
- return
3712
- }
3713
-
3714
- event.preventDefault()
3715
-
3716
- if (!shouldKeepTableCellSelection(tableCell)) {
3717
- setEditorSelectionFromEvent(event)
3718
- }
3719
-
3720
- const editDialogRect = event.currentTarget.closest('.edit-dialog')?.getBoundingClientRect() || event.currentTarget.getBoundingClientRect()
3721
- tableContextMenuPosition.value = {
3722
- x: event.clientX - editDialogRect.left,
3723
- y: event.clientY - editDialogRect.top
3724
- }
3725
- tableContextMenuVisible.value = true
3726
- }
3727
-
3728
- function getTableCommandUnavailableMessage(commandName) {
3729
- if (commandName === 'mergeCells') {
3730
- return '请先拖选两个或多个连续单元格,再执行合并。'
3731
- }
3732
-
3733
- if (commandName === 'splitCell') {
3734
- return '当前单元格没有可拆分的合并行或合并列。'
3735
- }
3736
-
3737
- return ''
3738
- }
3739
-
3740
- function runTableMenuCommand(commandName) {
3741
- const instance = editor.value
3742
-
3743
- if (!instance) {
3744
- return
3745
- }
3746
-
3747
- const commandMap = {
3748
- mergeCells: () => instance.chain().focus().mergeCells().run(),
3749
- splitCell: () => instance.chain().focus().splitCell().run(),
3750
- addRowBefore: () => instance.chain().focus().addRowBefore().run(),
3751
- addRowAfter: () => instance.chain().focus().addRowAfter().run(),
3752
- deleteRow: () => instance.chain().focus().deleteRow().run(),
3753
- addColumnBefore: () => instance.chain().focus().addColumnBefore().run(),
3754
- addColumnAfter: () => instance.chain().focus().addColumnAfter().run(),
3755
- deleteColumn: () => instance.chain().focus().deleteColumn().run(),
3756
- toggleHeaderRow: () => instance.chain().focus().toggleHeaderRow().run(),
3757
- toggleHeaderColumn: () => instance.chain().focus().toggleHeaderColumn().run(),
3758
- deleteTable: () => instance.chain().focus().deleteTable().run()
3759
- }
3760
-
3761
- const isCommandAvailable = instance.can()?.[commandName]?.()
3762
-
3763
- if (isCommandAvailable === false) {
3764
- const message = getTableCommandUnavailableMessage(commandName)
3765
-
3766
- if (message) {
3767
- ElMessage.warning(message)
3768
- }
3769
-
3770
- hideTableContextMenu()
3771
- return
3772
- }
3773
-
3774
- const commandResult = commandMap[commandName]?.()
3775
-
3776
- if (commandResult === false) {
3777
- const message = getTableCommandUnavailableMessage(commandName)
3778
-
3779
- if (message) {
3780
- ElMessage.warning(message)
3781
- }
3782
- }
3783
-
3784
- hideTableContextMenu()
3785
- }
3786
-
3787
- function setTextAlign(alignment) {
3788
- editor.value?.chain().focus().setTextAlign(alignment).run()
3789
- }
3790
-
3791
- function rememberEditorSelection() {
3792
- const selection = editor.value?.state.selection
3793
-
3794
- if (!selection) {
3795
- return
3796
- }
3797
-
3798
- pendingEditorSelection.value = {
3799
- from: selection.from,
3800
- to: selection.to
3801
- }
3802
- }
3803
-
3804
- function getEditorCommandWithSelection() {
3805
- const command = editor.value?.chain().focus()
3806
-
3807
- if (!command) {
3808
- return null
3809
- }
3810
-
3811
- if (pendingEditorSelection.value) {
3812
- return command.setTextSelection(pendingEditorSelection.value)
3813
- }
3814
-
3815
- return command
3816
- }
3817
-
3818
- function openTextColorPicker() {
3819
- rememberEditorSelection()
3820
- textColorInputRef.value?.click()
3821
- }
3822
-
3823
- function openHighlightColorPicker() {
3824
- rememberEditorSelection()
3825
- highlightColorInputRef.value?.click()
3826
- }
3827
-
3828
- function onTextColorChange(event) {
3829
- const color = event.target.value || DEFAULT_TEXT_COLOR
3830
- currentTextColor.value = color
3831
- getEditorCommandWithSelection()?.setColor(color).run()
3832
- pendingEditorSelection.value = null
3833
- }
3834
-
3835
- function onHighlightColorChange(event) {
3836
- const color = event.target.value || DEFAULT_HIGHLIGHT_COLOR
3837
- currentHighlightColor.value = color
3838
- getEditorCommandWithSelection()?.setHighlight({ color }).run()
3839
- pendingEditorSelection.value = null
3840
- }
3841
-
3842
- function clearHighlightColor() {
3843
- rememberEditorSelection()
3844
- getEditorCommandWithSelection()?.unsetHighlight().run()
3845
- currentHighlightColor.value = DEFAULT_HIGHLIGHT_COLOR
3846
- pendingEditorSelection.value = null
3847
- }
3848
-
3849
- function undoEditor() {
3850
- editor.value?.chain().focus().undo().run()
3851
- }
3852
-
3853
- function redoEditor() {
3854
- editor.value?.chain().focus().redo().run()
3855
- }
3856
-
3857
- watch(
3858
- () => props.renderData,
3859
- (value) => {
3860
- editableRenderData.value = createEditableRenderData(value)
3861
- renderPreview()
3862
- },
3863
- {
3864
- deep: true,
3865
- immediate: true
3866
- }
3867
- )
3868
-
3869
- watch(
3870
- () => props.renderType,
3871
- () => {
3872
- renderPreview()
3873
- }
3874
- )
3875
-
3876
- defineExpose({
3877
- exportDocx,
3878
- getRenderData: () => deepClone(editableRenderData.value),
3879
- getSavePayload: () => buildSavePayload(),
3880
- getEditorHtml: () => editor.value?.getHTML() || '',
3881
- getEditorJson: () => editor.value?.getJSON() || null
3882
- })
3883
-
3884
- onBeforeUnmount(() => {
3885
- renderToken += 1
3886
- if (outlineScrollRaf) {
3887
- cancelAnimationFrame(outlineScrollRaf)
3888
- outlineScrollRaf = 0
3889
- }
3890
- clearPreview()
3891
- editor.value?.destroy()
3892
- })
3893
- </script>
3894
-
3895
- <style scoped lang="less">
3896
- .view-page {
3897
- height: 100%;
3898
- min-height: 0;
3899
- //position: relative;
3900
- display: flex;
3901
- justify-content: stretch;
3902
-
3903
- .edit-box,
3904
- .view-box {
3905
- width: 100%;
3906
- }
3907
-
3908
- &.has-outline {
3909
- justify-content: flex-end;
3910
- }
3911
-
3912
- &.has-outline .edit-box,
3913
- &.has-outline .view-box {
3914
- width: calc(100% - 250px);
3915
- }
3916
- }
3917
-
3918
- .document-outline {
3919
- //position: absolute;
3920
- //top: 18px;
3921
- //right: 18px;
3922
- //z-index: 8;
3923
- width: 240px;
3924
- max-height: calc(100% - 36px);
3925
- padding: 14px 12px;
3926
- overflow: auto;
3927
- border: 1px solid rgba(19, 191, 166, 0.22);
3928
- border-radius: 14px;
3929
- background: rgba(255, 255, 255, 0.92);
3930
- box-shadow: 0 14px 36px rgba(20, 32, 61, 0.16);
3931
- backdrop-filter: blur(10px);
3932
- }
3933
-
3934
- .outline-title {
3935
- margin-bottom: 8px;
3936
- padding: 0 4px 8px;
3937
- border-bottom: 1px solid #e6edf5;
3938
- color: #1f2a44;
3939
- font-size: 14px;
3940
- font-weight: 700;
3941
- }
3942
-
3943
- .outline-item {
3944
- width: 100%;
3945
- display: block;
3946
- margin: 2px 0;
3947
- padding: 7px 8px;
3948
- border: none;
3949
- border-radius: 9px;
3950
- background: transparent;
3951
- color: #4f5b72;
3952
- font-size: 13px;
3953
- line-height: 1.35;
3954
- text-align: left;
3955
- cursor: pointer;
3956
- transition: all 0.18s ease;
3957
- }
3958
-
3959
- .outline-item:hover,
3960
- .outline-item.is-active {
3961
- color: #008f7c;
3962
- background: rgba(19, 191, 166, 0.1);
3963
- }
3964
-
3965
- .outline-item.level-2 {
3966
- padding-left: 20px;
3967
- }
3968
-
3969
- .outline-item.level-3 {
3970
- padding-left: 34px;
3971
- font-size: 12px;
3972
- color: #6f7b91;
3973
- }
3974
-
3975
- .outline-text {
3976
- display: -webkit-box;
3977
- overflow: hidden;
3978
- -webkit-line-clamp: 2;
3979
- -webkit-box-orient: vertical;
3980
- }
3981
-
3982
- .view-box,
3983
- .edit-box {
3984
- height: 100%;
3985
- min-height: 0;
3986
- overflow: auto;
3987
- padding: 0;
3988
- background: #858585;
3989
- }
3990
-
3991
- .status-box {
3992
- min-height: 240px;
3993
- display: flex;
3994
- align-items: center;
3995
- justify-content: center;
3996
- color: #606266;
3997
- font-size: 14px;
3998
- background: #fff;
3999
- border-radius: 8px;
4000
- height: 100%;
4001
- }
4002
-
4003
- .status-box.is-error {
4004
- color: #f56c6c;
4005
- }
4006
-
4007
- .docx-preview-container {
4008
- min-height: 100%;
4009
- width: 100%;
4010
- }
4011
-
4012
- :deep(.docx-preview-wrapper) {
4013
- min-height: 100%;
4014
- width: 100%;
4015
- box-sizing: border-box;
4016
- background: #FFFFFF;
4017
- padding: 0;
4018
- .docx-preview{
4019
- width: 100%!important;
4020
- }
4021
- }
4022
-
4023
- .edit-dialog {
4024
- position: relative;
4025
- display: flex;
4026
- flex-direction: column;
4027
- gap: 12px;
4028
- }
4029
-
4030
- .edit-dialog-tip {
4031
- color: #4f5b72;
4032
- font-size: 13px;
4033
- line-height: 20px;
4034
- }
4035
-
4036
- .edit-dialog-path {
4037
- margin-left: 8px;
4038
- color: #9098a7;
4039
- }
4040
-
4041
- .editor-toolbar {
4042
- display: flex;
4043
- flex-wrap: wrap;
4044
- align-items: center;
4045
- gap: 8px;
4046
- padding: 14px;
4047
- background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
4048
- border: 1px solid #dbe6f3;
4049
- border-radius: 14px;
4050
- box-shadow: 0 10px 30px rgba(31, 42, 68, 0.06);
4051
- }
4052
-
4053
- .toolbar-btn {
4054
- width: 38px;
4055
- height: 38px;
4056
- display: inline-flex;
4057
- align-items: center;
4058
- justify-content: center;
4059
- border: 1px solid #d9e0ec;
4060
- border-radius: 11px;
4061
- background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
4062
- color: #2b3345;
4063
- cursor: pointer;
4064
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 2px 5px rgba(31, 42, 68, 0.04);
4065
- transition: all 0.16s ease;
4066
- }
4067
-
4068
- .toolbar-btn:hover {
4069
- border-color: #13bfa6;
4070
- color: #13bfa6;
4071
- background: linear-gradient(180deg, #ffffff 0%, #effdfa 100%);
4072
- box-shadow: 0 4px 12px rgba(19, 191, 166, 0.12);
4073
- }
4074
-
4075
- .toolbar-btn.is-active {
4076
- color: #13bfa6;
4077
- border-color: #13bfa6;
4078
- background: rgba(19, 191, 166, 0.1);
4079
- }
4080
-
4081
- .toolbar-btn:disabled {
4082
- cursor: not-allowed;
4083
- opacity: 0.45;
4084
- }
4085
-
4086
- .toolbar-tip-wrap {
4087
- display: inline-flex;
4088
- }
4089
-
4090
- .toolbar-divider {
4091
- width: 1px;
4092
- height: 22px;
4093
- background: #e5e9f2;
4094
- margin: 0 2px;
4095
- }
4096
-
4097
- .toolbar-svg {
4098
- width: 24px;
4099
- height: 24px;
4100
- display: block;
4101
- color: currentColor;
4102
- overflow: visible;
4103
- }
4104
-
4105
- .heading-svg {
4106
- width: 26px;
4107
- height: 26px;
4108
- }
4109
-
4110
- .svg-stroke {
4111
- fill: none;
4112
- stroke: currentColor;
4113
- stroke-width: 2;
4114
- stroke-linecap: square;
4115
- stroke-linejoin: round;
4116
- }
4117
-
4118
- .svg-stroke.thin {
4119
- stroke-width: 1.25;
4120
- }
4121
-
4122
- .svg-stroke.round {
4123
- stroke-linecap: round;
4124
- }
4125
-
4126
- .svg-stroke.muted {
4127
- color: #8c96a8;
4128
- }
4129
-
4130
- .toolbar-btn:hover .svg-stroke.muted,
4131
- .toolbar-btn.is-active .svg-stroke.muted {
4132
- color: currentColor;
4133
- }
4134
-
4135
- .svg-stroke.accent {
4136
- color: #13bfa6;
4137
- stroke-width: 2.2;
4138
- }
4139
-
4140
- .svg-stroke.danger {
4141
- color: #e47777;
4142
- stroke-width: 2.2;
4143
- }
4144
-
4145
- .svg-soft-fill {
4146
- fill: currentColor;
4147
- opacity: 0.14;
4148
- }
4149
-
4150
- .svg-color-fill {
4151
- fill: currentColor;
4152
- }
4153
-
4154
- .color-preview-stroke {
4155
- stroke-width: 2.8;
4156
- }
4157
-
4158
- .svg-heading-main,
4159
- .svg-heading-level,
4160
- .svg-bold {
4161
- fill: currentColor;
4162
- font-family: Georgia, 'Times New Roman', serif;
4163
- font-weight: 700;
4164
- }
4165
-
4166
- .svg-heading-main {
4167
- font-size: 20px;
4168
- }
4169
-
4170
- .svg-heading-level {
4171
- font-family: Arial, sans-serif;
4172
- font-size: 10px;
4173
- font-weight: 800;
4174
- }
4175
-
4176
- .svg-bold {
4177
- font-size: 18px;
4178
- }
4179
-
4180
- .color-btn {
4181
- position: relative;
4182
- width: 42px;
4183
- padding: 0;
4184
- }
4185
-
4186
- .clear-highlight-btn {
4187
- width: 38px;
4188
- }
4189
-
4190
- .table-trigger-btn {
4191
- width: 40px;
4192
- }
4193
-
4194
- .table-op-btn {
4195
- width: 44px;
4196
- overflow: visible;
4197
- }
4198
-
4199
- .hidden-color-input {
4200
- position: absolute;
4201
- opacity: 0;
4202
- pointer-events: none;
4203
- width: 0;
4204
- height: 0;
4205
- }
4206
-
4207
- .table-picker {
4208
- display: flex;
4209
- flex-direction: column;
4210
- gap: 12px;
4211
- }
4212
-
4213
- .table-picker-title {
4214
- color: #1f2a44;
4215
- font-size: 13px;
4216
- font-weight: 700;
4217
- }
4218
-
4219
- .table-picker-grid {
4220
- display: grid;
4221
- grid-template-columns: repeat(10, 1fr);
4222
- gap: 4px;
4223
- }
4224
-
4225
- .table-picker-cell {
4226
- width: 24px;
4227
- height: 24px;
4228
- border: 1px solid #c9d4e5;
4229
- border-radius: 4px;
4230
- background: #fff;
4231
- cursor: pointer;
4232
- transition: all 0.15s ease;
4233
- }
4234
-
4235
- .table-picker-cell.is-active,
4236
- .table-picker-cell:hover {
4237
- border-color: #13bfa6;
4238
- background: rgba(19, 191, 166, 0.14);
4239
- }
4240
-
4241
- .table-picker-tip {
4242
- color: #65708a;
4243
- font-size: 12px;
4244
- }
4245
-
4246
- .table-context-menu {
4247
- position: absolute;
4248
- z-index: 20;
4249
- min-width: 148px;
4250
- padding: 6px;
4251
- border: 1px solid #d8dfeb;
4252
- border-radius: 8px;
4253
- background: #fff;
4254
- box-shadow: 0 16px 36px rgba(31, 42, 68, 0.18);
4255
- }
4256
-
4257
- .table-context-menu button {
4258
- width: 100%;
4259
- height: 30px;
4260
- display: flex;
4261
- align-items: center;
4262
- padding: 0 10px;
4263
- border: none;
4264
- border-radius: 6px;
4265
- background: transparent;
4266
- color: #2b3345;
4267
- font-size: 13px;
4268
- text-align: left;
4269
- cursor: pointer;
4270
- }
4271
-
4272
- .table-context-menu button:hover {
4273
- background: rgba(19, 191, 166, 0.1);
4274
- color: #008f7c;
4275
- }
4276
-
4277
- .table-context-menu button.is-danger {
4278
- color: #c45656;
4279
- }
4280
-
4281
- .table-context-menu button.is-danger:hover {
4282
- background: rgba(196, 86, 86, 0.1);
4283
- }
4284
-
4285
- .table-context-divider {
4286
- height: 1px;
4287
- margin: 5px 4px;
4288
- background: #edf1f7;
4289
- }
4290
-
4291
- .editor-content-shell {
4292
- min-height: 420px;
4293
- background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
4294
- border: 1px solid #dbe6f3;
4295
- border-radius: 18px;
4296
- overflow: auto;
4297
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7), 0 14px 38px rgba(31, 42, 68, 0.08);
4298
- }
4299
-
4300
- .dialog-footer {
4301
- display: flex;
4302
- justify-content: flex-end;
4303
- }
4304
-
4305
- :deep(.docx-wrapper) {
4306
- min-height: 100%;
4307
- width: 100%;
4308
- box-sizing: border-box;
4309
- padding: 28px 0 48px !important;
4310
- background: #858585 !important;
4311
- }
4312
-
4313
- :deep(.docx-preview) {
4314
- min-height: 100%;
4315
- background: #858585;
4316
- span{
4317
- line-height: 2em;
4318
- }
4319
- }
4320
-
4321
- :deep(.docx) {
4322
- margin: 0 auto 24px;
4323
- box-shadow: 0 16px 40px rgba(20, 32, 61, 0.16);
4324
- border-radius: 2px;
4325
- }
4326
-
4327
- :deep(.docx p),
4328
- :deep(.docx li),
4329
- :deep(.docx span),
4330
- :deep(.docx td),
4331
- :deep(.docx th) {
4332
- line-height: 1.6 !important;
4333
- }
4334
-
4335
- :deep(.editable-field-anchor) {
4336
- display: inline;
4337
- padding: 0;
4338
- margin: 0;
4339
- border: none;
4340
- border-radius: 4px;
4341
- background: rgba(19, 191, 166, 0.12);
4342
- cursor: pointer;
4343
- transition: all 0.2s ease;
4344
- white-space: pre-wrap;
4345
- word-break: break-word;
4346
- box-decoration-break: clone;
4347
- -webkit-box-decoration-break: clone;
4348
- font: inherit;
4349
- font-family: inherit;
4350
- font-size: inherit;
4351
- font-weight: inherit;
4352
- font-style: inherit;
4353
- line-height: inherit;
4354
- letter-spacing: inherit;
4355
- color: inherit;
4356
- vertical-align: inherit;
4357
- text-decoration-line: underline;
4358
- text-decoration-style: dashed;
4359
- text-decoration-color: rgba(19, 191, 166, 0.85);
4360
- text-underline-offset: 3px;
4361
- outline: 1px solid rgba(19, 191, 166, 0.28);
4362
- box-shadow: 0 0 0 1px rgba(19, 191, 166, 0.08);
4363
- }
4364
-
4365
- :deep(.editable-field-anchor:hover) {
4366
- background: rgba(19, 191, 166, 0.2);
4367
- text-decoration-color: #13bfa6;
4368
- outline-color: rgba(19, 191, 166, 0.5);
4369
- box-shadow: 0 6px 14px rgba(19, 191, 166, 0.12);
4370
- }
4371
-
4372
- :deep(.editable-field-anchor.is-placeholder) {
4373
- color: rgba(79, 91, 114, 0.58);
4374
- font-style: italic;
4375
- }
4376
-
4377
- :deep(.editable-field-block) {
4378
- display: block;
4379
- //padding: 6px 8px;
4380
- margin: 2px 0;
4381
- border-radius: 8px;
4382
- background: rgba(19, 191, 166, 0.08);
4383
- outline: 1px dashed rgba(19, 191, 166, 0.36);
4384
- cursor: pointer;
4385
- transition: all 0.2s ease;
4386
- font: inherit;
4387
- //font-family: inherit;
4388
- //font-size: inherit;
4389
- //font-weight: inherit;
4390
- //font-style: inherit;
4391
- line-height: inherit;
4392
- letter-spacing: inherit;
4393
- color: inherit;
4394
- font-size: 16px;
4395
- padding: 25px!important;
4396
- }
4397
-
4398
- :deep(.editable-field-block:hover) {
4399
- background: rgba(19, 191, 166, 0.14);
4400
- outline-color: rgba(19, 191, 166, 0.58);
4401
- box-shadow: 0 8px 18px rgba(19, 191, 166, 0.12);
4402
- }
4403
-
4404
- :deep(.editable-field-block.is-placeholder) {
4405
- min-height: 32px;
4406
- display: flex;
4407
- align-items: center;
4408
- color: rgba(79, 91, 114, 0.58);
4409
- font-style: italic;
4410
- }
4411
-
4412
- :deep(.editable-field-block.is-table-field) {
4413
- padding: 6px 8px !important;
4414
- background: rgba(19, 191, 166, 0.04);
4415
- }
4416
-
4417
- :deep(.editable-field-block p) {
4418
- margin: 0 0 0.5em;
4419
- font: inherit;
4420
- font-family: inherit;
4421
- font-size: inherit;
4422
- font-weight: inherit;
4423
- font-style: inherit;
4424
- line-height: inherit;
4425
- letter-spacing: inherit;
4426
- color: inherit;
4427
- }
4428
-
4429
- :deep(.editable-field-block p:last-child) {
4430
- margin-bottom: 0;
4431
- }
4432
-
4433
- :deep(.ProseMirror) {
4434
- min-height: 420px;
4435
- padding: 20px 24px;
4436
- outline: none;
4437
- color: #2b3345;
4438
- font-size: 15px;
4439
- line-height: 1.9;
4440
- }
4441
-
4442
- :deep(.ProseMirror .is-empty.is-editor-empty::before) {
4443
- content: attr(data-placeholder);
4444
- height: 0;
4445
- float: left;
4446
- color: rgba(79, 91, 114, 0.45);
4447
- pointer-events: none;
4448
- }
4449
-
4450
- :deep(.ProseMirror h1),
4451
- :deep(.ProseMirror h2),
4452
- :deep(.ProseMirror h3) {
4453
- color: #1f2a44;
4454
- line-height: 1.5;
4455
- margin: 0 0 16px;
4456
- }
4457
-
4458
- :deep(.ProseMirror h1) {
4459
- font-size: 30px;
4460
- }
4461
-
4462
- :deep(.ProseMirror h2) {
4463
- font-size: 22px;
4464
- }
4465
-
4466
- :deep(.ProseMirror h3) {
4467
- font-size: 18px;
4468
- }
4469
-
4470
- :deep(.ProseMirror p) {
4471
- margin: 0 0 14px;
4472
- }
4473
-
4474
- :deep(.ProseMirror table) {
4475
- width: 100% !important;
4476
- min-width: 100% !important;
4477
- max-width: 100% !important;
4478
- margin: 0 0 16px;
4479
- border-collapse: collapse;
4480
- table-layout: fixed;
4481
- border: 1.2px solid #000;
4482
- color: #000;
4483
- font-family: v-bind(EDITOR_TABLE_FONT_FAMILY);
4484
- font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE);
4485
- line-height: 1.15;
4486
- }
4487
-
4488
- :deep(.ProseMirror td),
4489
- :deep(.ProseMirror th) {
4490
- position: relative;
4491
- min-width: 72px;
4492
- height: 28px;
4493
- padding: 2px 6px;
4494
- border: 1.2px solid #000;
4495
- color: #000;
4496
- font-family: v-bind(EDITOR_TABLE_FONT_FAMILY);
4497
- font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE);
4498
- line-height: 1.15;
4499
- text-align: center;
4500
- vertical-align: middle;
4501
- }
4502
-
4503
- :deep(.ProseMirror th) {
4504
- background: #a6a6a6;
4505
- font-size: v-bind(EDITOR_TABLE_HEADER_FONT_SIZE);
4506
- font-weight: 700;
4507
- }
4508
-
4509
- :deep(.ProseMirror td p) {
4510
- margin: 0;
4511
- font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
4512
- font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE) !important;
4513
- line-height: 1.15 !important;
4514
- }
4515
-
4516
- :deep(.ProseMirror th p) {
4517
- margin: 0;
4518
- font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
4519
- font-size: v-bind(EDITOR_TABLE_HEADER_FONT_SIZE) !important;
4520
- line-height: 1.15 !important;
4521
- }
4522
-
4523
- :deep(.ProseMirror td *),
4524
- :deep(.ProseMirror th *) {
4525
- font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
4526
- }
4527
-
4528
- :deep(.ProseMirror .selectedCell:after) {
4529
- content: '';
4530
- position: absolute;
4531
- inset: 0;
4532
- background: rgba(19, 191, 166, 0.12);
4533
- pointer-events: none;
4534
- }
4535
-
4536
- :deep(.ProseMirror .tableWrapper) {
4537
- width: 100% !important;
4538
- max-width: 100% !important;
4539
- margin: 0 0 16px;
4540
- overflow-x: auto;
4541
- }
4542
-
4543
- :deep(.editable-field-block table) {
4544
- width: 100% !important;
4545
- min-width: 100% !important;
4546
- max-width: 100% !important;
4547
- margin: 0 0 12px;
4548
- border-collapse: collapse;
4549
- table-layout: fixed;
4550
- border: 1.2px solid #000;
4551
- color: #000;
4552
- font-family: v-bind(EDITOR_TABLE_FONT_FAMILY);
4553
- font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE);
4554
- line-height: 1.15;
4555
- }
4556
-
4557
- :deep(.editable-field-block .tableWrapper) {
4558
- width: 100% !important;
4559
- max-width: 100% !important;
4560
- }
4561
-
4562
- :deep(.editable-field-block td),
4563
- :deep(.editable-field-block th) {
4564
- height: 28px;
4565
- padding: 2px 6px;
4566
- border: 1.2px solid #000;
4567
- color: #000;
4568
- font-family: v-bind(EDITOR_TABLE_FONT_FAMILY);
4569
- font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE);
4570
- line-height: 1.15;
4571
- text-align: center;
4572
- vertical-align: middle;
4573
- }
4574
-
4575
- :deep(.editable-field-block th) {
4576
- background: #a6a6a6;
4577
- font-size: v-bind(EDITOR_TABLE_HEADER_FONT_SIZE);
4578
- font-weight: 700;
4579
- }
4580
-
4581
- :deep(.editable-field-block td p) {
4582
- margin: 0;
4583
- font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
4584
- font-size: v-bind(EDITOR_TABLE_BODY_FONT_SIZE) !important;
4585
- line-height: 1.15 !important;
4586
- }
4587
-
4588
- :deep(.editable-field-block th p) {
4589
- margin: 0;
4590
- font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
4591
- font-size: v-bind(EDITOR_TABLE_HEADER_FONT_SIZE) !important;
4592
- line-height: 1.15 !important;
4593
- }
4594
-
4595
- :deep(.editable-field-block td *),
4596
- :deep(.editable-field-block th *) {
4597
- font-family: v-bind(EDITOR_TABLE_FONT_FAMILY) !important;
4598
- }
4599
-
4600
- :deep(.editDialog-dialog){
4601
- height: 70%;
4602
- .el-dialog__body{
4603
- height: calc(100% - 80px);
4604
- .edit-dialog{
4605
- height: 100%;
4606
- .editor-content-shell{
4607
- height: 100%;
4608
- overflow: auto;
4609
- }
4610
- }
4611
- }
4612
- }
4613
- </style>