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" ? (
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
setFilenamePattern(e.target.value)}
+ className="w-full border rounded px-3 py-2 text-sm font-mono"
+ placeholder="如:{{编号}}_报告.xlsx"
+ />
+
支持使用 {"{{字段名}}"} 作为变量
+
+
+
+
+
+ {results && (
+
+
生成结果
+
window.api.openDirectory(outputDir)}
+ />
+
+ )}
+
+
+ );
+}