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

380 lines
14 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>新建采购任务原型</title>
<style>
* { box-sizing: border-box; }
:root {
color-scheme: light;
--ink: #18201d;
--muted: #5b6862;
--line: #cfd7d3;
--surface: #fff;
--canvas: #f3f5f4;
--brand: #0b6b50;
--brand-dark: #07513d;
--danger: #a62f2f;
--info: #185b8f;
}
body {
margin: 0;
min-width: 280px;
color: var(--ink);
background: var(--canvas);
font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
letter-spacing: 0;
}
button, input, textarea { font: inherit; }
button, input, textarea { min-height: 44px; }
button:focus-visible, input:focus-visible, textarea:focus-visible {
outline: 3px solid rgba(26, 115, 184, .28);
outline-offset: 2px;
}
.prototype {
position: fixed;
inset: 0 0 auto;
z-index: 10;
min-height: 32px;
padding: 6px 16px;
color: #fff;
background: #24312c;
text-align: center;
font-size: 12px;
font-weight: 700;
}
.app { min-height: 100vh; padding-top: 32px; }
header {
height: 58px;
display: flex;
align-items: center;
gap: 28px;
padding: 0 clamp(16px, 4vw, 40px);
border-bottom: 1px solid var(--line);
background: #fff;
}
.brand { font-size: 16px; font-weight: 750; white-space: nowrap; }
nav { display: flex; align-items: stretch; height: 100%; gap: 6px; }
nav a {
display: grid;
place-items: center;
padding: 0 12px;
color: #3d4b45;
text-decoration: none;
border-bottom: 3px solid transparent;
}
nav a[aria-current="page"] { color: var(--brand-dark); border-color: var(--brand); font-weight: 700; }
.user { margin-left: auto; color: var(--muted); }
main { width: min(100% - 32px, 920px); margin: 0 auto; padding: 28px 0 56px; }
.title-row { display: flex; justify-content: space-between; gap: 16px; align-items: start; margin-bottom: 18px; }
h1 { margin: 0; font-size: 24px; line-height: 1.3; }
.subtitle { margin: 4px 0 0; color: var(--muted); }
.panel {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
overflow: hidden;
}
.section { padding: 22px 24px; border-bottom: 1px solid var(--line); }
.section:last-child { border-bottom: 0; }
h2 { margin: 0 0 16px; font-size: 16px; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 20px; }
.span-2 { grid-column: 1 / -1; }
label { display: block; margin-bottom: 6px; font-weight: 650; }
.required::after { content: " *"; color: var(--danger); }
input, textarea {
width: 100%;
padding: 9px 11px;
border: 1px solid #9eaaa4;
border-radius: 5px;
color: var(--ink);
background: #fff;
}
textarea { min-height: 104px; resize: vertical; }
.help, .error { min-height: 20px; margin: 4px 0 0; font-size: 13px; }
.help { color: var(--muted); }
.error { color: var(--danger); }
.upload {
display: grid;
grid-template-columns: 1fr 160px;
gap: 20px;
align-items: start;
}
.dropzone {
padding: 18px;
border: 1px dashed #82928a;
border-radius: 6px;
background: #f9fbfa;
}
.dropzone input { padding: 7px; border: 0; min-height: 40px; }
.preview {
aspect-ratio: 4 / 3;
display: grid;
place-items: center;
padding: 12px;
border: 1px solid var(--line);
border-radius: 6px;
background: #eef2f0;
color: var(--muted);
text-align: center;
}
.preview.has-file { color: #fff; background: #52645c; }
.notice {
display: none;
margin-bottom: 16px;
padding: 11px 13px;
border-left: 4px solid var(--info);
background: #eef7fd;
color: #184e76;
}
.notice.error-state { border-color: var(--danger); background: #fff1f1; color: #762222; }
.actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 10px;
padding: 16px 24px;
background: #fafbfa;
}
button, .link-button {
padding: 9px 14px;
border: 1px solid #91a099;
border-radius: 5px;
background: #fff;
color: #26352f;
cursor: pointer;
text-decoration: none;
}
.primary { border-color: var(--brand); background: var(--brand); color: #fff; font-weight: 700; }
.primary:hover { background: var(--brand-dark); }
button:disabled { opacity: .65; cursor: wait; }
.demo-tools { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
.demo-tools button { min-height: 38px; padding-block: 6px; }
dialog {
width: min(100% - 32px, 430px);
padding: 0;
border: 1px solid var(--line);
border-radius: 8px;
color: var(--ink);
}
dialog::backdrop { background: rgba(13, 25, 20, .5); }
.dialog-body { padding: 22px; }
.dialog-body h2 { margin-bottom: 8px; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 14px 22px; border-top: 1px solid var(--line); }
@media (max-width: 680px) {
header { gap: 12px; }
nav a { padding-inline: 8px; }
.user { display: none; }
main { width: min(100% - 20px, 920px); padding-top: 20px; }
.grid, .upload { grid-template-columns: 1fr; }
.span-2 { grid-column: auto; }
.preview { max-width: 220px; }
.section { padding: 18px 14px; }
.actions { padding: 14px; }
}
@media (max-width: 440px) {
.prototype { padding-inline: 2px; font-size: 10px; white-space: nowrap; }
.brand { font-size: 14px; }
nav a:first-child { display: none; }
h1 { font-size: 21px; }
}
</style>
</head>
<body>
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
<div class="app">
<header>
<div class="brand">采购任务管理</div>
<nav aria-label="主导航">
<a href="admin-task-list.html">任务列表</a>
<a href="admin-task-create.html" aria-current="page">新建任务</a>
</nav>
<div class="user">演示管理员</div>
</header>
<main>
<div class="title-row">
<div>
<h1>新建采购任务</h1>
<p class="subtitle">填写商品要求和不可覆盖的采购约束</p>
</div>
</div>
<div id="notice" class="notice" role="status" tabindex="-1"></div>
<form id="task-form" class="panel" novalidate>
<section class="section" aria-labelledby="basic-title">
<h2 id="basic-title">商品要求</h2>
<div class="grid">
<div class="span-2">
<label class="required" for="title">商品标题</label>
<input id="title" name="title" maxlength="120" placeholder="例如:演示用桌面收纳盒">
<p id="title-error" class="error"></p>
</div>
<div class="span-2">
<label class="required" for="sku">SKU</label>
<input id="sku" name="sku" maxlength="120" placeholder="例如:浅灰色 · 中号">
<p id="sku-error" class="error"></p>
<p class="help">SKU 是不可覆盖的原始采购要求。</p>
</div>
<div class="span-2">
<label for="description">商品描述</label>
<textarea id="description" name="description" placeholder="填写规格、颜色、材质等判断条件"></textarea>
<p class="help">仅填写采购判断所需信息,不放订单、地址或个人信息。</p>
</div>
<div>
<label class="required" for="quantity">数量</label>
<input id="quantity" name="quantity" type="number" value="1" min="1" step="1" inputmode="numeric">
<p id="quantity-error" class="error"></p>
</div>
<div>
<label for="budget">最高商品总预算(元)</label>
<input id="budget" name="budget" type="number" min="0.01" step="0.01" inputmode="decimal" placeholder="不填写则不设上限">
<p id="budget-error" class="error"></p>
</div>
</div>
</section>
<section class="section" aria-labelledby="image-title">
<h2 id="image-title">参考图片</h2>
<div class="upload">
<div class="dropzone">
<label class="required" for="image">选择一张商品参考图</label>
<input id="image" name="image" type="file" accept="image/png,image/jpeg,image/webp">
<p id="file-meta" class="help">支持 JPG、PNG、WebP;原型不读取或上传文件。</p>
<p id="image-error" class="error"></p>
</div>
<div id="preview" class="preview" role="img" aria-label="尚未选择参考图片">图片预览位置</div>
</div>
</section>
<div class="actions">
<a class="link-button" href="admin-task-list.html">取消</a>
<button id="submit" class="primary" type="submit">创建任务</button>
</div>
</form>
<div class="demo-tools" aria-label="原型状态">
<button type="button" data-state="upload">模拟图片错误</button>
<button type="button" data-state="create">模拟创建失败</button>
<button type="button" data-state="uncertain">模拟网络结果不确定</button>
<button type="button" data-state="reset">重置状态</button>
</div>
</main>
</div>
<dialog id="leave-dialog" aria-labelledby="leave-title">
<div class="dialog-body">
<h2 id="leave-title">放弃未提交内容?</h2>
<p>已填写的商品要求不会保存。</p>
</div>
<div class="dialog-actions">
<button id="stay" type="button">继续填写</button>
<button id="discard" type="button">放弃并离开</button>
</div>
</dialog>
<script>
const form = document.querySelector('#task-form');
const notice = document.querySelector('#notice');
const submit = document.querySelector('#submit');
const image = document.querySelector('#image');
const preview = document.querySelector('#preview');
const leaveDialog = document.querySelector('#leave-dialog');
let dirty = false;
form.addEventListener('input', () => { dirty = true; });
image.addEventListener('change', () => {
document.querySelector('#image-error').textContent = '';
if (!image.files.length) return;
const file = image.files[0];
document.querySelector('#file-meta').textContent = `${file.name} · ${(file.size / 1024).toFixed(1)} KB`;
preview.textContent = '已选择参考图';
preview.classList.add('has-file');
preview.setAttribute('aria-label', `已选择参考图片:${file.name}`);
dirty = true;
});
function clearFeedback() {
['title', 'sku', 'quantity', 'budget', 'image'].forEach((name) => {
document.querySelector(`#${name}-error`).textContent = '';
});
notice.style.display = 'none';
notice.classList.remove('error-state');
}
function showNotice(message, error = false) {
notice.textContent = message;
notice.style.display = 'block';
notice.classList.toggle('error-state', error);
notice.focus();
}
form.addEventListener('submit', (event) => {
event.preventDefault();
clearFeedback();
let firstInvalid = null;
const title = document.querySelector('#title');
const sku = document.querySelector('#sku');
const quantity = document.querySelector('#quantity');
const budget = document.querySelector('#budget');
if (!title.value.trim()) {
document.querySelector('#title-error').textContent = '请输入商品标题。';
firstInvalid ||= title;
}
if (!sku.value.trim()) {
document.querySelector('#sku-error').textContent = '请输入 SKU。';
firstInvalid ||= sku;
}
if (!Number.isInteger(Number(quantity.value)) || Number(quantity.value) < 1) {
document.querySelector('#quantity-error').textContent = '数量必须是大于 0 的整数。';
firstInvalid ||= quantity;
}
if (budget.value && Number(budget.value) <= 0) {
document.querySelector('#budget-error').textContent = '商品总预算必须大于 0,或留空。';
firstInvalid ||= budget;
}
if (!image.files.length) {
document.querySelector('#image-error').textContent = '请选择一张参考图片。';
firstInvalid ||= image;
}
if (firstInvalid) {
showNotice('请检查表单中的错误后再创建。', true);
firstInvalid.focus();
return;
}
submit.disabled = true;
submit.textContent = '正在创建…';
window.setTimeout(() => {
submit.disabled = false;
submit.textContent = '创建任务';
dirty = false;
showNotice('原型:任务已创建,状态为“待领取(PENDING)”,随后进入任务详情。');
}, 700);
});
document.querySelectorAll('a').forEach((link) => {
link.addEventListener('click', (event) => {
if (!dirty) return;
event.preventDefault();
leaveDialog.dataset.target = link.href;
leaveDialog.showModal();
document.querySelector('#stay').focus();
});
});
document.querySelector('#stay').addEventListener('click', () => leaveDialog.close());
document.querySelector('#discard').addEventListener('click', () => {
dirty = false;
window.location.href = leaveDialog.dataset.target;
});
document.querySelector('.demo-tools').addEventListener('click', (event) => {
const state = event.target.dataset.state;
if (!state) return;
clearFeedback();
if (state === 'upload') {
document.querySelector('#image-error').textContent = '图片无法解码,请重新选择 JPG、PNG 或 WebP 文件。';
image.focus();
}
if (state === 'create') showNotice('图片已上传,但任务创建失败。再次提交时将复用已上传图片。', true);
if (state === 'uncertain') showNotice('网络中断,正在使用幂等标识查询创建结果,请勿重复提交。', true);
if (state === 'reset') showNotice('已恢复默认表单状态。');
});
</script>
</body>
</html>