Ver código fonte

增加合并word

yourname 3 meses atrás
pai
commit
ebb0cb0cff
1 arquivos alterados com 386 adições e 22 exclusões
  1. 386 22
      src/client/home/pages/WordPreview.tsx

+ 386 - 22
src/client/home/pages/WordPreview.tsx

@@ -12,7 +12,8 @@ import { Alert, AlertDescription } from '@/client/components/ui/alert';
 import {
   FileText, Upload, Download, Eye, FileWarning, FileSpreadsheet,
   RefreshCw, CheckCircle, AlertCircle, DownloadCloud, Image, Package,
-  X, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, Settings
+  X, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, Settings,
+  CheckSquare, Square, Download, Archive, CheckCheck
 } from 'lucide-react';
 import { toast } from 'sonner';
 import WordViewer from '@/client/home/components/WordViewer';
@@ -82,6 +83,13 @@ export default function WordPreview() {
   const [imageSizeSettings, setImageSizeSettings] = useState<ImageSizeSettings>({ width: 200, height: 150 });
   const [showSizeSettings, setShowSizeSettings] = useState(false);
   
+  // 新增状态:选择下载功能
+  const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
+  const [isDownloading, setIsDownloading] = useState(false);
+  const [downloadProgress, setDownloadProgress] = useState(0);
+  const [mergeDownloading, setMergeDownloading] = useState(false);
+  const [wordMergeDownloading, setWordMergeDownloading] = useState(false);
+  
   const wordFileInputRef = useRef<HTMLInputElement>(null);
   const excelFileInputRef = useRef<HTMLInputElement>(null);
   const imageZipInputRef = useRef<HTMLInputElement>(null);
@@ -525,6 +533,228 @@ export default function WordPreview() {
     });
   };
 
