Ver Fonte

✨ feat(word): 集成mammoth.js实现.docx文件在线预览功能

- 添加mammoth依赖用于解析docx文档内容
- 重构WordViewer组件支持完整docx内容解析和样式渲染
- 优化预览界面增加文档信息统计和纯文本查看功能
- 更新WordPreview页面支持文件大小验证和格式限制
- 移除旧版.doc格式支持,专注.docx格式兼容性
yourname há 4 meses atrás
pai
commit
e1cfb5a159

+ 3 - 2
package.json

@@ -62,6 +62,7 @@
     "ioredis": "^5.6.1",
     "jsonwebtoken": "^9.0.2",
     "lucide-react": "^0.536.0",
+    "mammoth": "^1.10.0",
     "minio": "^8.0.5",
     "mysql2": "^3.14.2",
     "next-themes": "^0.4.6",
@@ -73,15 +74,15 @@
     "react-resizable-panels": "^3.0.4",
     "react-router": "^7.7.0",
     "react-router-dom": "^7.7.0",
-    "recharts": "2.15.4",
     "react-toastify": "^11.0.5",
+    "recharts": "2.15.4",
     "reflect-metadata": "^0.2.2",
     "sirv": "^3.0.1",
-    "uuid": "^11.1.0",
     "sonner": "^2.0.7",
     "tailwind-merge": "^3.3.1",
     "tw-animate-css": "^1.3.6",
     "typeorm": "^0.3.25",
+    "uuid": "^11.1.0",
     "vaul": "^1.1.2",
     "zod": "^4.0.15"
   },

+ 159 - 0
pnpm-lock.yaml

@@ -164,6 +164,9 @@ importers:
       lucide-react:
         specifier: ^0.536.0
         version: 0.536.0(react@19.1.0)
+      mammoth:
+        specifier: ^1.10.0
+        version: 1.10.0
       minio:
         specifier: ^8.0.5
         version: 8.0.5
@@ -1625,6 +1628,10 @@ packages:
     peerDependencies:
       vite: ^4 || ^5 || ^6 || ^7
 
+  '@xmldom/xmldom@0.8.10':
+    resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
+    engines: {node: '>=10.0.0'}
+
   '@zxing/text-encoding@0.9.0':
     resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
 
@@ -1658,6 +1665,9 @@ packages:
     resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==}
     engines: {node: '>= 6.0.0'}
 
+  argparse@1.0.10:
+    resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+
   aria-hidden@1.2.6:
     resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
     engines: {node: '>=10'}
@@ -1692,6 +1702,9 @@ packages:
   block-stream2@2.1.0:
     resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==}
 
+  bluebird@3.4.7:
+    resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
+
   brace-expansion@2.0.2:
     resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
 
@@ -1781,6 +1794,9 @@ packages:
   copy-to-clipboard@3.3.3:
     resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
 
+  core-util-is@1.0.3:
+    resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+
   cross-env@7.0.3:
     resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
     engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
@@ -1900,6 +1916,9 @@ packages:
   detect-node-es@1.1.0:
     resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
 
+  dingbat-to-unicode@1.0.1:
+    resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==}
+
   dom-helpers@5.2.1:
     resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
 
@@ -1911,6 +1930,9 @@ packages:
     resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==}
     engines: {node: '>=12'}
 
+  duck@0.1.12:
+    resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==}
+
   dunder-proto@1.0.1:
     resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
     engines: {node: '>= 0.4'}
@@ -2087,6 +2109,9 @@ packages:
   ieee754@1.2.1:
     resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
 
+  immediate@3.0.6:
+    resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
+
   inherits@2.0.4:
     resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
 
@@ -2138,6 +2163,9 @@ packages:
     resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
     engines: {node: '>= 0.4'}
 
+  isarray@1.0.0:
+    resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+
   isarray@2.0.5:
     resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
 
@@ -2161,12 +2189,18 @@ packages:
     resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
     engines: {node: '>=12', npm: '>=6'}
 
+  jszip@3.10.1:
+    resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
+
   jwa@1.4.2:
     resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
 
   jws@3.2.2:
     resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
 
+  lie@3.3.0:
+    resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+
   lightningcss-darwin-arm64@1.30.1:
     resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
     engines: {node: '>= 12.0.0'}
