Files
cmbuyer/docs/design/web-login.html
T

249 lines
13 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 {
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="采购服务原型工作台">
<span class="brand-mark" aria-hidden="true">采</span>
<span>采购服务</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>