Browse Source

feat: rewrite LoginView with on-demand TDesign

main
xiaosi 1 month ago
parent
commit
71c8d478c0
  1. 763
      src/views/LoginView/LoginView.vue

763
src/views/LoginView/LoginView.vue

@ -1,85 +1,3 @@
<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> <script setup>
import { reactive, ref } from 'vue' import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -106,7 +24,15 @@ const captchaValue = ref('7K4P')
const loginError = ref('') const loginError = ref('')
const loginCodeTimer = ref(0) const loginCodeTimer = ref(0)
const reg = reactive({ company: '', contact: '', phone: '', email: '', password: '', confirm: '', agreement: false })
const reg = reactive({
company: '',
contact: '',
phone: '',
email: '',
password: '',
confirm: '',
agreement: false,
})
const registerError = ref('') const registerError = ref('')
const forgotVisible = ref(false) const forgotVisible = ref(false)
@ -120,13 +46,28 @@ function refreshCaptcha() {
async function submitLogin() { async function submitLogin() {
loginError.value = '' 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 }
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 loading.value = true
try { try {
const data = await request.post('/v1/auth/login', { phone: account.value, password: password.value })
const data = await request.post('/v1/auth/login', {
phone: account.value,
password: password.value,
})
userStore.setLogin(data.accessToken, data.user) userStore.setLogin(data.accessToken, data.user)
ui.toast('登录成功') ui.toast('登录成功')
router.push('/monitor') router.push('/monitor')
@ -140,15 +81,25 @@ async function submitLogin() {
async function submitRegister() { async function submitRegister() {
registerError.value = '' registerError.value = ''
if (!reg.company || !reg.contact || !reg.phone || !reg.email || !reg.password || !reg.confirm) { if (!reg.company || !reg.contact || !reg.phone || !reg.email || !reg.password || !reg.confirm) {
registerError.value = '请完整填写注册信息。'; return
registerError.value = '请完整填写注册信息。'
return
} }
if (reg.password.length < 6 || reg.password !== reg.confirm) { if (reg.password.length < 6 || reg.password !== reg.confirm) {
registerError.value = '请确认密码一致且不少于 6 位。'; return
registerError.value = '请确认密码一致且不少于 6 位。'
return
}
if (!reg.agreement) {
registerError.value = '请先同意平台服务协议和隐私政策。'
return
} }
if (!reg.agreement) { registerError.value = '请先同意平台服务协议和隐私政策。'; return }
loading.value = true loading.value = true
try { try {
const data = await request.post('/v1/auth/register', { name: reg.company, phone: reg.phone, email: reg.email, password: reg.password })
const data = await request.post('/v1/auth/register', {
name: reg.company,
phone: reg.phone,
email: reg.email,
password: reg.password,
})
if (data && data.accessToken) { if (data && data.accessToken) {
userStore.setLogin(data.accessToken, data.user) userStore.setLogin(data.accessToken, data.user)
ui.toast('注册成功') ui.toast('注册成功')
@ -167,7 +118,10 @@ async function submitRegister() {
} }
function sendLoginCode() { function sendLoginCode() {
if (!smsPhone.value) { loginError.value = '请先输入手机号。'; return }
if (!smsPhone.value) {
loginError.value = '请先输入手机号。'
return
}
if (loginCodeTimer.value) return if (loginCodeTimer.value) return
loginCodeTimer.value = 60 loginCodeTimer.value = 60
const timer = setInterval(() => { const timer = setInterval(() => {
@ -179,19 +133,632 @@ function sendLoginCode() {
} }
function sendForgotCode() { function sendForgotCode() {
if (!forgot.phone) { forgotError.value = '请先输入手机号。'; return }
if (!forgot.phone) {
forgotError.value = '请先输入手机号。'
return
}
request.post('/v1/auth/sms-code', { phone: forgot.phone }).catch(() => {}) request.post('/v1/auth/sms-code', { phone: forgot.phone }).catch(() => {})
ui.toast('短信验证码已发送') ui.toast('短信验证码已发送')
} }
async function submitForgot() { async function submitForgot() {
forgotError.value = '' forgotError.value = ''
if (!forgot.account || !forgot.phone || !forgot.code || !forgot.password) { forgotError.value = '请完整填写找回信息。'; return }
if (forgot.password.length < 6) { forgotError.value = '新密码至少需要 6 位。'; return }
if (!forgot.account || !forgot.phone || !forgot.code || !forgot.password) {
forgotError.value = '请完整填写找回信息。'
return false
}
if (forgot.password.length < 6) {
forgotError.value = '新密码至少需要 6 位。'
return false
}
try { try {
await request.post('/v1/auth/reset-password', { phone: forgot.phone, smsCode: forgot.code, newPassword: forgot.password })
await request.post('/v1/auth/reset-password', {
phone: forgot.phone,
smsCode: forgot.code,
newPassword: forgot.password,
})
forgotVisible.value = false forgotVisible.value = false
ui.toast('密码已重置,请使用新密码登录') ui.toast('密码已重置,请使用新密码登录')
} catch (e) {}
} catch (e) {
return false
}
} }
</script> </script>
<template>
<section :class="s.page">
<div :class="s.visual">
<div :class="s.visualMap" />
<div :class="s.visualShade" />
<div :class="s.brand">
<div :class="s.brandMark"><span /><span /><span /></div>
<div>
<strong>嘉谷</strong>
<small>低空智控平台</small>
</div>
</div>
<div :class="s.visualCopy">
<span>LOW-ALTITUDE INTELLIGENCE</span>
<h1>让每一次飞行<br />都可控可追溯</h1>
<p>统一管理机巢无人机任务与视频资产</p>
</div>
<div :class="s.visualFooter">
<span>嘉谷低空智控平台</span>
<span>© 2026 嘉谷科技</span>
</div>
</div>
<div :class="s.formSide">
<div :class="s.formWrap">
<div v-show="mode === 'login'" :class="s.panel">
<div :class="s.methodTabs">
<button
type="button"
:class="[s.tab, { [s.tabActive]: method === 'password' }]"
@click="method = 'password'"
>
账号密码登录
</button>
<button
type="button"
:class="[s.tab, { [s.tabActive]: method === 'sms' }]"
@click="method = 'sms'"
>
手机号登录
</button>
</div>
<t-form label-width="0" @submit="submitLogin">
<template v-if="method === 'password'">
<t-form-item>
<t-input
v-model="account"
size="large"
clearable
placeholder="请输入手机号或企业邮箱"
autocomplete="username"
>
<template #prefix-icon>
<t-icon name="user" />
</template>
</t-input>
</t-form-item>
<t-form-item>
<t-input
v-model="password"
size="large"
clearable
:type="pwdVisible ? 'text' : 'password'"
placeholder="请输入登录密码"
autocomplete="current-password"
>
<template #prefix-icon>
<t-icon name="lock-on" />
</template>
<template #suffix-icon>
<t-icon
:name="pwdVisible ? 'browse' : 'browse-off'"
:class="s.iconClick"
@click="pwdVisible = !pwdVisible"
/>
</template>
</t-input>
</t-form-item>
</template>
<template v-else>
<t-form-item>
<t-input
v-model="smsPhone"
size="large"
clearable
type="tel"
placeholder="请输入绑定手机号"
>
<template #prefix-icon>
<t-icon name="mobile" />
</template>
</t-input>
</t-form-item>
<t-form-item>
<div :class="s.codeRow">
<t-input
v-model="smsCode"
size="large"
clearable
:maxlength="6"
placeholder="请输入短信验证码"
>
<template #prefix-icon>
<t-icon name="mail" />
</template>
</t-input>
<t-button
theme="default"
variant="outline"
size="large"
:disabled="loginCodeTimer > 0"
@click="sendLoginCode"
>
{{ loginCodeTimer > 0 ? `${loginCodeTimer}s 后重试` : '获取验证码' }}
</t-button>
</div>
</t-form-item>
</template>
<t-form-item>
<div :class="s.codeRow">
<t-input
v-model="captcha"
size="large"
clearable
:maxlength="4"
placeholder="请输入验证码"
>
<template #prefix-icon>
<t-icon name="secured" />
</template>
</t-input>
<t-button
theme="default"
variant="outline"
size="large"
:class="s.captchaBtn"
title="刷新验证码"
@click="refreshCaptcha"
>
{{ captchaValue }}
</t-button>
</div>
</t-form-item>
<div :class="s.options">
<t-checkbox v-model="remember">记住登录状态</t-checkbox>
<t-button theme="primary" variant="text" @click="forgotVisible = true">
忘记密码
</t-button>
</div>
<div v-if="loginError" :class="s.error">{{ loginError }}</div>
<t-form-item>
<t-button
block
size="large"
type="submit"
theme="primary"
:loading="loading"
>
登录平台
</t-button>
</t-form-item>
</t-form>
<p :class="s.help">
还没有企业账号
<t-button theme="primary" variant="text" @click="mode = 'register'">
注册企业账号
</t-button>
</p>
</div>
<div v-show="mode === 'register'" :class="s.panel">
<div :class="s.heading">
<span>企业入驻</span>
<h2>注册账号</h2>
<p>创建企业账户开始使用低空智控平台</p>
</div>
<t-form label-width="0" @submit="submitRegister">
<t-form-item>
<t-input
v-model="reg.company"
size="large"
clearable
placeholder="请输入企业全称"
>
<template #prefix-icon>
<t-icon name="root-list" />
</template>
</t-input>
</t-form-item>
<div :class="s.row">
<t-form-item>
<t-input v-model="reg.contact" size="large" clearable placeholder="联系人姓名" />
</t-form-item>
<t-form-item>
<t-input
v-model="reg.phone"
size="large"
clearable
type="tel"
placeholder="联系电话"
/>
</t-form-item>
</div>
<t-form-item>
<t-input
v-model="reg.email"
size="large"
clearable
type="email"
placeholder="企业邮箱,用于接收平台通知"
>
<template #prefix-icon>
<t-icon name="mail" />
</template>
</t-input>
</t-form-item>
<div :class="s.row">
<t-form-item>
<t-input
v-model="reg.password"
size="large"
type="password"
clearable
placeholder="设置密码(至少 6 位)"
/>
</t-form-item>
<t-form-item>
<t-input
v-model="reg.confirm"
size="large"
type="password"
clearable
placeholder="确认密码"
/>
</t-form-item>
</div>
<t-form-item>
<t-checkbox v-model="reg.agreement">
我已阅读并同意平台服务协议隐私政策
</t-checkbox>
</t-form-item>
<div v-if="registerError" :class="s.error">{{ registerError }}</div>
<t-form-item>
<t-button
block
size="large"
type="submit"
theme="primary"
:loading="loading"
>
提交注册
</t-button>
</t-form-item>
</t-form>
<p :class="s.help">
已有企业账号
<t-button theme="primary" variant="text" @click="mode = 'login'">
返回登录
</t-button>
</p>
</div>
</div>
</div>
<t-dialog
v-model:visible="forgotVisible"
header="找回登录密码"
width="420px"
:confirm-btn="{ content: '确认重置', theme: 'primary' }"
cancel-btn="取消"
@confirm="submitForgot"
>
<t-form label-width="0">
<t-form-item>
<t-input v-model="forgot.account" size="large" clearable placeholder="请输入登录账号(手机号)" />
</t-form-item>
<t-form-item>
<t-input
v-model="forgot.phone"
size="large"
clearable
type="tel"
placeholder="请输入接收验证码的手机号"
/>
</t-form-item>
<t-form-item>
<div :class="s.codeRow">
<t-input
v-model="forgot.code"
size="large"
clearable
:maxlength="6"
placeholder="请输入短信验证码"
/>
<t-button theme="default" variant="outline" size="large" @click="sendForgotCode">
获取验证码
</t-button>
</div>
</t-form-item>
<t-form-item>
<t-input
v-model="forgot.password"
size="large"
type="password"
clearable
placeholder="设置新密码(至少 6 位)"
/>
</t-form-item>
<div v-if="forgotError" :class="s.error">{{ forgotError }}</div>
</t-form>
</t-dialog>
</section>
</template>
<style lang="less" module="s">
.page {
position: relative;
display: block;
min-height: 100vh;
overflow: hidden;
background: #102a35;
}
.visual {
position: absolute;
z-index: 0;
inset: 0;
width: 100%;
min-height: 100vh;
color: #fff;
background: #1b323c;
overflow: hidden;
}
.visualMap {
position: absolute;
inset: 0;
background-image: url('/assets/login-hero-dock-drone.png');
background-size: cover;
background-position: center;
filter: saturate(0.82) contrast(1.04);
}
.visualShade {
position: absolute;
inset: 0;
background:
linear-gradient(90deg, rgba(10, 31, 40, 0.78), rgba(12, 38, 48, 0.18) 62%, rgba(9, 26, 34, 0.34)),
linear-gradient(0deg, rgba(9, 26, 34, 0.72), transparent 42%);
}
.brand {
position: absolute;
z-index: 1;
top: 36px;
left: 40px;
display: flex;
align-items: center;
gap: 12px;
strong {
display: block;
font-size: 18px;
font-weight: 600;
}
small {
display: block;
margin-top: 2px;
opacity: 0.78;
font-size: 12px;
}
}
.brandMark {
display: grid;
grid-template-columns: repeat(3, 6px);
gap: 4px;
span {
width: 6px;
height: 18px;
border-radius: 2px;
background: #5ec8f0;
&:nth-child(2) {
opacity: 0.7;
height: 12px;
align-self: end;
}
&:nth-child(3) {
opacity: 0.45;
height: 8px;
align-self: end;
}
}
}
.visualCopy {
position: absolute;
z-index: 1;
left: 40px;
bottom: 96px;
max-width: 420px;
> span {
letter-spacing: 0.12em;
font-size: 11px;
opacity: 0.72;
}
h1 {
margin: 14px 0 12px;
font-size: 34px;
line-height: 1.25;
font-weight: 600;
}
p {
margin: 0;
opacity: 0.82;
font-size: 14px;
}
}
.visualFooter {
position: absolute;
z-index: 1;
left: 40px;
right: 40px;
bottom: 28px;
display: flex;
justify-content: space-between;
opacity: 0.65;
font-size: 12px;
}
.formSide {
position: relative;
z-index: 2;
width: 100%;
min-height: 100vh;
padding: 40px 24px;
display: flex;
align-items: center;
justify-content: center;
}
.formWrap {
width: min(400px, 100%);
padding: 36px 32px 28px;
border: 1px solid rgba(225, 231, 236, 0.9);
border-radius: 10px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 20px 55px rgba(10, 31, 43, 0.25);
}
.panel {
width: 100%;
}
.methodTabs {
height: 38px;
margin: 0 0 22px;
display: grid;
grid-template-columns: 1fr 1fr;
border-bottom: 1px solid #dce4e9;
}
.tab {
position: relative;
border: 0;
color: #7d8b94;
background: transparent;
font-size: 13px;
cursor: pointer;
}
.tabActive {
color: #1f6feb;
font-weight: 600;
&::after {
content: '';
position: absolute;
right: 0;
bottom: -1px;
left: 0;
height: 2px;
background: #1f6feb;
}
}
.codeRow {
width: 100%;
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
align-items: center;
}
.captchaBtn {
min-width: 96px;
letter-spacing: 0.18em;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.options {
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0 14px;
}
.error {
margin: 0 0 12px;
color: #d54941;
font-size: 13px;
}
.help {
margin: 8px 0 0;
color: #6b7b86;
font-size: 13px;
text-align: center;
}
.heading {
margin-bottom: 22px;
> span {
color: #1f6feb;
font-size: 12px;
}
h2 {
margin: 10px 0 8px;
color: #263945;
font-size: 28px;
font-weight: 600;
}
p {
margin: 0;
color: #6b7b86;
font-size: 13px;
}
}
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.iconClick {
cursor: pointer;
}
@media (max-width: 820px) {
.visual {
display: none;
}
.page {
background: #f3f6f9;
}
.formSide {
padding: 28px 22px;
}
.formWrap {
box-shadow: 0 12px 28px rgba(28, 53, 70, 0.08);
}
.row {
grid-template-columns: 1fr;
}
}
</style>

Loading…
Cancel
Save