@@ -2272,6 +2306,9 @@ packages:
     resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
     hasBin: true
 
+  lop@0.4.2:
+    resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==}
+
   lru-cache@10.4.3:
     resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
 
@@ -2291,6 +2328,11 @@ packages:
   magic-string@0.30.17:
     resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
 
+  mammoth@1.10.0:
+    resolution: {integrity: sha512-9HOmqt8uJ5rz7q8XrECU5gRjNftCq4GNG0YIrA6f9iQPCeLgpvgcmRBHi9NQWJQIpT/MAXeg1oKliAK1xoB3eg==}
+    engines: {node: '>=12.0.0'}
+    hasBin: true
+
   math-intrinsics@1.1.0:
     resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
     engines: {node: '>= 0.4'}
@@ -2380,9 +2422,19 @@ packages:
   openapi3-ts@4.5.0:
     resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==}
 
+  option@0.2.4:
+    resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==}
+
   package-json-from-dist@1.0.1:
     resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
 
+  pako@1.0.11:
+    resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
+
+  path-is-absolute@1.0.1:
+    resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+    engines: {node: '>=0.10.0'}
+
   path-key@3.1.1:
     resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
     engines: {node: '>=8'}
@@ -2406,6 +2458,9 @@ packages:
     resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
     engines: {node: ^10 || ^12 || >=14}
 
+  process-nextick-args@2.0.1:
+    resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+
   prop-types@15.8.1:
     resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 
@@ -2742,6 +2797,9 @@ packages:
     resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
     engines: {node: '>=0.10.0'}
 
+  readable-stream@2.3.8:
+    resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+
   readable-stream@3.6.2:
     resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
     engines: {node: '>= 6'}
@@ -2782,6 +2840,9 @@ packages:
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
 
+  safe-buffer@5.1.2:
+    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
   safe-buffer@5.2.1:
     resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
 
@@ -2816,6 +2877,9 @@ packages:
     resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
     engines: {node: '>= 0.4'}
 
+  setimmediate@1.0.5:
+    resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
+
   sha.js@2.4.12:
     resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==}
     engines: {node: '>= 0.10'}
@@ -2851,6 +2915,9 @@ packages:
     resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
     engines: {node: '>=6'}
 
+  sprintf-js@1.0.3:
+    resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
+
   sql-highlight@6.1.0:
     resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==}
     engines: {node: '>=14'}
@@ -2883,6 +2950,9 @@ packages:
     resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
     engines: {node: '>=12'}
 
+  string_decoder@1.1.1:
+    resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+
   string_decoder@1.3.0:
     resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
 
@@ -3018,6 +3088,9 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
+  underscore@1.13.7:
+    resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
+
   undici-types@7.8.0:
     resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
 
@@ -3143,6 +3216,10 @@ packages:
     resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
     engines: {node: '>=4.0.0'}
 
+  xmlbuilder@10.1.1:
+    resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==}
+    engines: {node: '>=4.0'}
+
   xmlbuilder@11.0.1:
     resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
     engines: {node: '>=4.0'}
@@ -4406,6 +4483,8 @@ snapshots:
     transitivePeerDependencies:
       - '@swc/helpers'
 
+  '@xmldom/xmldom@0.8.10': {}
+
   '@zxing/text-encoding@0.9.0':
     optional: true
 
@@ -4481,6 +4560,10 @@ snapshots:
 
   app-root-path@3.1.0: {}
 
+  argparse@1.0.10:
+    dependencies:
+      sprintf-js: 1.0.3
+
   aria-hidden@1.2.6:
     dependencies:
       tslib: 2.8.1
@@ -4516,6 +4599,8 @@ snapshots:
     dependencies:
       readable-stream: 3.6.2
 
+  bluebird@3.4.7: {}
+
   brace-expansion@2.0.2:
     dependencies:
       balanced-match: 1.0.2
@@ -4614,6 +4699,8 @@ snapshots:
     dependencies:
       toggle-selection: 1.0.6
 
+  core-util-is@1.0.3: {}
+
   cross-env@7.0.3:
     dependencies:
       cross-spawn: 7.0.6
@@ -4700,6 +4787,8 @@ snapshots:
 
   detect-node-es@1.1.0: {}
 
