@vg-print/plugin-api-word 0.1.0 → 0.1.1
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/CHANGELOG.md +3 -0
- package/README.md +32 -1
- package/package.json +1 -1
- package/src/convert.js +40 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## 0.1.0 — 2026-11-06
|
|
4
4
|
|
|
5
|
+
- 默认用背景同色细实线遮盖布局表格及无边框单元格的编辑网格线,保留模板真实边框和可编辑内容;补充显示效果限制。
|
|
6
|
+
- 核对 Vue 2 / Vue 3 的注册接口与打印核心,确认同一插件通过宿主注入即可共用;补充 `vg-print-vue2` 注册示例与工具链支持范围。
|
|
7
|
+
- 核对布局表格的无边框设置,补充 Word/WPS 编辑网格线和实际打印边框的区别及隐藏方法。
|
|
5
8
|
- 新增 `hiprint.register({ plugins: [pluginApiWord()] })` 注入与 `template.toWord()` 方法。
|
|
6
9
|
- 新增独立 `exportWord()` API,支持下载和返回 DOCX Blob。
|
|
7
10
|
- 基于引擎渲染 DOM 转换原生段落、表格、横纵合并及并排布局;图表/条码等局部图片化。
|
package/README.md
CHANGED
|
@@ -37,6 +37,27 @@ const blob = await template.toWord(printData, {
|
|
|
37
37
|
|
|
38
38
|
必须传入模板实例,原始模板 JSON 请先使用 `createTemplate()` 构造。插件注册前后创建的实例共享同一原型;重复注册只更新默认配置。
|
|
39
39
|
|
|
40
|
+
## Vue 2 / Vue 3 兼容
|
|
41
|
+
|
|
42
|
+
同一个插件包可用于 Vue 3 的 `vg-print` 和 Vue 2 的 `vg-print-vue2`。插件只使用注册入口传入的 `hiprint.PrintTemplate.prototype` 和模板的 `getHtml()`,没有 Vue 依赖,也不会自行导入任一版本的打印引擎。两个项目当前的打印核心源码一致,注册接口、纸页 DOM 和图片预处理接口也一致。
|
|
43
|
+
|
|
44
|
+
Vue 3 使用上面的示例。Vue 2 使用其对应的打印包注册,插件用法相同:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { hiprint, createTemplate } from 'vg-print-vue2'
|
|
48
|
+
import 'vg-print-vue2/style.css'
|
|
49
|
+
import pluginApiWord from '@vg-print/plugin-api-word'
|
|
50
|
+
|
|
51
|
+
// 可放在异步初始化函数中,无需使用 Vue.use()。
|
|
52
|
+
async function exportReport(templateJson, printData) {
|
|
53
|
+
await hiprint.register({ plugins: [pluginApiWord()] })
|
|
54
|
+
const template = createTemplate(templateJson)
|
|
55
|
+
return template.toWord(printData, { name: '报告.docx' })
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
独立 `exportWord()` API 同样适用于两种模板实例,不依赖设计器是否提供 Word 按钮。当前 Vue 2 项目的 Vite 4 工具链支持本包的 ESM 入口;其他旧工具链需要自行配置依赖转译。Vue 2 兼容不代表支持 CommonJS `require` 或 IE 浏览器。
|
|
60
|
+
|
|
40
61
|
也可以不注册,直接使用独立 API:
|
|
41
62
|
|
|
42
63
|
```js
|
|
@@ -100,11 +121,21 @@ const blob = await wordTemplate.toWord(printData, { isDownload: false })
|
|
|
100
121
|
- 读取 `getHtml()` 渲染后的页面,复用字段绑定、格式化、内容隐藏和引擎分页结果。
|
|
101
122
|
- 普通文本、长文本保留可编辑内容、基本字体样式、显式换行和对齐。
|
|
102
123
|
- 动态/静态表格、e2table、w2html 中的 HTML 表格转换为原生 Word 表格,支持列宽、横纵合并、边框、背景和表头。
|
|
103
|
-
-
|
|
124
|
+
- 并排组件用视觉无边框的表格布局;矩形容器按实际包围范围分组,内容编辑后自然撑高。装饰矩形包围其他内容时也会按容器处理。
|
|
104
125
|
- 长文本标题与正文的横向布局转换为双列表格。
|
|
105
126
|
- 图片、条码、二维码、图表、竖线、椭圆作为图片;内部文字和图表数据不可原生编辑。
|
|
106
127
|
- 渲染 DOM 已包含的水印保留为背景图片;编辑型 Word 不是防篡改文件。
|
|
107
128
|
|
|
129
|
+
### Word/WPS 中的灰色虚线
|
|
130
|
+
|
|
131
|
+
并排布局、组件定位和标题/正文对齐使用原生 Word 表格。仅设置“无边框”时,Word/WPS 开启“查看网格线”仍会显示灰色虚线。
|
|
132
|
+
|
|
133
|
+
插件默认在没有模板边框的位置写入与背景同色的 0.5pt 实线,以遮住灰色网格线。白色纸面使用白色实线,有纯色背景的单元格使用其背景色,文字和表格仍可编辑。这是视觉遮盖,不是修改接收方的网格线设置;更改文档背景、使用非纯色背景或编辑器特殊显示模式时可能看到这些同色边框,具体效果仍需在 Word/WPS 中验证。
|
|
134
|
+
|
|
135
|
+
模板本身的横线、矩形边框和表格单元格边框仍会保留。选中表格时的移动柄、插入行列的“+”按钮属于编辑器交互控件,退出表格编辑状态后隐藏,插件不能移除。
|
|
136
|
+
|
|
137
|
+
若特定编辑器仍显示辅助线,可在 Word 的“表格布局(或表格工具 → 布局)”中关闭“查看网格线”;WPS 可在“表格工具”中查找对应选项。
|
|
138
|
+
|
|
108
139
|
不保证任意模板与预览逐像素一致。Word/WPS 的字体度量、行距、自动换行和分页与浏览器不同;自由重叠、旋转文字、复杂 CSS、混合定位和复杂列表不保证还原。未知组件使用图片回退,重叠组件按阅读顺序输出并报告警告。
|
|
109
140
|
|
|
110
141
|
常规报告/规整表单更适合导出;不使用固定行高截断长文本。页眉页脚和页码来自引擎每页的渲染内容,作为每节正文输出,不转换为自动更新的 Word 页码域或原生重复页眉页脚。
|
package/package.json
CHANGED
package/src/convert.js
CHANGED
|
@@ -9,7 +9,6 @@ const twip = px => Math.round(Math.max(0, Number(px) || 0) * 15)
|
|
|
9
9
|
const number = value => Number.parseFloat(value) || 0
|
|
10
10
|
const styleOf = element => getComputedStyle(element)
|
|
11
11
|
const NONE = { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' }
|
|
12
|
-
const NO_BORDERS = { top: NONE, bottom: NONE, left: NONE, right: NONE, insideHorizontal: NONE, insideVertical: NONE }
|
|
13
12
|
const empty = () => new Paragraph({ spacing: { before: 0, after: 0, line: 1 }, children: [new TextRun({ text: '', size: 1 })] })
|
|
14
13
|
const finish = children => {
|
|
15
14
|
// Word 单元格和文档结尾需要段落,嵌套表格后补一个极小空段落。
|
|
@@ -41,6 +40,33 @@ function borders(style) {
|
|
|
41
40
|
}))
|
|
42
41
|
}
|
|
43
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 网格线属于编辑器本机设置,DOCX 不能强制关闭。
|
|
45
|
+
* 用与纸面/单元格背景同色的 0.5pt 实线覆盖无边框处的灰色网格线。
|
|
46
|
+
* 保留模板明确设置的边框;不将文字截图,也不改变组件列宽。
|
|
47
|
+
*/
|
|
48
|
+
function concealedBorders(background = 'FFFFFF') {
|
|
49
|
+
const edge = { style: BorderStyle.SINGLE, size: 4, color: background }
|
|
50
|
+
return { top: edge, bottom: edge, left: edge, right: edge, insideHorizontal: edge, insideVertical: edge }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function backgroundOf(element) {
|
|
54
|
+
for (let node = element; node; node = node.parentElement) {
|
|
55
|
+
const background = color(styleOf(node).backgroundColor)
|
|
56
|
+
if (background) return background
|
|
57
|
+
// 纸页外的应用主题不是 Word 纸面颜色,透明纸面按白色处理。
|
|
58
|
+
if (node.classList.contains('hiprint-printPaper')) break
|
|
59
|
+
}
|
|
60
|
+
return 'FFFFFF'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cellBorders(element) {
|
|
64
|
+
const fallback = concealedBorders(backgroundOf(element))
|
|
65
|
+
return Object.fromEntries(Object.entries(borders(styleOf(element))).map(([side, border]) =>
|
|
66
|
+
[side, border.style === BorderStyle.NONE ? fallback[side] : border]
|
|
67
|
+
))
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
function visible(element) {
|
|
45
71
|
const style = styleOf(element)
|
|
46
72
|
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
|
|
@@ -121,7 +147,7 @@ async function htmlBlocks(root, context, maxWidth) {
|
|
|
121
147
|
const horizontal = items.every((item, index) => items.slice(index + 1).every(other =>
|
|
122
148
|
Math.min(item.right, other.right) - Math.max(item.left, other.left) <= 1
|
|
123
149
|
))
|
|
124
|
-
if (horizontal) return layout(items, Math.min(origin.width || maxWidth, maxWidth), context)
|
|
150
|
+
if (horizontal) return layout(items, Math.min(origin.width || maxWidth, maxWidth), context, backgroundOf(root))
|
|
125
151
|
context.warn('html-layout', '富文本复杂定位已按阅读顺序转换', root, context.pageIndex)
|
|
126
152
|
}
|
|
127
153
|
const output = []
|
|
@@ -180,8 +206,8 @@ async function htmlBlocks(root, context, maxWidth) {
|
|
|
180
206
|
async function htmlTable(table, context, maxWidth) {
|
|
181
207
|
const rows = Array.from(table.rows).filter(visible)
|
|
182
208
|
if (!rows.length) return new Table({
|
|
183
|
-
rows: [new TableRow({ children: [new TableCell({ children: [empty()] })] })],
|
|
184
|
-
borders:
|
|
209
|
+
rows: [new TableRow({ children: [new TableCell({ children: [empty()], borders: concealedBorders(backgroundOf(table)) })] })],
|
|
210
|
+
borders: concealedBorders(backgroundOf(table))
|
|
185
211
|
})
|
|
186
212
|
const occupied = []
|
|
187
213
|
const placements = rows.map((row, rowIndex) => {
|
|
@@ -234,7 +260,7 @@ async function htmlTable(table, context, maxWidth) {
|
|
|
234
260
|
columnSpan: colSpan,
|
|
235
261
|
rowSpan,
|
|
236
262
|
width: { size: cellWidth, type: WidthType.DXA },
|
|
237
|
-
borders:
|
|
263
|
+
borders: cellBorders(cell),
|
|
238
264
|
margins: {
|
|
239
265
|
top: twip(number(style.paddingTop)), bottom: twip(number(style.paddingBottom)),
|
|
240
266
|
left: twip(number(style.paddingLeft)), right: twip(number(style.paddingRight))
|
|
@@ -249,7 +275,7 @@ async function htmlTable(table, context, maxWidth) {
|
|
|
249
275
|
height: { value: twip(rows[rowIndex].getBoundingClientRect().height), rule: HeightRule.ATLEAST }
|
|
250
276
|
}))
|
|
251
277
|
}
|
|
252
|
-
return new Table({ rows: result, width: { size: twip(width), type: WidthType.DXA }, columnWidths: widths, layout: TableLayoutType.FIXED, borders:
|
|
278
|
+
return new Table({ rows: result, width: { size: twip(width), type: WidthType.DXA }, columnWidths: widths, layout: TableLayoutType.FIXED, borders: concealedBorders(backgroundOf(table)) })
|
|
253
279
|
}
|
|
254
280
|
|
|
255
281
|
function elementType(element) {
|
|
@@ -304,7 +330,7 @@ async function content(item, context) {
|
|
|
304
330
|
return [new Paragraph({ border: { bottom: border }, spacing: { before: 0, after: 0, line: 1 }, children: [new TextRun({ text: '', size: 1 })] })]
|
|
305
331
|
}
|
|
306
332
|
if (type === 'container') {
|
|
307
|
-
if (item.children?.length) return layout(item.children, item.width, context)
|
|
333
|
+
if (item.children?.length) return layout(item.children, item.width, context, backgroundOf(element))
|
|
308
334
|
return [empty()]
|
|
309
335
|
}
|
|
310
336
|
if (type !== 'content') throw new Error(`不支持的 Word 组件类型:${type}`)
|
|
@@ -314,8 +340,9 @@ async function content(item, context) {
|
|
|
314
340
|
return htmlBlocks(contentRoot, context, item.width)
|
|
315
341
|
}
|
|
316
342
|
|
|
317
|
-
async function layout(items, width, context) {
|
|
343
|
+
async function layout(items, width, context, background = 'FFFFFF') {
|
|
318
344
|
const result = []
|
|
345
|
+
const layoutBorders = concealedBorders(background)
|
|
319
346
|
let previousBottom = 0
|
|
320
347
|
for (const row of groupRows(items, context)) {
|
|
321
348
|
if (row.top > previousBottom + 1) result.push(spacer(row.top - previousBottom))
|
|
@@ -328,7 +355,7 @@ async function layout(items, width, context) {
|
|
|
328
355
|
}
|
|
329
356
|
if (item.left > cursor + 1) {
|
|
330
357
|
const gap = twip(item.left - cursor)
|
|
331
|
-
cells.push(new TableCell({ children: [empty()], borders:
|
|
358
|
+
cells.push(new TableCell({ children: [empty()], borders: layoutBorders, width: { size: gap, type: WidthType.DXA } }))
|
|
332
359
|
widths.push(gap)
|
|
333
360
|
}
|
|
334
361
|
const available = Math.max(1, Math.min(item.width, width - Math.max(0, item.left)))
|
|
@@ -339,7 +366,7 @@ async function layout(items, width, context) {
|
|
|
339
366
|
cells.push(new TableCell({
|
|
340
367
|
children: finish(children),
|
|
341
368
|
width: { size: cellWidth, type: WidthType.DXA },
|
|
342
|
-
borders:
|
|
369
|
+
borders: cellBorders(item.element),
|
|
343
370
|
margins: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
344
371
|
shading: color(style.backgroundColor) ? { fill: color(style.backgroundColor) } : undefined,
|
|
345
372
|
verticalAlign: 'top'
|
|
@@ -349,7 +376,7 @@ async function layout(items, width, context) {
|
|
|
349
376
|
}
|
|
350
377
|
if (cursor < width - 1) {
|
|
351
378
|
const gap = twip(width - cursor)
|
|
352
|
-
cells.push(new TableCell({ children: [empty()], borders:
|
|
379
|
+
cells.push(new TableCell({ children: [empty()], borders: layoutBorders, width: { size: gap, type: WidthType.DXA } }))
|
|
353
380
|
widths.push(gap)
|
|
354
381
|
}
|
|
355
382
|
result.push(new Table({
|
|
@@ -357,7 +384,7 @@ async function layout(items, width, context) {
|
|
|
357
384
|
columnWidths: widths,
|
|
358
385
|
width: { size: twip(width), type: WidthType.DXA },
|
|
359
386
|
layout: TableLayoutType.FIXED,
|
|
360
|
-
borders:
|
|
387
|
+
borders: layoutBorders,
|
|
361
388
|
margins: { top: 0, bottom: 0, left: 0, right: 0 }
|
|
362
389
|
}))
|
|
363
390
|
// 不强制写入设计高度;后续行由 Word 按实际内容高度推动。
|
|
@@ -400,7 +427,7 @@ export async function convertPage(page, context) {
|
|
|
400
427
|
}
|
|
401
428
|
const roots = items.filter(item => !parentOf.has(item))
|
|
402
429
|
roots.forEach(relative)
|
|
403
|
-
const children = await layout(roots, page.width, context)
|
|
430
|
+
const children = await layout(roots, page.width, context, backgroundOf(page.element))
|
|
404
431
|
// 引擎生成的水印保留为背景图片,正文文字和表格仍然可以编辑。
|
|
405
432
|
const watermark = Array.from(page.element.querySelectorAll('[class]')).find(el =>
|
|
406
433
|
Array.from(el.classList).some(name => name.startsWith('__vm__'))
|