docs: 增加档口入库码交互原型 (#225)
This commit is contained in:
@@ -0,0 +1,881 @@
|
||||
<!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; }
|
||||
|
||||
.summary {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
min-width: 112px;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-content: center;
|
||||
gap: 2px 8px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.summary-card:hover { border-color: #aeb7c2; }
|
||||
.summary-card.active { border-color: var(--primary); box-shadow: inset 0 -2px 0 var(--primary); }
|
||||
.summary-card strong { grid-row: 1 / span 2; align-self: center; font-size: 20px; }
|
||||
.summary-card span { color: #444; }
|
||||
.summary-card small { color: var(--muted); }
|
||||
.summary-card.warning strong { color: var(--warning); }
|
||||
.summary-card.danger strong { color: var(--danger); }
|
||||
.summary-card.success strong { color: var(--success); }
|
||||
|
||||
.summary-note {
|
||||
flex: 1 1 auto;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 4px;
|
||||
color: var(--muted);
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.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; }
|
||||
.summary { overflow-x: auto; }
|
||||
.summary-note { display: none; }
|
||||
}
|
||||
</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="summary" aria-label="当前记录状态汇总">
|
||||
<button type="button" class="summary-card active" data-summary-filter="all">
|
||||
<strong id="count-all">0</strong><span>全部</span><small>本次导入</small>
|
||||
</button>
|
||||
<button type="button" class="summary-card" data-summary-filter="pending">
|
||||
<strong id="count-pending">0</strong><span>待匹配</span><small>等待确定性匹配</small>
|
||||
</button>
|
||||
<button type="button" class="summary-card" data-summary-filter="ready">
|
||||
<strong id="count-ready">0</strong><span>可回写</span><small>允许勾选</small>
|
||||
</button>
|
||||
<button type="button" class="summary-card success" data-summary-filter="updated">
|
||||
<strong id="count-updated">0</strong><span>已回写</span><small>远端复核成功</small>
|
||||
</button>
|
||||
<button type="button" class="summary-card warning" data-summary-filter="needs_check">
|
||||
<strong id="count-needs_check">0</strong><span>需核对</span><small>结果未知,不重试</small>
|
||||
</button>
|
||||
<button type="button" class="summary-card danger" data-summary-filter="problem">
|
||||
<strong id="count-problem">0</strong><span>其他未完成</span><small>已跳过 / 失败</small>
|
||||
</button>
|
||||
<div class="summary-note">业务键:日期 + 订单号 + 档口及货号 + 标准化颜色尺码</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="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 => ({
|
||||
"&": "&", "<": "<", ">": ">", "'": "'", '"': """
|
||||
})[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("#count-all").textContent = records.length;
|
||||
document.querySelector("#count-pending").textContent = count("pending");
|
||||
document.querySelector("#count-ready").textContent = count("ready");
|
||||
document.querySelector("#count-updated").textContent = count("updated");
|
||||
document.querySelector("#count-needs_check").textContent = count("needs_check");
|
||||
document.querySelector("#count-problem").textContent = count("skipped") + count("failed");
|
||||
}
|
||||
|
||||
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;
|
||||
document.querySelectorAll("[data-summary-filter]").forEach(card => {
|
||||
card.classList.toggle("active", card.dataset.summaryFilter === 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("[data-summary-filter]").forEach(card => {
|
||||
card.addEventListener("click", () => setFilter(card.dataset.summaryFilter));
|
||||
});
|
||||
|
||||
document.querySelectorAll(".nav a").forEach(link => {
|
||||
if (!link.classList.contains("active")) {
|
||||
link.addEventListener("click", event => {
|
||||
event.preventDefault();
|
||||
showToast("这是独立原型,其他模块链接未接入。");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
render();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user