+  dingbat-to-unicode@1.0.1: {}
+
   dom-helpers@5.2.1:
     dependencies:
       '@babel/runtime': 7.28.2
@@ -4709,6 +4798,10 @@ snapshots:
 
   dotenv@17.2.1: {}
 
+  duck@0.1.12:
+    dependencies:
+      underscore: 1.13.7
+
   dunder-proto@1.0.1:
     dependencies:
       call-bind-apply-helpers: 1.0.2
@@ -4897,6 +4990,8 @@ snapshots:
 
   ieee754@1.2.1: {}
 
+  immediate@3.0.6: {}
+
   inherits@2.0.4: {}
 
   input-otp@1.4.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
@@ -4953,6 +5048,8 @@ snapshots:
     dependencies:
       which-typed-array: 1.1.19
 
+  isarray@1.0.0: {}
+
   isarray@2.0.5: {}
 
   isexe@2.0.0: {}
@@ -4984,6 +5081,13 @@ snapshots:
       ms: 2.1.3
       semver: 7.7.2
 
+  jszip@3.10.1:
+    dependencies:
+      lie: 3.3.0
+      pako: 1.0.11
+      readable-stream: 2.3.8
+      setimmediate: 1.0.5
+
   jwa@1.4.2:
     dependencies:
       buffer-equal-constant-time: 1.0.1
@@ -4995,6 +5099,10 @@ snapshots:
       jwa: 1.4.2
       safe-buffer: 5.2.1
 
+  lie@3.3.0:
+    dependencies:
+      immediate: 3.0.6
+
   lightningcss-darwin-arm64@1.30.1:
     optional: true
 
@@ -5066,6 +5174,12 @@ snapshots:
     dependencies:
       js-tokens: 4.0.0
 
+  lop@0.4.2:
+    dependencies:
+      duck: 0.1.12
+      option: 0.2.4
+      underscore: 1.13.7
+
   lru-cache@10.4.3: {}
 
   lru-cache@7.18.3: {}
@@ -5080,6 +5194,19 @@ snapshots:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.5.4
 
+  mammoth@1.10.0:
+    dependencies:
+      '@xmldom/xmldom': 0.8.10
+      argparse: 1.0.10
+      base64-js: 1.5.1
+      bluebird: 3.4.7
+      dingbat-to-unicode: 1.0.1
+      jszip: 3.10.1
+      lop: 0.4.2
+      path-is-absolute: 1.0.1
+      underscore: 1.13.7
+      xmlbuilder: 10.1.1
+
   math-intrinsics@1.1.0: {}
 
   mime-db@1.52.0: {}
@@ -5162,8 +5289,14 @@ snapshots:
     dependencies:
       yaml: 2.8.0
 
+  option@0.2.4: {}
+
   package-json-from-dist@1.0.1: {}
 
+  pako@1.0.11: {}
+
+  path-is-absolute@1.0.1: {}
+
   path-key@3.1.1: {}
 
   path-scurry@1.11.1:
@@ -5183,6 +5316,8 @@ snapshots:
       picocolors: 1.1.1
       source-map-js: 1.2.1
 
+  process-nextick-args@2.0.1: {}
+
   prop-types@15.8.1:
     dependencies:
       loose-envify: 1.4.0
@@ -5609,6 +5744,16 @@ snapshots:
 
   react@19.1.0: {}
 
+  readable-stream@2.3.8:
+    dependencies:
+      core-util-is: 1.0.3
+      inherits: 2.0.4
+      isarray: 1.0.0
+      process-nextick-args: 2.0.1
+      safe-buffer: 5.1.2
+      string_decoder: 1.1.1
+      util-deprecate: 1.0.2
+
   readable-stream@3.6.2:
     dependencies:
       inherits: 2.0.4
@@ -5672,6 +5817,8 @@ snapshots:
       '@rollup/rollup-win32-x64-msvc': 4.46.0
       fsevents: 2.3.3
 
+  safe-buffer@5.1.2: {}
+
   safe-buffer@5.2.1: {}
 
   safe-regex-test@1.1.0:
@@ -5705,6 +5852,8 @@ snapshots:
       gopd: 1.2.0
       has-property-descriptors: 1.0.2
 
