import { useState, useRef } from 'react'; import * as XLSX from 'xlsx'; import PizZip from 'pizzip'; import Docxtemplater from 'docxtemplater'; import ImageModule from 'open-docxtemplater-image-module-2'; import JSZip from 'jszip'; import { Button } from '@/client/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card'; 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, Eye, FileWarning, FileSpreadsheet, RefreshCw, CheckCircle, AlertCircle, DownloadCloud, Image, Package, 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'; import { Badge } from '@/client/components/ui/badge'; import { Progress } from '@/client/components/ui/progress'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/client/components/ui/dialog'; interface WordFile { id: string; name: string; size: number; url: string; previewUrl?: string; } interface ExcelRow { [key: string]: string | number; } interface ImageMapping { [key: string]: { [imageName: string]: File; }; } interface ImageSizeSettings { width: number; height: number; } interface ProcessingResult { originalFile: File; generatedFiles: Array<{ name: string; content: Blob; fields: Record; }>; total: number; } export default function WordPreview() { const [selectedWordFile, setSelectedWordFile] = useState(null); const [selectedExcelFile, setSelectedExcelFile] = useState(null); const [imageZipFile, setImageZipFile] = useState(null); const [previewFile, setPreviewFile] = useState(null); const [isLoading, setIsLoading] = useState(false); const [previewLoading, setPreviewLoading] = useState(false); const [showPreview, setShowPreview] = useState(false); const [processingResult, setProcessingResult] = useState(null); const [excelData, setExcelData] = useState([]); const [processingProgress, setProcessingProgress] = useState(0); const [imageMappings, setImageMappings] = useState({}); const [imagePreviewUrls, setImagePreviewUrls] = useState>>({}); const [selectedImage, setSelectedImage] = useState<{ url: string; name: string; folder: string; } | null>(null); const [currentImageIndex, setCurrentImageIndex] = useState(0); const [allImages, setAllImages] = useState>([]); const [imageSizeSettings, setImageSizeSettings] = useState({ width: 200, height: 150 }); const [showSizeSettings, setShowSizeSettings] = useState(false); // 新增状态:选择下载功能 const [selectedFiles, setSelectedFiles] = useState>(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(null); const excelFileInputRef = useRef(null); const imageZipInputRef = useRef(null); // 文件选择处理 const handleWordFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { const validTypes = [ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ]; const maxSize = 10 * 1024 * 1024; if (!validTypes.includes(file.type)) { toast.error('请选择有效的Word文件(.docx格式)'); return; } if (file.size > maxSize) { toast.error('文件大小超过10MB限制'); return; } setSelectedWordFile(file); setShowPreview(false); toast.success('Word模板已选择'); } }; const handleExcelFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { const validTypes = [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel' ]; const maxSize = 10 * 1024 * 1024; if (!validTypes.includes(file.type)) { toast.error('请选择有效的Excel文件(.xlsx/.xls格式)'); return; } if (file.size > maxSize) { toast.error('文件大小超过10MB限制'); return; } setSelectedExcelFile(file); parseExcelFile(file); toast.success('Excel数据文件已选择'); } }; const handleImageZipSelect = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { const validTypes = ['application/zip', 'application/x-zip-compressed']; const maxSize = 50 * 1024 * 1024; if (!validTypes.includes(file.type)) { toast.error('请选择有效的ZIP压缩文件'); return; } if (file.size > maxSize) { toast.error('压缩文件大小超过50MB限制'); return; } setImageZipFile(file); await parseImageZip(file); toast.success('图片压缩文件已选择'); } }; // 解析Excel文件 const parseExcelFile = async (file: File) => { try { const data = await file.arrayBuffer(); const workbook = XLSX.read(data, { type: 'array' }); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][]; if (jsonData.length < 2) { toast.error('Excel文件格式不正确,需要包含表头和至少一行数据'); return; } const headers = jsonData[0] as string[]; const rows: ExcelRow[] = []; for (let i = 1; i < jsonData.length; i++) { const row = jsonData[i]; const rowData: ExcelRow = {}; headers.forEach((header, index) => { rowData[header] = row[index] || ''; }); rows.push(rowData); } setExcelData(rows); toast.success(`成功解析 ${rows.length} 条数据记录`); } catch (error) { toast.error('Excel文件解析失败'); console.error('Excel parsing error:', error); } }; // 解析图片压缩文件 const parseImageZip = async (file: File) => { try { const zip = new JSZip(); const zipContent = await zip.loadAsync(file); const newImageMappings: ImageMapping = {}; const newImagePreviewUrls: Record> = {}; const allImages: Array<{ url: string; name: string; folder: string }> = []; // 解析文件夹结构:第一层为序号,第二层为图片 for (const [path, zipEntry] of Object.entries(zipContent.files)) { if (!zipEntry.dir && isImageFile(path)) { const pathParts = path.split('/'); if (pathParts.length >= 2) { const folderIndex = pathParts[0]; // 序号文件夹 const imageName = pathParts[pathParts.length - 1].split('.')[0]; // 去掉扩展名的文件名 const fullImageName = pathParts[pathParts.length - 1]; // 完整文件名 if (!newImageMappings[folderIndex]) { newImageMappings[folderIndex] = {}; newImagePreviewUrls[folderIndex] = {}; } const imageFile = await zipEntry.async('blob'); newImageMappings[folderIndex][imageName] = new File([imageFile], imageName, { type: getImageMimeType(path) }); // 创建预览URL const previewUrl = URL.createObjectURL(imageFile); newImagePreviewUrls[folderIndex][imageName] = previewUrl; allImages.push({ url: previewUrl, name: fullImageName, folder: folderIndex }); } } } setImageMappings(newImageMappings); setImagePreviewUrls(newImagePreviewUrls); setAllImages(allImages); toast.success(`成功解析 ${Object.keys(newImageMappings).length} 个文件夹的图片`); } catch (error) { toast.error('图片压缩文件解析失败'); console.error('Image zip parsing error:', error); } }; // 工具函数 const isImageFile = (filename: string): boolean => { const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']; return imageExtensions.some(ext => filename.toLowerCase().endsWith(ext)); }; const getImageMimeType = (filename: string): string => { const extension = filename.toLowerCase().split('.').pop(); const mimeTypes: Record = { 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif', 'bmp': 'image/bmp', 'webp': 'image/webp' }; return mimeTypes[extension || ''] || 'image/jpeg'; }; // 不再需要从模板解析图片尺寸,使用用户设置 // 计算保持比例的图片尺寸 const calculateProportionalSize = ( originalWidth: number, originalHeight: number, maxWidth: number, maxHeight: number ): [number, number] => { const widthRatio = maxWidth / originalWidth; const heightRatio = maxHeight / originalHeight; // 使用较小的比例以保持原始长宽比 const ratio = Math.min(widthRatio, heightRatio, 1); const newWidth = Math.round(originalWidth * ratio); const newHeight = Math.round(originalHeight * ratio); return [newWidth, newHeight]; }; // 替换Word字段并插入图片 const replaceFieldsInWord = async (wordFile: File, excelRow: ExcelRow, rowIndex: number): Promise => { try { const arrayBuffer = await wordFile.arrayBuffer(); const zip = new PizZip(arrayBuffer); // 预加载所有图片数据,避免Promise问题 const folderIndex = (rowIndex + 1).toString(); const imageDataMap: Record = {}; // 预加载当前文件夹的所有图片 if (imageMappings[folderIndex]) { for (const [imageName, imageFile] of Object.entries(imageMappings[folderIndex])) { try { imageDataMap[imageName] = await imageFile.arrayBuffer(); } catch (error) { console.warn(`Failed to load image ${imageName}:`, error); } } } // 使用用户设置的图片尺寸限制 const defaultLimit = imageSizeSettings; // 配置图片模块 - 使用实际图片尺寸并应用限制 const imageSizeCache = new Map(); const imageOpts = { centered: false, getImage: (tagValue: string): ArrayBuffer | null => { if (tagValue && typeof tagValue === 'string') { return imageDataMap[tagValue] || null; } return null; }, getSize: (img: ArrayBuffer, tagValue: string, tagName: string) => { try { const cacheKey = `${tagValue}_${img.byteLength}`; if (imageSizeCache.has(cacheKey)) { return imageSizeCache.get(cacheKey)!; } // 获取图片原始尺寸 let originalWidth = 200; let originalHeight = 150; const view = new DataView(img); // PNG格式检测 if (view.getUint32(0) === 0x89504E47 && view.getUint32(4) === 0x0D0A1A0A) { originalWidth = view.getUint32(16); originalHeight = view.getUint32(20); } // JPEG格式检测 else if (view.getUint16(0) === 0xFFD8) { let offset = 2; while (offset < img.byteLength - 10) { if (view.getUint8(offset) === 0xFF) { const marker = view.getUint8(offset + 1); if (marker >= 0xC0 && marker <= 0xC3) { originalHeight = view.getUint16(offset + 5); originalWidth = view.getUint16(offset + 7); break; } const length = view.getUint16(offset + 2); offset += length + 2; continue; } offset++; } } // 计算符合尺寸限制的最终尺寸 const [finalWidth, finalHeight] = calculateProportionalSize( originalWidth, originalHeight, defaultLimit.width, defaultLimit.height ); const finalSize: [number, number] = [finalWidth, finalHeight]; imageSizeCache.set(cacheKey, finalSize); return finalSize; } catch (error) { console.warn('Failed to get image size, using default:', error); return [defaultLimit.width, defaultLimit.height]; } } }; const imageModule = new ImageModule(imageOpts); // 创建Docxtemplater实例 const doc = new Docxtemplater(zip, { modules: [imageModule], paragraphLoop: true, linebreaks: true, }); // 处理嵌套数据结构 const processedData: Record = {}; // 处理普通字段 Object.entries(excelRow).forEach(([key, value]) => { if (key.includes('.')) { const parts = key.split('.'); let current = processedData; for (let i = 0; i < parts.length - 1; i++) { if (!current[parts[i]]) { current[parts[i]] = {}; } current = current[parts[i]]; } current[parts[parts.length - 1]] = value; } else { processedData[key] = value; } }); // 处理图片字段 - 确保图片名称正确传递给模板 if (imageMappings[folderIndex]) { for (const [imageName] of Object.entries(imageMappings[folderIndex])) { // 确保图片名称作为标签值传递给模板 processedData[imageName] = imageName; } } doc.render(processedData); const generatedDoc = doc.getZip().generate({ type: 'blob', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }); return generatedDoc; } catch (error) { console.error('Word处理错误:', error); throw new Error(`Word文档处理失败,请检查模板格式: ${error instanceof Error ? error.message : String(error)}`); } }; // 处理文件 const processFiles = async () => { if (!selectedWordFile || !selectedExcelFile || excelData.length === 0) { toast.error('请先选择Word模板和Excel数据文件'); return; } setIsLoading(true); setProcessingProgress(0); try { const generatedFiles: ProcessingResult['generatedFiles'] = []; for (let i = 0; i < excelData.length; i++) { const row = excelData[i]; const processedBlob = await replaceFieldsInWord(selectedWordFile, row, i); const fileName = `processed_${i + 1}_${selectedWordFile.name}`; generatedFiles.push({ name: fileName, content: processedBlob, fields: row }); setProcessingProgress(((i + 1) / excelData.length) * 100); } const result: ProcessingResult = { originalFile: selectedWordFile, generatedFiles, total: generatedFiles.length }; setProcessingResult(result); toast.success(`成功生成 ${generatedFiles.length} 个文档`); } catch (error) { toast.error('文档处理失败'); console.error('Processing error:', error); } finally { setIsLoading(false); setProcessingProgress(0); } }; // 预览功能 const handlePreview = async () => { if (!selectedWordFile) { toast.error('请先选择Word文件'); return; } setPreviewLoading(true); setShowPreview(true); try { const fileUrl = URL.createObjectURL(selectedWordFile); const wordFile: WordFile = { id: Date.now().toString(), name: selectedWordFile.name, size: selectedWordFile.size, url: fileUrl, previewUrl: fileUrl }; setPreviewFile(wordFile); toast.success('正在预览Word模板...'); } catch (error) { toast.error('文件预览失败'); console.error('Preview error:', error); setShowPreview(false); } finally { setPreviewLoading(false); } }; // 下载功能 const downloadProcessedFile = (file: ProcessingResult['generatedFiles'][0]) => { const url = URL.createObjectURL(file.content); const a = document.createElement('a'); a.href = url; a.download = file.name; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const downloadAllFiles = () => { if (!processingResult) return; processingResult.generatedFiles.forEach((file, index) => { setTimeout(() => { downloadProcessedFile(file); }, index * 500); }); }; // 新增:选择下载功能 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(/]*>([\s\S]*?)<\/w:body>/); if (bodyMatch && bodyMatch[1]) { mergedContent += bodyMatch[1]; } } // 添加结束标签 mergedContent += ''; // 更新合并后的内容 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); setImageZipFile(null); setExcelData([]); setProcessingResult(null); setImageMappings({}); setImagePreviewUrls({}); setSelectedImage(null); setAllImages([]); setPreviewFile(null); setShowPreview(false); if (wordFileInputRef.current) wordFileInputRef.current.value = ''; if (excelFileInputRef.current) excelFileInputRef.current.value = ''; if (imageZipInputRef.current) imageZipInputRef.current.value = ''; toast.success('已清除所有文件'); }; const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; // 图片预览功能 const openImagePreview = (url: string, name: string, folder: string) => { const imageIndex = allImages.findIndex(img => img.url === url); setCurrentImageIndex(imageIndex !== -1 ? imageIndex : 0); setSelectedImage({ url, name, folder }); }; const closeImagePreview = () => { setSelectedImage(null); }; const navigateImage = (direction: 'prev' | 'next') => { if (allImages.length === 0) return; let newIndex = currentImageIndex; if (direction === 'prev') { newIndex = currentImageIndex > 0 ? currentImageIndex - 1 : allImages.length - 1; } else { newIndex = currentImageIndex < allImages.length - 1 ? currentImageIndex + 1 : 0; } setCurrentImageIndex(newIndex); setSelectedImage(allImages[newIndex]); }; const getTotalImages = () => { return Object.values(imagePreviewUrls).reduce((total, folder) => total + Object.keys(folder).length, 0); }; return (

元亨Word批量处理增强版

支持图片压缩文件,自动生成替换字段和图片的文档

{/* 文件上传区域 */}
{/* Word模板上传 */} 选择Word模板 支持 .docx 格式的Word文档,最大10MB
{selectedWordFile && (

文件名: {selectedWordFile.name}

大小: {formatFileSize(selectedWordFile.size)}

)}
{/* Excel数据上传 */} 选择Excel数据 支持 .xlsx/.xls 格式的Excel文档,最大10MB
{selectedExcelFile && (

文件名: {selectedExcelFile.name}

大小: {formatFileSize(selectedExcelFile.size)}

{excelData.length > 0 && (

数据行数: {excelData.length}

)}
)}
{/* 图片压缩文件上传 */} 选择图片压缩包 支持 .zip 格式压缩包,最大50MB
{imageZipFile && (

文件名: {imageZipFile.name}

大小: {formatFileSize(imageZipFile.size)}

{Object.keys(imageMappings).length > 0 && (

文件夹数: {Object.keys(imageMappings).length}

)}
)}
{/* 操作按钮 */} 操作区域 选择文件后执行相应操作
{/* 图片尺寸设置 */}

图片尺寸设置

{showSizeSettings && (
setImageSizeSettings(prev => ({ ...prev, width: Math.max(0, Math.min(1000, parseInt(e.target.value) || 0)) }))} className="mt-1" />
setImageSizeSettings(prev => ({ ...prev, height: Math.max(0, Math.min(1000, parseInt(e.target.value) || 0)) }))} className="mt-1" />

• 当前设置:宽度 {imageSizeSettings.width}px,高度 {imageSizeSettings.height}px

• 系统将自动保持图片长宽比例

• 支持范围:0-1000 像素,可输入任意数值

• 建议尺寸:单列620×1000像素,双列300×500像素

)}
{isLoading && (

正在处理文档... {Math.round(processingProgress)}%

)}
{/* 预览区域 */} {showPreview && selectedWordFile && ( 文档预览 {selectedWordFile.name} )} {/* 图片映射预览 */} {Object.keys(imageMappings).length > 0 && ( 图片映射预览 共 {getTotalImages()} 张图片,点击缩略图查看大图
{Object.entries(imagePreviewUrls).map(([folder, images]) => (

文件夹 {folder} ({Object.keys(images).length} 张图片)

{Object.entries(images).map(([imageName, previewUrl]) => (
openImagePreview(previewUrl, imageName, folder)} >
{imageName}

{imageName}

))}
))}
)} {/* 数据预览 */} {excelData.length > 0 && ( Excel数据预览 显示前5行数据,共 {excelData.length} 行
{Object.keys(excelData[0]).map(header => ( ))} {excelData.slice(0, 5).map((row, index) => ( {Object.values(row).map((value, valueIndex) => ( ))} ))}
{header}
{String(value)}
{excelData.length > 5 && (

还有 {excelData.length - 5} 行数据...

)}
)} {/* 处理结果预览 */} {processingResult && processingResult.generatedFiles.length > 0 && ( 处理结果预览 预览第一个生成的文档 )} {/* 处理结果 */} {processingResult && ( 处理完成 共生成 {processingResult.total} 个文档
{/* 批量操作工具栏 */}
已选择 {selectedFiles.size} 个文档 {selectedFiles.size > 0 && ( )}
{selectedFiles.size > 0 && ( <> )}
{/* 下载全部选项 */}
{/* 下载进度显示 */} {(isDownloading || mergeDownloading) && (

{isDownloading ? `正在下载文档... ${Math.round(downloadProgress)}%` : '正在打包文档...'}

)} {/* 文档列表 */}
{processingResult.generatedFiles.map((file, index) => (

{file.name}

包含 {Object.keys(file.fields).length} 个字段

))}
)} {/* 使用说明 */} 使用说明(增强版)

