| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
- <title>中科兴皖 · 登录页 UI Demo</title>
- <style>
- :root {
- --brand-green: #0a9f6d;
- --brand-green-dark: #087f57;
- --brand-green-soft: #e9f9f2;
- --barley-yellow: #d6a343;
- --barley-yellow-soft: #fff7e8;
- --bg-page: #f5f8f7;
- --surface: #ffffff;
- --text-primary: #1f2a26;
- --text-muted: #6b7b74;
- --border: #dcebe4;
- --danger: #d14343;
- --radius-card: 16px;
- --radius-ctrl: 12px;
- --shadow-soft: 0 4px 16px rgba(8, 127, 87, 0.08);
- }
- * { box-sizing: border-box; }
- html, body {
- margin: 0;
- padding: 0;
- min-height: 100%;
- font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
- color: var(--text-primary);
- background:
- radial-gradient(900px 420px at 10% -10%, rgba(10, 159, 109, 0.14), transparent 55%),
- radial-gradient(700px 380px at 90% 0%, rgba(214, 163, 67, 0.12), transparent 50%),
- linear-gradient(180deg, #eef6f2 0%, #f7faf9 40%, #f3f6f5 100%);
- }
- body {
- display: flex;
- justify-content: center;
- padding: 24px 16px 40px;
- }
- #app {
- width: 100%;
- max-width: 980px;
- display: grid;
- grid-template-columns: 280px 1fr;
- gap: 24px;
- align-items: start;
- }
- @media (max-width: 860px) {
- #app { grid-template-columns: 1fr; }
- .doc-panel { position: static; }
- }
- .doc-panel {
- position: sticky;
- top: 24px;
- background: rgba(255, 255, 255, 0.92);
- border: 1px solid var(--border);
- border-radius: 18px;
- padding: 20px 18px;
- box-shadow: var(--shadow-soft);
- }
- .doc-panel h1 {
- margin: 0 0 6px;
- font-size: 20px;
- line-height: 1.3;
- color: var(--brand-green-dark);
- }
- .doc-panel .sub {
- margin: 0 0 16px;
- font-size: 12px;
- color: var(--text-muted);
- line-height: 1.5;
- }
- .token-row {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-bottom: 14px;
- }
- .token-chip {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-size: 11px;
- color: var(--text-muted);
- background: #fff;
- border: 1px solid var(--border);
- border-radius: 999px;
- padding: 4px 8px 4px 4px;
- }
- .token-chip i {
- width: 14px;
- height: 14px;
- border-radius: 50%;
- display: block;
- }
- .notes {
- margin: 0;
- padding: 0;
- list-style: none;
- font-size: 12px;
- color: var(--text-muted);
- line-height: 1.55;
- }
- .notes li {
- position: relative;
- padding-left: 12px;
- margin-bottom: 6px;
- }
- .notes li::before {
- content: "";
- position: absolute;
- left: 0;
- top: 7px;
- width: 5px;
- height: 5px;
- border-radius: 50%;
- background: var(--brand-green);
- }
- .phone-stage {
- display: flex;
- justify-content: center;
- }
- .phone {
- width: 390px;
- height: 844px;
- background: #111;
- border-radius: 42px;
- padding: 12px;
- box-shadow:
- 0 24px 60px rgba(15, 40, 30, 0.18),
- inset 0 0 0 1px rgba(255, 255, 255, 0.08);
- position: relative;
- }
- .phone::before {
- content: "";
- position: absolute;
- top: 18px;
- left: 50%;
- transform: translateX(-50%);
- width: 118px;
- height: 28px;
- background: #0a0a0a;
- border-radius: 20px;
- z-index: 5;
- }
- .phone-screen {
- width: 100%;
- height: 100%;
- border-radius: 32px;
- overflow: hidden;
- background: var(--bg-page);
- position: relative;
- display: flex;
- flex-direction: column;
- }
- .status-bar {
- height: 54px;
- padding: 16px 22px 0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 12px;
- font-weight: 600;
- color: var(--text-primary);
- position: relative;
- z-index: 2;
- flex-shrink: 0;
- }
- .status-bar .icons {
- display: flex;
- gap: 5px;
- align-items: center;
- opacity: 0.75;
- font-size: 10px;
- }
- .login-scroll {
- flex: 1;
- overflow-y: auto;
- padding: 12px 24px 28px;
- background:
- radial-gradient(260px 160px at 8% -8%, rgba(10, 159, 109, 0.16), transparent 60%),
- radial-gradient(220px 140px at 96% 4%, rgba(214, 163, 67, 0.14), transparent 55%),
- linear-gradient(180deg, #eef6f2 0%, #f5f8f7 48%, #f3f6f5 100%);
- }
- .brand {
- text-align: center;
- padding: 28px 0 22px;
- }
- .brand-mark {
- width: 64px;
- height: 64px;
- margin: 0 auto 12px;
- border-radius: 18px;
- background: linear-gradient(145deg, var(--brand-green-dark), var(--brand-green));
- color: #fff;
- font-size: 26px;
- font-weight: 700;
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 10px 24px rgba(8, 127, 87, 0.28);
- letter-spacing: 0.02em;
- }
- .brand h2 {
- margin: 0;
- font-size: 22px;
- font-weight: 650;
- letter-spacing: 0.03em;
- }
- .brand p {
- margin: 8px 0 0;
- font-size: 13px;
- color: var(--text-muted);
- }
- .form-card {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius-card);
- padding: 20px 18px 18px;
- box-shadow: var(--shadow-soft);
- }
- .field {
- margin-bottom: 14px;
- }
- .field label {
- display: block;
- font-size: 12px;
- color: var(--text-muted);
- margin-bottom: 6px;
- }
- .field-control {
- position: relative;
- display: flex;
- align-items: center;
- }
- .field-control input {
- width: 100%;
- height: 44px;
- border: 1px solid var(--border);
- border-radius: 10px;
- background: #f8fbfa;
- padding: 0 12px;
- font-size: 14px;
- color: var(--text-primary);
- outline: none;
- transition: border-color .15s, box-shadow .15s;
- }
- .field-control input:focus {
- border-color: var(--brand-green);
- box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.12);
- background: #fff;
- }
- .field-control input.is-error {
- border-color: var(--danger);
- }
- .field-control.with-action input {
- padding-right: 52px;
- }
- .field-action {
- position: absolute;
- right: 10px;
- appearance: none;
- border: none;
- background: transparent;
- color: var(--brand-green);
- font-size: 12px;
- font-weight: 500;
- cursor: pointer;
- padding: 6px;
- min-height: 32px;
- }
- .field-error {
- margin-top: 6px;
- font-size: 12px;
- color: var(--danger);
- display: none;
- }
- .field-error.show { display: block; }
- .btn-primary {
- width: 100%;
- height: 44px;
- border: none;
- border-radius: var(--radius-ctrl);
- background: var(--brand-green);
- color: #fff;
- font-size: 15px;
- font-weight: 600;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- transition: background .15s, opacity .15s;
- }
- .btn-primary:hover { background: var(--brand-green-dark); }
- .btn-primary:disabled {
- opacity: 0.55;
- cursor: not-allowed;
- }
- .btn-primary .spinner {
- width: 16px;
- height: 16px;
- border: 2px solid rgba(255,255,255,0.35);
- border-top-color: #fff;
- border-radius: 50%;
- animation: spin .7s linear infinite;
- display: none;
- }
- .btn-primary.is-loading .spinner { display: block; }
- .btn-primary.is-loading .btn-text { opacity: 0.9; }
- @keyframes spin { to { transform: rotate(360deg); } }
- .agree {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- margin-top: 14px;
- font-size: 12px;
- color: var(--text-muted);
- line-height: 1.45;
- cursor: pointer;
- user-select: none;
- }
- .agree input {
- position: absolute;
- opacity: 0;
- pointer-events: none;
- }
- .agree-box {
- width: 16px;
- height: 16px;
- border-radius: 50%;
- border: 1.5px solid var(--brand-green);
- flex-shrink: 0;
- margin-top: 1px;
- position: relative;
- transition: background .15s;
- }
- .agree input:checked + .agree-box {
- background: var(--brand-green);
- }
- .agree input:checked + .agree-box::after {
- content: "";
- position: absolute;
- left: 4px;
- top: 2px;
- width: 5px;
- height: 8px;
- border: solid #fff;
- border-width: 0 2px 2px 0;
- transform: rotate(45deg);
- }
- .agree a {
- color: var(--brand-green);
- text-decoration: none;
- }
- .agree a:hover { text-decoration: underline; }
- .divider {
- display: flex;
- align-items: center;
- gap: 12px;
- margin: 22px 0 14px;
- font-size: 12px;
- color: var(--text-muted);
- }
- .divider::before,
- .divider::after {
- content: "";
- flex: 1;
- height: 1px;
- background: var(--border);
- }
- .btn-wechat {
- width: 100%;
- height: 44px;
- border-radius: var(--radius-ctrl);
- border: 1px solid var(--border);
- background: #fff;
- color: var(--text-primary);
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- transition: background .15s, border-color .15s;
- }
- .btn-wechat:hover {
- background: #f8fbfa;
- border-color: #b7d9cb;
- }
- .btn-wechat:disabled {
- opacity: 0.55;
- cursor: not-allowed;
- }
- .btn-wechat img {
- width: 22px;
- height: 22px;
- object-fit: contain;
- display: block;
- }
- .home-indicator {
- height: 22px;
- display: flex;
- align-items: flex-end;
- justify-content: center;
- padding-bottom: 8px;
- flex-shrink: 0;
- background: transparent;
- position: absolute;
- left: 0;
- right: 0;
- bottom: 0;
- pointer-events: none;
- }
- .home-indicator span {
- width: 120px;
- height: 4px;
- border-radius: 4px;
- background: rgba(31, 42, 38, 0.22);
- }
- .toast {
- position: absolute;
- left: 50%;
- top: 46%;
- transform: translate(-50%, -50%);
- background: rgba(31, 42, 38, 0.88);
- color: #fff;
- font-size: 13px;
- padding: 10px 16px;
- border-radius: 10px;
- max-width: 78%;
- text-align: center;
- line-height: 1.4;
- opacity: 0;
- pointer-events: none;
- transition: opacity .2s;
- z-index: 20;
- }
- .toast.show { opacity: 1; }
- </style>
- </head>
- <body>
- <div id="app">
- <aside class="doc-panel">
- <h1>登录页 Demo</h1>
- <p class="sub">方案 B · 账号密码为主 · 微信授权为次<br/>权威说明见设计文档</p>
- <div class="token-row">
- <span class="token-chip"><i style="background:#0a9f6d"></i>#0a9f6d</span>
- <span class="token-chip"><i style="background:#087f57"></i>#087f57</span>
- <span class="token-chip"><i style="background:#f5f8f7;border:1px solid #dcebe4"></i>#f5f8f7</span>
- </div>
- <ul class="notes">
- <li>账号:用户名 / 工号(与 web-admin 一致)</li>
- <li>未勾选协议时拦截两种登录</li>
- <li>微信按钮使用 <code>assets/wechat-logo.png</code></li>
- <li>本期仅 UI 模拟,不联调 API</li>
- <li>设计文档:<code>docs/superpowers/specs/2026-08-10-miniprogram-login-ui-design.md</code></li>
- </ul>
- </aside>
- <div class="phone-stage">
- <div class="phone">
- <div class="phone-screen">
- <div class="status-bar">
- <span>9:41</span>
- <span class="icons">▌▌▌ Wi‑Fi 100%</span>
- </div>
- <div class="login-scroll">
- <div class="brand">
- <div class="brand-mark" aria-hidden="true">兴</div>
- <h2>中科兴皖</h2>
- <p>员工工作台登录</p>
- </div>
- <form class="form-card" id="loginForm" novalidate>
- <div class="field">
- <label for="username">账号</label>
- <div class="field-control">
- <input id="username" name="username" type="text" autocomplete="username" placeholder="请输入用户名 / 工号" />
- </div>
- <div class="field-error" id="usernameError">请输入用户名 / 工号</div>
- </div>
- <div class="field">
- <label for="password">密码</label>
- <div class="field-control with-action">
- <input id="password" name="password" type="password" autocomplete="current-password" placeholder="请输入密码" />
- <button type="button" class="field-action" id="togglePwd" aria-label="显示或隐藏密码">显示</button>
- </div>
- <div class="field-error" id="passwordError">请输入密码</div>
- </div>
- <button type="submit" class="btn-primary" id="loginBtn">
- <span class="spinner" aria-hidden="true"></span>
- <span class="btn-text">登录</span>
- </button>
- <label class="agree">
- <input type="checkbox" id="agree" />
- <span class="agree-box" aria-hidden="true"></span>
- <span>已阅读并同意 <a href="javascript:void(0)" id="linkUser">《用户协议》</a> 和 <a href="javascript:void(0)" id="linkPrivacy">《隐私政策》</a></span>
- </label>
- </form>
- <div class="divider">或</div>
- <button type="button" class="btn-wechat" id="wechatBtn">
- <img src="assets/wechat-logo.png" alt="" width="22" height="22" />
- 微信授权登录
- </button>
- </div>
- <div class="home-indicator"><span></span></div>
- <div class="toast" id="toast" role="status" aria-live="polite"></div>
- </div>
- </div>
- </div>
- </div>
- <script>
- (function () {
- var toastEl = document.getElementById('toast')
- var toastTimer = null
- function toast(msg) {
- toastEl.textContent = msg
- toastEl.classList.add('show')
- clearTimeout(toastTimer)
- toastTimer = setTimeout(function () {
- toastEl.classList.remove('show')
- }, 1800)
- }
- function ensureAgree() {
- if (!document.getElementById('agree').checked) {
- toast('请先同意用户协议与隐私政策')
- return false
- }
- return true
- }
- function clearErrors() {
- document.getElementById('username').classList.remove('is-error')
- document.getElementById('password').classList.remove('is-error')
- document.getElementById('usernameError').classList.remove('show')
- document.getElementById('passwordError').classList.remove('show')
- }
- document.getElementById('togglePwd').addEventListener('click', function () {
- var input = document.getElementById('password')
- var show = input.type === 'password'
- input.type = show ? 'text' : 'password'
- this.textContent = show ? '隐藏' : '显示'
- })
- document.getElementById('linkUser').addEventListener('click', function (e) {
- e.preventDefault()
- e.stopPropagation()
- toast('用户协议(Demo 占位)')
- })
- document.getElementById('linkPrivacy').addEventListener('click', function (e) {
- e.preventDefault()
- e.stopPropagation()
- toast('隐私政策(Demo 占位)')
- })
- document.getElementById('loginForm').addEventListener('submit', function (e) {
- e.preventDefault()
- clearErrors()
- if (!ensureAgree()) return
- var username = document.getElementById('username').value.trim()
- var password = document.getElementById('password').value
- var ok = true
- if (!username) {
- document.getElementById('username').classList.add('is-error')
- document.getElementById('usernameError').classList.add('show')
- ok = false
- }
- if (!password) {
- document.getElementById('password').classList.add('is-error')
- document.getElementById('passwordError').classList.add('show')
- ok = false
- }
- if (!ok) return
- var btn = document.getElementById('loginBtn')
- btn.disabled = true
- btn.classList.add('is-loading')
- setTimeout(function () {
- btn.disabled = false
- btn.classList.remove('is-loading')
- toast('登录成功(Demo)')
- }, 900)
- })
- document.getElementById('wechatBtn').addEventListener('click', function () {
- if (!ensureAgree()) return
- var btn = this
- btn.disabled = true
- toast('唤起微信授权(Demo)')
- setTimeout(function () {
- btn.disabled = false
- }, 900)
- })
- })()
- </script>
- </body>
- </html>
|