zkxw-mobile-login-ui-demo.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
  6. <title>中科兴皖 · 登录页 UI Demo</title>
  7. <style>
  8. :root {
  9. --brand-green: #0a9f6d;
  10. --brand-green-dark: #087f57;
  11. --brand-green-soft: #e9f9f2;
  12. --barley-yellow: #d6a343;
  13. --barley-yellow-soft: #fff7e8;
  14. --bg-page: #f5f8f7;
  15. --surface: #ffffff;
  16. --text-primary: #1f2a26;
  17. --text-muted: #6b7b74;
  18. --border: #dcebe4;
  19. --danger: #d14343;
  20. --radius-card: 16px;
  21. --radius-ctrl: 12px;
  22. --shadow-soft: 0 4px 16px rgba(8, 127, 87, 0.08);
  23. }
  24. * { box-sizing: border-box; }
  25. html, body {
  26. margin: 0;
  27. padding: 0;
  28. min-height: 100%;
  29. font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
  30. color: var(--text-primary);
  31. background:
  32. radial-gradient(900px 420px at 10% -10%, rgba(10, 159, 109, 0.14), transparent 55%),
  33. radial-gradient(700px 380px at 90% 0%, rgba(214, 163, 67, 0.12), transparent 50%),
  34. linear-gradient(180deg, #eef6f2 0%, #f7faf9 40%, #f3f6f5 100%);
  35. }
  36. body {
  37. display: flex;
  38. justify-content: center;
  39. padding: 24px 16px 40px;
  40. }
  41. #app {
  42. width: 100%;
  43. max-width: 980px;
  44. display: grid;
  45. grid-template-columns: 280px 1fr;
  46. gap: 24px;
  47. align-items: start;
  48. }
  49. @media (max-width: 860px) {
  50. #app { grid-template-columns: 1fr; }
  51. .doc-panel { position: static; }
  52. }
  53. .doc-panel {
  54. position: sticky;
  55. top: 24px;
  56. background: rgba(255, 255, 255, 0.92);
  57. border: 1px solid var(--border);
  58. border-radius: 18px;
  59. padding: 20px 18px;
  60. box-shadow: var(--shadow-soft);
  61. }
  62. .doc-panel h1 {
  63. margin: 0 0 6px;
  64. font-size: 20px;
  65. line-height: 1.3;
  66. color: var(--brand-green-dark);
  67. }
  68. .doc-panel .sub {
  69. margin: 0 0 16px;
  70. font-size: 12px;
  71. color: var(--text-muted);
  72. line-height: 1.5;
  73. }
  74. .token-row {
  75. display: flex;
  76. flex-wrap: wrap;
  77. gap: 8px;
  78. margin-bottom: 14px;
  79. }
  80. .token-chip {
  81. display: inline-flex;
  82. align-items: center;
  83. gap: 6px;
  84. font-size: 11px;
  85. color: var(--text-muted);
  86. background: #fff;
  87. border: 1px solid var(--border);
  88. border-radius: 999px;
  89. padding: 4px 8px 4px 4px;
  90. }
  91. .token-chip i {
  92. width: 14px;
  93. height: 14px;
  94. border-radius: 50%;
  95. display: block;
  96. }
  97. .notes {
  98. margin: 0;
  99. padding: 0;
  100. list-style: none;
  101. font-size: 12px;
  102. color: var(--text-muted);
  103. line-height: 1.55;
  104. }
  105. .notes li {
  106. position: relative;
  107. padding-left: 12px;
  108. margin-bottom: 6px;
  109. }
  110. .notes li::before {
  111. content: "";
  112. position: absolute;
  113. left: 0;
  114. top: 7px;
  115. width: 5px;
  116. height: 5px;
  117. border-radius: 50%;
  118. background: var(--brand-green);
  119. }
  120. .phone-stage {
  121. display: flex;
  122. justify-content: center;
  123. }
  124. .phone {
  125. width: 390px;
  126. height: 844px;
  127. background: #111;
  128. border-radius: 42px;
  129. padding: 12px;
  130. box-shadow:
  131. 0 24px 60px rgba(15, 40, 30, 0.18),
  132. inset 0 0 0 1px rgba(255, 255, 255, 0.08);
  133. position: relative;
  134. }
  135. .phone::before {
  136. content: "";
  137. position: absolute;
  138. top: 18px;
  139. left: 50%;
  140. transform: translateX(-50%);
  141. width: 118px;
  142. height: 28px;
  143. background: #0a0a0a;
  144. border-radius: 20px;
  145. z-index: 5;
  146. }
  147. .phone-screen {
  148. width: 100%;
  149. height: 100%;
  150. border-radius: 32px;
  151. overflow: hidden;
  152. background: var(--bg-page);
  153. position: relative;
  154. display: flex;
  155. flex-direction: column;
  156. }
  157. .status-bar {
  158. height: 54px;
  159. padding: 16px 22px 0;
  160. display: flex;
  161. justify-content: space-between;
  162. align-items: center;
  163. font-size: 12px;
  164. font-weight: 600;
  165. color: var(--text-primary);
  166. position: relative;
  167. z-index: 2;
  168. flex-shrink: 0;
  169. }
  170. .status-bar .icons {
  171. display: flex;
  172. gap: 5px;
  173. align-items: center;
  174. opacity: 0.75;
  175. font-size: 10px;
  176. }
  177. .login-scroll {
  178. flex: 1;
  179. overflow-y: auto;
  180. padding: 12px 24px 28px;
  181. background:
  182. radial-gradient(260px 160px at 8% -8%, rgba(10, 159, 109, 0.16), transparent 60%),
  183. radial-gradient(220px 140px at 96% 4%, rgba(214, 163, 67, 0.14), transparent 55%),
  184. linear-gradient(180deg, #eef6f2 0%, #f5f8f7 48%, #f3f6f5 100%);
  185. }
  186. .brand {
  187. text-align: center;
  188. padding: 28px 0 22px;
  189. }
  190. .brand-mark {
  191. width: 64px;
  192. height: 64px;
  193. margin: 0 auto 12px;
  194. border-radius: 18px;
  195. background: linear-gradient(145deg, var(--brand-green-dark), var(--brand-green));
  196. color: #fff;
  197. font-size: 26px;
  198. font-weight: 700;
  199. display: flex;
  200. align-items: center;
  201. justify-content: center;
  202. box-shadow: 0 10px 24px rgba(8, 127, 87, 0.28);
  203. letter-spacing: 0.02em;
  204. }
  205. .brand h2 {
  206. margin: 0;
  207. font-size: 22px;
  208. font-weight: 650;
  209. letter-spacing: 0.03em;
  210. }
  211. .brand p {
  212. margin: 8px 0 0;
  213. font-size: 13px;
  214. color: var(--text-muted);
  215. }
  216. .form-card {
  217. background: var(--surface);
  218. border: 1px solid var(--border);
  219. border-radius: var(--radius-card);
  220. padding: 20px 18px 18px;
  221. box-shadow: var(--shadow-soft);
  222. }
  223. .field {
  224. margin-bottom: 14px;
  225. }
  226. .field label {
  227. display: block;
  228. font-size: 12px;
  229. color: var(--text-muted);
  230. margin-bottom: 6px;
  231. }
  232. .field-control {
  233. position: relative;
  234. display: flex;
  235. align-items: center;
  236. }
  237. .field-control input {
  238. width: 100%;
  239. height: 44px;
  240. border: 1px solid var(--border);
  241. border-radius: 10px;
  242. background: #f8fbfa;
  243. padding: 0 12px;
  244. font-size: 14px;
  245. color: var(--text-primary);
  246. outline: none;
  247. transition: border-color .15s, box-shadow .15s;
  248. }
  249. .field-control input:focus {
  250. border-color: var(--brand-green);
  251. box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.12);
  252. background: #fff;
  253. }
  254. .field-control input.is-error {
  255. border-color: var(--danger);
  256. }
  257. .field-control.with-action input {
  258. padding-right: 52px;
  259. }
  260. .field-action {
  261. position: absolute;
  262. right: 10px;
  263. appearance: none;
  264. border: none;
  265. background: transparent;
  266. color: var(--brand-green);
  267. font-size: 12px;
  268. font-weight: 500;
  269. cursor: pointer;
  270. padding: 6px;
  271. min-height: 32px;
  272. }
  273. .field-error {
  274. margin-top: 6px;
  275. font-size: 12px;
  276. color: var(--danger);
  277. display: none;
  278. }
  279. .field-error.show { display: block; }
  280. .btn-primary {
  281. width: 100%;
  282. height: 44px;
  283. border: none;
  284. border-radius: var(--radius-ctrl);
  285. background: var(--brand-green);
  286. color: #fff;
  287. font-size: 15px;
  288. font-weight: 600;
  289. cursor: pointer;
  290. display: inline-flex;
  291. align-items: center;
  292. justify-content: center;
  293. gap: 8px;
  294. transition: background .15s, opacity .15s;
  295. }
  296. .btn-primary:hover { background: var(--brand-green-dark); }
  297. .btn-primary:disabled {
  298. opacity: 0.55;
  299. cursor: not-allowed;
  300. }
  301. .btn-primary .spinner {
  302. width: 16px;
  303. height: 16px;
  304. border: 2px solid rgba(255,255,255,0.35);
  305. border-top-color: #fff;
  306. border-radius: 50%;
  307. animation: spin .7s linear infinite;
  308. display: none;
  309. }
  310. .btn-primary.is-loading .spinner { display: block; }
  311. .btn-primary.is-loading .btn-text { opacity: 0.9; }
  312. @keyframes spin { to { transform: rotate(360deg); } }
  313. .agree {
  314. display: flex;
  315. align-items: flex-start;
  316. gap: 8px;
  317. margin-top: 14px;
  318. font-size: 12px;
  319. color: var(--text-muted);
  320. line-height: 1.45;
  321. cursor: pointer;
  322. user-select: none;
  323. }
  324. .agree input {
  325. position: absolute;
  326. opacity: 0;
  327. pointer-events: none;
  328. }
  329. .agree-box {
  330. width: 16px;
  331. height: 16px;
  332. border-radius: 50%;
  333. border: 1.5px solid var(--brand-green);
  334. flex-shrink: 0;
  335. margin-top: 1px;
  336. position: relative;
  337. transition: background .15s;
  338. }
  339. .agree input:checked + .agree-box {
  340. background: var(--brand-green);
  341. }
  342. .agree input:checked + .agree-box::after {
  343. content: "";
  344. position: absolute;
  345. left: 4px;
  346. top: 2px;
  347. width: 5px;
  348. height: 8px;
  349. border: solid #fff;
  350. border-width: 0 2px 2px 0;
  351. transform: rotate(45deg);
  352. }
  353. .agree a {
  354. color: var(--brand-green);
  355. text-decoration: none;
  356. }
  357. .agree a:hover { text-decoration: underline; }
  358. .divider {
  359. display: flex;
  360. align-items: center;
  361. gap: 12px;
  362. margin: 22px 0 14px;
  363. font-size: 12px;
  364. color: var(--text-muted);
  365. }
  366. .divider::before,
  367. .divider::after {
  368. content: "";
  369. flex: 1;
  370. height: 1px;
  371. background: var(--border);
  372. }
  373. .btn-wechat {
  374. width: 100%;
  375. height: 44px;
  376. border-radius: var(--radius-ctrl);
  377. border: 1px solid var(--border);
  378. background: #fff;
  379. color: var(--text-primary);
  380. font-size: 14px;
  381. font-weight: 500;
  382. cursor: pointer;
  383. display: inline-flex;
  384. align-items: center;
  385. justify-content: center;
  386. gap: 8px;
  387. transition: background .15s, border-color .15s;
  388. }
  389. .btn-wechat:hover {
  390. background: #f8fbfa;
  391. border-color: #b7d9cb;
  392. }
  393. .btn-wechat:disabled {
  394. opacity: 0.55;
  395. cursor: not-allowed;
  396. }
  397. .btn-wechat img {
  398. width: 22px;
  399. height: 22px;
  400. object-fit: contain;
  401. display: block;
  402. }
  403. .home-indicator {
  404. height: 22px;
  405. display: flex;
  406. align-items: flex-end;
  407. justify-content: center;
  408. padding-bottom: 8px;
  409. flex-shrink: 0;
  410. background: transparent;
  411. position: absolute;
  412. left: 0;
  413. right: 0;
  414. bottom: 0;
  415. pointer-events: none;
  416. }
  417. .home-indicator span {
  418. width: 120px;
  419. height: 4px;
  420. border-radius: 4px;
  421. background: rgba(31, 42, 38, 0.22);
  422. }
  423. .toast {
  424. position: absolute;
  425. left: 50%;
  426. top: 46%;
  427. transform: translate(-50%, -50%);
  428. background: rgba(31, 42, 38, 0.88);
  429. color: #fff;
  430. font-size: 13px;
  431. padding: 10px 16px;
  432. border-radius: 10px;
  433. max-width: 78%;
  434. text-align: center;
  435. line-height: 1.4;
  436. opacity: 0;
  437. pointer-events: none;
  438. transition: opacity .2s;
  439. z-index: 20;
  440. }
  441. .toast.show { opacity: 1; }
  442. </style>
  443. </head>
  444. <body>
  445. <div id="app">
  446. <aside class="doc-panel">
  447. <h1>登录页 Demo</h1>
  448. <p class="sub">方案 B · 账号密码为主 · 微信授权为次<br/>权威说明见设计文档</p>
  449. <div class="token-row">
  450. <span class="token-chip"><i style="background:#0a9f6d"></i>#0a9f6d</span>
  451. <span class="token-chip"><i style="background:#087f57"></i>#087f57</span>
  452. <span class="token-chip"><i style="background:#f5f8f7;border:1px solid #dcebe4"></i>#f5f8f7</span>
  453. </div>
  454. <ul class="notes">
  455. <li>账号:用户名 / 工号(与 web-admin 一致)</li>
  456. <li>未勾选协议时拦截两种登录</li>
  457. <li>微信按钮使用 <code>assets/wechat-logo.png</code></li>
  458. <li>本期仅 UI 模拟,不联调 API</li>
  459. <li>设计文档:<code>docs/superpowers/specs/2026-08-10-miniprogram-login-ui-design.md</code></li>
  460. </ul>
  461. </aside>
  462. <div class="phone-stage">
  463. <div class="phone">
  464. <div class="phone-screen">
  465. <div class="status-bar">
  466. <span>9:41</span>
  467. <span class="icons">▌▌▌ Wi‑Fi 100%</span>
  468. </div>
  469. <div class="login-scroll">
  470. <div class="brand">
  471. <div class="brand-mark" aria-hidden="true">兴</div>
  472. <h2>中科兴皖</h2>
  473. <p>员工工作台登录</p>
  474. </div>
  475. <form class="form-card" id="loginForm" novalidate>
  476. <div class="field">
  477. <label for="username">账号</label>
  478. <div class="field-control">
  479. <input id="username" name="username" type="text" autocomplete="username" placeholder="请输入用户名 / 工号" />
  480. </div>
  481. <div class="field-error" id="usernameError">请输入用户名 / 工号</div>
  482. </div>
  483. <div class="field">
  484. <label for="password">密码</label>
  485. <div class="field-control with-action">
  486. <input id="password" name="password" type="password" autocomplete="current-password" placeholder="请输入密码" />
  487. <button type="button" class="field-action" id="togglePwd" aria-label="显示或隐藏密码">显示</button>
  488. </div>
  489. <div class="field-error" id="passwordError">请输入密码</div>
  490. </div>
  491. <button type="submit" class="btn-primary" id="loginBtn">
  492. <span class="spinner" aria-hidden="true"></span>
  493. <span class="btn-text">登录</span>
  494. </button>
  495. <label class="agree">
  496. <input type="checkbox" id="agree" />
  497. <span class="agree-box" aria-hidden="true"></span>
  498. <span>已阅读并同意 <a href="javascript:void(0)" id="linkUser">《用户协议》</a> 和 <a href="javascript:void(0)" id="linkPrivacy">《隐私政策》</a></span>
  499. </label>
  500. </form>
  501. <div class="divider">或</div>
  502. <button type="button" class="btn-wechat" id="wechatBtn">
  503. <img src="assets/wechat-logo.png" alt="" width="22" height="22" />
  504. 微信授权登录
  505. </button>
  506. </div>
  507. <div class="home-indicator"><span></span></div>
  508. <div class="toast" id="toast" role="status" aria-live="polite"></div>
  509. </div>
  510. </div>
  511. </div>
  512. </div>
  513. <script>
  514. (function () {
  515. var toastEl = document.getElementById('toast')
  516. var toastTimer = null
  517. function toast(msg) {
  518. toastEl.textContent = msg
  519. toastEl.classList.add('show')
  520. clearTimeout(toastTimer)
  521. toastTimer = setTimeout(function () {
  522. toastEl.classList.remove('show')
  523. }, 1800)
  524. }
  525. function ensureAgree() {
  526. if (!document.getElementById('agree').checked) {
  527. toast('请先同意用户协议与隐私政策')
  528. return false
  529. }
  530. return true
  531. }
  532. function clearErrors() {
  533. document.getElementById('username').classList.remove('is-error')
  534. document.getElementById('password').classList.remove('is-error')
  535. document.getElementById('usernameError').classList.remove('show')
  536. document.getElementById('passwordError').classList.remove('show')
  537. }
  538. document.getElementById('togglePwd').addEventListener('click', function () {
  539. var input = document.getElementById('password')
  540. var show = input.type === 'password'
  541. input.type = show ? 'text' : 'password'
  542. this.textContent = show ? '隐藏' : '显示'
  543. })
  544. document.getElementById('linkUser').addEventListener('click', function (e) {
  545. e.preventDefault()
  546. e.stopPropagation()
  547. toast('用户协议(Demo 占位)')
  548. })
  549. document.getElementById('linkPrivacy').addEventListener('click', function (e) {
  550. e.preventDefault()
  551. e.stopPropagation()
  552. toast('隐私政策(Demo 占位)')
  553. })
  554. document.getElementById('loginForm').addEventListener('submit', function (e) {
  555. e.preventDefault()
  556. clearErrors()
  557. if (!ensureAgree()) return
  558. var username = document.getElementById('username').value.trim()
  559. var password = document.getElementById('password').value
  560. var ok = true
  561. if (!username) {
  562. document.getElementById('username').classList.add('is-error')
  563. document.getElementById('usernameError').classList.add('show')
  564. ok = false
  565. }
  566. if (!password) {
  567. document.getElementById('password').classList.add('is-error')
  568. document.getElementById('passwordError').classList.add('show')
  569. ok = false
  570. }
  571. if (!ok) return
  572. var btn = document.getElementById('loginBtn')
  573. btn.disabled = true
  574. btn.classList.add('is-loading')
  575. setTimeout(function () {
  576. btn.disabled = false
  577. btn.classList.remove('is-loading')
  578. toast('登录成功(Demo)')
  579. }, 900)
  580. })
  581. document.getElementById('wechatBtn').addEventListener('click', function () {
  582. if (!ensureAgree()) return
  583. var btn = this
  584. btn.disabled = true
  585. toast('唤起微信授权(Demo)')
  586. setTimeout(function () {
  587. btn.disabled = false
  588. }, 900)
  589. })
  590. })()
  591. </script>
  592. </body>
  593. </html>