Files
cmbuyer/docs/design/web-task-workbench.html
T

599 lines
46 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>采购任务 · 采购服务原型</title>
<style>
:root {
--bg:#f4f7fb; --surface:#fff; --subtle:#f8fafc; --text:#172033; --muted:#526079;
--border:#cfd8e6; --border-strong:#91a0b4; --primary:#155eef; --primary-hover:#0b4ed1;
--primary-soft:#eaf1ff; --danger:#b42318; --danger-soft:#fef3f2; --success:#067647;
--success-soft:#ecfdf3; --warning:#8a4b0f; --warning-soft:#fff7e8; --focus:#ffbf47;
--shadow:0 7px 24px rgba(23,32,51,.08);
font-family:"Segoe UI","Microsoft YaHei UI",system-ui,sans-serif;
}
* { box-sizing:border-box; }
html { min-width:320px; background:var(--bg); }
body { margin:0; min-height:100dvh; color:var(--text); background:var(--bg); font-size:16px; line-height:1.5; }
body:has(dialog[open]) { overflow:hidden; }
a { color:var(--primary); text-underline-offset:3px; }
button,input,select { font:inherit; }
button,.button,input,select { min-height:44px; }
button { cursor:pointer; }
button:disabled { cursor:not-allowed; opacity:.56; }
:focus-visible { outline:3px solid var(--focus); outline-offset:2px; }
.skip-link { position:fixed; z-index:1000; top:8px; left:8px; padding:10px 14px; color:#fff; background:var(--text); transform:translateY(-160%); }
.skip-link:focus { transform:translateY(0); }
.prototype-banner { position:sticky; z-index:50; top:0; padding:7px 16px; color:#fff; background:#29364d; text-align:center; font-size:.78rem; font-weight:700; letter-spacing:.04em; }
.app-header { position:sticky; z-index:40; top:33px; min-height:64px; display:flex; align-items:center; gap:24px; padding:0 max(16px,calc((100vw - 1480px)/2)); border-bottom:1px solid var(--border); background:rgba(255,255,255,.98); }
.brand { display:flex; align-items:center; gap:10px; color:var(--text); text-decoration:none; font-weight:750; white-space:nowrap; }
.brand-mark { display:grid; place-items:center; width:32px; height:32px; border-radius:8px; color:#fff; background:var(--primary); font-size:.8rem; }
nav.primary { display:flex; align-items:stretch; align-self:stretch; gap:4px; }
nav.primary a { display:flex; align-items:center; padding:0 12px; color:#3d4b63; text-decoration:none; font-weight:650; border-bottom:3px solid transparent; }
nav.primary a[aria-current="page"] { color:var(--primary); border-bottom-color:var(--primary); }
.header-account { margin-left:auto; color:var(--muted); font-size:.88rem; white-space:nowrap; }
main { width:min(100% - 32px,1480px); margin:0 auto; padding:24px 0 64px; }
.page-head { display:flex; justify-content:space-between; align-items:end; gap:20px; margin-bottom:17px; }
h1 { margin:0; font-size:clamp(1.55rem,3vw,2rem); }
.subtitle { margin:5px 0 0; color:var(--muted); }
.safety-note { max-width:560px; margin:0; color:#29466f; font-size:.84rem; text-align:right; }
.safety-note strong { color:#163c8c; }
.workbench { border:1px solid var(--border); border-radius:12px; background:var(--surface); box-shadow:var(--shadow); overflow:hidden; }
.toolbar { border-bottom:1px solid var(--border); background:var(--surface); }
.toolbar-row { min-height:68px; display:flex; align-items:center; justify-content:space-between; gap:12px; padding:11px 14px; }
.toolbar-row + .toolbar-row { border-top:1px solid #e6ebf2; background:var(--subtle); }
.toolbar-group { display:flex; flex-wrap:wrap; align-items:center; gap:9px; }
.toolbar-hint { margin:0; color:var(--muted); font-size:.8rem; }
.button { display:inline-flex; align-items:center; justify-content:center; gap:7px; padding:8px 14px; border:1px solid transparent; border-radius:8px; text-decoration:none; font-weight:700; white-space:nowrap; }
.button svg { width:17px; height:17px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
.button-primary { color:#fff; background:var(--primary); }
.button-primary:hover:not(:disabled) { background:var(--primary-hover); }
.button-secondary { color:var(--text); border-color:var(--border); background:var(--surface); }
.button-secondary:hover:not(:disabled) { background:#eef2f7; }
.button-quiet { color:#34425b; border-color:transparent; background:transparent; }
.button-quiet:hover:not(:disabled) { background:#e9eef5; }
.filter-form { width:100%; display:flex; align-items:end; justify-content:space-between; gap:12px; }
.filter-field { width:min(100%,520px); }
.filter-field label,.field label { display:block; margin-bottom:5px; color:#34425b; font-size:.82rem; font-weight:700; }
input,select { width:100%; padding:8px 11px; border:1px solid var(--border-strong); border-radius:8px; color:var(--text); background:#fff; }
input[aria-invalid="true"] { border-color:var(--danger); box-shadow:0 0 0 1px var(--danger); }
.filter-actions { display:flex; align-items:end; gap:9px; }
.advanced-filters { position:relative; }
.advanced-filters > summary { min-height:44px; display:flex; align-items:center; padding:8px 12px; border:1px solid var(--border); border-radius:8px; color:#34425b; background:#fff; cursor:pointer; font-weight:700; white-space:nowrap; }
.advanced-filters[open] > summary { color:#163c8c; border-color:#8aacf2; background:var(--primary-soft); }
.filter-popover { position:absolute; z-index:12; top:51px; right:0; width:310px; display:grid; gap:11px; padding:13px; border:1px solid var(--border); border-radius:9px; background:#fff; box-shadow:var(--shadow); }
.filter-popover label { display:block; margin-bottom:4px; color:#34425b; font-size:.78rem; font-weight:700; }
.selection-mode { width:100%; display:flex; align-items:center; justify-content:space-between; gap:12px; }
.selection-mode[hidden],.filter-form[hidden] { display:none; }
.selection-copy { min-width:0; }
.selection-copy strong { display:block; color:#163c8c; }
.selection-copy span { color:var(--muted); font-size:.8rem; }
.table-summary { min-height:45px; display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:8px; padding:8px 14px; color:var(--muted); font-size:.82rem; border-bottom:1px solid #e5eaf1; }
.prototype-controls { position:relative; }
.prototype-controls summary { cursor:pointer; color:#34425b; font-weight:700; }
.demo-menu { position:absolute; z-index:10; right:0; top:30px; width:220px; display:grid; gap:6px; padding:9px; border:1px solid var(--border); border-radius:9px; background:#fff; box-shadow:var(--shadow); }
.demo-menu button { min-height:38px; padding:6px 9px; border:1px solid var(--border); border-radius:7px; color:#34425b; background:var(--subtle); text-align:left; }
.table-scroll { width:100%; overflow:auto; scrollbar-gutter:stable; }
table { width:100%; min-width:1180px; border-collapse:separate; border-spacing:0; table-layout:fixed; }
th,td { padding:11px 10px; border-bottom:1px solid #e3e8ef; text-align:left; vertical-align:middle; overflow-wrap:anywhere; }
th { position:sticky; top:0; z-index:4; color:#3b4960; background:#f1f5f9; font-size:.78rem; font-weight:750; white-space:nowrap; }
th:first-child,td:first-child { position:sticky; left:0; z-index:3; width:52px; text-align:center; background:inherit; }
th:nth-child(2),td:nth-child(2) { position:sticky; left:52px; z-index:3; width:270px; background:inherit; box-shadow:1px 0 0 #dce3ec; }
th:first-child,th:nth-child(2) { z-index:6; background:#f1f5f9; }
th:nth-child(3) { width:105px; } th:nth-child(4) { width:90px; } th:nth-child(5) { width:108px; }
th:nth-child(6) { width:72px; } th:nth-child(7) { width:260px; } th:nth-child(8) { width:130px; } th:nth-child(9) { width:166px; }
tbody tr { background:#fff; }
tbody tr:nth-child(even) { background:#fbfcfe; }
tbody tr:hover { background:#edf4ff; }
tbody tr:focus { position:relative; z-index:2; background:#eaf1ff; }
tbody tr[data-selected="true"] { background:#eaf1ff; }
tbody tr[data-new="true"] { animation:new-row 1.1s ease-out; }
td { font-size:.87rem; }
.row-check,.head-check { width:18px; height:18px; margin:0; accent-color:var(--primary); }
.product-link { display:inline-flex; align-items:start; gap:5px; color:#173e91; font-weight:700; line-height:1.35; }
.product-link svg { width:14px; height:14px; flex:0 0 auto; margin-top:2px; fill:none; stroke:currentColor; stroke-width:2; }
.task-link { display:block; width:max-content; max-width:100%; margin-top:4px; color:var(--muted); font-size:.75rem; font-variant-numeric:tabular-nums; }
.money,.quantity,.created { font-variant-numeric:tabular-nums; }
.money,.quantity { text-align:right; }
.result { color:#34425b; font-variant-numeric:tabular-nums; }
.result.empty { color:#7b8799; }
.status { display:inline-flex; align-items:center; gap:6px; padding:4px 8px; border:1px solid #b9cffc; border-radius:999px; color:#163c8c; background:var(--primary-soft); font-size:.74rem; font-weight:750; white-space:nowrap; }
.status::before { content:""; width:7px; height:7px; border-radius:50%; background:currentColor; }
.status.warning { color:var(--warning); border-color:#e8c983; background:var(--warning-soft); }
.status.success { color:var(--success); border-color:#a6e3c4; background:var(--success-soft); }
.status.danger { color:var(--danger); border-color:#f3b7b2; background:var(--danger-soft); }
.status.neutral { color:#526079; border-color:#cfd8e6; background:#f1f5f9; }
.table-state { min-height:320px; display:none; place-items:center; padding:36px 20px; text-align:center; background:#fff; }
.table-state.show { display:grid; }
.table-state h2 { margin:0 0 6px; font-size:1.08rem; }
.table-state p { max-width:520px; margin:0 0 14px; color:var(--muted); }
.table-state.error { color:var(--danger); background:var(--danger-soft); }
.skeletons { width:min(100%,850px); display:grid; gap:10px; }
.skeleton { height:45px; border-radius:7px; background:linear-gradient(90deg,#e9edf3 25%,#f7f9fb 37%,#e9edf3 63%); background-size:400% 100%; animation:shimmer 1.4s ease infinite; }
.toast { position:fixed; z-index:120; right:18px; bottom:18px; width:min(420px,calc(100% - 36px)); padding:12px 14px; border:1px solid #a6e3c4; border-radius:9px; color:var(--success); background:var(--success-soft); box-shadow:var(--shadow); }
.toast[hidden] { display:none; }
dialog { color:var(--text); }
dialog::backdrop { background:rgba(15,23,42,.58); }
.create-dialog { width:min(720px,calc(100% - 28px)); max-height:calc(100dvh - 28px); padding:0; border:0; border-radius:13px; box-shadow:0 24px 80px rgba(0,0,0,.3); }
.dialog-head { display:flex; align-items:start; justify-content:space-between; gap:14px; padding:17px 20px 13px; border-bottom:1px solid var(--border); }
.dialog-head h2 { margin:0; font-size:1.15rem; }
.dialog-head p { margin:4px 0 0; color:var(--muted); font-size:.84rem; }
.icon-button { width:44px; height:44px; display:grid; flex:0 0 auto; place-items:center; padding:0; border:1px solid var(--border); border-radius:8px; color:#34425b; background:#fff; }
.icon-button svg { width:19px; height:19px; fill:none; stroke:currentColor; stroke-width:2; }
.create-form { padding:18px 20px 20px; }
.form-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:13px 15px; }
.field.full { grid-column:1/-1; }
.field-hint,.field-error { min-height:1.2em; margin:4px 0 0; font-size:.76rem; }
.field-hint { color:var(--muted); }
.field-error { color:var(--danger); font-weight:650; }
.error-summary { display:none; margin:0 0 14px; padding:10px 12px; border:1px solid #f3b7b2; border-radius:8px; color:var(--danger); background:var(--danger-soft); }
.error-summary.show { display:block; }
.dialog-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:9px; margin-top:18px; padding-top:15px; border-top:1px solid var(--border); }
.detail-drawer { width:min(860px,94vw); height:100dvh; max-width:none; max-height:none; margin:0 0 0 auto; padding:0; border:0; border-radius:0; box-shadow:-18px 0 55px rgba(0,0,0,.22); overflow:hidden; }
.detail-drawer[open] { display:grid; grid-template-rows:auto minmax(0,1fr) auto; }
.drawer-head { display:flex; align-items:start; justify-content:space-between; gap:16px; padding:18px 20px 14px; border-bottom:1px solid var(--border); background:#fff; }
.drawer-head h2 { margin:3px 0 0; font-size:1.28rem; line-height:1.3; }
.drawer-eyebrow { margin:0; color:var(--muted); font-size:.76rem; font-variant-numeric:tabular-nums; }
.drawer-body { min-height:0; overflow-y:auto; padding:18px 20px 28px; background:var(--bg); }
.drawer-safety { margin:0 0 14px; padding:11px 13px; border:1px solid #b9cffc; border-radius:9px; color:#29466f; background:var(--primary-soft); }
.drawer-panel { margin-bottom:13px; padding:15px; border:1px solid var(--border); border-radius:10px; background:#fff; }
.drawer-panel h3 { margin:0 0 10px; font-size:1rem; }
.detail-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:9px; margin:0; }
.detail-grid div { min-width:0; padding:10px 11px; border:1px solid #e1e7ef; border-radius:8px; background:var(--subtle); }
.detail-grid dt { color:var(--muted); font-size:.75rem; }
.detail-grid dd { margin:2px 0 0; font-weight:650; overflow-wrap:anywhere; }
.evidence-placeholder { min-height:160px; display:grid; place-items:center; padding:20px; border:1px dashed #aebacc; border-radius:9px; color:var(--muted); background:#f1f5f9; text-align:center; }
.next-step { padding:11px 13px; border-left:4px solid var(--primary); border-radius:6px; color:#29466f; background:var(--primary-soft); }
.drawer-actions { display:flex; justify-content:flex-end; gap:9px; padding:13px 20px; border-top:1px solid var(--border); background:#fff; }
@keyframes shimmer { to { background-position:-100% 0; } }
@keyframes new-row { from { background:#fff2bf; } to { background:#fff; } }
@media (max-width:767px) {
.app-header { top:33px; padding-inline:16px; gap:10px; }
.header-account { display:none; }
nav.primary a { padding-inline:8px; }
main { width:min(100% - 20px,1480px); padding-top:18px; }
.page-head { align-items:start; flex-direction:column; }
.safety-note { text-align:left; }
.toolbar-row,.filter-form,.selection-mode { align-items:stretch; flex-direction:column; }
.toolbar-group,.filter-field { width:100%; }
.toolbar-group .button,.filter-form .button,.selection-mode .button { flex:1 1 auto; }
.filter-actions { width:100%; align-items:stretch; flex-direction:column; }
.advanced-filters,.advanced-filters > summary { width:100%; }
.filter-popover { position:static; width:100%; margin-top:7px; box-shadow:none; }
.form-grid,.detail-grid { grid-template-columns:1fr; }
.field.full { grid-column:auto; }
.create-dialog { width:100%; max-height:100dvh; height:100dvh; margin:0; border-radius:0; }
.detail-drawer { width:100vw; }
.drawer-actions { flex-direction:column-reverse; }
.drawer-actions .button { width:100%; }
}
@media (max-width:430px) {
.prototype-banner { position:static; padding-inline:8px; }
.app-header { top:0; }
.brand span:last-child,nav.primary a:last-child { display:none; }
.toolbar-group { display:grid; grid-template-columns:1fr 1fr; }
}
@media (prefers-reduced-motion:reduce) {
*,*::before,*::after { scroll-behavior:auto!important; animation-duration:.01ms!important; animation-iteration-count:1!important; transition-duration:.01ms!important; }
}
</style>
</head>
<body>
<a class="skip-link" href="#main">跳到主要内容</a>
<div class="prototype-banner">PROTOTYPE - 仅供枚举交互,非实现依据</div>
<header class="app-header">
<a class="brand" href="web-task-workbench.html" aria-label="采购服务原型工作台"><span class="brand-mark" aria-hidden="true">采</span><span>采购服务</span></a>
<nav class="primary" aria-label="主导航"><a href="web-task-workbench.html" aria-current="page">采购任务</a><a href="web-task-create.html">建单直达页</a></nav>
<span class="header-account">采购管理员 · caigou_admin</span>
</header>
<main id="main">
<header class="page-head">
<div><h1>采购任务</h1><p class="subtitle">创建后先保存为待开始;勾选后统一进入第一趟试选队列。</p></div>
<p class="safety-note"><strong>只开始试选,不会下单。</strong> 下单必须在试选完成并经人工确认后另行授权,付款始终由人完成。</p>
</header>
<section class="workbench" aria-label="采购任务表格工作台">
<div class="toolbar">
<div class="toolbar-row">
<div class="toolbar-group">
<button class="button button-secondary" type="button" disabled aria-describedby="import-hint">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 19h14"/></svg>导入
</button>
<button class="button button-primary" id="open-create" type="button">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>创建
</button>
</div>
<p class="toolbar-hint" id="import-hint">导入暂未开放;MVP 只支持手工创建。</p>
</div>
<div class="toolbar-row">
<form class="filter-form" id="filter-form" role="search">
<div class="filter-field"><label for="keyword">标题关键词</label><input id="keyword" type="search" placeholder="输入采购任务标题" autocomplete="off"></div>
<div class="filter-actions">
<details class="advanced-filters" id="advanced-filters"><summary>更多条件</summary><div class="filter-popover"><div><label for="status-filter">状态</label><select id="status-filter"><option value="all">全部状态</option><option value="DRAFT">待开始</option><option value="PENDING">待领取</option><option value="WAITING_CONFIRMATION">等待确认</option><option value="WAITING_PAYMENT">待付款</option><option value="RECONCILIATION_REQUIRED">结果待核查</option><option value="SUCCEEDED">已完成</option></select></div><div><label for="time-filter">创建时间</label><select id="time-filter"><option value="all">全部时间</option><option value="today">今天</option><option value="week">最近 7 天</option></select></div></div></details>
<div class="toolbar-group"><button class="button button-primary" type="submit">筛选</button><button class="button button-secondary" id="clear-filter" type="button">清除</button></div>
</div>
</form>
<div class="selection-mode" id="selection-mode" hidden>
<div class="selection-copy"><strong id="selection-count">已选择 0 条</strong><span>只会加入第一趟试选队列,不签发授权、不创建订单、不付款。</span></div>
<div class="toolbar-group"><button class="button button-primary" id="start-trial" type="button">开始试选</button><button class="button button-secondary" id="clear-selection" type="button">清除选择</button></div>
</div>
</div>
</div>
<div class="table-summary">
<span id="result-summary" aria-live="polite">正在准备假数据任务…</span>
<details class="prototype-controls"><summary>原型状态</summary><div class="demo-menu"><button type="button" data-demo="default">默认表格</button><button type="button" data-demo="loading">加载中</button><button type="button" data-demo="empty">空结果</button><button type="button" data-demo="error">加载失败</button></div></details>
</div>
<div class="table-scroll" id="table-scroll" tabindex="0" aria-label="采购任务表格,可横向滚动">
<table aria-describedby="table-help">
<caption id="table-help" style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0">双击非控件区域或聚焦行后按 Enter 查看详情;空格勾选待开始任务。</caption>
<thead><tr><th scope="col"><input class="head-check" id="select-all" type="checkbox" aria-label="选择当前筛选结果中全部待开始任务"></th><th scope="col">标题</th><th scope="col">颜色</th><th scope="col">尺码</th><th scope="col" class="money">价格上限</th><th scope="col" class="quantity">数量</th><th scope="col">采购结果 <span title="固定顺序:颜色、尺码、单价、数量">ⓘ</span></th><th scope="col">状态</th><th scope="col" aria-sort="descending">创建时间 ↓</th></tr></thead>
<tbody id="task-rows"></tbody>
</table>
</div>
<section class="table-state" id="loading-state" aria-live="polite"><div class="skeletons"><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div><p>正在加载采购任务,请稍候…</p></div></section>
<section class="table-state" id="empty-state"><div><h2>没有匹配的采购任务</h2><p>更换标题关键词,或者清除当前筛选。</p><button class="button button-primary" type="button" data-action="clear-filter">清除筛选</button></div></section>
<section class="table-state error" id="error-state" role="alert"><div><h2>任务列表加载失败</h2><p>当前筛选条件已保留。检查连接后可以安全重试。</p><button class="button button-primary" type="button" data-action="retry">重试加载</button></div></section>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<dialog class="create-dialog" id="create-dialog" aria-labelledby="create-title">
<header class="dialog-head"><div><h2 id="create-title">创建采购任务</h2><p>保存后为待开始,不会立即驱动手机。</p></div><button class="icon-button" type="button" data-close-create aria-label="关闭创建弹窗"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button></header>
<form class="create-form" id="create-form" novalidate>
<div class="error-summary" id="create-error-summary" role="alert" tabindex="-1"></div>
<div class="form-grid">
<div class="field full"><label for="create-name">任务标题 *</label><input id="create-name" name="title" value="秋季仓库工作服补货" aria-describedby="create-name-error"><p class="field-error" id="create-name-error"></p></div>
<div class="field full"><label for="create-url">拼多多商品链接 *</label><input id="create-url" name="product_url" type="url" inputmode="url" value="https://mobile.yangkeduo.invalid/goods.html?goods_id=900000001" aria-describedby="create-url-hint create-url-error"><p class="field-hint" id="create-url-hint">原型使用 .invalid 假链接;生产环境需解析出 goods_id。</p><p class="field-error" id="create-url-error"></p></div>
<div class="field"><label for="create-color">颜色 *</label><input id="create-color" name="color" value="藏青色" aria-describedby="create-color-error"><p class="field-error" id="create-color-error"></p></div>
<div class="field"><label for="create-size">尺码 *</label><input id="create-size" name="size" value="XL" aria-describedby="create-size-error"><p class="field-error" id="create-size-error"></p></div>
<div class="field"><label for="create-price">价格上限(总额) *</label><input id="create-price" name="max_price" type="text" inputmode="decimal" value="80.00" aria-describedby="create-price-hint create-price-error"><p class="field-hint" id="create-price-hint">金额使用十进制字符串,最多两位小数。</p><p class="field-error" id="create-price-error"></p></div>
<div class="field"><label for="create-quantity">数量 *</label><input id="create-quantity" name="quantity" type="number" inputmode="numeric" min="1" step="1" value="2" aria-describedby="create-quantity-error"><p class="field-error" id="create-quantity-error"></p></div>
</div>
<div class="dialog-actions"><button class="button button-secondary" type="button" data-close-create>取消</button><button class="button button-primary" type="submit">保存任务</button></div>
</form>
</dialog>
<dialog class="detail-drawer" id="detail-drawer" aria-labelledby="drawer-title">
<header class="drawer-head"><div><p class="drawer-eyebrow" id="drawer-id">任务详情</p><h2 id="drawer-title" tabindex="-1">采购任务</h2></div><button class="icon-button" id="close-drawer" type="button" aria-label="关闭任务详情"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button></header>
<div class="drawer-body">
<p class="drawer-safety"><strong>系统只创建待付款订单,绝不自动付款。</strong> 当前详情使用假数据,不访问拼多多或真实接口。</p>
<section class="drawer-panel"><h3>采购要求</h3><dl class="detail-grid" id="drawer-requirements"></dl></section>
<section class="drawer-panel"><h3>采购结果</h3><div id="drawer-result"></div></section>
<section class="drawer-panel"><h3>当前状态与下一步</h3><div class="next-step" id="drawer-next-step"></div></section>
<section class="drawer-panel"><h3>执行证据</h3><div class="evidence-placeholder"><div><strong>假数据证据占位</strong><br>真实可读字段必须等待 T-103 真机取证;不展示地址、手机号或付款信息。</div></div></section>
</div>
<footer class="drawer-actions"><button class="button button-secondary" id="drawer-close-secondary" type="button">关闭</button><a class="button button-primary" id="full-detail-link" href="web-task-detail.html">在完整页面打开</a></footer>
</dialog>
<script>
(() => {
'use strict';
const tasks = [
{ id:'T-20260803-023', title:'仓库雨衣补货', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000023', color:'荧光黄', size:'XXL', maxPrice:'96.00', quantity:3, status:'DRAFT', createdAt:'2026-08-03T14:42:00+08:00', result:null },
{ id:'T-20260803-022', title:'8 月仓库工作服补货', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000022', color:'白色', size:'XL', maxPrice:'80.00', quantity:2, status:'WAITING_CONFIRMATION', createdAt:'2026-08-03T13:18:00+08:00', result:{ phase:'试选', color:'白色', size:'XL', unitPrice:'32.50', quantity:2 } },
{ id:'T-20260803-021', title:'办公室收纳盒', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000021', color:'透明', size:'L', maxPrice:'60.00', quantity:2, status:'RECONCILIATION_REQUIRED', createdAt:'2026-08-03T11:36:00+08:00', result:{ phase:'下单待核查', color:'透明', size:'L', unitPrice:'24.00', quantity:2 } },
{ id:'T-20260803-020', title:'仓库防滑手套', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000020', color:'黑色', size:'均码', maxPrice:'64.00', quantity:5, status:'PENDING', createdAt:'2026-08-03T10:45:00+08:00', result:null },
{ id:'T-20260801-009', title:'标签打印纸补货', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000009', color:'白色', size:'50×30', maxPrice:'50.00', quantity:1, status:'WAITING_PAYMENT', createdAt:'2026-08-01T16:12:00+08:00', result:{ phase:'已下单', color:'白色', size:'50×30', unitPrice:'46.00', quantity:1 } },
{ id:'T-20260731-042', title:'打包胶带补货', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000042', color:'透明', size:'45mm', maxPrice:'55.00', quantity:4, status:'SUCCEEDED', createdAt:'2026-07-31T09:20:00+08:00', result:{ phase:'已下单', color:'透明', size:'45mm', unitPrice:'12.00', quantity:4 } }
];
const statusMeta = {
DRAFT:{ label:'待开始', tone:'neutral', next:'勾选后点击“开始试选”,任务才会进入设备领取队列。' },
PENDING:{ label:'待领取', tone:'', next:'已进入第一趟试选队列,等待桌面端轮询领取。' },
WAITING_CONFIRMATION:{ label:'等待确认', tone:'', next:'查看规格面板证据,确认机器选对后才可签发下单授权。' },
RECONCILIATION_REQUIRED:{ label:'结果待核查', tone:'warning', next:'订单可能已创建。只核查同一提交记录,不得重新提交。' },
WAITING_PAYMENT:{ label:'待付款', tone:'warning', next:'人在拼多多核对商品、规格、数量和金额后手工付款;系统不会付款。' },
SUCCEEDED:{ label:'已完成', tone:'success', next:'已由人工付款并标记完成,可查看执行证据与审计记录。' }
};
const rows = document.querySelector('#task-rows');
const tableScroll = document.querySelector('#table-scroll');
const summary = document.querySelector('#result-summary');
const keyword = document.querySelector('#keyword');
const statusFilter = document.querySelector('#status-filter');
const timeFilter = document.querySelector('#time-filter');
const filterForm = document.querySelector('#filter-form');
const selectionMode = document.querySelector('#selection-mode');
const selectionCount = document.querySelector('#selection-count');
const selectAll = document.querySelector('#select-all');
const states = { loading:document.querySelector('#loading-state'), empty:document.querySelector('#empty-state'), error:document.querySelector('#error-state') };
const selectedIds = new Set();
let appliedKeyword = '';
let appliedStatus = 'all';
let appliedTime = 'all';
let newTaskId = '';
let demoState = 'default';
let lastDetailTrigger = null;
let toastTimer = null;
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (character) => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#039;' })[character]);
}
function normalizeMoney(value) {
const parts = value.split('.');
const whole = parts[0].replace(/^0+(?=\d)/, '') || '0';
const fraction = `${parts[1] || ''}00`.slice(0,2);
return `${whole}.${fraction}`;
}
function formatDate(value) {
return new Intl.DateTimeFormat('zh-CN',{ year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', hour12:false }).format(new Date(value));
}
function visibleTasks() {
const now = new Date();
return [...tasks].sort((a,b) => new Date(b.createdAt) - new Date(a.createdAt)).filter((task) => {
const matchesTitle = !appliedKeyword || task.title.toLowerCase().includes(appliedKeyword.toLowerCase());
const matchesStatus = appliedStatus === 'all' || task.status === appliedStatus;
const created = new Date(task.createdAt);
const matchesTime = appliedTime === 'all' || (appliedTime === 'today' && created.toDateString() === now.toDateString()) || (appliedTime === 'week' && now - created <= 7 * 24 * 60 * 60 * 1000);
return matchesTitle && matchesStatus && matchesTime;
});
}
function formatResult(task) {
if (!task.result) return '<span class="result empty">—</span>';
const result = task.result;
const readable = `${result.phase}:${result.color},${result.size},${result.unitPrice} 元,${result.quantity} 件`;
return `<span class="result" aria-label="${escapeHtml(readable)}"><strong>${escapeHtml(result.phase)}:</strong>${escapeHtml(result.color)} | ${escapeHtml(result.size)} | ¥${escapeHtml(result.unitPrice)} | ${result.quantity}件</span>`;
}
function renderRows() {
const visible = visibleTasks();
const validIds = new Set(tasks.filter((task) => task.status === 'DRAFT').map((task) => task.id));
[...selectedIds].forEach((id) => { if (!validIds.has(id)) selectedIds.delete(id); });
rows.innerHTML = visible.map((task) => {
const selectable = task.status === 'DRAFT';
const checked = selectedIds.has(task.id);
const meta = statusMeta[task.status] || { label:task.status, tone:'neutral', next:'查看完整详情了解下一步。' };
return `<tr tabindex="0" data-task-row="${escapeHtml(task.id)}" data-selected="${checked}" data-new="${task.id === newTaskId}" aria-label="任务 ${escapeHtml(task.title)},状态 ${escapeHtml(meta.label)}。双击或按 Enter 查看详情${selectable ? ',按空格切换选择' : ''}">
<td><input class="row-check" type="checkbox" data-select-task="${escapeHtml(task.id)}" aria-label="选择任务 ${escapeHtml(task.title)}" ${checked ? 'checked' : ''} ${selectable ? '' : `disabled title="仅待开始任务可勾选;当前为${escapeHtml(meta.label)}"`}></td>
<td><a class="product-link" href="${escapeHtml(task.productUrl)}" target="_blank" rel="noopener noreferrer" aria-label="在新标签打开拼多多商品假链接:${escapeHtml(task.title)}">${escapeHtml(task.title)}<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-9 9M19 13v6H5V5h6"/></svg></a><a class="task-link" href="#/tasks/${encodeURIComponent(task.id)}" data-detail-link="${escapeHtml(task.id)}">${escapeHtml(task.id)} · 查看任务详情</a></td>
<td>${escapeHtml(task.color)}</td><td>${escapeHtml(task.size)}</td><td class="money">¥${escapeHtml(task.maxPrice)}</td><td class="quantity">${task.quantity}</td><td>${formatResult(task)}</td><td><span class="status ${meta.tone}">${escapeHtml(meta.label)}</span></td><td class="created"><time datetime="${escapeHtml(task.createdAt)}">${escapeHtml(formatDate(task.createdAt))}</time></td>
</tr>`;
}).join('');
tableScroll.hidden = demoState !== 'default' || visible.length === 0;
Object.values(states).forEach((state) => state.classList.remove('show'));
if (demoState !== 'default') states[demoState].classList.add('show');
else if (!visible.length) states.empty.classList.add('show');
summary.textContent = demoState === 'loading' ? '正在加载任务。' : demoState === 'error' ? '任务加载失败,筛选条件已保留。' : demoState === 'empty' ? '当前演示空结果。' : `共 ${visible.length} 条任务,按创建时间倒序;其中 ${visible.filter((task) => task.status === 'DRAFT').length} 条待开始。`;
updateSelectionUi();
if (newTaskId) window.setTimeout(() => { newTaskId = ''; }, 1200);
}
function updateSelectionUi() {
const count = selectedIds.size;
filterForm.hidden = count > 0;
selectionMode.hidden = count === 0;
selectionCount.textContent = `已选择 ${count} 条待开始任务`;
const selectableVisible = visibleTasks().filter((task) => task.status === 'DRAFT');
const selectedVisible = selectableVisible.filter((task) => selectedIds.has(task.id));
selectAll.checked = selectableVisible.length > 0 && selectedVisible.length === selectableVisible.length;
selectAll.indeterminate = selectedVisible.length > 0 && selectedVisible.length < selectableVisible.length;
selectAll.disabled = selectableVisible.length === 0;
}
function showToast(message) {
const toast = document.querySelector('#toast');
window.clearTimeout(toastTimer);
toast.textContent = message;
toast.hidden = false;
toastTimer = window.setTimeout(() => { toast.hidden = true; }, 4500);
}
function showDemo(state) {
demoState = state;
selectedIds.clear();
document.querySelector('.prototype-controls').open = false;
renderRows();
}
filterForm.addEventListener('submit', (event) => {
event.preventDefault();
appliedKeyword = keyword.value.trim();
appliedStatus = statusFilter.value;
appliedTime = timeFilter.value;
selectedIds.clear();
demoState = 'default';
document.querySelector('#advanced-filters').open = false;
renderRows();
});
function clearFilter() {
keyword.value = '';
statusFilter.value = 'all';
timeFilter.value = 'all';
appliedKeyword = '';
appliedStatus = 'all';
appliedTime = 'all';
selectedIds.clear();
demoState = 'default';
renderRows();
keyword.focus();
}
document.querySelector('#clear-filter').addEventListener('click', clearFilter);
document.querySelectorAll('[data-action="clear-filter"]').forEach((button) => button.addEventListener('click', clearFilter));
document.querySelectorAll('[data-action="retry"]').forEach((button) => button.addEventListener('click', () => showDemo('default')));
document.querySelectorAll('[data-demo]').forEach((button) => button.addEventListener('click', () => showDemo(button.dataset.demo)));
selectAll.addEventListener('change', () => {
visibleTasks().filter((task) => task.status === 'DRAFT').forEach((task) => selectAll.checked ? selectedIds.add(task.id) : selectedIds.delete(task.id));
renderRows();
});
rows.addEventListener('change', (event) => {
const checkbox = event.target.closest('[data-select-task]');
if (!checkbox) return;
checkbox.checked ? selectedIds.add(checkbox.dataset.selectTask) : selectedIds.delete(checkbox.dataset.selectTask);
renderRows();
});
document.querySelector('#clear-selection').addEventListener('click', () => { selectedIds.clear(); renderRows(); keyword.focus(); });
document.querySelector('#start-trial').addEventListener('click', () => {
const ids = [...selectedIds];
tasks.forEach((task) => { if (selectedIds.has(task.id) && task.status === 'DRAFT') task.status = 'PENDING'; });
selectedIds.clear();
renderRows();
showToast(`已将 ${ids.length} 条任务加入第一趟试选队列。没有签发下单授权,也不会付款。`);
});
function isInteractive(target) { return Boolean(target.closest('a,button,input,label,select,textarea,summary')); }
rows.addEventListener('dblclick', (event) => {
if (isInteractive(event.target)) return;
const row = event.target.closest('[data-task-row]');
if (row) openDetail(row.dataset.taskRow, row, true);
});
rows.addEventListener('keydown', (event) => {
const row = event.target.closest('[data-task-row]');
if (!row || event.target !== row) return;
if (event.key === 'Enter') { event.preventDefault(); openDetail(row.dataset.taskRow, row, true); }
if (event.key === ' ') {
const task = tasks.find((item) => item.id === row.dataset.taskRow);
if (!task || task.status !== 'DRAFT') return;
event.preventDefault();
selectedIds.has(task.id) ? selectedIds.delete(task.id) : selectedIds.add(task.id);
renderRows();
document.querySelector(`[data-task-row="${CSS.escape(task.id)}"]`)?.focus();
}
});
rows.addEventListener('click', (event) => {
const link = event.target.closest('[data-detail-link]');
if (!link || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
event.preventDefault();
openDetail(link.dataset.detailLink, link, true);
});
const createDialog = document.querySelector('#create-dialog');
const createForm = document.querySelector('#create-form');
const createFields = {
title:document.querySelector('#create-name'), url:document.querySelector('#create-url'), color:document.querySelector('#create-color'), size:document.querySelector('#create-size'), price:document.querySelector('#create-price'), quantity:document.querySelector('#create-quantity')
};
const createErrors = Object.fromEntries(Object.keys(createFields).map((name) => [name,document.querySelector(`#create-${name === 'title' ? 'name' : name}-error`)]));
const createSummary = document.querySelector('#create-error-summary');
function clearCreateErrors() {
Object.values(createFields).forEach((field) => field.removeAttribute('aria-invalid'));
Object.values(createErrors).forEach((node) => { node.textContent = ''; });
createSummary.classList.remove('show');
createSummary.textContent = '';
}
function showCreateErrors(messages) {
clearCreateErrors();
Object.entries(messages).forEach(([name,message]) => { createFields[name].setAttribute('aria-invalid','true'); createErrors[name].textContent = message; });
createSummary.textContent = `无法保存任务:请修正 ${Object.keys(messages).length} 个字段。`;
createSummary.classList.add('show');
createSummary.focus();
}
document.querySelector('#open-create').addEventListener('click', () => { clearCreateErrors(); createDialog.showModal(); createFields.title.focus(); });
document.querySelectorAll('[data-close-create]').forEach((button) => button.addEventListener('click', () => createDialog.close()));
createForm.addEventListener('submit', (event) => {
event.preventDefault();
const messages = {};
if (!createFields.title.value.trim()) messages.title = '请输入任务标题。';
if (!/^https:\/\/[^\s]+[?&]goods_id=\d+/i.test(createFields.url.value.trim())) messages.url = '请输入能解析出 goods_id 的 HTTPS 商品链接。';
if (!createFields.color.value.trim()) messages.color = '请输入颜色。';
if (!createFields.size.value.trim()) messages.size = '请输入尺码。';
if (!/^\d+(\.\d{1,2})?$/.test(createFields.price.value.trim()) || /^0+(\.0{1,2})?$/.test(createFields.price.value.trim())) messages.price = '请输入大于 0、最多两位小数的价格上限。';
if (!/^\d+$/.test(createFields.quantity.value) || parseInt(createFields.quantity.value,10) < 1) messages.quantity = '数量必须是大于 0 的整数。';
if (Object.keys(messages).length) { showCreateErrors(messages); return; }
const now = new Date();
const datePart = `${now.getFullYear()}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}`;
const id = `T-${datePart}-${String(tasks.length + 24).padStart(3,'0')}`;
tasks.push({ id, title:createFields.title.value.trim(), productUrl:createFields.url.value.trim(), color:createFields.color.value.trim(), size:createFields.size.value.trim(), maxPrice:normalizeMoney(createFields.price.value.trim()), quantity:parseInt(createFields.quantity.value,10), status:'DRAFT', createdAt:now.toISOString(), result:null });
newTaskId = id;
appliedKeyword = '';
keyword.value = '';
statusFilter.value = 'all';
timeFilter.value = 'all';
appliedStatus = 'all';
appliedTime = 'all';
selectedIds.clear();
demoState = 'default';
createDialog.close();
renderRows();
const newRow = document.querySelector(`[data-task-row="${CSS.escape(id)}"]`);
newRow?.focus();
showToast(`任务 ${id} 已保存为待开始,并按创建时间显示在第一行。`);
});
const drawer = document.querySelector('#detail-drawer');
const drawerTitle = document.querySelector('#drawer-title');
const drawerId = document.querySelector('#drawer-id');
const drawerRequirements = document.querySelector('#drawer-requirements');
const drawerResult = document.querySelector('#drawer-result');
const drawerNext = document.querySelector('#drawer-next-step');
const fullDetailLink = document.querySelector('#full-detail-link');
function routeTaskId() {
const match = window.location.hash.match(/^#\/tasks\/(.+)$/);
return match ? decodeURIComponent(match[1]) : null;
}
function renderDrawer(task) {
const meta = statusMeta[task.status] || { label:task.status, tone:'neutral', next:'查看完整详情了解下一步。' };
drawerId.textContent = `${task.id} · ${meta.label}`;
drawerTitle.textContent = task.title;
drawerRequirements.innerHTML = `<div><dt>颜色</dt><dd>${escapeHtml(task.color)}</dd></div><div><dt>尺码</dt><dd>${escapeHtml(task.size)}</dd></div><div><dt>价格上限</dt><dd>¥${escapeHtml(task.maxPrice)}</dd></div><div><dt>数量</dt><dd>${task.quantity} 件</dd></div><div><dt>状态</dt><dd><span class="status ${meta.tone}">${escapeHtml(meta.label)}</span></dd></div><div><dt>创建时间</dt><dd>${escapeHtml(formatDate(task.createdAt))}</dd></div>`;
drawerResult.innerHTML = task.result ? `<p style="margin:0 0 10px">${formatResult(task)}</p><p style="margin:0;color:var(--muted);font-size:.82rem">展示值来自结构化颜色、尺码、单价和数量;“|”只用于页面格式化。</p>` : '<p class="result empty" style="margin:0">尚无采购结果。任务未完成试选或下单,不用其他数据填充。</p>';
drawerNext.textContent = meta.next;
fullDetailLink.href = `web-task-detail.html#${encodeURIComponent(task.id)}`;
}
function openDetail(id, trigger, pushRoute) {
const task = tasks.find((item) => item.id === id);
if (!task) { showToast('没有找到该任务,列表可能已经刷新。'); return; }
if (trigger) lastDetailTrigger = trigger.closest?.('[data-task-row]') || trigger;
renderDrawer(task);
if (!drawer.open) drawer.showModal();
if (pushRoute && routeTaskId() !== id) history.pushState({ taskDrawer:true, taskId:id },'',`#/tasks/${encodeURIComponent(id)}`);
window.setTimeout(() => drawerTitle.focus(),0);
}
function closeDrawerPhysical() {
if (drawer.open) drawer.close();
const focusTarget = lastDetailTrigger;
lastDetailTrigger = null;
window.setTimeout(() => focusTarget?.focus?.(),0);
}
function requestDrawerClose() {
if (history.state?.taskDrawer && routeTaskId()) history.back();
else {
history.replaceState({},'',`${location.pathname}${location.search}`);
closeDrawerPhysical();
}
}
function syncDrawerRoute() {
const id = routeTaskId();
if (id) openDetail(id,null,false);
else closeDrawerPhysical();
}
drawer.addEventListener('cancel', (event) => { event.preventDefault(); requestDrawerClose(); });
document.querySelector('#close-drawer').addEventListener('click', requestDrawerClose);
document.querySelector('#drawer-close-secondary').addEventListener('click', requestDrawerClose);
window.addEventListener('popstate', syncDrawerRoute);
window.addEventListener('hashchange', syncDrawerRoute);
renderRows();
syncDrawerRoute();
})();
</script>
</body>
</html>