Files
xb/app/app.js

1197 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// DEPRECATED: this file is not served by current web entry.
// Active frontend entry is /opt/xibao-web/app/static/app.js.
const state = {
lastResult: null,
keyFields: ["branch", "amount", "type"],
lastGeneratedImages: [],
activeProgressToken: "",
progressTimer: null,
correctionContext: null,
};
const SKIP_REASON_MAP = {
skip_line_rule: "说明/标题行",
branch_not_found: "未识别网点",
type_not_found: "未识别产品类型或期限",
amount_not_found: "未识别金额",
demand_deposit_not_generate: "活期类信息且未写明确期限,默认不生成",
};
const DUP_REASON_MAP = {
history_duplicate: "历史重复",
input_duplicate: "本次重复",
};
function setMsg(text, isError = false) {
const el = document.getElementById("msg");
el.textContent = text || "";
el.style.color = isError ? "#9f2f2f" : "#5b6f69";
}
function setProgressVisible(visible) {
const wrap = document.getElementById("progress-wrap");
if (!wrap) {
return;
}
wrap.classList.toggle("hidden", !visible);
}
function renderProgress(progress) {
const stageEl = document.getElementById("progress-stage");
const pctEl = document.getElementById("progress-percent");
const detailEl = document.getElementById("progress-detail");
const fillEl = document.getElementById("progress-fill");
if (!stageEl || !pctEl || !detailEl || !fillEl) {
return;
}
const stage = progress?.stage || "处理中";
const percent = Math.max(0, Math.min(100, Number(progress?.percent ?? 0)));
const detail = progress?.detail || "";
const status = progress?.status || "";
const error = progress?.error || "";
stageEl.textContent = stage;
pctEl.textContent = `${percent}%`;
fillEl.style.width = `${percent}%`;
if (status === "error" && error) {
detailEl.textContent = `${detail ? `${detail} - ` : ""}${error}`;
} else {
detailEl.textContent = detail;
}
}
function stopProgressPolling() {
if (state.progressTimer) {
clearInterval(state.progressTimer);
state.progressTimer = null;
}
}
async function fetchProgressOnce(token) {
if (!token) {
return;
}
const res = await fetch(`/api/progress/${encodeURIComponent(token)}`);
let data = {};
try {
data = await res.json();
} catch (e) {
return;
}
if (!data.ok || !data.progress) {
return;
}
renderProgress(data.progress);
if (data.progress.status === "done" || data.progress.status === "error") {
stopProgressPolling();
}
}
function startProgressPolling(token) {
stopProgressPolling();
state.activeProgressToken = token || "";
if (!state.activeProgressToken) {
return;
}
state.progressTimer = setInterval(() => {
void fetchProgressOnce(state.activeProgressToken);
}, 700);
}
function getRawText() {
return document.getElementById("raw-text").value.trim();
}
function getTemplateFile() {
return document.getElementById("template-file").value.trim();
}
function getOutputDir() {
return document.getElementById("output-dir").value.trim();
}
function translateSkipReason(reason) {
if (!reason) {
return "未知原因";
}
if (reason.startsWith("branch_not_allowed:")) {
const branch = reason.split(":", 2)[1] || "";
return `网点不在白名单: ${branch}`;
}
return SKIP_REASON_MAP[reason] || reason;
}
function translateDupReason(reason) {
return DUP_REASON_MAP[reason] || reason || "未知原因";
}
function normalizeSkipLineText(line) {
return String(line || "").replace(/\s+/g, "").trim();
}
function isSameSkippedItem(a, b) {
const lineA = normalizeSkipLineText(a?.line || "");
const lineB = normalizeSkipLineText(b?.line || "");
const reasonA = String(a?.reason || "").trim();
const reasonB = String(b?.reason || "").trim();
return lineA && lineA === lineB && reasonA === reasonB;
}
function toInlineUrl(url) {
if (!url) {
return "";
}
return url.includes("?") ? `${url}&inline=1` : `${url}?inline=1`;
}
async function postJson(url, body) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body || {}),
});
let data = {};
try {
data = await res.json();
} catch (e) {
throw new Error(`接口返回非JSON: ${url}`);
}
return { status: res.status, data };
}
function normalizeAmountInput(raw) {
const s = String(raw || "").trim();
if (!s) {
return "";
}
const m = s.match(/\d+(?:\.\d+)?/);
if (!m) {
return s;
}
const n = Math.max(0, Math.floor(Number(m[0]) || 0));
return `${n}`;
}
function toEditableAmount(raw) {
const s = String(raw || "").trim();
if (!s) {
return "";
}
return s.replace(/万元/g, "").replace(/万/g, "");
}
function closeCorrectionModal() {
const modal = document.getElementById("correction-modal");
modal.classList.add("hidden");
state.correctionContext = null;
}
function openCorrectionModal(record) {
const modal = document.getElementById("correction-modal");
const branchEl = document.getElementById("corr-branch");
const amountEl = document.getElementById("corr-amount");
const typeEl = document.getElementById("corr-type");
const pageEl = document.getElementById("corr-page");
const statusEl = document.getElementById("corr-status");
const noteEl = document.getElementById("corr-note");
const rememberEl = document.getElementById("corr-remember");
const keywordEl = document.getElementById("corr-keyword");
const keywordWrap = document.getElementById("corr-keyword-wrap");
state.correctionContext = {
record: { ...(record || {}) },
};
branchEl.value = record?.branch || "";
amountEl.value = toEditableAmount(record?.amount || "");
typeEl.value = record?.type || "";
pageEl.value = record?.page || "";
statusEl.value = record?.status || "";
noteEl.value = "";
rememberEl.checked = false;
keywordEl.value = "";
keywordWrap.classList.add("hidden");
modal.classList.remove("hidden");
}
function buildCorrectionOverrides() {
const ctx = state.correctionContext || {};
const base = ctx.record || {};
const branch = document.getElementById("corr-branch").value.trim();
const amount = normalizeAmountInput(document.getElementById("corr-amount").value);
const type = document.getElementById("corr-type").value.trim();
const page = document.getElementById("corr-page").value.trim();
const status = document.getElementById("corr-status").value.trim();
const out = {};
if (branch && branch !== (base.branch || "")) {
out.branch = branch;
}
if (amount && amount !== (base.amount || "")) {
out.amount = amount;
}
if (type && type !== (base.type || "")) {
out.type = type;
}
if (page && page !== (base.page || "")) {
out.page = page;
}
if (status && status !== (base.status || "")) {
out.status = status;
}
return out;
}
async function applyCorrection() {
const ctx = state.correctionContext || {};
const record = ctx.record;
if (!record) {
setMsg("缺少待修正记录", true);
return;
}
const overrides = buildCorrectionOverrides();
const rememberRule = Boolean(document.getElementById("corr-remember").checked);
const ruleKeyword = document.getElementById("corr-keyword").value.trim();
const note = document.getElementById("corr-note").value.trim();
setLoading(true);
setMsg("修正生成中...");
try {
const { data } = await postJson("/api/correction/apply", {
record,
overrides,
remember_rule: rememberRule,
rule_keyword: ruleKeyword || undefined,
note,
template_file: getTemplateFile() || undefined,
output_dir: getOutputDir() || undefined,
});
if (!data.ok) {
throw new Error(data.error || "修正失败");
}
const images = Array.isArray(data.download_images) ? data.download_images : [];
if (images.length > 0) {
state.lastGeneratedImages = [...images, ...(state.lastGeneratedImages || [])];
renderPreview(state.lastGeneratedImages);
updateDownloadButtonState(false);
}
closeCorrectionModal();
await loadHistoryView();
await loadConfig();
if (data.rule) {
setMsg("修正成功并已记住规则。");
} else {
setMsg("修正成功,已生成新图片。");
}
} finally {
setLoading(false);
}
}
function appendTextCell(tr, text) {
const td = document.createElement("td");
td.textContent = text ?? "";
tr.appendChild(td);
return td;
}
function appendEmptyRow(tbody, colSpan) {
const tr = document.createElement("tr");
const td = document.createElement("td");
td.colSpan = colSpan;
td.textContent = "暂无数据";
td.className = "muted";
tr.appendChild(td);
tbody.appendChild(tr);
}
function makeMiniButton(label, className, onClick) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = `mini-btn ${className || ""}`.trim();
btn.textContent = label;
btn.addEventListener("click", onClick);
return btn;
}
async function markIssue(markType, sourceLine, record = {}, note = "") {
const line = (sourceLine || "").trim();
if (!line) {
setMsg("缺少原始行,无法标记日志", true);
return;
}
const typeText = markType === "generation_error" ? "生成错误" : "识别错误";
try {
const { data } = await postJson("/api/log/mark", {
mark_type: markType,
source_line: line,
record,
note,
});
if (!data.ok) {
throw new Error(data.error || "标记失败");
}
setMsg(`已标记${typeText}${line}`);
} catch (err) {
setMsg(err.message || String(err), true);
}
}
async function suppressSkippedItem(item) {
const line = String(item?.line || "").trim();
if (!line) {
setMsg("跳过项缺少原始行,无法屏蔽", true);
return;
}
try {
const { data } = await postJson("/api/skipped/suppress", {
line,
reason: item?.reason || "",
});
if (!data.ok) {
throw new Error(data.error || "屏蔽失败");
}
if (state.lastResult && Array.isArray(state.lastResult.skipped)) {
const remain = state.lastResult.skipped.filter((x) => !isSameSkippedItem(x, item));
state.lastResult.skipped = remain;
if (state.lastResult.summary && typeof state.lastResult.summary === "object") {
state.lastResult.summary.skipped = remain.length;
renderSummary(state.lastResult.summary);
}
renderSkipped(remain);
document.getElementById("skip-panel").open = remain.length > 0;
}
setMsg("已屏蔽该跳过项,清空历史后会恢复。");
} catch (err) {
setMsg(err.message || String(err), true);
}
}
function renderSummary(summary) {
document.getElementById("m-input").textContent = summary?.input_lines ?? 0;
document.getElementById("m-parsed").textContent = summary?.parsed ?? 0;
document.getElementById("m-new").textContent = summary?.new ?? 0;
document.getElementById("m-dup").textContent = summary?.duplicate ?? 0;
document.getElementById("m-skip").textContent = summary?.skipped ?? 0;
}
function renderNewRecords(records) {
const tbody = document.getElementById("new-body");
tbody.innerHTML = "";
if (!records || records.length === 0) {
appendEmptyRow(tbody, 7);
return;
}
records.forEach((row) => {
const tr = document.createElement("tr");
appendTextCell(tr, row.branch);
appendTextCell(tr, row.amount);
appendTextCell(tr, row.type);
appendTextCell(tr, row.page);
appendTextCell(tr, row.status);
appendTextCell(tr, row.output_file);
const actionTd = document.createElement("td");
actionTd.className = "cell-actions";
const actions = document.createElement("div");
actions.className = "actions-inline";
const sourceLine = row.source_line || row.raw_text || "";
const baseRecord = {
branch: row.branch,
amount: row.amount,
type: row.type,
page: row.page,
status: row.status,
source_line: sourceLine,
raw_text: row.raw_text || "",
output_file: row.output_file || "",
};
actions.appendChild(
makeMiniButton("修正", "", () => {
openCorrectionModal(baseRecord);
})
);
actions.appendChild(
makeMiniButton("识别错", "secondary", () => {
void markIssue("recognition_error", sourceLine, baseRecord);
})
);
actions.appendChild(
makeMiniButton("生成错", "danger", () => {
void markIssue("generation_error", sourceLine, baseRecord);
})
);
actionTd.appendChild(actions);
tr.appendChild(actionTd);
tbody.appendChild(tr);
});
}
function triggerDownload(url) {
const a = document.createElement("a");
a.href = url;
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}
function isMobileClient() {
const ua = navigator.userAgent || "";
return /Android|iPhone|iPad|iPod|Mobile|HarmonyOS/i.test(ua);
}
function getSingleImageActionLabel() {
return isMobileClient() ? "下载" : "复制";
}
async function copyImageToClipboard(downloadUrl) {
if (!downloadUrl) {
throw new Error("缺少图片地址");
}
if (!navigator?.clipboard?.write || typeof window.ClipboardItem === "undefined") {
throw new Error("当前浏览器不支持图片复制");
}
const res = await fetch(toInlineUrl(downloadUrl), { cache: "no-store" });
if (!res.ok) {
throw new Error("获取图片失败");
}
const blob = await res.blob();
const contentType = blob.type || "image/png";
const item = new window.ClipboardItem({ [contentType]: blob });
await navigator.clipboard.write([item]);
}
async function handleSingleImageAction(downloadUrl) {
if (isMobileClient()) {
triggerDownload(downloadUrl);
return;
}
try {
await copyImageToClipboard(downloadUrl);
setMsg("复制成功");
} catch (err) {
setMsg(`复制失败:${err?.message || "请检查浏览器权限"}`, true);
}
}
function renderDuplicateRecords(records) {
const tbody = document.getElementById("dup-body");
tbody.innerHTML = "";
if (!records || records.length === 0) {
appendEmptyRow(tbody, 6);
return;
}
records.forEach((row) => {
const tr = document.createElement("tr");
appendTextCell(tr, row.branch);
appendTextCell(tr, row.amount);
appendTextCell(tr, row.type);
appendTextCell(tr, row.status);
appendTextCell(tr, translateDupReason(row.duplicate_reason));
const actionTd = document.createElement("td");
actionTd.className = "cell-actions";
const actions = document.createElement("div");
actions.className = "actions-inline";
if (row.download_url) {
actions.appendChild(
makeMiniButton("预览", "secondary", () => {
window.open(toInlineUrl(row.download_url), "_blank", "noopener,noreferrer");
})
);
actions.appendChild(
makeMiniButton(getSingleImageActionLabel(), "secondary", () => {
void handleSingleImageAction(row.download_url);
})
);
} else {
const muted = document.createElement("span");
muted.className = "muted";
muted.textContent = "暂无图片";
actions.appendChild(muted);
}
const sourceLine = row.source_line || row.raw_text || "";
const baseRecord = {
branch: row.branch,
amount: row.amount,
type: row.type,
page: row.page,
status: row.status,
source_line: sourceLine,
raw_text: row.raw_text || "",
output_file: row.output_file || "",
duplicate_reason: row.duplicate_reason,
};
actions.appendChild(
makeMiniButton("修正", "", () => {
openCorrectionModal(baseRecord);
})
);
actions.appendChild(
makeMiniButton("识别错", "secondary", () => {
void markIssue("recognition_error", sourceLine, baseRecord);
})
);
actions.appendChild(
makeMiniButton("生成错", "danger", () => {
void markIssue("generation_error", sourceLine, baseRecord);
})
);
actionTd.appendChild(actions);
tr.appendChild(actionTd);
tbody.appendChild(tr);
});
}
function renderHistoryRecords(items, totalCount = 0) {
const tbody = document.getElementById("history-body");
const note = document.getElementById("history-note");
if (!tbody || !note) {
return;
}
tbody.innerHTML = "";
const rows = Array.isArray(items) ? items : [];
note.textContent = `总计 ${totalCount} 条,当前显示 ${rows.length}`;
if (rows.length === 0) {
appendEmptyRow(tbody, 7);
return;
}
rows.forEach((row) => {
const tr = document.createElement("tr");
appendTextCell(tr, row.created_at || "");
appendTextCell(tr, row.branch || "");
appendTextCell(tr, row.amount || "");
appendTextCell(tr, row.type || "");
appendTextCell(tr, row.status || "");
appendTextCell(tr, row.source_line || row.raw_text || "");
const actionTd = document.createElement("td");
actionTd.className = "cell-actions";
const actions = document.createElement("div");
actions.className = "actions-inline";
if (row.download_url) {
actions.appendChild(
makeMiniButton("预览", "secondary", () => {
window.open(toInlineUrl(row.download_url), "_blank", "noopener,noreferrer");
})
);
actions.appendChild(
makeMiniButton(getSingleImageActionLabel(), "secondary", () => {
void handleSingleImageAction(row.download_url);
})
);
} else {
const muted = document.createElement("span");
muted.className = "muted";
muted.textContent = "图片已清理";
actions.appendChild(muted);
}
const sourceLine = row.source_line || row.raw_text || "";
const baseRecord = {
branch: row.branch,
amount: row.amount,
type: row.type,
page: row.page,
status: row.status,
source_line: sourceLine,
raw_text: row.raw_text || "",
output_file: row.output_file || "",
created_at: row.created_at,
};
actions.appendChild(
makeMiniButton("修正", "", () => {
openCorrectionModal(baseRecord);
})
);
actions.appendChild(
makeMiniButton("识别错", "secondary", () => {
void markIssue("recognition_error", sourceLine, baseRecord);
})
);
actions.appendChild(
makeMiniButton("生成错", "danger", () => {
void markIssue("generation_error", sourceLine, baseRecord);
})
);
actionTd.appendChild(actions);
tr.appendChild(actionTd);
tbody.appendChild(tr);
});
}
async function loadHistoryView(showMsg = false) {
const res = await fetch("/api/history/view?limit=500");
let data = {};
try {
data = await res.json();
} catch (e) {
throw new Error("历史接口返回异常");
}
if (!data.ok) {
throw new Error(data.error || "加载历史失败");
}
renderHistoryRecords(data.items || [], data.count || 0);
if (showMsg) {
setMsg(`历史已刷新:共 ${data.count || 0} 条。`);
}
}
function renderSkipped(skipped) {
const ul = document.getElementById("skip-list");
ul.innerHTML = "";
if (!skipped || skipped.length === 0) {
const li = document.createElement("li");
li.textContent = "无跳过项";
li.className = "muted";
ul.appendChild(li);
return;
}
skipped.forEach((item) => {
const li = document.createElement("li");
const main = document.createElement("span");
const reason = document.createElement("strong");
reason.textContent = translateSkipReason(item.reason);
main.appendChild(reason);
main.appendChild(document.createTextNode(`${item.line || ""}`));
const actions = document.createElement("span");
actions.className = "skip-actions";
actions.appendChild(
makeMiniButton("屏蔽", "danger", () => {
void suppressSkippedItem(item);
})
);
actions.appendChild(
makeMiniButton("标记识别错", "secondary", () => {
void markIssue("recognition_error", item.line || "", {
reason: item.reason,
stage: "skipped",
});
})
);
li.appendChild(main);
li.appendChild(actions);
ul.appendChild(li);
});
}
function renderPreview(items) {
const grid = document.getElementById("preview-grid");
const note = document.getElementById("preview-note");
grid.innerHTML = "";
if (!items || items.length === 0) {
note.textContent = "生成后显示";
const empty = document.createElement("div");
empty.className = "preview-empty muted";
empty.textContent = "暂无预览图";
grid.appendChild(empty);
return;
}
note.textContent = `${items.length}`;
items.forEach((item) => {
const card = document.createElement("article");
card.className = "preview-item";
const img = document.createElement("img");
img.className = "preview-image";
img.loading = "lazy";
img.alt = item.name || "预览图";
img.src = toInlineUrl(item.download_url);
img.addEventListener("click", () => {
window.open(toInlineUrl(item.download_url), "_blank", "noopener,noreferrer");
});
const bar = document.createElement("div");
bar.className = "preview-bar";
const name = document.createElement("span");
name.className = "preview-name";
name.textContent = item.name || "未命名图片";
const btn = document.createElement("button");
btn.className = "preview-btn secondary";
btn.type = "button";
btn.textContent = getSingleImageActionLabel();
btn.addEventListener("click", () => {
void handleSingleImageAction(item.download_url);
});
bar.appendChild(name);
bar.appendChild(btn);
card.appendChild(img);
card.appendChild(bar);
grid.appendChild(card);
});
}
function renderResult(result) {
state.lastResult = result;
state.keyFields = result?.dedup_key_fields || ["branch", "amount", "type"];
renderSummary(result.summary || {});
renderNewRecords(result.new_records || []);
renderDuplicateRecords(result.duplicate_records || []);
renderSkipped(result.skipped || []);
document.getElementById("dup-panel").open = (result.duplicate_records || []).length > 0;
document.getElementById("skip-panel").open = (result.skipped || []).length > 0;
}
function updateDownloadButtonState(loading = false) {
const btn = document.getElementById("download-btn");
if (!btn) {
return;
}
const hasImages = Array.isArray(state.lastGeneratedImages) && state.lastGeneratedImages.length > 0;
btn.disabled = loading || !hasImages;
}
function setLoading(loading) {
document.getElementById("generate-btn").disabled = loading;
document.getElementById("parse-btn").disabled = loading;
document.getElementById("force-clear-btn").disabled = loading;
document.getElementById("history-refresh-btn").disabled = loading;
document.getElementById("clear-btn").disabled = loading;
const corrSubmit = document.getElementById("corr-submit");
const corrCancel = document.getElementById("corr-cancel");
if (corrSubmit) {
corrSubmit.disabled = loading;
}
if (corrCancel) {
corrCancel.disabled = loading;
}
updateDownloadButtonState(loading);
}
async function triggerMultiDownloads(items) {
if (!Array.isArray(items)) {
return { total: 0, started: 0, blocked: 0, mobile: false };
}
const validItems = items.filter((x) => x && x.download_url);
if (validItems.length === 0) {
return { total: 0, started: 0, blocked: 0, mobile: isMobileClient() };
}
if (isMobileClient()) {
let started = 0;
let blocked = 0;
validItems.forEach((it) => {
const win = window.open(it.download_url, "_blank", "noopener,noreferrer");
if (win) {
started += 1;
} else {
blocked += 1;
}
});
return { total: validItems.length, started, blocked, mobile: true };
}
let started = 0;
for (let i = 0; i < validItems.length; i += 1) {
triggerDownload(validItems[i].download_url);
started += 1;
await new Promise((resolve) => setTimeout(resolve, 180));
}
return { total: validItems.length, started, blocked: 0, mobile: false };
}
function askInsuranceYear() {
return new Promise((resolve) => {
const modal = document.getElementById("insurance-modal");
const btn3 = document.getElementById("insurance-3");
const btn5 = document.getElementById("insurance-5");
const btnCancel = document.getElementById("insurance-cancel");
const cleanup = () => {
modal.classList.add("hidden");
btn3.onclick = null;
btn5.onclick = null;
btnCancel.onclick = null;
};
btn3.onclick = () => {
cleanup();
resolve("3");
};
btn5.onclick = () => {
cleanup();
resolve("5");
};
btnCancel.onclick = () => {
cleanup();
resolve(null);
};
modal.classList.remove("hidden");
});
}
async function loadConfig() {
const res = await fetch("/api/config");
const data = await res.json();
if (!data.ok) {
throw new Error(data.error || "加载配置失败");
}
const c = data.config;
const div = document.getElementById("config-summary");
div.innerHTML = [
`<div>模板文件:${c.template_file || "(未配置)"}</div>`,
`<div>输出目录:${c.output_dir || "(未配置)"}</div>`,
`<div>触发关键词:${c.trigger_keyword || "#接龙"}</div>`,
`<div>历史条数:${data.history_count}</div>`,
`<div>今日日志:${data.review_log_count || 0}</div>`,
].join("");
if (!getTemplateFile()) {
document.getElementById("template-file").value = c.template_file || "";
}
if (!getOutputDir()) {
document.getElementById("output-dir").value = c.output_dir || "";
}
}
async function parseOnly(insuranceYear = null) {
const rawText = getRawText();
if (!rawText) {
setMsg("请先输入接龙文本", true);
return;
}
setMsg("解析中...");
setLoading(true);
try {
const { data } = await postJson("/api/parse", {
raw_text: rawText,
insurance_year: insuranceYear,
});
if (!data.ok) {
throw new Error(data.error || "解析失败");
}
renderResult(data.result);
if (data.result.needs_insurance_choice) {
setMsg("检测到保险未写年限生成时会要求选择3年交或5年交。", false);
} else {
setMsg(
`完成:有效 ${data.result.summary.parsed} 条,新增 ${data.result.summary.new} 条,重复 ${data.result.summary.duplicate} 条。`
);
}
} finally {
setLoading(false);
}
}
async function generateOnly() {
const rawText = getRawText();
if (!rawText) {
setMsg("请先输入接龙文本", true);
return;
}
const templateFile = getTemplateFile();
const outputDir = getOutputDir();
const progressToken = `${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
setMsg("生成中...");
setProgressVisible(true);
renderProgress({
stage: "提交任务",
percent: 1,
detail: "请求已发出",
status: "running",
});
startProgressPolling(progressToken);
void fetchProgressOnce(progressToken);
setLoading(true);
let insuranceYear = null;
try {
for (let i = 0; i < 2; i += 1) {
const { status, data } = await postJson("/api/generate", {
raw_text: rawText,
insurance_year: insuranceYear,
progress_token: progressToken,
template_file: templateFile || undefined,
output_dir: outputDir || undefined,
save_history: true,
});
if (data.progress_token) {
state.activeProgressToken = data.progress_token;
}
await fetchProgressOnce(state.activeProgressToken || progressToken);
if (data.ok) {
if (data.result) {
renderResult(data.result);
}
if (data.generated_count > 0) {
const downloadImages = data.download_images || [];
state.lastGeneratedImages = downloadImages;
renderPreview(downloadImages);
updateDownloadButtonState(false);
setMsg(`生成完成:${data.generated_count} 张,可点击“下载全部”按钮下载。`);
} else {
state.lastGeneratedImages = [];
renderPreview([]);
updateDownloadButtonState(false);
setMsg(data.message || "没有可生成的新记录");
}
renderProgress({
stage: "完成",
percent: 100,
detail: `生成结束,产出 ${data.generated_count || 0}`,
status: "done",
});
stopProgressPolling();
await loadConfig();
await loadHistoryView();
return;
}
if (status === 400 && data.error_code === "insurance_year_required") {
renderProgress({
stage: "等待选择",
percent: 15,
detail: "保险年限待选择3年交/5年交",
status: "need_input",
});
if (data.result) {
renderResult(data.result);
}
const selected = await askInsuranceYear();
if (!selected) {
setMsg("你已取消保险年限选择,本次未生成。", true);
return;
}
insuranceYear = selected;
continue;
}
throw new Error(data.error || data.message || "生成失败");
}
throw new Error("保险年限选择后仍未生成,请重试");
} catch (err) {
renderProgress({
stage: "失败",
percent: 100,
detail: err?.message || String(err),
status: "error",
error: err?.message || String(err),
});
stopProgressPolling();
throw err;
} finally {
setLoading(false);
}
}
async function downloadAllGenerated() {
const items = state.lastGeneratedImages || [];
if (!Array.isArray(items) || items.length === 0) {
setMsg("暂无可下载图片,请先生成。", true);
return;
}
setMsg(`开始下载:${items.length} 张...`);
setLoading(true);
try {
const stat = await triggerMultiDownloads(items);
if (stat.mobile) {
if (stat.blocked > 0) {
setMsg(
`移动端已触发 ${stat.started}/${stat.total} 张下载。若未全部下载,请允许弹窗后重试,或在预览区逐张下载。`,
true
);
} else {
setMsg(`移动端已触发 ${stat.started} 张下载。`);
}
} else {
setMsg(`下载完成:${stat.started} 张。`);
}
} finally {
setLoading(false);
}
}
async function clearHistory() {
const ok = window.confirm("确认清空历史记录?");
if (!ok) {
return;
}
setLoading(true);
setMsg("清空中...");
try {
const { data } = await postJson("/api/history/clear", {});
if (!data.ok) {
throw new Error(data.error || "清空失败");
}
setMsg("历史已清空");
await loadConfig();
await loadHistoryView();
state.lastGeneratedImages = [];
renderPreview([]);
updateDownloadButtonState(false);
} finally {
setLoading(false);
}
}
async function forceClearOutput() {
const ok = window.confirm("确认强制清理已生成截图与任务文件?这不会清空历史记录。");
if (!ok) {
return;
}
setLoading(true);
setMsg("清理截图中...");
try {
const { data } = await postJson("/api/output/clear", {});
if (!data.ok) {
throw new Error(data.error || "清理失败");
}
state.lastGeneratedImages = [];
renderPreview([]);
updateDownloadButtonState(false);
await loadHistoryView();
setProgressVisible(false);
stopProgressPolling();
setMsg(
`清理完成:删除任务目录 ${data.removed_dirs || 0} 个,删除文件 ${data.removed_files || 0} 个。`
);
} finally {
setLoading(false);
}
}
function bindEvents() {
document.getElementById("parse-btn").addEventListener("click", async () => {
try {
await parseOnly();
} catch (err) {
setMsg(err.message || String(err), true);
}
});
document.getElementById("generate-btn").addEventListener("click", async () => {
try {
await generateOnly();
} catch (err) {
setMsg(err.message || String(err), true);
}
});
document.getElementById("download-btn").addEventListener("click", async () => {
try {
await downloadAllGenerated();
} catch (err) {
setMsg(err.message || String(err), true);
}
});
document.getElementById("force-clear-btn").addEventListener("click", async () => {
try {
await forceClearOutput();
} catch (err) {
setMsg(err.message || String(err), true);
}
});
document.getElementById("history-refresh-btn").addEventListener("click", async () => {
try {
await loadHistoryView(true);
} catch (err) {
setMsg(err.message || String(err), true);
}
});
document.getElementById("clear-btn").addEventListener("click", async () => {
try {
await clearHistory();
} catch (err) {
setMsg(err.message || String(err), true);
}
});
const corrModal = document.getElementById("correction-modal");
const corrRemember = document.getElementById("corr-remember");
const corrKeywordWrap = document.getElementById("corr-keyword-wrap");
document.getElementById("corr-cancel").addEventListener("click", () => {
closeCorrectionModal();
});
document.getElementById("corr-submit").addEventListener("click", async () => {
try {
await applyCorrection();
} catch (err) {
setMsg(err.message || String(err), true);
}
});
corrRemember.addEventListener("change", () => {
corrKeywordWrap.classList.toggle("hidden", !corrRemember.checked);
});
corrModal.addEventListener("click", (ev) => {
if (ev.target === corrModal) {
closeCorrectionModal();
}
});
}
async function init() {
bindEvents();
state.lastGeneratedImages = [];
renderPreview([]);
updateDownloadButtonState(false);
try {
await loadConfig();
await loadHistoryView();
setMsg("可直接粘贴接龙并生成。", false);
} catch (err) {
setMsg(err.message || String(err), true);
}
}
init();