feat: 将登录页面的表单输入、验证和复选框替换为 Naive UI 组件并进行样式调整

This commit is contained in:
Wei_佳 2025-11-21 12:51:46 +08:00
parent 8b17d74359
commit ba2bc6c53e

View File

@ -11,58 +11,60 @@
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
:src="loginTitleImg" :src="loginTitleImg"
/> />
<div class="block_1 flex-row"> <n-form ref="formRef" :model="loginInfo" :rules="rules" :show-label="false">
<div class="image-text_1 flex-row"> <n-form-item path="phone">
<img <div class="block_1 flex-row">
class="thumbnail_1" <div class="image-text_1 flex-row">
referrerpolicy="no-referrer" <img
:src="iconUserImg" class="thumbnail_1"
/> referrerpolicy="no-referrer"
<input :src="iconUserImg"
v-model="loginInfo.phone" />
class="input-reset text-group_1" <n-input
placeholder="请输入账号" v-model:value="loginInfo.phone"
type="text" class="input-reset text-group_1 custom-n-input"
@keypress.enter="handleLogin" placeholder="请输入账号"
/> type="text"
</div> :bordered="false"
</div> @keypress.enter="handleLogin"
<div class="block_2 flex-row justify-between"> />
<div class="section_4 flex-row"> </div>
<div class="image-text_2 flex-row"> </div>
<img </n-form-item>
class="thumbnail_2" <div class="block_2 flex-row justify-between">
referrerpolicy="no-referrer" <n-form-item path="verifyCode" class="verify-code-item">
:src="iconCodeImg" <div class="section_4 flex-row">
/> <div class="image-text_2 flex-row">
<input <img
v-model="loginInfo.verifyCode" class="thumbnail_2"
class="input-reset text-group_2" referrerpolicy="no-referrer"
placeholder="请输入验证码" :src="iconCodeImg"
type="text" />
@keypress.enter="handleLogin" <n-input
/> v-model:value="loginInfo.verifyCode"
class="input-reset text-group_2 custom-n-input"
placeholder="请输入验证码"
type="text"
:bordered="false"
@keypress.enter="handleLogin"
/>
</div>
</div>
</n-form-item>
<div
class="text-wrapper_1 flex-col cursor-pointer"
@click="handleSendCode"
:class="{ 'disabled': countdown > 0 }"
>
<span class="text_1">{{ countdown > 0 ? `${countdown}秒后重新获取` : '获取验证码' }}</span>
</div> </div>
</div> </div>
<div </n-form>
class="text-wrapper_1 flex-col cursor-pointer" <div class="block_3 flex-row">
@click="handleSendCode" <n-checkbox v-model:checked="isAgreed" class="agreement-checkbox">
:class="{ 'disabled': countdown > 0 }" <span class="text-group_3">阅读并同意</span>
> <span class="text_2">用户协议</span>
<span class="text_1">{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}</span> </n-checkbox>
</div>
</div>
<div class="block_3 flex-row justify-between">
<div class="image-text_3 flex-row cursor-pointer" @click="isAgreed = !isAgreed">
<img
class="thumbnail_3"
referrerpolicy="no-referrer"
:src="iconCheckboxImg"
:style="{ opacity: isAgreed ? 1 : 0.5, filter: isAgreed ? 'none' : 'grayscale(100%)' }"
/>
<span class="text-group_3" style="margin-left: 5px;">阅读并同意</span>
</div>
<span class="text_2 cursor-pointer">用户协议</span>
</div> </div>
<div <div
class="text-wrapper_2 flex-col cursor-pointer" class="text-wrapper_2 flex-col cursor-pointer"
@ -110,6 +112,17 @@ if(localStorage.getItem('phone')){
loginInfo.value.phone = localStorage.getItem('phone') loginInfo.value.phone = localStorage.getItem('phone')
} }
const formRef = ref(null)
const rules = {
phone: [
{ required: true, message: '请输入手机号', trigger: ['input', 'blur'] },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: ['input', 'blur'] }
],
verifyCode: [
{ required: true, message: '请输入验证码', trigger: ['input', 'blur'] }
]
}
// Validate phone // Validate phone
function validatePhone(phone) { function validatePhone(phone) {
const phoneReg = /^1[3-9]\d{9}$/ const phoneReg = /^1[3-9]\d{9}$/
@ -153,50 +166,38 @@ async function handleSendCode() {
// Login // Login
async function handleLogin() { async function handleLogin() {
const { phone, verifyCode } = loginInfo.value const { phone, verifyCode } = loginInfo.value
if (!phone) {
$message.warning('请输入手机号')
return
}
if (!validatePhone(phone)) {
$message.warning('请输入正确的手机号')
return
}
if (!verifyCode) {
$message.warning('请输入验证码')
return
}
if (!isAgreed.value) { if (!isAgreed.value) {
$message.warning('请阅读并同意用户协议') $message.warning('请阅读并同意用户协议')
return return
} }
loading.value = true formRef.value?.validate(async (errors) => {
if (!errors) {
try { loading.value = true
const res = await api.loginWithVerifyCode({ phone, code: verifyCode }) try {
if (res.data?.access_token) { const res = await api.loginWithVerifyCode({ phone, code: verifyCode })
setToken(res.data.access_token) if (res.data?.access_token) {
localStorage.setItem('phone', phone) setToken(res.data.access_token)
localStorage.setItem('phone', phone)
if (query.redirect) {
const path = query.redirect if (query.redirect) {
Reflect.deleteProperty(query, 'redirect') const path = query.redirect
router.push({ path, query }) Reflect.deleteProperty(query, 'redirect')
} else { router.push({ path, query })
router.push('/home') } else {
router.push('/home')
}
} else {
$message.error('登录失败未获取到token')
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
} }
} else {
$message.error('登录失败未获取到token')
} }
} catch (error) { })
console.error(error)
} finally {
loading.value = false
}
} }
onBeforeUnmount(() => { onBeforeUnmount(() => {
@ -411,35 +412,19 @@ onBeforeUnmount(() => {
.block_3 { .block_3 {
width: 100%; width: 100%;
height: 14px;
margin-top: 36px; margin-top: 36px;
align-items: center; align-items: center;
} }
.image-text_3 {
display: flex;
align-items: center;
transition: all 0.3s ease;
}
.image-text_3:hover {
opacity: 0.8;
}
.thumbnail_3 {
width: 14px;
height: 14px;
}
.text-group_3 { .text-group_3 {
color: rgba(136, 136, 136, 1); color: rgba(136, 136, 136, 1);
font-size: 12px; font-size: 12px;
letter-spacing: 0.32px; letter-spacing: 0.32px;
font-family: Alibaba-PuHuiTi-R, sans-serif; font-family: Alibaba-PuHuiTi-R, sans-serif;
font-weight: normal; font-weight: normal;
text-align: center;
white-space: nowrap; white-space: nowrap;
line-height: 12px; line-height: 12px;
margin-right: 4px;
} }
.text_2 { .text_2 {
@ -448,11 +433,10 @@ onBeforeUnmount(() => {
letter-spacing: 0.32px; letter-spacing: 0.32px;
font-family: Alibaba-PuHuiTi-M, sans-serif; font-family: Alibaba-PuHuiTi-M, sans-serif;
font-weight: normal; font-weight: normal;
text-align: left;
white-space: nowrap; white-space: nowrap;
line-height: 12px; line-height: 12px;
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative; cursor: pointer;
} }
.text_2:hover { .text_2:hover {
@ -495,4 +479,117 @@ onBeforeUnmount(() => {
.text-wrapper_2:hover .text_3 { .text-wrapper_2:hover .text_3 {
letter-spacing: 1px; letter-spacing: 1px;
} }
/* Custom Naive UI Overrides */
.custom-n-input {
background-color: transparent !important;
font-size: 14px;
line-height: 14px;
}
.custom-n-input :deep(.n-input-wrapper) {
padding: 0 !important;
}
.custom-n-input :deep(.n-input__input-el) {
height: 16px !important;
line-height: 16px !important;
color: rgba(48, 49, 51, 1);
font-family: Alibaba-PuHuiTi-R, sans-serif;
}
.custom-n-input :deep(.n-input__placeholder) {
color: rgba(189, 189, 189, 1) !important;
}
.custom-n-input :deep(.n-input__state-border),
.custom-n-input :deep(.n-input__border) {
display: none !important;
}
/* Fix Form Item Layout to prevent shifts */
:deep(.n-form-item-feedback-wrapper) {
position: absolute;
top: 100%;
left: 0;
padding-top: 2px;
line-height: 1;
}
:deep(.n-form-item) {
display: block;
margin-bottom: 0 !important;
}
/* Ensure the code input item doesn't break flex layout */
.verify-code-item {
flex: 1;
/* The section_4 inside has fixed width, so we just let it be */
max-width: 191px;
}
/* Input Container Interactions */
.block_1,
.section_4 {
transition: all 0.3s ease;
}
.block_1:hover,
.section_4:hover {
border-color: rgba(163, 1, 19, 0.5);
}
.block_1:focus-within,
.section_4:focus-within {
border-color: rgba(163, 1, 19, 1);
box-shadow: 0 0 0 2px rgba(163, 1, 19, 0.1);
}
/* Error State Styling */
:deep(.n-form-item.n-form-item--error) .block_1,
:deep(.n-form-item.n-form-item--error) .section_4 {
border-color: #d03050;
}
:deep(.n-form-item.n-form-item--error) .block_1:focus-within,
:deep(.n-form-item.n-form-item--error) .section_4:focus-within {
border-color: #d03050;
box-shadow: 0 0 0 2px rgba(208, 48, 80, 0.1);
}
/* Checkbox Customization */
:deep(.agreement-checkbox .n-checkbox-box) {
border-radius: 2px;
transition: all 0.3s ease;
}
/* Checked State - Solid Red */
:deep(.agreement-checkbox.n-checkbox--checked .n-checkbox-box) {
background-color: rgba(163, 1, 19, 1) !important;
border: 1px solid rgba(163, 1, 19, 1) !important;
}
/* Hide the separate border element when checked to prevent color overlap/double borders */
:deep(.agreement-checkbox.n-checkbox--checked .n-checkbox-box .n-checkbox-box__border) {
display: none !important;
}
/* Hover State - Red Border */
:deep(.agreement-checkbox:hover .n-checkbox-box .n-checkbox-box__border) {
border-color: rgba(163, 1, 19, 1) !important;
}
/* Focus State - Red Border & Shadow */
:deep(.agreement-checkbox.n-checkbox--focus .n-checkbox-box .n-checkbox-box__border) {
border-color: rgba(163, 1, 19, 1) !important;
}
:deep(.agreement-checkbox.n-checkbox--focus .n-checkbox-box) {
box-shadow: 0 0 0 2px rgba(163, 1, 19, 0.2) !important;
}
/* Ensure checkmark is white */
:deep(.agreement-checkbox .n-checkbox-icon) {
color: #fff !important;
}
</style> </style>