Files
cmroubao/docs/design/admin-task-detail.html
T

487 lines
20 KiB
HTML

<!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>
* { box-sizing: border-box; }
[hidden] { display: none !important; }
:root {
color-scheme: light;
--ink: #18201d;
--muted: #5b6862;
--line: #cfd7d3;
--surface: #fff;
--canvas: #f3f5f4;
--brand: #0b6b50;
--brand-dark: #07513d;
--info: #185b8f;
--warn: #865f12;
--danger: #9b3030;
}
body {
margin: 0;
min-width: 280px;
color: var(--ink);
background: var(--canvas);
font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
letter-spacing: 0;
}
button { font: inherit; min-height: 42px; }
button:focus-visible, a:focus-visible {
outline: 3px solid rgba(26, 115, 184, .28);
outline-offset: 2px;
}
.prototype {
position: fixed;
inset: 0 0 auto;
z-index: 10;
min-height: 32px;
padding: 6px 16px;
color: #fff;
background: #24312c;
text-align: center;
font-size: 12px;
font-weight: 700;
}
.app { min-height: 100vh; padding-top: 32px; }
header {
height: 58px;
display: flex;
align-items: center;
gap: 28px;
padding: 0 clamp(16px, 4vw, 40px);
border-bottom: 1px solid var(--line);
background: #fff;
}
.brand { font-size: 16px; font-weight: 750; white-space: nowrap; }
nav { display: flex; height: 100%; gap: 6px; }
nav a {
display: grid;
place-items: center;
padding: 0 12px;
color: #3d4b45;
text-decoration: none;
border-bottom: 3px solid transparent;
}
nav a[aria-current="page"] { color: var(--brand-dark); border-color: var(--brand); font-weight: 700; }
.user { margin-left: auto; color: var(--muted); }
main { width: min(100% - 32px, 1120px); margin: 0 auto; padding: 24px 0 56px; }
.back { display: inline-block; margin-bottom: 12px; color: var(--brand-dark); font-weight: 700; }
.title-row { display: flex; justify-content: space-between; align-items: start; gap: 16px; margin-bottom: 16px; }
h1 { margin: 0 0 3px; font-size: 24px; line-height: 1.3; }
.task-id { color: var(--muted); }
.title-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 13px;
border: 1px solid #91a099;
border-radius: 5px;
color: #26352f;
background: #fff;
cursor: pointer;
}
.danger-button { color: #842828; border-color: #c68a8a; }
.status {
display: inline-flex;
min-height: 30px;
align-items: center;
padding: 4px 9px;
border: 1px solid #7db6a3;
border-radius: 5px;
color: #07513d;
background: #eaf7f1;
font-size: 12px;
font-weight: 750;
}
.status.pending { color: #6a4a0b; border-color: #d6b970; background: #fff8e5; }
.status.cancelled { color: #842828; border-color: #d99c9c; background: #fff1f1; }
.success-banner {
display: flex;
align-items: start;
gap: 12px;
margin-bottom: 16px;
padding: 15px 16px;
border: 1px solid #76af9c;
border-left: 5px solid var(--brand);
border-radius: 6px;
background: #eaf7f1;
}
.success-mark {
flex: 0 0 28px;
width: 28px;
height: 28px;
display: grid;
place-items: center;
border-radius: 50%;
color: #fff;
background: var(--brand);
font-weight: 800;
}
.success-banner h2 { margin: 0; font-size: 18px; color: #064c39; }
.success-banner p { margin: 2px 0 0; color: #315e4f; }
.layout { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(280px, .8fr); gap: 16px; }
.stack { display: grid; align-content: start; gap: 16px; }
.panel {
padding: 20px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
}
.panel h2 { margin: 0 0 14px; font-size: 16px; }
.section-note { margin: -8px 0 14px; color: var(--muted); font-size: 13px; }
dl { display: grid; grid-template-columns: 130px minmax(0, 1fr); gap: 10px 14px; margin: 0; }
dt { color: var(--muted); }
dd { margin: 0; overflow-wrap: anywhere; }
.requirement {
display: grid;
grid-template-columns: 150px minmax(0, 1fr);
gap: 16px;
}
.image-placeholder {
aspect-ratio: 4 / 3;
display: grid;
place-items: center;
padding: 12px;
border: 1px solid var(--line);
border-radius: 6px;
color: #405149;
background:
linear-gradient(135deg, transparent 48%, #c8d3ce 49%, #c8d3ce 51%, transparent 52%),
#e8eeeb;
text-align: center;
}
.description { margin: 0 0 14px; white-space: pre-line; }
.constraints { display: flex; flex-wrap: wrap; gap: 8px; }
.constraint { padding: 5px 8px; border: 1px solid #aab5b0; border-radius: 5px; background: #f8faf9; font-size: 12px; }
.derived { border-left: 4px solid var(--info); background: #f3f8fb; }
.derived-list { margin: 0; padding-left: 20px; }
.candidate-head { display: flex; justify-content: space-between; gap: 10px; align-items: start; }
.candidate-title { margin: 0; font-size: 15px; }
.price { color: #7d351b; font-size: 18px; font-weight: 750; }
.checks { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 12px 0; }
.check { padding: 8px; border: 1px solid var(--line); border-radius: 5px; }
.check strong { display: block; }
.pass { color: #07513d; }
.unknown { color: #785612; }
.evidence {
min-height: 150px;
display: grid;
place-items: center;
padding: 16px;
border: 1px solid var(--line);
border-radius: 6px;
background: #eef2f0;
text-align: center;
}
.evidence-error { color: #762222; background: #fff1f1; }
.evidence-error p { margin: 0 0 10px; }
.timeline { margin: 0; padding: 0 0 0 23px; }
.timeline li { position: relative; padding: 0 0 18px 9px; }
.timeline li:last-child { padding-bottom: 0; }
.timeline time { display: block; color: var(--muted); font-size: 12px; }
.notice {
display: none;
margin-bottom: 16px;
padding: 13px 14px;
border-left: 4px solid var(--danger);
background: #fff1f1;
color: #762222;
}
.page-state {
display: none;
min-height: 360px;
place-items: center;
padding: 30px 18px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
text-align: center;
}
.page-state.visible { display: grid; }
.page-state div { max-width: 520px; }
.page-state h2 { margin: 0 0 7px; font-size: 20px; }
.page-state p { margin: 0 0 16px; color: var(--muted); }
.demo-tools { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.demo-tools button { min-height: 36px; padding-block: 6px; font-size: 13px; }
.demo-tools button[aria-pressed="true"] { color: #fff; border-color: #394a43; background: #394a43; }
dialog {
width: min(100% - 32px, 450px);
padding: 0;
border: 1px solid var(--line);
border-radius: 8px;
color: var(--ink);
}
dialog::backdrop { background: rgba(13, 25, 20, .52); }
.dialog-body { padding: 22px; }
.dialog-body h2 { margin: 0 0 8px; font-size: 19px; }
.dialog-body p { margin: 0; color: var(--muted); }
.dialog-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 14px 22px; border-top: 1px solid var(--line); }
.confirm-cancel { color: #fff; border-color: var(--danger); background: var(--danger); font-weight: 700; }
@media (max-width: 850px) {
.layout { grid-template-columns: 1fr; }
}
@media (max-width: 620px) {
header { gap: 10px; }
.brand { font-size: 14px; }
nav a { padding-inline: 8px; }
nav a:last-child { display: none; }
.user { display: none; }
main { width: min(100% - 20px, 1120px); padding-top: 18px; }
.title-row { flex-direction: column; }
.title-actions { justify-content: flex-start; }
.requirement { grid-template-columns: 1fr; }
.image-placeholder { max-width: 220px; }
.checks { grid-template-columns: 1fr; }
.panel { padding: 16px 14px; }
dl { grid-template-columns: 1fr; gap: 2px; }
dd { margin-bottom: 8px; }
}
@media (max-width: 420px) {
.prototype { padding-inline: 2px; font-size: 10px; white-space: nowrap; }
}
</style>
</head>
<body>
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
<div class="app">
<header>
<div class="brand">采购任务管理</div>
<nav aria-label="主导航">
<a href="admin-task-list.html" aria-current="page">任务列表</a>
<a href="admin-task-create.html">新建任务</a>
</nav>
<div class="user">演示管理员</div>
</header>
<main>
<a class="back" href="admin-task-list.html">返回任务列表</a>
<div id="notice" class="notice" role="status" tabindex="-1"></div>
<div id="content">
<div class="title-row">
<div>
<h1>演示桌面收纳盒</h1>
<span class="task-id">任务编号 RB-DEMO-1004</span>
</div>
<div class="title-actions">
<span id="status" class="status">验证完成 · SUCCEEDED</span>
<button id="cancel-task" class="danger-button" type="button" hidden>取消任务</button>
</div>
</div>
<section id="success-banner" class="success-banner" aria-labelledby="success-title">
<div class="success-mark" aria-hidden="true">✓</div>
<div>
<h2 id="success-title">验证完成,未提交订单</h2>
<p>采购人员已完成人工确认;系统只回传验证结果,order_submitted=false。</p>
</div>
</section>
<div class="layout">
<div class="stack">
<section class="panel" aria-labelledby="requirement-title">
<h2 id="requirement-title">原始采购要求</h2>
<p class="section-note">以下内容来自管理员输入,不会被 AI 结果覆盖。</p>
<div class="requirement">
<div class="image-placeholder" role="img" aria-label="演示商品参考图片占位">脱敏参考图<br>RB-DEMO-1004</div>
<div>
<p class="description">桌面用分格收纳盒,浅灰色,塑料材质。
需要带可拆分隔板,外观接近参考图。</p>
<div class="constraints">
<span class="constraint">数量:2</span>
<span class="constraint">最高总预算:60.00 元</span>
<span class="constraint">关键属性:浅灰 / 分隔板</span>
</div>
</div>
</div>
</section>
<section class="panel derived" aria-labelledby="derived-title">
<h2 id="derived-title">AI 派生理解</h2>
<p class="section-note">辅助判断信息,必须经过结构与范围校验。</p>
<ul class="derived-list">
<li>检索词:桌面分格收纳盒、可拆隔板、浅灰</li>
<li>推断材质:PP 塑料(置信度:中)</li>
<li>不可忽略:数量 2、总价不超过 60.00 元</li>
</ul>
</section>
<section class="panel" aria-labelledby="candidate-title">
<div class="candidate-head">
<div>
<h2 id="candidate-title" class="candidate-title">人工确认的候选</h2>
<span class="task-id">候选 2 / 最多检查 5 个</span>
</div>
<div class="price">合计 49.80 元</div>
</div>
<p>演示多格桌面塑料收纳盒(浅灰,含可拆隔板)</p>
<div class="checks">
<div class="check"><strong class="pass">符合</strong>数量与总预算</div>
<div class="check"><strong class="pass">符合</strong>颜色与分隔板</div>
<div class="check"><strong class="pass">符合</strong>外观接近参考图</div>
<div class="check"><strong class="unknown">人工确认</strong>材质详情以页面为准</div>
</div>
<dl>
<dt>人员结论</dt><dd>接受候选</dd>
<dt>提交订单</dt><dd>否(系统硬停止点)</dd>
</dl>
</section>
<section class="panel" aria-labelledby="evidence-title">
<h2 id="evidence-title">执行证据</h2>
<div id="evidence-ok" class="evidence" role="img" aria-label="脱敏后的候选页面证据截图占位">
脱敏证据截图占位<br>不包含账号、地址或会话信息
</div>
<div id="evidence-error" class="evidence evidence-error" hidden>
<div>
<p><strong>证据图片加载失败</strong><br>其他任务信息仍可查看。</p>
<button id="retry-evidence" type="button">重试加载证据</button>
</div>
</div>
</section>
</div>
<aside class="stack" aria-label="任务状态与时间线">
<section class="panel" aria-labelledby="summary-title">
<h2 id="summary-title">任务摘要</h2>
<dl>
<dt>创建时间</dt><dd>2026-07-25 09:42</dd>
<dt>执行设备</dt><dd>演示设备 A</dd>
<dt>执行人员</dt><dd>演示采购员</dd>
<dt>最后更新</dt><dd>2026-07-25 10:24</dd>
<dt>结果同步</dt><dd>已同步</dd>
</dl>
</section>
<section class="panel" aria-labelledby="timeline-title">
<h2 id="timeline-title">执行时间线</h2>
<ol class="timeline">
<li><strong>验证结果已回传</strong><time datetime="2026-07-25T10:24:00+08:00">10:24</time></li>
<li><strong>采购人员接受候选</strong><time datetime="2026-07-25T10:22:00+08:00">10:22</time></li>
<li><strong>到达人工确认硬停止点</strong><time datetime="2026-07-25T10:18:00+08:00">10:18</time></li>
<li><strong>开始检查候选</strong><time datetime="2026-07-25T10:10:00+08:00">10:10</time></li>
<li><strong>演示设备 A 领取任务</strong><time datetime="2026-07-25T09:56:00+08:00">09:56</time></li>
<li><strong>任务已创建</strong><time datetime="2026-07-25T09:42:00+08:00">09:42</time></li>
</ol>
</section>
</aside>
</div>
</div>
<section id="page-state" class="page-state" aria-live="polite">
<div>
<h2 id="state-title"></h2>
<p id="state-copy"></p>
<button id="state-action" type="button"></button>
</div>
</section>
<div class="demo-tools" aria-label="原型页面状态">
<button type="button" data-state="success" aria-pressed="true">验证完成</button>
<button type="button" data-state="pending" aria-pressed="false">待领取可取消</button>
<button type="button" data-state="evidence" aria-pressed="false">证据失败</button>
<button type="button" data-state="loading" aria-pressed="false">加载中</button>
<button type="button" data-state="failed" aria-pressed="false">加载失败</button>
<button type="button" data-state="forbidden" aria-pressed="false">权限不足</button>
<button type="button" data-state="missing" aria-pressed="false">任务不存在</button>
</div>
</main>
</div>
<dialog id="cancel-dialog" aria-labelledby="cancel-title">
<div class="dialog-body">
<h2 id="cancel-title">确认取消任务?</h2>
<p>任务取消后不会自动恢复,采购执行员将不能再领取此任务。</p>
</div>
<div class="dialog-actions">
<button id="keep-task" type="button">保留任务</button>
<button id="confirm-cancel" class="confirm-cancel" type="button">确认取消</button>
</div>
</dialog>
<script>
const content = document.querySelector('#content');
const pageState = document.querySelector('#page-state');
const successBanner = document.querySelector('#success-banner');
const statusBadge = document.querySelector('#status');
const cancelTask = document.querySelector('#cancel-task');
const evidenceOk = document.querySelector('#evidence-ok');
const evidenceError = document.querySelector('#evidence-error');
const notice = document.querySelector('#notice');
const cancelDialog = document.querySelector('#cancel-dialog');
const stateTitle = document.querySelector('#state-title');
const stateCopy = document.querySelector('#state-copy');
const stateAction = document.querySelector('#state-action');
const pageStates = {
loading: ['正在加载任务详情', '页面框架和返回入口保持可用,请稍候。', '取消加载'],
failed: ['任务详情加载失败', '没有可用的缓存内容,请检查网络后重试。', '重试'],
forbidden: ['没有查看权限', '当前账号无权查看此任务,页面不会泄露任务标题、图片或状态。', '返回任务列表'],
missing: ['任务不存在', '该编号不存在或已不可用。页面不会显示任何任务内容。', '返回任务列表']
};
function setPressed(state) {
document.querySelectorAll('.demo-tools button').forEach((button) => {
button.setAttribute('aria-pressed', String(button.dataset.state === state));
});
}
function setContentState(state) {
setPressed(state);
notice.style.display = 'none';
content.style.display = 'block';
pageState.classList.remove('visible');
evidenceOk.hidden = false;
evidenceError.hidden = true;
document.querySelector('#retry-evidence').disabled = false;
document.querySelector('#retry-evidence').textContent = '重试加载证据';
successBanner.hidden = state === 'pending';
cancelTask.hidden = state !== 'pending';
statusBadge.classList.remove('cancelled');
statusBadge.classList.toggle('pending', state === 'pending');
statusBadge.textContent = state === 'pending' ? '待领取 · PENDING' : '验证完成 · SUCCEEDED';
if (state === 'evidence') {
evidenceOk.hidden = true;
evidenceError.hidden = false;
}
}
function setPageState(state) {
setPressed(state);
content.style.display = 'none';
pageState.classList.add('visible');
const values = pageStates[state];
stateTitle.textContent = values[0];
stateCopy.textContent = values[1];
stateAction.textContent = values[2];
}
document.querySelector('.demo-tools').addEventListener('click', (event) => {
const state = event.target.dataset.state;
if (!state) return;
if (['success', 'pending', 'evidence'].includes(state)) setContentState(state);
else setPageState(state);
});
stateAction.addEventListener('click', () => {
if (stateAction.textContent === '返回任务列表') window.location.href = 'admin-task-list.html';
else setContentState('success');
});
cancelTask.addEventListener('click', () => {
cancelDialog.showModal();
document.querySelector('#keep-task').focus();
});
document.querySelector('#keep-task').addEventListener('click', () => cancelDialog.close());
document.querySelector('#confirm-cancel').addEventListener('click', () => {
cancelDialog.close();
cancelTask.hidden = true;
statusBadge.textContent = '已取消 · CANCELLED';
statusBadge.classList.remove('pending');
statusBadge.classList.add('cancelled');
notice.textContent = '任务已取消,不会自动恢复。原型不发送任何请求。';
notice.style.display = 'block';
notice.focus();
});
document.querySelector('#retry-evidence').addEventListener('click', (event) => {
event.currentTarget.disabled = true;
event.currentTarget.textContent = '正在重试…';
window.setTimeout(() => {
evidenceOk.hidden = false;
evidenceError.hidden = true;
setPressed('success');
}, 600);
});
</script>
</body>
</html>