feat: 修改密码

This commit is contained in:
rd
2025-09-12 12:00:26 +08:00
parent 465cb2fcad
commit 98d600693a
8 changed files with 489 additions and 33 deletions

View File

@ -122,7 +122,7 @@ export const sendUpdateMobileCaptcha = (data: any) => {
// 修改绑定的手机号
export const updateMobile = (data: any) => {
return Http.post(`/v1/me/mobile`, data);
return Http.patch(`/v1/me/mobile`, data);
};
// 修改我的信息

View File

@ -92,3 +92,19 @@ export const getMyPrimaryEnterprise = () => {
export const postUpdateMobileCaptcha = (params = {}) => {
return Http.post('/v1/sms/update-mobile-captcha', params);
};
// 发送修改密码验证码
export const postUpdatePasswordCaptcha = (params = {}) => {
return Http.post('/v1/sms/update-password-captcha', params);
};
// 验证修改密码验证码
export const postCheckUpdatePasswordCaptcha = (params = {}) => {
return Http.post('/v1/sms/check-update-password-captcha', params);
};
// 修改密码
export const postUpdatePassword = (params = {}) => {
return Http.patch('/v1/me/password', params);
};

View File

@ -39,13 +39,13 @@
</Form>
<div class="flex justify-end mt-20px">
<Button class="mr-16px" size="large" @click="onClose">取消</Button>
<Button type="primary" size="large" @click="handleConfirm">确定</Button>
<Button type="primary" size="large" :loading="submitLoading" @click="handleConfirm">确定</Button>
</div>
</Modal>
</template>
<script setup>
import { computed, onUnmounted, ref } from 'vue';
import { computed, ref } from 'vue';
import { Button, Form, FormItem, Input, message, Modal } from 'ant-design-vue';
import { postUpdateMobileCaptcha } from '@/api/all/login';
import { updateMobile } from '@/api/all';
@ -58,6 +58,7 @@ const visible = ref(false);
const timer = ref();
const isLegalMobile = ref(false);
const hasGetCode = ref(false);
const submitLoading = ref(false);
const countdown = ref(0);
@ -141,8 +142,7 @@ const startCountdown = () => {
timer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer.value);
timer.value = null;
clearTimer();
}
}, 1000);
};
@ -150,15 +150,27 @@ const startCountdown = () => {
// 确定按钮点击
const handleConfirm = () => {
formRef.value.validate().then(async () => {
const { code } = await updateMobile(form.value);
if (code === 200) {
message.success('修改成功!');
store.getUserInfo();
visible.value = false;
try {
submitLoading.value = true;
const { code } = await updateMobile(form.value);
if (code === 200) {
await store.getUserInfo();
message.success('修改成功!');
onClose();
}
} finally {
submitLoading.value = false;
}
});
};
const clearTimer = () => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
};
// 取消按钮点击
const onClose = () => {
form.value = {
@ -167,16 +179,15 @@ const onClose = () => {
};
countdown.value = 0;
visible.value = false;
hasGetCode.value = false;
isLegalMobile.value = false;
submitLoading.value = false;
formRef.value.resetFields();
formRef.value.clearValidate();
};
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value);
}
});
clearTimer();
};
// 暴露方法供外部调用
defineExpose({

View File

@ -15,7 +15,7 @@
</Form>
<div class="flex justify-end mt-20px">
<Button class="mr-16px" size="large" @click="onClose">取消</Button>
<Button type="primary" size="large" :loading="loading" @click="handleNicknameConfirm">确定</Button>
<Button type="primary" size="large" :loading="submitLoading" @click="handleNicknameConfirm">确定</Button>
</div>
</Modal>
</template>
@ -30,7 +30,7 @@ import { useUserStore } from '@/stores';
const store = useUserStore();
const formRef = ref();
const visible = ref(false);
const loading = ref(false);
const submitLoading = ref(false);
const form = ref({
name: '',
});
@ -43,11 +43,16 @@ const open = (userName) => {
// 确定按钮点击
const handleNicknameConfirm = async () => {
const { code } = await updateMyInfo(form.value);
if (code === 200) {
message.success('修改成功!');
store.getUserInfo();
visible.value = false;
try {
submitLoading.value = true;
const { code } = await updateMyInfo(form.value);
if (code === 200) {
await store.getUserInfo();
message.success('修改成功!');
onClose();
}
} finally {
submitLoading.value = false;
}
};
@ -55,6 +60,7 @@ const handleNicknameConfirm = async () => {
const onClose = () => {
form.value.name = '';
visible.value = false;
submitLoading.valeu = false;
};
// 暴露方法供外部调用

View File

@ -0,0 +1,212 @@
<template>
<Modal
v-model:open="visible"
:title="title"
:width="412"
centered
:footer="null"
class="change-password-modal"
@cancel="onClose"
>
<div class="modal-content">
<p class="title-text">安全验证</p>
<template v-if="isCheckPass">
<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">
<FormItem name="password" class="password-form-item">
<Input.Password v-model:value="formData.password" placeholder="新密码" size="large" class="!h-48px">
<template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template>
</Input.Password>
</FormItem>
<FormItem name="confirm_password" class="password-form-item">
<Input.Password
v-model:value="formData.confirm_password"
placeholder="密码确认"
size="large"
class="!h-48px"
>
<template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template>
</Input.Password>
</FormItem>
</Form>
<Button
type="primary"
:disabled="disabledSubmitBtn"
:loading="submitLoading"
class="w-full !h-48px mt-36px !rounded-8px"
@click="handleSubmit"
>完成更改
</Button>
</template>
<template v-else>
<p class="cts text-center mt-16px">为进一步保证您的账号安全确保为您本人操作</p>
<p class="cts mb-36px text-center">请先完成安全验证</p>
<p class="cts !color-#211F24 mb-24px">请输入发送至{{ showMobile }} 的短信验证码</p>
<div class="verify-code-wrap mb-24px">
<VerificationCode ref="verificationCodeRef" @success="onCheckPass" />
</div>
<p class="cts !text-12px !lh-20px !color-#939499" v-if="countdown > 0">
{{ countdown }} 秒后您可以再次发送验证码
</p>
<Button type="text" class="pl-0" v-if="hasGetCode && countdown === 0" @click="getCaptcha">重新发送</Button>
</template>
</div>
</Modal>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Button, Form, FormItem, Input, message, Modal } from 'ant-design-vue';
import VerificationCode from '../verification-code/index.vue';
import { postUpdatePasswordCaptcha, postUpdatePassword } from '@/api/all/login';
import icon1 from '@/assets/img/login/icon-close.png';
import icon2 from '@/assets/img/login/icon-open.png';
const emit = defineEmits(['success', 'cancel']);
const visible = ref(false);
const phone = ref('');
const isCheckPass = ref(false);
const formRef = ref(null);
// 验证码输入相关
const countdown = ref(0);
const timer = ref<number | null>(null);
const hasGetCode = ref(false);
const submitLoading = ref(false);
const verificationCodeRef = ref(null);
const formData = ref({
captcha: '',
password: '',
confirm_password: '',
});
const formRules = {
password: [
{
required: true,
validator: (_rule, value) => {
if (formData.value.confirm_password) {
formRef.value.validateFields('confirm_password');
}
return Promise.resolve();
},
trigger: ['blur'],
},
],
confirm_password: [
{
required: true,
validator: (_rule, value) => {
if (value !== formData.value.password) {
return Promise.reject('确认密码与设置的密码不同');
}
return Promise.resolve();
},
trigger: ['blur'],
},
],
};
const title = computed(() => (isCheckPass.value ? '更改密码' : '安全验证'));
const showMobile = computed(() => {
return phone.value?.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
});
const isPassPassword = computed(() => {
return (
formData.value.confirm_password &&
formData.value.password &&
formData.value.confirm_password === formData.value.password
);
});
const disabledSubmitBtn = computed(() => {
return !isPassPassword.value || !formData.value.password.trim();
});
const onCheckPass = (captcha) => {
isCheckPass.value = true;
formData.value.captcha = captcha;
};
const getCaptcha = async () => {
try {
const { code, message: msg } = await postUpdatePasswordCaptcha();
if (code === 200) {
startCountdown();
message.success(msg);
}
} catch (error) {
// 重置倒计时
countdown.value = 0;
clearInterval(timer.value);
}
};
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
hasGetCode.value = true;
timer.value = window.setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearTimer();
}
}, 1000);
};
const clearTimer = () => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
};
// 提交验证
const handleSubmit = async () => {
try {
submitLoading.value = true;
const { code } = await postUpdatePassword(formData.value);
if (code === 200) {
message.success('更改成功');
onClose();
}
} finally {
submitLoading.value = false;
}
};
const open = (mobile) => {
getCaptcha();
phone.value = mobile;
visible.value = true;
nextTick(() => {
verificationCodeRef.value?.init();
});
};
const onClose = () => {
visible.value = false;
phone.value = '';
countdown.value = 0;
hasGetCode.value = false;
submitLoading.value = false;
formRef.value?.resetFields();
formRef.value?.clearValidate();
clearTimer();
};
defineExpose({
open,
});
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,60 @@
.change-password-modal {
.ant-modal-content {
border-radius: 16px;
.ant-modal-header {
border-radius: 16px 16px 0 0;
border-bottom: none !important;
.ant-modal-title {
display: none;
}
}
.ant-modal-body {
padding: 0 32px 8px !important;
position: relative;
top: -24px;
display: flex;
flex-direction: column;
justify-content: center;
.cts {
color: var(--Text-3, #737478);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
.title-text {
color: var(--Text-1, #211F24);
text-align: center;
font-family: $font-family-medium;
font-size: 24px;
font-style: normal;
font-weight: 500;
line-height: 32px;
}
.ant-form {
.ant-form-item {
&:not(:last-child) {
margin-bottom: 24px !important;
}
.ant-input-affix-wrapper {
border-radius: 8px !important;
}
}
}
}
.ant-modal-footer {
height: 56px !important;
border-top: none !important;
}
}
}

View File

@ -0,0 +1,154 @@
<template>
<div class="verify-code-container">
<!-- 验证码输入区域 -->
<div class="code-inputs">
<Input
v-for="(item, index) in codeArray"
ref="inputRef"
:key="index"
:value="item.value"
:class="{ error: item.error, fill: item.value }"
@input="handleInput(index, $event.target.value)"
@focus="handleFocus(index)"
@blur="handleBlur(index)"
:maxlength="1"
placeholder=""
/>
</div>
<!-- 错误提示 -->
<p v-if="errorMessage" class="error-message">
{{ errorMessage }}
</p>
</div>
</template>
<script setup>
import { Input } from 'ant-design-vue';
import { ref, watch } from 'vue';
import { postCheckUpdatePasswordCaptcha } from '@/api/all/login';
const emits = defineEmits(['success']);
const codeArray = ref([]);
const activeIndex = ref(0); // 当前激活的输入框索引
const errorMessage = ref('');
const inputRef = ref(null);
// 处理输入事件
const handleInput = (index, value) => {
if (!value || !/^\d$/.test(value)) return;
codeArray.value[index].value = value;
if (index < 5) {
activeIndex.value = index + 1;
inputRef.value?.[activeIndex.value]?.focus?.();
}
};
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 captcha = codeArray.value.map((item) => item.value).join('');
const { code } = await postCheckUpdatePasswordCaptcha({ captcha });
if (code === 200) {
emits('success', captcha);
} else {
activeIndex.value = 0;
inputRef.value?.[activeIndex.value]?.focus?.();
errorMessage.value = '验证码错误,请检查后重试';
codeArray.value.forEach((item) => {
item.error = true;
item.value = '';
});
}
};
const init = () => {
activeIndex.value = 0;
errorMessage.value = '';
inputRef.value = null;
codeArray.value = new Array(6).fill().map(() => ({ value: '', error: false }));
nextTick(() => {
inputRef.value?.[activeIndex.value]?.focus?.();
});
};
// 监听输入变化
watch(
() => codeArray.value,
(newVal) => {
if (newVal.every((item) => item.value)) {
validateCode();
}
},
{ deep: true },
);
defineExpose({ init });
</script>
<style scoped lang="scss">
.verify-code-container {
.code-inputs {
display: flex;
gap: 12px;
.ant-input {
width: 48px;
height: 48px;
border-radius: 8px !important;
text-align: center;
font-family: $font-family-medium;
font-size: 24px !important;
font-style: normal;
font-weight: 500;
line-height: 32px;
border: 1px solid #d7d7d9;
outline: none;
&:hover {
border-color: #6d4cfe !important;
}
&.fill {
color: #6d4cfe;
}
&.error {
border-color: #f64b31 !important;
}
}
}
}
.error-message {
color: var(--Functional-Red-6, #f64b31);
font-family: $font-family-regular;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
margin-top: 4px;
}
</style>

View File

@ -25,7 +25,7 @@
v-for="(item, index) in new Array(10).fill(0)"
:key="index"
></span>
<Button type="text" size="small" class="!p-0 !h-22px ml-10px">更改</Button>
<Button type="text" size="small" class="!p-0 !h-22px ml-10px" @click="openChangePasswordModal">更改</Button>
</div>
</div>
</div>
@ -110,11 +110,7 @@
/>
</Modal>
<SafetyVerificationModal
ref="safetyVerificationModalRef"
@success="handleVerifySuccess"
@cancel="handleVerifyCancel"
/>
<ChangePasswordModal ref="changePasswordModalRef" />
<ChangeNameModal ref="changeNameModalRef" />
<ChangeMobileModal ref="changeMobileModalRef" />
</div>
@ -123,7 +119,7 @@
import { Avatar, Button, Form, FormItem, Input, message } from 'ant-design-vue';
import Modal from '@/components/modal.vue';
import PuzzleVerification from '@/views/login/components/PuzzleVerification.vue';
import SafetyVerificationModal from './components/safety-verification-modal/index.vue';
import ChangePasswordModal from './components/change-password-modal/index.vue';
import ChangeNameModal from './components/change-name-modal/index.vue';
import ChangeMobileModal from './components/change-mobile-modal/index.vue';
@ -137,7 +133,7 @@ import { useUserStore } from '@/stores';
import icon1 from './img/icon1.png';
const store = useUserStore();
const safetyVerificationModalRef = ref(null);
const changePasswordModalRef = ref(null);
const changeNameModalRef = ref(null);
const changeMobileModalRef = ref(null);
@ -175,8 +171,6 @@ const mobile = computed(() => {
return _mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
});
console.log(store.userInfo);
// 表单校验规则
const formRules = {
mobile: [
@ -256,6 +250,9 @@ const openChangeNameModal = () => {
const openChangeMobileModal = () => {
changeMobileModalRef.value.open();
};
const openChangePasswordModal = () => {
changePasswordModalRef.value.open(dataSource.value.mobile);
};
async function handleSubmitUserInfo() {
await updateMyInfo(userInfoForm);