|
@@ -12,7 +12,8 @@ import { Alert, AlertDescription } from '@/client/components/ui/alert';
|
|
|
import {
|
|
import {
|
|
|
FileText, Upload, Download, Eye, FileWarning, FileSpreadsheet,
|
|
FileText, Upload, Download, Eye, FileWarning, FileSpreadsheet,
|
|
|
RefreshCw, CheckCircle, AlertCircle, DownloadCloud, Image, Package,
|
|
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';
|
|
} from 'lucide-react';
|
|
|
import { toast } from 'sonner';
|
|
import { toast } from 'sonner';
|
|
|
import WordViewer from '@/client/home/components/WordViewer';
|
|
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 [imageSizeSettings, setImageSizeSettings] = useState<ImageSizeSettings>({ width: 200, height: 150 });
|
|
|
const [showSizeSettings, setShowSizeSettings] = useState(false);
|
|
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 wordFileInputRef = useRef<HTMLInputElement>(null);
|
|
|
const excelFileInputRef = useRef<HTMLInputElement>(null);
|
|
const excelFileInputRef = useRef<HTMLInputElement>(null);
|
|
|
const imageZipInputRef = 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 = () => {
|
|
const clearAllFiles = () => {
|
|
|
setSelectedWordFile(null);
|
|
setSelectedWordFile(null);
|
|
|
setSelectedExcelFile(null);
|
|
setSelectedExcelFile(null);
|
|
@@ -970,33 +1200,167 @@ export default function WordPreview() {
|
|
|
</CardHeader>
|
|
</CardHeader>
|
|
|
<CardContent>
|
|
<CardContent>
|
|
|
<div className="space-y-4">
|
|
<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">
|
|
<div className="space-y-2 max-h-64 overflow-y-auto">
|
|
|
{processingResult.generatedFiles.map((file, index) => (
|
|
{processingResult.generatedFiles.map((file, index) => (
|
|
|
<div
|
|
<div
|
|
|
key={index}
|
|
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>
|
|
</div>
|
|
|
- <Button
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- onClick={() => downloadProcessedFile(file)}
|
|
|
|
|
- >
|
|
|
|
|
- <Download className="h-4 w-4" />
|
|
|
|
|
- </Button>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|
|
|
</div>
|
|
</div>
|