Jelajahi Sumber

fix(web-admin): polyfill Buffer/global for contract Word export in Vite

html-to-docx still reads Node globals after Vite rebundles; inject buffer polyfill, map global to globalThis, and alias the package to the browser build.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 1 bulan lalu
induk
melakukan
f872333df6

+ 1 - 0
web-admin/package.json

@@ -67,6 +67,7 @@
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "next",
     "axios": "^1.12.2",
+    "buffer": "^6.0.3",
     "crypto-js": "^4.2.0",
     "dagre": "^0.8.5",
     "echarts": "^6.0.0",

+ 11 - 0
web-admin/pnpm-lock.yaml

@@ -47,6 +47,9 @@ importers:
       axios:
         specifier: ^1.12.2
         version: 1.12.2
+      buffer:
+        specifier: ^6.0.3
+        version: 6.0.3
       crypto-js:
         specifier: ^4.2.0
         version: 4.2.0
@@ -1743,6 +1746,9 @@ packages:
   buffer@5.7.1:
     resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
 
+  buffer@6.0.3:
+    resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
   buffers@0.1.1:
     resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==}
     engines: {node: '>=0.2.0'}
@@ -6089,6 +6095,11 @@ snapshots:
       base64-js: 1.5.1
       ieee754: 1.2.1
 
+  buffer@6.0.3:
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+
   buffers@0.1.1: {}
 
   bundle-name@4.1.0:

+ 12 - 0
web-admin/src/env.d.ts

@@ -17,6 +17,18 @@ declare module 'vue-img-cutter'
 
 declare module 'file-saver'
 
+declare module 'buffer'
+
+declare module '@turbodocx/html-to-docx' {
+  const HTMLtoDOCX: (
+    htmlString: string,
+    headerHTMLString?: string | null,
+    documentOptions?: Record<string, unknown>,
+    footerHTMLString?: string | null
+  ) => Promise<Blob | ArrayBuffer>
+  export default HTMLtoDOCX
+}
+
 declare module '@turbodocx/html-to-docx/dist/html-to-docx.browser.esm.js' {
   const HTMLtoDOCX: (
     htmlString: string,

+ 24 - 3
web-admin/src/utils/crm/contract-html-to-docx.ts

@@ -1,5 +1,7 @@
 /** 合同打印弹窗:HTML → DOCX 预处理与生成 */
 
+import { Buffer } from 'buffer'
+
 const DOCX_MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
 
 type HtmlToDocxFn = (
@@ -9,13 +11,30 @@ type HtmlToDocxFn = (
   footerHTMLString?: string | null
 ) => Promise<Blob | ArrayBuffer>
 
+/**
+ * html-to-docx(含 browser 包)在 Vite 二次打包后仍会直接读全局 Buffer/global。
+ * 必须在调用转换前注入,否则 buildImage 报 Buffer is not defined,随后 global is not defined。
+ */
+function ensureDocxNodeGlobals(): void {
+  const g = globalThis as typeof globalThis & {
+    Buffer?: typeof Buffer
+    global?: typeof globalThis
+    process?: { env?: Record<string, string | undefined> }
+  }
+  if (!g.Buffer) g.Buffer = Buffer
+  if (!g.global) g.global = g
+  if (!g.process) g.process = { env: {} }
+  else if (!g.process.env) g.process.env = {}
+}
+
 /**
  * 懒加载 browser 构建。
- * Vite 默认会解析到 Node ESM(依赖 Buffer/global),浏览器会报错;
- * 顶层静态 import browser 包又会让 Node 单测因 XMLHttpRequest 初始化失败。
+ * Vite 默认会解析到 Node ESM(依赖 Buffer/global);显式 browser 入口 + 上方 globals 注入。
  */
 async function loadHtmlToDocx(): Promise<HtmlToDocxFn> {
-  const mod = await import('@turbodocx/html-to-docx/dist/html-to-docx.browser.esm.js')
+  ensureDocxNodeGlobals()
+  // 经 vite alias 指向 browser.esm.js
+  const mod = await import('@turbodocx/html-to-docx')
   const candidate = typeof mod === 'function' ? mod : (mod as { default?: unknown }).default
   if (typeof candidate !== 'function') {
     throw new Error('html-to-docx 加载失败:未获得可调用的转换函数')
@@ -84,9 +103,11 @@ export async function generateContractDocxBlob(
   preparedHtml: string,
   title?: string
 ): Promise<Blob> {
+  ensureDocxNodeGlobals()
   const HTMLtoDOCX = await loadHtmlToDocx()
   const htmlWithImages = await inlineRemoteImagesAsDataUrls(preparedHtml)
   const fullHtml = `<!DOCTYPE html><html><head><meta charset="UTF-8" /></head><body>${htmlWithImages}</body></html>`
+  ensureDocxNodeGlobals()
   const result = await HTMLtoDOCX(fullHtml, null, {
     orientation: 'portrait',
     title: title || '合同',

+ 10 - 2
web-admin/vite.config.ts

@@ -22,7 +22,9 @@ export default ({ mode }: { mode: string }) => {
 
   return defineConfig({
     define: {
-      __APP_VERSION__: JSON.stringify(VITE_VERSION)
+      __APP_VERSION__: JSON.stringify(VITE_VERSION),
+      // html-to-docx 等依赖会读 Node 全局;浏览器映射到 globalThis
+      global: 'globalThis'
     },
     base: VITE_BASE_URL,
     server: {
@@ -56,7 +58,12 @@ export default ({ mode }: { mode: string }) => {
         '@icons': resolvePath('src/assets/icons'),
         '@utils': resolvePath('src/utils'),
         '@stores': resolvePath('src/store'),
-        '@styles': resolvePath('src/assets/styles')
+        '@styles': resolvePath('src/assets/styles'),
+        // 合同 Word 导出:强制 browser 构建,避免 Vite 落到 Node ESM
+        '@turbodocx/html-to-docx': resolvePath(
+          'node_modules/@turbodocx/html-to-docx/dist/html-to-docx.browser.esm.js'
+        ),
+        buffer: 'buffer/'
       }
     },
     build: {
@@ -140,6 +147,7 @@ export default ({ mode }: { mode: string }) => {
         'xgplayer',
         'crypto-js',
         'file-saver',
+        'buffer',
         'vue-img-cutter',
         'element-plus/es',
         'element-plus/es/components/*/style/css',