248 lines
13 KiB
HTML
248 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">
|
|
<title>登录 · cmbuyer 原型</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="cmbuyer 原型工作台">
|
|
<span class="brand-mark" aria-hidden="true">CM</span>
|
|
<span>cmbuyer</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>
|