feat: 样式升级

This commit is contained in:
2026-03-16 23:54:01 +08:00
parent 110fe62404
commit 4a5fdd3961
42 changed files with 1931 additions and 1404 deletions

View File

@@ -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,