+  setimmediate@1.0.5: {}
+
   sha.js@2.4.12:
     dependencies:
       inherits: 2.0.4
@@ -5734,6 +5883,8 @@ snapshots:
 
   split-on-first@1.1.0: {}
 
+  sprintf-js@1.0.3: {}
+
   sql-highlight@6.1.0: {}
 
   sqlstring@2.3.3: {}
@@ -5762,6 +5913,10 @@ snapshots:
       emoji-regex: 9.2.2
       strip-ansi: 7.1.0
 
+  string_decoder@1.1.1:
+    dependencies:
+      safe-buffer: 5.1.2
+
   string_decoder@1.3.0:
     dependencies:
       safe-buffer: 5.2.1
@@ -5859,6 +6014,8 @@ snapshots:
 
   typescript@5.8.3: {}
 
+  underscore@1.13.7: {}
+
   undici-types@7.8.0: {}
 
   use-callback-ref@1.3.3(@types/react@19.1.8)(react@19.1.0):
@@ -5982,6 +6139,8 @@ snapshots:
       sax: 1.4.1
       xmlbuilder: 11.0.1
 
+  xmlbuilder@10.1.1: {}
+
   xmlbuilder@11.0.1: {}
 
   y18n@5.0.8: {}

+ 181 - 66
src/client/admin-shadcn/components/WordViewer.tsx

@@ -3,6 +3,7 @@ import { Card, CardContent } from '@/client/components/ui/card';
 import { Alert, AlertDescription } from '@/client/components/ui/alert';
 import { Button } from '@/client/components/ui/button';
 import { Loader2, AlertCircle, FileText } from 'lucide-react';
