feat: add pcf analysis web app and api
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PCF 焊接数据导入生成工具</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="panel">
|
||||
<header>
|
||||
<h1>PCF 焊接数据导入生成工具</h1>
|
||||
<p>上传多个 .pcf 文件,生成基于焊接数据导入模板的 .xls 文件。</p>
|
||||
</header>
|
||||
|
||||
<form id="uploadForm">
|
||||
<label class="dropzone">
|
||||
<input id="files" name="files" type="file" accept=".pcf" multiple required>
|
||||
<span>选择 PCF 文件</span>
|
||||
<small>支持一次选择多个 .pcf 文件</small>
|
||||
</label>
|
||||
<section id="fileList" class="file-list hidden" aria-live="polite"></section>
|
||||
<button id="submitBtn" type="submit">开始生成</button>
|
||||
</form>
|
||||
|
||||
<section id="result" class="result hidden"></section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById("uploadForm");
|
||||
const result = document.getElementById("result");
|
||||
const submitBtn = document.getElementById("submitBtn");
|
||||
const fileInput = document.getElementById("files");
|
||||
const fileList = document.getElementById("fileList");
|
||||
|
||||
fileInput.addEventListener("change", () => {
|
||||
const files = Array.from(fileInput.files || []);
|
||||
if (!files.length) {
|
||||
fileList.classList.add("hidden");
|
||||
fileList.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
fileList.classList.remove("hidden");
|
||||
fileList.innerHTML = `
|
||||
<h2>已选择文件(${files.length})</h2>
|
||||
<ul>
|
||||
${files.map(file => `<li><span>${escapeHtml(file.name)}</span><small>${formatBytes(file.size)}</small></li>`).join("")}
|
||||
</ul>
|
||||
`;
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const files = document.getElementById("files").files;
|
||||
if (!files.length) return;
|
||||
|
||||
const data = new FormData();
|
||||
for (const file of files) data.append("files", file);
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = "生成中...";
|
||||
result.classList.remove("hidden");
|
||||
result.innerHTML = "<p>正在解析 PCF 并生成 Excel...</p>";
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/jobs", { method: "POST", body: data });
|
||||
const payload = await response.json();
|
||||
if (!response.ok) throw new Error(payload.detail || "生成失败");
|
||||
|
||||
result.innerHTML = `
|
||||
<h2>生成完成</h2>
|
||||
<dl>
|
||||
<dt>任务编号</dt><dd>${payload.job_id}</dd>
|
||||
<dt>文件数</dt><dd>${payload.input_files}</dd>
|
||||
<dt>焊口数</dt><dd>${payload.weld_count}</dd>
|
||||
<dt>警告数</dt><dd>${payload.warning_count}</dd>
|
||||
<dt>错误数</dt><dd>${payload.error_count}</dd>
|
||||
</dl>
|
||||
<div class="downloads">
|
||||
<a href="${payload.downloads.xls}">下载导入文件 .xls</a>
|
||||
<a href="${payload.downloads.issues}">下载问题清单 .csv</a>
|
||||
<a href="${payload.downloads.report}">下载解析报告 .json</a>
|
||||
</div>
|
||||
${(payload.messages || []).length ? `<ul>${payload.messages.map(m => `<li>${m}</li>`).join("")}</ul>` : ""}
|
||||
${renderTabs(payload.tables || {})}
|
||||
`;
|
||||
} catch (error) {
|
||||
result.innerHTML = `<p class="error">${error.message}</p>`;
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = "开始生成";
|
||||
}
|
||||
});
|
||||
|
||||
function renderTabs(tables) {
|
||||
const importColumns = tables.import_columns || (tables.import_headers || []).map(h => ({key: h, label: h}));
|
||||
const importRows = tables.import_rows || [];
|
||||
const issueRows = tables.issue_rows || [];
|
||||
return `
|
||||
<section class="tabs" data-active="import">
|
||||
<div class="tab-buttons">
|
||||
<button type="button" class="tab-button active" data-tab="import">导入数据(${importRows.length})</button>
|
||||
<button type="button" class="tab-button" data-tab="issues">问题清单(${issueRows.length})</button>
|
||||
</div>
|
||||
<div class="tab-panel" data-panel="import">
|
||||
${renderColumnTable(importColumns, importRows, "暂无导入数据")}
|
||||
</div>
|
||||
<div class="tab-panel hidden" data-panel="issues">
|
||||
${renderTable(["类型", "文件", "焊口代号", "字段", "内容", "处理状态"], issueRows, "暂无问题")}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderColumnTable(columns, rows, emptyText) {
|
||||
if (!rows.length) return `<p>${emptyText}</p>`;
|
||||
return `
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr>${columns.map(col => `<th>${escapeHtml(col.label || col.key)}</th>`).join("")}</tr></thead>
|
||||
<tbody>
|
||||
${rows.map(row => `<tr>${columns.map(col => `<td>${escapeHtml(row[col.key] ?? "")}</td>`).join("")}</tr>`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTable(headers, rows, emptyText) {
|
||||
if (!rows.length) return `<p>${emptyText}</p>`;
|
||||
return `
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr>${headers.map(h => `<th>${escapeHtml(h)}</th>`).join("")}</tr></thead>
|
||||
<tbody>
|
||||
${rows.map(row => `<tr>${headers.map(h => `<td>${escapeHtml(row[h] ?? "")}</td>`).join("")}</tr>`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function formatBytes(size) {
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
||||
return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
result.addEventListener("click", (event) => {
|
||||
const button = event.target.closest(".tab-button");
|
||||
if (!button) return;
|
||||
const tabs = button.closest(".tabs");
|
||||
const name = button.dataset.tab;
|
||||
tabs.querySelectorAll(".tab-button").forEach(btn => btn.classList.toggle("active", btn === button));
|
||||
tabs.querySelectorAll(".tab-panel").forEach(panel => panel.classList.toggle("hidden", panel.dataset.panel !== name));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user