nextday / static /app.js
Ethscriptions's picture
Upload 2 files
028c113 verified
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 = `
<td><input type="text" data-key="hall" value="${escapeHtml(String(row.hall || ""))}" placeholder="2号厅 / 2"></td>
<td><input type="text" data-key="start" value="${escapeHtml(String(row.start || ""))}" placeholder="14:00"></td>
<td><input type="text" data-key="end" value="${escapeHtml(String(row.end || ""))}" placeholder="16:00"></td>
<td><button type="button" class="btn secondary small">删除</button></td>
`;
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 = "<thead><tr><th>提示</th></tr></thead><tbody><tr><td>无数据</td></tr></tbody>";
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 = `<div class="k">${escapeHtml(item.k)}</div><div class="v">${escapeHtml(String(item.v))}</div>`;
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 = `
<section class="subpanel">
<h3>${escapeHtml(cand.title)}</h3>
<div>${cand.gantt_html || "<div class='muted'>无甘特图</div>"}</div>
<h4>评分拆解</h4>
<div class="table-wrap"><table id="score_breakdown_table" class="simple-table"></table></div>
<h4>结果汇总</h4>
<div class="table-wrap"><table id="summary_table" class="simple-table"></table></div>
<h4>排片明细</h4>
<div class="table-wrap"><table id="schedule_table" class="simple-table"></table></div>
<h4>🔍 场次合理性检查日志</h4>
<pre class="code-block">${escapeHtml(cand.log_text || "")}</pre>
</section>
`;
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
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);
});
});