feat: 邮箱密码修改

This commit is contained in:
2026-05-28 11:27:39 +08:00
parent a4861da21a
commit e4f1c535a7
7 changed files with 163 additions and 59 deletions

View File

@@ -63,15 +63,6 @@ export const fetchDownloadItemsByGet = (params: Download): Promise<ApiResponse>
})
}
export interface UserProfile {
firstName: string
lastName: string
username: string
roles: string[]
region: string
language: string
email: string
}
// 获取用户信息
export const fetchUserProfile = (): Promise<ApiResponse> => {
return request({
@@ -81,6 +72,18 @@ export const fetchUserProfile = (): Promise<ApiResponse> => {
}
// 设置用户信息
export interface UserProfile {
firstName: string
lastName: string
username: string
roles: string[]
region: string
language: string
email: string
oldPassword?: string
newPassword?: string
verifyCode?: string
}
export const updateUserProfile = (data: UserProfile): Promise<ApiResponse> => {
return request({
url: '/buyer/profile/setProfile',
@@ -88,3 +91,20 @@ export const updateUserProfile = (data: UserProfile): Promise<ApiResponse> => {
data
})
}
// 获取设置页验证码
export const fetchVerifyCode = (): Promise<ApiResponse> => {
return request({
url: '/buyer/profile/sendEmailChangeCode',
method: 'post'
})
}
// 验证设置页验证码
export const verifyEmailCode = (verifyCode: string): Promise<ApiResponse> => {
return request({
url: '/buyer/profile/verifyEmailChangeCode',
method: 'post',
data: { verifyCode }
})
}

View File

@@ -107,6 +107,7 @@ export default {
discard: 'DISCARD',
edit: 'EDIT',
saveChange: 'SAVE CHANGE',
verifyEmail: 'VERIFY EMAIL',
saving: 'SAVING...'
},
dialog: {
@@ -117,7 +118,7 @@ export default {
resendCodeIn: 'Resend Code in {time}'
},
messages: {
enterNewEmailFirst: 'Please enter your new email address first',
enterNewEmailFirst: 'Please enter your email address first',
invalidEmail: 'Please enter a valid email address',
sameEmail: 'Please enter a different email address',
alreadyVerified: 'This email has already been verified',
@@ -125,6 +126,11 @@ export default {
enterVerificationCode: 'Please enter the 6-digit verification code',
verificationCompleted: 'Email verification completed',
verifyEmailBeforeSave: 'Please verify your new email before saving',
currentPasswordRequired: 'Please enter your current password',
passwordLengthError: 'Password length must be between {min} and {max} characters',
passwordSpecial: 'Password must contain special characters',
passwordCase: 'Password must include upper/lowercase letters and numbers',
passwordNotSameAsOld: 'New password cannot be the same as current password',
settingsUpdated: 'Settings updated'
},
roles: {

View File

@@ -104,6 +104,7 @@ export default {
discard: '放弃',
edit: '编辑',
saveChange: '保存更改',
verifyEmail: '验证邮箱',
saving: '保存中...'
},
dialog: {
@@ -122,6 +123,11 @@ export default {
enterVerificationCode: '请输入 6 位验证码',
verificationCompleted: '邮箱验证完成',
verifyEmailBeforeSave: '请先完成新邮箱验证再保存',
currentPasswordRequired: '请输入当前密码',
passwordLengthError: '密码长度必须在 {min} 到 {max} 个字符之间',
passwordSpecial: '密码必须包含特殊符号',
passwordCase: '密码必须包含大小写字母和数字',
passwordNotSameAsOld: '新密码不能与旧密码相同',
settingsUpdated: '设置已更新'
},
roles: {

View File

@@ -32,9 +32,9 @@
{{ isEmailVerified ? t('Settings.security.verified') : t('Settings.security.verify') }}
</button> -->
</div>
<div v-if="isEmailVerified" class="security-tip verified-tip">
<!-- <div v-if="isEmailVerified" class="security-tip verified-tip">
{{ t('Settings.security.verifiedTip') }}
</div>
</div> -->
</div>
<div class="inner-divider" />

View File

@@ -1,7 +1,10 @@
<template>
<div class="action-container">
<template v-if="isEditing">
<button type="button" class="primary-btn" :disabled="saving" @click="emit('save')">
<button v-if="needsEmailVerification" type="button" class="primary-btn" :disabled="saving" @click="emit('verify')">
{{ t('Settings.buttons.verifyEmail') }}
</button>
<button v-else type="button" class="primary-btn" :disabled="saving" @click="emit('save')">
{{ saving ? t('Settings.buttons.saving') : t('Settings.buttons.saveChange') }}
</button>
<button type="button" class="secondary-btn" :disabled="saving" @click="emit('discard')">
@@ -22,11 +25,13 @@ import { useI18n } from 'vue-i18n'
defineProps<{
isEditing: boolean
saving: boolean
needsEmailVerification: boolean
}>()
const emit = defineEmits<{
(event: 'edit'): void
(event: 'save'): void
(event: 'verify'): void
(event: 'discard'): void
}>()

View File

@@ -48,8 +48,10 @@
<SettingsActions
:is-editing="isEditing"
:saving="saving"
:needs-email-verification="needsEmailVerification"
@edit="handleEdit"
@save="handleSave"
@verify="handleVerifyEmail"
@discard="handleDiscard"
/>
</div>
@@ -58,7 +60,7 @@
<EmailVerificationDialog
:visible="isVerificationDialogVisible"
:email="verificationTargetEmail"
:email="displayData.email"
:saving="saving"
@close="closeVerificationDialog"
@resend="handleSendVerifyCode"
@@ -95,6 +97,7 @@
displayRegionLabel,
fullName,
roleModel,
needsEmailVerification,
handleEdit,
handleDiscard,
handleSave,

View File

@@ -1,6 +1,13 @@
import { computed, ref, shallowRef, watch, type Ref } from 'vue'
import { ElMessage } from 'element-plus'
import { fetchUserProfile, type UserProfile, updateUserProfile } from '@/api/user'
import md5 from 'md5'
import {
fetchUserProfile,
type UserProfile,
updateUserProfile,
verifyEmailCode,
fetchVerifyCode
} from '@/api/user'
import regionList from '@/utils/area'
import {
languageValues,
@@ -10,6 +17,7 @@ import {
type SecurityDraft,
type SettingsData
} from './types'
import { validateCase, validateLength, validateSpecial } from '@/views/login/tools'
type Translate = (key: string, ...args: unknown[]) => string
@@ -79,6 +87,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
const isVerificationDialogVisible = shallowRef(false)
const verificationTargetEmail = shallowRef('')
const verifiedEmail = shallowRef('')
const verificationCode = shallowRef('')
const roleList = computed(() =>
roleValues.map((value) => ({
@@ -104,6 +113,10 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
const isEmailVerified = computed(
() => hasNewEmailChange.value && verifiedEmail.value === normalizedNewEmail.value
)
const hasNewPasswordChange = computed(() => securityDraft.value.newPassword.length > 0)
const needsEmailVerification = computed(
() => (hasNewEmailChange.value || hasNewPasswordChange.value) && !isEmailVerified.value
)
const displayLanguageLabel = computed(() =>
displayData.value.language ? t(`Settings.languages.${displayData.value.language}`) : ''
)
@@ -137,6 +150,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
isVerificationDialogVisible.value = false
verificationTargetEmail.value = ''
verifiedEmail.value = ''
verificationCode.value = ''
}
const syncAppLanguage = (language: LanguageValue) => {
@@ -192,41 +206,67 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
}
const handleVerifyEmail = () => {
if (!hasNewEmailChange.value && !hasNewPasswordChange.value) return
const nextEmail = normalizedNewEmail.value
const newPassword = securityDraft.value.newPassword
const currentPassword = securityDraft.value.currentPassword
let targetEmail = ''
if (!nextEmail) {
ElMessage.warning(t('Settings.messages.enterNewEmailFirst'))
return
if (hasNewPasswordChange.value) {
if (validateLength(newPassword)) {
ElMessage.warning(t('Settings.messages.passwordLengthError', { min: 6, max: 20 }))
return
}
if (validateSpecial(newPassword)) {
ElMessage.warning(t('Settings.messages.passwordSpecial'))
return
}
if (validateCase(newPassword)) {
ElMessage.warning(t('Settings.messages.passwordCase'))
return
}
if (newPassword === currentPassword) {
ElMessage.warning(t('Settings.messages.passwordNotSameAsOld'))
return
}
}
if (!emailPattern.test(nextEmail)) {
ElMessage.warning(t('Settings.messages.invalidEmail'))
return
if (hasNewEmailChange.value) {
if (!emailPattern.test(nextEmail)) {
ElMessage.warning(t('Settings.messages.invalidEmail'))
return
}
if (nextEmail === sourceData.value.email) {
ElMessage.warning(t('Settings.messages.sameEmail'))
return
}
if (verifiedEmail.value === nextEmail) {
ElMessage.success(t('Settings.messages.alreadyVerified'))
return
}
targetEmail = nextEmail
} else if (hasNewPasswordChange.value) {
targetEmail = sourceData.value.email
}
if (nextEmail === sourceData.value.email) {
ElMessage.warning(t('Settings.messages.sameEmail'))
return
}
if (!targetEmail) return
if (verifiedEmail.value === nextEmail) {
ElMessage.success(t('Settings.messages.alreadyVerified'))
return
}
verificationTargetEmail.value = nextEmail
verificationTargetEmail.value = targetEmail
handleSendVerifyCode()
}
const handleSendVerifyCode = () => {
// AccountSendVerifyCode({
// email: verificationTargetEmail.value,
// operationType: 'FORGET_PWD'
// }).then((res) => {
// console.log(res)
// ElMessage.success(t('Settings.messages.verificationCodeSent'))
// isVerificationDialogVisible.value = true
// })
fetchVerifyCode().then(() => {
ElMessage.success(t('Settings.messages.verificationCodeSent'))
isVerificationDialogVisible.value = true
})
}
const handleVerificationSubmit = (code: string) => {
@@ -234,24 +274,13 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
ElMessage.warning(t('Settings.messages.enterVerificationCode'))
return
}
verifiedEmail.value = verificationTargetEmail.value
closeVerificationDialog()
ElMessage.success(t('Settings.messages.verificationCompleted'))
}
const buildNextData = (): SettingsData => {
const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email
return {
firstName: draftData.value.firstName.trim(),
lastName: draftData.value.lastName.trim(),
username: draftData.value.username.trim(),
email: nextEmail,
roles: [...draftData.value.roles],
language: draftData.value.language,
region: draftData.value.region
}
// send code to backend and store the code locally so save() can include it
verifyEmailCode(code).then((res) => {
verificationCode.value = code
verifiedEmail.value = verificationTargetEmail.value
closeVerificationDialog()
ElMessage.success(t('Settings.messages.verificationCompleted'))
})
}
const handleSave = async () => {
@@ -260,13 +289,47 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
return
}
const nextData = buildNextData()
if (hasNewPasswordChange.value && !verificationCode.value) {
ElMessage.warning(t('Settings.messages.verifyEmailBeforeSave'))
return
}
const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email
const nextData: UserProfile = {
firstName: draftData.value.firstName.trim(),
lastName: draftData.value.lastName.trim(),
username: draftData.value.username.trim(),
email: nextEmail,
roles: draftData.value.roles as string[],
language: draftData.value.language,
region: draftData.value.region
}
// 如果改邮箱或改密码,需要添加验证码和密码信息
if (hasNewEmailChange.value || hasNewPasswordChange.value) {
nextData.verifyCode = verificationCode.value
}
if (hasNewPasswordChange.value) {
nextData.oldPassword = md5(securityDraft.value.currentPassword)
nextData.newPassword = md5(securityDraft.value.newPassword)
}
const previousLanguage = sourceData.value.language
saving.value = true
try {
await updateUserProfile(nextData)
sourceData.value = cloneSettingsData(nextData)
const settingsData: SettingsData = {
firstName: nextData.firstName,
lastName: nextData.lastName,
username: nextData.username,
email: nextData.email,
roles: nextData.roles as RoleValue[],
language: nextData.language as LanguageValue | '',
region: nextData.region as any
}
sourceData.value = cloneSettingsData(settingsData)
console.log(nextData)
if (nextData.language && nextData.language !== previousLanguage) {
@@ -321,6 +384,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
displayRegionLabel,
fullName,
roleModel,
needsEmailVerification,
handleEdit,
handleDiscard,
handleSave,