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:
WeiCHEN
2026-09-13 02:50:19 +00:00
committed by weichen
co-authored by Cursor
parent 1a1875bc8c
commit fefb53ef30
4 changed files with 1119 additions and 2 deletions
+298 -1
View File
@@ -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
View File
@@ -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",
+662
View File
@@ -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;
}
+156
View File
@@ -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;
}