const runtimeFieldIds = [ "business_start", "business_end", "turnaround_base", "golden_start", "golden_end", "efficiency_enabled", "efficiency_penalty_coef", "eff_daily_delta_cap", "rule1_enabled", "rule1_gap", "rule2_enabled", "rule2_threshold", "rule2_window_minutes", "rule2_penalty", "rule2_exempt_ranges", "rule3_enabled", "rule3_gap_minutes", "rule3_penalty", "rule4_enabled", "rule4_earliest", "rule4_latest", "rule9_enabled", "rule9_hot_top_n", "rule9_min_ratio", "rule9_penalty", "rule11_enabled", "rule11_after_time", "rule11_penalty", "rule12_enabled", "rule12_penalty_each", "rule13_enabled", "rule13_forbidden_halls", "tms_allowance", "iterations", "random_seed" ]; const checkIds = [ "efficiency_enabled", "rule1_enabled", "rule2_enabled", "rule3_enabled", "rule4_enabled", "rule9_enabled", "rule11_enabled", "rule12_enabled", "rule13_enabled" ]; const tuningColumns = [ "选中", "影片", "今日场次", "今日黄金场次", "今日全天效率", "今日黄金效率", "最少场次", "最多场次", "固定场次", "最少黄金场次", "最多黄金场次", "最低场次占比", "最高场次占比" ]; const readOnlyColumns = new Set(["影片", "今日场次", "今日黄金场次", "今日全天效率", "今日黄金效率"]); const boolColumns = new Set(["选中"]); const integerColumns = new Set(["最少场次", "最多场次", "固定场次", "最少黄金场次", "最多黄金场次"]); const floatColumns = new Set(["最低场次占比", "最高场次占比"]); const sessionConstraintColumns = ["最少场次", "最多场次", "固定场次", "最少黄金场次", "最多黄金场次"]; const ratioConstraintColumns = ["最低场次占比", "最高场次占比"]; const state = { runtimeCfg: null, bundle: null, tuningRows: [], jobState: null, results: null, activeCandidateIndex: 0, pollTimer: null, }; function deepClone(v) { if (typeof structuredClone === "function") { return structuredClone(v); } return JSON.parse(JSON.stringify(v)); } function $(id) { return document.getElementById(id); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function requestJSON(url, options = {}) { const init = { headers: { "Content-Type": "application/json" }, credentials: "same-origin", ...options, }; const resp = await fetch(url, init); let data = {}; try { data = await resp.json(); } catch { data = {}; } if (!resp.ok || data.success === false) { const msg = data.error || data.message || `请求失败: ${resp.status}`; throw new Error(msg); } return data; } function showMessage(text, type = "info", ttl = 2800) { const el = $("message"); el.className = `message ${type}`; el.textContent = text; el.classList.remove("hidden"); if (ttl > 0) { setTimeout(() => { el.classList.add("hidden"); }, ttl); } } function getDatePlusOne(dateText) { const d = new Date(`${dateText}T00:00:00`); if (Number.isNaN(d.getTime())) { return "-"; } d.setDate(d.getDate() + 1); return d.toISOString().slice(0, 10); } function updateTargetDateTip() { const base = $("base_date").value; $("target_date_tip").textContent = getDatePlusOne(base); } function toNumberOrNull(raw, parseFloatMode = false) { const val = String(raw ?? "").trim(); if (val === "") { return null; } const num = parseFloatMode ? Number.parseFloat(val) : Number.parseInt(val, 10); return Number.isFinite(num) ? num : null; } function collectMaintenanceBlocks() { const tbody = $("maintenance_table").querySelector("tbody"); const rows = Array.from(tbody.querySelectorAll("tr")); return rows .map((tr) => { const hall = tr.querySelector("input[data-key='hall']")?.value?.trim() || ""; const start = tr.querySelector("input[data-key='start']")?.value?.trim() || ""; const end = tr.querySelector("input[data-key='end']")?.value?.trim() || ""; return { hall, start, end }; }) .filter((x) => x.hall && x.start && x.end); } function collectRuntimeCfg() { const cfg = {}; for (const id of runtimeFieldIds) { const el = $(id); if (!el) continue; if (checkIds.includes(id)) { cfg[id] = !!el.checked; } else if (el.type === "number") { cfg[id] = el.step && String(el.step).includes(".") ? Number.parseFloat(el.value || "0") : Number.parseInt(el.value || "0", 10); } else { cfg[id] = el.value; } } cfg.maintenance_blocks = collectMaintenanceBlocks(); return cfg; } function setMaintenanceRows(rows = []) { const tbody = $("maintenance_table").querySelector("tbody"); tbody.innerHTML = ""; const list = rows.length ? rows : []; for (const row of list) { appendMaintenanceRow(row); } } function appendMaintenanceRow(row = { hall: "", start: "", end: "" }) { const tbody = $("maintenance_table").querySelector("tbody"); const tr = document.createElement("tr"); tr.innerHTML = ` `; tr.querySelector("button").addEventListener("click", () => tr.remove()); tbody.appendChild(tr); } function setRuntimeCfg(cfg) { state.runtimeCfg = deepClone(cfg); for (const id of runtimeFieldIds) { const el = $(id); if (!el) continue; const value = cfg[id]; if (checkIds.includes(id)) { el.checked = !!value; } else { el.value = value ?? ""; } } setMaintenanceRows(cfg.maintenance_blocks || []); refreshEnableStates(); } function refreshEnableStates() { $("efficiency_penalty_coef").disabled = !$("efficiency_enabled").checked; $("eff_daily_delta_cap").disabled = !$("efficiency_enabled").checked; $("rule1_gap").disabled = !$("rule1_enabled").checked; const rule2Enabled = $("rule2_enabled").checked; $("rule2_threshold").disabled = !rule2Enabled; $("rule2_window_minutes").disabled = !rule2Enabled; $("rule2_penalty").disabled = !rule2Enabled; const rule3Enabled = $("rule3_enabled").checked; $("rule3_gap_minutes").disabled = !rule3Enabled; $("rule3_penalty").disabled = !rule3Enabled; const rule4Enabled = $("rule4_enabled").checked; $("rule4_earliest").disabled = !rule4Enabled; $("rule4_latest").disabled = !rule4Enabled; const rule9Enabled = $("rule9_enabled").checked; $("rule9_hot_top_n").disabled = !rule9Enabled; $("rule9_min_ratio").disabled = !rule9Enabled; $("rule9_penalty").disabled = !rule9Enabled; const rule11Enabled = $("rule11_enabled").checked; $("rule11_after_time").disabled = !rule11Enabled; $("rule11_penalty").disabled = !rule11Enabled; $("rule12_penalty_each").disabled = !$("rule12_enabled").checked; $("rule13_forbidden_halls").disabled = !$("rule13_enabled").checked; } function renderSimpleTable(el, rows, columns = null) { el.innerHTML = ""; if (!rows || rows.length === 0) { el.innerHTML = "提示无数据"; return; } const cols = columns || Object.keys(rows[0]); const thead = document.createElement("thead"); const htr = document.createElement("tr"); for (const c of cols) { const th = document.createElement("th"); th.textContent = c; htr.appendChild(th); } thead.appendChild(htr); const tbody = document.createElement("tbody"); for (const row of rows) { const tr = document.createElement("tr"); for (const c of cols) { const td = document.createElement("td"); const v = row[c]; td.textContent = v === null || v === undefined ? "" : String(v); tr.appendChild(td); } tbody.appendChild(tr); } el.appendChild(thead); el.appendChild(tbody); } function renderMapTop(tableId, mapObj, keyTitle, valTitle, limit = 20) { const entries = Object.entries(mapObj || {}).slice(0, limit); const rows = entries.map(([k, v]) => ({ [keyTitle]: k, [valTitle]: v })); renderSimpleTable($(tableId), rows, [keyTitle, valTitle]); } function renderMetrics(container, items) { container.innerHTML = ""; for (const item of items) { const card = document.createElement("div"); card.className = "metric"; card.innerHTML = `
${escapeHtml(item.k)}
${escapeHtml(String(item.v))}
`; container.appendChild(card); } } function getSelectedValues(selectId) { const el = $(selectId); if (!el) return []; return Array.from(el.selectedOptions).map((x) => x.value); } function setAllSelected(selectId, selected = true) { const el = $(selectId); if (!el) return; for (const opt of Array.from(el.options)) { opt.selected = !!selected; } } function renderExcludeEffect(effect) { const tip = $("exclude_effect_tip"); if (!tip) return; const e = effect || {}; const total = Number(e.total_count || 0); const removed = Number(e.removed_count || 0); const remaining = Number(e.remaining_count || Math.max(0, total - removed)); const affectedMovies = Number(e.affected_movies || 0); const reasons = Object.entries(e.reason_breakdown || {}) .map(([k, v]) => `${k}:${v}`) .join(";"); if (!total) { tip.textContent = "尚未应用剔除。"; return; } tip.textContent = `当前剔除效果:已剔除 ${removed}/${total} 场,剩余 ${remaining} 场,影响影片 ${affectedMovies} 部。${reasons ? ` 原因分布:${reasons}` : ""}`; } function renderBundle(bundle) { state.bundle = bundle; state.tuningRows = deepClone(bundle.tuning_rows || []); $("loaded_section").classList.remove("hidden"); $("load_summary").textContent = `目标日期 ${bundle.target_str},影片 ${bundle.movies_count} 部,已售锁定场 ${bundle.locked_count}。`; const exclude = $("exclude_select"); exclude.innerHTML = ""; const selectedSession = new Set(bundle.excluded_session_keys || []); for (const item of bundle.exclude_options || []) { const opt = document.createElement("option"); opt.value = String(item.key ?? ""); opt.textContent = String(item.label ?? ""); opt.selected = selectedSession.has(opt.value); opt.dataset.suspected = item.suspected ? "1" : "0"; exclude.appendChild(opt); } const movieSelect = $("exclude_movie_select"); movieSelect.innerHTML = ""; const selectedMovies = new Set(bundle.excluded_movies || []); for (const item of bundle.exclude_movie_options || []) { const opt = document.createElement("option"); opt.value = String(item.value ?? ""); opt.textContent = String(item.label ?? item.value ?? ""); opt.selected = selectedMovies.has(opt.value); movieSelect.appendChild(opt); } const hallSelect = $("exclude_hall_select"); hallSelect.innerHTML = ""; const selectedHalls = new Set(bundle.excluded_halls || []); for (const item of bundle.exclude_hall_options || []) { const opt = document.createElement("option"); opt.value = String(item.value ?? ""); opt.textContent = String(item.label ?? item.value ?? ""); opt.selected = selectedHalls.has(opt.value); hallSelect.appendChild(opt); } const rules = bundle.exclude_rules || {}; $("exclude_rule_zero_sales").checked = !!rules.zero_sales; $("exclude_rule_early_morning").checked = !!rules.early_morning; renderExcludeEffect(bundle.exclude_effect || {}); renderTuningTable(); } function renderTuningTable() { const table = $("tuning_table"); table.innerHTML = ""; const thead = document.createElement("thead"); const hr = document.createElement("tr"); for (const c of tuningColumns) { const th = document.createElement("th"); th.textContent = c; hr.appendChild(th); } thead.appendChild(hr); const tbody = document.createElement("tbody"); for (let i = 0; i < state.tuningRows.length; i += 1) { const row = state.tuningRows[i] || {}; const tr = document.createElement("tr"); for (const col of tuningColumns) { const td = document.createElement("td"); if (boolColumns.has(col)) { const input = document.createElement("input"); input.type = "checkbox"; input.checked = !!row[col]; input.addEventListener("change", () => { state.tuningRows[i][col] = !!input.checked; }); td.appendChild(input); } else if (readOnlyColumns.has(col)) { td.textContent = row[col] === null || row[col] === undefined ? "" : String(row[col]); } else if (integerColumns.has(col) || floatColumns.has(col)) { const input = document.createElement("input"); input.type = "number"; input.step = floatColumns.has(col) ? "0.1" : "1"; input.value = row[col] === null || row[col] === undefined ? "" : String(row[col]); const update = () => { state.tuningRows[i][col] = toNumberOrNull(input.value, floatColumns.has(col)); }; input.addEventListener("input", update); input.addEventListener("change", update); td.appendChild(input); } else { const input = document.createElement("input"); input.type = "text"; input.value = row[col] === null || row[col] === undefined ? "" : String(row[col]); input.addEventListener("input", () => { state.tuningRows[i][col] = input.value; }); input.addEventListener("change", () => { state.tuningRows[i][col] = input.value; }); td.appendChild(input); } tr.appendChild(td); } tbody.appendChild(tr); } table.appendChild(thead); table.appendChild(tbody); } function collectExclusionPayload() { return { excluded_session_keys: getSelectedValues("exclude_select"), excluded_movies: getSelectedValues("exclude_movie_select"), excluded_halls: getSelectedValues("exclude_hall_select"), exclude_rules: { zero_sales: !!$("exclude_rule_zero_sales").checked, early_morning: !!$("exclude_rule_early_morning").checked, }, }; } function applyTuningBatchMutation(mutator, successMsg) { if (!state.tuningRows || state.tuningRows.length === 0) { showMessage("当前没有可编辑约束行", "warn"); return; } const selectedIndexes = state.tuningRows .map((row, idx) => ({ idx, selected: !!row["选中"] })) .filter((x) => x.selected) .map((x) => x.idx); const targetIndexes = selectedIndexes.length ? selectedIndexes : state.tuningRows.map((_, idx) => idx); targetIndexes.forEach((idx) => mutator(state.tuningRows[idx], idx)); renderTuningTable(); showMessage(`${successMsg}(处理 ${targetIndexes.length} 行)`, "ok", 1800); } function setResultsHidden(hidden) { if (hidden) { $("results_section").classList.add("hidden"); } else { $("results_section").classList.remove("hidden"); } } function renderJobState(jobState) { state.jobState = jobState; if (!state.bundle && jobState.status !== "idle") { $("loaded_section").classList.remove("hidden"); } const metrics = [ { k: "任务状态", v: jobState.status || "idle" }, { k: "当前进度", v: `${((jobState.progress || 0) * 100).toFixed(1)}%` }, { k: "可行方案", v: jobState.feasible_count || 0 }, { k: "硬性淘汰", v: jobState.hard_reject || 0 }, { k: "已运行时长", v: `${(jobState.elapsed_seconds || 0).toFixed(1)}s` }, ]; renderMetrics($("job_metrics"), metrics); const progress = Math.max(0, Math.min(1, Number(jobState.progress || 0))); $("job_progress_bar").style.width = `${progress * 100}%`; if ((jobState.iterations || 0) > 0) { $("job_caption").textContent = `迭代进度:${jobState.iter_done || 0}/${jobState.iterations || 0};构造失败 ${jobState.build_reject || 0},硬规则淘汰 ${jobState.rule_reject || 0};${jobState.message || ""}`; } else { $("job_caption").textContent = jobState.message || ""; } renderMapTop("reject_reason_table", jobState.reject_reason_top || {}, "淘汰原因", "次数", 20); renderMapTop("reject_detail_table", jobState.reject_detail_top || {}, "详细原因", "次数", 20); $("run_btn").disabled = ["running", "paused"].includes(jobState.status); $("pause_btn").disabled = jobState.status !== "running"; $("resume_btn").disabled = jobState.status !== "paused"; $("stop_btn").disabled = !["running", "paused"].includes(jobState.status); } function renderRejectSummary(summary) { setResultsHidden(false); renderMetrics($("results_metrics"), [ { k: "最近一次状态", v: state.jobState?.status || "-" }, { k: "运行耗时", v: `${(summary.elapsed_seconds || 0).toFixed(1)}s` }, { k: "构造阶段失败", v: summary.build_reject || 0 }, { k: "硬性规则淘汰", v: summary.rule_reject || 0 }, ]); renderMapTop("reason_stats_table", summary.reject_reason_stats || {}, "淘汰原因", "次数", 200); renderMapTop("detail_stats_table", summary.reject_detail_stats || {}, "详细原因", "次数", 200); renderSimpleTable($("phase_stats_table"), [], ["淘汰阶段", "次数"]); $("reject_examples").textContent = ""; $("candidate_tabs").innerHTML = ""; $("candidate_content").innerHTML = ""; } function renderResults(data) { state.results = data; setResultsHidden(false); const s = data.summary || {}; renderMetrics($("results_metrics"), [ { k: "目标排片日期", v: data.target_str || "-" }, { k: "可行方案数", v: s.total_feasible || 0 }, { k: "硬性规则淘汰", v: s.hard_reject || 0 }, { k: "已售锁定场", v: s.locked_count || 0 }, { k: "生成耗时", v: `${(s.elapsed_seconds || 0).toFixed(1)}s` }, ]); renderMapTop("phase_stats_table", s.reject_phase_stats || {}, "淘汰阶段", "次数", 20); renderMapTop("reason_stats_table", s.reject_reason_stats || {}, "淘汰原因", "次数", 200); renderMapTop("detail_stats_table", s.reject_detail_stats || {}, "详细原因", "次数", 200); const exampleLines = []; const examples = s.reject_examples || {}; for (const [reason, arr] of Object.entries(examples)) { if (!arr || !arr.length) continue; exampleLines.push(`${reason}:`); for (const x of arr.slice(0, 3)) { exampleLines.push(`- ${x}`); } } $("reject_examples").textContent = exampleLines.join("\n"); $("movie_targets_json").textContent = JSON.stringify(s.movie_targets || {}, null, 2); renderSimpleTable($("today_eff_table"), s.today_eff_rows || []); const tabs = $("candidate_tabs"); tabs.innerHTML = ""; const candidates = data.candidates || []; candidates.forEach((cand, idx) => { const btn = document.createElement("button"); btn.type = "button"; btn.className = `tab-btn${idx === 0 ? " active" : ""}`; btn.textContent = cand.title; btn.addEventListener("click", () => { state.activeCandidateIndex = idx; renderCandidate(); }); tabs.appendChild(btn); }); state.activeCandidateIndex = 0; renderCandidate(); } function renderCandidate() { const payload = state.results; if (!payload || !payload.candidates || payload.candidates.length === 0) { $("candidate_content").innerHTML = ""; return; } const idx = Math.min(Math.max(0, state.activeCandidateIndex || 0), payload.candidates.length - 1); state.activeCandidateIndex = idx; const cand = payload.candidates[idx]; const tabButtons = Array.from($("candidate_tabs").querySelectorAll(".tab-btn")); tabButtons.forEach((btn, i) => { btn.classList.toggle("active", i === idx); }); const content = $("candidate_content"); content.innerHTML = `

${escapeHtml(cand.title)}

${cand.gantt_html || "
无甘特图
"}

评分拆解

结果汇总

排片明细

🔍 场次合理性检查日志

${escapeHtml(cand.log_text || "")}
`; renderSimpleTable($("score_breakdown_table"), cand.score_breakdown || [], ["规则", "分值", "说明"]); renderSimpleTable($("summary_table"), cand.summary_table || []); renderSimpleTable($("schedule_table"), cand.schedule_table || []); } function escapeHtml(text) { return String(text) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } async function saveConfig() { const runtime_cfg = collectRuntimeCfg(); const data = await requestJSON("/api/config/save", { method: "POST", body: JSON.stringify({ runtime_cfg }), }); setRuntimeCfg(data.runtime_cfg); showMessage("参数已保存", "ok"); } async function resetConfig() { const data = await requestJSON("/api/config/reset", { method: "POST", body: "{}" }); setRuntimeCfg(data.runtime_cfg); showMessage("已恢复默认参数", "ok"); } async function loadData() { showMessage("正在加载数据,请稍候...", "info", 1200); const runtime_cfg = collectRuntimeCfg(); const base_date = $("base_date").value; const data = await requestJSON("/api/load-data", { method: "POST", body: JSON.stringify({ base_date, runtime_cfg }), }); setRuntimeCfg(data.runtime_cfg); renderBundle(data.bundle); renderJobState(await fetchJobStateRaw()); setResultsHidden(true); showMessage(data.message || "数据加载完成", "ok"); } async function applyExclusions() { if (!state.bundle) { showMessage("请先加载数据", "warn"); return; } const payload = collectExclusionPayload(); const data = await requestJSON("/api/update-exclusions", { method: "POST", body: JSON.stringify(payload), }); renderBundle(data.bundle); showMessage(data.message || "剔除已应用并重算", "ok"); } async function startJob() { if (!state.bundle) { showMessage("请先加载数据", "warn"); return; } const data = await requestJSON("/api/job/start", { method: "POST", body: JSON.stringify({ tuning_rows: state.tuningRows }), }); renderJobState(data.job_state || state.jobState || {}); startPolling(); setResultsHidden(true); showMessage(data.message || "后台任务已启动", "ok"); } async function controlJob(action) { const data = await requestJSON("/api/job/control", { method: "POST", body: JSON.stringify({ action }), }); renderJobState(data.job_state || {}); showMessage(`已发送${action}指令`, "ok"); } async function fetchJobStateRaw() { const data = await requestJSON("/api/job/state", { method: "GET" }); return data.job_state || {}; } async function fetchJobState() { const jobState = await fetchJobStateRaw(); renderJobState(jobState); if (["running", "paused"].includes(jobState.status)) { startPolling(); } else { stopPolling(); if (["completed", "failed", "stopped"].includes(jobState.status)) { await sleep(200); await fetchResults(); } } } async function fetchResults() { const target = state.bundle?.target_str || getDatePlusOne($("base_date").value); const data = await requestJSON(`/api/results?target_str=${encodeURIComponent(target)}`, { method: "GET" }); if (data.has_results) { renderResults(data); showMessage("结果已更新", "ok", 1500); return; } if (data.reject_summary) { renderRejectSummary(data.reject_summary); showMessage("最近一次任务为失败/停止,已展示淘汰统计", "warn", 2400); return; } setResultsHidden(true); } function startPolling() { if (state.pollTimer) return; state.pollTimer = setInterval(() => { fetchJobState().catch((e) => { console.error(e); showMessage(e.message || "轮询失败", "err", 1800); stopPolling(); }); }, 900); } function stopPolling() { if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; } } async function init() { $("base_date").addEventListener("change", updateTargetDateTip); checkIds.forEach((id) => { $(id).addEventListener("change", refreshEnableStates); }); $("add_maintenance_btn").addEventListener("click", () => appendMaintenanceRow()); $("save_cfg_btn").addEventListener("click", async () => { try { await saveConfig(); } catch (e) { showMessage(e.message, "err"); } }); $("reset_cfg_btn").addEventListener("click", async () => { try { await resetConfig(); } catch (e) { showMessage(e.message, "err"); } }); $("load_data_btn").addEventListener("click", async () => { try { await loadData(); } catch (e) { showMessage(e.message, "err", 3600); } }); $("apply_exclude_btn").addEventListener("click", async () => { try { await applyExclusions(); } catch (e) { showMessage(e.message, "err", 3600); } }); $("exclude_select_all_btn").addEventListener("click", () => { setAllSelected("exclude_select", true); showMessage("已全选场次,点击“应用剔除”后生效", "ok", 1400); }); $("exclude_select_none_btn").addEventListener("click", () => { setAllSelected("exclude_select", false); showMessage("已取消全部场次选择", "ok", 1400); }); $("exclude_select_suspected_btn").addEventListener("click", () => { const el = $("exclude_select"); let count = 0; for (const opt of Array.from(el.options)) { const pick = opt.dataset.suspected === "1"; opt.selected = pick; if (pick) count += 1; } showMessage(`已选中 ${count} 条疑似特殊场次`, "ok", 1600); }); $("exclude_clear_btn").addEventListener("click", () => { setAllSelected("exclude_select", false); setAllSelected("exclude_movie_select", false); setAllSelected("exclude_hall_select", false); $("exclude_rule_zero_sales").checked = false; $("exclude_rule_early_morning").checked = false; showMessage("剔除条件已清空,点击“应用剔除”后恢复", "ok", 1800); }); $("tuning_select_all_btn").addEventListener("click", () => { if (!state.tuningRows.length) { showMessage("请先加载数据", "warn"); return; } state.tuningRows.forEach((row) => { row["选中"] = true; }); renderTuningTable(); showMessage("已全选所有影片行", "ok", 1500); }); $("tuning_select_none_btn").addEventListener("click", () => { if (!state.tuningRows.length) { showMessage("请先加载数据", "warn"); return; } state.tuningRows.forEach((row) => { row["选中"] = false; }); renderTuningTable(); showMessage("已取消所有影片勾选", "ok", 1500); }); $("tuning_clear_sessions_btn").addEventListener("click", () => { applyTuningBatchMutation((row) => { sessionConstraintColumns.forEach((col) => { row[col] = null; }); }, "场次约束已清空"); }); $("tuning_clear_ratio_btn").addEventListener("click", () => { applyTuningBatchMutation((row) => { ratioConstraintColumns.forEach((col) => { row[col] = null; }); }, "占比约束已清空"); }); $("run_btn").addEventListener("click", async () => { try { await startJob(); } catch (e) { showMessage(e.message, "err", 3200); } }); $("pause_btn").addEventListener("click", async () => { try { await controlJob("pause"); } catch (e) { showMessage(e.message, "err"); } }); $("resume_btn").addEventListener("click", async () => { try { await controlJob("resume"); } catch (e) { showMessage(e.message, "err"); } }); $("stop_btn").addEventListener("click", async () => { try { await controlJob("stop"); } catch (e) { showMessage(e.message, "err"); } }); try { const data = await requestJSON("/api/session", { method: "GET" }); $("base_date").value = data.base_date; updateTargetDateTip(); setRuntimeCfg(data.runtime_cfg || {}); if (data.bundle) { renderBundle(data.bundle); } renderJobState(data.job_state || {}); if (["running", "paused"].includes(data.job_state?.status)) { startPolling(); } if (["completed", "failed", "stopped"].includes(data.job_state?.status)) { await fetchResults(); } } catch (e) { showMessage(e.message || "初始化失败", "err", 5000); } } window.addEventListener("beforeunload", () => { stopPolling(); }); document.addEventListener("DOMContentLoaded", () => { init().catch((e) => { showMessage(e.message || "初始化失败", "err", 5000); }); });