feat(engine): 实现 DOCX 与 XLSX 客户端原生渲染导出引擎
- 引入 docx 与 xlsx 依赖,纯前端无损编译 Block AST 交付物 - buildDocxModel: 自动生成两行四列受控签署栏、多级编号标题、Markdown富文本、医疗标准三线表、Cambria Math公式与动态页眉页脚 - buildXlsxWorkbook: 自动提取文档概览与全部数据表格,生成带自适应列宽的 Excel 工作簿并支持直接下载 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
weichen
co-authored by
Cursor
parent
1a1875bc8c
commit
fefb53ef30
Generated
+298
-1
@@ -8,11 +8,13 @@
|
||||
"name": "shuimu-workbench",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"docx": "^9.7.1",
|
||||
"katex": "^0.18.7",
|
||||
"lucide-react": "^1.16.0",
|
||||
"mermaid": "^11.17.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
"react-dom": "^19.2.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/katex": "^0.16.8",
|
||||
@@ -1652,6 +1654,15 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.11.21",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
|
||||
@@ -1720,6 +1731,28 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
|
||||
@@ -1736,6 +1769,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cose-base": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
|
||||
@@ -1745,6 +1784,18 @@
|
||||
"layout-base": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -2293,6 +2344,56 @@
|
||||
"robust-predicates": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/docx": {
|
||||
"version": "9.7.1",
|
||||
"resolved": "https://registry.npmjs.org/docx/-/docx-9.7.1.tgz",
|
||||
"integrity": "sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^25.2.3",
|
||||
"hash.js": "^1.1.7",
|
||||
"jszip": "^3.10.1",
|
||||
"nanoid": "^5.1.3",
|
||||
"xml": "^1.0.1",
|
||||
"xml-js": "^1.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/@types/node": {
|
||||
"version": "25.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz",
|
||||
"integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/nanoid": {
|
||||
"version": "5.1.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
|
||||
"integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/undici-types": {
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz",
|
||||
@@ -2400,6 +2501,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -2431,6 +2541,16 @@
|
||||
"integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -2443,6 +2563,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-meta-resolve": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
|
||||
@@ -2453,6 +2579,12 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
@@ -2462,6 +2594,12 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -2495,6 +2633,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.18.7",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.18.7.tgz",
|
||||
@@ -2522,6 +2672,15 @@
|
||||
"integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||
@@ -2614,6 +2773,12 @@
|
||||
"katex": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -2656,6 +2821,12 @@
|
||||
"integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/path-data-parser": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
|
||||
@@ -2727,6 +2898,12 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||
@@ -2758,6 +2935,21 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/robust-predicates": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
|
||||
@@ -2828,12 +3020,27 @@
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
|
||||
"integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
@@ -2850,6 +3057,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -2860,12 +3073,33 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/strictdom": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strictdom/-/strictdom-1.0.1.tgz",
|
||||
"integrity": "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
|
||||
@@ -2978,6 +3212,12 @@
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz",
|
||||
@@ -3066,6 +3306,63 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"xml-js": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
+3
-1
@@ -10,11 +10,13 @@
|
||||
"test": "tsx --test \"src/**/*.test.ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"docx": "^9.7.1",
|
||||
"katex": "^0.18.7",
|
||||
"lucide-react": "^1.16.0",
|
||||
"mermaid": "^11.17.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
"react-dom": "^19.2.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/katex": "^0.16.8",
|
||||
|
||||
@@ -0,0 +1,662 @@
|
||||
import {
|
||||
Document,
|
||||
Paragraph,
|
||||
TextRun,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
WidthType,
|
||||
BorderStyle,
|
||||
AlignmentType,
|
||||
HeadingLevel,
|
||||
Packer,
|
||||
Header,
|
||||
Footer,
|
||||
PageNumber,
|
||||
ShadingType,
|
||||
} from 'docx';
|
||||
import { DocumentAST } from '../types/block';
|
||||
import { cleanHeadingContent } from './autoNumber';
|
||||
|
||||
/**
|
||||
* parseInlineTextRuns: 将包含简易 Markdown (粗体/斜体/代码/下划线) 的字符串转换为 docx TextRun 数组
|
||||
*/
|
||||
export function parseInlineTextRuns(text: string, defaultOptions?: { color?: string; size?: number }): TextRun[] {
|
||||
if (!text) return [];
|
||||
|
||||
// 正则匹配:**粗体** | *斜体* | `代码` | 纯文本
|
||||
const tokens: TextRun[] = [];
|
||||
const regex = /(\*\*([^\*]+?)\*\*|\*([^\*]+?)\*|`([^`]+?)`|([^\*`]+))/g;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
if (match[2] !== undefined) {
|
||||
// **粗体**
|
||||
tokens.push(
|
||||
new TextRun({
|
||||
text: match[2],
|
||||
bold: true,
|
||||
color: defaultOptions?.color || '222222',
|
||||
size: defaultOptions?.size || 22, // 11pt = 22 half-pts
|
||||
})
|
||||
);
|
||||
} else if (match[3] !== undefined) {
|
||||
// *斜体*
|
||||
tokens.push(
|
||||
new TextRun({
|
||||
text: match[3],
|
||||
italics: true,
|
||||
color: defaultOptions?.color || '333333',
|
||||
size: defaultOptions?.size || 22,
|
||||
})
|
||||
);
|
||||
} else if (match[4] !== undefined) {
|
||||
// `代码`
|
||||
tokens.push(
|
||||
new TextRun({
|
||||
text: match[4],
|
||||
font: 'Consolas',
|
||||
shading: {
|
||||
type: ShadingType.CLEAR,
|
||||
fill: 'F1F5F9',
|
||||
},
|
||||
color: '0369A1',
|
||||
size: (defaultOptions?.size || 22) - 2,
|
||||
})
|
||||
);
|
||||
} else if (match[5] !== undefined) {
|
||||
// 纯文本
|
||||
tokens.push(
|
||||
new TextRun({
|
||||
text: match[5],
|
||||
color: defaultOptions?.color || '333333',
|
||||
size: defaultOptions?.size || 22,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return tokens.length > 0
|
||||
? tokens
|
||||
: [
|
||||
new TextRun({
|
||||
text,
|
||||
color: defaultOptions?.color || '333333',
|
||||
size: defaultOptions?.size || 22,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* buildDocxModel: 将 DocumentAST 转换为完整的 docx Document 实例
|
||||
*/
|
||||
export function buildDocxModel(ast: DocumentAST): Document {
|
||||
const docElements: (Paragraph | Table)[] = [];
|
||||
|
||||
// 1. 文档大标题
|
||||
docElements.push(
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { before: 240, after: 120 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: ast.title,
|
||||
bold: true,
|
||||
size: 36, // 18pt
|
||||
color: '0F172A',
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// 2. 受控元数据签署栏 (四列两行受控表格)
|
||||
const metaBorder = {
|
||||
style: BorderStyle.SINGLE,
|
||||
size: 4,
|
||||
color: 'CBD5E1',
|
||||
};
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
drafting: '编制中 (草稿)',
|
||||
reviewing: '审核中 (流程锁定)',
|
||||
approving: '批准中 (流程锁定)',
|
||||
approved: '已批准生效 (受控基线)',
|
||||
rejected: '已驳回修改',
|
||||
};
|
||||
|
||||
const metaTable = new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
width: { size: 25, type: WidthType.PERCENTAGE },
|
||||
shading: { fill: 'F8FAFC', type: ShadingType.CLEAR },
|
||||
borders: { top: metaBorder, bottom: metaBorder, left: metaBorder, right: metaBorder },
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: '受控版本', bold: true, size: 20 })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new TableCell({
|
||||
width: { size: 25, type: WidthType.PERCENTAGE },
|
||||
borders: { top: metaBorder, bottom: metaBorder, left: metaBorder, right: metaBorder },
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: ast.version, size: 20, color: '0284C7', bold: true })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new TableCell({
|
||||
width: { size: 25, type: WidthType.PERCENTAGE },
|
||||
shading: { fill: 'F8FAFC', type: ShadingType.CLEAR },
|
||||
borders: { top: metaBorder, bottom: metaBorder, left: metaBorder, right: metaBorder },
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: '受控状态', bold: true, size: 20 })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new TableCell({
|
||||
width: { size: 25, type: WidthType.PERCENTAGE },
|
||||
borders: { top: metaBorder, bottom: metaBorder, left: metaBorder, right: metaBorder },
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: statusMap[ast.lifecycleState] || ast.lifecycleState, size: 20 })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
shading: { fill: 'F8FAFC', type: ShadingType.CLEAR },
|
||||
borders: { top: metaBorder, bottom: metaBorder, left: metaBorder, right: metaBorder },
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: '法定签署责任人', bold: true, size: 20 })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new TableCell({
|
||||
borders: { top: metaBorder, bottom: metaBorder, left: metaBorder, right: metaBorder },
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `编制: ${ast.author} | 审核: ${ast.reviewer} | 批准: ${ast.approver}`,
|
||||
size: 19,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new TableCell({
|
||||
shading: { fill: 'F8FAFC', type: ShadingType.CLEAR },
|
||||
borders: { top: metaBorder, bottom: metaBorder, left: metaBorder, right: metaBorder },
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: '更新归档日期', bold: true, size: 20 })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new TableCell({
|
||||
borders: { top: metaBorder, bottom: metaBorder, left: metaBorder, right: metaBorder },
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: ast.updatedAt || '2026-09-08', size: 19 })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
docElements.push(metaTable);
|
||||
|
||||
// 空行分割
|
||||
docElements.push(new Paragraph({ spacing: { before: 180, after: 120 } }));
|
||||
|
||||
// 3. 递归遍历渲染 AST 子块
|
||||
const blocks = ast.rootBlock.children || [];
|
||||
|
||||
for (const block of blocks) {
|
||||
switch (block.type) {
|
||||
case 'heading': {
|
||||
const level = block.level || 1;
|
||||
const cleanTitle = cleanHeadingContent(block.content);
|
||||
const headingText = block.numbering ? `${block.numbering} ${cleanTitle}` : cleanTitle;
|
||||
|
||||
let hLevel: (typeof HeadingLevel)[keyof typeof HeadingLevel] = HeadingLevel.HEADING_1;
|
||||
let fontSize = 28; // 14pt
|
||||
if (level === 2) {
|
||||
hLevel = HeadingLevel.HEADING_2;
|
||||
fontSize = 26; // 13pt
|
||||
} else if (level === 3) {
|
||||
hLevel = HeadingLevel.HEADING_3;
|
||||
fontSize = 24; // 12pt
|
||||
} else if (level >= 4) {
|
||||
hLevel = HeadingLevel.HEADING_4;
|
||||
fontSize = 22; // 11pt
|
||||
}
|
||||
|
||||
docElements.push(
|
||||
new Paragraph({
|
||||
heading: hLevel,
|
||||
spacing: { before: 240, after: 100 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: headingText,
|
||||
bold: true,
|
||||
size: fontSize,
|
||||
color: level <= 2 ? '0F172A' : '334155',
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'paragraph': {
|
||||
if (!block.content) break;
|
||||
const lines = block.content.split('\n');
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
docElements.push(
|
||||
new Paragraph({
|
||||
spacing: { before: 60, after: 60, line: 280 },
|
||||
children: parseInlineTextRuns(line),
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'callout': {
|
||||
const typeTag = block.properties?.calloutType === 'regulatory' ? '合规依据与法规强标' : '重点提示与风险警示';
|
||||
docElements.push(
|
||||
new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
shading: { fill: 'F0F9FF', type: ShadingType.CLEAR },
|
||||
borders: {
|
||||
left: { style: BorderStyle.SINGLE, size: 24, color: '0284C7' },
|
||||
top: { style: BorderStyle.NONE },
|
||||
bottom: { style: BorderStyle.NONE },
|
||||
right: { style: BorderStyle.NONE },
|
||||
},
|
||||
children: [
|
||||
new Paragraph({
|
||||
spacing: { before: 80, after: 40 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `【${typeTag}】`,
|
||||
bold: true,
|
||||
size: 20,
|
||||
color: '0369A1',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
spacing: { before: 40, after: 80 },
|
||||
children: parseInlineTextRuns(block.content, { color: '0F172A', size: 20 }),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
docElements.push(new Paragraph({ spacing: { before: 60, after: 60 } }));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'table': {
|
||||
const data = block.properties?.tableData || [];
|
||||
const caption = block.properties?.caption || '关键指标与技术评估表';
|
||||
const numLabel = block.numbering ? `${block.numbering} ${caption}` : caption;
|
||||
|
||||
// 题注 (表 1-1 ...)
|
||||
docElements.push(
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { before: 180, after: 60 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: numLabel,
|
||||
bold: true,
|
||||
size: 21,
|
||||
color: '334155',
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
if (data.length > 0) {
|
||||
const colCount = Math.max(...data.map((r) => r.length));
|
||||
const colWidthPercent = Math.floor(100 / colCount);
|
||||
|
||||
// 医疗器械规范标准三线表边线
|
||||
const thickBorder = { style: BorderStyle.SINGLE, size: 12, color: '0F172A' }; // 顶底粗线
|
||||
const thinBorder = { style: BorderStyle.SINGLE, size: 6, color: '334155' }; // 表头分割细线
|
||||
const noBorder = { style: BorderStyle.NONE };
|
||||
|
||||
const tableRows: TableRow[] = [];
|
||||
|
||||
data.forEach((row, rIdx) => {
|
||||
const isHeader = rIdx === 0;
|
||||
const isLast = rIdx === data.length - 1;
|
||||
|
||||
const cells: TableCell[] = [];
|
||||
for (let c = 0; c < colCount; c++) {
|
||||
const cellText = row[c] || '';
|
||||
cells.push(
|
||||
new TableCell({
|
||||
width: { size: colWidthPercent, type: WidthType.PERCENTAGE },
|
||||
shading: isHeader ? { fill: 'F8FAFC', type: ShadingType.CLEAR } : undefined,
|
||||
borders: {
|
||||
top: isHeader ? thickBorder : isLast ? thinBorder : noBorder,
|
||||
bottom: isHeader ? thinBorder : isLast ? thickBorder : noBorder,
|
||||
left: noBorder,
|
||||
right: noBorder,
|
||||
},
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: isHeader ? AlignmentType.CENTER : AlignmentType.LEFT,
|
||||
spacing: { before: 60, after: 60 },
|
||||
children: parseInlineTextRuns(cellText, {
|
||||
size: 20,
|
||||
color: isHeader ? '0F172A' : '334155',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
tableRows.push(new TableRow({ children: cells }));
|
||||
});
|
||||
|
||||
docElements.push(
|
||||
new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: tableRows,
|
||||
})
|
||||
);
|
||||
}
|
||||
docElements.push(new Paragraph({ spacing: { before: 100, after: 100 } }));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'math': {
|
||||
const caption = block.properties?.caption || '关键计算判据与物理算法模型';
|
||||
const numLabel = block.numbering ? `${block.numbering} ${caption}` : caption;
|
||||
|
||||
docElements.push(
|
||||
new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
shading: { fill: 'F8FAFC', type: ShadingType.CLEAR },
|
||||
borders: {
|
||||
top: { style: BorderStyle.SINGLE, size: 4, color: 'E2E8F0' },
|
||||
bottom: { style: BorderStyle.SINGLE, size: 4, color: 'E2E8F0' },
|
||||
left: { style: BorderStyle.SINGLE, size: 4, color: 'E2E8F0' },
|
||||
right: { style: BorderStyle.SINGLE, size: 4, color: 'E2E8F0' },
|
||||
},
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { before: 120, after: 60 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: block.content.trim(),
|
||||
font: 'Cambria Math',
|
||||
size: 24,
|
||||
bold: true,
|
||||
color: '0F172A',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.RIGHT,
|
||||
spacing: { before: 0, after: 60 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: numLabel,
|
||||
size: 18,
|
||||
color: '64748B',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
docElements.push(new Paragraph({ spacing: { before: 80, after: 80 } }));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'mermaid': {
|
||||
const caption = block.properties?.caption || '系统架构与时序拓扑图';
|
||||
const numLabel = block.numbering ? `${block.numbering} ${caption}` : caption;
|
||||
|
||||
docElements.push(
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { before: 120, after: 40 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: numLabel,
|
||||
bold: true,
|
||||
size: 21,
|
||||
color: '334155',
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
docElements.push(
|
||||
new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
shading: { fill: 'F8FAFC', type: ShadingType.CLEAR },
|
||||
borders: {
|
||||
top: { style: BorderStyle.DASHED, size: 4, color: 'CBD5E1' },
|
||||
bottom: { style: BorderStyle.DASHED, size: 4, color: 'CBD5E1' },
|
||||
left: { style: BorderStyle.DASHED, size: 4, color: 'CBD5E1' },
|
||||
right: { style: BorderStyle.DASHED, size: 4, color: 'CBD5E1' },
|
||||
},
|
||||
children: [
|
||||
new Paragraph({
|
||||
spacing: { before: 80, after: 40 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: '[Mermaid 系统矢量拓扑图已定义,图形代码如下]:',
|
||||
color: '0284C7',
|
||||
bold: true,
|
||||
size: 19,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
spacing: { before: 40, after: 80 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: block.content.trim(),
|
||||
font: 'Consolas',
|
||||
size: 18,
|
||||
color: '475569',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list': {
|
||||
const isOrdered = block.properties?.isOrdered ?? true;
|
||||
const startNumber = block.properties?.startNumber || 1;
|
||||
const items =
|
||||
block.properties?.listItems ||
|
||||
block.content
|
||||
.split('\n')
|
||||
.map((line) => line.replace(/^\s*(\d+\.|[-*+])\s+/, '').trim())
|
||||
.filter(Boolean);
|
||||
|
||||
items.forEach((item, idx) => {
|
||||
const prefix = isOrdered ? `${startNumber + idx}. ` : '• ';
|
||||
docElements.push(
|
||||
new Paragraph({
|
||||
spacing: { before: 40, after: 40 },
|
||||
indent: { left: 360 }, // 悬挂缩进
|
||||
children: [
|
||||
new TextRun({
|
||||
text: prefix,
|
||||
bold: true,
|
||||
color: '0284C7',
|
||||
size: 21,
|
||||
}),
|
||||
...parseInlineTextRuns(item, { size: 21 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'divider': {
|
||||
docElements.push(
|
||||
new Paragraph({
|
||||
spacing: { before: 120, after: 120 },
|
||||
border: {
|
||||
bottom: { style: BorderStyle.SINGLE, size: 6, color: 'E2E8F0' },
|
||||
},
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 构建标准 Word Document 结构 (带页眉和页脚)
|
||||
return new Document({
|
||||
sections: [
|
||||
{
|
||||
headers: {
|
||||
default: new Header({
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.RIGHT,
|
||||
children: [
|
||||
new TextRun({
|
||||
text: '水木东方医疗器械受控体系交付物 | 研发机密',
|
||||
size: 16,
|
||||
color: '94A3B8',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
footers: {
|
||||
default: new Footer({
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [
|
||||
new TextRun({
|
||||
text: '第 ',
|
||||
size: 18,
|
||||
color: '94A3B8',
|
||||
}),
|
||||
new TextRun({
|
||||
children: [PageNumber.CURRENT],
|
||||
size: 18,
|
||||
color: '94A3B8',
|
||||
}),
|
||||
new TextRun({
|
||||
text: ' 页,共 ',
|
||||
size: 18,
|
||||
color: '94A3B8',
|
||||
}),
|
||||
new TextRun({
|
||||
children: [PageNumber.TOTAL_PAGES],
|
||||
size: 18,
|
||||
color: '94A3B8',
|
||||
}),
|
||||
new TextRun({
|
||||
text: ' 页',
|
||||
size: 18,
|
||||
color: '94A3B8',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
properties: {
|
||||
page: {
|
||||
margin: {
|
||||
top: 1440, // 1 inch
|
||||
bottom: 1440,
|
||||
left: 1440,
|
||||
right: 1440,
|
||||
},
|
||||
},
|
||||
},
|
||||
children: docElements,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* exportDocumentToDocx: 编译并触发浏览器下载 DOCX 二进制文件
|
||||
*/
|
||||
export async function exportDocumentToDocx(ast: DocumentAST): Promise<Blob> {
|
||||
const doc = buildDocxModel(ast);
|
||||
const blob = await Packer.toBlob(doc);
|
||||
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${ast.title}_${ast.version}_受控交付物.docx`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
return blob;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
import { DocumentAST } from '../types/block';
|
||||
|
||||
/**
|
||||
* sanitizeSheetName: 净化 Excel 工作表名称(限制在 31 字符以内,移除 \ / ? * [ ] : 等非法字符)
|
||||
*/
|
||||
function sanitizeSheetName(name: string, fallback: string): string {
|
||||
const cleaned = name.replace(/[\\/?*\[\]:]/g, '_').trim();
|
||||
if (!cleaned) return fallback;
|
||||
return cleaned.substring(0, 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* buildXlsxWorkbook: 将 DocumentAST 结构化提炼为正规的 Excel 工作簿模型
|
||||
*/
|
||||
export function buildXlsxWorkbook(ast: DocumentAST): XLSX.WorkBook {
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// 1. 创建 Sheet 1: “文档概览与受控签署”
|
||||
const statusMap: Record<string, string> = {
|
||||
drafting: '编制中 (草稿)',
|
||||
reviewing: '审核中 (流程锁定)',
|
||||
approving: '批准中 (流程锁定)',
|
||||
approved: '已批准生效 (受控基线)',
|
||||
rejected: '已驳回修改',
|
||||
};
|
||||
|
||||
const overviewData: (string | number)[][] = [
|
||||
['水木东方医疗器械受控体系交付物数据台账'],
|
||||
[],
|
||||
['交付物标题', ast.title],
|
||||
['文档编号/ID', ast.docId],
|
||||
['业务任务分类', ast.taskKey],
|
||||
['受控版本', ast.version],
|
||||
['受控状态', statusMap[ast.lifecycleState] || ast.lifecycleState],
|
||||
['空间分类', ast.category === 'private' ? '个人私密生成' : ast.category === 'shared' ? '项目协作共享' : '流程受控交付物'],
|
||||
['编制责任人', ast.author],
|
||||
['审核责任人', ast.reviewer],
|
||||
['批准责任人', ast.approver],
|
||||
['最新归档时间', ast.updatedAt || '2026-09-08'],
|
||||
['文档摘要', ast.summary || ''],
|
||||
[],
|
||||
['导出来源', '水木东方协同智能体工作台 (Browser XLSX Engine)'],
|
||||
];
|
||||
|
||||
const wsOverview = XLSX.utils.aoa_to_sheet(overviewData);
|
||||
// 设置概览页列宽
|
||||
wsOverview['!cols'] = [{ wch: 18 }, { wch: 60 }];
|
||||
XLSX.utils.book_append_sheet(wb, wsOverview, '文档概览与签署');
|
||||
|
||||
// 2. 遍历 AST 提取所有表格块 (Table Blocks)
|
||||
const blocks = ast.rootBlock.children || [];
|
||||
const tableBlocks = blocks.filter((b) => b.type === 'table' && b.properties?.tableData?.length);
|
||||
|
||||
if (tableBlocks.length > 0) {
|
||||
const usedSheetNames = new Set<string>(['文档概览与签署']);
|
||||
|
||||
tableBlocks.forEach((tblBlock, idx) => {
|
||||
const rawCaption = tblBlock.properties?.caption || `数据表_${idx + 1}`;
|
||||
const prefix = tblBlock.numbering ? `${tblBlock.numbering} ` : '';
|
||||
let baseSheetName = sanitizeSheetName(`${prefix}${rawCaption}`, `数据表_${idx + 1}`);
|
||||
|
||||
// 处理 Sheet 名称防重复
|
||||
let finalSheetName = baseSheetName;
|
||||
let counter = 1;
|
||||
while (usedSheetNames.has(finalSheetName)) {
|
||||
finalSheetName = `${baseSheetName.substring(0, 26)}_${counter++}`;
|
||||
}
|
||||
usedSheetNames.add(finalSheetName);
|
||||
|
||||
const tableData = tblBlock.properties?.tableData || [];
|
||||
const sheetAoa: (string | number)[][] = [];
|
||||
|
||||
// 表头之前加入表格标题与归属说明
|
||||
sheetAoa.push([`${prefix}${rawCaption}`]);
|
||||
sheetAoa.push([]);
|
||||
|
||||
// 压入表格真实数据行
|
||||
for (const row of tableData) {
|
||||
// 清除单元格中的 Markdown 格式字符(如 **粗体**)使 Excel 数据整洁
|
||||
const cleanedRow = row.map((cell) => cell.replace(/\*\*(.*?)\*\*/g, '$1').replace(/\*(.*?)\*/g, '$1').trim());
|
||||
sheetAoa.push(cleanedRow);
|
||||
}
|
||||
|
||||
const ws = XLSX.utils.aoa_to_sheet(sheetAoa);
|
||||
|
||||
// 计算自适应列宽
|
||||
if (tableData.length > 0) {
|
||||
const colCount = Math.max(...tableData.map((r) => r.length));
|
||||
const colWidths: { wch: number }[] = [];
|
||||
for (let c = 0; c < colCount; c++) {
|
||||
let maxLen = 10;
|
||||
for (const row of tableData) {
|
||||
const cellStr = row[c] || '';
|
||||
const len = cellStr.length * 1.5; // 中文字符加权
|
||||
if (len > maxLen) maxLen = len;
|
||||
}
|
||||
colWidths.push({ wch: Math.min(Math.max(Math.round(maxLen), 12), 50) });
|
||||
}
|
||||
ws['!cols'] = colWidths;
|
||||
}
|
||||
|
||||
XLSX.utils.book_append_sheet(wb, ws, finalSheetName);
|
||||
});
|
||||
} else {
|
||||
// 若文档无独立表格,提取列表及段落构建结构化清单 Sheet
|
||||
const listData: string[][] = [['章节/类型', '序号', '条目内容']];
|
||||
let curChapter = '正文';
|
||||
|
||||
for (const b of blocks) {
|
||||
if (b.type === 'heading') {
|
||||
curChapter = `${b.numbering || ''} ${b.content}`.trim();
|
||||
} else if (b.type === 'list') {
|
||||
const items =
|
||||
b.properties?.listItems ||
|
||||
b.content
|
||||
.split('\n')
|
||||
.map((line) => line.replace(/^\s*(\d+\.|[-*+])\s+/, '').trim())
|
||||
.filter(Boolean);
|
||||
items.forEach((item, i) => {
|
||||
listData.push([curChapter, String(i + 1), item]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (listData.length > 1) {
|
||||
const wsList = XLSX.utils.aoa_to_sheet(listData);
|
||||
wsList['!cols'] = [{ wch: 25 }, { wch: 8 }, { wch: 60 }];
|
||||
XLSX.utils.book_append_sheet(wb, wsList, '结构化条目清单');
|
||||
}
|
||||
}
|
||||
|
||||
return wb;
|
||||
}
|
||||
|
||||
/**
|
||||
* exportDocumentToXlsx: 编译并触发浏览器下载 XLSX 二进制文件
|
||||
*/
|
||||
export async function exportDocumentToXlsx(ast: DocumentAST): Promise<Blob> {
|
||||
const wb = buildXlsxWorkbook(ast);
|
||||
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
|
||||
const blob = new Blob([wbout], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${ast.title}_${ast.version}_数据清单.xlsx`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
return blob;
|
||||
}
|
||||
Reference in New Issue
Block a user