395 lines
9.5 KiB
Vue
395 lines
9.5 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, reactive } from 'vue';
|
|||
|
|
import { useRouter } from 'vue-router';
|
|||
|
|
import { message } from 'ant-design-vue';
|
|||
|
|
import type { FormInstance } from 'ant-design-vue';
|
|||
|
|
import { useUserStore } from '@/store/modules/user';
|
|||
|
|
import { encrypt } from '@/utils/rsaEncrypt';
|
|||
|
|
import { updatePassword } from '@/api/changePassWord';
|
|||
|
|
|
|||
|
|
const router = useRouter();
|
|||
|
|
const userStore = useUserStore();
|
|||
|
|
|
|||
|
|
const props = defineProps<{ open: boolean }>();
|
|||
|
|
const emit = defineEmits<{ 'update:open': [value: boolean] }>();
|
|||
|
|
|
|||
|
|
const closeModal = () => emit('update:open', false);
|
|||
|
|
|
|||
|
|
// 表单引用
|
|||
|
|
const formRef = ref<FormInstance>();
|
|||
|
|
|
|||
|
|
// 提交状态
|
|||
|
|
const submitting = ref(false);
|
|||
|
|
|
|||
|
|
// 表单数据
|
|||
|
|
interface ChangePasswordForm {
|
|||
|
|
oldPassword: string;
|
|||
|
|
newPassword: string;
|
|||
|
|
confirmPassword: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const formState = reactive<ChangePasswordForm>({
|
|||
|
|
oldPassword: '',
|
|||
|
|
newPassword: '',
|
|||
|
|
confirmPassword: ''
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ==================== 密码校验函数 ====================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 校验密码复杂度:至少包含四类字符中的三类
|
|||
|
|
*/
|
|||
|
|
const checkComplexity = (
|
|||
|
|
password: string
|
|||
|
|
): { valid: boolean; message: string } => {
|
|||
|
|
let count = 0;
|
|||
|
|
const hasUpper = /[A-Z]/.test(password);
|
|||
|
|
const hasLower = /[a-z]/.test(password);
|
|||
|
|
const hasNumber = /[0-9]/.test(password);
|
|||
|
|
const hasSpecial = /[^A-Za-z0-9]/.test(password);
|
|||
|
|
|
|||
|
|
if (hasUpper) count++;
|
|||
|
|
if (hasLower) count++;
|
|||
|
|
if (hasNumber) count++;
|
|||
|
|
if (hasSpecial) count++;
|
|||
|
|
|
|||
|
|
if (count >= 3) {
|
|||
|
|
return { valid: true, message: '' };
|
|||
|
|
}
|
|||
|
|
return {
|
|||
|
|
valid: false,
|
|||
|
|
message: '密码必须包含大写字母、小写字母、数字、特殊字符中的至少三类'
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 校验连续重复字符
|
|||
|
|
*/
|
|||
|
|
const checkConsecutiveRepeat = (
|
|||
|
|
password: string
|
|||
|
|
): { valid: boolean; message: string } => {
|
|||
|
|
for (let i = 0; i < password.length - 1; i++) {
|
|||
|
|
if (password[i] === password[i + 1]) {
|
|||
|
|
return {
|
|||
|
|
valid: false,
|
|||
|
|
message: '密码不能包含2位及以上相同字符的连续重复(如11、aa)'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return { valid: true, message: '' };
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 校验连续递增/递减数字
|
|||
|
|
*/
|
|||
|
|
const checkConsecutiveNumbers = (
|
|||
|
|
password: string
|
|||
|
|
): { valid: boolean; message: string } => {
|
|||
|
|
for (let i = 0; i < password.length - 1; i++) {
|
|||
|
|
const curr = password.charCodeAt(i);
|
|||
|
|
const next = password.charCodeAt(i + 1);
|
|||
|
|
|
|||
|
|
// 检查是否都是数字
|
|||
|
|
if (curr >= 48 && curr <= 57 && next >= 48 && next <= 57) {
|
|||
|
|
if (Math.abs(next - curr) === 1) {
|
|||
|
|
return {
|
|||
|
|
valid: false,
|
|||
|
|
message: '密码不能包含2位及以上连续递增或递减的数字(如12、21)'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return { valid: true, message: '' };
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 校验连续递增/递减字母
|
|||
|
|
*/
|
|||
|
|
const checkConsecutiveLetters = (
|
|||
|
|
password: string
|
|||
|
|
): { valid: boolean; message: string } => {
|
|||
|
|
for (let i = 0; i < password.length - 1; i++) {
|
|||
|
|
const curr = password.charCodeAt(i);
|
|||
|
|
const next = password.charCodeAt(i + 1);
|
|||
|
|
|
|||
|
|
// 检查是否都是字母(大写或小写)
|
|||
|
|
const isCurrLetter =
|
|||
|
|
(curr >= 65 && curr <= 90) || (curr >= 97 && curr <= 122);
|
|||
|
|
const isNextLetter =
|
|||
|
|
(next >= 65 && next <= 90) || (next >= 97 && next <= 122);
|
|||
|
|
|
|||
|
|
if (isCurrLetter && isNextLetter) {
|
|||
|
|
if (Math.abs(next - curr) === 1) {
|
|||
|
|
return {
|
|||
|
|
valid: false,
|
|||
|
|
message: '密码不能包含2位及以上连续递增或递减的字母(如ab、ba)'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return { valid: true, message: '' };
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 校验用户名关联
|
|||
|
|
*/
|
|||
|
|
const checkUsernameRelation = (
|
|||
|
|
password: string,
|
|||
|
|
username: string
|
|||
|
|
): { valid: boolean; message: string } => {
|
|||
|
|
if (!username) {
|
|||
|
|
return { valid: true, message: '' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const lowerPassword = password.toLowerCase();
|
|||
|
|
const lowerUsername = username.toLowerCase();
|
|||
|
|
|
|||
|
|
if (lowerPassword.includes(lowerUsername)) {
|
|||
|
|
return {
|
|||
|
|
valid: false,
|
|||
|
|
message: '密码不能包含用户名或其核心部分'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { valid: true, message: '' };
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ==================== 表单验证规则 ====================
|
|||
|
|
|
|||
|
|
const rules = {
|
|||
|
|
oldPassword: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
|
|||
|
|
newPassword: [
|
|||
|
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
|||
|
|
{ min: 10, message: '密码长度不能少于10位', trigger: 'blur' },
|
|||
|
|
{
|
|||
|
|
validator: async (_rule: any, value: string) => {
|
|||
|
|
if (!value) {
|
|||
|
|
return Promise.resolve();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 校验复杂度
|
|||
|
|
const complexity = checkComplexity(value);
|
|||
|
|
if (!complexity.valid) {
|
|||
|
|
return Promise.reject(new Error(complexity.message));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 校验连续重复
|
|||
|
|
const repeat = checkConsecutiveRepeat(value);
|
|||
|
|
if (!repeat.valid) {
|
|||
|
|
return Promise.reject(new Error(repeat.message));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 校验连续数字
|
|||
|
|
const numbers = checkConsecutiveNumbers(value);
|
|||
|
|
if (!numbers.valid) {
|
|||
|
|
return Promise.reject(new Error(numbers.message));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 校验连续字母
|
|||
|
|
const letters = checkConsecutiveLetters(value);
|
|||
|
|
if (!letters.valid) {
|
|||
|
|
return Promise.reject(new Error(letters.message));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 校验用户名关联
|
|||
|
|
const username = userStore.username || '';
|
|||
|
|
const usernameCheck = checkUsernameRelation(value, username);
|
|||
|
|
if (!usernameCheck.valid) {
|
|||
|
|
return Promise.reject(new Error(usernameCheck.message));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Promise.resolve();
|
|||
|
|
},
|
|||
|
|
trigger: 'blur'
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
confirmPassword: [
|
|||
|
|
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
|
|||
|
|
{
|
|||
|
|
validator: async (_rule: any, value: string) => {
|
|||
|
|
if (!value) {
|
|||
|
|
return Promise.resolve();
|
|||
|
|
}
|
|||
|
|
if (value !== formState.newPassword) {
|
|||
|
|
return Promise.reject(new Error('两次输入的密码不一致'));
|
|||
|
|
}
|
|||
|
|
return Promise.resolve();
|
|||
|
|
},
|
|||
|
|
trigger: 'blur'
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 提交表单
|
|||
|
|
*/
|
|||
|
|
const handleSubmit = async () => {
|
|||
|
|
try {
|
|||
|
|
// 表单验证
|
|||
|
|
await formRef.value?.validate();
|
|||
|
|
|
|||
|
|
submitting.value = true;
|
|||
|
|
|
|||
|
|
// RSA 加密密码
|
|||
|
|
const encryptedOldPassword = encrypt(formState.oldPassword);
|
|||
|
|
const encryptedNewPassword = encrypt(formState.newPassword);
|
|||
|
|
|
|||
|
|
if (!encryptedOldPassword || !encryptedNewPassword) {
|
|||
|
|
message.error('密码加密失败');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用接口
|
|||
|
|
const res: any = await updatePassword({
|
|||
|
|
id: userStore.userid,
|
|||
|
|
password: encryptedNewPassword,
|
|||
|
|
oldPassword: encryptedOldPassword
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 兼容字符串和数字类型的状态码
|
|||
|
|
if (res.code === 0 || res.code === '0') {
|
|||
|
|
message.success('密码修改成功,请重新登录');
|
|||
|
|
|
|||
|
|
// 清空 token 和用户信息
|
|||
|
|
userStore.resetToken();
|
|||
|
|
closeModal();
|
|||
|
|
|
|||
|
|
// 跳转到登录页
|
|||
|
|
setTimeout(() => {
|
|||
|
|
router.push('/login');
|
|||
|
|
}, 1500);
|
|||
|
|
} else {
|
|||
|
|
message.error(res.msg || '密码修改失败');
|
|||
|
|
}
|
|||
|
|
} catch (error: any) {
|
|||
|
|
if (error.errorFields) {
|
|||
|
|
// 表单验证错误
|
|||
|
|
message.error('请检查表单填写是否正确');
|
|||
|
|
} else {
|
|||
|
|
// 其他错误
|
|||
|
|
message.error(error.message || '密码修改失败,请稍后重试');
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
submitting.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重置表单
|
|||
|
|
*/
|
|||
|
|
const handleReset = () => {
|
|||
|
|
formRef.value?.resetFields();
|
|||
|
|
formState.oldPassword = '';
|
|||
|
|
formState.newPassword = '';
|
|||
|
|
formState.confirmPassword = '';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 关闭弹窗并重置表单
|
|||
|
|
*/
|
|||
|
|
const handleCancel = () => {
|
|||
|
|
formRef.value?.resetFields();
|
|||
|
|
formState.oldPassword = '';
|
|||
|
|
formState.newPassword = '';
|
|||
|
|
formState.confirmPassword = '';
|
|||
|
|
closeModal();
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<a-modal
|
|||
|
|
:open="props.open"
|
|||
|
|
title="修改密码"
|
|||
|
|
:footer="null"
|
|||
|
|
:mask-closable="false"
|
|||
|
|
width="480px"
|
|||
|
|
@cancel="handleCancel"
|
|||
|
|
>
|
|||
|
|
<a-form
|
|||
|
|
ref="formRef"
|
|||
|
|
:model="formState"
|
|||
|
|
:rules="rules"
|
|||
|
|
layout="vertical"
|
|||
|
|
class="password-form"
|
|||
|
|
>
|
|||
|
|
<a-form-item label="旧密码" name="oldPassword">
|
|||
|
|
<a-input-password
|
|||
|
|
v-model:value="formState.oldPassword"
|
|||
|
|
placeholder="请输入旧密码"
|
|||
|
|
allow-clear
|
|||
|
|
/>
|
|||
|
|
</a-form-item>
|
|||
|
|
|
|||
|
|
<a-form-item label="新密码" name="newPassword">
|
|||
|
|
<a-input-password
|
|||
|
|
v-model:value="formState.newPassword"
|
|||
|
|
placeholder="请输入新密码"
|
|||
|
|
allow-clear
|
|||
|
|
/>
|
|||
|
|
</a-form-item>
|
|||
|
|
|
|||
|
|
<a-form-item label="确认新密码" name="confirmPassword">
|
|||
|
|
<a-input-password
|
|||
|
|
v-model:value="formState.confirmPassword"
|
|||
|
|
placeholder="请再次输入新密码"
|
|||
|
|
allow-clear
|
|||
|
|
/>
|
|||
|
|
</a-form-item>
|
|||
|
|
|
|||
|
|
<a-form-item class="button-group">
|
|||
|
|
<a-space>
|
|||
|
|
<a-button
|
|||
|
|
type="primary"
|
|||
|
|
@click="handleSubmit"
|
|||
|
|
:loading="submitting"
|
|||
|
|
class="submit-btn"
|
|||
|
|
>
|
|||
|
|
提交
|
|||
|
|
</a-button>
|
|||
|
|
<a-button @click="handleReset">重置</a-button>
|
|||
|
|
</a-space>
|
|||
|
|
</a-form-item>
|
|||
|
|
</a-form>
|
|||
|
|
</a-modal>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
.password-form {
|
|||
|
|
:deep(.ant-form-item-label) {
|
|||
|
|
label {
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ant-input-password) {
|
|||
|
|
border-radius: 4px;
|
|||
|
|
|
|||
|
|
&:hover {
|
|||
|
|
border-color: #005293;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&:focus {
|
|||
|
|
border-color: #005293;
|
|||
|
|
box-shadow: 0 0 0 2px rgba(0, 82, 147, 0.2);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.button-group {
|
|||
|
|
margin-top: 16px;
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
|
|||
|
|
.submit-btn {
|
|||
|
|
width: 100px;
|
|||
|
|
background-color: #005293;
|
|||
|
|
border-color: #005293;
|
|||
|
|
|
|||
|
|
&:hover {
|
|||
|
|
background-color: #003d6e;
|
|||
|
|
border-color: #003d6e;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|