| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- <template>
- <main class="login-page">
- <div class="glow glow-one"></div>
- <div class="glow glow-two"></div>
- <div class="stage">
- <section class="panel" aria-label="登录">
- <div class="brand-side">
- <div class="brand-top">
- <img class="logo" src="/logo.png" alt="平台 Logo">
- <div class="brand-name">沅陵县城市地下管网管控平台</div>
- </div>
- <div class="hero">
- <h1>汇聚地下管网运行态势<br><span>统览城市生命线安全</span></h1>
- <p>供水 · 排水 · 燃气 · 窨井盖全域感知,智能预警联动处置,<br>协同应急指挥调度,守护城市运行安全。</p>
- <div class="tags"><span>实时监测</span><span>智能预警</span><span>联动处置</span><span>应急指挥</span></div>
- </div>
- <div class="illustration" aria-hidden="true">
- <svg viewBox="0 0 520 360" xmlns="http://www.w3.org/2000/svg">
- <defs>
- <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>
- <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>
- <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>
- <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>
- <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>
- </defs>
- <ellipse cx="260" cy="300" rx="210" ry="46" fill="#b9d4ff" opacity=".55" />
- <ellipse cx="260" cy="288" rx="210" ry="46" fill="url(#login-p3)" />
- <ellipse cx="260" cy="286" rx="168" ry="35" fill="none" stroke="#bcd2f5" stroke-width="2" />
- <polygon points="150,250 260,300 370,250 260,205" fill="url(#login-p2)" opacity=".9" />
- <polygon points="150,250 260,300 260,322 150,272" fill="#3d82e6" />
- <polygon points="370,250 260,300 260,322 370,272" fill="#2b6bd0" />
- <polygon points="185,232 260,266 335,232 260,200" fill="#e8f2ff" />
- <g>
- <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)" />
- <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" />
- </g>
- <g>
- <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)" />
- <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" />
- </g>
- <g>
- <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" />
- <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" />
- </g>
- <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>
- <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" />
- <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" />
- </svg>
- </div>
- </div>
- <div class="form-side">
- <div class="login-tabs"><span>账号登录</span></div>
- <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form" @submit.prevent="handleLogin">
- <el-form-item prop="username" class="field">
- <label for="login-username">账号</label>
- <div class="input-box">
- <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>
- <input id="login-username" v-model.trim="loginForm.username" type="text" autocomplete="username" placeholder="请输入用户名/手机号">
- </div>
- </el-form-item>
- <el-form-item prop="password" class="field">
- <label for="login-password">密码</label>
- <div class="input-box">
- <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>
- <input id="login-password" v-model="loginForm.password" type="password" autocomplete="current-password" placeholder="请输入密码" @keyup.enter="handleLogin">
- </div>
- </el-form-item>
- <el-form-item v-if="captchaEnabled" prop="code" class="field">
- <label for="login-code">验证码</label>
- <div class="input-box code-box">
- <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>
- <input id="login-code" v-model.trim="loginForm.code" type="text" autocomplete="off" placeholder="请输入计算结果" maxlength="6" @keyup.enter="handleLogin">
- <button type="button" class="captcha" title="点击刷新验证码" @click="getCode"><img :src="codeUrl" alt="验证码"></button>
- </div>
- </el-form-item>
- <div class="form-row">
- <label class="remember"><input v-model="loginForm.rememberMe" type="checkbox">记住账号</label>
- <button type="button" class="forgot" @click="showForgotPassword">忘记密码?</button>
- </div>
- <button type="submit" class="login-button" :disabled="loading">{{ loading ? '登 录 中...' : '立 即 登 录' }}</button>
- </el-form>
- <div class="mini-qr">
- <div class="qr-box">
- <img v-if="qrAvailable" :src="mobileAppQrUrl" alt="移动端 APP 下载二维码" @error="qrAvailable = false">
- <span v-else>二维码<br>待配置</span>
- </div>
- <div class="qr-text"><b>移动端APP下载</b>扫码下载,随时掌握管网动态</div>
- </div>
- </div>
- </section>
- </div>
- <footer>Copyright © 2026 沅陵县住房和城乡建设局 All Rights Reserved.</footer>
- </main>
- </template>
- <script setup>
- import Cookies from 'js-cookie'
- import { ElMessageBox } from 'element-plus'
- import { getCodeImg } from '@/api/login'
- import useUserStore from '@/store/modules/user'
- const userStore = useUserStore()
- const router = useRouter()
- const loginRef = ref()
- const loginForm = ref({
- username: import.meta.env.DEV ? 'admin' : '',
- password: import.meta.env.DEV ? 'Admin@123' : '',
- rememberMe: false,
- code: '',
- uuid: ''
- })
- const loginRules = {
- username: [{ required: true, trigger: 'blur', message: '请输入您的账号' }],
- password: [{ required: true, trigger: 'blur', message: '请输入您的密码' }],
- code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
- }
- const codeUrl = ref('')
- const loading = ref(false)
- const captchaEnabled = ref(true)
- const qrAvailable = ref(true)
- const mobileAppQrUrl = `${import.meta.env.BASE_URL}images/mobile-app-qr.png`
- async function handleLogin() {
- if (loading.value) return
- const valid = await loginRef.value.validate().catch(() => false)
- if (!valid) return
- loading.value = true
- if (loginForm.value.rememberMe) {
- Cookies.set('username', loginForm.value.username, { expires: 30 })
- Cookies.set('rememberMe', 'true', { expires: 30 })
- } else {
- Cookies.remove('username')
- Cookies.remove('rememberMe')
- }
- Cookies.remove('password')
- try {
- await userStore.login(loginForm.value)
- await router.push('/portal')
- } catch {
- loading.value = false
- loginForm.value.code = ''
- if (captchaEnabled.value) getCode()
- }
- }
- async function getCode() {
- try {
- const res = await getCodeImg()
- captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
- if (captchaEnabled.value) {
- codeUrl.value = `data:image/gif;base64,${res.img}`
- loginForm.value.uuid = res.uuid
- } else {
- loginForm.value.code = ''
- loginForm.value.uuid = ''
- }
- } catch {
- codeUrl.value = ''
- }
- }
- function restoreRememberedAccount() {
- if (!import.meta.env.DEV) loginForm.value.username = Cookies.get('username') || ''
- loginForm.value.rememberMe = Cookies.get('rememberMe') === 'true'
- Cookies.remove('password')
- }
- function showForgotPassword() {
- ElMessageBox.alert('请联系平台管理员', '忘记密码', { confirmButtonText: '我知道了' })
- }
- restoreRememberedAccount()
- getCode()
- </script>
- <style scoped lang="scss">
- .login-page {
- --blue: #2f7cf6; --text-one: #16233b; --text-two: #52627a; --text-three: #8a9ab2; --line: #e6ecf5;
- min-width: 100%; min-height: 100vh; position: relative; overflow: hidden; color: var(--text-one);
- background: linear-gradient(135deg, #dcebff 0%, #f2f7ff 45%, #e7f0ff 100%);
- font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", sans-serif;
- }
- .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%); }
- .glow { position: fixed; border-radius: 50%; filter: blur(10px); pointer-events: none; }
- .glow-one { width: 420px; height: 420px; right: -120px; top: -120px; background: radial-gradient(circle, rgba(64, 140, 255, .28), transparent 70%); }
- .glow-two { width: 360px; height: 360px; left: -100px; bottom: -120px; background: radial-gradient(circle, rgba(88, 170, 255, .22), transparent 70%); }
- .stage { position: relative; z-index: 1; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 32px 48px 56px; }
- .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); }
- .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)); }
- .brand-top { display: flex; align-items: center; gap: 11px; }
- .logo { width: 38px; height: 38px; flex: 0 0 auto; border-radius: 9px; display: block; object-fit: contain; background: #fff; }
- .brand-name { font-size: 32px; font-weight: 800; line-height: 1.3; }
- .hero { margin-top: 72px; position: relative; z-index: 1; }
- .hero h1 { margin: 0; font-size: 17px; line-height: 1.6; font-weight: 700; }
- .hero h1 span { color: transparent; background: linear-gradient(90deg, #1e66e8, #16b4f2); background-clip: text; }
- .hero p { margin: 14px 0 0; font-size: 14px; color: var(--text-two); line-height: 1.9; }
- .tags { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 20px; }
- .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; }
- .illustration { flex: 1; min-height: 0; margin: 8px -10px 0; display: flex; align-items: center; justify-content: center; }
- .illustration svg { width: min(100%, 480px); height: auto; filter: drop-shadow(0 24px 30px rgba(40, 100, 200, .18)); }
- .form-side { width: 400px; flex: 0 0 400px; padding: 46px 44px 30px; display: flex; flex-direction: column; background: #fff; }
- .login-tabs { display: flex; margin-bottom: 30px; }
- .login-tabs span { position: relative; padding-bottom: 10px; color: var(--text-one); font-size: 15px; font-weight: 700; }
- .login-tabs span::after { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 3px; border-radius: 2px; background: linear-gradient(90deg, #2f7cf6, #16b4f2); }
- .login-form { width: 100%; }
- .field { margin-bottom: 17px; }
- .field label { display: block; width: 100%; margin-bottom: 8px; color: var(--text-two); font-size: 13px; line-height: 18px; }
- .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; }
- .input-box:focus-within { background: #fff; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(47, 124, 246, .1); }
- .input-box > svg { width: 17px; height: 17px; flex: 0 0 auto; fill: #a8b6cb; }
- .input-box input { min-width: 0; flex: 1; border: 0; outline: 0; color: var(--text-one); background: transparent; font: inherit; font-size: 14px; }
- .input-box input::placeholder { color: #b6c2d4; }
- .code-box { padding-right: 0; overflow: hidden; gap: 8px; }
- .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); }
- .captcha img { width: 100%; height: 100%; display: block; object-fit: fill; }
- .form-row { margin: 2px 0 22px; display: flex; align-items: center; justify-content: space-between; font-size: 12px; }
- .remember { display: flex; align-items: center; gap: 6px; color: var(--text-three); cursor: pointer; }
- .remember input { accent-color: var(--blue); }
- .forgot { padding: 0; border: 0; color: var(--blue); background: none; cursor: pointer; font-size: 12px; }
- .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; }
- .login-button:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 16px 30px rgba(47, 124, 246, .4); }
- .login-button:disabled { cursor: wait; opacity: .72; }
- .mini-qr { margin-top: 44px; padding-top: 16px; display: flex; align-items: center; justify-content: center; gap: 12px; border-top: 1px dashed var(--line); }
- .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; }
- .qr-box img { width: 100%; height: 100%; display: block; object-fit: contain; }
- .qr-text { color: var(--text-two); font-size: 12px; line-height: 1.6; }
- .qr-text b { display: block; margin-bottom: 2px; color: var(--text-one); }
- footer { position: fixed; right: 0; bottom: 14px; left: 0; z-index: 2; color: #8aa0bd; font-size: 12px; text-align: center; }
- :deep(.el-form-item__content) { display: block; line-height: normal; }
- :deep(.el-form-item__error) { position: absolute; top: calc(100% + 2px); padding: 0; }
- @media (max-width: 980px) {
- .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; }
- }
- @media (max-width: 760px) {
- .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; }
- }
- </style>
|