350 lines
20 KiB
HTML
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>新建采购任务 · 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>
|
|
<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>
|