Browse Source

✨ feat(word): 实现Word模板图片插入功能

- 添加docxtemplater-image-module-free依赖以支持图片处理
- 配置图片模块处理逻辑,实现从文件夹加载图片
- 修改Word模板占位符格式为{%图片名%}
- 更新用户指南中的图片占位符使用说明
- 优化图片数据传递方式,简化模板渲染流程

📝 docs(word): 更新Word模板使用文档

- 修改文本占位符格式说明为{字段名}
- 更新图片占位符格式说明为{%图片名%}
- 添加图片占位符使用示例
- 完善图片处理相关注意事项
yourname 3 months ago
parent
commit
b88ff350af
3 changed files with 46 additions and 15 deletions
  1. 1 0
      package.json
  2. 17 0
      pnpm-lock.yaml
  3. 28 15
      src/client/admin-shadcn/pages/WordPreview.tsx

+ 1 - 0
package.json

@@ -55,6 +55,7 @@
     "dayjs": "^1.11.13",
     "debug": "^4.4.1",
     "docxtemplater": "^3.65.2",
+    "docxtemplater-image-module-free": "^1.1.1",
     "dotenv": "^17.2.1",
     "embla-carousel-react": "^8.6.0",
     "formdata-node": "^6.0.3",

+ 17 - 0
pnpm-lock.yaml

@@ -143,6 +143,9 @@ importers:
       docxtemplater:
         specifier: ^3.65.2
         version: 3.65.2
+      docxtemplater-image-module-free:
+        specifier: ^1.1.1
+        version: 1.1.1
       dotenv:
         specifier: ^17.2.1
         version: 17.2.1
@@ -1952,6 +1955,9 @@ packages:
   dingbat-to-unicode@1.0.1:
     resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==}
 
+  docxtemplater-image-module-free@1.1.1:
+    resolution: {integrity: sha512-aWOzVQN7ggDYjfoy3pTTNrcrZ7/CJrQcI9cT+hmyHE6nRLR67nt5yPFPe9hm9VWbfYIED2fi+3itOnF0TE/RWQ==}
+
   docxtemplater@3.65.2:
     resolution: {integrity: sha512-aDE2D0ir+4K1nruSFtIBbLE4vxyvB/qLNVVlXjVjtI6an4z2Qe5BprxusFsDRH8j6FCz3aXe6qxi3O+cGHnq+Q==}
     engines: {node: '>=0.10'}
@@ -3288,6 +3294,11 @@ packages:
     resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
     engines: {node: '>=4.0'}
 
+  xmldom@0.1.31:
+    resolution: {integrity: sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==}
+    engines: {node: '>=0.1'}
+    deprecated: Deprecated due to CVE-2021-21366 resolved in 0.5.0
+
   y18n@5.0.8:
     resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
     engines: {node: '>=10'}
@@ -4866,6 +4877,10 @@ snapshots:
 
   dingbat-to-unicode@1.0.1: {}
 
+  docxtemplater-image-module-free@1.1.1:
+    dependencies:
+      xmldom: 0.1.31
+
   docxtemplater@3.65.2:
     dependencies:
       '@xmldom/xmldom': 0.9.8
@@ -6250,6 +6265,8 @@ snapshots:
 
   xmlbuilder@11.0.1: {}
 
+  xmldom@0.1.31: {}
+
   y18n@5.0.8: {}
 
   yallist@5.0.0: {}

+ 28 - 15
src/client/admin-shadcn/pages/WordPreview.tsx

@@ -2,6 +2,7 @@ import { useState, useRef } from 'react';
 import * as XLSX from 'xlsx';
 import PizZip from 'pizzip';
 import Docxtemplater from 'docxtemplater';
+import ImageModule from 'docxtemplater-image-module-free';
 import JSZip from 'jszip';
 import { Button } from '@/client/components/ui/button';
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
@@ -228,9 +229,29 @@ export default function WordPreview() {
       const arrayBuffer = await wordFile.arrayBuffer();
       const zip = new PizZip(arrayBuffer);
       
+      // 配置图片模块
+      const imageOpts = {
+        centered: false,
+        getImage: (tagValue: string) => {
+          if (tagValue && typeof tagValue === 'string') {
+            // 从imageMappings中获取对应的图片
+            const folderIndex = (rowIndex + 1).toString();
+            const imageFile = imageMappings[folderIndex]?.[tagValue];
+            if (imageFile) {
+              return imageFile.arrayBuffer();
+            }
+          }
+          return null;
+        },
+        getSize: () => [200, 150] // 固定尺寸
+      };
+      
+      const imageModule = new ImageModule(imageOpts);
+
       const doc = new Docxtemplater(zip, {
         paragraphLoop: true,
         linebreaks: true,
+        modules: [imageModule]
       });
 
       // 处理嵌套数据结构
@@ -253,24 +274,15 @@ export default function WordPreview() {
         }
       });
 
-      // 处理图片字段
+      // 处理图片字段 - 直接传递图片名称
       const folderIndex = (rowIndex + 1).toString();
       if (imageMappings[folderIndex]) {
-        for (const [imageName, imageFile] of Object.entries(imageMappings[folderIndex])) {
-          try {
-            processedData[`image:${imageName}`] = {
-              data: imageFile,
-              width: 200,
-              height: 150
-            };
-          } catch (error) {
-            console.error(`处理图片失败: ${imageName}`, error);
-          }
+        for (const [imageName] of Object.entries(imageMappings[folderIndex])) {
+          processedData[imageName] = imageName;
         }
       }
 
-      doc.setData(processedData);
-      doc.render();
+      doc.render(processedData);
       
       const generatedDoc = doc.getZip().generate({
         type: 'blob',
@@ -754,7 +766,7 @@ export default function WordPreview() {
             <div>
               <h4 className="font-medium mb-1">1. 准备Word模板</h4>
               <p className="text-muted-foreground">
-                使用 {'{{'}字段名{'}'} 格式作为文本占位符,使用 {'{{'}image:图片名{'}'} 格式作为图片占位符
+                使用 {'{字段名}'} 格式作为文本占位符,使用 {'{%image:图片名%}'} 格式作为图片占位符
               </p>
             </div>
             
@@ -775,7 +787,7 @@ export default function WordPreview() {
             <div>
               <h4 className="font-medium mb-1">4. 图片命名规则</h4>
               <p className="text-muted-foreground">
-                例如:模板中使用 {'{{'}image:logo{'}'},则图片文件应命名为 logo.jpg/png等
+                例如:模板中使用 {'{%logo%}'},则图片文件应命名为 logo.jpg/png等
               </p>
             </div>
           </div>
@@ -797,6 +809,7 @@ export default function WordPreview() {
             <p>• 文件夹序号必须与Excel行号对应(第1行对应文件夹"1")</p>
             <p>• 如果图片不存在,对应位置将留空</p>
             <p>• 支持jpg、jpeg、png、gif、bmp、webp格式图片</p>
+            <p>• 图片占位符使用 {'{%图片名%}'} 格式,如 {'{%logo%}'}</p>
           </div>
         </CardContent>
       </Card>