feat: 批量生成页(文件/手动输入、进度展示)
This commit is contained in:
parent
a37f17b3b6
commit
a01c09dc9f
45
renderer/src/components/ProgressPanel.jsx
Normal file
45
renderer/src/components/ProgressPanel.jsx
Normal file
@ -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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4 text-sm">
|
||||
<span className="text-green-600 font-medium">成功:{success}</span>
|
||||
{failed > 0 && <span className="text-red-500 font-medium">失败:{failed}</span>}
|
||||
</div>
|
||||
{outputDir && (
|
||||
<button
|
||||
onClick={onOpenDir}
|
||||
className="text-blue-600 text-sm hover:underline"
|
||||
>
|
||||
打开输出文件夹 →
|
||||
</button>
|
||||
)}
|
||||
{failed > 0 && (
|
||||
<div className="border border-red-100 rounded bg-red-50 p-3 space-y-1">
|
||||
<p className="text-xs font-medium text-red-600">失败详情:</p>
|
||||
{results
|
||||
.filter((r) => r.status === "error")
|
||||
.map((r) => (
|
||||
<p key={r.row} className="text-xs text-red-500">
|
||||
第 {r.row + 1} 行:{r.error}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-48 overflow-y-auto space-y-1">
|
||||
{results.map((r) => (
|
||||
<div key={r.row} className={`text-xs px-2 py-1 rounded flex justify-between ${r.status === "success" ? "bg-green-50 text-green-700" : "bg-red-50 text-red-600"}`}>
|
||||
<span>{r.status === "success" ? "✓" : "✗"} 第 {r.row + 1} 行</span>
|
||||
<span className="truncate ml-2 text-gray-400">{r.file?.split("/").pop() || r.file?.split("\\").pop()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1 +1,195 @@
|
||||
export default function Generate() { return <div>Generate</div>; }
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-50 p-8">
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">批量生成</h1>
|
||||
<button onClick={() => navigate("/templates")} className="text-gray-400 hover:text-gray-600 text-sm">
|
||||
← 返回模板列表
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg border p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">选择模板</label>
|
||||
<select
|
||||
value={selectedId}
|
||||
onChange={(e) => setSelectedId(e.target.value)}
|
||||
className="w-full border rounded px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">请选择...</option>
|
||||
{templates.map((t) => (
|
||||
<option key={t.id} value={t.id}>{t.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">数据来源</label>
|
||||
<div className="flex gap-4 text-sm">
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input type="radio" value="file" checked={dataSource === "file"} onChange={() => setDataSource("file")} />
|
||||
上传 Excel 数据源
|
||||
</label>
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input type="radio" value="manual" checked={dataSource === "manual"} onChange={() => setDataSource("manual")} />
|
||||
手动填写
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dataSource === "file" ? (
|
||||
<div className="flex gap-2">
|
||||
<input readOnly value={dataFilePath} className="flex-1 border rounded px-3 py-2 text-sm bg-gray-50" placeholder="选择数据源 Excel 文件..." />
|
||||
<button onClick={handleSelectDataFile} className="border rounded px-4 py-2 text-sm hover:bg-gray-100">选择文件</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="text-sm border-collapse w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
{fields.map((f) => (
|
||||
<th key={f.id} className="border px-2 py-1 bg-gray-50 text-left text-xs font-medium">{f.name}</th>
|
||||
))}
|
||||
<th className="border px-2 py-1 bg-gray-50 w-8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{manualRows.map((row, idx) => (
|
||||
<tr key={idx}>
|
||||
{fields.map((f) => (
|
||||
<td key={f.id} className="border px-1 py-1">
|
||||
<input
|
||||
value={row[f.name] || ""}
|
||||
onChange={(e) => updateManualRow(idx, f.name, e.target.value)}
|
||||
className="w-full px-1 py-0.5 text-sm outline-none"
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
<td className="border px-2 py-1 text-center">
|
||||
<button onClick={() => removeManualRow(idx)} className="text-gray-300 hover:text-red-500 text-xs">✕</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<button onClick={addManualRow} className="text-blue-600 text-xs mt-2 hover:underline">+ 添加行</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">输出目录</label>
|
||||
<div className="flex gap-2">
|
||||
<input readOnly value={outputDir} className="flex-1 border rounded px-3 py-2 text-sm bg-gray-50" placeholder="选择输出文件夹..." />
|
||||
<button onClick={handleSelectOutputDir} className="border rounded px-4 py-2 text-sm hover:bg-gray-100">选择目录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">输出文件命名规则</label>
|
||||
<input
|
||||
value={filenamePattern}
|
||||
onChange={(e) => setFilenamePattern(e.target.value)}
|
||||
className="w-full border rounded px-3 py-2 text-sm font-mono"
|
||||
placeholder="如:{{编号}}_报告.xlsx"
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">支持使用 {"{{字段名}}"} 作为变量</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={generating || !selectedId || !outputDir}
|
||||
className="w-full bg-blue-600 text-white rounded px-4 py-2 text-sm hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{generating ? "生成中..." : "开始生成"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{results && (
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<h2 className="font-medium mb-4">生成结果</h2>
|
||||
<ProgressPanel
|
||||
results={results}
|
||||
outputDir={outputDir}
|
||||
onOpenDir={() => window.api.openDirectory(outputDir)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user