You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
197 lines
11 KiB
197 lines
11 KiB
<template>
|
|
<section class="login-page">
|
|
<div class="login-visual">
|
|
<div class="login-visual-map"></div>
|
|
<div class="login-visual-shade"></div>
|
|
<div class="login-brand">
|
|
<div class="brand-mark"><span></span><span></span><span></span></div>
|
|
<div><strong>嘉谷</strong><small>低空智控平台</small></div>
|
|
</div>
|
|
<div class="login-visual-copy">
|
|
<span>LOW-ALTITUDE INTELLIGENCE</span>
|
|
<h1>让每一次飞行<br />都可控、可追溯</h1>
|
|
<p>统一管理机巢、无人机、任务与视频资产</p>
|
|
</div>
|
|
<div class="login-visual-footer"><span>嘉谷低空智控平台</span><span>© 2026 嘉谷科技</span></div>
|
|
</div>
|
|
|
|
<div class="login-form-side">
|
|
<div class="login-form-wrap">
|
|
<div class="login-panel" :class="{ active: mode === 'login' }">
|
|
<div class="login-method-tabs">
|
|
<button :class="{ active: method === 'password' }" type="button" @click="method = 'password'">账号密码登录</button>
|
|
<button :class="{ active: method === 'sms' }" type="button" @click="method = 'sms'">手机号登录</button>
|
|
</div>
|
|
<form novalidate @submit.prevent="submitLogin">
|
|
<div class="login-method-fields password-fields" v-show="method === 'password'">
|
|
<label><span>账号</span><div class="login-input"><svg><use href="#i-user"/></svg><input v-model="account" type="text" placeholder="请输入手机号或企业邮箱" autocomplete="username" /></div></label>
|
|
<label><span>密码</span><div class="login-input"><svg><use href="#i-lock"/></svg><input v-model="password" :type="pwdVisible ? 'text' : 'password'" placeholder="请输入登录密码" autocomplete="current-password" /><button type="button" @click="pwdVisible = !pwdVisible" :title="pwdVisible ? '隐藏密码' : '显示密码'"><svg><use href="#i-eye"/></svg></button></div></label>
|
|
</div>
|
|
<div class="login-method-fields sms-fields" v-show="method === 'sms'">
|
|
<label><span>手机号</span><div class="login-input"><svg><use href="#i-phone"/></svg><input v-model="smsPhone" type="tel" placeholder="请输入绑定手机号" /></div></label>
|
|
<label><span>短信验证码</span><div class="login-input"><svg><use href="#i-message"/></svg><input v-model="smsCode" inputmode="numeric" maxlength="6" placeholder="请输入短信验证码" /><button class="send-code-button" type="button" :disabled="loginCodeTimer > 0" @click="sendLoginCode">{{ loginCodeTimer > 0 ? loginCodeTimer + 's 后重试' : '获取验证码' }}</button></div></label>
|
|
</div>
|
|
<label><span>图形验证码</span><div class="login-input captcha-input"><svg><use href="#i-shield"/></svg><input v-model="captcha" maxlength="4" placeholder="请输入验证码" /><button class="captcha-image" type="button" title="刷新验证码" @click="refreshCaptcha">{{ captchaValue }}</button></div></label>
|
|
<div class="login-options">
|
|
<label><input v-model="remember" type="checkbox" /><span></span>记住登录状态</label>
|
|
<button type="button" @click="forgotVisible = true">忘记密码?</button>
|
|
</div>
|
|
<div class="login-error">{{ loginError }}</div>
|
|
<button class="login-submit" type="submit" :disabled="loading">登录平台<svg><use href="#i-chevron"/></svg></button>
|
|
</form>
|
|
<small class="login-help">还没有企业账号?<button type="button" class="login-switch" @click="mode = 'register'">注册企业账号</button></small>
|
|
</div>
|
|
|
|
<div class="login-panel" :class="{ active: mode === 'register' }">
|
|
<div class="login-form-heading"><span>企业入驻</span><h2>注册账号</h2><p>创建企业账户,开始使用低空智控平台</p></div>
|
|
<form novalidate @submit.prevent="submitRegister">
|
|
<label><span>企业名称</span><div class="login-input"><svg><use href="#i-box"/></svg><input v-model="reg.company" type="text" placeholder="请输入企业全称" /></div></label>
|
|
<div class="login-form-row">
|
|
<label><span>联系人</span><input v-model="reg.contact" type="text" placeholder="姓名" /></label>
|
|
<label><span>联系电话</span><input v-model="reg.phone" type="tel" placeholder="手机号" /></label>
|
|
</div>
|
|
<label><span>企业邮箱</span><div class="login-input"><svg><use href="#i-user"/></svg><input v-model="reg.email" type="email" placeholder="用于接收平台通知" /></div></label>
|
|
<div class="login-form-row">
|
|
<label><span>设置密码</span><input v-model="reg.password" type="password" placeholder="至少 6 位" /></label>
|
|
<label><span>确认密码</span><input v-model="reg.confirm" type="password" placeholder="再次输入密码" /></label>
|
|
</div>
|
|
<label class="register-agreement"><input v-model="reg.agreement" type="checkbox" /><span></span>我已阅读并同意《平台服务协议》和《隐私政策》</label>
|
|
<div class="login-error">{{ registerError }}</div>
|
|
<button class="login-submit" type="submit" :disabled="loading">提交注册<svg><use href="#i-chevron"/></svg></button>
|
|
</form>
|
|
<small class="login-help">已有企业账号?<button type="button" class="login-switch" @click="mode = 'login'">返回登录</button></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-backdrop" :class="{ open: forgotVisible }">
|
|
<div class="modal forgot-password-dialog" role="dialog" aria-modal="true">
|
|
<header><div><span>账号安全</span><h3>找回登录密码</h3></div><button class="icon-button" type="button" @click="forgotVisible = false"><svg><use href="#i-x"/></svg></button></header>
|
|
<form novalidate @submit.prevent="submitForgot">
|
|
<label><span>登录账号</span><input v-model="forgot.account" type="text" placeholder="请输入手机号" /></label>
|
|
<label><span>绑定手机号</span><input v-model="forgot.phone" type="tel" placeholder="请输入接收验证码的手机号" /></label>
|
|
<label><span>短信验证码</span><div class="login-input"><svg><use href="#i-message"/></svg><input v-model="forgot.code" inputmode="numeric" maxlength="6" placeholder="请输入短信验证码" /><button class="send-code-button" type="button" @click="sendForgotCode">获取验证码</button></div></label>
|
|
<label><span>设置新密码</span><input v-model="forgot.password" type="password" placeholder="至少 6 位" /></label>
|
|
<div class="form-error">{{ forgotError }}</div>
|
|
<footer><button class="plain-command" type="button" @click="forgotVisible = false">取消</button><button class="primary-command" type="submit">确认重置</button></footer>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { reactive, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import request from '../api/request'
|
|
import { useUserStore } from '../store/user'
|
|
import { useUiStore } from '../store/ui'
|
|
|
|
const router = useRouter()
|
|
const userStore = useUserStore()
|
|
const ui = useUiStore()
|
|
|
|
const mode = ref('login')
|
|
const method = ref('password')
|
|
const loading = ref(false)
|
|
const pwdVisible = ref(false)
|
|
const remember = ref(true)
|
|
|
|
const account = ref('')
|
|
const password = ref('')
|
|
const smsPhone = ref('')
|
|
const smsCode = ref('')
|
|
const captcha = ref('')
|
|
const captchaValue = ref('7K4P')
|
|
const loginError = ref('')
|
|
const loginCodeTimer = ref(0)
|
|
|
|
const reg = reactive({ company: '', contact: '', phone: '', email: '', password: '', confirm: '', agreement: false })
|
|
const registerError = ref('')
|
|
|
|
const forgotVisible = ref(false)
|
|
const forgot = reactive({ account: '', phone: '', code: '', password: '' })
|
|
const forgotError = ref('')
|
|
|
|
function refreshCaptcha() {
|
|
const chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'
|
|
captchaValue.value = Array.from({ length: 4 }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
|
|
}
|
|
|
|
async function submitLogin() {
|
|
loginError.value = ''
|
|
if (!captcha.value) { loginError.value = '请输入图形验证码。'; return }
|
|
if (captcha.value.toUpperCase() !== captchaValue.value) { loginError.value = '图形验证码不正确,请重新输入。'; return }
|
|
if (method.value === 'password' && (!account.value || !password.value)) { loginError.value = '请输入账号和密码。'; return }
|
|
if (method.value === 'sms') { loginError.value = '短信登录暂未开放,请使用账号密码登录。'; return }
|
|
loading.value = true
|
|
try {
|
|
const data = await request.post('/v1/auth/login', { phone: account.value, password: password.value })
|
|
userStore.setLogin(data.accessToken, data.user)
|
|
ui.toast('登录成功')
|
|
router.push('/monitor')
|
|
} catch (e) {
|
|
// 错误已由拦截器提示
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function submitRegister() {
|
|
registerError.value = ''
|
|
if (!reg.company || !reg.contact || !reg.phone || !reg.email || !reg.password || !reg.confirm) {
|
|
registerError.value = '请完整填写注册信息。'; return
|
|
}
|
|
if (reg.password.length < 6 || reg.password !== reg.confirm) {
|
|
registerError.value = '请确认密码一致且不少于 6 位。'; return
|
|
}
|
|
if (!reg.agreement) { registerError.value = '请先同意平台服务协议和隐私政策。'; return }
|
|
loading.value = true
|
|
try {
|
|
const data = await request.post('/v1/auth/register', { name: reg.company, phone: reg.phone, email: reg.email, password: reg.password })
|
|
if (data && data.accessToken) {
|
|
userStore.setLogin(data.accessToken, data.user)
|
|
ui.toast('注册成功')
|
|
router.push('/monitor')
|
|
} else {
|
|
mode.value = 'login'
|
|
account.value = reg.phone
|
|
password.value = ''
|
|
ui.toast('注册成功,请登录')
|
|
}
|
|
} catch (e) {
|
|
// 错误已由拦截器提示
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function sendLoginCode() {
|
|
if (!smsPhone.value) { loginError.value = '请先输入手机号。'; return }
|
|
if (loginCodeTimer.value) return
|
|
loginCodeTimer.value = 60
|
|
const timer = setInterval(() => {
|
|
loginCodeTimer.value -= 1
|
|
if (loginCodeTimer.value <= 0) clearInterval(timer)
|
|
}, 1000)
|
|
request.post('/v1/auth/sms-code', { phone: smsPhone.value }).catch(() => {})
|
|
ui.toast('短信验证码已发送')
|
|
}
|
|
|
|
function sendForgotCode() {
|
|
if (!forgot.phone) { forgotError.value = '请先输入手机号。'; return }
|
|
request.post('/v1/auth/sms-code', { phone: forgot.phone }).catch(() => {})
|
|
ui.toast('短信验证码已发送')
|
|
}
|
|
|
|
async function submitForgot() {
|
|
forgotError.value = ''
|
|
if (!forgot.account || !forgot.phone || !forgot.code || !forgot.password) { forgotError.value = '请完整填写找回信息。'; return }
|
|
if (forgot.password.length < 6) { forgotError.value = '新密码至少需要 6 位。'; return }
|
|
try {
|
|
await request.post('/v1/auth/reset-password', { phone: forgot.phone, smsCode: forgot.code, newPassword: forgot.password })
|
|
forgotVisible.value = false
|
|
ui.toast('密码已重置,请使用新密码登录')
|
|
} catch (e) {}
|
|
}
|
|
</script>
|
|
|