gcs-ui-lib 1.2.29 → 1.2.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/gcs-ui-lib.common.js +29220 -29220
- package/lib/gcs-ui-lib.umd.js +29220 -29220
- package/lib/gcs-ui-lib.umd.min.js +101 -101
- package/package.json +1 -1
- package/src/utils/exportPageSnapshot.js +498 -499
|
@@ -1,137 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 高保真页面 HTML 快照导出(Vue2 页面适用)
|
|
3
|
+
*
|
|
4
|
+
* 相比 gcs-ui-lib/exportPageSnapshot:
|
|
5
|
+
* - 不做 CSS 裁剪,保留文档全部可访问样式
|
|
6
|
+
* - 克隆节点后内联关键 computed 布局样式,提升离线还原度
|
|
7
|
+
* - 内嵌图标字体(woff/woff2),跳过大体积宋体
|
|
8
|
+
* - 默认不注入 PDF 专用覆盖样式,避免表格/字号被压扁
|
|
9
|
+
*/
|
|
3
10
|
|
|
4
|
-
/** 后端填充区域占位符,Java 侧按此字符串替换为审批进度等 HTML */
|
|
5
11
|
export const PAGE_SNAPSHOT_BACKEND_PLACEHOLDER = '<!-- PAGE_SNAPSHOT_BACKEND_PLACEHOLDER -->'
|
|
6
12
|
|
|
7
|
-
/** 页面底部操作栏,快照时自动排除 */
|
|
8
|
-
const PAGE_SNAPSHOT_SKIP_FOOTER_CLASS = 'page-button-shadow'
|
|
9
|
-
|
|
10
13
|
const SNAPSHOT_FILENAME_PREFIX = 'page-snapshot'
|
|
11
|
-
const
|
|
14
|
+
const SKIP_TAGS = new Set(['script', 'style', 'link', 'noscript', 'svg'])
|
|
15
|
+
const SKIP_CLASSES = [
|
|
16
|
+
'page-snapshot-btn',
|
|
17
|
+
'page-button-shadow',
|
|
18
|
+
'detpl-form-operate-box',
|
|
19
|
+
'self-footer'
|
|
20
|
+
]
|
|
12
21
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
const SCROLL_UNWRAP_SELECTORS = [
|
|
23
|
+
'.el-table__body-wrapper',
|
|
24
|
+
'.el-table__fixed-body-wrapper',
|
|
25
|
+
'.el-scrollbar__wrap',
|
|
26
|
+
'.el-scrollbar__view',
|
|
27
|
+
'.vxe-table--body-wrapper',
|
|
28
|
+
'.vxe-table--body-inner-wrapper',
|
|
29
|
+
'.vxe-table--main-wrapper'
|
|
17
30
|
]
|
|
18
31
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
margin
|
|
27
|
-
|
|
32
|
+
/** 内联 computed 样式时关注的属性(布局 + 常见视觉) */
|
|
33
|
+
const INLINE_STYLE_PROPS = [
|
|
34
|
+
'display', 'flex', 'flex-direction', 'flex-wrap', 'flex-grow', 'flex-shrink', 'flex-basis',
|
|
35
|
+
'align-items', 'align-self', 'align-content', 'justify-content', 'justify-self', 'gap', 'row-gap', 'column-gap',
|
|
36
|
+
'grid-template-columns', 'grid-template-rows', 'grid-column', 'grid-row',
|
|
37
|
+
'position', 'top', 'right', 'bottom', 'left', 'z-index', 'float', 'clear',
|
|
38
|
+
'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
|
|
39
|
+
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
40
|
+
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
41
|
+
'box-sizing', 'overflow', 'overflow-x', 'overflow-y',
|
|
42
|
+
'border', 'border-top', 'border-right', 'border-bottom', 'border-left',
|
|
43
|
+
'border-radius', 'border-collapse', 'table-layout',
|
|
44
|
+
'background', 'background-color', 'background-image', 'background-size', 'background-position',
|
|
45
|
+
'color', 'font-size', 'font-weight', 'font-style', 'line-height', 'letter-spacing',
|
|
46
|
+
'text-align', 'text-decoration', 'vertical-align', 'white-space', 'word-break', 'word-wrap',
|
|
47
|
+
'opacity', 'visibility', 'box-shadow', 'transform'
|
|
48
|
+
]
|
|
28
49
|
|
|
50
|
+
const SNAPSHOT_BASE_CSS = `
|
|
29
51
|
html, body {
|
|
30
52
|
margin: 0;
|
|
31
|
-
padding:
|
|
53
|
+
padding: 8px;
|
|
32
54
|
background: #fff;
|
|
33
|
-
font-family: SimSun, "Songti SC", "Microsoft YaHei", serif;
|
|
34
|
-
font-size: 10px;
|
|
35
|
-
line-height: 1.4;
|
|
36
55
|
-webkit-print-color-adjust: exact;
|
|
37
56
|
print-color-adjust: exact;
|
|
38
57
|
}
|
|
39
58
|
|
|
40
|
-
* { box-sizing: border-box; }
|
|
41
|
-
|
|
42
|
-
/* 页面根容器:展开高度,避免只导出可视区域 */
|
|
43
59
|
n20-page,
|
|
44
60
|
[class$="-wrap"],
|
|
45
61
|
.n20-page-content,
|
|
46
|
-
.page-content
|
|
62
|
+
.page-content,
|
|
63
|
+
.action-parse-form-container {
|
|
47
64
|
height: auto !important;
|
|
48
65
|
max-height: none !important;
|
|
49
66
|
overflow: visible !important;
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
/* 解除表格/滚动区裁剪(长列表被截断的常见原因) */
|
|
53
|
-
.el-table__body-wrapper,
|
|
54
|
-
.el-table__fixed-body-wrapper,
|
|
55
|
-
.el-scrollbar__wrap,
|
|
56
|
-
.el-scrollbar__view,
|
|
57
|
-
.vxe-table--body-wrapper,
|
|
58
|
-
.vxe-table--body-inner-wrapper,
|
|
59
|
-
.vxe-table--main-wrapper {
|
|
60
|
-
max-height: none !important;
|
|
61
|
-
height: auto !important;
|
|
62
|
-
overflow: visible !important;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.el-table,
|
|
66
|
-
.vxe-table {
|
|
67
|
-
width: 100% !important;
|
|
68
|
-
page-break-inside: auto;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/* 原生 table:分页与表头重复 */
|
|
72
|
-
table {
|
|
73
|
-
width: 100%;
|
|
74
|
-
border-collapse: collapse;
|
|
75
|
-
page-break-inside: auto;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
thead {
|
|
79
|
-
display: table-header-group;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
tfoot {
|
|
83
|
-
display: table-footer-group;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
tbody {
|
|
87
|
-
display: table-row-group;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
tr {
|
|
91
|
-
page-break-inside: avoid;
|
|
92
|
-
page-break-after: auto;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
td, th {
|
|
96
|
-
max-width: none;
|
|
97
|
-
word-wrap: break-word;
|
|
98
|
-
word-break: break-word;
|
|
99
|
-
overflow-wrap: break-word;
|
|
100
|
-
white-space: normal !important;
|
|
101
|
-
padding: 4px;
|
|
102
|
-
font-size: 10px;
|
|
103
|
-
vertical-align: top;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/* Element 表头/行 */
|
|
107
|
-
.el-table__header-wrapper thead {
|
|
108
|
-
display: table-header-group;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.el-table__body tr {
|
|
112
|
-
page-break-inside: avoid;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/* vxe-table 行块级分页 */
|
|
116
|
-
.vxe-body--row {
|
|
117
|
-
page-break-inside: avoid;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/* 表单分组:尽量避免拦腰切断 */
|
|
121
|
-
.el-form-item,
|
|
122
|
-
.el-row,
|
|
123
|
-
.form-group {
|
|
124
|
-
page-break-inside: avoid;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/* 横向过长时略缩小(Flying Saucer 对 zoom 支持有限,以字号为主) */
|
|
128
|
-
.el-table th,
|
|
129
|
-
.el-table td,
|
|
130
|
-
.vxe-cell {
|
|
131
|
-
font-size: 10px;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/* 快照不需要的 UI */
|
|
135
69
|
.el-dialog__wrapper,
|
|
136
70
|
.el-message,
|
|
137
71
|
.el-message-box__wrapper,
|
|
@@ -143,62 +77,45 @@ td, th {
|
|
|
143
77
|
}
|
|
144
78
|
`
|
|
145
79
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return `${SNAPSHOT_FILENAME_PREFIX}.html`
|
|
152
|
-
}
|
|
153
|
-
}
|
|
80
|
+
const SNAPSHOT_PDF_CSS = `
|
|
81
|
+
@page { size: A4 landscape; margin: 15mm; }
|
|
82
|
+
html, body { font-family: SimSun, "Songti SC", "Microsoft YaHei", serif; font-size: 10px; line-height: 1.4; }
|
|
83
|
+
td, th { white-space: normal !important; padding: 4px; font-size: 10px; vertical-align: top; }
|
|
84
|
+
`
|
|
154
85
|
|
|
155
|
-
/** 安全执行,异常时返回 fallback,不中断主流程 */
|
|
156
86
|
function safeRun(fn, fallback, label) {
|
|
157
87
|
try {
|
|
158
88
|
return fn()
|
|
159
89
|
} catch (e) {
|
|
160
|
-
if (label) {
|
|
161
|
-
console.warn(`[exportPageSnapshot] ${label}`, e)
|
|
162
|
-
}
|
|
90
|
+
if (label) console.warn(`[exportPageHtml] ${label}`, e)
|
|
163
91
|
return fallback
|
|
164
92
|
}
|
|
165
93
|
}
|
|
166
94
|
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function removeTempStyleNode(node) {
|
|
172
|
-
try {
|
|
173
|
-
if (node?.parentNode) node.parentNode.removeChild(node)
|
|
174
|
-
} catch (e) {
|
|
175
|
-
// ignore
|
|
176
|
-
}
|
|
95
|
+
function buildSnapshotFilename() {
|
|
96
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
97
|
+
return `${SNAPSHOT_FILENAME_PREFIX}-${ts}.html`
|
|
177
98
|
}
|
|
178
99
|
|
|
179
|
-
function shouldSkipElement(el
|
|
100
|
+
function shouldSkipElement(el) {
|
|
180
101
|
if (!el || el.nodeType !== Node.ELEMENT_NODE) return false
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return skipClassNames.some((cls) => el.classList?.contains(cls))
|
|
102
|
+
if (SKIP_TAGS.has(el.tagName.toLowerCase())) return true
|
|
103
|
+
return SKIP_CLASSES.some((cls) => el.classList?.contains(cls))
|
|
184
104
|
}
|
|
185
105
|
|
|
186
|
-
function clonePageNode(node
|
|
106
|
+
function clonePageNode(node) {
|
|
187
107
|
if (!node) return null
|
|
188
108
|
|
|
189
|
-
|
|
109
|
+
return safeRun(() => {
|
|
190
110
|
if (node.nodeType === Node.TEXT_NODE) return node.cloneNode(false)
|
|
191
111
|
if (node.nodeType !== Node.ELEMENT_NODE) return null
|
|
192
|
-
if (shouldSkipElement(node
|
|
112
|
+
if (shouldSkipElement(node)) return null
|
|
193
113
|
|
|
194
114
|
const tag = node.tagName.toLowerCase()
|
|
195
115
|
const cloned = node.cloneNode(false)
|
|
116
|
+
|
|
196
117
|
Array.from(node.attributes || []).forEach((attr) => {
|
|
197
|
-
|
|
198
|
-
cloned.setAttribute(attr.name, attr.value)
|
|
199
|
-
} catch (e) {
|
|
200
|
-
// ignore single attr failure
|
|
201
|
-
}
|
|
118
|
+
safeRun(() => cloned.setAttribute(attr.name, attr.value), undefined)
|
|
202
119
|
})
|
|
203
120
|
|
|
204
121
|
if (tag === 'input') {
|
|
@@ -220,374 +137,461 @@ function clonePageNode(node, skipTags, skipClassNames) {
|
|
|
220
137
|
}
|
|
221
138
|
|
|
222
139
|
Array.from(node.childNodes).forEach((child) => {
|
|
223
|
-
const childClone =
|
|
224
|
-
() => clonePageNode(child, skipTags, skipClassNames),
|
|
225
|
-
null,
|
|
226
|
-
'克隆子节点失败'
|
|
227
|
-
)
|
|
140
|
+
const childClone = clonePageNode(child)
|
|
228
141
|
if (childClone) cloned.appendChild(childClone)
|
|
229
142
|
})
|
|
230
143
|
return cloned
|
|
231
|
-
}
|
|
232
|
-
console.warn('[exportPageSnapshot] 克隆节点失败', e)
|
|
233
|
-
return null
|
|
234
|
-
}
|
|
144
|
+
}, null, '克隆节点失败')
|
|
235
145
|
}
|
|
236
146
|
|
|
237
|
-
function
|
|
238
|
-
|
|
147
|
+
function unwrapScrollContainers(root) {
|
|
148
|
+
if (!root?.querySelectorAll) return
|
|
149
|
+
SCROLL_UNWRAP_SELECTORS.forEach((sel) => {
|
|
150
|
+
root.querySelectorAll(sel).forEach((el) => {
|
|
151
|
+
el.style.maxHeight = 'none'
|
|
152
|
+
el.style.height = 'auto'
|
|
153
|
+
el.style.overflow = 'visible'
|
|
154
|
+
})
|
|
155
|
+
})
|
|
239
156
|
}
|
|
240
157
|
|
|
241
|
-
|
|
158
|
+
/**
|
|
159
|
+
* 删除页面锚点导航 DOM(N20-anchor 右侧/左侧导航栏)
|
|
160
|
+
* @param {HTMLElement} root
|
|
161
|
+
*/
|
|
162
|
+
export function removeAnchorNav(root) {
|
|
163
|
+
if (!root?.querySelectorAll) return
|
|
164
|
+
|
|
165
|
+
root.querySelectorAll('.detpl-edit-form-container.flex-box').forEach((flexBox) => {
|
|
166
|
+
Array.from(flexBox.children).forEach((child) => {
|
|
167
|
+
if (child.classList.contains('flex-item')) return
|
|
168
|
+
if (child.querySelector('.n20-anchor2-nav')) {
|
|
169
|
+
child.remove()
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
flexBox.style?.removeProperty('padding-right')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
root.querySelectorAll('.n20-anchor, .n20-anchor-left, .n20-anchor2-sidebar').forEach((el) => {
|
|
176
|
+
el.remove()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
root.querySelectorAll('.n20-anchor2-nav').forEach((el) => {
|
|
180
|
+
if (!el.closest('.flex-item')) {
|
|
181
|
+
el.remove()
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
}
|
|
242
185
|
|
|
243
|
-
/**
|
|
244
|
-
const
|
|
186
|
+
/** 底部固定提交/操作按钮组选择器 */
|
|
187
|
+
const BOTTOM_OPERATE_SELECTORS = [
|
|
188
|
+
'.page-button-shadow',
|
|
189
|
+
'.detpl-form-operate-box',
|
|
190
|
+
'.self-footer',
|
|
191
|
+
'.page-footer-shadow'
|
|
192
|
+
]
|
|
245
193
|
|
|
246
194
|
/**
|
|
247
|
-
*
|
|
248
|
-
*
|
|
195
|
+
* 删除底部固定提交按钮组 DOM
|
|
196
|
+
* @param {HTMLElement} root
|
|
249
197
|
*/
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
/^n20-approve/,
|
|
253
|
-
/^n20-title/,
|
|
254
|
-
/^n20-tips$/,
|
|
255
|
-
/^n20-time$/,
|
|
256
|
-
/^n20-worker$/,
|
|
257
|
-
/^n20-description/,
|
|
258
|
-
/^n20-icon-/,
|
|
259
|
-
/^result-left-name$/,
|
|
260
|
-
/^worker-icon$/,
|
|
261
|
-
/^color-primary$/,
|
|
262
|
-
/^color-0$/,
|
|
263
|
-
/^expand$/,
|
|
264
|
-
/^is-reverse$/
|
|
265
|
-
]
|
|
198
|
+
export function removeBottomOperateButtons(root) {
|
|
199
|
+
if (!root?.querySelectorAll) return
|
|
266
200
|
|
|
267
|
-
|
|
268
|
-
|
|
201
|
+
BOTTOM_OPERATE_SELECTORS.forEach((selector) => {
|
|
202
|
+
root.querySelectorAll(selector).forEach((el) => el.remove())
|
|
203
|
+
})
|
|
269
204
|
}
|
|
270
205
|
|
|
271
|
-
function
|
|
272
|
-
if (!
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return p.test(className)
|
|
277
|
-
} catch (e) {
|
|
278
|
-
return false
|
|
279
|
-
}
|
|
280
|
-
})
|
|
206
|
+
function mergeInlineStyle(el, extraCss) {
|
|
207
|
+
if (!extraCss) return
|
|
208
|
+
const prev = el.getAttribute('style') || ''
|
|
209
|
+
const merged = prev ? `${prev};${extraCss}` : extraCss
|
|
210
|
+
el.setAttribute('style', merged)
|
|
281
211
|
}
|
|
282
212
|
|
|
283
|
-
function
|
|
284
|
-
|
|
213
|
+
function buildComputedStyleText(el) {
|
|
214
|
+
const computed = window.getComputedStyle(el)
|
|
215
|
+
const chunks = []
|
|
285
216
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const { classes } = extractSimpleTokens(noPseudo)
|
|
294
|
-
return classes.some(isPreservedPlaceholderClass)
|
|
295
|
-
})
|
|
296
|
-
}, false, '保留 class 判断失败')
|
|
217
|
+
INLINE_STYLE_PROPS.forEach((prop) => {
|
|
218
|
+
const val = computed.getPropertyValue(prop)
|
|
219
|
+
if (!val || val === 'initial' || val === 'auto' && prop !== 'height' && prop !== 'width') return
|
|
220
|
+
if (prop === 'background-image' && val === 'none') return
|
|
221
|
+
chunks.push(`${prop}:${val}`)
|
|
222
|
+
})
|
|
223
|
+
return chunks.join(';')
|
|
297
224
|
}
|
|
298
225
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const usage = createEmptyHtmlUsage()
|
|
302
|
-
if (!html || typeof html !== 'string') return usage
|
|
226
|
+
function inlineComputedStyles(sourceEl, cloneEl) {
|
|
227
|
+
if (!sourceEl || !cloneEl || sourceEl.nodeType !== Node.ELEMENT_NODE) return
|
|
303
228
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
usage.tags.add(m[1].toLowerCase())
|
|
313
|
-
}
|
|
314
|
-
for (const m of html.matchAll(/\s(data-v-[a-f0-9]+)/g)) {
|
|
315
|
-
usage.attrs.add(m[1])
|
|
316
|
-
}
|
|
317
|
-
return usage
|
|
318
|
-
}, usage, '解析 HTML 使用信息失败')
|
|
229
|
+
mergeInlineStyle(cloneEl, buildComputedStyleText(sourceEl))
|
|
230
|
+
|
|
231
|
+
const srcChildren = sourceEl.children || []
|
|
232
|
+
const cloneChildren = cloneEl.children || []
|
|
233
|
+
const len = Math.min(srcChildren.length, cloneChildren.length)
|
|
234
|
+
for (let i = 0; i < len; i++) {
|
|
235
|
+
inlineComputedStyles(srcChildren[i], cloneChildren[i])
|
|
236
|
+
}
|
|
319
237
|
}
|
|
320
238
|
|
|
321
|
-
function
|
|
322
|
-
|
|
323
|
-
|
|
239
|
+
function normalizeCssChunk(text) {
|
|
240
|
+
return (text || '').replace(/\s+/g, ' ').trim()
|
|
241
|
+
}
|
|
324
242
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
243
|
+
function collectDocumentCss() {
|
|
244
|
+
const seen = new Set()
|
|
245
|
+
const rules = []
|
|
246
|
+
const processedStyleNodes = new Set()
|
|
247
|
+
|
|
248
|
+
const addRule = (cssText) => {
|
|
249
|
+
const chunk = normalizeCssChunk(cssText)
|
|
250
|
+
if (!chunk || seen.has(chunk)) return
|
|
251
|
+
seen.add(chunk)
|
|
252
|
+
rules.push(cssText.trim())
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
safeRun(() => {
|
|
256
|
+
Array.from(document.styleSheets || []).forEach((sheet) => {
|
|
257
|
+
safeRun(() => {
|
|
258
|
+
const sheetRules = sheet.cssRules || sheet.rules
|
|
259
|
+
if (sheet.ownerNode) processedStyleNodes.add(sheet.ownerNode)
|
|
260
|
+
if (!sheetRules) return
|
|
261
|
+
Array.from(sheetRules).forEach((rule) => {
|
|
262
|
+
safeRun(() => addRule(rule.cssText), undefined)
|
|
263
|
+
})
|
|
264
|
+
}, () => {
|
|
265
|
+
if (sheet.ownerNode?.tagName === 'STYLE') {
|
|
266
|
+
processedStyleNodes.add(sheet.ownerNode)
|
|
267
|
+
addRule(sheet.ownerNode.textContent || '')
|
|
268
|
+
}
|
|
269
|
+
})
|
|
341
270
|
})
|
|
271
|
+
}, undefined, '遍历样式表失败')
|
|
342
272
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
273
|
+
safeRun(() => {
|
|
274
|
+
document.querySelectorAll('style').forEach((el) => {
|
|
275
|
+
if (!processedStyleNodes.has(el)) addRule(el.textContent || '')
|
|
276
|
+
})
|
|
277
|
+
}, undefined, '收集 style 标签失败')
|
|
346
278
|
|
|
347
|
-
|
|
348
|
-
|
|
279
|
+
return rules.join('\n')
|
|
280
|
+
}
|
|
349
281
|
|
|
282
|
+
function captureRootCssVariables() {
|
|
350
283
|
return safeRun(() => {
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const [, attr, op, val] = fullMatch
|
|
358
|
-
if (attr === 'class') {
|
|
359
|
-
const matched = op === '$='
|
|
360
|
-
? [...usage.classes].some((c) => c.endsWith(val))
|
|
361
|
-
: op === '^='
|
|
362
|
-
? [...usage.classes].some((c) => c.startsWith(val))
|
|
363
|
-
: op === '*='
|
|
364
|
-
? [...usage.classes].some((c) => c.includes(val))
|
|
365
|
-
: usage.classes.has(val)
|
|
366
|
-
if (!matched) return false
|
|
367
|
-
} else if (!usage.attrs.has(attr)) {
|
|
368
|
-
return false
|
|
369
|
-
}
|
|
370
|
-
} else if (usage.attrs.has(content)) {
|
|
371
|
-
// [data-v-xxx] 等精确属性
|
|
372
|
-
} else if (!/^class/.test(content)) {
|
|
373
|
-
return false
|
|
284
|
+
const styles = getComputedStyle(document.documentElement)
|
|
285
|
+
const vars = []
|
|
286
|
+
for (let i = 0; i < styles.length; i++) {
|
|
287
|
+
const prop = styles[i]
|
|
288
|
+
if (prop.startsWith('--')) {
|
|
289
|
+
vars.push(`${prop}:${styles.getPropertyValue(prop)}`)
|
|
374
290
|
}
|
|
375
|
-
m = attrRe.exec(selector)
|
|
376
291
|
}
|
|
377
|
-
return
|
|
378
|
-
},
|
|
292
|
+
return vars.length ? `:root { ${vars.join(';')} }` : ''
|
|
293
|
+
}, '', '收集 CSS 变量失败')
|
|
379
294
|
}
|
|
380
295
|
|
|
381
|
-
function
|
|
382
|
-
|
|
296
|
+
function stripHeavyFontFaces(css) {
|
|
297
|
+
return (css || '').replace(/@font-face\s*\{[^}]*font-family\s*:\s*SUN[^}]*\}/gi, '')
|
|
298
|
+
}
|
|
383
299
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
300
|
+
function getAppStaticBase() {
|
|
301
|
+
const base = typeof process !== 'undefined' && process.env?.BASE_URL
|
|
302
|
+
? process.env.BASE_URL
|
|
303
|
+
: '/gdebit/'
|
|
304
|
+
try {
|
|
305
|
+
return new URL(`static/`, new URL(base, window.location.origin)).href
|
|
306
|
+
} catch (e) {
|
|
307
|
+
return `${window.location.origin}/gdebit/static/`
|
|
308
|
+
}
|
|
309
|
+
}
|
|
388
310
|
|
|
389
|
-
|
|
311
|
+
function getStylesheetBaseUrls() {
|
|
312
|
+
const bases = new Set([getAppStaticBase(), `${window.location.origin}/`])
|
|
390
313
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
.
|
|
314
|
+
safeRun(() => {
|
|
315
|
+
document.querySelectorAll('link[rel="stylesheet"][href]').forEach((link) => {
|
|
316
|
+
bases.add(new URL('.', link.href).href)
|
|
317
|
+
})
|
|
318
|
+
Array.from(document.styleSheets || []).forEach((sheet) => {
|
|
319
|
+
if (!sheet.href) return
|
|
320
|
+
bases.add(new URL('.', sheet.href).href)
|
|
321
|
+
})
|
|
322
|
+
}, undefined)
|
|
394
323
|
|
|
395
|
-
|
|
396
|
-
|
|
324
|
+
return [...bases]
|
|
325
|
+
}
|
|
397
326
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
327
|
+
function getLoadedFontResourceUrls() {
|
|
328
|
+
return safeRun(() => {
|
|
329
|
+
return performance
|
|
330
|
+
.getEntriesByType('resource')
|
|
331
|
+
.filter((entry) => /\.(woff2?|ttf|otf)(\?|$)/i.test(entry.name))
|
|
332
|
+
.map((entry) => entry.name)
|
|
333
|
+
}, [], '读取已加载字体资源失败')
|
|
334
|
+
}
|
|
401
335
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
336
|
+
function toAbsoluteFontAssetUrl(assetUrl) {
|
|
337
|
+
if (!assetUrl) return ''
|
|
338
|
+
if (/^https?:\/\//i.test(assetUrl) || assetUrl.startsWith('data:')) return assetUrl
|
|
339
|
+
try {
|
|
340
|
+
return new URL(assetUrl, window.location.origin).href
|
|
341
|
+
} catch (e) {
|
|
342
|
+
return assetUrl
|
|
343
|
+
}
|
|
344
|
+
}
|
|
405
345
|
|
|
406
|
-
|
|
407
|
-
|
|
346
|
+
function safeRequireFont(modulePath) {
|
|
347
|
+
try {
|
|
348
|
+
return toAbsoluteFontAssetUrl(require(modulePath))
|
|
349
|
+
} catch (e) {
|
|
350
|
+
return ''
|
|
351
|
+
}
|
|
408
352
|
}
|
|
409
353
|
|
|
410
|
-
|
|
411
|
-
|
|
354
|
+
/** webpack 解析的图标字体 URL(/gdebit/static/fonts/...) */
|
|
355
|
+
function getWebpackIconFontUrls() {
|
|
356
|
+
const paths = [
|
|
357
|
+
'element-ui/lib/theme-chalk/fonts/element-icons.woff',
|
|
358
|
+
'element-ui/lib/theme-chalk/fonts/element-icons.ttf',
|
|
359
|
+
'n20-common-lib/src/assets/iconFont2/iconfont.woff2',
|
|
360
|
+
'n20-common-lib/src/assets/iconFont2/iconfont.woff',
|
|
361
|
+
'n20-common-lib/src/assets/iconFont2/iconfont.ttf'
|
|
362
|
+
]
|
|
412
363
|
|
|
413
|
-
return
|
|
414
|
-
if (selectorHasPreservedClass(selectorText)) return true
|
|
415
|
-
return selectorText.split(',').some((sel) => selectorMatchesUsage(sel, usage))
|
|
416
|
-
}, false, '选择器列表匹配失败')
|
|
364
|
+
return [...new Set(paths.map(safeRequireFont).filter(Boolean))]
|
|
417
365
|
}
|
|
418
366
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
367
|
+
const ICON_FONT_FAMILIES = [
|
|
368
|
+
'element-icons',
|
|
369
|
+
'core-lib-iconfont',
|
|
370
|
+
'iconfont',
|
|
371
|
+
'vxeiconfont'
|
|
372
|
+
]
|
|
422
373
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (animName && animName !== 'none') {
|
|
426
|
-
animName.split(',').forEach((n) => names.add(n.trim()))
|
|
427
|
-
}
|
|
428
|
-
const anim = style.animation
|
|
429
|
-
if (anim && anim !== 'none') {
|
|
430
|
-
const first = anim.trim().split(/\s+/)[0]
|
|
431
|
-
if (first && !/^[\d.]/.test(first)) names.add(first)
|
|
432
|
-
}
|
|
433
|
-
return names
|
|
434
|
-
}, names, '解析动画名失败')
|
|
374
|
+
function escapeRegExp(text) {
|
|
375
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
435
376
|
}
|
|
436
377
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const
|
|
378
|
+
/** 导出前触发图标字体加载,便于 performance / document.fonts 拿到真实 URL */
|
|
379
|
+
async function ensureIconFontsLoaded() {
|
|
380
|
+
const probe = document.createElement('div')
|
|
381
|
+
probe.style.cssText = 'position:absolute;left:-9999px;visibility:hidden;pointer-events:none'
|
|
382
|
+
probe.innerHTML = [
|
|
383
|
+
'<i class="el-icon-arrow-down"></i>',
|
|
384
|
+
'<i class="el-icon-date"></i>',
|
|
385
|
+
'<i class="n20-icon-yuefenqiehuan-zuoce"></i>',
|
|
386
|
+
'<i class="vxe-icon-caret-down"></i>'
|
|
387
|
+
].join('')
|
|
440
388
|
|
|
441
|
-
|
|
442
|
-
try {
|
|
443
|
-
if (!rule) return
|
|
389
|
+
document.body.appendChild(probe)
|
|
444
390
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
391
|
+
await safeRun(async () => {
|
|
392
|
+
if (!document.fonts?.load) return
|
|
393
|
+
const tasks = ICON_FONT_FAMILIES.map((family) => (
|
|
394
|
+
document.fonts.load(`16px ${family}`).catch(() => {})
|
|
395
|
+
))
|
|
396
|
+
await Promise.all(tasks)
|
|
397
|
+
await document.fonts.ready
|
|
398
|
+
}, undefined)
|
|
449
399
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (rule.cssText) kept.push(rule.cssText)
|
|
454
|
-
return
|
|
455
|
-
}
|
|
400
|
+
await new Promise((resolve) => setTimeout(resolve, 120))
|
|
401
|
+
document.body.removeChild(probe)
|
|
402
|
+
}
|
|
456
403
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (!inner.length) return
|
|
460
|
-
const prefix = rule.type === CSSRule.MEDIA_RULE ? '@media' : '@supports'
|
|
461
|
-
kept.push(`${prefix} ${rule.conditionText} {\n${inner.join('\n')}\n}`)
|
|
462
|
-
return
|
|
463
|
-
}
|
|
404
|
+
function collectFontUrlsFromStyleSheets() {
|
|
405
|
+
const urls = new Set()
|
|
464
406
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
} catch (e) {
|
|
469
|
-
// 单条规则解析失败时尽量保留原文,避免样式缺失
|
|
407
|
+
safeRun(() => {
|
|
408
|
+
Array.from(document.styleSheets || []).forEach((sheet) => {
|
|
409
|
+
let rules
|
|
470
410
|
try {
|
|
471
|
-
|
|
472
|
-
} catch (
|
|
473
|
-
|
|
411
|
+
rules = sheet.cssRules
|
|
412
|
+
} catch (e) {
|
|
413
|
+
return
|
|
474
414
|
}
|
|
475
|
-
|
|
476
|
-
|
|
415
|
+
if (!rules) return
|
|
416
|
+
|
|
417
|
+
const base = sheet.href || window.location.href
|
|
418
|
+
Array.from(rules).forEach((rule) => {
|
|
419
|
+
if (rule.type !== CSSRule.FONT_FACE_RULE) return
|
|
420
|
+
const cssText = rule.cssText || ''
|
|
421
|
+
if (!/iconfont|element-icons|vxeicon/i.test(cssText)) return
|
|
422
|
+
for (const match of cssText.matchAll(/url\(\s*["']?([^"')]+)["']?\s*\)/g)) {
|
|
423
|
+
const raw = match[1].trim()
|
|
424
|
+
if (raw.startsWith('data:')) {
|
|
425
|
+
urls.add(raw)
|
|
426
|
+
continue
|
|
427
|
+
}
|
|
428
|
+
safeRun(() => urls.add(new URL(raw, base).href))
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
}, undefined)
|
|
477
433
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
434
|
+
return [...urls]
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function getAllIconFontFetchUrls() {
|
|
438
|
+
const urls = new Set()
|
|
439
|
+
|
|
440
|
+
getWebpackIconFontUrls().forEach((url) => urls.add(url))
|
|
441
|
+
getLoadedFontResourceUrls().forEach((url) => {
|
|
442
|
+
if (/iconfont|element-icons|vxeicon|\.woff2?|\.ttf|\.otf/i.test(url)) {
|
|
443
|
+
urls.add(url)
|
|
485
444
|
}
|
|
486
445
|
})
|
|
446
|
+
collectFontUrlsFromStyleSheets().forEach((url) => urls.add(url))
|
|
487
447
|
|
|
488
|
-
return
|
|
448
|
+
return [...urls]
|
|
489
449
|
}
|
|
490
450
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (!html) return cssText
|
|
495
|
-
|
|
496
|
-
const usage = collectHtmlUsage(html)
|
|
497
|
-
const animationNames = new Set()
|
|
498
|
-
const node = document.createElement('style')
|
|
499
|
-
node.textContent = cssText
|
|
451
|
+
function getFilename(url) {
|
|
452
|
+
return (url || '').split('/').pop().split('?')[0]
|
|
453
|
+
}
|
|
500
454
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
console.warn('[exportPageSnapshot] 注入临时样式失败,跳过 CSS 裁剪', e)
|
|
505
|
-
return cssText
|
|
455
|
+
function buildFontFetchCandidates(rawUrl) {
|
|
456
|
+
if (!rawUrl || rawUrl.startsWith('data:') || rawUrl.startsWith('blob:')) {
|
|
457
|
+
return rawUrl ? [rawUrl] : []
|
|
506
458
|
}
|
|
507
459
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
} catch (e) {
|
|
514
|
-
console.warn('[exportPageSnapshot] CSS 裁剪失败,使用原始样式', e)
|
|
515
|
-
return cssText
|
|
516
|
-
} finally {
|
|
517
|
-
removeTempStyleNode(node)
|
|
460
|
+
const candidates = new Set()
|
|
461
|
+
const filename = getFilename(rawUrl)
|
|
462
|
+
|
|
463
|
+
if (/^https?:\/\//i.test(rawUrl)) {
|
|
464
|
+
candidates.add(rawUrl)
|
|
518
465
|
}
|
|
466
|
+
|
|
467
|
+
safeRun(() => candidates.add(new URL(rawUrl, window.location.href).href))
|
|
468
|
+
getStylesheetBaseUrls().forEach((base) => {
|
|
469
|
+
safeRun(() => candidates.add(new URL(rawUrl, base).href))
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
getAllIconFontFetchUrls().forEach((knownUrl) => {
|
|
473
|
+
if (knownUrl.includes(filename)) {
|
|
474
|
+
candidates.add(knownUrl)
|
|
475
|
+
}
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
return [...candidates]
|
|
519
479
|
}
|
|
520
480
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
const
|
|
525
|
-
|
|
481
|
+
function isFontBinary(buffer) {
|
|
482
|
+
if (!buffer || buffer.byteLength < 4) return false
|
|
483
|
+
const bytes = new Uint8Array(buffer.slice(0, 4))
|
|
484
|
+
const signature = String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3])
|
|
485
|
+
return (
|
|
486
|
+
signature === 'wOFF'
|
|
487
|
+
|| signature === 'wOF2'
|
|
488
|
+
|| signature === 'OTTO'
|
|
489
|
+
|| (bytes[0] === 0x00 && bytes[1] === 0x01 && bytes[2] === 0x00 && bytes[3] === 0x00)
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function isFontDataUrl(dataUrl) {
|
|
494
|
+
if (!dataUrl || typeof dataUrl !== 'string') return false
|
|
495
|
+
if (!dataUrl.startsWith('data:')) return false
|
|
496
|
+
if (/^data:text\/html/i.test(dataUrl)) return false
|
|
497
|
+
if (/^data:application\/json/i.test(dataUrl)) return false
|
|
498
|
+
if (/^data:image\//i.test(dataUrl)) return false
|
|
499
|
+
return /^data:font\//i.test(dataUrl) || /^data:application\//i.test(dataUrl)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function fetchFontAsDataUrl(rawUrl) {
|
|
503
|
+
const candidates = buildFontFetchCandidates(rawUrl)
|
|
504
|
+
|
|
505
|
+
for (const candidate of candidates) {
|
|
506
|
+
if (candidate.startsWith('data:')) {
|
|
507
|
+
if (isFontDataUrl(candidate)) return candidate
|
|
508
|
+
continue
|
|
509
|
+
}
|
|
526
510
|
|
|
527
|
-
const addRule = (cssText) => {
|
|
528
511
|
try {
|
|
529
|
-
const
|
|
530
|
-
if (!
|
|
531
|
-
|
|
532
|
-
|
|
512
|
+
const resp = await fetch(candidate)
|
|
513
|
+
if (!resp.ok) continue
|
|
514
|
+
|
|
515
|
+
const contentType = resp.headers.get('content-type') || ''
|
|
516
|
+
if (/text\/html|application\/json/i.test(contentType)) continue
|
|
517
|
+
|
|
518
|
+
const buffer = await resp.arrayBuffer()
|
|
519
|
+
if (!isFontBinary(buffer)) continue
|
|
520
|
+
|
|
521
|
+
const blob = new Blob([buffer], {
|
|
522
|
+
type: contentType || 'application/octet-stream'
|
|
523
|
+
})
|
|
524
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
525
|
+
const reader = new FileReader()
|
|
526
|
+
reader.onload = () => resolve(reader.result)
|
|
527
|
+
reader.onerror = reject
|
|
528
|
+
reader.readAsDataURL(blob)
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
if (isFontDataUrl(dataUrl)) return dataUrl
|
|
533
532
|
} catch (e) {
|
|
534
|
-
//
|
|
533
|
+
// try next candidate
|
|
535
534
|
}
|
|
536
535
|
}
|
|
537
536
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
try {
|
|
541
|
-
const sheetRules = sheet.cssRules || sheet.rules
|
|
542
|
-
if (!sheetRules) return
|
|
543
|
-
if (sheet.ownerNode) processedStyleNodes.add(sheet.ownerNode)
|
|
544
|
-
Array.from(sheetRules).forEach((rule) => {
|
|
545
|
-
safeRun(() => addRule(rule.cssText), undefined, '收集 CSS 规则失败')
|
|
546
|
-
})
|
|
547
|
-
} catch (e) {
|
|
548
|
-
if (sheet.ownerNode && sheet.ownerNode.tagName === 'STYLE') {
|
|
549
|
-
processedStyleNodes.add(sheet.ownerNode)
|
|
550
|
-
addRule(sheet.ownerNode.textContent || '')
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
})
|
|
554
|
-
}, undefined, '遍历样式表失败')
|
|
555
|
-
|
|
556
|
-
safeRun(() => {
|
|
557
|
-
document.querySelectorAll('style').forEach((el) => {
|
|
558
|
-
if (!processedStyleNodes.has(el)) {
|
|
559
|
-
addRule(el.textContent || '')
|
|
560
|
-
}
|
|
561
|
-
})
|
|
562
|
-
}, undefined, '收集 style 标签失败')
|
|
537
|
+
throw new Error(`font fetch failed: ${rawUrl}`)
|
|
538
|
+
}
|
|
563
539
|
|
|
564
|
-
|
|
540
|
+
function shouldEmbedFontUrl(url) {
|
|
541
|
+
if (!url || url.startsWith('data:')) return false
|
|
542
|
+
if (/SIMSUN|simsun/i.test(url)) return false
|
|
543
|
+
return /iconfont|element-icons|vxeicon|\.woff2?|\.ttf|\.otf/i.test(url)
|
|
565
544
|
}
|
|
566
545
|
|
|
567
|
-
function
|
|
568
|
-
|
|
546
|
+
async function embedIconFonts(css) {
|
|
547
|
+
await ensureIconFontsLoaded()
|
|
569
548
|
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
549
|
+
const filenameToData = new Map()
|
|
550
|
+
|
|
551
|
+
for (const knownUrl of getAllIconFontFetchUrls()) {
|
|
552
|
+
const filename = getFilename(knownUrl)
|
|
553
|
+
if (!filename || filenameToData.has(filename)) continue
|
|
554
|
+
if (knownUrl.startsWith('data:') && isFontDataUrl(knownUrl)) {
|
|
555
|
+
filenameToData.set(filename, knownUrl)
|
|
556
|
+
continue
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
filenameToData.set(filename, await fetchFontAsDataUrl(knownUrl))
|
|
560
|
+
} catch (e) {
|
|
561
|
+
// try next source
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const rawUrls = [...new Set(
|
|
566
|
+
[...(css || '').matchAll(/url\(\s*["']?([^"')]+)["']?\s*\)/g)].map((m) => m[1].trim())
|
|
567
|
+
)]
|
|
568
|
+
|
|
569
|
+
for (const rawUrl of rawUrls) {
|
|
570
|
+
if (!shouldEmbedFontUrl(rawUrl)) continue
|
|
571
|
+
const filename = getFilename(rawUrl)
|
|
572
|
+
if (filenameToData.has(filename)) continue
|
|
573
|
+
try {
|
|
574
|
+
filenameToData.set(filename, await fetchFontAsDataUrl(rawUrl))
|
|
575
|
+
} catch (e) {
|
|
576
|
+
console.warn('[exportPageHtml] 字体内嵌失败', rawUrl, e)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
let result = css
|
|
581
|
+
filenameToData.forEach((dataUrl, filename) => {
|
|
582
|
+
const pattern = new RegExp(
|
|
583
|
+
`url\\(\\s*["']?[^"')]*${escapeRegExp(filename)}[^"')]*["']?\\s*\\)`,
|
|
584
|
+
'gi'
|
|
585
|
+
)
|
|
586
|
+
result = result.replace(pattern, `url("${dataUrl}")`)
|
|
583
587
|
})
|
|
584
|
-
}
|
|
585
588
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
589
|
+
result = result.replace(
|
|
590
|
+
/url\(\s*["']?data:text\/html[^"')]+["']?\s*\)/gi,
|
|
591
|
+
'url("")'
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
return result
|
|
591
595
|
}
|
|
592
596
|
|
|
593
597
|
function downloadBlob(blob, filename) {
|
|
@@ -600,66 +604,63 @@ function downloadBlob(blob, filename) {
|
|
|
600
604
|
a.click()
|
|
601
605
|
document.body.removeChild(a)
|
|
602
606
|
URL.revokeObjectURL(url)
|
|
603
|
-
}, undefined, '
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* @param {string} pageHtml
|
|
608
|
-
* @param {{ fullCss?: boolean }} [options]
|
|
609
|
-
* @param {boolean} [options.fullCss=false] - true 时导出全量 CSS,false 时按 HTML 裁剪未使用规则
|
|
610
|
-
*/
|
|
611
|
-
function resolvePageCss(pageHtml, { fullCss = false } = {}) {
|
|
612
|
-
const rawCss = collectUniquePageCss()
|
|
613
|
-
if (!pageHtml || fullCss) return rawCss
|
|
614
|
-
|
|
615
|
-
return safeRun(
|
|
616
|
-
() => purgeUnusedCss(rawCss, pageHtml),
|
|
617
|
-
rawCss,
|
|
618
|
-
'CSS 裁剪失败,使用原始样式'
|
|
619
|
-
)
|
|
607
|
+
}, undefined, '下载失败')
|
|
620
608
|
}
|
|
621
609
|
|
|
622
|
-
function
|
|
610
|
+
function normalizeOptions(options) {
|
|
623
611
|
if (typeof options === 'boolean') {
|
|
624
|
-
return { autoDownload: options,
|
|
612
|
+
return { autoDownload: options, inlineStyles: false, embedFonts: true, forPdf: false }
|
|
625
613
|
}
|
|
626
614
|
return {
|
|
627
615
|
autoDownload: false,
|
|
628
|
-
|
|
616
|
+
inlineStyles: false,
|
|
617
|
+
embedFonts: true,
|
|
618
|
+
forPdf: false,
|
|
629
619
|
...(options || {})
|
|
630
620
|
}
|
|
631
621
|
}
|
|
632
622
|
|
|
633
623
|
/**
|
|
634
|
-
*
|
|
635
|
-
* @param {HTMLElement} rootEl
|
|
636
|
-
* @param {{
|
|
637
|
-
* @param {boolean} [options.fullCss=false] - true 时导出全量 CSS,false 时按 HTML 裁剪未使用规则
|
|
624
|
+
* 构建高保真页面 HTML 快照
|
|
625
|
+
* @param {HTMLElement} rootEl
|
|
626
|
+
* @param {{ autoDownload?: boolean, inlineStyles?: boolean, embedFonts?: boolean, forPdf?: boolean }} [options]
|
|
638
627
|
*/
|
|
639
|
-
export function
|
|
640
|
-
const {
|
|
628
|
+
export async function buildPageHtmlSnapshot(rootEl, options = {}) {
|
|
629
|
+
const { inlineStyles, embedFonts, forPdf } = normalizeOptions(options)
|
|
630
|
+
|
|
641
631
|
if (!rootEl || rootEl.nodeType !== Node.ELEMENT_NODE) {
|
|
642
632
|
return { ok: false, message: '未找到页面内容' }
|
|
643
633
|
}
|
|
644
634
|
|
|
645
|
-
|
|
635
|
+
try {
|
|
636
|
+
if (embedFonts) {
|
|
637
|
+
await ensureIconFontsLoaded()
|
|
638
|
+
}
|
|
639
|
+
|
|
646
640
|
const filename = buildSnapshotFilename()
|
|
647
641
|
const pageTitle = document.title || SNAPSHOT_FILENAME_PREFIX
|
|
648
642
|
const ts = filename.replace(`${SNAPSHOT_FILENAME_PREFIX}-`, '').replace('.html', '')
|
|
649
643
|
|
|
650
|
-
const pageClone = clonePageNode(rootEl
|
|
651
|
-
if (!pageClone) {
|
|
652
|
-
return { ok: false, message: '克隆页面失败' }
|
|
653
|
-
}
|
|
644
|
+
const pageClone = clonePageNode(rootEl)
|
|
645
|
+
if (!pageClone) return { ok: false, message: '克隆页面失败' }
|
|
654
646
|
|
|
655
647
|
unwrapScrollContainers(pageClone)
|
|
648
|
+
removeAnchorNav(pageClone)
|
|
649
|
+
removeBottomOperateButtons(pageClone)
|
|
656
650
|
|
|
657
|
-
const pageHtml =
|
|
658
|
-
if (!pageHtml) {
|
|
659
|
-
return { ok: false, message: '生成页面 HTML 失败' }
|
|
660
|
-
}
|
|
651
|
+
const pageHtml = pageClone.outerHTML
|
|
652
|
+
if (!pageHtml) return { ok: false, message: '生成页面 HTML 失败' }
|
|
661
653
|
|
|
662
|
-
|
|
654
|
+
let css = [
|
|
655
|
+
captureRootCssVariables(),
|
|
656
|
+
stripHeavyFontFaces(collectDocumentCss()),
|
|
657
|
+
SNAPSHOT_BASE_CSS,
|
|
658
|
+
forPdf ? SNAPSHOT_PDF_CSS : ''
|
|
659
|
+
].filter(Boolean).join('\n')
|
|
660
|
+
|
|
661
|
+
if (embedFonts) {
|
|
662
|
+
css = await embedIconFonts(css)
|
|
663
|
+
}
|
|
663
664
|
|
|
664
665
|
const html = `<!DOCTYPE html>
|
|
665
666
|
<html lang="zh-CN">
|
|
@@ -669,12 +670,11 @@ export function buildPageSnapshot(rootEl, options = {}) {
|
|
|
669
670
|
<title>${pageTitle} - ${ts}</title>
|
|
670
671
|
<style>
|
|
671
672
|
${css}
|
|
672
|
-
${SNAPSHOT_PDF_CSS}
|
|
673
673
|
</style>
|
|
674
674
|
</head>
|
|
675
675
|
<body>
|
|
676
676
|
${pageHtml}
|
|
677
|
-
${
|
|
677
|
+
${PAGE_SNAPSHOT_BACKEND_PLACEHOLDER}
|
|
678
678
|
</body>
|
|
679
679
|
</html>`
|
|
680
680
|
|
|
@@ -682,23 +682,40 @@ ${buildBackendPlaceholderSection()}
|
|
|
682
682
|
const file = new File([blob], filename, { type: blob.type })
|
|
683
683
|
|
|
684
684
|
return { ok: true, html, filename, blob, file, ts, pageTitle }
|
|
685
|
-
}
|
|
685
|
+
} catch (e) {
|
|
686
|
+
console.warn('[exportPageHtml] 构建页面快照失败', e)
|
|
687
|
+
return { ok: false, message: '构建页面快照失败' }
|
|
688
|
+
}
|
|
686
689
|
}
|
|
687
690
|
|
|
688
691
|
/**
|
|
689
|
-
* 构建上传用 FormData,字段名默认 file。
|
|
690
692
|
* @param {HTMLElement} rootEl
|
|
691
|
-
* @param {
|
|
692
|
-
* @param {boolean} [options.fullCss=false] - true 时导出全量 CSS,false 时按 HTML 裁剪未使用规则
|
|
693
|
+
* @param {boolean|object} [options]
|
|
693
694
|
*/
|
|
694
|
-
export function
|
|
695
|
-
const
|
|
695
|
+
export async function exportPageHtml(rootEl, options = false) {
|
|
696
|
+
const { autoDownload, ...rest } = normalizeOptions(options)
|
|
697
|
+
const result = await buildPageHtmlSnapshot(rootEl, rest)
|
|
698
|
+
|
|
699
|
+
if (!result.ok) return result
|
|
700
|
+
if (autoDownload) downloadBlob(result.blob, result.filename)
|
|
701
|
+
return result
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** @deprecated 兼容旧名,与 exportPageHtml 相同 */
|
|
705
|
+
export const exportPageSnapshot = exportPageHtml
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* 上传用 FormData
|
|
709
|
+
* @param {HTMLElement} rootEl
|
|
710
|
+
* @param {object} [options]
|
|
711
|
+
*/
|
|
712
|
+
export async function createPageHtmlFormData(rootEl, options = {}) {
|
|
713
|
+
const built = await buildPageHtmlSnapshot(rootEl, options)
|
|
696
714
|
if (!built.ok) return built
|
|
697
715
|
|
|
698
716
|
return safeRun(() => {
|
|
699
717
|
const formData = new FormData()
|
|
700
|
-
formData.append(
|
|
701
|
-
|
|
718
|
+
formData.append('file', built.file, built.filename)
|
|
702
719
|
return {
|
|
703
720
|
ok: true,
|
|
704
721
|
formData,
|
|
@@ -709,21 +726,3 @@ export function createPageSnapshotFormData(rootEl, options = {}) {
|
|
|
709
726
|
}
|
|
710
727
|
}, { ok: false, message: '构建上传数据失败' }, '构建 FormData 失败')
|
|
711
728
|
}
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* @param {HTMLElement} rootEl
|
|
715
|
-
* @param {boolean|{ autoDownload?: boolean, fullCss?: boolean }} [options=false]
|
|
716
|
-
* @param {boolean} [options.autoDownload=false] - true 时额外触发浏览器下载(调试用)
|
|
717
|
-
* @param {boolean} [options.fullCss=false] - true 时导出全量 CSS,false 时按 HTML 裁剪未使用规则
|
|
718
|
-
*/
|
|
719
|
-
export function exportPageSnapshot(rootEl, options = false) {
|
|
720
|
-
const { autoDownload, fullCss } = normalizeSnapshotOptions(options)
|
|
721
|
-
const result = buildPageSnapshot(rootEl, { fullCss })
|
|
722
|
-
if (!result.ok) return result
|
|
723
|
-
|
|
724
|
-
if (autoDownload) {
|
|
725
|
-
downloadBlob(result.blob, result.filename)
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
return result
|
|
729
|
-
}
|