feat(prototype): add MVP web interaction flows
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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>
|
||||
Reference in New Issue
Block a user