feat: 更新表单和按钮样式,优化验证码组件逻辑

- 在 `App.vue` 中添加 `autoInsertSpaceInButton` 属性
- 更新 `login/style.scss` 和 `styles/components/ant-input.scss` 样式文件
- 优化 `verification-code/index.vue` 组件的验证逻辑和样式
- 更新 `styles/components/ant-button.scss` 样式文件
- 优化 `register-form/index.vue` 和 `change-password-modal/index.vue` 组件的密码输入逻辑和样式
This commit is contained in:
rd
2025-09-16 17:59:18 +08:00
8 changed files with 174 additions and 82 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<ConfigProvider :locale="zhCN" :theme="redTheme"> <ConfigProvider :autoInsertSpaceInButton="false" :locale="zhCN" :theme="redTheme">
<router-view v-if="$route.meta.withoutLayout" /> <router-view v-if="$route.meta.withoutLayout" />
<LayoutBasic v-else /> <LayoutBasic v-else />
</ConfigProvider> </ConfigProvider>

View File

@ -19,6 +19,7 @@
padding: 0 16px; padding: 0 16px;
height: 28px; height: 28px;
} }
} }
.ant-btn-default { .ant-btn-default {
&:disabled { &:disabled {
@ -51,6 +52,8 @@
background-color: $color-primary !important; background-color: $color-primary !important;
border-color: transparent !important; border-color: transparent !important;
color: #fff !important; color: #fff !important;
&:disabled { &:disabled {
color: #fff !important; color: #fff !important;
border-color: transparent !important; border-color: transparent !important;
@ -66,7 +69,12 @@
outline: none; outline: none;
outline-offset: unset; outline-offset: unset;
} }
&:active {
background: $color-primary-7 !important;
}
} }
&.ant-btn-dangerous { &.ant-btn-dangerous {
background-color: $color-error !important; background-color: $color-error !important;
&:disabled { &:disabled {
@ -76,6 +84,10 @@
&:hover { &:hover {
background-color: $color-error-5 !important; background-color: $color-error-5 !important;
} }
&:active {
background-color: $color-error-7 !important;
}
} }
} }
&.ant-btn-background-ghost { &.ant-btn-background-ghost {
@ -93,6 +105,11 @@
color: $color-primary-5 !important; color: $color-primary-5 !important;
background-color: #fff !important; background-color: #fff !important;
} }
&:active {
border-color: $color-primary-7 !important;
color: $color-primary-7 !important;
}
} }
&.ant-btn-dangerous { &.ant-btn-dangerous {
border: 1px solid $color-error !important; border: 1px solid $color-error !important;
@ -107,6 +124,11 @@
border-color: $color-error-5 !important; border-color: $color-error-5 !important;
color: $color-error-5 !important; color: $color-error-5 !important;
} }
&:active {
border-color: $color-error-7 !important;
color: $color-error-7 !important;
}
} }
} }
} }
@ -124,7 +146,13 @@
border-color: $color-primary-5 !important; border-color: $color-primary-5 !important;
color: $color-primary-5 !important; color: $color-primary-5 !important;
} }
&:active {
border-color: $color-primary-7 !important;
color: $color-primary-7 !important;
}
} }
&.ant-btn-dangerous { &.ant-btn-dangerous {
border: 1px solid $color-error !important; border: 1px solid $color-error !important;
color: $color-error !important; color: $color-error !important;
@ -137,6 +165,11 @@
border-color: $color-error-5 !important; border-color: $color-error-5 !important;
color: $color-error-5 !important; color: $color-error-5 !important;
} }
&:active {
border-color: $color-error-7 !important;
color: $color-error-7 !important;
}
} }
} }
} }
@ -154,6 +187,10 @@
color: $color-primary-5 !important; color: $color-primary-5 !important;
border-color: transparent !important; border-color: transparent !important;
} }
&:active {
color: $color-primary-7 !important;
}
} }
&.ant-btn-dangerous { &.ant-btn-dangerous {
color: $color-error !important; color: $color-error !important;
@ -164,6 +201,10 @@
&:hover { &:hover {
color: $color-error-5 !important; color: $color-error-5 !important;
} }
&:active {
color: $color-error-7 !important;
}
} }
} }
} }
@ -181,6 +222,10 @@
color: $color-primary-5 !important; color: $color-primary-5 !important;
border-color: transparent !important; border-color: transparent !important;
} }
&:active {
color: $color-primary-7 !important;
}
} }
&.ant-btn-dangerous { &.ant-btn-dangerous {
color: $color-error !important; color: $color-error !important;
@ -191,6 +236,10 @@
&:hover { &:hover {
color: $color-error-5 !important; color: $color-error-5 !important;
} }
&:active {
color: $color-error-7 !important;
}
} }
} }
} }

