B-42: 导入功能加固——逐工作表确认、流式上传与诊断证据
This commit is contained in:
+260
-24
@@ -1151,6 +1151,8 @@ function resetUpload() {
|
||||
$("#uploadForm")?.reset();
|
||||
if ($("#filePreview")) $("#filePreview").hidden = true;
|
||||
if ($("#parseResult")) $("#parseResult").hidden = true;
|
||||
if ($("#sheetReview")) $("#sheetReview").hidden = true;
|
||||
if ($("#sheetList")) $("#sheetList").replaceChildren();
|
||||
if ($("#dropzone")) $("#dropzone").hidden = false;
|
||||
if ($("#parseButton")) {
|
||||
$("#parseButton").disabled = true;
|
||||
@@ -1203,41 +1205,265 @@ async function parseFile() {
|
||||
result = { status: "error", message: "解析服务暂时不可用,请稍后重试。" };
|
||||
}
|
||||
state.parseResult = result;
|
||||
const duplicated = result.status === "duplicate";
|
||||
const opaqueDuplicate = duplicated && !result.bank;
|
||||
const panel = $("#parseResult");
|
||||
panel.classList.toggle("is-exception", !parsed);
|
||||
$("use", panel).setAttribute("href", parsed ? "icons.svg#circle-check" : "icons.svg#circle-alert");
|
||||
$("strong", panel).textContent = duplicated ? "文件已导入过" : parsed ? "文件解析完成" : "未识别到银行模板";
|
||||
$("#parseSummary").textContent = parsed
|
||||
? opaqueDuplicate
|
||||
? "相同内容的文件已由其他公司导入,仅记录重复状态,不重复入账。"
|
||||
: `${result.bank} · 表头第 ${result.header_row} 行 · ${result.transactions} 条明细 · ${result.warnings.length ? `${result.warnings.length} 项提示` : "校验通过"}${duplicated ? " · 重复上传,复用已有批次" : ""}`
|
||||
: `${result.message} 系统不会猜测模板或自动入账。`;
|
||||
panel.hidden = false;
|
||||
$("#parseButton span").textContent = parsed ? "确认导入" : "提交异常";
|
||||
$("#parseButton").disabled = false;
|
||||
$("#parseButton").dataset.stage = "confirm";
|
||||
renderParseResult(result, parsed);
|
||||
}
|
||||
|
||||
function confirmImport() {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `<td><strong>IMP-260806-019</strong></td><td>${$("#accountSelect").value}</td><td>${state.parseResult?.period_start || "待确认"}—${state.parseResult?.period_end || "待确认"}</td><td>${state.parseResult?.transactions ?? "—"}</td><td><span class="status warning">待计算</span></td><td><span class="status neutral">已解析</span></td><td>刚刚</td>`;
|
||||
$("#importRows")?.prepend(row);
|
||||
function renderParseResult(result, parsed) {
|
||||
const panel = $("#parseResult");
|
||||
if (!panel) return;
|
||||
const sheets = Array.isArray(result.sheets) ? result.sheets : [];
|
||||
const duplicated = result.status === "duplicate";
|
||||
const opaque = duplicated && !sheets.length;
|
||||
panel.classList.toggle("is-exception", !parsed);
|
||||
$("use", panel).setAttribute("href", parsed ? "icons.svg#circle-check" : "icons.svg#circle-alert");
|
||||
$("#parseTitle").textContent = duplicated ? "文件已导入过" : parsed ? "文件解析完成" : "未识别到银行模板";
|
||||
const pendingCount = sheets.filter((s) => s.outcome === "parsed" && s.review_status === "pending").length;
|
||||
const exceptionCount = sheets.filter((s) => s.outcome === "exception").length;
|
||||
const ignoredCount = sheets.filter((s) => s.outcome === "ignored" || s.review_status === "ignored").length;
|
||||
let summary;
|
||||
if (opaque) {
|
||||
summary = "相同内容的文件已由其他公司导入,仅记录重复状态,不重复入账。";
|
||||
} else if (sheets.length) {
|
||||
summary = `${sheets.length} 个工作表:${pendingCount} 个待确认${exceptionCount ? `、${exceptionCount} 个异常` : ""}${ignoredCount ? `、${ignoredCount} 个忽略` : ""}。解析成功不等于业务确认。`;
|
||||
} else {
|
||||
summary = `${result.message || ""} 系统不会猜测模板或自动入账。`;
|
||||
}
|
||||
$("#parseSummary").textContent = summary;
|
||||
panel.hidden = false;
|
||||
renderSheetList(sheets, result.batch_id);
|
||||
|
||||
const button = $("#parseButton");
|
||||
button.dataset.stage = "confirm";
|
||||
if (!parsed) {
|
||||
button.querySelector("span").textContent = "关闭";
|
||||
} else if (opaque) {
|
||||
button.querySelector("span").textContent = "完成";
|
||||
} else if (sheets.length) {
|
||||
button.querySelector("span").textContent = pendingCount ? `确认全部(${pendingCount} 个待确认)` : "完成";
|
||||
} else {
|
||||
button.querySelector("span").textContent = "完成";
|
||||
}
|
||||
}
|
||||
|
||||
function sheetStatusMeta(sheet) {
|
||||
if (sheet.review_status === "confirmed") return { className: "success", label: "已确认" };
|
||||
if (sheet.review_status === "ignored") return { className: "neutral", label: "已忽略" };
|
||||
if (sheet.outcome === "exception") return { className: "danger", label: "异常待处理" };
|
||||
if (sheet.outcome === "ignored") return { className: "neutral", label: "空表忽略" };
|
||||
return { className: "warning", label: "待确认" };
|
||||
}
|
||||
|
||||
function renderSheetList(sheets, batchId) {
|
||||
const wrap = $("#sheetReview");
|
||||
const list = $("#sheetList");
|
||||
if (!wrap || !list || !sheets.length) {
|
||||
if (wrap) wrap.hidden = true;
|
||||
return;
|
||||
}
|
||||
wrap.hidden = false;
|
||||
list.replaceChildren(...sheets.map((sheet) => buildSheetItem(sheet, batchId)));
|
||||
}
|
||||
|
||||
function buildSheetItem(sheet, batchId) {
|
||||
const item = document.createElement("article");
|
||||
item.className = "sheet-item";
|
||||
if (sheet.review_status === "pending") item.classList.add("is-pending");
|
||||
const meta = sheetStatusMeta(sheet);
|
||||
const head = document.createElement("div");
|
||||
head.className = "sheet-item-head";
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = sheet.sheet_name;
|
||||
const badge = document.createElement("em");
|
||||
badge.className = `status ${meta.className}`;
|
||||
badge.textContent = meta.label;
|
||||
head.append(name, badge);
|
||||
const details = document.createElement("p");
|
||||
details.className = "sheet-item-meta";
|
||||
if (sheet.outcome === "parsed" && sheet.bank) {
|
||||
const period = sheet.period_start ? ` · ${sheet.period_start}—${sheet.period_end}` : "";
|
||||
details.textContent = `${sheet.bank} · ${sheet.transactions} 条明细${period}`;
|
||||
} else if (sheet.message) {
|
||||
details.textContent = sheet.message;
|
||||
} else {
|
||||
details.textContent = "空工作表。";
|
||||
}
|
||||
if (sheet.review_reason) {
|
||||
details.textContent += ` · 原因:${sheet.review_reason}`;
|
||||
}
|
||||
const body = document.createElement("div");
|
||||
body.append(head, details);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "sheet-item-actions";
|
||||
if (sheet.review_status === "pending") {
|
||||
if (sheet.outcome === "parsed") {
|
||||
const confirmButton = document.createElement("button");
|
||||
confirmButton.type = "button";
|
||||
confirmButton.className = "text-button";
|
||||
confirmButton.textContent = "确认";
|
||||
confirmButton.addEventListener("click", () => sheetReviewAction(batchId, sheet.sheet_name, "confirm"));
|
||||
actions.append(confirmButton);
|
||||
}
|
||||
const ignoreButton = document.createElement("button");
|
||||
ignoreButton.type = "button";
|
||||
ignoreButton.className = "text-button";
|
||||
ignoreButton.textContent = "忽略";
|
||||
ignoreButton.addEventListener("click", () => sheetReviewAction(batchId, sheet.sheet_name, "ignore"));
|
||||
actions.append(ignoreButton);
|
||||
}
|
||||
item.append(body, actions);
|
||||
return item;
|
||||
}
|
||||
|
||||
async function sheetReviewAction(batchId, sheetName, decision) {
|
||||
const payload = { sheets: [sheetName] };
|
||||
if (decision === "ignore") {
|
||||
const reason = (window.prompt("请填写忽略原因(必填):", "") || "").trim();
|
||||
if (!reason) {
|
||||
showToast("忽略未提交", "必须填写忽略原因");
|
||||
return;
|
||||
}
|
||||
payload.reason = reason;
|
||||
}
|
||||
const response = await fetch(`/api/batches/${batchId}/${decision}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}).catch(() => null);
|
||||
if (response?.status === 401 || response?.status === 403) {
|
||||
window.location.href = "index.html";
|
||||
return;
|
||||
}
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
if (!response || !response.ok) {
|
||||
showToast("操作失败", result?.message || "请稍后重试");
|
||||
return;
|
||||
}
|
||||
showToast(decision === "confirm" ? "工作表已确认" : "工作表已忽略", `${sheetName}`);
|
||||
await refreshAfterSheetAction(result, batchId);
|
||||
}
|
||||
|
||||
async function refreshAfterSheetAction(result, batchId) {
|
||||
if (Array.isArray(result.sheets)) renderSheetList(result.sheets, batchId);
|
||||
await loadImportBatches();
|
||||
const button = $("#parseButton");
|
||||
if (!button) return;
|
||||
const pending = (result.sheets || []).filter((s) => s.outcome === "parsed" && s.review_status === "pending").length;
|
||||
if (pending === 0 && (result.sheets || []).length) {
|
||||
button.querySelector("span").textContent = "完成";
|
||||
button.dataset.stage = "done";
|
||||
} else {
|
||||
button.querySelector("span").textContent = `确认全部(${pending} 个待确认)`;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmImport() {
|
||||
const result = state.parseResult;
|
||||
const batchId = result?.batch_id;
|
||||
const sheets = Array.isArray(result?.sheets) ? result.sheets : [];
|
||||
const pending = sheets
|
||||
.filter((s) => s.outcome === "parsed" && s.review_status === "pending")
|
||||
.map((s) => s.sheet_name);
|
||||
if (!pending.length) {
|
||||
$("#uploadDialog").close();
|
||||
await loadImportBatches();
|
||||
showView("upload");
|
||||
return;
|
||||
}
|
||||
const button = $("#parseButton");
|
||||
button.disabled = true;
|
||||
button.querySelector("span").textContent = "正在确认...";
|
||||
const response = await fetch(`/api/batches/${batchId}/confirm`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sheets: pending }),
|
||||
}).catch(() => null);
|
||||
if (response?.status === 401 || response?.status === 403) {
|
||||
window.location.href = "index.html";
|
||||
return;
|
||||
}
|
||||
const outcome = await response?.json().catch(() => ({}));
|
||||
if (!response || !response.ok) {
|
||||
button.disabled = false;
|
||||
button.querySelector("span").textContent = "确认失败,点击重试";
|
||||
showToast("确认失败", outcome?.message || "请稍后重试");
|
||||
return;
|
||||
}
|
||||
button.disabled = false;
|
||||
if (Array.isArray(outcome.sheets)) renderSheetList(outcome.sheets, batchId);
|
||||
await loadImportBatches();
|
||||
$("#uploadDialog").close();
|
||||
showView("upload");
|
||||
showToast("流水已进入归集队列", "原始文件和解析结果已保留");
|
||||
showToast("流水已确认", `${outcome.updated.length} 个工作表已确认;未确认的工作表不参与计算`);
|
||||
}
|
||||
|
||||
function submitImportException() {
|
||||
$("#uploadDialog").close();
|
||||
showView("upload");
|
||||
showToast("解析异常已提交", `${state.selectedFile?.name || "该文件"} 未入账,等待总账或模板维护人员处理`);
|
||||
showToast("解析异常未入账", `${state.selectedFile?.name || "该文件"} 不会进入匹配与计算,请核对模板后重新导出`);
|
||||
}
|
||||
|
||||
function renderBatchRow(batch) {
|
||||
const row = document.createElement("tr");
|
||||
const idCell = document.createElement("td");
|
||||
const id = document.createElement("strong");
|
||||
id.textContent = `IMP-${String(batch.id).padStart(6, "0")}`;
|
||||
const file = document.createElement("small");
|
||||
file.textContent = batch.original_filename || "";
|
||||
idCell.append(id, file);
|
||||
|
||||
const bank = document.createElement("td");
|
||||
bank.textContent = batch.bank_name || "—";
|
||||
|
||||
const period = document.createElement("td");
|
||||
period.textContent = batch.period_start && batch.period_end
|
||||
? `${batch.period_start}—${batch.period_end}`
|
||||
: "—";
|
||||
|
||||
const count = document.createElement("td");
|
||||
count.className = "number";
|
||||
count.textContent = `${batch.confirmed_transactions ?? 0} 笔`;
|
||||
|
||||
const coverage = document.createElement("td");
|
||||
const coverageStatus = batch.status === "exception"
|
||||
? { className: "danger", label: "未导入" }
|
||||
: batch.pending_sheets > 0
|
||||
? { className: "warning", label: `${batch.pending_sheets} 个待确认` }
|
||||
: batch.confirmed_sheets > 0
|
||||
? { className: "success", label: "已确认" }
|
||||
: { className: "neutral", label: "待处理" };
|
||||
coverage.innerHTML = `<span class="status ${coverageStatus.className}">${coverageStatus.label}</span>`;
|
||||
|
||||
const parseState = document.createElement("td");
|
||||
const statusParts = [];
|
||||
if (batch.exception_sheets > 0) statusParts.push(`${batch.exception_sheets} 个异常`);
|
||||
if (batch.ignored_sheets > 0) statusParts.push(`${batch.ignored_sheets} 个忽略`);
|
||||
parseState.textContent = statusParts.length ? statusParts.join("、") : "解析成功";
|
||||
|
||||
const time = document.createElement("td");
|
||||
time.textContent = String(batch.created_at || "").slice(0, 16).replace("T", " ");
|
||||
|
||||
row.append(idCell, bank, period, count, coverage, parseState, time);
|
||||
return row;
|
||||
}
|
||||
|
||||
async function loadImportBatches() {
|
||||
const tbody = $("#importRows");
|
||||
if (!tbody) return;
|
||||
const response = await fetch("/api/batches").catch(() => null);
|
||||
if (response?.status === 401 || response?.status === 403) {
|
||||
window.location.href = "index.html";
|
||||
return;
|
||||
}
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
const batches = Array.isArray(result?.batches) ? result.batches : [];
|
||||
if (batches.length) tbody.replaceChildren(...batches.map(renderBatchRow));
|
||||
}
|
||||
|
||||
function initCompany() {
|
||||
renderCompanyManualRecords();
|
||||
loadCompanyAccounts();
|
||||
loadImportBatches();
|
||||
const uploadDialog = $("#uploadDialog");
|
||||
$$('[data-open-upload]').forEach((button) => button.addEventListener("click", () => uploadDialog.showModal()));
|
||||
$$('[data-close-upload]').forEach((button) => button.addEventListener("click", () => uploadDialog.close()));
|
||||
@@ -1260,9 +1486,19 @@ function initCompany() {
|
||||
}
|
||||
$("#uploadForm")?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
if ($("#parseButton").dataset.stage === "confirm") {
|
||||
if (["parsed", "duplicate"].includes(state.parseResult?.status)) confirmImport();
|
||||
else submitImportException();
|
||||
const stage = $("#parseButton").dataset.stage;
|
||||
if (stage === "confirm" || stage === "done") {
|
||||
const result = state.parseResult;
|
||||
const parsed = result && ["parsed", "duplicate"].includes(result.status);
|
||||
if (parsed && stage === "confirm") {
|
||||
await confirmImport();
|
||||
} else if (parsed) {
|
||||
$("#uploadDialog").close();
|
||||
showView("upload");
|
||||
await loadImportBatches();
|
||||
} else {
|
||||
submitImportException();
|
||||
}
|
||||
return;
|
||||
}
|
||||
$("#parseButton").disabled = true;
|
||||
|
||||
Reference in New Issue
Block a user