+import mammoth from 'mammoth';
 
 interface WordViewerProps {
   file: File | null;
@@ -14,63 +15,98 @@ export default function WordViewer({ file, fileUrl }: WordViewerProps) {
   const [error, setError] = useState<string | null>(null);
   const [previewHtml, setPreviewHtml] = useState<string>('');
   const [wordContent, setWordContent] = useState<string>('');
+  const [documentInfo, setDocumentInfo] = useState<any>(null);
 
   const readWordFile = async (file: File) => {
     setIsLoading(true);
     setError(null);
     
     try {
-      // 使用 FileReader 读取文件内容
-      const reader = new FileReader();
+      const arrayBuffer = await file.arrayBuffer();
       
-      reader.onload = async (e) => {
-        try {
-          const arrayBuffer = e.target?.result as ArrayBuffer;
-          
-          // 这里可以实现更复杂的Word文档解析
-          // 由于浏览器限制,我们只能显示基本信息
-          // 实际项目中可以考虑使用第三方库如 mammoth.js
-          
-          // 创建文本预览(实际项目中应该使用专门的DOCX解析库)
-          const textPreview = `文档名称: ${file.name}\n` +
-                             `文件大小: ${formatFileSize(file.size)}\n` +
-                             `文件类型: ${file.type}\n` +
-                             `最后修改: ${file.lastModifiedDate?.toLocaleString() || '未知'}`;
-          
-          setWordContent(textPreview);
-          setPreviewHtml(`
-            <div class="word-preview">
-              <div class="word-header">
-                <h3>${file.name}</h3>
-                <p>文件大小: ${formatFileSize(file.size)}</p>
-              </div>
-              <div class="word-content">
-                <p>由于浏览器安全限制,Word文档的完整内容无法在浏览器中直接预览。</p>
-                <p>建议使用以下方式查看:</p>
-                <ul>
-                  <li>下载文件后用Microsoft Word打开</li>
-                  <li>使用Google Docs等在线工具</li>
-                  <li>转换为PDF格式后预览</li>
-                </ul>
-              </div>
-            </div>
-          `);
-        } catch (err) {
-          setError('文件解析失败');
-          console.error('File parsing error:', err);
-        } finally {
-          setIsLoading(false);
+      // 使用 mammoth.js 解析 DOCX 文件
+      const result = await mammoth.convertToHtml(
+        { arrayBuffer },
+        {
+          styleMap: [
+            "p[style-name='Heading 1'] => h1:fresh",
+            "p[style-name='Heading 2'] => h2:fresh",
+            "p[style-name='Heading 3'] => h3:fresh",
+            "p[style-name='Heading 4'] => h4:fresh",
+            "p[style-name='Heading 5'] => h5:fresh",
+            "p[style-name='Heading 6'] => h6:fresh",
+            "p => p:fresh"
+          ],
+          includeDefaultStyleMap: true
+        }
+      );
+      
+      // 获取文档信息
+      const info = await mammoth.extractRawText({ arrayBuffer });
+      
+      setPreviewHtml(result.value);
+      setWordContent(info.value);
+      
+      // 提取文档元数据
+      setDocumentInfo({
+        name: file.name,
+        size: file.size,
+        type: file.type,
+        lastModified: file.lastModifiedDate,
+        wordCount: info.value.split(/\s+/).filter(word => word.length > 0).length,
+        characterCount: info.value.length
+      });
+      
+    } catch (err) {
+      console.error('Word parsing error:', err);
+      
+      if (file.type === 'application/msword') {
+        setError('不支持旧的 .doc 格式,请使用 .docx 格式的Word文档');
+      } else if (file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
+        setError('不支持的文件格式,请选择 .docx 格式的Word文档');
+      } else {
+        setError('文件解析失败,请检查文件是否损坏');
+      }
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  const loadFromUrl = async (url: string) => {
+    setIsLoading(true);
+    setError(null);
+    
+    try {
+      const response = await fetch(url);
+      if (!response.ok) throw new Error('无法加载文件');
+      
+      const arrayBuffer = await response.arrayBuffer();
+      
+      const result = await mammoth.convertToHtml(
+        { arrayBuffer },
+        {
+          styleMap: [
+            "p[style-name='Heading 1'] => h1:fresh",
+            "p[style-name='Heading 2'] => h2:fresh",
+            "p[style-name='Heading 3'] => h3:fresh",
+            "p[style-name='Heading 4'] => h4:fresh",
+            "p[style-name='Heading 5'] => h5:fresh",
+            "p[style-name='Heading 6'] => h6:fresh",
+            "p => p:fresh"
+          ],
+          includeDefaultStyleMap: true
         }
-      };
+      );
       
-      reader.onerror = () => {
-        setError('文件读取失败');
-        setIsLoading(false);
-      };
+      const info = await mammoth.extractRawText({ arrayBuffer });
+      
+      setPreviewHtml(result.value);
+      setWordContent(info.value);
       
-      reader.readAsArrayBuffer(file);
     } catch (err) {
-      setError('文件处理失败');
+      setError('文件加载失败,请检查网络连接和文件权限');
+      console.error('URL loading error:', err);
+    } finally {
       setIsLoading(false);
     }
   };
@@ -87,8 +123,7 @@ export default function WordViewer({ file, fileUrl }: WordViewerProps) {
     if (file) {
       readWordFile(file);
     } else if (fileUrl) {
-      // 处理URL方式的文件
-      setError('URL方式的文件预览需要服务器支持');
+      loadFromUrl(fileUrl);
     }
   }, [file, fileUrl]);
 
@@ -111,7 +146,7 @@ export default function WordViewer({ file, fileUrl }: WordViewerProps) {
         <CardContent className="pt-6">
           <div className="flex items-center justify-center py-12">
             <Loader2 className="h-8 w-8 animate-spin text-primary" />
-            <span className="ml-2">正在加载文档...</span>
+            <span className="ml-2">正在解析文档...</span>
           </div>
         </CardContent>
       </Card>
@@ -141,39 +176,119 @@ export default function WordViewer({ file, fileUrl }: WordViewerProps) {
       <CardContent className="pt-6">
         <div className="word-viewer-container">
           <style jsx>{`
-            .word-preview {
+            .word-viewer-container {
+              max-width: 100%;
+              overflow-x: auto;
+            }
+            .word-content {
               font-family: 'Times New Roman', serif;
               line-height: 1.6;
+              color: #1f2937;
             }
-            .word-header {
-              border-bottom: 2px solid #e5e7eb;
-              padding-bottom: 1rem;
-              margin-bottom: 1rem;
+            .word-content h1 {
+              font-size: 2rem;
+              font-weight: bold;
+              margin: 1.5rem 0 1rem 0;
+              color: #111827;
+            }
+            .word-content h2 {
+              font-size: 1.75rem;
+              font-weight: bold;
+              margin: 1.5rem 0 0.75rem 0;
+              color: #111827;
             }
-            .word-header h3 {
+            .word-content h3 {
               font-size: 1.5rem;
               font-weight: bold;
-              margin-bottom: 0.5rem;
+              margin: 1.25rem 0 0.75rem 0;
+              color: #111827;
             }
-            .word-content {
-              color: #374151;
+            .word-content h4 {
+              font-size: 1.25rem;
+              font-weight: bold;
+              margin: 1rem 0 0.5rem 0;
+              color: #111827;
             }
-            .word-content ul {
-              list-style-type: disc;
-              margin-left: 1.5rem;
+            .word-content h5 {
+              font-size: 1.125rem;
+              font-weight: bold;
+              margin: 1rem 0 0.5rem 0;
+              color: #111827;
+            }
+            .word-content h6 {
+              font-size: 1rem;
+              font-weight: bold;
+              margin: 0.75rem 0 0.5rem 0;
+              color: #111827;
+            }
+            .word-content p {
+              margin-bottom: 0.75rem;
+            }
+            .word-content ul, .word-content ol {
+              margin: 0.5rem 0 0.5rem 2rem;
             }
             .word-content li {
+              margin-bottom: 0.25rem;
+            }
+            .word-content table {
+              width: 100%;
+              border-collapse: collapse;
+              margin: 1rem 0;
+            }
+            .word-content th, .word-content td {
+              border: 1px solid #d1d5db;
+              padding: 0.5rem;
+              text-align: left;
+            }
+            .word-content th {
+              background-color: #f9fafb;
+              font-weight: bold;
+            }
+            .document-info {
+              background-color: #f9fafb;
+              border: 1px solid #e5e7eb;
+              border-radius: 0.5rem;
+              padding: 1rem;
+              margin-bottom: 1rem;
+            }
+            .document-info h4 {
+              font-weight: bold;
               margin-bottom: 0.5rem;
             }
+            .document-info p {
+              margin: 0.25rem 0;
+              font-size: 0.875rem;
+            }
           `}</style>
           
-          <div dangerouslySetInnerHTML={{ __html: previewHtml }} />
+          {documentInfo && (
+            <div className="document-info">
+              <h4>文档信息</h4>
+              <p><strong>文件名:</strong> {documentInfo.name}</p>
+              <p><strong>大小:</strong> {formatFileSize(documentInfo.size)}</p>
+              <p><strong>类型:</strong> {documentInfo.type}</p>
+              <p><strong>字数:</strong> {documentInfo.wordCount}</p>
+              <p><strong>字符数:</strong> {documentInfo.characterCount}</p>
+              {documentInfo.lastModified && (
+                <p><strong>修改时间:</strong> {documentInfo.lastModified.toLocaleString()}</p>
+              )}
+            </div>
+          )}
+          
+          <div 
+            className="word-content"
+            dangerouslySetInnerHTML={{ __html: previewHtml }} 
+          />
           
           {wordContent && (
-            <div className="mt-4 p-4 bg-muted rounded-lg">
-              <h4 className="font-semibold mb-2">文档信息</h4>
-              <pre className="text-sm whitespace-pre-wrap">{wordContent}</pre>
-            </div>
+            <details className="mt-4">
+              <summary className="cursor-pointer text-sm text-muted-foreground hover:text-foreground">
+                查看纯文本内容
+              </summary>
+              <div className="mt-2 p-4 bg-muted rounded-lg">
+                <pre className="text-sm whitespace-pre-wrap font-mono">{wordContent}</pre>
+              </div>
+            </details>
           )}
         </div>
       </CardContent>

+ 129 - 40
src/client/admin-shadcn/pages/WordPreview.tsx

@@ -4,9 +4,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/cli
 import { Input } from '@/client/components/ui/input';
 import { Label } from '@/client/components/ui/label';
 import { Alert, AlertDescription } from '@/client/components/ui/alert';
-import { FileText, Upload, Download } from 'lucide-react';
+import { FileText, Upload, Download, Eye, FileWarning } from 'lucide-react';
 import { toast } from 'sonner';
 import WordViewer from '@/client/admin-shadcn/components/WordViewer';
+import { AlertCircle } from 'lucide-react';
 
 interface WordFile {
   id: string;
@@ -21,24 +22,34 @@ export default function WordPreview() {
   const [previewFile, setPreviewFile] = useState<WordFile | null>(null);
   const [isLoading, setIsLoading] = useState(false);
   const [previewLoading, setPreviewLoading] = useState(false);
+  const [showPreview, setShowPreview] = useState(false);
 
   const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
     const file = event.target.files?.[0];
     if (file) {
-      // 检查文件类型
+      // 检查文件类型和大小
       const validTypes = [
-        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-        'application/msword'
+        'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
       ];
+      const maxSize = 10 * 1024 * 1024; // 10MB
       
       if (!validTypes.includes(file.type)) {
-        toast.error('请选择有效的Word文件(.docx或.doc)');
+        if (file.type === 'application/msword') {
+          toast.error('不支持旧的 .doc 格式,请使用 .docx 格式的Word文档');
+        } else {
+          toast.error('请选择有效的Word文件(.docx格式)');
+        }
+        return;
+      }
+      
+      if (file.size > maxSize) {
+        toast.error('文件大小超过10MB限制');
         return;
       }
 
       setSelectedFile(file);
-      setPreviewFile(null);
-      toast.success('文件已选择');
+      setShowPreview(false);
+      toast.success('文件已选择,可以开始预览');
     }
   };
 
@@ -49,9 +60,9 @@ export default function WordPreview() {
     }
 
     setPreviewLoading(true);
+    setShowPreview(true);
     
     try {
-      // 创建文件预览URL
       const fileUrl = URL.createObjectURL(selectedFile);
       const wordFile: WordFile = {
         id: Date.now().toString(),
@@ -62,27 +73,22 @@ export default function WordPreview() {
       };
       
       setPreviewFile(wordFile);
-      toast.success('文件加载成功,正在预览...');
+      toast.success('文档解析中...');
     } catch (error) {
       toast.error('文件预览失败,请重试');
       console.error('Preview error:', error);
+      setShowPreview(false);
     } finally {
       setPreviewLoading(false);
     }
   };
 
-  const handleDownload = () => {
-    if (!previewFile) {
-      toast.error('没有可下载的文件');
-      return;
-    }
-    
-    const link = document.createElement('a');
-    link.href = previewFile.url;
-    link.download = previewFile.name;
-    document.body.appendChild(link);
-    link.click();
-    document.body.removeChild(link);
+  const handleClearFile = () => {
+    setSelectedFile(null);
+    setPreviewFile(null);
+    setShowPreview(false);
+    const input = document.getElementById('word-file') as HTMLInputElement;
+    if (input) input.value = '';
   };
 
   const formatFileSize = (bytes: number) => {
@@ -97,7 +103,7 @@ export default function WordPreview() {
     <div className="space-y-6">
       <div>
         <h1 className="text-3xl font-bold tracking-tight">Word文档在线预览</h1>
-        <p className="text-muted-foreground">上传并预览Word文档内容</p>
+        <p className="text-muted-foreground">上传并预览Word文档内容,支持 .docx 格式</p>
       </div>
 
       <div className="grid gap-6 md:grid-cols-2">
@@ -109,7 +115,7 @@ export default function WordPreview() {
               选择Word文件
             </CardTitle>
             <CardDescription>
-              支持 .docx 和 .doc 格式的Word文档
+              支持 .docx 格式的Word文档,最大10MB
             </CardDescription>
           </CardHeader>
           <CardContent className="space-y-4">
@@ -118,7 +124,7 @@ export default function WordPreview() {
               <Input
                 id="word-file"
                 type="file"
-                accept=".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+                accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
                 onChange={handleFileSelect}
               />
             </div>
@@ -136,16 +142,35 @@ export default function WordPreview() {
               </Alert>
             )}
 
-            <Button
-              onClick={handlePreview}
-              disabled={!selectedFile}
-              className="w-full"
-            >
-              <>
-                <Upload className="h-4 w-4 mr-2" />
-                开始预览
-              </>
-            </Button>
+            <div className="flex gap-2">
+              <Button
+                onClick={handlePreview}
+                disabled={!selectedFile || previewLoading}
+                className="flex-1"
+              >
+                {previewLoading ? (
+                  <>
+                    <Loader2 className="h-4 w-4 mr-2 animate-spin" />
+                    解析中...
+                  </>
+                ) : (
+                  <>
+                    <Eye className="h-4 w-4 mr-2" />
+                    开始预览
+                  </>
+                )}
+              </Button>
+              
+              {selectedFile && (
+                <Button
+                  variant="outline"
+                  onClick={handleClearFile}
+                  className="flex-1"
+                >
+                  清除文件
+                </Button>
+              )}
+            </div>
           </CardContent>
         </Card>
 
@@ -157,11 +182,56 @@ export default function WordPreview() {
               文档预览
             </CardTitle>
             <CardDescription>
-              {previewFile ? previewFile.name : '请先选择并预览文档'}
+              {selectedFile ? selectedFile.name : '请先选择并预览文档'}
             </CardDescription>
           </CardHeader>
           <CardContent>
-            <WordViewer file={selectedFile} />
+            {!showPreview ? (
+              <div className="text-center py-12">
+                <FileWarning className="h-16 w-16 mx-auto mb-4 text-muted-foreground" />
+                <p className="text-muted-foreground">
+                  {selectedFile ? '点击"开始预览"按钮查看文档内容' : '请先选择Word文档'}
+                </p>
+              </div>
+            ) : (
+              <WordViewer file={selectedFile} />
+            )}
+          </CardContent>
+        </Card>
+      </div>
+
+      {/* 功能特性 */}
+      <div className="grid gap-4 md:grid-cols-3">
+        <Card>
+          <CardHeader>
+            <CardTitle className="text-lg">格式支持</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <p className="text-sm text-muted-foreground">
+              完美支持 .docx 格式,保留原始文档格式和布局
+            </p>
+          </CardContent>
+        </Card>
+        
+        <Card>
+          <CardHeader>
+            <CardTitle className="text-lg">本地处理</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <p className="text-sm text-muted-foreground">
+              所有文件处理都在本地浏览器完成,保护您的隐私安全
+            </p>
+          </CardContent>
+        </Card>
+        
+        <Card>
+          <CardHeader>
+            <CardTitle className="text-lg">实时预览</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <p className="text-sm text-muted-foreground">
+              即时解析文档内容,快速查看Word文档的完整内容
+            </p>
           </CardContent>
         </Card>
       </div>
@@ -173,10 +243,29 @@ export default function WordPreview() {
         </CardHeader>
         <CardContent>
           <div className="space-y-2 text-sm">
-            <p>• 支持的文件格式:.docx(Word 2007及以上版本)和 .doc(Word 97-2003)</p>
-            <p>• 文件大小限制:建议不超过10MB</p>
-            <p>• 浏览器限制:部分浏览器可能无法完美显示Word文档的复杂格式</p>
-            <p>• 隐私保护:所有文件处理都在本地浏览器完成,不会上传到服务器</p>
+            <p>• <strong>支持的文件格式:</strong> .docx(Word 2007及以上版本)</p>
+            <p>• <strong>文件大小限制:</strong> 最大10MB,超过此限制的文件可能无法正确解析</p>
+            <p>• <strong>隐私保护:</strong> 所有文件处理都在本地浏览器完成,不会上传到任何服务器</p>
+            <p>• <strong>格式兼容性:</strong> 支持大部分Word格式,包括标题、列表、表格、图片等</p>
+            <p>• <strong>不支持格式:</strong> 旧的 .doc 格式(Word 97-2003)暂不支持</p>
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* 注意事项 */}
+      <Card>
+        <CardHeader>
+          <CardTitle className="flex items-center gap-2">
+            <AlertCircle className="h-5 w-5" />
+            注意事项
+          </CardTitle>
+        </CardHeader>
+        <CardContent>
+          <div className="space-y-2 text-sm">
+            <p>• 复杂格式的文档可能需要更长的解析时间</p>
+            <p>• 某些高级格式(如宏、表单控件)可能无法完美显示</p>
+            <p>• 建议在现代浏览器中使用以获得最佳体验</p>
+            <p>• 如果文档解析失败,请检查文件是否损坏</p>
           </div>
         </CardContent>
       </Card>