feat: 样式升级
This commit is contained in:
@@ -101,80 +101,86 @@
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<a-form
|
||||
:model="smsForm"
|
||||
:rules="smsRules"
|
||||
ref="smsFormRef"
|
||||
layout="vertical"
|
||||
class="login-form"
|
||||
>
|
||||
<a-form-item name="mobile" label="手机号码">
|
||||
<form class="login-form" @submit.prevent="handleSmsLogin">
|
||||
<!-- 手机号 -->
|
||||
<div class="form-item" :class="{ 'has-error': errors.mobile }">
|
||||
<label class="form-label">手机号码</label>
|
||||
<div class="input-wrapper">
|
||||
<a-input
|
||||
v-model:value="smsForm.mobile"
|
||||
size="large"
|
||||
<div class="input-prefix">
|
||||
<Icon icon="lucide:smartphone" class="input-icon" />
|
||||
</div>
|
||||
<input
|
||||
v-model="smsForm.mobile"
|
||||
type="tel"
|
||||
class="custom-input"
|
||||
:class="{ 'input-error': errors.mobile }"
|
||||
placeholder="请输入手机号"
|
||||
:maxlength="11"
|
||||
maxlength="11"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
<template #prefix>
|
||||
<PhoneOutlined class="input-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
@blur="validateMobile"
|
||||
/>
|
||||
<div class="input-glow"></div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<span v-if="errors.mobile" class="error-message">{{ errors.mobile }}</span>
|
||||
</div>
|
||||
|
||||
<a-form-item name="code" label="验证码">
|
||||
<!-- 验证码 -->
|
||||
<div class="form-item" :class="{ 'has-error': errors.code }">
|
||||
<label class="form-label">验证码</label>
|
||||
<div class="code-row">
|
||||
<div class="input-wrapper code-input">
|
||||
<a-input
|
||||
v-model:value="smsForm.code"
|
||||
size="large"
|
||||
<div class="input-prefix">
|
||||
<Icon icon="lucide:shield-check" class="input-icon" />
|
||||
</div>
|
||||
<input
|
||||
v-model="smsForm.code"
|
||||
type="text"
|
||||
class="custom-input"
|
||||
:class="{ 'input-error': errors.code }"
|
||||
placeholder="请输入验证码"
|
||||
:maxlength="4"
|
||||
maxlength="4"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
<template #prefix>
|
||||
<SafetyOutlined class="input-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
@blur="validateCode"
|
||||
/>
|
||||
<div class="input-glow"></div>
|
||||
</div>
|
||||
<a-button
|
||||
type="primary"
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
:disabled="codeCountdown > 0"
|
||||
:loading="sendingCode"
|
||||
@click="sendSmsCode"
|
||||
class="code-btn"
|
||||
@click="sendSmsCode"
|
||||
>
|
||||
<span v-if="sendingCode" class="custom-spinner small"></span>
|
||||
<span v-if="codeCountdown > 0">{{ codeCountdown }}s</span>
|
||||
<span v-else>获取验证码</span>
|
||||
</a-button>
|
||||
</Button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<span v-if="errors.code" class="error-message">{{ errors.code }}</span>
|
||||
</div>
|
||||
|
||||
<a-form-item class="submit-item">
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
size="large"
|
||||
block
|
||||
:loading="loggingIn"
|
||||
@click="handleSmsLogin"
|
||||
class="submit-btn"
|
||||
<!-- 提交按钮 -->
|
||||
<div class="submit-item">
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
class="submit-btn w-full"
|
||||
:disabled="loggingIn"
|
||||
>
|
||||
<span class="btn-text">立即登录</span>
|
||||
<span class="btn-arrow">→</span>
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<span v-if="loggingIn" class="custom-spinner"></span>
|
||||
<template v-else>
|
||||
<span class="btn-text">立即登录</span>
|
||||
<span class="btn-arrow">→</span>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 底部装饰 -->
|
||||
<div class="card-footer">
|
||||
@@ -192,11 +198,9 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
PhoneOutlined,
|
||||
SafetyOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import authApi, { SMS_SCENE } from '@/api/auth'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
@@ -208,7 +212,12 @@ const smsForm = reactive({
|
||||
mobile: '',
|
||||
code: ''
|
||||
})
|
||||
const smsFormRef = ref()
|
||||
|
||||
// 错误信息
|
||||
const errors = reactive({
|
||||
mobile: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
// 状态
|
||||
const sendingCode = ref(false)
|
||||
@@ -218,6 +227,39 @@ const codeCountdown = ref(0)
|
||||
// 验证码倒计时
|
||||
let countdownTimer = null
|
||||
|
||||
// 表单验证
|
||||
const validateMobile = () => {
|
||||
if (!smsForm.mobile) {
|
||||
errors.mobile = '请输入手机号'
|
||||
return false
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(smsForm.mobile)) {
|
||||
errors.mobile = '请输入正确的手机号'
|
||||
return false
|
||||
}
|
||||
errors.mobile = ''
|
||||
return true
|
||||
}
|
||||
|
||||
const validateCode = () => {
|
||||
if (!smsForm.code) {
|
||||
errors.code = '请输入验证码'
|
||||
return false
|
||||
}
|
||||
if (!/^\d{4}$/.test(smsForm.code)) {
|
||||
errors.code = '验证码为4位数字'
|
||||
return false
|
||||
}
|
||||
errors.code = ''
|
||||
return true
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
const mobileValid = validateMobile()
|
||||
const codeValid = validateCode()
|
||||
return mobileValid && codeValid
|
||||
}
|
||||
|
||||
// ========== 粒子系统 ==========
|
||||
const particleCanvas = ref(null)
|
||||
let particles = []
|
||||
@@ -361,13 +403,17 @@ const smsRules = {
|
||||
|
||||
// 发送验证码
|
||||
async function sendSmsCode() {
|
||||
// 验证手机号
|
||||
if (!validateMobile()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await smsFormRef.value.validateFields(['mobile'])
|
||||
sendingCode.value = true
|
||||
|
||||
await authApi.sendSmsCode(smsForm.mobile, SMS_SCENE.MEMBER_LOGIN)
|
||||
|
||||
message.success('验证码已发送')
|
||||
toast.success('验证码已发送')
|
||||
|
||||
// 开始倒计时
|
||||
codeCountdown.value = 60
|
||||
@@ -379,11 +425,11 @@ async function sendSmsCode() {
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
if (error?.response?.data?.message) {
|
||||
message.error(error.response.data.message)
|
||||
toast.error(error.response.data.message)
|
||||
} else if (error.message) {
|
||||
message.error(error.message)
|
||||
toast.error(error.message)
|
||||
} else {
|
||||
message.error('发送验证码失败')
|
||||
toast.error('发送验证码失败')
|
||||
}
|
||||
} finally {
|
||||
sendingCode.value = false
|
||||
@@ -392,8 +438,12 @@ async function sendSmsCode() {
|
||||
|
||||
// 短信登录
|
||||
async function handleSmsLogin() {
|
||||
// 验证表单
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await smsFormRef.value.validateFields()
|
||||
loggingIn.value = true
|
||||
|
||||
const info = await authApi.loginBySms(smsForm.mobile, smsForm.code)
|
||||
@@ -412,14 +462,14 @@ async function handleSmsLogin() {
|
||||
// 获取完整的用户信息
|
||||
await userStore.fetchUserInfo()
|
||||
|
||||
message.success('登录成功')
|
||||
toast.success('登录成功')
|
||||
|
||||
router.push({ name: '对标分析' })
|
||||
} catch (error) {
|
||||
if (error?.response?.data?.message) {
|
||||
message.error(error.response.data.message)
|
||||
toast.error(error.response.data.message)
|
||||
} else {
|
||||
message.error('登录失败,请检查输入信息')
|
||||
toast.error('登录失败,请检查输入信息')
|
||||
}
|
||||
} finally {
|
||||
loggingIn.value = false
|
||||
@@ -825,79 +875,92 @@ async function handleSmsLogin() {
|
||||
|
||||
/* ========== 表单样式 ========== */
|
||||
.login-form {
|
||||
:deep(.ant-form-item) {
|
||||
.form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label > label) {
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: @text-secondary;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
.error-message {
|
||||
display: block;
|
||||
color: #E5484D;
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
&.has-error {
|
||||
.custom-input {
|
||||
border-color: #E5484D !important;
|
||||
background: rgba(229, 72, 77, 0.05) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
background: rgba(255, 255, 255, 0.03) !important;
|
||||
border: 1px solid @border-subtle !important;
|
||||
border-radius: 12px !important;
|
||||
height: 52px !important;
|
||||
box-shadow: none !important;
|
||||
transition: all 0.3s ease !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid @border-subtle;
|
||||
border-radius: 12px;
|
||||
height: 52px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: @border-active !important;
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
border-color: @border-active;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
&.ant-input-affix-wrapper-focused {
|
||||
border-color: @primary-gold !important;
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
box-shadow: 0 0 0 3px rgba(212, 168, 83, 0.1) !important;
|
||||
&:focus-within {
|
||||
border-color: @primary-gold;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 0 0 3px rgba(212, 168, 83, 0.1);
|
||||
|
||||
.input-glow {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input) {
|
||||
color: @text-primary !important;
|
||||
font-size: 15px !important;
|
||||
background: transparent !important;
|
||||
|
||||
&::placeholder {
|
||||
color: @text-muted !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input-prefix) {
|
||||
color: @text-muted !important;
|
||||
margin-right: 12px !important;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-has-error .ant-input-affix-wrapper) {
|
||||
border-color: #E5484D !important;
|
||||
background: rgba(229, 72, 77, 0.05) !important;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-explain-error) {
|
||||
color: #E5484D;
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
.input-prefix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 16px;
|
||||
color: @text-muted;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding: 0 16px 0 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: @text-primary;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: @text-muted;
|
||||
}
|
||||
|
||||
&.input-error {
|
||||
border-color: #E5484D;
|
||||
}
|
||||
}
|
||||
|
||||
.input-glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -908,10 +971,6 @@ async function handleSmsLogin() {
|
||||
box-shadow: 0 0 20px rgba(212, 168, 83, 0.15);
|
||||
}
|
||||
|
||||
.input-wrapper:focus-within .input-glow {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 验证码行 */
|
||||
.code-row {
|
||||
display: flex;
|
||||
@@ -974,7 +1033,7 @@ async function handleSmsLogin() {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(212, 168, 83, 0.3);
|
||||
|
||||
@@ -991,6 +1050,11 @@ async function handleSmsLogin() {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-text,
|
||||
.btn-arrow {
|
||||
position: relative;
|
||||
@@ -1003,6 +1067,25 @@ async function handleSmsLogin() {
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义加载动画 */
|
||||
.custom-spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(10, 11, 13, 0.2);
|
||||
border-top-color: @deep-black;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
|
||||
&.small {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 卡片底部 */
|
||||
.card-footer {
|
||||
display: flex;
|
||||
@@ -1237,8 +1320,8 @@ async function handleSmsLogin() {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
height: 48px !important;
|
||||
.input-wrapper {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.code-btn,
|
||||
|
||||
Reference in New Issue
Block a user