docs(prototype): confirm p0 web and app flows
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
<!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 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', '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 quantity = document.querySelector('#quantity');
|
||||
const budget = document.querySelector('#budget');
|
||||
if (!title.value.trim()) {
|
||||
document.querySelector('#title-error').textContent = '请输入商品标题。';
|
||||
firstInvalid ||= title;
|
||||
}
|
||||
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>
|
||||
Reference in New Issue
Block a user