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

350 lines
20 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>新建采购任务 · 采购服务原型</title>
<style>
: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" aria-label="采购服务原型工作台"><span class="brand-mark" aria-hidden="true">采</span><span>采购服务</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>
<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>