View File

@ -20,6 +20,7 @@
color: var(--Text-4, #939499); color: var(--Text-4, #939499);
} }
&:hover { &:hover {
background-color: #fff !important; background-color: #fff !important;
} }
@ -43,6 +44,15 @@
border-color: $color-error !important; border-color: $color-error !important;
} }
} }
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus,
&:-webkit-autofill:active {
-webkit-transition-delay: 99999s;
-webkit-transition: color 99999s ease-out,
background-color 99999s ease-out;
}
} }
input.ant-input { input.ant-input {
height: 32px; height: 32px;
@ -64,11 +74,17 @@ textarea.ant-input {
background-color: var(--BG-200, #f2f3f5) !important; background-color: var(--BG-200, #f2f3f5) !important;
} }
&.ant-input-affix-wrapper-status-error { &:not(.ant-input-affix-wrapper-disabled) {
&:not(.ant-input-affix-wrapper-disabled) { &.ant-input-affix-wrapper-status-error {
border-color: $color-error !important; border-color: $color-error !important;
} }
&:hover {
border-color: $color-primary !important;
}
} }
&:focus, &:focus,
&-focused { &-focused {
box-shadow: none !important; box-shadow: none !important;

View File

@ -14,18 +14,25 @@
<p class="cts mt-36px !color-#211F24 mb-24px">验证成功请更改密码</p> <p class="cts mt-36px !color-#211F24 mb-24px">验证成功请更改密码</p>
<Form ref="formRef" :model="formData" :rules="formRules" auto-label-width class="w-348 form-wrap"> <Form ref="formRef" :model="formData" :rules="formRules" auto-label-width class="w-348 form-wrap">
<FormItem name="password" class="password-form-item"> <FormItem name="password" class="password-form-item">
<Input.Password v-model:value="formData.password" placeholder="新密码" size="large" class="!h-48px"> <Input.Password
v-model:value="formData.password"
class="!h-48px"
placeholder="新密码"
size="large"
@change="onPasswordChange"
>
<template #iconRender="visible"> <template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" /> <img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template> </template>
</Input.Password> </Input.Password>
</FormItem> </FormItem>
<FormItem name="confirm_password" class="password-form-item"> <FormItem class="password-form-item !mb-36px" name="confirm_password">
<Input.Password <Input.Password
v-model:value="formData.confirm_password" v-model:value="formData.confirm_password"
placeholder="密码确认" placeholder="密码确认"
size="large" size="large"
class="!h-48px" class="!h-48px"
@change="onPasswordChange"
> >
<template #iconRender="visible"> <template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" /> <img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
@ -37,7 +44,7 @@
type="primary" type="primary"
:disabled="disabledSubmitBtn" :disabled="disabledSubmitBtn"
:loading="submitLoading" :loading="submitLoading"
class="w-full !h-48px mt-36px !rounded-8px" class="w-full !h-48px mt-36px !rounded-8px h-22px"
@click="handleSubmit" @click="handleSubmit"
>完成更改 >完成更改
</Button> </Button>
@ -54,7 +61,9 @@
<p class="cts !text-12px !lh-20px !color-#939499" v-if="countdown > 0"> <p class="cts !text-12px !lh-20px !color-#939499" v-if="countdown > 0">
{{ countdown }} 秒后您可以再次发送验证码 {{ countdown }} 秒后您可以再次发送验证码
</p> </p>
<Button type="text" class="pl-0" v-if="hasGetCode && countdown === 0" @click="getCaptcha">重新发送</Button> <Button v-if="hasGetCode && countdown === 0" class="pl-0 h-22px" type="text" @click="getCaptcha"
>重新发送
</Button>
</template> </template>
</div> </div>
@ -79,6 +88,11 @@ import icon2 from '@/assets/img/login/icon-open.png';
const emit = defineEmits(['success', 'cancel']); const emit = defineEmits(['success', 'cancel']);
const INITIAL_FORM_DATA = {
captcha: '',
password: '',
confirm_password: '',
};
const visible = ref(false); const visible = ref(false);
const phone = ref(''); const phone = ref('');
const isCheckPass = ref(false); const isCheckPass = ref(false);
@ -90,11 +104,7 @@ const timer = ref<number | null>(null);
const hasGetCode = ref(false); const hasGetCode = ref(false);
const submitLoading = ref(false); const submitLoading = ref(false);
const verificationCodeRef = ref(null); const verificationCodeRef = ref(null);
const formData = ref({ const formData = ref(cloneDeep(INITIAL_FORM_DATA));
captcha: '',
password: '',
confirm_password: '',
});
const formRules = { const formRules = {
password: [ password: [
@ -113,7 +123,7 @@ const formRules = {
{ {
required: true, required: true,
validator: (_rule, value) => { validator: (_rule, value) => {
if (value !== formData.value.password) { if (value && value !== formData.value.password) {
return Promise.reject('确认密码与设置的密码不同'); return Promise.reject('确认密码与设置的密码不同');
} }
return Promise.resolve(); return Promise.resolve();
@ -143,6 +153,12 @@ const onCheckPass = (captcha) => {
formData.value.captcha = captcha; formData.value.captcha = captcha;
}; };
const onPasswordChange = () => {
nextTick(() => {
formRef.value.clearValidate('confirm_password');
});
};
const getCaptcha = async () => { const getCaptcha = async () => {
try { try {
const { code, message: msg } = await postUpdatePasswordCaptcha(); const { code, message: msg } = await postUpdatePasswordCaptcha();
@ -216,6 +232,8 @@ const onClose = () => {
countdown.value = 0; countdown.value = 0;
hasGetCode.value = false; hasGetCode.value = false;
submitLoading.value = false; submitLoading.value = false;
isCheckPass.value = false;
formData.value = cloneDeep(INITIAL_FORM_DATA);
formRef.value?.resetFields(); formRef.value?.resetFields();
formRef.value?.clearValidate(); formRef.value?.clearValidate();
clearTimer(); clearTimer();

View File

@ -39,14 +39,10 @@
.ant-form { .ant-form {
.ant-form-item { .ant-form-item {
&:not(:last-child) { margin-bottom: 24px !important;
margin-bottom: 24px !important;
}
.ant-input-affix-wrapper { .ant-input-affix-wrapper {
border-radius: 8px !important; border-radius: 8px !important;
} }
} }
} }

View File

@ -7,10 +7,9 @@
ref="inputRef" ref="inputRef"
:key="index" :key="index"
:value="item.value" :value="item.value"
:class="{ error: item.error, fill: item.value }" :class="{ error: isError, fill: item.value }"
@input="handleInput(index, $event.target.value)" @input="handleInput(index, $event.target.value)"
@focus="handleFocus(index)" @focus="handleFocus"
@blur="handleBlur(index)"
:maxlength="1" :maxlength="1"
placeholder="" placeholder=""
/> />
@ -34,11 +33,21 @@ const emits = defineEmits(['success']);
const codeArray = ref([]); const codeArray = ref([]);
const activeIndex = ref(0); // 当前激活的输入框索引 const activeIndex = ref(0); // 当前激活的输入框索引
const errorMessage = ref(''); const errorMessage = ref('');
const isError = ref(false);
const inputRef = ref(null); const inputRef = ref(null);
const handleFocus = (index) => {
isError.value = false;
errorMessage.value = '';
};
// 处理输入事件 // 处理输入事件
const handleInput = (index, value) => { const handleInput = (index, value) => {
if (!value || !/^\d$/.test(value)) return; if (!value) {
return;
}
isError.value = false;
errorMessage.value = '';
codeArray.value[index].value = value; codeArray.value[index].value = value;
@ -48,45 +57,36 @@ const handleInput = (index, value) => {
} }
}; };
const handleFocus = (index) => {
activeIndex.value = index;
};
const handleBlur = (index) => {
const _value = codeArray.value[index].value;
if (!_value || !/^\d$/.test(_value)) {
codeArray.value[index].error = true;
} else {
if (errorMessage.value) {
codeArray.value.forEach((item) => {
item.error = false;
});
}
errorMessage.value = '';
codeArray.value[index].error = false;
}
};
// 验证验证码逻辑 // 验证验证码逻辑
const validateCode = async () => { const validateCode = async () => {
const captcha = codeArray.value.map((item) => item.value).join(''); return new Promise((resolve, reject) => {
const { code } = await postCheckUpdatePasswordCaptcha({ captcha }); isError.value = codeArray.value.some((_value) => !_value);
if (code === 200) { isError.value ? reject() : resolve();
emits('success', captcha); });
} else { };
activeIndex.value = 0;
inputRef.value?.[activeIndex.value]?.focus?.(); const onSubmit = () => {
errorMessage.value = '验证码错误,请检查后重试'; validateCode().then(async () => {
codeArray.value.forEach((item) => { const captcha = codeArray.value.map((item) => item.value).join('');
item.error = true; const { code } = await postCheckUpdatePasswordCaptcha({ captcha });
item.value = ''; if (code === 200) {
}); emits('success', captcha);
} } else {
inputRef.value?.[activeIndex.value]?.blur?.();
activeIndex.value = 0;
errorMessage.value = '验证码错误,请检查后重试';
isError.value = true;
codeArray.value.forEach((item) => {
item.value = '';
});
}
});
}; };
const init = () => { const init = () => {
activeIndex.value = 0; activeIndex.value = 0;
errorMessage.value = ''; errorMessage.value = '';
isError.value = false;
inputRef.value = null; inputRef.value = null;
codeArray.value = new Array(6).fill().map(() => ({ value: '', error: false })); codeArray.value = new Array(6).fill().map(() => ({ value: '', error: false }));
@ -100,7 +100,7 @@ watch(
() => codeArray.value, () => codeArray.value,
(newVal) => { (newVal) => {
if (newVal.every((item) => item.value)) { if (newVal.every((item) => item.value)) {
validateCode(); onSubmit();
} }
}, },
{ deep: true }, { deep: true },
@ -129,17 +129,20 @@ defineExpose({ init });
caret-color: #6d4cfe; caret-color: #6d4cfe;
&.error {
border-color: #f64b31 !important;
}
&:hover { &:hover {
border-color: #6d4cfe !important; border-color: #6d4cfe !important;
} }
&:focus-within {
border-color: #6d4cfe !important;
}
&.fill { &.fill {
color: #6d4cfe; color: #6d4cfe;
} }
&.error {
border-color: #f64b31 !important;
}
} }
} }
} }

View File

@ -2,11 +2,13 @@
<template> <template>
<div class="flex items-center w-400 h-100%"> <div class="flex items-center w-400 h-100%">
<div class="w-full bg-#fff rounded-16px px-40px py-32px flex flex-col items-center"> <div class="w-full bg-#fff rounded-16px px-40px py-32px flex flex-col items-center">
<div class="flex items-center mb-24px w-full cursor-pointer" @click="onBack"> <div class="flex items-center mb-24px w-full">
<icon-left size="24" class="mr-4px color-#000" /> <div class="flex items-center cursor-pointer" @click="onBack">
<span class="color-#000 text-20px font-500 lh-28px font-family-medium">{{ <icon-left class="mr-4px color-#000" size="24" />
isResetPassword ? '重置密码' : '手机注册' <span class="color-#000 text-20px font-500 lh-28px font-family-medium">{{
}}</span> isResetPassword ? '重置密码' : '手机注册'
}}</span>
</div>
</div> </div>
<Form ref="formRef" :model="formData" :rules="formRules" auto-label-width class="w-320 form-wrap"> <Form ref="formRef" :model="formData" :rules="formRules" auto-label-width class="w-320 form-wrap">
<FormItem name="mobile"> <FormItem name="mobile">
@ -19,14 +21,14 @@
/> />
</FormItem> </FormItem>
<FormItem name="password" class="password-form-item"> <FormItem name="password" class="password-form-item">
<Input.Password v-model:value="formData.password" placeholder="新密码" @change="clearErrorMsg"> <Input.Password v-model:value="formData.password" placeholder="新密码" @change="onPasswordChange">
<template #iconRender="visible"> <template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" /> <img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template> </template>
</Input.Password> </Input.Password>
</FormItem> </FormItem>
<FormItem name="confirm_password" class="password-form-item"> <FormItem name="confirm_password" class="password-form-item">
<Input.Password v-model:value="formData.confirm_password" placeholder="密码确认" @change="clearErrorMsg"> <Input.Password v-model:value="formData.confirm_password" placeholder="密码确认" @change="onPasswordChange">
<template #iconRender="visible"> <template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" /> <img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template> </template>
@ -48,12 +50,12 @@
</div> </div>
</template> </template>
</Input> </Input>
<p class="color-#F64B31 text-12px font-400 lh-20px font-family-regular" v-show="errMsg"> <p class="color-#F64B31 text-12px font-400 lh-20px h-20px font-family-regular">
{{ errMsg }} {{ errMsg }}
</p> </p>
</FormItem> </FormItem>
<FormItem class="mt-52px"> <FormItem class="mt-32px">
<div class="text-12px flex justify-center items-center mb-16px"> <div class="text-12px flex justify-center items-center mb-16px">
<Checkbox v-model:checked="hasCheck" class="mr-8px"></Checkbox> <Checkbox v-model:checked="hasCheck" class="mr-8px"></Checkbox>
<span class="text-12px color-#737478 font-400 lh-20px font-family-regular" <span class="text-12px color-#737478 font-400 lh-20px font-family-regular"
@ -186,7 +188,7 @@ const formRules = {
// if (value.length < 6) { // if (value.length < 6) {
// return Promise.reject('密码长度不能小于6位'); // return Promise.reject('密码长度不能小于6位');
// } // }
if (value !== formData.value.password) { if (value && value !== formData.value.password) {
return Promise.reject('确认密码与设置的密码不同'); return Promise.reject('确认密码与设置的密码不同');
} }
return Promise.resolve(); return Promise.resolve();
@ -229,13 +231,20 @@ const canGetCaptcha = computed(() => {
}); });
const disabledSubmitBtn = computed(() => { const disabledSubmitBtn = computed(() => {
return !isPassPassword.value || !hasCheck.value || !isLegalMobile.value || !formData.value.password.trim(); return !isPassPassword.value || !hasCheck.value || !isLegalMobile.value || !formData.value.password.trim() || !formData.value.captcha;
}); });
const clearErrorMsg = () => { const clearErrorMsg = () => {
errMsg.value = ''; errMsg.value = '';
}; };
const onPasswordChange = () => {
clearErrorMsg();
nextTick(() => {
formRef.value.clearValidate('confirm_password');
});
};
const validateField = (field) => { const validateField = (field) => {
formRef.value.validateFields(field); formRef.value.validateFields(field);
}; };

View File

@ -6,11 +6,11 @@
} }
.ant-input-affix-wrapper { .ant-input-affix-wrapper {
height: 48px; height: 48px;
padding: 0 10px; padding: 10px 16px;
border-radius: 8px !important; border-radius: 8px !important;
.ant-input { .ant-input {
border-radius: 8px !important; border-radius: 8px !important;
font-size: 16px; font-size: 16px !important;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 24px; line-height: 24px;
@ -44,16 +44,17 @@
} }
} }
} }
:deep(.btn-login) {
&:disabled { //:deep(.btn-login) {
background-color: #c5b7ff !important; // //&:disabled {
} // // background-color: #c5b7ff !important;
&:not(:disabled) { // //}
&:hover { // //&:not(:disabled) {
background-color: $color-primary-3 !important; // // &:hover {
} // // background-color: $color-primary-3 !important;
} // // }
} // //}
//}
.login-bg { .login-bg {
position: fixed; position: fixed;
left: 0; left: 0;