Files

806 lines
27 KiB
HTML
Raw Permalink 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.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>档口入库码 · Admin 交互原型</title>
<style>
:root {
color-scheme: light;
--nav: #2b3440;
--nav-hover: #3a4552;
--primary: #1f6feb;
--border: #e1e4e8;
--border-strong: #ccd1d6;
--page: #f5f6f8;
--muted: #667085;
--danger: #b42318;
--success: #176b36;
--warning: #7a4b00;
}
* { box-sizing: border-box; }
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
body {
height: 100vh;
height: 100dvh;
margin: 0;
overflow: hidden;
display: flex;
flex-direction: column;
font-family: "Microsoft YaHei", "PingFang SC", system-ui, sans-serif;
font-size: 14px;
color: #222;
background: var(--page);
}
button,
input,
select { font: inherit; }
button:focus-visible,
input:focus-visible,
select:focus-visible,
a:focus-visible {
outline: 3px solid rgba(31, 111, 235, .28);
outline-offset: 2px;
}
[hidden] { display: none !important; }
.nav {
flex: 0 0 auto;
min-height: 44px;
display: flex;
align-items: center;
gap: 2px;
padding: 0 12px;
background: var(--nav);
white-space: nowrap;
}
.nav-brand {
margin-right: 14px;
color: #fff;
font-weight: 600;
font-size: 14px;
}
.nav a {
padding: 13px 10px;
color: #c8cdd4;
text-decoration: none;
font-size: 13px;
}
.nav a:hover { color: #fff; background: var(--nav-hover); }
.nav a.active { color: #fff; background: var(--primary); }
.nav-spacer { flex: 1 1 auto; min-width: 8px; }
.nav-user { padding: 0 6px; color: #fff; font-size: 13px; }
.nav-button {
min-height: 30px;
padding: 4px 8px;
border: 1px solid #56616d;
border-radius: 3px;
color: #c8cdd4;
background: transparent;
}
.page {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 16px 8px;
}
.prototype-notice {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 8px;
min-height: 34px;
margin: 0;
padding: 7px 10px;
border: 1px solid #b8d4fa;
border-left: 4px solid var(--primary);
border-radius: 4px;
color: #174ea6;
background: #eef6ff;
}
.prototype-notice strong { white-space: nowrap; }
.toolbar {
flex: 0 0 auto;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 9px 10px;
border: 1px solid var(--border);
border-radius: 4px;
background: #fff;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.toolbar-separator {
width: 1px;
height: 28px;
background: var(--border);
}
label { color: #555; white-space: nowrap; }
input[type="date"],
input[type="search"],
select {
height: 30px;
padding: 4px 8px;
border: 1px solid var(--border-strong);
border-radius: 3px;
background: #fff;
}
input[type="search"] { width: 190px; }
select { min-width: 112px; }
button {
min-height: 30px;
padding: 5px 12px;
border: 1px solid var(--border-strong);
border-radius: 3px;
color: #222;
background: #fff;
cursor: pointer;
white-space: nowrap;
}
button:hover { background: #f0f2f4; }
button:disabled { opacity: .48; cursor: not-allowed; }
button.primary { color: #fff; border-color: var(--primary); background: var(--primary); }
button.primary:hover { background: #195fc9; }
button.danger { color: var(--danger); border-color: #efb2ac; }
button.link-button { min-height: auto; padding: 0; border: 0; color: var(--primary); background: transparent; }
.file-field {
max-width: 228px;
overflow: hidden;
color: #444;
text-overflow: ellipsis;
white-space: nowrap;
}
.toolbar-actions { margin-left: auto; }
.table-wrap {
flex: 1 1 auto;
min-height: 180px;
overflow: auto;
scrollbar-gutter: stable;
border: 1px solid var(--border);
border-radius: 4px;
background: #fff;
}
table { width: 100%; min-width: 1490px; border-collapse: collapse; }
th,
td {
padding: 7px 9px;
border-bottom: 1px solid #eef0f2;
text-align: left;
white-space: nowrap;
}
th {
position: sticky;
top: 0;
z-index: 2;
color: #444;
background: #fafbfc;
font-weight: 600;
}
tbody tr:hover { background: #f7f9fb; }
tbody tr.selected { background: #eef6ff; }
tbody tr.selected:hover { background: #e5f1ff; }
tbody tr.row-warning { background: #fffbe6; }
tbody tr.row-error { background: #fff5f4; }
.col-check { width: 38px; text-align: center; }
.col-order { min-width: 145px; }
.col-stall { min-width: 140px; }
.col-spec { min-width: 118px; }
.col-code { min-width: 162px; }
.col-state { min-width: 108px; }
.col-message { min-width: 260px; max-width: 340px; }
.col-time { min-width: 142px; }
.truncate {
display: block;
overflow: hidden;
color: #555;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-pill {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
color: #555;
background: #eceff2;
font-size: 12px;
}
.status-pending { color: #555; background: #eceff2; }
.status-ready { color: #174ea6; background: #e8f1ff; }
.status-applying { color: var(--warning); background: #fff3cd; }
.status-updated,
.status-already_filled { color: var(--success); background: #dff3e5; }
.status-skipped { color: var(--warning); background: #fff3cd; }
.status-failed { color: var(--danger); background: #fde8e8; }
.status-needs_check { color: #7c2d12; background: #ffedd5; }
.statusbar {
flex: 0 0 auto;
min-height: 38px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 14px;
padding: 8px 16px;
color: #c8cdd4;
background: var(--nav);
font-size: 13px;
}
.status-message { margin-right: auto; color: #fff; }
.pagination { display: flex; gap: 10px; }
.pagination span { color: #7f8a97; }
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 20;
display: grid;
place-items: center;
padding: 24px;
background: rgba(28, 36, 46, .48);
}
.modal {
width: min(520px, 100%);
overflow: hidden;
border: 1px solid var(--border);
border-radius: 6px;
background: #fff;
box-shadow: 0 18px 50px rgba(0, 0, 0, .22);
}
.modal-head,
.modal-foot {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
}
.modal-head { border-bottom: 1px solid var(--border); }
.modal-head h2 { margin: 0; font-size: 18px; }
.modal-x { margin-left: auto; min-width: 30px; padding: 2px 8px; font-size: 20px; }
.modal-body { padding: 16px; line-height: 1.65; }
.modal-body p { margin: 0 0 10px; }
.modal-body ul { margin: 8px 0; padding-left: 22px; }
.modal-foot { justify-content: flex-end; border-top: 1px solid var(--border); background: #fafbfc; }
.confirm-box {
padding: 10px 12px;
border-left: 4px solid var(--primary);
background: #f5f9ff;
}
.danger-note { color: var(--danger); }
.toast {
position: fixed;
right: 20px;
bottom: 54px;
z-index: 30;
max-width: 420px;
padding: 10px 14px;
border: 1px solid #9bc2f7;
border-radius: 4px;
color: #174ea6;
background: #eef6ff;
box-shadow: 0 8px 24px rgba(0, 0, 0, .14);
}
@media (max-width: 1100px) {
.nav { overflow-x: auto; }
}
</style>
</head>
<body>
<nav class="nav" aria-label="Admin 主导航">
<span class="nav-brand">采集采购管理端</span>
<a href="#">蝦皮数据</a>
<a href="#">PDD 商品</a>
<a href="#">顺运宝数据</a>
<a href="#">采集采购</a>
<a href="#">客户端列表</a>
<a href="#" class="active" aria-current="page">档口入库码</a>
<a href="#">店铺管理</a>
<a href="#">用户管理</a>
<a href="#">AI 配置</a>
<span class="nav-spacer"></span>
<span class="nav-user">admin</span>
<button type="button" class="nav-button">退出登录</button>
</nav>
<main class="page">
<p class="prototype-notice" role="status">
<strong>交互原型</strong>
<span>当前页面只使用虚构演示数据,不会上传 Excel、连接 Admin 或回写顺运宝。</span>
</p>
<section class="toolbar" aria-label="导入、匹配和回写操作">
<div class="toolbar-group">
<label for="biz-date">业务日期</label>
<input id="biz-date" type="date" value="2026-08-14">
</div>
<div class="toolbar-separator" aria-hidden="true"></div>
<div class="toolbar-group">
<button type="button" id="choose-file">选择 Excel</button>
<input id="file-input" type="file" accept=".xlsx" hidden>
<span class="file-field" id="file-name" title="档口入库码_2026-08-14.xlsx(演示文件)">档口入库码_2026-08-14.xlsx(演示)</span>
<button type="button" id="import-demo">重新导入演示数据</button>
</div>
<div class="toolbar-separator" aria-hidden="true"></div>
<div class="toolbar-group">
<label for="status-filter">状态</label>
<select id="status-filter">
<option value="all">全部状态</option>
<option value="pending">待匹配</option>
<option value="ready">可回写</option>
<option value="updated">已回写</option>
<option value="already_filled">已存在</option>
<option value="skipped">已跳过</option>
<option value="failed">失败</option>
<option value="needs_check">需核对</option>
</select>
<label for="keyword" class="visually-hidden">搜索订单号、档口、规格或入库码</label>
<input id="keyword" type="search" placeholder="订单号 / 档口 / 规格 / 入库码">
<button type="button" id="clear-filter">清除</button>
</div>
<div class="toolbar-group toolbar-actions">
<button type="button" id="match-button" class="primary">匹配顺运宝商品</button>
<button type="button" id="write-button" disabled>回写已选 <span id="selected-count">0</span> 条</button>
</div>
</section>
<section class="table-wrap" role="region" aria-label="档口入库码记录列表" tabindex="0">
<table>
<thead>
<tr>
<th class="col-check"><input id="check-all" type="checkbox" aria-label="全选当前页可回写记录"></th>
<th>业务日期</th>
<th class="col-order">订单号</th>
<th class="col-stall">档口及货号</th>
<th class="col-spec">Excel 颜色尺码</th>
<th class="col-spec">顺运宝规格</th>
<th class="col-code">档口入库码</th>
<th class="col-code">远端原值</th>
<th class="col-state">状态</th>
<th class="col-message">结果 / 原因</th>
<th class="col-time">更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="records-body"></tbody>
</table>
</section>
</main>
<footer class="statusbar">
<span id="status-message" class="status-message" role="status" aria-live="polite">已加载演示数据,可以先点击“匹配顺运宝商品”。</span>
<span id="summary-counts">共 0 条,可回写 0 条,需核对 0 条</span>
<span id="visible-count">当前显示 0 条</span>
<span>第 1 / 1 页</span>
<nav class="pagination" aria-label="分页"><span>首页</span><span>上一页</span><span>下一页</span><span>末页</span></nav>
</footer>
<div class="modal-backdrop" id="write-modal" hidden>
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="write-title">
<header class="modal-head">
<h2 id="write-title">确认回写档口入库码</h2>
<button type="button" class="modal-x" data-close-modal aria-label="关闭">×</button>
</header>
<div class="modal-body">
<div class="confirm-box">
将回写 <strong id="confirm-count">0</strong> 条记录,其中 <strong id="replace-count">0</strong> 条存在旧快递单号,需要先删除旧值再写入。
</div>
<ul>
<li>系统会在每条写入前重新读取顺运宝详情并核验商品。</li>
<li>写入后会再次查询;只有远端值一致才标记“已回写”。</li>
<li class="danger-note">超时或结果不确定时只标记“需核对”,不会自动重复写入。</li>
</ul>
</div>
<footer class="modal-foot">
<button type="button" data-close-modal>取消</button>
<button type="button" id="confirm-write" class="primary">确认回写</button>
</footer>
</section>
</div>
<div class="modal-backdrop" id="check-modal" hidden>
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="check-title">
<header class="modal-head">
<h2 id="check-title">重新核对远端结果</h2>
<button type="button" class="modal-x" data-close-modal aria-label="关闭">×</button>
</header>
<div class="modal-body">
<p>这里只会重新读取顺运宝详情,不会再次写入。</p>
<p>演示结果:远端快递单号已经等于 <strong id="check-code"></strong>,可以安全标记为“已回写”。</p>
</div>
<footer class="modal-foot">
<button type="button" data-close-modal>取消</button>
<button type="button" id="confirm-check" class="primary">读取并确认结果</button>
</footer>
</section>
</div>
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
<script>
(() => {
"use strict";
const initialRecords = () => [
{
id: 1, date: "2026-08-14", order: "260814DEMO001", stall: "A12-101",
rawSpec: "黑色,M", sybSpec: "—", code: "DK260814A010101", oldCode: "—",
status: "pending", message: "等待匹配顺运宝货运单商品", updated: "08-14 09:20"
},
{
id: 2, date: "2026-08-14", order: "260814DEMO002", stall: "B07-305",
rawSpec: "杏色,XL", sybSpec: "—", code: "DK260814A010102", oldCode: "OLD-DEMO-002",
status: "pending", message: "等待匹配顺运宝货运单商品", updated: "08-14 09:20"
},
{
id: 3, date: "2026-08-14", order: "260814DEMO003", stall: "C03-118",
rawSpec: "白色,均码", sybSpec: "—", code: "DK260814A010103", oldCode: "—",
status: "pending", message: "等待匹配顺运宝货运单商品", updated: "08-14 09:20"
},
{
id: 4, date: "2026-08-14", order: "260814DEMO004", stall: "D21-006",
rawSpec: "卡其色,L", sybSpec: "卡其色,L", code: "DK260814A010104", oldCode: "—",
status: "needs_check", message: "上次写入响应超时,禁止自动重试", updated: "08-14 09:18"
},
{
id: 5, date: "2026-08-14", order: "260814DEMO005", stall: "A16-208",
rawSpec: "蓝色,2XL", sybSpec: "蓝色,2XL", code: "DK260814A010105", oldCode: "DK260814A010105",
status: "already_filled", message: "远端已是相同入库码,无需重复写入", updated: "08-14 09:16"
},
{
id: 6, date: "2026-08-14", order: "260814DEMO006", stall: "F09-511",
rawSpec: "灰色,S", sybSpec: "灰色,S", code: "DK260814A010106", oldCode: "—",
status: "skipped", message: "商品已有采购平台或采购单号,不允许覆盖", updated: "08-14 09:15"
},
{
id: 7, date: "2026-08-14", order: "260814DEMO007", stall: "B18-022",
rawSpec: "粉色,M", sybSpec: "—", code: "DK260814A010107", oldCode: "—",
status: "failed", message: "本地未找到对应的顺运宝货运单", updated: "08-14 09:14"
}
];
const statusLabels = {
pending: "待匹配",
ready: "可回写",
applying: "回写中",
updated: "已回写",
already_filled: "已存在",
skipped: "已跳过",
failed: "失败",
needs_check: "需核对"
};
let records = initialRecords();
let selected = new Set();
let checkTarget = null;
const body = document.querySelector("#records-body");
const statusFilter = document.querySelector("#status-filter");
const keyword = document.querySelector("#keyword");
const checkAll = document.querySelector("#check-all");
const writeButton = document.querySelector("#write-button");
const selectedCount = document.querySelector("#selected-count");
const statusMessage = document.querySelector("#status-message");
const writeModal = document.querySelector("#write-modal");
const checkModal = document.querySelector("#check-modal");
const toast = document.querySelector("#toast");
function escapeHTML(value) {
return String(value).replace(/[&<>'"]/g, char => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;"
})[char]);
}
function visibleRecords() {
const state = statusFilter.value;
const search = keyword.value.trim().toLowerCase();
return records.filter(record => {
const stateMatched = state === "all"
|| record.status === state
|| (state === "problem" && ["skipped", "failed"].includes(record.status));
const haystack = [record.order, record.stall, record.rawSpec, record.sybSpec, record.code].join(" ").toLowerCase();
return stateMatched && (!search || haystack.includes(search));
});
}
function rowClass(record) {
const classes = [];
if (selected.has(record.id)) classes.push("selected");
if (["skipped", "needs_check"].includes(record.status)) classes.push("row-warning");
if (record.status === "failed") classes.push("row-error");
return classes.join(" ");
}
function rowAction(record) {
if (record.status === "needs_check") {
return `<button type="button" class="link-button" data-recheck="${record.id}">重新核对</button>`;
}
if (record.status === "failed") {
return `<button type="button" class="link-button" data-detail="${record.id}">查看原因</button>`;
}
return "—";
}
function render() {
const rows = visibleRecords();
body.innerHTML = rows.length ? rows.map(record => `
<tr class="${rowClass(record)}">
<td class="col-check"><input type="checkbox" data-row-check="${record.id}"
aria-label="选择订单 ${escapeHTML(record.order)}" ${selected.has(record.id) ? "checked" : ""}
${record.status === "ready" ? "" : "disabled"}></td>
<td>${escapeHTML(record.date)}</td>
<td>${escapeHTML(record.order)}</td>
<td>${escapeHTML(record.stall)}</td>
<td>${escapeHTML(record.rawSpec)}</td>
<td>${escapeHTML(record.sybSpec)}</td>
<td>${escapeHTML(record.code)}</td>
<td>${escapeHTML(record.oldCode)}</td>
<td><span class="status-pill status-${record.status}">${statusLabels[record.status]}</span></td>
<td class="col-message"><span class="truncate" title="${escapeHTML(record.message)}">${escapeHTML(record.message)}</span></td>
<td>${escapeHTML(record.updated)}</td>
<td>${rowAction(record)}</td>
</tr>`).join("") : `
<tr><td colspan="12" style="padding: 44px; text-align: center; color: #777;">当前筛选条件下没有记录。请清除搜索条件后再试。</td></tr>`;
document.querySelector("#visible-count").textContent = `当前显示 ${rows.length} 条`;
updateCounts();
updateSelection();
}
function count(state) {
return records.filter(record => record.status === state).length;
}
function updateCounts() {
document.querySelector("#summary-counts").textContent =
`共 ${records.length} 条,可回写 ${count("ready")} 条,需核对 ${count("needs_check")} 条`;
}
function updateSelection() {
[...selected].forEach(id => {
const record = records.find(item => item.id === id);
if (!record || record.status !== "ready") selected.delete(id);
});
selectedCount.textContent = selected.size;
writeButton.disabled = selected.size === 0;
const eligible = visibleRecords().filter(record => record.status === "ready");
const checkedEligible = eligible.filter(record => selected.has(record.id));
checkAll.disabled = eligible.length === 0;
checkAll.checked = eligible.length > 0 && checkedEligible.length === eligible.length;
checkAll.indeterminate = checkedEligible.length > 0 && checkedEligible.length < eligible.length;
}
function setMessage(message) {
statusMessage.textContent = message;
}
function showToast(message) {
toast.textContent = message;
toast.hidden = false;
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => { toast.hidden = true; }, 3200);
}
function setFilter(state) {
statusFilter.value = state;
render();
}
document.querySelector("#choose-file").addEventListener("click", () => {
document.querySelector("#file-input").click();
});
document.querySelector("#file-input").addEventListener("change", event => {
const file = event.target.files[0];
if (!file) return;
document.querySelector("#file-name").textContent = `${file.name}(原型不读取)`;
showToast("已选择文件。原型不会读取或上传该文件,请使用“重新导入演示数据”体验流程。");
});
document.querySelector("#import-demo").addEventListener("click", () => {
records = initialRecords();
selected.clear();
statusFilter.value = "all";
keyword.value = "";
document.querySelector("#biz-date").value = "2026-08-14";
document.querySelector("#file-name").textContent = "档口入库码_2026-08-14.xlsx(演示)";
setFilter("all");
setMessage("演示导入完成:7 条记录,新增 7 条,重复 0 条,Excel 临时文件已删除。");
showToast("演示导入完成。下一步请点击“匹配顺运宝商品”。");
});
document.querySelector("#match-button").addEventListener("click", () => {
const pending = records.filter(record => record.status === "pending");
if (!pending.length) {
showToast("没有待匹配记录。可重新导入演示数据再次体验。");
return;
}
pending.forEach(record => {
if (record.id === 1) {
Object.assign(record, { status: "ready", sybSpec: "黑色,M", message: "唯一匹配:货运单商品和档口均一致", updated: "08-14 09:22" });
} else if (record.id === 2) {
Object.assign(record, { status: "ready", sybSpec: "杏色,XL", message: "唯一匹配;回写前需处理旧快递单号", updated: "08-14 09:22" });
} else {
Object.assign(record, { status: "skipped", sybSpec: "白色,均码", message: "同订单同规格匹配到 2 条商品,不能自动选择", updated: "08-14 09:22" });
}
});
setFilter("ready");
setMessage("匹配完成:可回写 2 条,已跳过 1 条。歧义记录没有自动选择。");
showToast("匹配完成。请勾选“可回写”记录后继续。");
});
body.addEventListener("change", event => {
const checkbox = event.target.closest("[data-row-check]");
if (!checkbox) return;
const id = Number(checkbox.dataset.rowCheck);
if (checkbox.checked) selected.add(id); else selected.delete(id);
render();
});
checkAll.addEventListener("change", () => {
visibleRecords().filter(record => record.status === "ready").forEach(record => {
if (checkAll.checked) selected.add(record.id); else selected.delete(record.id);
});
render();
});
body.addEventListener("click", event => {
const recheck = event.target.closest("[data-recheck]");
if (recheck) {
checkTarget = Number(recheck.dataset.recheck);
const record = records.find(item => item.id === checkTarget);
document.querySelector("#check-code").textContent = record.code;
checkModal.hidden = false;
return;
}
const detail = event.target.closest("[data-detail]");
if (detail) showToast("失败原因已完整显示在“结果 / 原因”列;正式页面会保留该行供筛选和重试匹配。");
});
writeButton.addEventListener("click", () => {
const chosen = records.filter(record => selected.has(record.id) && record.status === "ready");
document.querySelector("#confirm-count").textContent = chosen.length;
document.querySelector("#replace-count").textContent = chosen.filter(record => record.oldCode !== "—").length;
writeModal.hidden = false;
});
document.querySelector("#confirm-write").addEventListener("click", () => {
writeModal.hidden = true;
const chosen = records.filter(record => selected.has(record.id) && record.status === "ready");
chosen.forEach(record => {
record.status = "applying";
record.message = "正在重新核验并回写,请勿重复操作";
record.updated = "08-14 09:24";
});
selected.clear();
render();
setMessage(`正在逐条回写 ${chosen.length} 条记录,页面可继续查看进度。`);
window.setTimeout(() => {
chosen.forEach(record => {
record.status = "updated";
record.oldCode = record.code;
record.message = "写入完成,远端再次查询结果一致";
record.updated = "08-14 09:25";
});
setFilter("updated");
setMessage(`回写完成:成功 ${chosen.length} 条,失败 0 条,需核对 0 条。`);
showToast("演示回写完成,已切换到“已回写”记录。");
}, 900);
});
document.querySelector("#confirm-check").addEventListener("click", () => {
const record = records.find(item => item.id === checkTarget);
if (record) {
record.status = "updated";
record.oldCode = record.code;
record.message = "重新读取远端后确认写入成功;没有重复写入";
record.updated = "08-14 09:26";
}
checkModal.hidden = true;
checkTarget = null;
setFilter("updated");
setMessage("重新核对完成:远端值一致,已把记录标记为“已回写”。");
});
document.querySelectorAll("[data-close-modal]").forEach(button => {
button.addEventListener("click", () => {
button.closest(".modal-backdrop").hidden = true;
});
});
document.querySelectorAll(".modal-backdrop").forEach(backdrop => {
backdrop.addEventListener("click", event => {
if (event.target === backdrop) backdrop.hidden = true;
});
});
document.addEventListener("keydown", event => {
if (event.key === "Escape") {
writeModal.hidden = true;
checkModal.hidden = true;
}
});
statusFilter.addEventListener("change", () => setFilter(statusFilter.value));
keyword.addEventListener("input", render);
document.querySelector("#clear-filter").addEventListener("click", () => {
keyword.value = "";
setFilter("all");
keyword.focus();
});
document.querySelectorAll(".nav a").forEach(link => {
if (!link.classList.contains("active")) {
link.addEventListener("click", event => {
event.preventDefault();
showToast("这是独立原型,其他模块链接未接入。");
});
}
});
render();
})();
</script>
</body>
</html>