From a01c09dc9f4b976a5fa4c5142f72ac13236cc0dd Mon Sep 17 00:00:00 2001 From: MikiVL Date: Tue, 5 May 2026 13:47:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=B9=E9=87=8F=E7=94=9F=E6=88=90?= =?UTF-8?q?=E9=A1=B5=EF=BC=88=E6=96=87=E4=BB=B6/=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E3=80=81=E8=BF=9B=E5=BA=A6=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- renderer/src/components/ProgressPanel.jsx | 45 +++++ renderer/src/pages/Generate.jsx | 196 +++++++++++++++++++++- 2 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 renderer/src/components/ProgressPanel.jsx diff --git a/renderer/src/components/ProgressPanel.jsx b/renderer/src/components/ProgressPanel.jsx new file mode 100644 index 0000000..d4fc845 --- /dev/null +++ b/renderer/src/components/ProgressPanel.jsx @@ -0,0 +1,45 @@ +import React from "react"; + +export default function ProgressPanel({ results, outputDir, onOpenDir }) { + if (!results) return null; + + const success = results.filter((r) => r.status === "success").length; + const failed = results.filter((r) => r.status === "error").length; + + return ( +
+
+ 成功:{success} + {failed > 0 && 失败:{failed}} +
+ {outputDir && ( + + )} + {failed > 0 && ( +
+

失败详情:

+ {results + .filter((r) => r.status === "error") + .map((r) => ( +

+ 第 {r.row + 1} 行:{r.error} +

+ ))} +
+ )} +
+ {results.map((r) => ( +
+ {r.status === "success" ? "✓" : "✗"} 第 {r.row + 1} 行 + {r.file?.split("/").pop() || r.file?.split("\\").pop()} +
+ ))} +
+
+ ); +} diff --git a/renderer/src/pages/Generate.jsx b/renderer/src/pages/Generate.jsx index f5c1a86..12b6aca 100644 --- a/renderer/src/pages/Generate.jsx +++ b/renderer/src/pages/Generate.jsx @@ -1 +1,195 @@ -export default function Generate() { return
Generate
; } +import React, { useState, useEffect } from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import ProgressPanel from "../components/ProgressPanel"; + +export default function Generate() { + const navigate = useNavigate(); + const location = useLocation(); + const preselectedId = location.state?.templateId; + + const [templates, setTemplates] = useState([]); + const [selectedId, setSelectedId] = useState(preselectedId || ""); + const [dataSource, setDataSource] = useState("file"); + const [dataFilePath, setDataFilePath] = useState(""); + const [manualRows, setManualRows] = useState([{}]); + const [outputDir, setOutputDir] = useState(""); + const [filenamePattern, setFilenamePattern] = useState("输出_{{编号}}.xlsx"); + const [generating, setGenerating] = useState(false); + const [results, setResults] = useState(null); + + useEffect(() => { + window.api.listTemplates().then(setTemplates); + }, []); + + const selectedTemplate = templates.find((t) => t.id === selectedId); + const fields = selectedTemplate?.fields || []; + + async function handleSelectDataFile() { + const path = await window.api.selectFile([ + { name: "Excel 文件", extensions: ["xlsx"] }, + ]); + if (path) setDataFilePath(path); + } + + async function handleSelectOutputDir() { + const dir = await window.api.selectDirectory(); + if (dir) setOutputDir(dir); + } + + function updateManualRow(rowIdx, fieldName, value) { + setManualRows((prev) => { + const next = [...prev]; + next[rowIdx] = { ...next[rowIdx], [fieldName]: value }; + return next; + }); + } + + function addManualRow() { + setManualRows((prev) => [...prev, {}]); + } + + function removeManualRow(idx) { + setManualRows((prev) => prev.filter((_, i) => i !== idx)); + } + + async function handleGenerate() { + if (!selectedId) return alert("请选择模板"); + if (!outputDir) return alert("请选择输出目录"); + + setGenerating(true); + setResults(null); + try { + const result = await window.api.generate({ + template_path: selectedTemplate.file_path, + fields: fields, + rows: dataSource === "manual" ? manualRows : null, + data_file_path: dataSource === "file" ? dataFilePath : null, + output_dir: outputDir, + filename_pattern: filenamePattern, + }); + setResults(result.results); + } finally { + setGenerating(false); + } + } + + return ( +
+
+
+

批量生成

+ +
+ +
+
+ + +
+ +
+ +
+ + +
+
+ + {dataSource === "file" ? ( +
+ + +
+ ) : ( +
+ + + + {fields.map((f) => ( + + ))} + + + + + {manualRows.map((row, idx) => ( + + {fields.map((f) => ( + + ))} + + + ))} + +
{f.name}
+ updateManualRow(idx, f.name, e.target.value)} + className="w-full px-1 py-0.5 text-sm outline-none" + /> + + +
+ +
+ )} + +
+ +
+ + +
+
+ +
+ + setFilenamePattern(e.target.value)} + className="w-full border rounded px-3 py-2 text-sm font-mono" + placeholder="如:{{编号}}_报告.xlsx" + /> +

支持使用 {"{{字段名}}"} 作为变量

+
+ + +
+ + {results && ( +
+

生成结果

+ window.api.openDirectory(outputDir)} + /> +
+ )} +
+
+ ); +}