487 lines
20 KiB
HTML
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>
|