web-admin-login-wx-qr-demo.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  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" />
  6. <title>山峡一体化门户 · 登录页 Demo(扫码 + 账号密码)</title>
  7. <style>
  8. :root {
  9. --primary: #10b981;
  10. --primary-dark: #059669;
  11. --primary-bg: #ecfdf5;
  12. --text: #1e293b;
  13. --muted: #64748b;
  14. --border: #e5e7eb;
  15. --danger: #dc2626;
  16. }
  17. * { box-sizing: border-box; margin: 0; padding: 0; }
  18. body {
  19. font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
  20. color: var(--text);
  21. min-height: 100vh;
  22. }
  23. .page {
  24. min-height: 100vh;
  25. display: flex;
  26. justify-content: flex-end;
  27. align-items: center;
  28. padding: 24px 200px 56px 24px;
  29. position: relative;
  30. background:
  31. linear-gradient(120deg, rgba(15, 23, 42, 0.35), rgba(5, 150, 105, 0.25)),
  32. radial-gradient(ellipse at 20% 30%, #86efac 0%, transparent 50%),
  33. linear-gradient(135deg, #0f766e 0%, #134e4a 40%, #1e293b 100%);
  34. background-size: cover;
  35. background-position: center;
  36. }
  37. .page::before {
  38. content: "";
  39. position: absolute;
  40. inset: 0;
  41. background:
  42. url("data:image/svg+xml,%3Csvg width='60' height='60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-opacity='0.06'%3E%3Cpath d='M0 0h60v60H0z'/%3E%3Cpath stroke='%23fff' stroke-width='1' d='M0 30h60M30 0v60'/%3E%3C/g%3E%3C/svg%3E");
  43. opacity: 0.5;
  44. pointer-events: none;
  45. }
  46. .login-card {
  47. width: 400px;
  48. padding: 28px 28px 20px;
  49. border-radius: 10px;
  50. background: rgba(255, 255, 255, 0.72);
  51. backdrop-filter: blur(12px);
  52. border: 1px solid rgba(255, 255, 255, 0.35);
  53. box-shadow: 0 20px 50px rgba(15, 23, 42, 0.18);
  54. position: relative;
  55. z-index: 1;
  56. }
  57. .brand {
  58. display: flex;
  59. flex-direction: column;
  60. align-items: center;
  61. margin-bottom: 8px;
  62. }
  63. .brand-mark {
  64. width: 180px;
  65. height: 40px;
  66. border-radius: 8px;
  67. background: linear-gradient(90deg, var(--primary), var(--primary-dark));
  68. color: #fff;
  69. display: flex;
  70. align-items: center;
  71. justify-content: center;
  72. font-weight: 700;
  73. letter-spacing: 2px;
  74. font-size: 15px;
  75. margin-bottom: 10px;
  76. }
  77. .brand h1 {
  78. font-size: 20px;
  79. font-weight: 600;
  80. margin-bottom: 6px;
  81. }
  82. .brand .hint {
  83. font-size: 13px;
  84. color: var(--muted);
  85. text-align: center;
  86. min-height: 20px;
  87. margin-bottom: 16px;
  88. }
  89. .tabs {
  90. display: grid;
  91. grid-template-columns: 1fr 1fr;
  92. border-bottom: 1px solid var(--border);
  93. margin-bottom: 20px;
  94. }
  95. .tab {
  96. appearance: none;
  97. border: 0;
  98. background: transparent;
  99. padding: 12px 8px;
  100. font-size: 15px;
  101. color: var(--muted);
  102. cursor: pointer;
  103. position: relative;
  104. font-family: inherit;
  105. }
  106. .tab:hover { color: var(--primary); }
  107. .tab.active {
  108. color: var(--primary-dark);
  109. font-weight: 600;
  110. }
  111. .tab.active::after {
  112. content: "";
  113. position: absolute;
  114. left: 16%;
  115. right: 16%;
  116. bottom: -1px;
  117. height: 2px;
  118. background: var(--primary);
  119. border-radius: 2px;
  120. }
  121. .panel { display: none; }
  122. .panel.active { display: block; }
  123. .qr-box {
  124. display: flex;
  125. flex-direction: column;
  126. align-items: center;
  127. gap: 12px;
  128. padding-top: 4px;
  129. }
  130. .qr-frame {
  131. width: 200px;
  132. height: 200px;
  133. border: 1px solid var(--border);
  134. border-radius: 10px;
  135. background: #fff;
  136. display: flex;
  137. align-items: center;
  138. justify-content: center;
  139. overflow: hidden;
  140. position: relative;
  141. }
  142. .qr-frame svg { width: 168px; height: 168px; }
  143. .qr-overlay {
  144. position: absolute;
  145. inset: 0;
  146. background: rgba(255, 255, 255, 0.88);
  147. display: none;
  148. align-items: center;
  149. justify-content: center;
  150. text-align: center;
  151. padding: 16px;
  152. font-size: 13px;
  153. color: var(--muted);
  154. }
  155. .qr-overlay.show { display: flex; }
  156. .qr-status {
  157. font-size: 13px;
  158. color: var(--muted);
  159. text-align: center;
  160. min-height: 20px;
  161. }
  162. .qr-status.error { color: var(--danger); }
  163. .qr-status.ok { color: var(--primary-dark); }
  164. .btn {
  165. appearance: none;
  166. border: 1px solid var(--border);
  167. background: #fff;
  168. color: var(--text);
  169. border-radius: 8px;
  170. height: 40px;
  171. padding: 0 16px;
  172. font-size: 14px;
  173. cursor: pointer;
  174. display: inline-flex;
  175. align-items: center;
  176. gap: 6px;
  177. font-family: inherit;
  178. }
  179. .btn:hover { border-color: var(--primary); color: var(--primary-dark); }
  180. .btn-primary {
  181. width: 100%;
  182. height: 48px;
  183. border: 0;
  184. color: #fff;
  185. font-size: 16px;
  186. background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
  187. }
  188. .btn-primary:hover { opacity: 0.92; }
  189. .field {
  190. margin-bottom: 18px;
  191. }
  192. .field label {
  193. display: block;
  194. font-size: 12px;
  195. color: var(--muted);
  196. margin-bottom: 6px;
  197. }
  198. .input {
  199. width: 100%;
  200. height: 44px;
  201. border: 1px solid var(--border);
  202. border-radius: 8px;
  203. padding: 0 14px;
  204. font-size: 15px;
  205. background: rgba(255, 255, 255, 0.9);
  206. font-family: inherit;
  207. color: var(--text);
  208. outline: none;
  209. }
  210. .input:focus {
  211. border-color: var(--primary);
  212. box-shadow: 0 0 0 3px var(--primary-bg);
  213. }
  214. .footer {
  215. position: fixed;
  216. left: 0;
  217. right: 0;
  218. bottom: 0;
  219. height: 40px;
  220. line-height: 40px;
  221. text-align: center;
  222. color: rgba(255, 255, 255, 0.9);
  223. font-size: 12px;
  224. letter-spacing: 1px;
  225. z-index: 2;
  226. }
  227. /* Demo controls */
  228. .demo-bar {
  229. position: fixed;
  230. left: 20px;
  231. top: 20px;
  232. z-index: 10;
  233. width: min(360px, calc(100vw - 40px));
  234. background: rgba(15, 23, 42, 0.88);
  235. color: #e2e8f0;
  236. border-radius: 12px;
  237. padding: 14px 16px;
  238. font-size: 13px;
  239. backdrop-filter: blur(8px);
  240. box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
  241. }
  242. .demo-bar h2 {
  243. font-size: 14px;
  244. margin-bottom: 8px;
  245. color: #fff;
  246. }
  247. .demo-bar p {
  248. color: #94a3b8;
  249. line-height: 1.5;
  250. margin-bottom: 10px;
  251. }
  252. .demo-actions {
  253. display: flex;
  254. flex-wrap: wrap;
  255. gap: 8px;
  256. }
  257. .demo-actions button {
  258. appearance: none;
  259. border: 1px solid #334155;
  260. background: #1e293b;
  261. color: #e2e8f0;
  262. border-radius: 6px;
  263. padding: 6px 10px;
  264. font-size: 12px;
  265. cursor: pointer;
  266. font-family: inherit;
  267. }
  268. .demo-actions button:hover {
  269. border-color: var(--primary);
  270. color: #a7f3d0;
  271. }
  272. @media (max-width: 768px) {
  273. .page {
  274. justify-content: center;
  275. padding: 72px 16px 64px;
  276. }
  277. .login-card { width: min(400px, 100%); }
  278. .demo-bar {
  279. left: 12px;
  280. right: 12px;
  281. width: auto;
  282. }
  283. }
  284. </style>
  285. </head>
  286. <body>
  287. <aside class="demo-bar">
  288. <h2>登录页交互 Demo</h2>
  289. <p>
  290. 默认「微信扫码」优先;可切换到「账号密码」。左侧按钮可模拟扫码各状态(仅演示,无真实接口)。
  291. </p>
  292. <div class="demo-actions">
  293. <button type="button" data-sim="creating">生成中</button>
  294. <button type="button" data-sim="pending">待扫码</button>
  295. <button type="button" data-sim="scanned">已扫码</button>
  296. <button type="button" data-sim="success">登录成功</button>
  297. <button type="button" data-sim="expired">已过期</button>
  298. <button type="button" data-sim="failed">失败</button>
  299. </div>
  300. </aside>
  301. <div class="page">
  302. <div class="login-card">
  303. <div class="brand">
  304. <div class="brand-mark">山峡化工</div>
  305. <h1>山峡一体化门户</h1>
  306. <p class="hint" id="subtitle">微信扫一扫,授权手机号后自动登录</p>
  307. </div>
  308. <div class="tabs" role="tablist">
  309. <button type="button" class="tab active" data-tab="wechat" role="tab">微信扫码</button>
  310. <button type="button" class="tab" data-tab="password" role="tab">账号密码</button>
  311. </div>
  312. <div class="panel active" id="panel-wechat" role="tabpanel">
  313. <div class="qr-box">
  314. <div class="qr-frame">
  315. <!-- Decorative QR placeholder -->
  316. <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
  317. <rect width="100" height="100" fill="#fff"/>
  318. <rect x="8" y="8" width="28" height="28" fill="#0f172a"/>
  319. <rect x="14" y="14" width="16" height="16" fill="#fff"/>
  320. <rect x="18" y="18" width="8" height="8" fill="#0f172a"/>
  321. <rect x="64" y="8" width="28" height="28" fill="#0f172a"/>
  322. <rect x="70" y="14" width="16" height="16" fill="#fff"/>
  323. <rect x="74" y="18" width="8" height="8" fill="#0f172a"/>
  324. <rect x="8" y="64" width="28" height="28" fill="#0f172a"/>
  325. <rect x="14" y="70" width="16" height="16" fill="#fff"/>
  326. <rect x="18" y="74" width="8" height="8" fill="#0f172a"/>
  327. <rect x="44" y="8" width="8" height="8" fill="#0f172a"/>
  328. <rect x="56" y="20" width="8" height="8" fill="#0f172a"/>
  329. <rect x="44" y="32" width="8" height="8" fill="#0f172a"/>
  330. <rect x="44" y="44" width="12" height="12" fill="#10b981"/>
  331. <rect x="64" y="44" width="8" height="8" fill="#0f172a"/>
  332. <rect x="80" y="44" width="8" height="8" fill="#0f172a"/>
  333. <rect x="44" y="64" width="8" height="8" fill="#0f172a"/>
  334. <rect x="56" y="72" width="8" height="8" fill="#0f172a"/>
  335. <rect x="72" y="64" width="20" height="8" fill="#0f172a"/>
  336. <rect x="72" y="80" width="8" height="12" fill="#0f172a"/>
  337. <rect x="88" y="72" width="4" height="20" fill="#0f172a"/>
  338. </svg>
  339. <div class="qr-overlay" id="qrOverlay"></div>
  340. </div>
  341. <p class="qr-status" id="qrStatus">请使用微信扫一扫</p>
  342. <button type="button" class="btn" id="refreshBtn">↻ 刷新二维码</button>
  343. </div>
  344. </div>
  345. <div class="panel" id="panel-password" role="tabpanel">
  346. <form id="pwdForm" onsubmit="return false;">
  347. <div class="field">
  348. <label for="username">用户名</label>
  349. <input class="input" id="username" type="text" placeholder="请输入用户名" autocomplete="username" />
  350. </div>
  351. <div class="field">
  352. <label for="password">密码</label>
  353. <input class="input" id="password" type="password" placeholder="请输入密码" autocomplete="current-password" />
  354. </div>
  355. <button type="button" class="btn btn-primary" id="loginBtn">登 录</button>
  356. </form>
  357. </div>
  358. </div>
  359. </div>
  360. <div class="footer">
  361. Copyright © 2025–2026 山峡化工有限公司 All Rights Reserved.
  362. </div>
  363. <script>
  364. const subtitle = document.getElementById('subtitle')
  365. const qrStatus = document.getElementById('qrStatus')
  366. const qrOverlay = document.getElementById('qrOverlay')
  367. const tabs = document.querySelectorAll('.tab')
  368. const panels = {
  369. wechat: document.getElementById('panel-wechat'),
  370. password: document.getElementById('panel-password')
  371. }
  372. const statusMap = {
  373. creating: { text: '正在生成二维码…', cls: '', overlay: '生成中…' },
  374. pending: { text: '请使用微信扫一扫', cls: '', overlay: '' },
  375. scanned: { text: '已扫码,请在手机上确认授权', cls: 'ok', overlay: '已扫码\n请在手机确认' },
  376. success: { text: '登录成功,正在跳转…', cls: 'ok', overlay: '✓ 登录成功' },
  377. expired: { text: '二维码已过期,请刷新后重扫', cls: 'error', overlay: '已过期\n请刷新' },
  378. failed: { text: '扫码登录失败,请刷新后重试', cls: 'error', overlay: '登录失败' }
  379. }
  380. function setQrStatus(key) {
  381. const conf = statusMap[key] || statusMap.pending
  382. qrStatus.textContent = conf.text
  383. qrStatus.className = 'qr-status' + (conf.cls ? ' ' + conf.cls : '')
  384. if (conf.overlay) {
  385. qrOverlay.textContent = conf.overlay
  386. qrOverlay.classList.add('show')
  387. } else {
  388. qrOverlay.classList.remove('show')
  389. qrOverlay.textContent = ''
  390. }
  391. }
  392. function switchTab(name) {
  393. tabs.forEach((t) => t.classList.toggle('active', t.dataset.tab === name))
  394. Object.keys(panels).forEach((k) => panels[k].classList.toggle('active', k === name))
  395. subtitle.textContent =
  396. name === 'wechat'
  397. ? '微信扫一扫,授权手机号后自动登录'
  398. : '请输入账号密码登录系统'
  399. }
  400. tabs.forEach((tab) => {
  401. tab.addEventListener('click', () => switchTab(tab.dataset.tab))
  402. })
  403. document.querySelectorAll('[data-sim]').forEach((btn) => {
  404. btn.addEventListener('click', () => {
  405. switchTab('wechat')
  406. setQrStatus(btn.dataset.sim)
  407. })
  408. })
  409. document.getElementById('refreshBtn').addEventListener('click', () => {
  410. setQrStatus('creating')
  411. setTimeout(() => setQrStatus('pending'), 600)
  412. })
  413. document.getElementById('loginBtn').addEventListener('click', () => {
  414. const u = document.getElementById('username').value.trim()
  415. const p = document.getElementById('password').value.trim()
  416. if (!u || !p) {
  417. alert('请输入用户名和密码(Demo 仅做前端校验)')
  418. return
  419. }
  420. alert('Demo:账号密码登录成功(不会真实请求后端)')
  421. })
  422. setQrStatus('pending')
  423. </script>
  424. </body>
  425. </html>