|
|
@@ -2,7 +2,8 @@
|
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
|
import { ref } from 'vue'
|
|
|
import { getCaptcha, loginAccount } from '@/services/auth'
|
|
|
-import { setToken } from '@/utils/auth'
|
|
|
+import { getToken, setToken } from '@/utils/auth'
|
|
|
+import { useUserStore } from '@/stores/user'
|
|
|
|
|
|
const username = ref('')
|
|
|
const password = ref('')
|
|
|
@@ -14,6 +15,12 @@ const captchaUrl = ref('')
|
|
|
const captchaUuid = ref('')
|
|
|
const captchaLoading = ref(false)
|
|
|
const loggingIn = ref(false)
|
|
|
+const checkingSession = ref(false)
|
|
|
+const userStore = useUserStore()
|
|
|
+
|
|
|
+// 密码显隐图标(与参考设计一致)
|
|
|
+const eyeOffIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12%207c2.76%200%205%202.24%205%205%200%20.65-.13%201.26-.36%201.83l2.92%202.92c1.51-1.26%202.7-2.89%203.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4%200-2.74.25-3.98.7l2.16%202.16C10.74%207.13%2011.35%207%2012%207zM2%204.27l2.28%202.46.46.46C3.08%208.3%201.78%2010.02%201%2012c1.73%204.39%206%207.5%2011%207.5%201.55%200%203.03-.3%205.03-.83l1.82%201.82%201.27-1.27L3.27%203%202%204.27zM7.53%209.8l1.55%201.55c-.05.21-.08.43-.08.65%200%201.66%201.34%203%203%203%20.22%200%20.27-.03.65-.08l1.55%201.55c-.67.33-1.41.53-2.33.53-2.76%200-5-2.24-5-5%200-.69.2-1.53.53-2.2zm4.31-.78l3.15%203.15.02-.18c0-1.66-1.34-3-3-3l-.17.03z' fill='%2394a3b8'/%3E%3C/svg%3E"
|
|
|
+const eyeOnIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12%204.5C7%204.5%202.73%207.61%201%2012c1.73%204.39%206%207.5%2011%207.5s9.27-3.11%2011-7.5c-1.73-4.39-6-7.5-11-7.5zM12%2017c-2.76%200-5-2.24-5-5s2.24-5%205-5%205%202.24%205%205-2.24%205-5%205zm0-8c-1.66%200-3%201.34-3%203s1.34%203%203%203%203-1.34%203-3-1.34-3-3-3z' fill='%23256399'/%3E%3C/svg%3E"
|
|
|
|
|
|
async function loadCaptcha() {
|
|
|
if (captchaLoading.value) return
|
|
|
@@ -65,7 +72,26 @@ async function login() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-onLoad(() => {
|
|
|
+function showForgot() {
|
|
|
+ uni.showToast({ title: '请联系系统管理员重置密码', icon: 'none' })
|
|
|
+}
|
|
|
+
|
|
|
+onLoad(async () => {
|
|
|
+ // H5 的根路径默认落到 pages/login/index(它是 pages.json 的首个页面)。
|
|
|
+ // 如果本地仍有有效令牌,先重新获取用户信息确认会话未失效,再进入应用首页,
|
|
|
+ // 避免刷新 http://localhost:5174/ 后再次显示登录页。
|
|
|
+ if (getToken()) {
|
|
|
+ checkingSession.value = true
|
|
|
+ try {
|
|
|
+ await userStore.fetchProfile()
|
|
|
+ uni.switchTab({ url: '/pages/message/index' })
|
|
|
+ return
|
|
|
+ } catch {
|
|
|
+ // 令牌过期时 requestApi 会清理会话并保持在登录页,继续正常登录流程。
|
|
|
+ } finally {
|
|
|
+ checkingSession.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
const rememberedUsername = uni.getStorageSync('remembered-username')
|
|
|
if (rememberedUsername) {
|
|
|
username.value = rememberedUsername
|
|
|
@@ -76,63 +102,155 @@ onLoad(() => {
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
+ <view v-if="checkingSession" class="session-check">正在进入消息主页...</view>
|
|
|
<view class="login-page">
|
|
|
- <view class="network-lines"><view v-for="i in 8" :key="i" class="line" :style="{ top: `${i * 11}%`, transform: `rotate(${i % 2 ? -8 : 9}deg)` }"></view></view>
|
|
|
- <view class="login-wrap">
|
|
|
- <view class="brand">
|
|
|
- <view class="brand-icon"><view class="pipe-mark">⌁</view></view>
|
|
|
- <view class="brand-title">沅陵太常片区城市地下管网</view>
|
|
|
- <view class="brand-title">管控平台</view>
|
|
|
+ <view class="banner">
|
|
|
+ <view class="banner-deco">YL</view>
|
|
|
+ <view class="banner-title">您好,欢迎使用<text class="banner-sub">沅陵县城市地下管网管控平台</text></view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="card">
|
|
|
+ <view class="field">
|
|
|
+ <text class="field-label">账户</text>
|
|
|
+ <view class="ipt-wrap">
|
|
|
+ <input
|
|
|
+ class="ipt"
|
|
|
+ v-model="username"
|
|
|
+ type="text"
|
|
|
+ autocomplete="username"
|
|
|
+ placeholder="请输入用户名/手机号"
|
|
|
+ placeholder-style="color:#b6c2d4"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- <view class="login-card">
|
|
|
- <view class="welcome">欢迎登录 <text>管网管控平台</text></view>
|
|
|
- <view class="input-row"><view class="icon user-icon" aria-hidden="true"></view><input v-model="username" placeholder="请输入账号" /></view>
|
|
|
- <view class="input-row"><view class="icon lock-icon" aria-hidden="true"></view><input v-model="password" placeholder="请输入密码" :password="!showPassword" /><text class="eye" @tap="showPassword = !showPassword">{{ showPassword ? '◉' : '◌' }}</text></view>
|
|
|
- <view v-if="captchaEnabled" class="captcha-row">
|
|
|
- <view class="input-row"><view class="icon verify-icon" aria-hidden="true"></view><input v-model="captcha" maxlength="4" placeholder="验证码" /></view>
|
|
|
+
|
|
|
+ <view class="field">
|
|
|
+ <text class="field-label">密码</text>
|
|
|
+ <view class="ipt-wrap">
|
|
|
+ <input
|
|
|
+ class="ipt"
|
|
|
+ v-model="password"
|
|
|
+ :password="!showPassword"
|
|
|
+ autocomplete="current-password"
|
|
|
+ placeholder="请输入密码"
|
|
|
+ placeholder-style="color:#b6c2d4"
|
|
|
+ />
|
|
|
+ <view class="eye-btn" @tap="showPassword = !showPassword" aria-label="显示/隐藏密码">
|
|
|
+ <image class="eye-img" :src="showPassword ? eyeOnIcon : eyeOffIcon" mode="aspectFit" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="captchaEnabled" class="field">
|
|
|
+ <text class="field-label">验证码</text>
|
|
|
+ <view class="ipt-wrap code-wrap">
|
|
|
+ <input
|
|
|
+ class="ipt"
|
|
|
+ v-model="captcha"
|
|
|
+ type="text"
|
|
|
+ autocomplete="off"
|
|
|
+ maxlength="4"
|
|
|
+ placeholder="请输入验证码"
|
|
|
+ placeholder-style="color:#b6c2d4"
|
|
|
+ />
|
|
|
<view class="captcha" @tap="loadCaptcha">
|
|
|
<image v-if="captchaUrl" class="captcha-image" :src="captchaUrl" mode="scaleToFill" />
|
|
|
- <text v-else>{{ captchaLoading ? '加载中...' : '点击刷新' }}</text>
|
|
|
+ <text v-else class="captcha-tip">{{ captchaLoading ? '加载中...' : '点击刷新' }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
- <view class="remember" @tap="remember = !remember"><view class="check" :class="{ checked: remember }">{{ remember ? '✓' : '' }}</view><text>记住账号</text></view>
|
|
|
- <button class="login-btn" :class="{ disabled: loggingIn }" :loading="loggingIn" :disabled="loggingIn" @tap="login">{{ loggingIn ? '登录中...' : '登 录' }}</button>
|
|
|
</view>
|
|
|
- <text class="copyright">城市地下管网安全运行 · 智慧守护</text>
|
|
|
+
|
|
|
+ <view class="row">
|
|
|
+ <view class="chk" @tap="remember = !remember">
|
|
|
+ <view class="chk-box" :class="{ checked: remember }">{{ remember ? '✓' : '' }}</view>
|
|
|
+ <text class="chk-text">记住密码</text>
|
|
|
+ </view>
|
|
|
+ <text class="link" @tap="showForgot">忘记密码?</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <button class="btn btn-primary" :loading="loggingIn" :disabled="loggingIn" @tap="login">
|
|
|
+ {{ loggingIn ? '登录中...' : '登录' }}
|
|
|
+ </button>
|
|
|
</view>
|
|
|
+
|
|
|
+ <view class="footer">汇聚地下管网运行态势 · 统览城市生命线安全</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
-.login-page, .login-wrap, .login-card, .input-row, .captcha-row { box-sizing: border-box; }
|
|
|
-.login-page { position: relative; width: 100%; min-height: 100vh; overflow: hidden; color: #fff; background: linear-gradient(160deg, #16324f 0%, #1f4e79 48%, #2b6ca3 100%); }
|
|
|
-.network-lines { position: absolute; inset: 0; opacity: .12; }
|
|
|
-.line { position: absolute; left: -20%; width: 140%; height: 1rpx; background: repeating-linear-gradient(90deg, #fff 0, #fff 35rpx, transparent 35rpx, transparent 62rpx); }
|
|
|
-.login-wrap { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; width: 100%; min-height: 100vh; padding: calc(120rpx + env(safe-area-inset-top)) 55rpx 40rpx; }
|
|
|
-.brand { width: 100%; text-align: center; text-shadow: 0 4rpx 16rpx rgba(10,30,50,.45); }
|
|
|
-.brand-icon { display: flex; align-items: center; justify-content: center; width: 128rpx; height: 128rpx; margin: 0 auto 28rpx; border-radius: 34rpx; background: rgba(255,255,255,.96); box-shadow: 0 16rpx 40rpx rgba(8,26,44,.35); }
|
|
|
-.pipe-mark { color: #2563eb; font-size: 82rpx; font-weight: 600; transform: rotate(-18deg); }
|
|
|
-.brand-title { font-size: 38rpx; font-weight: 600; letter-spacing: 2rpx; line-height: 1.55; }
|
|
|
-.login-card { width: 100%; max-width: 640rpx; margin-top: 70rpx; padding: 50rpx 40rpx 56rpx; border-radius: 28rpx; background: #fff; box-shadow: 0 28rpx 70rpx rgba(6,22,38,.4); }
|
|
|
-.welcome { margin-bottom: 40rpx; color: #26374e; font-size: 31rpx; font-weight: 600; text-align: center; }
|
|
|
-.welcome text { color: #2563eb; }
|
|
|
-.input-row { display: flex; flex: 1; align-items: center; height: 94rpx; padding: 0 24rpx; margin-bottom: 24rpx; border: 1rpx solid #dfe7ef; border-radius: 18rpx; background: #f8fafc; }
|
|
|
-.icon { position: relative; display: flex; flex: 0 0 54rpx; align-items: center; justify-content: center; width: 54rpx; height: 54rpx; margin-right: 17rpx; border: 1rpx solid #c8dced; border-radius: 16rpx; background: linear-gradient(145deg, #edf6ff, #dcecff); color: #164c7f; }
|
|
|
-.user-icon::before { content: ''; position: absolute; top: 11rpx; width: 15rpx; height: 15rpx; border: 3rpx solid #164c7f; border-radius: 50%; }
|
|
|
-.user-icon::after { content: ''; position: absolute; bottom: 10rpx; width: 29rpx; height: 15rpx; border: 3rpx solid #164c7f; border-bottom: 0; border-radius: 18rpx 18rpx 0 0; }
|
|
|
-.lock-icon::before { content: ''; position: absolute; bottom: 11rpx; width: 26rpx; height: 21rpx; border: 3rpx solid #164c7f; border-radius: 5rpx; background: rgba(255,255,255,.3); }
|
|
|
-.lock-icon::after { content: ''; position: absolute; top: 12rpx; width: 17rpx; height: 18rpx; border: 3rpx solid #164c7f; border-bottom: 0; border-radius: 12rpx 12rpx 0 0; }
|
|
|
-.verify-icon::before { content: ''; position: absolute; width: 27rpx; height: 27rpx; border: 3rpx solid #164c7f; border-radius: 50%; }
|
|
|
-.verify-icon::after { content: ''; position: absolute; width: 13rpx; height: 7rpx; border-bottom: 3rpx solid #164c7f; border-left: 3rpx solid #164c7f; transform: translate(1rpx, -2rpx) rotate(-45deg); }
|
|
|
-input { flex: 1; color: #293a50; font-size: 28rpx; }
|
|
|
-.eye { padding: 18rpx 0 18rpx 20rpx; color: #8596a9; }
|
|
|
-.captcha-row { display: grid; grid-template-columns: 1fr 190rpx; gap: 18rpx; }
|
|
|
-.captcha { display: flex; align-items: center; justify-content: center; height: 94rpx; color: #1d4f88; border-radius: 16rpx; background: repeating-linear-gradient(15deg,#e9f3ff,#e9f3ff 7rpx,#d7e9fb 8rpx,#d7e9fb 10rpx); font-size: 31rpx; font-weight: 700; letter-spacing: 6rpx; font-style: italic; }
|
|
|
-.captcha-image { width: 100%; height: 100%; border-radius: 16rpx; }
|
|
|
-.remember { display: flex; align-items: center; width: max-content; margin: 2rpx 0 35rpx; color: #697b90; font-size: 25rpx; }
|
|
|
-.check { display: flex; align-items: center; justify-content: center; width: 31rpx; height: 31rpx; margin-right: 12rpx; border: 1rpx solid #b8c3d0; border-radius: 6rpx; }
|
|
|
-.check.checked { color: #fff; border-color: #2563eb; background: #2563eb; }
|
|
|
-.login-btn { height: 92rpx; color: #fff; border-radius: 16rpx; background: linear-gradient(135deg,#2479ec,#1858cc); box-shadow: 0 12rpx 26rpx rgba(37,99,235,.3); font-size: 31rpx; line-height: 92rpx; letter-spacing: 12rpx; }
|
|
|
-.login-btn.disabled { opacity: .7; }
|
|
|
-.copyright { margin-top: auto; padding-top: 40rpx; color: rgba(255,255,255,.68); font-size: 23rpx; }
|
|
|
+.login-page {
|
|
|
+ position: relative; width: 100%; min-height: 100vh; min-height: 100dvh; overflow-x: hidden;
|
|
|
+ color: #16233b; background: #f5f7fb;
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", sans-serif;
|
|
|
+}
|
|
|
+.session-check {
|
|
|
+ position: fixed; inset: 0; z-index: 20; display: flex; align-items: center; justify-content: center;
|
|
|
+ color: #52647a; background: #f5f7fb; font-size: 30rpx;
|
|
|
+}
|
|
|
+.banner {
|
|
|
+ position: relative; overflow: hidden;
|
|
|
+ background: linear-gradient(180deg, #1d4f80 0%, #256399 55%, #2a73a8 100%);
|
|
|
+ padding: calc(140rpx + env(safe-area-inset-top, 0px)) 64rpx 240rpx;
|
|
|
+}
|
|
|
+.banner::before {
|
|
|
+ content: ''; position: absolute; right: -120rpx; top: 30%;
|
|
|
+ width: 440rpx; height: 440rpx; border-radius: 50%;
|
|
|
+ background: radial-gradient(circle, rgba(255, 255, 255, .18), transparent 70%);
|
|
|
+ filter: blur(12rpx);
|
|
|
+}
|
|
|
+.banner-deco {
|
|
|
+ position: absolute; right: 56rpx; top: 50%; transform: translateY(-50%);
|
|
|
+ font-size: 192rpx; font-weight: 900; color: #fff; opacity: .18;
|
|
|
+ letter-spacing: -12rpx; line-height: 1; z-index: 0; user-select: none;
|
|
|
+}
|
|
|
+.banner-title {
|
|
|
+ position: relative; z-index: 1;
|
|
|
+ font-size: 52rpx; font-weight: 700; color: #fff; line-height: 1.5; letter-spacing: 2rpx;
|
|
|
+}
|
|
|
+.banner-sub {
|
|
|
+ display: block; font-size: 36rpx; font-weight: 500; margin-top: 12rpx;
|
|
|
+ letter-spacing: 2rpx; opacity: .92;
|
|
|
+}
|
|
|
+.card {
|
|
|
+ position: relative; z-index: 1;
|
|
|
+ margin: -180rpx 44rpx 48rpx;
|
|
|
+ background: #fff; border-radius: 40rpx;
|
|
|
+ box-shadow: 0 32rpx 72rpx rgba(20, 40, 80, .10);
|
|
|
+ padding: 60rpx 48rpx 68rpx;
|
|
|
+}
|
|
|
+.field { margin-bottom: 36rpx; }
|
|
|
+.field-label { display: block; font-size: 26rpx; font-weight: 600; color: #16233b; margin-bottom: 20rpx; }
|
|
|
+.ipt-wrap { position: relative; z-index: 2; display: flex; align-items: center; border-bottom: 1rpx solid #e6ecf2; transition: border-color .2s; }
|
|
|
+.ipt-wrap:focus-within { border-color: #256399; }
|
|
|
+.ipt { flex: 1; width: 100%; min-height: 72rpx; border: none; outline: none; background: transparent; padding: 20rpx 0; font-size: 30rpx; color: #16233b; }
|
|
|
+.code-wrap { display: flex; align-items: center; gap: 16rpx; }
|
|
|
+.eye-btn { position: absolute; right: 0; top: 50%; transform: translateY(-50%); padding: 12rpx 8rpx; }
|
|
|
+.eye-img { width: 40rpx; height: 40rpx; display: block; }
|
|
|
+.captcha {
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ height: 64rpx; padding: 0; border-radius: 10rpx; overflow: hidden;
|
|
|
+}
|
|
|
+.captcha-image { width: 160rpx; height: 64rpx; border-radius: 10rpx; }
|
|
|
+.captcha-tip { font-size: 24rpx; font-weight: 400; color: #94a3b8; }
|
|
|
+.row { display: flex; align-items: center; justify-content: space-between; margin: 12rpx 0 44rpx; font-size: 24rpx; color: #94a3b8; }
|
|
|
+.chk { display: flex; align-items: center; gap: 12rpx; }
|
|
|
+.chk-box {
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ width: 28rpx; height: 28rpx; border: 1rpx solid #b8c3d0; border-radius: 6rpx;
|
|
|
+ color: #fff; font-size: 22rpx;
|
|
|
+}
|
|
|
+.chk-box.checked { border-color: #256399; background: #256399; }
|
|
|
+.link { color: #94a3b8; }
|
|
|
+.link:active { color: #256399; }
|
|
|
+.btn { width: 100%; height: 96rpx; border: none; border-radius: 48rpx; font-size: 32rpx; font-weight: 600; }
|
|
|
+.btn-primary { background: linear-gradient(90deg, #1d4f80, #256399); color: #fff; box-shadow: 0 20rpx 44rpx rgba(37, 99, 153, .32); }
|
|
|
+.btn-primary:active { opacity: .92; }
|
|
|
+.footer {
|
|
|
+ text-align: center; padding: 60rpx 48rpx;
|
|
|
+ font-size: 28rpx; line-height: 1.9; color: #1d4f80; font-weight: 600; letter-spacing: 2rpx;
|
|
|
+}
|
|
|
+.footer::before {
|
|
|
+ content: ''; display: block; width: 72rpx; height: 4rpx; margin: 0 auto 28rpx; border-radius: 2rpx;
|
|
|
+ background: linear-gradient(90deg, #1d4f80, #256399);
|
|
|
+}
|
|
|
</style>
|