171 lines
6.4 KiB
HTML
171 lines
6.4 KiB
HTML
|
|
<!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>
|