1. 准备Word模板

使用 {'{字段名}'} 格式作为文本占位符,使用 {'{%图片名}'} 格式作为图片占位符

2. 设置图片尺寸(新增)

使用"图片尺寸设置"按钮设置图片最大尺寸,系统将自动限制所有图片尺寸并保留长宽比例

3. 准备Excel数据

Excel文件第一行为表头,列名应与Word模板中的字段名对应

4. 准备图片压缩包

压缩包结构:第一层为序号文件夹(1,2,3...对应Excel行),第二层为图片文件(文件名对应模板中的图片名)

5. 图片命名规则

例如:模板中使用 {'{%logo}'},则图片文件应命名为 logo.jpg/png等

{/* 注意事项 */} 注意事项

• Word模板中的字段名必须与Excel表头完全匹配

• 图片文件名必须与模板中的图片占位符匹配(不含扩展名)

• 文件夹序号必须与Excel行号对应(第1行对应文件夹"1")

• 如果图片不存在,对应位置将留空

• 支持jpg、jpeg、png、gif、bmp、webp格式图片

• 图片占位符使用 {'{%图片名%}'} 格式,如 {'{%logo%}'}

{/* 图片查看器模态框 */} 图片预览
{selectedImage?.folder} / {selectedImage?.name} ({currentImageIndex + 1} / {allImages.length})
{selectedImage && (
{selectedImage.name}
{/* 导航按钮 */} {allImages.length > 1 && ( <> )} {/* 底部信息 */}
文件名: {selectedImage.name}
)}
); }