login.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <template>
  2. <main class="login-page">
  3. <div class="glow glow-one"></div>
  4. <div class="glow glow-two"></div>
  5. <div class="stage">
  6. <section class="panel" aria-label="登录">
  7. <div class="brand-side">
  8. <div class="brand-top">
  9. <img class="logo" src="/logo.png" alt="平台 Logo">
  10. <div class="brand-name">沅陵县城市地下管网管控平台</div>
  11. </div>
  12. <div class="hero">
  13. <h1>汇聚地下管网运行态势<br><span>统览城市生命线安全</span></h1>
  14. <p>供水 · 排水 · 燃气 · 窨井盖全域感知,智能预警联动处置,<br>协同应急指挥调度,守护城市运行安全。</p>
  15. <div class="tags"><span>实时监测</span><span>智能预警</span><span>联动处置</span><span>应急指挥</span></div>
  16. </div>
  17. <div class="illustration" aria-hidden="true">
  18. <svg viewBox="0 0 520 360" xmlns="http://www.w3.org/2000/svg">
  19. <defs>
  20. <linearGradient id="login-p1" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#7db4ff" /><stop offset="1" stop-color="#2f7cf6" /></linearGradient>
  21. <linearGradient id="login-p2" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#a9d2ff" /><stop offset="1" stop-color="#5ba0f5" /></linearGradient>
  22. <linearGradient id="login-p3" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fff" /><stop offset="1" stop-color="#d8e7ff" /></linearGradient>
  23. <linearGradient id="login-p4" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#4fa0ff" /><stop offset="1" stop-color="#1e66e8" /></linearGradient>
  24. <linearGradient id="login-screen" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#eaf3ff" /><stop offset="1" stop-color="#c7ddff" /></linearGradient>
  25. </defs>
  26. <ellipse cx="260" cy="300" rx="210" ry="46" fill="#b9d4ff" opacity=".55" />
  27. <ellipse cx="260" cy="288" rx="210" ry="46" fill="url(#login-p3)" />
  28. <ellipse cx="260" cy="286" rx="168" ry="35" fill="none" stroke="#bcd2f5" stroke-width="2" />
  29. <polygon points="150,250 260,300 370,250 260,205" fill="url(#login-p2)" opacity=".9" />
  30. <polygon points="150,250 260,300 260,322 150,272" fill="#3d82e6" />
  31. <polygon points="370,250 260,300 260,322 370,272" fill="#2b6bd0" />
  32. <polygon points="185,232 260,266 335,232 260,200" fill="#e8f2ff" />
  33. <g>
  34. <polygon points="188,205 252,176 252,214 188,243" fill="url(#login-p4)" /><polygon points="252,176 316,205 316,243 252,214" fill="#6ea8fb" /><polygon points="188,205 252,176 316,205 252,234" fill="url(#login-p1)" />
  35. <circle cx="220" cy="209" r="7" fill="#dceaff" /><circle cx="220" cy="209" r="3.5" fill="#2f7cf6" /><circle cx="286" cy="224" r="7" fill="#dceaff" /><circle cx="286" cy="224" r="3.5" fill="#2f7cf6" />
  36. </g>
  37. <g>
  38. <polygon points="300,120 402,80 438,100 330,146" fill="#fff" /><polygon points="330,146 438,100 438,172 330,214" fill="#cfe1ff" /><polygon points="300,120 330,146 330,214 300,186" fill="#9ec2fb" /><polygon points="314,132 392,102 414,116 330,150" fill="url(#login-screen)" />
  39. <polyline points="322,140 345,128 362,136 380,118 402,124" fill="none" stroke="#2f7cf6" stroke-width="2.5" stroke-linecap="round" /><rect x="338" y="160" width="66" height="6" rx="3" fill="#9ec2fb" /><rect x="338" y="174" width="44" height="6" rx="3" fill="#bcd4fb" />
  40. </g>
  41. <g>
  42. <rect x="96" y="150" width="86" height="58" rx="8" fill="#fff" /><rect x="106" y="160" width="40" height="7" rx="3.5" fill="#8fb9f8" /><rect x="106" y="174" width="66" height="5" rx="2.5" fill="#d3e3fb" /><rect x="106" y="185" width="52" height="5" rx="2.5" fill="#d3e3fb" />
  43. <circle cx="166" cy="196" r="9" fill="#e7f1ff" stroke="#8fb9f8" stroke-width="2" /><path d="M162 196l3 3 5-6" stroke="#2f7cf6" stroke-width="2" fill="none" stroke-linecap="round" />
  44. </g>
  45. <g><polygon points="218,178 240,168 240,214 218,224" fill="#2b6bd0" /><polygon points="240,168 262,178 262,224 240,214" fill="#5f9af7" /><ellipse cx="240" cy="173" rx="22" ry="10" fill="#8fbbff" /><ellipse cx="240" cy="173" rx="11" ry="5" fill="#2563d8" /></g>
  46. <circle cx="128" cy="250" r="12" fill="#cfe2ff" stroke="#9ec2fb" stroke-width="2" /><circle cx="392" cy="250" r="9" fill="#dceaff" stroke="#9ec2fb" stroke-width="2" /><circle cx="150" cy="120" r="6" fill="#7db4ff" opacity=".7" /><circle cx="440" cy="220" r="5" fill="#16b4f2" opacity=".7" />
  47. <path d="M150 208 Q200 120 310 130" fill="none" stroke="#7db4ff" stroke-width="2" stroke-dasharray="5 6" /><circle cx="150" cy="208" r="4" fill="#2f7cf6" /><circle cx="310" cy="130" r="4" fill="#16b4f2" />
  48. </svg>
  49. </div>
  50. </div>
  51. <div class="form-side">
  52. <div class="login-tabs"><span>账号登录</span></div>
  53. <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form" @submit.prevent="handleLogin">
  54. <el-form-item prop="username" class="field">
  55. <label for="login-username">账号</label>
  56. <div class="input-box">
  57. <svg viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" /></svg>
  58. <input id="login-username" v-model.trim="loginForm.username" type="text" autocomplete="username" placeholder="请输入用户名/手机号">
  59. </div>
  60. </el-form-item>
  61. <el-form-item prop="password" class="field">
  62. <label for="login-password">密码</label>
  63. <div class="input-box">
  64. <svg viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1s3.1 1.39 3.1 3.1v2z" /></svg>
  65. <input id="login-password" v-model="loginForm.password" type="password" autocomplete="current-password" placeholder="请输入密码" @keyup.enter="handleLogin">
  66. </div>
  67. </el-form-item>
  68. <el-form-item v-if="captchaEnabled" prop="code" class="field">
  69. <label for="login-code">验证码</label>
  70. <div class="input-box code-box">
  71. <svg viewBox="0 0 24 24"><path d="M9 11H7v6h2v-6zm4 0h-2v6h2v-6zm4 0h-2v6h2v-6zm2.5-9H18V0h-2v2H8V0H6v2H4.5C3.67 2 3 2.67 3 3.5v15c0 .83.67 1.5 1.5 1.5h15c.83 0 1.5-.67 1.5-1.5v-15c0-.83-.67-1.5-1.5-1.5zM19 18H5V8h14v10z" /></svg>
  72. <input id="login-code" v-model.trim="loginForm.code" type="text" autocomplete="off" placeholder="请输入计算结果" maxlength="6" @keyup.enter="handleLogin">
  73. <button type="button" class="captcha" title="点击刷新验证码" @click="getCode"><img :src="codeUrl" alt="验证码"></button>
  74. </div>
  75. </el-form-item>
  76. <div class="form-row">
  77. <label class="remember"><input v-model="loginForm.rememberMe" type="checkbox">记住账号</label>
  78. <button type="button" class="forgot" @click="showForgotPassword">忘记密码?</button>
  79. </div>
  80. <button type="submit" class="login-button" :disabled="loading">{{ loading ? '登 录 中...' : '立 即 登 录' }}</button>
  81. </el-form>
  82. <div class="mini-qr">
  83. <div class="qr-box">
  84. <img v-if="qrAvailable" :src="mobileAppQrUrl" alt="移动端 APP 下载二维码" @error="qrAvailable = false">
  85. <span v-else>二维码<br>待配置</span>
  86. </div>
  87. <div class="qr-text"><b>移动端APP下载</b>扫码下载,随时掌握管网动态</div>
  88. </div>
  89. </div>
  90. </section>
  91. </div>
  92. <footer>Copyright © 2026 沅陵县住房和城乡建设局 All Rights Reserved.</footer>
  93. </main>
  94. </template>
  95. <script setup>
  96. import Cookies from 'js-cookie'
  97. import { ElMessageBox } from 'element-plus'
  98. import { getCodeImg } from '@/api/login'
  99. import useUserStore from '@/store/modules/user'
  100. const userStore = useUserStore()
  101. const router = useRouter()
  102. const loginRef = ref()
  103. const loginForm = ref({
  104. username: import.meta.env.DEV ? 'admin' : '',
  105. password: import.meta.env.DEV ? 'Admin@123' : '',
  106. rememberMe: false,
  107. code: '',
  108. uuid: ''
  109. })
  110. const loginRules = {
  111. username: [{ required: true, trigger: 'blur', message: '请输入您的账号' }],
  112. password: [{ required: true, trigger: 'blur', message: '请输入您的密码' }],
  113. code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
  114. }
  115. const codeUrl = ref('')
  116. const loading = ref(false)
  117. const captchaEnabled = ref(true)
  118. const qrAvailable = ref(true)
  119. const mobileAppQrUrl = `${import.meta.env.BASE_URL}images/mobile-app-qr.png`
  120. async function handleLogin() {
  121. if (loading.value) return
  122. const valid = await loginRef.value.validate().catch(() => false)
  123. if (!valid) return
  124. loading.value = true
  125. if (loginForm.value.rememberMe) {
  126. Cookies.set('username', loginForm.value.username, { expires: 30 })
  127. Cookies.set('rememberMe', 'true', { expires: 30 })
  128. } else {
  129. Cookies.remove('username')
  130. Cookies.remove('rememberMe')
  131. }
  132. Cookies.remove('password')
  133. try {
  134. await userStore.login(loginForm.value)
  135. await router.push('/portal')
  136. } catch {
  137. loading.value = false
  138. loginForm.value.code = ''
  139. if (captchaEnabled.value) getCode()
  140. }
  141. }
  142. async function getCode() {
  143. try {
  144. const res = await getCodeImg()
  145. captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
  146. if (captchaEnabled.value) {
  147. codeUrl.value = `data:image/gif;base64,${res.img}`
  148. loginForm.value.uuid = res.uuid
  149. } else {
  150. loginForm.value.code = ''
  151. loginForm.value.uuid = ''
  152. }
  153. } catch {
  154. codeUrl.value = ''
  155. }
  156. }
  157. function restoreRememberedAccount() {
  158. if (!import.meta.env.DEV) loginForm.value.username = Cookies.get('username') || ''
  159. loginForm.value.rememberMe = Cookies.get('rememberMe') === 'true'
  160. Cookies.remove('password')
  161. }
  162. function showForgotPassword() {
  163. ElMessageBox.alert('请联系平台管理员', '忘记密码', { confirmButtonText: '我知道了' })
  164. }
  165. restoreRememberedAccount()
  166. getCode()
  167. </script>
  168. <style scoped lang="scss">
  169. .login-page {
  170. --blue: #2f7cf6; --text-one: #16233b; --text-two: #52627a; --text-three: #8a9ab2; --line: #e6ecf5;
  171. min-width: 100%; min-height: 100vh; position: relative; overflow: hidden; color: var(--text-one);
  172. background: linear-gradient(135deg, #dcebff 0%, #f2f7ff 45%, #e7f0ff 100%);
  173. font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", sans-serif;
  174. }
  175. .login-page::before { content: ''; position: fixed; inset: 0; background-image: linear-gradient(rgba(47, 124, 246, .06) 1px, transparent 1px), linear-gradient(90deg, rgba(47, 124, 246, .06) 1px, transparent 1px); background-size: 52px 52px; mask-image: radial-gradient(ellipse at 40% 50%, #000 0%, transparent 78%); }
  176. .glow { position: fixed; border-radius: 50%; filter: blur(10px); pointer-events: none; }
  177. .glow-one { width: 420px; height: 420px; right: -120px; top: -120px; background: radial-gradient(circle, rgba(64, 140, 255, .28), transparent 70%); }
  178. .glow-two { width: 360px; height: 360px; left: -100px; bottom: -120px; background: radial-gradient(circle, rgba(88, 170, 255, .22), transparent 70%); }
  179. .stage { position: relative; z-index: 1; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 32px 48px 56px; }
  180. .panel { width: 100%; max-width: 1180px; height: min(700px, calc(100vh - 110px)); min-height: 590px; display: flex; overflow: hidden; background: rgba(255, 255, 255, .55); border: 1px solid rgba(255, 255, 255, .9); border-radius: 20px; box-shadow: 0 30px 80px rgba(42, 92, 170, .18); backdrop-filter: blur(10px); }
  181. .brand-side { flex: 1.15; min-width: 0; padding: 44px 48px 36px; display: flex; flex-direction: column; background: radial-gradient(circle at 80% 20%, rgba(90, 160, 255, .18), transparent 55%), linear-gradient(160deg, rgba(255, 255, 255, .35), rgba(231, 240, 255, .25)); }
  182. .brand-top { display: flex; align-items: center; gap: 11px; }
  183. .logo { width: 38px; height: 38px; flex: 0 0 auto; border-radius: 9px; display: block; object-fit: contain; background: #fff; }
  184. .brand-name { font-size: 32px; font-weight: 800; line-height: 1.3; }
  185. .hero { margin-top: 72px; position: relative; z-index: 1; }
  186. .hero h1 { margin: 0; font-size: 17px; line-height: 1.6; font-weight: 700; }
  187. .hero h1 span { color: transparent; background: linear-gradient(90deg, #1e66e8, #16b4f2); background-clip: text; }
  188. .hero p { margin: 14px 0 0; font-size: 14px; color: var(--text-two); line-height: 1.9; }
  189. .tags { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 20px; }
  190. .tags span { padding: 5px 12px; border: 1px solid rgba(47, 124, 246, .18); border-radius: 20px; color: #2b6ad0; background: rgba(47, 124, 246, .1); font-size: 12px; }
  191. .illustration { flex: 1; min-height: 0; margin: 8px -10px 0; display: flex; align-items: center; justify-content: center; }
  192. .illustration svg { width: min(100%, 480px); height: auto; filter: drop-shadow(0 24px 30px rgba(40, 100, 200, .18)); }
  193. .form-side { width: 400px; flex: 0 0 400px; padding: 46px 44px 30px; display: flex; flex-direction: column; background: #fff; }
  194. .login-tabs { display: flex; margin-bottom: 30px; }
  195. .login-tabs span { position: relative; padding-bottom: 10px; color: var(--text-one); font-size: 15px; font-weight: 700; }
  196. .login-tabs span::after { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 3px; border-radius: 2px; background: linear-gradient(90deg, #2f7cf6, #16b4f2); }
  197. .login-form { width: 100%; }
  198. .field { margin-bottom: 17px; }
  199. .field label { display: block; width: 100%; margin-bottom: 8px; color: var(--text-two); font-size: 13px; line-height: 18px; }
  200. .input-box { width: 100%; height: 44px; padding: 0 13px; display: flex; align-items: center; gap: 10px; background: #f8fafd; border: 1px solid var(--line); border-radius: 10px; transition: all .2s; }
  201. .input-box:focus-within { background: #fff; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(47, 124, 246, .1); }
  202. .input-box > svg { width: 17px; height: 17px; flex: 0 0 auto; fill: #a8b6cb; }
  203. .input-box input { min-width: 0; flex: 1; border: 0; outline: 0; color: var(--text-one); background: transparent; font: inherit; font-size: 14px; }
  204. .input-box input::placeholder { color: #b6c2d4; }
  205. .code-box { padding-right: 0; overflow: hidden; gap: 8px; }
  206. .captcha { align-self: stretch; width: 118px; flex: 0 0 118px; padding: 0; overflow: hidden; cursor: pointer; border: 0; border-left: 1px solid var(--line); background: linear-gradient(135deg, #eef4ff, #dce8fb); }
  207. .captcha img { width: 100%; height: 100%; display: block; object-fit: fill; }
  208. .form-row { margin: 2px 0 22px; display: flex; align-items: center; justify-content: space-between; font-size: 12px; }
  209. .remember { display: flex; align-items: center; gap: 6px; color: var(--text-three); cursor: pointer; }
  210. .remember input { accent-color: var(--blue); }
  211. .forgot { padding: 0; border: 0; color: var(--blue); background: none; cursor: pointer; font-size: 12px; }
  212. .login-button { width: 100%; height: 46px; border: 0; border-radius: 10px; color: #fff; background: linear-gradient(90deg, #2f7cf6, #1f9bf0); box-shadow: 0 12px 24px rgba(47, 124, 246, .32); cursor: pointer; font-size: 15px; font-weight: 700; letter-spacing: 6px; transition: transform .15s, box-shadow .15s; }
  213. .login-button:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 16px 30px rgba(47, 124, 246, .4); }
  214. .login-button:disabled { cursor: wait; opacity: .72; }
  215. .mini-qr { margin-top: 44px; padding-top: 16px; display: flex; align-items: center; justify-content: center; gap: 12px; border-top: 1px dashed var(--line); }
  216. .qr-box { width: 96px; height: 96px; flex: 0 0 96px; display: flex; align-items: center; justify-content: center; overflow: hidden; border: 1px solid var(--line); border-radius: 10px; color: var(--text-three); background: #f8fafd; font-size: 12px; line-height: 1.6; text-align: center; }
  217. .qr-box img { width: 100%; height: 100%; display: block; object-fit: contain; }
  218. .qr-text { color: var(--text-two); font-size: 12px; line-height: 1.6; }
  219. .qr-text b { display: block; margin-bottom: 2px; color: var(--text-one); }
  220. footer { position: fixed; right: 0; bottom: 14px; left: 0; z-index: 2; color: #8aa0bd; font-size: 12px; text-align: center; }
  221. :deep(.el-form-item__content) { display: block; line-height: normal; }
  222. :deep(.el-form-item__error) { position: absolute; top: calc(100% + 2px); padding: 0; }
  223. @media (max-width: 980px) {
  224. .stage { padding: 24px; } .brand-side { padding: 36px 32px; } .brand-name { font-size: 24px; } .form-side { width: 380px; flex-basis: 380px; padding-right: 36px; padding-left: 36px; }
  225. }
  226. @media (max-width: 760px) {
  227. .login-page { overflow: auto; } .stage { min-height: 100vh; padding: 20px 16px 54px; } .panel { height: auto; min-height: 0; max-width: 420px; } .brand-side { display: none; } .form-side { width: 100%; flex-basis: 100%; padding: 34px 28px 28px; } footer { position: absolute; padding: 0 16px; }
  228. }
  229. </style>