feat(prototype): add MVP web interaction flows
This commit is contained in:
@@ -0,0 +1,247 @@
|
|||||||
|
<!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 {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg: #f4f7fb;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--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 12px 30px rgba(23, 32, 51, .10);
|
||||||
|
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.55; }
|
||||||
|
a { color: var(--primary); text-underline-offset: 3px; }
|
||||||
|
button, input, select { font: inherit; }
|
||||||
|
button, a.button { min-height: 44px; }
|
||||||
|
button { cursor: pointer; }
|
||||||
|
button:disabled { cursor: not-allowed; opacity: .62; }
|
||||||
|
:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }
|
||||||
|
.skip-link { position: fixed; z-index: 1000; top: 8px; left: 8px; padding: 10px 14px; background: var(--text); color: #fff; transform: translateY(-150%); }
|
||||||
|
.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; }
|
||||||
|
.site-header { min-height: 64px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 24px; border-bottom: 1px solid var(--border); background: var(--surface); }
|
||||||
|
.brand { display: flex; align-items: center; gap: 10px; color: var(--text); text-decoration: none; font-weight: 700; }
|
||||||
|
.brand-mark { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 8px; color: #fff; background: var(--primary); font-size: .82rem; }
|
||||||
|
.header-meta { color: var(--muted); font-size: .88rem; }
|
||||||
|
main { min-height: calc(100dvh - 103px); display: grid; place-items: center; padding: 32px 16px 48px; }
|
||||||
|
.login-layout { width: min(100%, 960px); display: grid; grid-template-columns: minmax(0, 1fr) minmax(360px, 440px); gap: 32px; align-items: center; }
|
||||||
|
.intro { max-width: 520px; }
|
||||||
|
.eyebrow { margin: 0 0 8px; color: var(--primary); font-size: .83rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
|
||||||
|
h1 { margin: 0; font-size: clamp(1.75rem, 4vw, 2.5rem); line-height: 1.2; }
|
||||||
|
.lead { margin: 16px 0 24px; color: var(--muted); }
|
||||||
|
.guardrail { display: grid; gap: 6px; padding: 16px; border: 1px solid #b9cffc; border-radius: 10px; background: var(--primary-soft); }
|
||||||
|
.guardrail strong { color: #163c8c; }
|
||||||
|
.guardrail span { color: #29466f; font-size: .91rem; }
|
||||||
|
.card { padding: 24px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); }
|
||||||
|
.card h2 { margin: 0; font-size: 1.35rem; }
|
||||||
|
.card-subtitle { margin: 6px 0 18px; color: var(--muted); font-size: .92rem; }
|
||||||
|
.notice { margin: 0 0 18px; padding: 12px 14px; border-left: 4px solid var(--primary); border-radius: 6px; background: var(--primary-soft); color: #29466f; font-size: .9rem; }
|
||||||
|
.notice.warning { border-left-color: #b7751b; background: var(--warning-soft); color: var(--warning); }
|
||||||
|
.field { margin-bottom: 16px; }
|
||||||
|
label { display: block; margin-bottom: 6px; font-weight: 650; }
|
||||||
|
input { width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid #9ba9bc; border-radius: 8px; color: var(--text); background: #fff; }
|
||||||
|
input[aria-invalid="true"] { border-color: var(--danger); box-shadow: 0 0 0 1px var(--danger); }
|
||||||
|
.hint { margin: 5px 0 0; color: var(--muted); font-size: .82rem; }
|
||||||
|
.field-error { min-height: 1.3em; margin: 5px 0 0; color: var(--danger); font-size: .85rem; font-weight: 600; }
|
||||||
|
.actions { display: grid; gap: 10px; margin-top: 20px; }
|
||||||
|
.button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 16px; border: 1px solid transparent; border-radius: 8px; text-decoration: none; font-weight: 700; }
|
||||||
|
.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 { background: var(--surface-subtle); }
|
||||||
|
.spinner { width: 16px; height: 16px; border: 2px solid rgba(255,255,255,.5); border-top-color: #fff; border-radius: 50%; animation: spin .8s linear infinite; }
|
||||||
|
.feedback { display: none; margin-top: 16px; padding: 12px 14px; border-radius: 8px; }
|
||||||
|
.feedback.show { display: block; }
|
||||||
|
.feedback.error { color: var(--danger); border: 1px solid #f3b7b2; background: var(--danger-soft); }
|
||||||
|
.feedback.success { color: var(--success); border: 1px solid #a6e3c4; background: var(--success-soft); }
|
||||||
|
.prototype-controls { margin-top: 18px; padding-top: 16px; border-top: 1px dashed var(--border); }
|
||||||
|
.prototype-controls strong { display: block; margin-bottom: 8px; font-size: .82rem; color: var(--muted); }
|
||||||
|
.control-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.control-row button { min-height: 36px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 7px; color: #33415b; background: var(--surface-subtle); font-size: .82rem; }
|
||||||
|
.control-row button:hover { border-color: #8aaef5; background: var(--primary-soft); }
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.site-header { padding: 0 16px; }
|
||||||
|
.header-meta { display: none; }
|
||||||
|
main { place-items: start center; padding-top: 24px; }
|
||||||
|
.login-layout { grid-template-columns: 1fr; gap: 20px; }
|
||||||
|
.intro { max-width: none; }
|
||||||
|
.card { padding: 20px 16px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.prototype-banner { position: static; padding-inline: 8px; }
|
||||||
|
main { padding-inline: 12px; }
|
||||||
|
.guardrail { padding: 12px; }
|
||||||
|
}
|
||||||
|
@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="site-header">
|
||||||
|
<a class="brand" href="web-task-workbench.html" aria-label="cmbuyer 原型工作台">
|
||||||
|
<span class="brand-mark" aria-hidden="true">CM</span>
|
||||||
|
<span>cmbuyer</span>
|
||||||
|
</a>
|
||||||
|
<span class="header-meta">采购管理端 · 原型环境</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main">
|
||||||
|
<div class="login-layout">
|
||||||
|
<section class="intro" aria-labelledby="page-title">
|
||||||
|
<p class="eyebrow">采购任务管理</p>
|
||||||
|
<h1 id="page-title">登录后处理需要人决定的任务</h1>
|
||||||
|
<p class="lead">网页端负责建单、查看证据与人工决策;桌面端负责驱动手机执行。两端身份隔离。</p>
|
||||||
|
<div class="guardrail">
|
||||||
|
<strong>系统只创建待付款订单,不会自动付款</strong>
|
||||||
|
<span>付款始终由人在拼多多核对后完成。任何验证码、风控或结果不明都会停止并转人工。</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card" aria-labelledby="login-title">
|
||||||
|
<h2 id="login-title">管理端登录</h2>
|
||||||
|
<p class="card-subtitle">请使用采购管理员账号。原型不会保存或发送输入。</p>
|
||||||
|
<p class="notice" id="return-notice">登录成功后将返回站内页面:<strong>/tasks/T-20260803-018</strong></p>
|
||||||
|
<p class="notice warning" id="session-notice" hidden>会话已过期。请重新登录,完成后仍返回原任务。</p>
|
||||||
|
|
||||||
|
<form id="login-form" novalidate aria-describedby="form-feedback">
|
||||||
|
<div class="field">
|
||||||
|
<label for="username">账号</label>
|
||||||
|
<input id="username" name="username" type="text" autocomplete="off" value="caigou_admin" aria-describedby="username-hint username-error">
|
||||||
|
<p class="hint" id="username-hint">仅允许管理端账号登录。</p>
|
||||||
|
<p class="field-error" id="username-error"></p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<input id="password" name="password" type="password" autocomplete="off" value="prototype-only" aria-describedby="password-hint password-error">
|
||||||
|
<p class="hint" id="password-hint">原型数据可随时复位,不连接真实账号。</p>
|
||||||
|
<p class="field-error" id="password-error"></p>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="button button-primary" id="submit-button" type="submit">登录并继续</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="feedback" id="form-feedback" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<aside class="prototype-controls" aria-label="原型状态控制">
|
||||||
|
<strong>原型状态(不发送请求)</strong>
|
||||||
|
<div class="control-row">
|
||||||
|
<button type="button" data-demo="default">默认</button>
|
||||||
|
<button type="button" data-demo="invalid">校验错误</button>
|
||||||
|
<button type="button" data-demo="loading">提交中</button>
|
||||||
|
<button type="button" data-demo="expired">会话过期</button>
|
||||||
|
<button type="button" data-demo="success">登录成功</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
const form = document.querySelector('#login-form');
|
||||||
|
const username = document.querySelector('#username');
|
||||||
|
const password = document.querySelector('#password');
|
||||||
|
const usernameError = document.querySelector('#username-error');
|
||||||
|
const passwordError = document.querySelector('#password-error');
|
||||||
|
const submitButton = document.querySelector('#submit-button');
|
||||||
|
const feedback = document.querySelector('#form-feedback');
|
||||||
|
const sessionNotice = document.querySelector('#session-notice');
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
username.value = 'caigou_admin';
|
||||||
|
password.value = 'prototype-only';
|
||||||
|
username.removeAttribute('aria-invalid');
|
||||||
|
password.removeAttribute('aria-invalid');
|
||||||
|
usernameError.textContent = '';
|
||||||
|
passwordError.textContent = '';
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.innerHTML = '登录并继续';
|
||||||
|
feedback.className = 'feedback';
|
||||||
|
feedback.innerHTML = '';
|
||||||
|
sessionNotice.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showInvalid() {
|
||||||
|
reset();
|
||||||
|
username.value = '';
|
||||||
|
password.value = '';
|
||||||
|
username.setAttribute('aria-invalid', 'true');
|
||||||
|
password.setAttribute('aria-invalid', 'true');
|
||||||
|
usernameError.textContent = '请输入管理端账号。';
|
||||||
|
passwordError.textContent = '请输入密码。';
|
||||||
|
feedback.className = 'feedback error show';
|
||||||
|
feedback.textContent = '有 2 项需要修正,请从账号开始检查。';
|
||||||
|
username.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoading() {
|
||||||
|
reset();
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.innerHTML = '<span class="spinner" aria-hidden="true"></span>正在登录…';
|
||||||
|
feedback.className = 'feedback show';
|
||||||
|
feedback.textContent = '正在验证管理端会话,请勿重复提交。';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showExpired() {
|
||||||
|
reset();
|
||||||
|
sessionNotice.hidden = false;
|
||||||
|
feedback.className = 'feedback error show';
|
||||||
|
feedback.textContent = '原会话已失效,当前输入尚未提交。重新登录即可回到原任务。';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccess() {
|
||||||
|
reset();
|
||||||
|
submitButton.disabled = true;
|
||||||
|
feedback.className = 'feedback success show';
|
||||||
|
feedback.innerHTML = '登录成功。<a href="web-task-detail.html">进入原任务详情</a>,或<a href="web-task-workbench.html">返回工作台</a>。';
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!username.value.trim() || !password.value) {
|
||||||
|
showInvalid();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showLoading();
|
||||||
|
window.setTimeout(showSuccess, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-demo]').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const demo = button.dataset.demo;
|
||||||
|
if (demo === 'invalid') showInvalid();
|
||||||
|
else if (demo === 'loading') showLoading();
|
||||||
|
else if (demo === 'expired') showExpired();
|
||||||
|
else if (demo === 'success') showSuccess();
|
||||||
|
else reset();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
<!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;
|
||||||
|
--focus: #ffbf47; --shadow: 0 8px 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; }
|
||||||
|
a { color: var(--primary); text-underline-offset: 3px; }
|
||||||
|
button, input, select, textarea { font: inherit; }
|
||||||
|
button, .button, input, select { min-height: 44px; }
|
||||||
|
button { cursor: pointer; }
|
||||||
|
button:disabled { cursor: not-allowed; opacity: .58; }
|
||||||
|
: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 - 1280px) / 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 { display: flex; align-items: stretch; align-self: stretch; gap: 4px; }
|
||||||
|
nav a { display: flex; align-items: center; min-height: 44px; padding: 0 12px; color: #3d4b63; text-decoration: none; font-weight: 650; border-bottom: 3px solid transparent; }
|
||||||
|
nav 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, 1120px); margin: 0 auto; padding: 28px 0 64px; }
|
||||||
|
.breadcrumb { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; color: var(--muted); font-size: .87rem; }
|
||||||
|
.page-head { display: flex; justify-content: space-between; gap: 24px; align-items: end; margin-bottom: 20px; }
|
||||||
|
h1 { margin: 0; font-size: clamp(1.6rem, 3vw, 2rem); line-height: 1.25; }
|
||||||
|
.subtitle { margin: 6px 0 0; color: var(--muted); }
|
||||||
|
.safety-chip { flex: 0 0 auto; padding: 9px 12px; border: 1px solid #b9cffc; border-radius: 999px; color: #163c8c; background: var(--primary-soft); font-size: .85rem; font-weight: 700; }
|
||||||
|
.layout { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 20px; align-items: start; }
|
||||||
|
.panel { border: 1px solid var(--border); border-radius: 12px; background: var(--surface); box-shadow: var(--shadow); }
|
||||||
|
.panel-header { padding: 18px 20px 14px; border-bottom: 1px solid var(--border); }
|
||||||
|
.panel-header h2 { margin: 0; font-size: 1.15rem; }
|
||||||
|
.panel-header p { margin: 5px 0 0; color: var(--muted); font-size: .89rem; }
|
||||||
|
form { padding: 20px; }
|
||||||
|
fieldset { min-width: 0; margin: 0 0 22px; padding: 0; border: 0; }
|
||||||
|
legend { width: 100%; margin-bottom: 12px; padding: 0 0 7px; border-bottom: 1px solid #e6ebf2; font-size: .95rem; font-weight: 750; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
|
||||||
|
.field.full { grid-column: 1 / -1; }
|
||||||
|
label { display: block; margin-bottom: 6px; font-weight: 650; }
|
||||||
|
.required { color: var(--danger); }
|
||||||
|
input { width: 100%; padding: 9px 11px; border: 1px solid #91a0b4; border-radius: 8px; color: var(--text); background: #fff; }
|
||||||
|
input[aria-invalid="true"] { border-color: var(--danger); box-shadow: 0 0 0 1px var(--danger); }
|
||||||
|
.input-prefix { position: relative; }
|
||||||
|
.input-prefix span { position: absolute; top: 50%; left: 12px; transform: translateY(-50%); color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||||
|
.input-prefix input { padding-left: 30px; font-variant-numeric: tabular-nums; }
|
||||||
|
.hint, .error { margin: 5px 0 0; font-size: .82rem; }
|
||||||
|
.hint { color: var(--muted); }
|
||||||
|
.error { min-height: 1.25em; color: var(--danger); font-weight: 600; }
|
||||||
|
.error-summary { display: none; margin: 0 0 18px; padding: 12px 14px; border: 1px solid #f3b7b2; border-radius: 8px; color: var(--danger); background: var(--danger-soft); }
|
||||||
|
.error-summary.show { display: block; }
|
||||||
|
.form-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; padding-top: 18px; border-top: 1px solid var(--border); }
|
||||||
|
.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:not(:disabled) { background: var(--primary-hover); }
|
||||||
|
.button-secondary { color: var(--text); border-color: var(--border); background: var(--surface); }
|
||||||
|
.button-secondary:hover { background: var(--surface-subtle); }
|
||||||
|
.spinner { width: 16px; height: 16px; border: 2px solid rgba(255,255,255,.45); border-top-color: #fff; border-radius: 50%; animation: spin .8s linear infinite; }
|
||||||
|
.side-panel { position: sticky; top: 116px; }
|
||||||
|
.side-panel section { padding: 16px; border-bottom: 1px solid var(--border); }
|
||||||
|
.side-panel section:last-child { border-bottom: 0; }
|
||||||
|
.side-panel h2 { margin: 0 0 8px; font-size: 1rem; }
|
||||||
|
.side-panel p { margin: 0; color: var(--muted); font-size: .87rem; }
|
||||||
|
.steps { margin: 10px 0 0; padding-left: 22px; color: #33415b; font-size: .87rem; }
|
||||||
|
.steps li + li { margin-top: 7px; }
|
||||||
|
.prototype-controls strong { display: block; margin-bottom: 8px; color: var(--muted); font-size: .8rem; }
|
||||||
|
.control-row { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||||
|
.control-row button { min-height: 34px; padding: 5px 9px; border: 1px solid var(--border); border-radius: 7px; color: #33415b; background: var(--surface-subtle); font-size: .78rem; }
|
||||||
|
.status { display: none; margin-bottom: 20px; padding: 16px; border-radius: 10px; }
|
||||||
|
.status.show { display: block; }
|
||||||
|
.status.success { color: var(--success); border: 1px solid #a6e3c4; background: var(--success-soft); }
|
||||||
|
.status.error { color: var(--danger); border: 1px solid #f3b7b2; background: var(--danger-soft); }
|
||||||
|
.status h2 { margin: 0 0 5px; font-size: 1rem; }
|
||||||
|
.status p { margin: 0; }
|
||||||
|
.status .button { margin-top: 12px; }
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.layout { grid-template-columns: 1fr; }
|
||||||
|
.side-panel { position: static; display: grid; grid-template-columns: 1fr 1fr; }
|
||||||
|
.side-panel section { border-bottom: 0; }
|
||||||
|
.side-panel section + section { border-left: 1px solid var(--border); }
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.app-header { top: 33px; padding-inline: 16px; gap: 12px; }
|
||||||
|
nav a { padding-inline: 8px; }
|
||||||
|
.header-account { display: none; }
|
||||||
|
main { width: min(100% - 24px, 1120px); padding-top: 20px; }
|
||||||
|
.page-head { align-items: start; flex-direction: column; gap: 10px; }
|
||||||
|
.grid { grid-template-columns: 1fr; gap: 12px; }
|
||||||
|
.field.full { grid-column: auto; }
|
||||||
|
form { padding: 16px; }
|
||||||
|
.form-actions { flex-direction: column-reverse; }
|
||||||
|
.button { width: 100%; }
|
||||||
|
.side-panel { grid-template-columns: 1fr; }
|
||||||
|
.side-panel section + section { border-left: 0; border-top: 1px solid var(--border); }
|
||||||
|
}
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
.prototype-banner { position: static; padding-inline: 8px; }
|
||||||
|
.app-header { top: 0; }
|
||||||
|
nav a:first-child { display: none; }
|
||||||
|
.brand span: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 aria-label="主导航">
|
||||||
|
<a href="web-task-workbench.html">任务工作台</a>
|
||||||
|
<a href="web-task-create.html" aria-current="page">新建任务</a>
|
||||||
|
</nav>
|
||||||
|
<span class="header-account">采购管理员 · caigou_admin</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main">
|
||||||
|
<nav class="breadcrumb" aria-label="面包屑"><a href="web-task-workbench.html">任务工作台</a><span aria-hidden="true">/</span><span>新建任务</span></nav>
|
||||||
|
<header class="page-head">
|
||||||
|
<div><h1>新建采购任务</h1><p class="subtitle">把商品链接与精确规格交给机器执行,一次只建一条。</p></div>
|
||||||
|
<span class="safety-chip">价格上限是必填资金边界</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="status" id="page-status" aria-live="polite"></section>
|
||||||
|
|
||||||
|
<div class="layout">
|
||||||
|
<section class="panel" aria-labelledby="form-title">
|
||||||
|
<header class="panel-header"><h2 id="form-title">采购要求</h2><p>所有内容均为可复位假数据,本页不会提交到服务器。</p></header>
|
||||||
|
<form id="task-form" novalidate>
|
||||||
|
<div class="error-summary" id="error-summary" role="alert" tabindex="-1"></div>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>任务与商品</legend>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="field full">
|
||||||
|
<label for="title">任务名称 <span class="required" aria-hidden="true">*</span></label>
|
||||||
|
<input id="title" name="title" value="8 月仓库工作服补货" aria-describedby="title-hint title-error">
|
||||||
|
<p class="hint" id="title-hint">用于工作台查询,不会发送给拼多多。</p>
|
||||||
|
<p class="error" id="title-error"></p>
|
||||||
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<label for="product-url">拼多多商品链接 <span class="required" aria-hidden="true">*</span></label>
|
||||||
|
<input id="product-url" name="product-url" type="url" inputmode="url" value="https://mobile.example.invalid/goods.html?goods_id=123456789" aria-describedby="url-hint url-error">
|
||||||
|
<p class="hint" id="url-hint">MVP 只接受任务自带商品链接;无法解析商品标识时保留全部输入。</p>
|
||||||
|
<p class="error" id="url-error"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>规格与资金边界</legend>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="field">
|
||||||
|
<label for="color">颜色分类 <span class="required" aria-hidden="true">*</span></label>
|
||||||
|
<input id="color" name="color" value="白色" aria-describedby="color-hint color-error">
|
||||||
|
<p class="hint" id="color-hint">按维度精确填写,不写“差不多”的替代项。</p>
|
||||||
|
<p class="error" id="color-error"></p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="size">尺码 <span class="required" aria-hidden="true">*</span></label>
|
||||||
|
<input id="size" name="size" value="XL" aria-describedby="size-hint size-error">
|
||||||
|
<p class="hint" id="size-hint">例如 XL;执行时必须等值匹配。</p>
|
||||||
|
<p class="error" id="size-error"></p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="quantity">数量 <span class="required" aria-hidden="true">*</span></label>
|
||||||
|
<input id="quantity" name="quantity" type="number" inputmode="numeric" min="1" step="1" value="2" aria-describedby="quantity-hint quantity-error">
|
||||||
|
<p class="hint" id="quantity-hint">必须是大于 0 的整数。</p>
|
||||||
|
<p class="error" id="quantity-error"></p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="max-price">价格上限(总额) <span class="required" aria-hidden="true">*</span></label>
|
||||||
|
<div class="input-prefix"><span aria-hidden="true">¥</span><input id="max-price" name="max-price" type="text" inputmode="decimal" value="80.00" aria-describedby="price-hint price-error"></div>
|
||||||
|
<p class="hint" id="price-hint">十进制金额;留空或格式错误都不能建单。</p>
|
||||||
|
<p class="error" id="price-error"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a class="button button-secondary" href="web-task-workbench.html">取消并返回</a>
|
||||||
|
<button class="button button-primary" id="submit-button" type="submit">创建任务</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="panel side-panel" aria-label="流程与原型控制">
|
||||||
|
<section>
|
||||||
|
<h2>创建后发生什么</h2>
|
||||||
|
<ol class="steps">
|
||||||
|
<li>任务进入「机器在跑」。</li>
|
||||||
|
<li>第一趟只试选、读价、截图,然后退出商品。</li>
|
||||||
|
<li>你看过证据后再决定是否授权。</li>
|
||||||
|
</ol>
|
||||||
|
<p style="margin-top:10px"><strong>系统不会自动付款。</strong></p>
|
||||||
|
</section>
|
||||||
|
<section class="prototype-controls">
|
||||||
|
<h2>原型状态</h2>
|
||||||
|
<strong>只切换假数据,不发送请求</strong>
|
||||||
|
<div class="control-row">
|
||||||
|
<button type="button" data-demo="reset">复位</button>
|
||||||
|
<button type="button" data-demo="invalid">必填错误</button>
|
||||||
|
<button type="button" data-demo="link-error">链接解析失败</button>
|
||||||
|
<button type="button" data-demo="loading">提交中</button>
|
||||||
|
<button type="button" data-demo="success">创建成功</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
const form = document.querySelector('#task-form');
|
||||||
|
const submitButton = document.querySelector('#submit-button');
|
||||||
|
const summary = document.querySelector('#error-summary');
|
||||||
|
const status = document.querySelector('#page-status');
|
||||||
|
const fields = {
|
||||||
|
title: document.querySelector('#title'),
|
||||||
|
url: document.querySelector('#product-url'),
|
||||||
|
color: document.querySelector('#color'),
|
||||||
|
size: document.querySelector('#size'),
|
||||||
|
quantity: document.querySelector('#quantity'),
|
||||||
|
price: document.querySelector('#max-price')
|
||||||
|
};
|
||||||
|
const defaults = {
|
||||||
|
title: '8 月仓库工作服补货',
|
||||||
|
url: 'https://mobile.example.invalid/goods.html?goods_id=123456789',
|
||||||
|
color: '白色', size: 'XL', quantity: '2', price: '80.00'
|
||||||
|
};
|
||||||
|
const errors = {
|
||||||
|
title: document.querySelector('#title-error'), url: document.querySelector('#url-error'),
|
||||||
|
color: document.querySelector('#color-error'), size: document.querySelector('#size-error'),
|
||||||
|
quantity: document.querySelector('#quantity-error'), price: document.querySelector('#price-error')
|
||||||
|
};
|
||||||
|
|
||||||
|
function clearErrors() {
|
||||||
|
Object.values(fields).forEach((field) => field.removeAttribute('aria-invalid'));
|
||||||
|
Object.values(errors).forEach((node) => { node.textContent = ''; });
|
||||||
|
summary.classList.remove('show');
|
||||||
|
summary.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showErrors(messages) {
|
||||||
|
clearErrors();
|
||||||
|
const names = Object.keys(messages);
|
||||||
|
names.forEach((name) => {
|
||||||
|
fields[name].setAttribute('aria-invalid', 'true');
|
||||||
|
errors[name].textContent = messages[name];
|
||||||
|
});
|
||||||
|
summary.innerHTML = `<strong>无法创建任务:</strong>请修正 ${names.length} 个字段。`;
|
||||||
|
summary.classList.add('show');
|
||||||
|
summary.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setReady() {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.innerHTML = '创建任务';
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
Object.entries(defaults).forEach(([name, value]) => { fields[name].value = value; });
|
||||||
|
clearErrors();
|
||||||
|
status.className = 'status';
|
||||||
|
status.innerHTML = '';
|
||||||
|
form.hidden = false;
|
||||||
|
setReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showInvalid() {
|
||||||
|
reset();
|
||||||
|
fields.title.value = '';
|
||||||
|
fields.color.value = '';
|
||||||
|
fields.quantity.value = '0';
|
||||||
|
fields.price.value = '';
|
||||||
|
showErrors({ title: '请输入任务名称。', color: '请输入颜色分类。', quantity: '数量必须是大于 0 的整数。', price: '价格上限是资金边界,不能为空。' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLinkError() {
|
||||||
|
clearErrors();
|
||||||
|
fields.url.value = 'https://example.invalid/shared-link';
|
||||||
|
showErrors({ url: '无法从此链接解析商品标识。请粘贴拼多多商品完整链接后重试;其他输入已保留。' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoading() {
|
||||||
|
clearErrors();
|
||||||
|
status.className = 'status';
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.innerHTML = '<span class="spinner" aria-hidden="true"></span>正在创建…';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccess() {
|
||||||
|
clearErrors();
|
||||||
|
setReady();
|
||||||
|
form.hidden = true;
|
||||||
|
status.className = 'status success show';
|
||||||
|
status.innerHTML = '<h2>任务已创建</h2><p>任务编号 <strong>T-20260803-019</strong>,已进入「机器在跑」等待领取。系统尚未进行任何拼多多操作。</p><a class="button button-primary" href="web-task-workbench.html">查看任务工作台</a>';
|
||||||
|
status.focus?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
const messages = {};
|
||||||
|
if (!fields.title.value.trim()) messages.title = '请输入任务名称。';
|
||||||
|
if (!fields.url.value.includes('goods_id=')) messages.url = '链接中没有可解析的商品标识,请检查后重试。';
|
||||||
|
if (!fields.color.value.trim()) messages.color = '请输入颜色分类。';
|
||||||
|
if (!fields.size.value.trim()) messages.size = '请输入尺码。';
|
||||||
|
if (!/^\d+$/.test(fields.quantity.value) || Number(fields.quantity.value) < 1) messages.quantity = '数量必须是大于 0 的整数。';
|
||||||
|
if (!/^\d+(\.\d{1,2})?$/.test(fields.price.value) || Number(fields.price.value) <= 0) messages.price = '请输入大于 0、最多两位小数的总额上限。';
|
||||||
|
if (Object.keys(messages).length) { showErrors(messages); return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!validate()) return;
|
||||||
|
showLoading();
|
||||||
|
window.setTimeout(showSuccess, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-demo]').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const demo = button.dataset.demo;
|
||||||
|
if (demo === 'invalid') showInvalid();
|
||||||
|
else if (demo === 'link-error') { reset(); showLinkError(); }
|
||||||
|
else if (demo === 'loading') { reset(); showLoading(); }
|
||||||
|
else if (demo === 'success') { reset(); showSuccess(); }
|
||||||
|
else reset();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
<!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; --subtle:#f8fafc; --text:#172033; --muted:#526079; --border:#cfd8e6;
|
||||||
|
--primary:#155eef; --primary-hover:#0b4ed1; --primary-soft:#eaf1ff; --danger:#b42318; --danger-hover:#8f1d14;
|
||||||
|
--danger-soft:#fef3f2; --success:#067647; --success-soft:#ecfdf3; --warning:#8a4b0f; --warning-soft:#fff7e8;
|
||||||
|
--focus:#ffbf47; --shadow:0 6px 20px 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; }
|
||||||
|
a { color:var(--primary); text-underline-offset:3px; }
|
||||||
|
button,input,select,textarea { 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: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 - 1280px)/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,1200px); margin:0 auto; padding:24px 0 64px; }
|
||||||
|
.breadcrumb { display:flex; flex-wrap:wrap; gap:8px; margin-bottom:11px; color:var(--muted); font-size:.86rem; }
|
||||||
|
.page-head { display:flex; align-items:start; justify-content:space-between; gap:20px; margin-bottom:16px; }
|
||||||
|
h1 { margin:0; font-size:clamp(1.5rem,3vw,1.95rem); line-height:1.25; }
|
||||||
|
.task-id { margin:5px 0 0; color:var(--muted); font-size:.87rem; font-variant-numeric:tabular-nums; }
|
||||||
|
.state-chip { display:inline-flex; align-items:center; gap:7px; padding:7px 10px; border:1px solid #b9cffc; border-radius:999px; color:#163c8c; background:var(--primary-soft); font-size:.82rem; font-weight:750; }
|
||||||
|
.state-chip::before { content:""; width:8px; height:8px; border-radius:50%; background:currentColor; }
|
||||||
|
.safety-bar { display:grid; grid-template-columns:auto 1fr; gap:10px; align-items:start; margin-bottom:16px; padding:13px 15px; border:1px solid #b9cffc; border-radius:10px; background:var(--primary-soft); color:#29466f; }
|
||||||
|
.safety-bar strong { color:#163c8c; }
|
||||||
|
.layout { display:grid; grid-template-columns:minmax(0,1fr) 300px; gap:18px; align-items:start; }
|
||||||
|
.panel { border:1px solid var(--border); border-radius:12px; background:var(--surface); box-shadow:var(--shadow); }
|
||||||
|
.main-panel { min-width:0; }
|
||||||
|
.panel-head { display:flex; align-items:start; justify-content:space-between; gap:12px; padding:18px 20px 14px; border-bottom:1px solid var(--border); }
|
||||||
|
.panel-head h2 { margin:0; font-size:1.18rem; }
|
||||||
|
.panel-head p { margin:5px 0 0; color:var(--muted); font-size:.88rem; }
|
||||||
|
.panel-body { padding:20px; }
|
||||||
|
.state-panel[hidden] { display:none; }
|
||||||
|
.notice { margin:0 0 16px; padding:12px 14px; border-left:4px solid var(--primary); border-radius:6px; color:#29466f; background:var(--primary-soft); }
|
||||||
|
.notice.warning { border-left-color:#b7751b; color:var(--warning); background:var(--warning-soft); }
|
||||||
|
.notice.danger { border-left-color:var(--danger); color:var(--danger); background:var(--danger-soft); }
|
||||||
|
.notice.success { border-left-color:var(--success); color:var(--success); background:var(--success-soft); }
|
||||||
|
.notice strong { display:block; margin-bottom:2px; }
|
||||||
|
.comparison { width:100%; border-collapse:collapse; margin-bottom:16px; table-layout:fixed; }
|
||||||
|
.comparison th,.comparison td { padding:10px 9px; border-bottom:1px solid #e4e9f0; text-align:left; vertical-align:top; overflow-wrap:anywhere; }
|
||||||
|
.comparison th { color:var(--muted); background:var(--subtle); font-size:.8rem; }
|
||||||
|
.comparison td { font-size:.9rem; }
|
||||||
|
.result-text { font-weight:750; }
|
||||||
|
.result-text.good { color:var(--success); }
|
||||||
|
.result-text.bad { color:var(--danger); }
|
||||||
|
.evidence { margin:16px 0; border:1px solid var(--border); border-radius:10px; overflow:hidden; background:#eef2f7; }
|
||||||
|
.evidence svg { display:block; width:100%; height:auto; min-height:180px; }
|
||||||
|
.evidence figcaption { padding:9px 12px; color:var(--muted); background:var(--surface); font-size:.8rem; }
|
||||||
|
.summary-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; margin:0 0 18px; }
|
||||||
|
.summary-item { min-width:0; padding:11px 12px; border:1px solid #e0e6ee; border-radius:8px; background:var(--subtle); }
|
||||||
|
.summary-item dt { color:var(--muted); font-size:.76rem; }
|
||||||
|
.summary-item dd { margin:2px 0 0; overflow-wrap:anywhere; font-weight:650; font-variant-numeric:tabular-nums; }
|
||||||
|
.actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:10px; margin-top:18px; padding-top:16px; border-top:1px solid var(--border); }
|
||||||
|
.actions.between { justify-content:space-between; }
|
||||||
|
.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:not(:disabled) { background:var(--primary-hover); }
|
||||||
|
.button-secondary { color:var(--text); border-color:var(--border); background:var(--surface); }
|
||||||
|
.button-secondary:hover:not(:disabled) { background:var(--subtle); }
|
||||||
|
.button-danger { color:#fff; background:var(--danger); }
|
||||||
|
.button-danger:hover:not(:disabled) { background:var(--danger-hover); }
|
||||||
|
.button-danger-outline { color:var(--danger); border-color:#e0a29c; background:var(--surface); }
|
||||||
|
.button-danger-outline:hover:not(:disabled) { background:var(--danger-soft); }
|
||||||
|
.spinner { width:16px; height:16px; border:2px solid rgba(255,255,255,.45); border-top-color:#fff; border-radius:50%; animation:spin .8s linear infinite; }
|
||||||
|
.substate-control { display:flex; align-items:center; gap:10px; margin-bottom:14px; padding:10px 12px; border:1px dashed #aebacc; border-radius:8px; background:var(--subtle); }
|
||||||
|
.substate-control label { flex:0 0 auto; font-size:.82rem; font-weight:700; }
|
||||||
|
.substate-control select { width:100%; padding:8px 10px; border:1px solid #91a0b4; border-radius:7px; background:#fff; }
|
||||||
|
.side-panel { position:sticky; top:116px; }
|
||||||
|
.side-panel section { padding:15px; border-bottom:1px solid var(--border); }
|
||||||
|
.side-panel section:last-child { border-bottom:0; }
|
||||||
|
.side-panel h2 { margin:0 0 8px; font-size:.98rem; }
|
||||||
|
.side-panel p { margin:0; color:var(--muted); font-size:.84rem; }
|
||||||
|
.state-select { width:100%; margin-top:8px; padding:8px 10px; border:1px solid #91a0b4; border-radius:7px; background:#fff; }
|
||||||
|
.timeline { list-style:none; margin:10px 0 0; padding:0; }
|
||||||
|
.timeline li { position:relative; padding:0 0 13px 18px; color:#33415b; font-size:.82rem; }
|
||||||
|
.timeline li::before { content:""; position:absolute; left:0; top:.4em; width:8px; height:8px; border-radius:50%; background:#8ba0bd; }
|
||||||
|
.timeline li::after { content:""; position:absolute; left:3px; top:1em; bottom:1px; width:2px; background:#d8e0eb; }
|
||||||
|
.timeline li:last-child::after { display:none; }
|
||||||
|
.timeline strong { display:block; color:var(--text); }
|
||||||
|
.fence-lock { padding:14px; border:2px solid #e0bd75; border-radius:10px; background:var(--warning-soft); }
|
||||||
|
.fence-lock h3 { margin:0 0 5px; color:var(--warning); font-size:1rem; }
|
||||||
|
.fence-lock p { margin:4px 0; color:#674018; }
|
||||||
|
.mono { font-family:ui-monospace,"Cascadia Mono",Consolas,monospace; font-variant-numeric:tabular-nums; overflow-wrap:anywhere; }
|
||||||
|
.checklist { display:grid; gap:9px; margin:15px 0; }
|
||||||
|
.checklist label,.radio-list label { display:flex; gap:10px; align-items:flex-start; min-height:44px; padding:10px 12px; border:1px solid var(--border); border-radius:8px; background:var(--surface); }
|
||||||
|
.checklist input,.radio-list input { width:18px; height:18px; flex:0 0 auto; margin-top:2px; accent-color:var(--primary); }
|
||||||
|
.radio-list { display:grid; gap:8px; margin:12px 0; }
|
||||||
|
textarea { width:100%; min-height:84px; padding:10px; border:1px solid #91a0b4; border-radius:8px; resize:vertical; }
|
||||||
|
.field-label { display:block; margin:14px 0 6px; font-weight:700; }
|
||||||
|
.terminal-mark { display:grid; place-items:center; width:54px; height:54px; margin:0 auto 12px; border-radius:50%; color:var(--success); border:2px solid currentColor; font-weight:800; }
|
||||||
|
.terminal { text-align:center; }
|
||||||
|
.terminal p { max-width:600px; margin:0 auto 10px; color:var(--muted); }
|
||||||
|
.live-region { margin-bottom:14px; }
|
||||||
|
.live-region:empty { display:none; }
|
||||||
|
dialog { width:min(100% - 32px,500px); padding:0; border:0; border-radius:12px; color:var(--text); box-shadow:0 24px 80px rgba(0,0,0,.3); }
|
||||||
|
dialog::backdrop { background:rgba(15,23,42,.58); }
|
||||||
|
.dialog-head { padding:18px 20px 12px; border-bottom:1px solid var(--border); }
|
||||||
|
.dialog-head h2 { margin:0; font-size:1.15rem; }
|
||||||
|
.dialog-body { padding:18px 20px; }
|
||||||
|
.dialog-body p { margin:0 0 10px; }
|
||||||
|
.dialog-actions { display:flex; justify-content:flex-end; flex-wrap:wrap; gap:9px; padding:14px 20px 18px; }
|
||||||
|
@keyframes spin { to { transform:rotate(360deg); } }
|
||||||
|
@media (max-width:900px) { .layout{grid-template-columns:1fr}.side-panel{position:static;display:grid;grid-template-columns:1fr 1fr}.side-panel section{border-bottom:0}.side-panel section+section{border-left:1px solid var(--border)} }
|
||||||
|
@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,1200px);padding-top:18px}.page-head{flex-direction:column;gap:10px}.panel-head,.panel-body{padding-inline:15px}.summary-grid{grid-template-columns:1fr}.actions,.actions.between{flex-direction:column-reverse}.actions .button{width:100%}.substate-control{align-items:stretch;flex-direction:column}.side-panel{grid-template-columns:1fr}.side-panel section+section{border-left:0;border-top:1px solid var(--border)}
|
||||||
|
.comparison,.comparison tbody,.comparison tr,.comparison td{display:block;width:100%}.comparison thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.comparison tr{padding:8px 0;border-bottom:1px solid #e4e9f0}.comparison td{display:grid;grid-template-columns:110px 1fr;gap:8px;padding:5px 0;border:0}.comparison td::before{content:attr(data-label);color:var(--muted);font-size:.78rem;font-weight:700}
|
||||||
|
}
|
||||||
|
@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}.safety-bar{grid-template-columns:1fr}.comparison td{grid-template-columns:92px 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"><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">
|
||||||
|
<nav class="breadcrumb" aria-label="面包屑"><a href="web-task-workbench.html">任务工作台</a><span aria-hidden="true">/</span><span>任务详情</span></nav>
|
||||||
|
<header class="page-head">
|
||||||
|
<div><h1>8 月仓库工作服补货</h1><p class="task-id">任务 T-20260803-018 · 假数据</p></div>
|
||||||
|
<span class="state-chip" id="state-chip">等待确认</span>
|
||||||
|
</header>
|
||||||
|
<div class="safety-bar"><strong>只下单,不付款</strong><span>系统最多创建待付款订单。付款始终由人在拼多多核对后完成;待付款状态不是成功。</span></div>
|
||||||
|
<div class="layout">
|
||||||
|
<section class="panel main-panel" aria-label="当前任务状态">
|
||||||
|
<div class="live-region" id="live-region" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<article class="state-panel" data-state-panel="waiting">
|
||||||
|
<header class="panel-head"><div><h2>机器选对了吗?</h2><p>确认前必须查看实际勾选结果、价格和规格面板证据。</p></div></header>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="substate-control"><label for="trial-variant">原型子状态</label><select id="trial-variant"><option value="match">规格一致、金额未超</option><option value="mismatch">规格不一致</option><option value="over">金额超上限</option><option value="missing">截图缺失</option></select></div>
|
||||||
|
<div class="notice" id="trial-notice"><strong>可以授权</strong>机器所选规格与要求一致,合计未超过资金上限。</div>
|
||||||
|
<table class="comparison">
|
||||||
|
<thead><tr><th scope="col">核对项</th><th scope="col">你要的</th><th scope="col">机器选到</th><th scope="col">结论</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td data-label="核对项">颜色分类</td><td data-label="你要的">白色</td><td data-label="机器选到" id="actual-color">白色</td><td data-label="结论"><span class="result-text good" id="color-result">符合</span></td></tr>
|
||||||
|
<tr><td data-label="核对项">尺码</td><td data-label="你要的">XL</td><td data-label="机器选到">XL</td><td data-label="结论"><span class="result-text good">符合</span></td></tr>
|
||||||
|
<tr><td data-label="核对项">金额</td><td data-label="你要的">总额不超过 ¥80.00</td><td data-label="机器选到" id="actual-price">¥32.50 × 2 = ¥65.00</td><td data-label="结论"><span class="result-text good" id="price-result">未超上限</span></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<figure class="evidence" id="trial-evidence">
|
||||||
|
<svg viewBox="0 0 760 300" role="img" aria-labelledby="trial-svg-title trial-svg-desc">
|
||||||
|
<title id="trial-svg-title">规格面板截图假数据占位</title><desc id="trial-svg-desc">用于确认信息架构,不代表真实拼多多页面结构或字段位置。</desc>
|
||||||
|
<rect width="760" height="300" fill="#e7edf5"/><rect x="30" y="26" width="700" height="248" rx="12" fill="#fff" stroke="#b8c4d5"/>
|
||||||
|
<text x="55" y="62" fill="#526079" font-size="18">假数据证据占位 · 不代表真实 App 页面</text><line x1="55" y1="78" x2="705" y2="78" stroke="#d6dde8"/>
|
||||||
|
<rect x="55" y="102" width="190" height="42" rx="7" fill="#eaf1ff" stroke="#8aacf2"/><text x="72" y="129" fill="#163c8c" font-size="16">颜色分类:白色(假)</text>
|
||||||
|
<rect x="265" y="102" width="150" height="42" rx="7" fill="#eaf1ff" stroke="#8aacf2"/><text x="282" y="129" fill="#163c8c" font-size="16">尺码:XL(假)</text>
|
||||||
|
<rect x="55" y="166" width="360" height="58" rx="7" fill="#f8fafc" stroke="#cfd8e6"/><text x="72" y="201" fill="#172033" font-size="22">规格面板读价:¥32.50(假)</text>
|
||||||
|
<text x="55" y="252" fill="#526079" font-size="15">真实可读字段需等待 T-103 真机取证后复核</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>规格面板证据 · 假数据 · 未包含地址、手机号或付款信息</figcaption>
|
||||||
|
</figure>
|
||||||
|
<label class="field-label" for="auth-note">授权备注(可选)</label><textarea id="auth-note" maxlength="500" placeholder="只记录本次决定所需的非敏感信息"></textarea>
|
||||||
|
<div class="actions between"><button class="button button-danger-outline" id="reject-button" type="button">退回,不买</button><button class="button button-primary" id="authorize-button" type="button">确认下单(不付款)</button></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="state-panel" data-state-panel="authorized" hidden>
|
||||||
|
<header class="panel-head"><div><h2>授权已签发,等待第二趟</h2><p>尚未建立提交围栏;旧授权仍可安全放弃,但必须重新试选取价。</p></div></header>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="notice"><strong>系统正在等待桌面端领取</strong>当前没有创建订单,也没有发生付款。</div>
|
||||||
|
<dl class="summary-grid"><div class="summary-item"><dt>锁定单价</dt><dd>¥32.50</dd></div><div class="summary-item"><dt>授权总额上限</dt><dd>¥80.00</dd></div><div class="summary-item"><dt>授权编号</dt><dd class="mono">AUTH-018-V5</dd></div><div class="summary-item"><dt>围栏状态</dt><dd>尚未建立</dd></div></dl>
|
||||||
|
<div class="notice warning"><strong>放弃不会复用旧价</strong>任务将转为待重新试选,完成新试选后才能再次确认。</div>
|
||||||
|
<div class="actions"><button class="button button-danger" type="button" data-open-abandon>放弃授权并重新试选</button></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="state-panel" data-state-panel="ordering-pre" hidden>
|
||||||
|
<header class="panel-head"><div><h2>第二趟执行中 · 围栏前</h2><p>桌面端正在重新选同一规格并复核价格,尚未取得提交许可。</p></div></header>
|
||||||
|
<div class="panel-body">
|
||||||
|
<ol class="timeline"><li><strong>已重新打开商品</strong>页面细节不在本原型定义</li><li><strong>正在重新选择规格</strong>必须按维度精确匹配</li><li><strong>待价格闸门复核</strong>现价必须等于锁定价 ¥32.50</li><li><strong>尚未申请提交围栏</strong>不会点击提交订单</li></ol>
|
||||||
|
<div class="notice warning"><strong>围栏前仍可安全放弃</strong>放弃后不会沿用旧试选价格,任务回到待重新试选。</div>
|
||||||
|
<div class="actions"><button class="button button-danger" type="button" data-open-abandon>放弃授权并重新试选</button></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="state-panel" data-state-panel="retrial" hidden>
|
||||||
|
<header class="panel-head"><div><h2>等待重新试选</h2><p>旧授权已作废,旧价格不再有效。</p></div></header>
|
||||||
|
<div class="panel-body terminal"><div class="terminal-mark" aria-hidden="true">新</div><div class="notice"><strong>下一步安全可执行</strong>桌面端会重新跑第一趟,读取新价格并回传新证据;在此之前页面不会显示确认入口。</div><p>当前没有有效授权,没有建立提交围栏,也不会创建订单。</p><div class="actions"><a class="button button-primary" href="web-task-workbench.html">返回任务工作台</a></div></div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="state-panel" data-state-panel="fenced" hidden>
|
||||||
|
<header class="panel-head"><div><h2>提交围栏已建立</h2><p>只允许恢复和核查这一笔提交记录。</p></div></header>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="fence-lock"><h3>禁止再次提交或放弃</h3><p>唯一提交记录:<strong class="mono">SUB-018-5A7C</strong></p><p>围栏建立后即使连接中断,也不能释放授权、重新领取或再点一次。</p></div>
|
||||||
|
<dl class="summary-grid" style="margin-top:16px"><div class="summary-item"><dt>提交记录</dt><dd class="mono">SUB-018-5A7C</dd></div><div class="summary-item"><dt>授权状态</dt><dd>已围栏</dd></div><div class="summary-item"><dt>确认页金额</dt><dd>¥65.00</dd></div><div class="summary-item"><dt>付款状态</dt><dd>未付款</dd></div></dl>
|
||||||
|
<div class="notice warning"><strong>这里没有重试或放弃入口</strong>如果结果不明,只能核查同一提交编号,避免产生第二笔订单。</div>
|
||||||
|
<div class="actions"><button class="button button-primary" type="button" data-go-reconcile>进入人工核查</button></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="state-panel" data-state-panel="reconcile" hidden>
|
||||||
|
<header class="panel-head"><div><h2>订单结果需要人工核查</h2><p>订单可能已创建,请勿再次提交;授权额度继续预留。</p></div></header>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="notice warning"><strong>结果不明,不代表失败</strong>提交记录 <span class="mono">SUB-018-5A7C</span> 已被永久围栏。当前只记录核查结果,不发起新的下单。</div>
|
||||||
|
<dl class="summary-grid"><div class="summary-item"><dt>观察结果</dt><dd>页面超时,无法确认</dd></div><div class="summary-item"><dt>付款状态</dt><dd>未知;系统未付款</dd></div><div class="summary-item"><dt>证据</dt><dd>1 张脱敏截图(假)</dd></div><div class="summary-item"><dt>可用动作</dt><dd>仅人工核查</dd></div></dl>
|
||||||
|
<fieldset style="border:0;padding:0;margin:0"><legend style="font-weight:750">人工核查结论</legend><div class="radio-list"><label><input type="radio" name="review" value="created">已确认创建了待付款订单</label><label><input type="radio" name="review" value="not-created">已确认没有创建订单</label><label><input type="radio" name="review" value="unknown" checked>仍不明确,继续保留围栏</label></div></fieldset>
|
||||||
|
<label class="field-label" for="review-note">核查说明(必填,勿写敏感信息)</label><textarea id="review-note" placeholder="例如:已由人工查看拼多多待付款列表;本原型不访问真实页面">仍需人工查看待付款列表(假数据)</textarea>
|
||||||
|
<div class="actions"><button class="button button-primary" id="save-review" type="button">保存核查记录</button></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="state-panel" data-state-panel="payment" hidden>
|
||||||
|
<header class="panel-head"><div><h2>待付款核对</h2><p>订单已创建,但任务尚未成功。请先在拼多多逐项核对。</p></div></header>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="notice warning"><strong>对不上就别付</strong>不一致时回到这里标记异常,不要在拼多多改单。系统不会代付。</div>
|
||||||
|
<figure class="evidence"><svg viewBox="0 0 760 260" role="img" aria-labelledby="order-svg-title order-svg-desc"><title id="order-svg-title">订单截图假数据占位</title><desc id="order-svg-desc">仅用于原型核对流程,不表示真实拼多多订单页布局。</desc><rect width="760" height="260" fill="#e7edf5"/><rect x="30" y="25" width="700" height="210" rx="12" fill="#fff" stroke="#b8c4d5"/><text x="54" y="60" fill="#526079" font-size="18">待付款订单证据 · 假数据</text><line x1="54" y1="76" x2="706" y2="76" stroke="#d6dde8"/><text x="54" y="112" fill="#172033" font-size="18">商品:2026 夏季纯棉圆领短袖(假)</text><text x="54" y="150" fill="#172033" font-size="18">规格:白色 · XL 数量:2</text><text x="54" y="194" fill="#172033" font-size="22">授权金额:¥65.00 状态:待付款</text></svg><figcaption>订单证据 · 假数据 · 真实字段需 T-103 后复核</figcaption></figure>
|
||||||
|
<div class="checklist"><label><input type="checkbox" data-payment-check>拼多多中商品与这里展示的一致</label><label><input type="checkbox" data-payment-check>规格、数量与这里展示的一致</label><label><input type="checkbox" data-payment-check>付款金额不超过授权金额 ¥80.00</label><label><input type="checkbox" data-payment-check>我已由人工在拼多多完成付款</label></div>
|
||||||
|
<div class="actions between"><button class="button button-danger-outline" id="mark-abnormal" type="button">标记异常,先不付款</button><button class="button button-primary" id="mark-paid" type="button" disabled aria-describedby="paid-disabled-reason">确认已付款并完成</button></div>
|
||||||
|
<p id="paid-disabled-reason" style="margin:8px 0 0;color:var(--muted);font-size:.82rem">完成全部四项人工核对后才能标记。</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="state-panel" data-state-panel="manual" hidden>
|
||||||
|
<header class="panel-head"><div><h2>需要人工处理</h2><p>这是围栏前异常;系统已经停止,不会猜测或继续执行。</p></div></header>
|
||||||
|
<div class="panel-body"><div class="notice danger"><strong>价格与授权不一致</strong>授权单价 ¥32.50,第二趟读到 ¥35.00。系统没有申请提交围栏,也没有点击提交订单。</div><dl class="summary-grid"><div class="summary-item"><dt>失败阶段</dt><dd>第二趟 · 价格闸门二</dd></div><div class="summary-item"><dt>安全处置</dt><dd>停止并保留证据</dd></div><div class="summary-item"><dt>可做什么</dt><dd>核查原因,决定重新试选</dd></div><div class="summary-item"><dt>不可做什么</dt><dd>按新价继续下单</dd></div></dl><label class="field-label" for="manual-note">处理记录</label><textarea id="manual-note" placeholder="记录下一步,不填写敏感信息"></textarea><div class="actions"><button class="button button-primary" id="record-manual" type="button">记录人工处理结果</button></div></div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="state-panel" data-state-panel="succeeded" hidden><header class="panel-head"><div><h2>任务已完成</h2><p>人工付款并核对后才进入此状态。</p></div></header><div class="panel-body terminal"><div class="terminal-mark" aria-hidden="true">完成</div><p><strong>已由采购管理员于 2026-08-03 11:20 标记完成。</strong></p><p>付款由人在拼多多完成,系统未读取或保存任何付款凭据。</p><div class="actions"><a class="button button-primary" href="web-task-workbench.html">返回任务工作台</a></div></div></article>
|
||||||
|
<article class="state-panel" data-state-panel="failed" hidden><header class="panel-head"><div><h2>任务失败</h2><p>失败原因可区分,并保留当时证据。</p></div></header><div class="panel-body"><div class="notice danger"><strong>设备连接中断</strong>发生在第一趟打开商品前,没有进行规格选择或下单动作。</div><dl class="summary-grid"><div class="summary-item"><dt>原因码</dt><dd class="mono">DEVICE_DISCONNECTED</dd></div><div class="summary-item"><dt>下一步</dt><dd>检查设备连接后新建重试任务</dd></div></dl><div class="actions"><a class="button button-primary" href="web-task-workbench.html">返回工作台</a></div></div></article>
|
||||||
|
<article class="state-panel" data-state-panel="canceled" hidden><header class="panel-head"><div><h2>任务已取消</h2><p>管理员选择“退回,不买”,不会再自动执行。</p></div></header><div class="panel-body terminal"><div class="terminal-mark" style="color:var(--danger)" aria-hidden="true">停</div><p>没有签发下单授权,没有创建订单,也没有发生付款。</p><div class="actions"><a class="button button-primary" href="web-task-workbench.html">返回任务工作台</a></div></div></article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="panel side-panel" aria-label="原型状态与任务摘要">
|
||||||
|
<section><h2>原型状态切换</h2><p>演示任务在不同业务状态下的唯一主区块,不发送请求。</p><label for="state-select" style="display:block;margin-top:10px;font-weight:700;font-size:.82rem">业务状态</label><select class="state-select" id="state-select"><option value="waiting">WAITING_CONFIRMATION</option><option value="authorized">AUTHORIZED</option><option value="ordering-pre">ORDERING · 围栏前</option><option value="retrial">PENDING_RETRIAL</option><option value="fenced">ORDERING · 已围栏</option><option value="reconcile">RECONCILIATION_REQUIRED</option><option value="payment">WAITING_PAYMENT</option><option value="manual">NEEDS_MANUAL</option><option value="succeeded">SUCCEEDED</option><option value="failed">FAILED</option><option value="canceled">CANCELED</option></select></section>
|
||||||
|
<section><h2>任务要求</h2><dl class="summary-grid" style="grid-template-columns:1fr;margin:0"><div class="summary-item"><dt>颜色分类 / 尺码</dt><dd>白色 / XL</dd></div><div class="summary-item"><dt>数量</dt><dd>2 件</dd></div><div class="summary-item"><dt>价格上限</dt><dd>¥80.00</dd></div></dl></section>
|
||||||
|
<section><h2>固定安全边界</h2><p>金额均为十进制字符串假数据。原型不定义拼多多页面判据,不调用接口,不读取地址、手机号或付款凭据。</p></section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<dialog id="reject-dialog" aria-labelledby="reject-title"><div class="dialog-head"><h2 id="reject-title">确认退回,不买?</h2></div><div class="dialog-body"><p>任务将结束为已取消,不会签发下单授权,也不会再自动执行。</p></div><div class="dialog-actions"><button class="button button-secondary" type="button" data-close-dialog>继续检查</button><button class="button button-danger" type="button" id="confirm-reject">确认退回</button></div></dialog>
|
||||||
|
<dialog id="abandon-dialog" aria-labelledby="abandon-title"><div class="dialog-head"><h2 id="abandon-title">放弃当前授权?</h2></div><div class="dialog-body"><p>当前确认尚未建立提交围栏,可以安全放弃。</p><p><strong>放弃后必须重新跑第一趟取新价,不能复用旧授权或旧截图。</strong></p></div><div class="dialog-actions"><button class="button button-secondary" type="button" data-close-dialog>保留授权</button><button class="button button-danger" type="button" id="confirm-abandon">放弃并重新试选</button></div></dialog>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
const select = document.querySelector('#state-select');
|
||||||
|
const chip = document.querySelector('#state-chip');
|
||||||
|
const live = document.querySelector('#live-region');
|
||||||
|
const labels = { waiting:'等待确认', authorized:'已授权 · 围栏前', 'ordering-pre':'第二趟 · 围栏前', retrial:'等待重新试选', fenced:'提交围栏已建立', reconcile:'结果需要调和', payment:'待付款', manual:'需要人工', succeeded:'已完成', failed:'失败', canceled:'已取消' };
|
||||||
|
const panels = [...document.querySelectorAll('[data-state-panel]')];
|
||||||
|
const rejectDialog = document.querySelector('#reject-dialog');
|
||||||
|
const abandonDialog = document.querySelector('#abandon-dialog');
|
||||||
|
|
||||||
|
function showState(state, message = '', shouldScroll = true) {
|
||||||
|
panels.forEach((panel) => { panel.hidden = panel.dataset.statePanel !== state; });
|
||||||
|
select.value = state;
|
||||||
|
chip.textContent = labels[state];
|
||||||
|
live.className = message ? 'notice success live-region' : 'live-region';
|
||||||
|
live.textContent = message;
|
||||||
|
if (shouldScroll) {
|
||||||
|
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
document.querySelector('.main-panel').scrollIntoView({ block:'start', behavior:reduceMotion ? 'auto' : 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select.addEventListener('change', () => showState(select.value));
|
||||||
|
|
||||||
|
const variant = document.querySelector('#trial-variant');
|
||||||
|
const actualColor = document.querySelector('#actual-color');
|
||||||
|
const colorResult = document.querySelector('#color-result');
|
||||||
|
const actualPrice = document.querySelector('#actual-price');
|
||||||
|
const priceResult = document.querySelector('#price-result');
|
||||||
|
const trialNotice = document.querySelector('#trial-notice');
|
||||||
|
const evidence = document.querySelector('#trial-evidence');
|
||||||
|
const authorizeButton = document.querySelector('#authorize-button');
|
||||||
|
|
||||||
|
function setTrialVariant(value) {
|
||||||
|
actualColor.textContent = value === 'mismatch' ? '粉白色' : '白色';
|
||||||
|
colorResult.textContent = value === 'mismatch' ? '不符合:颜色不同' : '符合';
|
||||||
|
colorResult.className = `result-text ${value === 'mismatch' ? 'bad' : 'good'}`;
|
||||||
|
actualPrice.textContent = value === 'over' ? '¥43.50 × 2 = ¥87.00' : '¥32.50 × 2 = ¥65.00';
|
||||||
|
priceResult.textContent = value === 'over' ? '超出 ¥7.00' : '未超上限';
|
||||||
|
priceResult.className = `result-text ${value === 'over' ? 'bad' : 'good'}`;
|
||||||
|
evidence.hidden = value === 'missing';
|
||||||
|
authorizeButton.disabled = value !== 'match';
|
||||||
|
if (value === 'match') { trialNotice.className='notice'; trialNotice.innerHTML='<strong>可以授权</strong>机器所选规格与要求一致,合计未超过资金上限。'; }
|
||||||
|
else if (value === 'mismatch') { trialNotice.className='notice danger'; trialNotice.innerHTML='<strong>不能授权:规格不一致</strong>颜色分类不相同,只能退回或转人工;不提供“仍然确认”。'; }
|
||||||
|
else if (value === 'over') { trialNotice.className='notice danger'; trialNotice.innerHTML='<strong>不能授权:金额超上限</strong>合计超出 ¥7.00,不提供绕过资金边界的入口。'; }
|
||||||
|
else { trialNotice.className='notice danger'; trialNotice.innerHTML='<strong>不能授权:证据缺失</strong>未加载出规格面板截图,无证据不能签发授权。'; }
|
||||||
|
}
|
||||||
|
variant.addEventListener('change', () => setTrialVariant(variant.value));
|
||||||
|
|
||||||
|
authorizeButton.addEventListener('click', () => {
|
||||||
|
authorizeButton.disabled = true;
|
||||||
|
authorizeButton.innerHTML = '<span class="spinner" aria-hidden="true"></span>正在签发授权…';
|
||||||
|
window.setTimeout(() => { authorizeButton.innerHTML='确认下单(不付款)'; authorizeButton.disabled=false; showState('authorized','授权已签发并锁定单价 ¥32.50。这里只允许创建待付款订单,系统不会付款。'); }, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('#reject-button').addEventListener('click', () => rejectDialog.showModal());
|
||||||
|
document.querySelectorAll('[data-open-abandon]').forEach((button) => button.addEventListener('click', () => abandonDialog.showModal()));
|
||||||
|
document.querySelectorAll('[data-close-dialog]').forEach((button) => button.addEventListener('click', () => button.closest('dialog').close()));
|
||||||
|
document.querySelector('#confirm-reject').addEventListener('click', () => { rejectDialog.close(); showState('canceled','任务已退回并取消,不会再自动执行。'); });
|
||||||
|
document.querySelector('#confirm-abandon').addEventListener('click', () => { abandonDialog.close(); showState('retrial','旧授权已作废。任务必须先重新试选取新价,不能复用旧价格。'); });
|
||||||
|
document.querySelectorAll('[data-go-reconcile]').forEach((button) => button.addEventListener('click', () => showState('reconcile')));
|
||||||
|
document.querySelector('#save-review').addEventListener('click', () => { live.className='notice success live-region'; live.textContent='人工核查记录已保存为假数据。无论选择何种结论,本原型都不会发起新的下单。'; live.scrollIntoView({block:'nearest'}); });
|
||||||
|
|
||||||
|
const paymentChecks = [...document.querySelectorAll('[data-payment-check]')];
|
||||||
|
const markPaid = document.querySelector('#mark-paid');
|
||||||
|
function updatePaidButton() { markPaid.disabled = !paymentChecks.every((check) => check.checked); }
|
||||||
|
paymentChecks.forEach((check) => check.addEventListener('change', updatePaidButton));
|
||||||
|
markPaid.addEventListener('click', () => showState('succeeded','已记录人工付款与核对结果。系统没有执行付款。'));
|
||||||
|
document.querySelector('#mark-abnormal').addEventListener('click', () => showState('manual','已标记核对异常,任务不会被直接标记完成。'));
|
||||||
|
document.querySelector('#record-manual').addEventListener('click', () => { live.className='notice success live-region'; live.textContent='人工处理记录已保存为假数据。后续如需重来,仍必须重新试选取价。'; live.scrollIntoView({block:'nearest'}); });
|
||||||
|
|
||||||
|
[rejectDialog, abandonDialog].forEach((dialog) => dialog.addEventListener('click', (event) => { if (event.target === dialog) dialog.close(); }));
|
||||||
|
setTrialVariant('match');
|
||||||
|
showState('waiting', '', false);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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>
|
||||||
+40
-2
@@ -3,7 +3,7 @@ id: T-005
|
|||||||
title: 网页端 MVP 交互原型
|
title: 网页端 MVP 交互原型
|
||||||
phase: 0
|
phase: 0
|
||||||
deps: []
|
deps: []
|
||||||
status: TODO
|
status: DOING
|
||||||
created: 2026-08-03
|
created: 2026-08-03
|
||||||
context_ref: dfe3d56
|
context_ref: dfe3d56
|
||||||
work_branch: task/t-005-web-prototype
|
work_branch: task/t-005-web-prototype
|
||||||
@@ -89,4 +89,42 @@ web 端尚无生产代码。若直接实现 SSR 页面,登录、建单、三
|
|||||||
|
|
||||||
## 执行记录
|
## 执行记录
|
||||||
|
|
||||||
开工后记录修改文件、浏览器检查尺寸、控制台 / 网络 / 可访问性结果、未验证范围和人工确认项。
|
### 2026-08-03 · 第一阶段原型实现
|
||||||
|
|
||||||
|
- 任务状态保持 `DOING`:自动检查已完成,但 `needs_human_review: true`,未进行人工设计确认。
|
||||||
|
- 新增四个自包含页面:登录、手工建单、三泳道工作台、任务详情。四页使用同一套语义颜色、
|
||||||
|
字体、间距和导航;全部假数据可由页内控件复位,不调用 API,不保存凭据,不加载外部资源。
|
||||||
|
- 登录页覆盖站内返回、必填错误、提交中、会话过期和成功;建单页覆盖逐字段校验、链接解析
|
||||||
|
失败后保留输入、提交中和创建成功;工作台覆盖实时筛选、计数规则、第一条待决策展开以及
|
||||||
|
加载 / 空 / 错误;详情页覆盖试选不一致 / 超额 / 缺证据、授权、围栏前放弃后重新试选、
|
||||||
|
围栏后调和、结果不明、待付款核对、转人工和终态。
|
||||||
|
- 安全表达已独立复核:全程固定说明不自动付款;围栏前放弃明确转 `PENDING_RETRIAL` 且不复用
|
||||||
|
旧价;围栏后只显示唯一 `submission_id` 与人工核查入口,没有重试、再次提交或放弃按钮;
|
||||||
|
`WAITING_PAYMENT` 需证据和四项人工核对才能标记完成。
|
||||||
|
- `ui-ux-pro-max` 检索建议被用于焦点、触控尺寸、错误就近、恢复路径与响应式检查。其推荐的
|
||||||
|
红色主色和外部装饰字体与任务约束冲突,因此未采用;主操作保持蓝色,红色只用于错误或
|
||||||
|
破坏性动作,字体使用 `Segoe UI` / 系统字体。
|
||||||
|
|
||||||
|
**已运行验证**
|
||||||
|
|
||||||
|
- `python scripts/validate_agent_context.py`:通过(6 个任务路由,18 个有效仓库路径)。
|
||||||
|
- Node 静态脚本:四页均包含 doctype、viewport、固定原型横幅、`aria-live`、
|
||||||
|
`prefers-reduced-motion`;每页唯一内联脚本均可编译;无外部脚本、字体或图片引用。
|
||||||
|
- Chrome Headless + DevTools Protocol:逐页检查 375 / 768 / 1024 / 1440 px,16 个组合均无
|
||||||
|
页面级水平溢出、运行时异常或外部网络请求;登录错误、建单链接错误与输入保留、工作台空态
|
||||||
|
与复位、详情全部业务状态、规格不符禁用确认、核对完成后启用标记等状态切换均通过。
|
||||||
|
- Chrome accessibility tree / 键盘抽查:可见交互控件都有可访问名称,Tab 顺序可到达页内
|
||||||
|
原型控件,未发现键盘陷阱;模拟 reduced-motion 时媒体查询生效。原生对话框均有明确的取消
|
||||||
|
和确认按钮。
|
||||||
|
- 目视检查 Chrome 截图:1440 px 工作台 / 详情与 375 px 建单 / 详情的信息层级、换行和主操作
|
||||||
|
未见截断;证据图均明确标为假数据且不代表真实 App 页面。
|
||||||
|
- `git diff --check`:通过。
|
||||||
|
|
||||||
|
**未验证与待人工确认**
|
||||||
|
|
||||||
|
- Playwright MCP 因共享浏览器实例被占用而无法连接;上述浏览器检查改由独立 Chrome Headless
|
||||||
|
与 DevTools Protocol 完成。未做真实读屏器、高对比度模式或人工键盘全流程验收。
|
||||||
|
- 原型不证明 Go SSR、真实鉴权 / CSRF、接口并发、PySide6、真机页面字段或真实拼多多流程。
|
||||||
|
T-103 真机取证后仍需复核实际可读字段,不能把占位图当成页面判据。
|
||||||
|
- 待人确认:四页整体布局与信息密度、三泳道命名和默认展开、详情页各状态的唯一主动作、
|
||||||
|
「确认下单(不付款)」与围栏 / 调和文案、375 px 窄屏阅读体验。确认前任务不得标 `DONE`。
|
||||||
|
|||||||
Reference in New Issue
Block a user