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> => { export const fetchUserProfile = (): Promise<ApiResponse> => {
return request({ 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> => { export const updateUserProfile = (data: UserProfile): Promise<ApiResponse> => {
return request({ return request({
url: '/buyer/profile/setProfile', url: '/buyer/profile/setProfile',
@@ -88,3 +91,20 @@ export const updateUserProfile = (data: UserProfile): Promise<ApiResponse> => {
data 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', discard: 'DISCARD',
edit: 'EDIT', edit: 'EDIT',
saveChange: 'SAVE CHANGE', saveChange: 'SAVE CHANGE',
verifyEmail: 'VERIFY EMAIL',
saving: 'SAVING...' saving: 'SAVING...'
}, },
dialog: { dialog: {
@@ -117,7 +118,7 @@ export default {
resendCodeIn: 'Resend Code in {time}' resendCodeIn: 'Resend Code in {time}'
}, },
messages: { messages: {
enterNewEmailFirst: 'Please enter your new email address first', enterNewEmailFirst: 'Please enter your email address first',
invalidEmail: 'Please enter a valid email address', invalidEmail: 'Please enter a valid email address',
sameEmail: 'Please enter a different email address', sameEmail: 'Please enter a different email address',
alreadyVerified: 'This email has already been verified', alreadyVerified: 'This email has already been verified',
@@ -125,6 +126,11 @@ export default {
enterVerificationCode: 'Please enter the 6-digit verification code', enterVerificationCode: 'Please enter the 6-digit verification code',
verificationCompleted: 'Email verification completed', verificationCompleted: 'Email verification completed',
verifyEmailBeforeSave: 'Please verify your new email before saving', 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' settingsUpdated: 'Settings updated'
}, },
roles: { roles: {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,13 @@
import { computed, ref, shallowRef, watch, type Ref } from 'vue' import { computed, ref, shallowRef, watch, type Ref } from 'vue'
import { ElMessage } from 'element-plus' 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 regionList from '@/utils/area'
import { import {
languageValues, languageValues,
@@ -10,6 +17,7 @@ import {
type SecurityDraft, type SecurityDraft,
type SettingsData type SettingsData
} from './types' } from './types'
import { validateCase, validateLength, validateSpecial } from '@/views/login/tools'
type Translate = (key: string, ...args: unknown[]) => string type Translate = (key: string, ...args: unknown[]) => string
@@ -79,6 +87,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
const isVerificationDialogVisible = shallowRef(false) const isVerificationDialogVisible = shallowRef(false)
const verificationTargetEmail = shallowRef('') const verificationTargetEmail = shallowRef('')
const verifiedEmail = shallowRef('') const verifiedEmail = shallowRef('')
const verificationCode = shallowRef('')
const roleList = computed(() => const roleList = computed(() =>
roleValues.map((value) => ({ roleValues.map((value) => ({
@@ -104,6 +113,10 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
const isEmailVerified = computed( const isEmailVerified = computed(
() => hasNewEmailChange.value && verifiedEmail.value === normalizedNewEmail.value () => 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(() => const displayLanguageLabel = computed(() =>
displayData.value.language ? t(`Settings.languages.${displayData.value.language}`) : '' displayData.value.language ? t(`Settings.languages.${displayData.value.language}`) : ''
) )
@@ -137,6 +150,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
isVerificationDialogVisible.value = false isVerificationDialogVisible.value = false
verificationTargetEmail.value = '' verificationTargetEmail.value = ''
verifiedEmail.value = '' verifiedEmail.value = ''
verificationCode.value = ''
} }
const syncAppLanguage = (language: LanguageValue) => { const syncAppLanguage = (language: LanguageValue) => {
@@ -192,13 +206,36 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
} }
const handleVerifyEmail = () => { const handleVerifyEmail = () => {
const nextEmail = normalizedNewEmail.value if (!hasNewEmailChange.value && !hasNewPasswordChange.value) return
if (!nextEmail) { const nextEmail = normalizedNewEmail.value
ElMessage.warning(t('Settings.messages.enterNewEmailFirst')) const newPassword = securityDraft.value.newPassword
const currentPassword = securityDraft.value.currentPassword
let targetEmail = ''
if (hasNewPasswordChange.value) {
if (validateLength(newPassword)) {
ElMessage.warning(t('Settings.messages.passwordLengthError', { min: 6, max: 20 }))
return 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 (hasNewEmailChange.value) {
if (!emailPattern.test(nextEmail)) { if (!emailPattern.test(nextEmail)) {
ElMessage.warning(t('Settings.messages.invalidEmail')) ElMessage.warning(t('Settings.messages.invalidEmail'))
return return
@@ -214,19 +251,22 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
return return
} }
verificationTargetEmail.value = nextEmail targetEmail = nextEmail
} else if (hasNewPasswordChange.value) {
targetEmail = sourceData.value.email
}
if (!targetEmail) return
verificationTargetEmail.value = targetEmail
handleSendVerifyCode() handleSendVerifyCode()
} }
const handleSendVerifyCode = () => { const handleSendVerifyCode = () => {
// AccountSendVerifyCode({ fetchVerifyCode().then(() => {
// email: verificationTargetEmail.value, ElMessage.success(t('Settings.messages.verificationCodeSent'))
// operationType: 'FORGET_PWD' isVerificationDialogVisible.value = true
// }).then((res) => { })
// console.log(res)
// ElMessage.success(t('Settings.messages.verificationCodeSent'))
// isVerificationDialogVisible.value = true
// })
} }
const handleVerificationSubmit = (code: string) => { const handleVerificationSubmit = (code: string) => {
@@ -234,24 +274,13 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
ElMessage.warning(t('Settings.messages.enterVerificationCode')) ElMessage.warning(t('Settings.messages.enterVerificationCode'))
return return
} }
// 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 verifiedEmail.value = verificationTargetEmail.value
closeVerificationDialog() closeVerificationDialog()
ElMessage.success(t('Settings.messages.verificationCompleted')) 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
}
} }
const handleSave = async () => { const handleSave = async () => {
@@ -260,13 +289,47 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
return 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 const previousLanguage = sourceData.value.language
saving.value = true saving.value = true
try { try {
await updateUserProfile(nextData) 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) console.log(nextData)
if (nextData.language && nextData.language !== previousLanguage) { if (nextData.language && nextData.language !== previousLanguage) {
@@ -321,6 +384,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
displayRegionLabel, displayRegionLabel,
fullName, fullName,
roleModel, roleModel,
needsEmailVerification,
handleEdit, handleEdit,
handleDiscard, handleDiscard,
handleSave, handleSave,