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

276 lines
22 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>任务工作台 · cmbuyer 原型</title>
<style>
:root {
--bg: #f4f7fb; --surface: #fff; --surface-subtle: #f8fafc; --text: #172033; --muted: #526079;
--border: #cfd8e6; --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 5px 16px rgba(23,32,51,.07);
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; }
a { color: var(--primary); text-underline-offset: 3px; }
button, input, select { font: inherit; }
button, input, select, .button { min-height: 44px; }
button { cursor: pointer; }
button:disabled { cursor: not-allowed; opacity: .6; }
:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }
.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 - 1400px) / 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, 1400px); margin: 0 auto; padding: 26px 0 64px; }
.page-head { display: flex; justify-content: space-between; align-items: end; gap: 20px; margin-bottom: 18px; }
h1 { margin: 0; font-size: clamp(1.6rem, 3vw, 2rem); }
.subtitle { margin: 5px 0 0; color: var(--muted); }
.button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 9px 16px; border: 1px solid transparent; border-radius: 8px; text-decoration: none; font-weight: 700; }
.button-primary { color: #fff; background: var(--primary); }
.button-primary:hover { background: var(--primary-hover); }
.button-secondary { color: var(--text); border-color: var(--border); background: var(--surface); }
.button-secondary:hover { background: var(--surface-subtle); }
.query-panel { display: grid; grid-template-columns: minmax(220px, 1fr) 190px 190px auto; gap: 12px; align-items: end; padding: 16px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); box-shadow: var(--shadow); }
label { display: block; margin-bottom: 5px; color: #34425b; font-size: .84rem; font-weight: 700; }
input, select { width: 100%; padding: 9px 11px; border: 1px solid #91a0b4; border-radius: 8px; color: var(--text); background: #fff; }
.query-hint { margin: 8px 0 0; color: var(--muted); font-size: .8rem; }
.summary-line { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px; margin: 16px 2px 10px; color: var(--muted); font-size: .86rem; }
.demo-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
.demo-controls strong { margin-right: 2px; }
.demo-controls button { min-height: 34px; padding: 5px 9px; border: 1px solid var(--border); border-radius: 7px; color: #33415b; background: var(--surface-subtle); font-size: .78rem; }
.lanes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; align-items: start; }
.lane { min-width: 0; border: 1px solid var(--border); border-radius: 12px; background: #edf2f8; overflow: clip; }
.lane[hidden] { display: none; }
.lane-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 14px 14px 12px; border-bottom: 1px solid var(--border); background: var(--surface-subtle); }
.lane-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
.lane-title h2 { margin: 0; font-size: 1rem; }
.lane-mark { width: 10px; height: 10px; flex: 0 0 auto; border-radius: 50%; background: var(--primary); }
.lane[data-lane="machine"] .lane-mark { background: #526079; }
.lane[data-lane="done"] .lane-mark { background: var(--success); }
.count { display: inline-grid; min-width: 28px; height: 24px; place-items: center; padding: 0 7px; border-radius: 999px; color: #34425b; background: #e2e8f0; font-size: .78rem; font-weight: 750; font-variant-numeric: tabular-nums; }
.lane-description { margin: 0; padding: 0 14px 10px; color: var(--muted); background: var(--surface-subtle); font-size: .79rem; }
.task-list { display: grid; gap: 10px; padding: 10px; }
.task-card { min-width: 0; border: 1px solid #c6d0de; border-radius: 9px; background: var(--surface); box-shadow: 0 2px 5px rgba(23,32,51,.04); overflow: hidden; }
.task-card[hidden] { display: none; }
.card-top { display: flex; align-items: start; justify-content: space-between; gap: 8px; padding: 12px; }
.task-id { color: var(--muted); font-size: .76rem; font-variant-numeric: tabular-nums; }
.task-card h3 { margin: 3px 0 0; font-size: .96rem; line-height: 1.35; overflow-wrap: anywhere; }
.badge { flex: 0 0 auto; padding: 4px 7px; border: 1px solid #b9cffc; border-radius: 999px; color: #163c8c; background: var(--primary-soft); font-size: .72rem; font-weight: 700; }
.badge.warning { color: var(--warning); border-color: #f0cd86; background: var(--warning-soft); }
.badge.success { color: var(--success); border-color: #a6e3c4; background: var(--success-soft); }
.badge.danger { color: var(--danger); border-color: #f3b7b2; background: var(--danger-soft); }
.task-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; margin: 0; padding: 0 12px 12px; }
.task-meta div { min-width: 0; }
.task-meta dt { color: var(--muted); font-size: .72rem; }
.task-meta dd { margin: 1px 0 0; font-size: .84rem; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
details { border-top: 1px solid var(--border); }
summary { min-height: 44px; padding: 11px 12px; cursor: pointer; color: var(--primary); font-size: .84rem; font-weight: 700; }
.decision-preview { padding: 0 12px 12px; }
.decision-preview p { margin: 0 0 8px; color: #34425b; font-size: .84rem; }
.decision-preview .button { width: 100%; min-height: 40px; padding: 7px 10px; font-size: .84rem; }
.card-footer { display: flex; justify-content: space-between; gap: 8px; align-items: center; padding: 9px 12px; border-top: 1px solid #e5eaf1; color: var(--muted); font-size: .75rem; }
.card-footer a { font-weight: 700; }
.panel-state { display: none; min-height: 320px; place-items: center; padding: 36px 20px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); text-align: center; }
.panel-state.show { display: grid; }
.panel-state h2 { margin: 0 0 6px; font-size: 1.1rem; }
.panel-state p { max-width: 520px; margin: 0 0 14px; color: var(--muted); }
.panel-state.error { border-color: #f3b7b2; background: var(--danger-soft); }
.skeletons { width: min(100%, 700px); display: grid; gap: 12px; }
.skeleton { height: 86px; border-radius: 10px; background: linear-gradient(90deg, #e9edf3 25%, #f5f7fa 37%, #e9edf3 63%); background-size: 400% 100%; animation: shimmer 1.4s ease infinite; }
@keyframes shimmer { to { background-position: -100% 0; } }
@media (max-width: 1100px) { .lanes { grid-template-columns: 1fr; } .lane { max-width: none; } }
@media (max-width: 900px) { .query-panel { grid-template-columns: 1fr 1fr; } .query-panel .keyword { grid-column: 1 / -1; } }
@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% - 24px, 1400px); padding-top: 20px; }
.page-head { align-items: start; flex-direction: column; }
.page-head .button { width: 100%; }
.query-panel { grid-template-columns: 1fr; }
.query-panel .keyword { grid-column: auto; }
.query-panel .button { width: 100%; }
.summary-line { align-items: start; flex-direction: column; }
.task-meta { grid-template-columns: 1fr; }
}
@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; }
}
@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"><span class="brand-mark" aria-hidden="true">CM</span><span>cmbuyer</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>
<a class="button button-primary" href="web-task-create.html">新建采购任务</a>
</header>
<section class="query-panel" aria-label="任务查询">
<div class="keyword"><label for="keyword">关键词</label><input id="keyword" type="search" placeholder="标题、规格或任务编号" autocomplete="off"></div>
<div><label for="lane-filter">泳道</label><select id="lane-filter"><option value="all">全部泳道</option><option value="decision">等你决定</option><option value="machine">机器在跑</option><option value="done">已结束</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>
<button class="button button-secondary" id="clear-filters" type="button">清除筛选</button>
</section>
<p class="query-hint">各泳道计数只随关键词与时间范围变化,不随泳道选择变化。</p>
<div class="summary-line">
<span id="result-summary" aria-live="polite">共 6 条假数据任务,其中 3 条等你决定。</span>
<div class="demo-controls" aria-label="原型状态控制"><strong>原型状态:</strong><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>
</div>
<section class="panel-state" id="loading-state" aria-live="polite">
<div class="skeletons" aria-label="正在加载任务"><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div><p>正在加载任务,请稍候…</p></div>
</section>
<section class="panel-state" id="empty-state">
<div><h2>没有匹配的任务</h2><p>调整关键词或时间范围,或者清除全部筛选条件。</p><button class="button button-primary" type="button" data-action="clear">清除筛选</button></div>
</section>
<section class="panel-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 class="lanes" id="lanes" aria-label="任务泳道">
<section class="lane" data-lane="decision" aria-labelledby="decision-title">
<header class="lane-head"><div class="lane-title"><span class="lane-mark" aria-hidden="true"></span><h2 id="decision-title">等你决定</h2></div><span class="count" id="count-decision">3</span></header>
<p class="lane-description">确认试选、核对待付款或处理结果不明。</p>
<div class="task-list">
<article class="task-card" data-task data-lane-name="decision" data-date="today" data-search="T-20260803-018 工作服 白色 XL 等待确认">
<div class="card-top"><div><span class="task-id">T-20260803-018</span><h3>8 月仓库工作服补货</h3></div><span class="badge">等待确认</span></div>
<dl class="task-meta"><div><dt>要求</dt><dd>白色 · XL · 2 件</dd></div><div><dt>机器选到</dt><dd>白色 · XL</dd></div><div><dt>读到单价</dt><dd>¥32.50</dd></div><div><dt>上限 / 合计</dt><dd>¥80.00 / ¥65.00</dd></div></dl>
<details open><summary>已展开:机器选对了吗?</summary><div class="decision-preview"><p><strong>规格一致、金额未超上限。</strong>确认前仍需查看规格面板截图。</p><a class="button button-primary" href="web-task-detail.html">查看证据并决定</a></div></details>
<footer class="card-footer"><span>更新于 10:42</span><a href="web-task-detail.html">详情</a></footer>
</article>
<article class="task-card" data-task data-lane-name="decision" data-date="today" data-search="T-20260803-016 办公室收纳盒 透明 L 结果不明 调和">
<div class="card-top"><div><span class="task-id">T-20260803-016</span><h3>办公室收纳盒</h3></div><span class="badge warning">结果不明</span></div>
<dl class="task-meta"><div><dt>状态</dt><dd>可能已创建订单</dd></div><div><dt>提交记录</dt><dd>SUB-016-A</dd></div></dl>
<footer class="card-footer"><span>更新于 09:58</span><a href="web-task-detail.html">人工核查</a></footer>
</article>
<article class="task-card" data-task data-lane-name="decision" data-date="week" data-search="T-20260801-009 标签打印纸 白色 50x30 待付款">
<div class="card-top"><div><span class="task-id">T-20260801-009</span><h3>标签打印纸补货</h3></div><span class="badge warning">待付款</span></div>
<dl class="task-meta"><div><dt>规格</dt><dd>白色 · 50×30</dd></div><div><dt>授权金额</dt><dd>¥46.00</dd></div></dl>
<footer class="card-footer"><span>更新于 8 月 1 日</span><a href="web-task-detail.html">核对订单</a></footer>
</article>
</div>
</section>
<section class="lane" data-lane="machine" aria-labelledby="machine-title">
<header class="lane-head"><div class="lane-title"><span class="lane-mark" aria-hidden="true"></span><h2 id="machine-title">机器在跑</h2></div><span class="count" id="count-machine">2</span></header>
<p class="lane-description">待领取、试选、授权后第二趟与围栏前执行。</p>
<div class="task-list">
<article class="task-card" data-task data-lane-name="machine" data-date="today" data-search="T-20260803-019 工作服 深灰 L 待领取">
<div class="card-top"><div><span class="task-id">T-20260803-019</span><h3>新建的工作服任务</h3></div><span class="badge">待领取</span></div>
<dl class="task-meta"><div><dt>要求</dt><dd>深灰 · L · 1 件</dd></div><div><dt>价格上限</dt><dd>¥45.00</dd></div></dl>
<footer class="card-footer"><span>更新于 10:47</span><a href="web-task-detail.html">查看进度</a></footer>
</article>
<article class="task-card" data-task data-lane-name="machine" data-date="today" data-search="T-20260803-017 防滑手套 黑色 均码 第二趟 围栏前">
<div class="card-top"><div><span class="task-id">T-20260803-017</span><h3>仓库防滑手套</h3></div><span class="badge">第二趟 · 围栏前</span></div>
<dl class="task-meta"><div><dt>锁定单价</dt><dd>¥12.80</dd></div><div><dt>授权到期</dt><dd>11:25</dd></div></dl>
<footer class="card-footer"><span>更新于 10:45</span><a href="web-task-detail.html">查看授权</a></footer>
</article>
</div>
</section>
<section class="lane" data-lane="done" aria-labelledby="done-title">
<header class="lane-head"><div class="lane-title"><span class="lane-mark" aria-hidden="true"></span><h2 id="done-title">已结束</h2></div><span class="count" id="count-done">1</span></header>
<p class="lane-description">已付款完成、失败或已取消的归档任务。</p>
<div class="task-list">
<article class="task-card" data-task data-lane-name="done" data-date="week" data-search="T-20260731-042 打包胶带 透明 45mm 已完成">
<div class="card-top"><div><span class="task-id">T-20260731-042</span><h3>打包胶带补货</h3></div><span class="badge success">已完成</span></div>
<dl class="task-meta"><div><dt>规格</dt><dd>透明 · 45mm</dd></div><div><dt>人工标记</dt><dd>采购管理员</dd></div></dl>
<footer class="card-footer"><span>更新于 7 月 31 日</span><a href="web-task-detail.html">查看记录</a></footer>
</article>
</div>
</section>
</section>
</main>
<script>
(() => {
'use strict';
const keyword = document.querySelector('#keyword');
const laneFilter = document.querySelector('#lane-filter');
const timeFilter = document.querySelector('#time-filter');
const lanes = document.querySelector('#lanes');
const summary = document.querySelector('#result-summary');
const states = { loading: document.querySelector('#loading-state'), empty: document.querySelector('#empty-state'), error: document.querySelector('#error-state') };
const cards = [...document.querySelectorAll('[data-task]')];
function hideStates() {
Object.values(states).forEach((state) => state.classList.remove('show'));
lanes.hidden = false;
}
function applyFilters() {
hideStates();
const query = keyword.value.trim().toLowerCase();
const time = timeFilter.value;
const selectedLane = laneFilter.value;
const counts = { decision: 0, machine: 0, done: 0 };
let shown = 0;
cards.forEach((card) => {
const matchesText = !query || card.dataset.search.toLowerCase().includes(query);
const matchesTime = time === 'all' || card.dataset.date === time || (time === 'week' && card.dataset.date === 'today');
const matchesBase = matchesText && matchesTime;
if (matchesBase) counts[card.dataset.laneName] += 1;
const visible = matchesBase && (selectedLane === 'all' || selectedLane === card.dataset.laneName);
card.hidden = !visible;
if (visible) shown += 1;
});
Object.entries(counts).forEach(([name, value]) => { document.querySelector(`#count-${name}`).textContent = value; });
document.querySelectorAll('.lane').forEach((lane) => { lane.hidden = selectedLane !== 'all' && lane.dataset.lane !== selectedLane; });
summary.textContent = `当前显示 ${shown} 条;等你决定 ${counts.decision},机器在跑 ${counts.machine},已结束 ${counts.done}。`;
if (!shown) { lanes.hidden = true; states.empty.classList.add('show'); }
}
function clearFilters() {
keyword.value = '';
laneFilter.value = 'all';
timeFilter.value = 'all';
applyFilters();
keyword.focus();
}
function showDemo(name) {
if (name === 'default') { applyFilters(); return; }
lanes.hidden = true;
Object.values(states).forEach((state) => state.classList.remove('show'));
states[name].classList.add('show');
summary.textContent = name === 'loading' ? '正在加载任务。' : name === 'empty' ? '筛选结果为空。' : '任务加载失败,筛选条件已保留。';
}
keyword.addEventListener('input', applyFilters);
laneFilter.addEventListener('change', applyFilters);
timeFilter.addEventListener('change', applyFilters);
document.querySelector('#clear-filters').addEventListener('click', clearFilters);
document.querySelectorAll('[data-action="clear"]').forEach((button) => button.addEventListener('click', clearFilters));
document.querySelectorAll('[data-action="retry"]').forEach((button) => button.addEventListener('click', applyFilters));
document.querySelectorAll('[data-demo]').forEach((button) => button.addEventListener('click', () => showDemo(button.dataset.demo)));
applyFilters();
})();
</script>
</body>
</html>