+  // 新增:选择下载功能
+  const toggleFileSelection = (index: number) => {
+    const newSelectedFiles = new Set(selectedFiles);
+    if (newSelectedFiles.has(index)) {
+      newSelectedFiles.delete(index);
+    } else {
+      newSelectedFiles.add(index);
+    }
+    setSelectedFiles(newSelectedFiles);
+  };
+
+  const selectAllFiles = () => {
+    if (!processingResult) return;
+    const allIndices = new Set(Array.from({ length: processingResult.generatedFiles.length }, (_, i) => i));
+    setSelectedFiles(allIndices);
+  };
+
+  const clearSelection = () => {
+    setSelectedFiles(new Set());
+  };
+
+  const downloadSelectedFiles = async () => {
+    if (!processingResult || selectedFiles.size === 0) return;
+    
+    setIsDownloading(true);
+    setDownloadProgress(0);
+    
+    const filesToDownload = Array.from(selectedFiles)
+      .sort((a, b) => a - b)
+      .map(index => processingResult.generatedFiles[index]);
+    
+    for (let i = 0; i < filesToDownload.length; i++) {
+      downloadProcessedFile(filesToDownload[i]);
+      setDownloadProgress(((i + 1) / filesToDownload.length) * 100);
+      await new Promise(resolve => setTimeout(resolve, 300)); // 添加延迟避免浏览器阻塞
+    }
+    
+    setIsDownloading(false);
+    setDownloadProgress(0);
+    toast.success(`已下载 ${filesToDownload.length} 个文档`);
+  };
+
+  // 新增:Word文档合并下载功能
+  const mergeAndDownloadFiles = async () => {
+    if (!processingResult || selectedFiles.size === 0) return;
+    
+    setMergeDownloading(true);
+    
+    try {
+      const filesToMerge = Array.from(selectedFiles)
+        .sort((a, b) => a - b)
+        .map(index => processingResult.generatedFiles[index]);
+      
+      // 创建一个新的JSZip实例来合并文件
+      const JSZip = (await import('jszip')).default;
+      const zip = new JSZip();
+      
+      // 将所有选中的文件添加到zip中
+      filesToMerge.forEach((file, index) => {
+        zip.file(file.name, file.content);
+      });
+      
+      // 生成zip文件
+      const zipContent = await zip.generateAsync({
+        type: 'blob',
+        compression: 'DEFLATE',
+        compressionOptions: {
+          level: 6
+        }
+      });
+      
+      // 下载合并的zip文件
+      const url = URL.createObjectURL(zipContent);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = `合并文档_${new Date().toISOString().slice(0, 10)}.zip`;
+      document.body.appendChild(a);
+      a.click();
+      document.body.removeChild(a);
+      URL.revokeObjectURL(url);
+      
+      toast.success(`已合并并下载 ${filesToMerge.length} 个文档`);
+      
+    } catch (error) {
+      console.error('文档合并失败:', error);
+      toast.error('文档合并失败,请重试');
+    } finally {
+      setMergeDownloading(false);
+    }
+  };
+
+  // 新增:下载所有文件为zip
+  const downloadAllAsZip = async () => {
+    if (!processingResult) return;
+    
+    setMergeDownloading(true);
+    
+    try {
+      const JSZip = (await import('jszip')).default;
+      const zip = new JSZip();
+      
+      processingResult.generatedFiles.forEach((file, index) => {
+        zip.file(file.name, file.content);
+      });
+      
+      const zipContent = await zip.generateAsync({
+        type: 'blob',
+        compression: 'DEFLATE',
+        compressionOptions: {
+          level: 6
+        }
+      });
+      
+      const url = URL.createObjectURL(zipContent);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = `全部文档_${new Date().toISOString().slice(0, 10)}.zip`;
+      document.body.appendChild(a);
+      a.click();
+      document.body.removeChild(a);
+      URL.revokeObjectURL(url);
+      
+      toast.success(`已下载全部 ${processingResult.generatedFiles.length} 个文档为压缩包`);
+      
+    } catch (error) {
+      console.error('压缩包创建失败:', error);
+      toast.error('压缩包创建失败,请重试');
+    } finally {
+      setMergeDownloading(false);
+    }
+  };
+
+  // 新增:Word文档内容合并功能(将多个Word文档合并成一个Word文档)
+  const mergeWordDocuments = async () => {
+    if (!processingResult || selectedFiles.size === 0) return;
+    
+    setWordMergeDownloading(true);
+    
+    try {
+      const filesToMerge = Array.from(selectedFiles)
+        .sort((a, b) => a - b)
+        .map(index => processingResult.generatedFiles[index]);
+
+      // 加载docxtemplater和pizzip
+      const PizZip = (await import('pizzip')).default;
+      const Docxtemplater = (await import('docxtemplater')).default;
+
+      // 创建一个新的空Word文档作为基础
+      const baseArrayBuffer = await selectedWordFile!.arrayBuffer();
+      const baseZip = new PizZip(baseArrayBuffer);
+      const baseDoc = new Docxtemplater(baseZip, {
+        paragraphLoop: true,
+        linebreaks: true,
+      });
+
+      // 获取基础文档的内容
+      baseDoc.render({});
+      const baseContent = baseDoc.getZip().generate({
+        type: 'blob',
+        mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+      });
+
+      // 创建一个新的JSZip实例来合并内容
+      const JSZip = (await import('jszip')).default;
+      const mergedZip = new JSZip();
+      
+      // 加载基础文档
+      const baseDocx = await mergedZip.loadAsync(await baseContent.arrayBuffer());
+      
+      // 获取基础文档的word/document.xml内容
+      let mergedContent = await baseDocx.file('word/document.xml').async('text');
+      
+      // 移除基础文档的结束标签,以便添加其他文档内容
+      mergedContent = mergedContent.replace(/<\/w:body><\/w:document>$/, '');
+      
+      // 逐个添加其他文档的内容
+      for (const file of filesToMerge) {
+        const fileArrayBuffer = await file.content.arrayBuffer();
+        const fileZip = await mergedZip.loadAsync(fileArrayBuffer);
+        let fileContent = await fileZip.file('word/document.xml').async('text');
+        
+        // 提取文档主体内容(去掉xml声明和文档标签)
+        const bodyMatch = fileContent.match(/<w:body[^>]*>([\s\S]*?)<\/w:body>/);
+        if (bodyMatch && bodyMatch[1]) {
+          mergedContent += bodyMatch[1];
+        }
+      }
+      
+      // 添加结束标签
+      mergedContent += '</w:body></w:document>';
+      
+      // 更新合并后的内容
+      baseDocx.file('word/document.xml', mergedContent);
+      
+      // 生成合并后的Word文档
+      const mergedDoc = await baseDocx.generateAsync({
+        type: 'blob',
+        mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+        compression: 'DEFLATE',
+        compressionOptions: { level: 6 }
+      });
+      
+      // 下载合并后的Word文档
+      const url = URL.createObjectURL(mergedDoc);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = `合并Word文档_${new Date().toISOString().slice(0, 10)}.docx`;
+      document.body.appendChild(a);
+      a.click();
+      document.body.removeChild(a);
+      URL.revokeObjectURL(url);
+      
+      toast.success(`已成功合并 ${filesToMerge.length} 个Word文档为一个文件`);
+      
+    } catch (error) {
+      console.error('Word文档合并失败:', error);
+      toast.error('Word文档合并失败,请重试');
+    } finally {
+      setWordMergeDownloading(false);
+    }
+  };
+
   const clearAllFiles = () => {
     setSelectedWordFile(null);
     setSelectedExcelFile(null);
@@ -970,33 +1200,167 @@ export default function WordPreview() {
           </CardHeader>
           <CardContent>
             <div className="space-y-4">
-              <Button
-                onClick={downloadAllFiles}
-                className="w-full"
-              >
-                <DownloadCloud className="h-4 w-4 mr-2" />
-                下载全部文档
-              </Button>
-              
+              {/* 批量操作工具栏 */}
+              <div className="flex flex-wrap gap-2 items-center justify-between p-3 bg-blue-50 rounded-lg">
+                <div className="flex items-center gap-2">
+                  <span className="text-sm font-medium">
+                    已选择 {selectedFiles.size} 个文档
+                  </span>
+                  {selectedFiles.size > 0 && (
+                    <Button
+                      variant="ghost"
+                      size="sm"
+                      onClick={clearSelection}
+                      className="h-7 px-2 text-xs"
+                    >
+                      取消选择
+                    </Button>
+                  )}
+                </div>
+                
+                <div className="flex gap-2">
+                  <Button
+                    variant="outline"
+                    size="sm"
+                    onClick={selectAllFiles}
+                    className="h-7 px-2 text-xs"
+                  >
+                    <CheckSquare className="h-3 w-3 mr-1" />
+                    全选
+                  </Button>
+                  
+                  {selectedFiles.size > 0 && (
+                    <>
+                      <Button
+                        onClick={downloadSelectedFiles}
+                        disabled={isDownloading || selectedFiles.size === 0}
+                        size="sm"
+                        className="h-7 px-2 text-xs bg-green-600 hover:bg-green-700"
+                      >
+                        {isDownloading ? (
+                          <RefreshCw className="h-3 w-3 mr-1 animate-spin" />
+                        ) : (
+                          <Download className="h-3 w-3 mr-1" />
+                        )}
+                        下载选中 ({selectedFiles.size})
+                      </Button>
+                      
+                      <Button
+                        onClick={mergeAndDownloadFiles}
+                        disabled={mergeDownloading || selectedFiles.size === 0}
+                        size="sm"
+                        variant="outline"
+                        className="h-7 px-2 text-xs"
+                      >
+                        {mergeDownloading ? (
+                          <RefreshCw className="h-3 w-3 mr-1 animate-spin" />
+                        ) : (
+                          <Archive className="h-3 w-3 mr-1" />
+                        )}
+                        打包为ZIP
+                      </Button>
+                      
+                      <Button
+                        onClick={mergeWordDocuments}
+                        disabled={wordMergeDownloading || selectedFiles.size === 0}
+                        size="sm"
+                        variant="outline"
+                        className="h-7 px-2 text-xs bg-green-600 hover:bg-green-700 text-white"
+                      >
+                        {wordMergeDownloading ? (
+                          <RefreshCw className="h-3 w-3 mr-1 animate-spin" />
+                        ) : (
+                          <FileText className="h-3 w-3 mr-1" />
+                        )}
+                        合并为Word
+                      </Button>
+                    </>
+                  )}
+                </div>
+              </div>
+
+              {/* 下载全部选项 */}
+              <div className="grid grid-cols-2 gap-2">
+                <Button
+                  onClick={downloadAllFiles}
+                  variant="outline"
+                  className="w-full"
+                >
+                  <DownloadCloud className="h-4 w-4 mr-2" />
+                  逐个下载全部
+                </Button>
+                
+                <Button
+                  onClick={downloadAllAsZip}
+                  disabled={mergeDownloading}
+                  className="w-full bg-blue-600 hover:bg-blue-700"
+                >
+                  {mergeDownloading ? (
+                    <RefreshCw className="h-4 w-4 mr-2 animate-spin" />
+                  ) : (
+                    <Archive className="h-4 w-4 mr-2" />
+                  )}
+                  打包下载全部
+                </Button>
+              </div>
+
+              {/* 下载进度显示 */}
+              {(isDownloading || mergeDownloading) && (
+                <div className="space-y-2">
+                  <Progress
+                    value={isDownloading ? downloadProgress : 100}
+                    className="w-full"
+                  />
+                  <p className="text-sm text-muted-foreground text-center">
+                    {isDownloading ? `正在下载文档... ${Math.round(downloadProgress)}%` : '正在打包文档...'}
+                  </p>
+                </div>
+              )}
+
+              {/* 文档列表 */}
               <div className="space-y-2 max-h-64 overflow-y-auto">
                 {processingResult.generatedFiles.map((file, index) => (
                   <div
                     key={index}
-                    className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
+                    className={`flex items-center justify-between p-3 rounded-lg border ${
+                      selectedFiles.has(index)
+                        ? 'bg-blue-50 border-blue-200'
+                        : 'bg-gray-50 border-gray-200'
+                    }`}
                   >
-                    <div>
-                      <p className="font-medium">{file.name}</p>
-                      <p className="text-sm text-muted-foreground">
-                        包含 {Object.keys(file.fields).length} 个字段
-                      </p>
+                    <div className="flex items-center gap-3 flex-1 min-w-0">
+                      <Button
+                        variant="ghost"
+                        size="icon"
+                        className="h-6 w-6 shrink-0"
+                        onClick={() => toggleFileSelection(index)}
+                      >
+                        {selectedFiles.has(index) ? (
+                          <CheckSquare className="h-4 w-4 text-blue-600" />
+                        ) : (
+                          <Square className="h-4 w-4 text-gray-400" />
+                        )}
+                      </Button>
+                      
+                      <div className="min-w-0 flex-1">
+                        <p className="font-medium truncate">{file.name}</p>
+                        <p className="text-sm text-muted-foreground">
+                          包含 {Object.keys(file.fields).length} 个字段
+                        </p>
+                      </div>
+                    </div>
+                    
+                    <div className="flex items-center gap-1 shrink-0">
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => downloadProcessedFile(file)}
+                        className="h-7 w-7 p-0"
+                        title="单独下载"
+                      >
+                        <Download className="h-3 w-3" />
+                      </Button>
                     </div>
-                    <Button
-                      variant="ghost"
-                      size="sm"
-                      onClick={() => downloadProcessedFile(file)}
-                    >
-                      <Download className="h-4 w-4" />
-                    </Button>
                   </div>
                 ))}
               </div>