From 881f25ac2b19f949d54f8d577d5b4edc9ba980fd Mon Sep 17 00:00:00 2001 From: zhangyahui Date: Thu, 28 May 2026 11:46:23 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20security=E9=83=A8=E5=88=86=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E4=BA=8C=E6=AC=A1=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../setting/components/SecuritySection.vue | 436 ++++++++++-------- src/views/setting/index.vue | 8 + src/views/setting/useSettingsForm.ts | 18 + 3 files changed, 262 insertions(+), 200 deletions(-) diff --git a/src/views/setting/components/SecuritySection.vue b/src/views/setting/components/SecuritySection.vue index 6c73d08..c957f76 100644 --- a/src/views/setting/components/SecuritySection.vue +++ b/src/views/setting/components/SecuritySection.vue @@ -1,29 +1,42 @@ diff --git a/src/views/setting/index.vue b/src/views/setting/index.vue index a8a3edf..431dc9d 100644 --- a/src/views/setting/index.vue +++ b/src/views/setting/index.vue @@ -25,7 +25,11 @@ v-model:current-password="securityDraft.currentPassword" :email="displayData.email" :is-editing="isEditing" + :is-editing-email="isEditingEmail" + :is-editing-password="isEditingPassword" :is-email-verified="isEmailVerified" + @edit-email="handleEditEmail" + @edit-password="handleEditPassword" @reset-email="resetSecurityEmail" @reset-password="resetSecurityPassword" @verify-email="handleVerifyEmail" @@ -98,9 +102,13 @@ fullName, roleModel, needsEmailVerification, + isEditingEmail, + isEditingPassword, handleEdit, handleDiscard, handleSave, + handleEditEmail, + handleEditPassword, resetSecurityEmail, resetSecurityPassword, handleVerifyEmail, diff --git a/src/views/setting/useSettingsForm.ts b/src/views/setting/useSettingsForm.ts index 0b4a0bd..88fd4a7 100644 --- a/src/views/setting/useSettingsForm.ts +++ b/src/views/setting/useSettingsForm.ts @@ -88,6 +88,8 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { const verificationTargetEmail = shallowRef('') const verifiedEmail = shallowRef('') const verificationCode = shallowRef('') + const isEditingEmail = shallowRef(false) + const isEditingPassword = shallowRef(false) const roleList = computed(() => roleValues.map((value) => ({ @@ -178,6 +180,8 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { draftData.value = cloneSettingsData(sourceData.value) securityDraft.value = createEmptySecurityDraft() resetEmailVerificationState() + isEditingEmail.value = false + isEditingPassword.value = false } const handleEdit = () => { @@ -188,11 +192,21 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { const resetSecurityEmail = () => { securityDraft.value.newEmail = '' resetEmailVerificationState() + isEditingEmail.value = false } const resetSecurityPassword = () => { securityDraft.value.newPassword = '' securityDraft.value.currentPassword = '' + isEditingPassword.value = false + } + + const handleEditEmail = () => { + isEditingEmail.value = true + } + + const handleEditPassword = () => { + isEditingPassword.value = true } const handleDiscard = () => { @@ -385,11 +399,15 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { fullName, roleModel, needsEmailVerification, + isEditingEmail, + isEditingPassword, handleEdit, handleDiscard, handleSave, resetSecurityEmail, resetSecurityPassword, + handleEditEmail, + handleEditPassword, handleVerifyEmail, handleSendVerifyCode, handleVerificationSubmit, From 4e49584c603dd8db8082126b5aa05239e5d3dfed Mon Sep 17 00:00:00 2001 From: zhangyahui Date: Thu, 28 May 2026 13:08:01 +0800 Subject: [PATCH 2/5] =?UTF-8?q?bugfix:=20=E4=BC=A0=E5=8F=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/setting/useSettingsForm.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/views/setting/useSettingsForm.ts b/src/views/setting/useSettingsForm.ts index 88fd4a7..1b3ffec 100644 --- a/src/views/setting/useSettingsForm.ts +++ b/src/views/setting/useSettingsForm.ts @@ -316,7 +316,11 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { email: nextEmail, roles: draftData.value.roles as string[], language: draftData.value.language, - region: draftData.value.region + region: draftData.value.region, + email: '', + newPassword: '', + oldPassword: '', + verifyCode: '' } // 如果改邮箱或改密码,需要添加验证码和密码信息 From e45fd1135c80312f896cd3b35f531aa5bdf9e4b3 Mon Sep 17 00:00:00 2001 From: zhangyahui Date: Thu, 28 May 2026 13:10:06 +0800 Subject: [PATCH 3/5] =?UTF-8?q?bugfix:=20=E9=87=8D=E5=A4=8Dkey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/setting/useSettingsForm.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/views/setting/useSettingsForm.ts b/src/views/setting/useSettingsForm.ts index 1b3ffec..ddf0777 100644 --- a/src/views/setting/useSettingsForm.ts +++ b/src/views/setting/useSettingsForm.ts @@ -308,7 +308,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { return } - const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email + const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email || '' const nextData: UserProfile = { firstName: draftData.value.firstName.trim(), lastName: draftData.value.lastName.trim(), @@ -317,10 +317,9 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { roles: draftData.value.roles as string[], language: draftData.value.language, region: draftData.value.region, - email: '', newPassword: '', oldPassword: '', - verifyCode: '' + verifyCode: '' } // 如果改邮箱或改密码,需要添加验证码和密码信息 From f203d6146cd41644a6baf351e49b7be566de49d1 Mon Sep 17 00:00:00 2001 From: zhangyahui Date: Thu, 28 May 2026 13:45:59 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E8=AF=AD=E8=A8=80=E5=88=87?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/user.ts | 17 ++++++++ src/router/index.ts | 60 +++++++++++++++++++++++++++- src/views/main-header.vue | 8 ++++ src/views/setting/useSettingsForm.ts | 50 +++++++++++++++++++---- 4 files changed, 126 insertions(+), 9 deletions(-) diff --git a/src/api/user.ts b/src/api/user.ts index 815fac8..9e7da8c 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -108,3 +108,20 @@ export const verifyEmailCode = (verifyCode: string): Promise => { data: { verifyCode } }) } + +// 获取用户语言设置 +export const getUserLanguage = (): Promise => { + return request({ + url: '/buyer/profile/getLanguage', + method: 'post' + }) +} + +// 设置语言 +export const setUserLanguage = (language: string): Promise => { + return request({ + url: '/buyer/profile/setLanguage', + method: 'post', + data: { language } + }) +} \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index 288abb4..e166ef3 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,4 +1,16 @@ import { createRouter, createWebHistory } from 'vue-router' +import { useUserInfoStore } from '@/stores/userInfo' +import { getUserLanguage } from '@/api/user' +import i18n from '@/lang/index' + +// 语言映射:后端格式 -> i18n 格式 +const backendToI18nLanguage: Record = { + 'en': 'ENGLISH', + 'zh-CN': 'CHINESE_SIMPLIFIED' +} + +// 语言同步状态缓存(避免每次路由切换都请求) +let languageSynced = false /** * 路由缓存机制: @@ -83,6 +95,52 @@ router.beforeEach((to, from, next) => { next() }) -router.afterEach(() => {}) +router.afterEach(async () => { + // 检查用户是否已登录 + const userInfoStore = useUserInfoStore() + const token = userInfoStore.state.token + + if (!token) { + languageSynced = false // 未登录时重置同步状态 + return + } + + // 如果已经同步过,跳过 + if (languageSynced) { + return + } + + try { + // 获取用户语言设置 + const response = await getUserLanguage() + const userLanguage = (response as any)?.language // 后端返回 'en' 或 'zh-CN' + + if (!userLanguage) { + return + } + + // 转换为 i18n 格式 + const i18nLanguage = backendToI18nLanguage[userLanguage] + + if (!i18nLanguage) { + return + } + + // 获取当前 i18n 语言 + const currentLocale = i18n.global.locale.value + + // 如果用户语言和本地 i18n 不一致,更新 i18n + if (i18nLanguage !== currentLocale) { + i18n.global.locale.value = i18nLanguage as 'ENGLISH' | 'CHINESE_SIMPLIFIED' + localStorage.setItem('language', i18nLanguage) + } + + // 标记已同步 + languageSynced = true + } catch (error) { + // 静默失败,不影响页面正常加载 + console.warn('Failed to sync user language:', error) + } +}) export default router diff --git a/src/views/main-header.vue b/src/views/main-header.vue index 1eaf69f..fe65e9d 100644 --- a/src/views/main-header.vue +++ b/src/views/main-header.vue @@ -87,6 +87,8 @@ import myEvent from '@/utils/myEvent' import { useI18n } from 'vue-i18n' import { useUserInfoStore } from '@/stores/userInfo' + import { setUserLanguage } from '@/api/user' + const { t, locale } = useI18n() const userInfoStore = useUserInfoStore() const router = useRouter() @@ -153,6 +155,12 @@ const onLanguageClick = () => { locale.value = locale.value === 'ENGLISH' ? 'CHINESE_SIMPLIFIED' : 'ENGLISH' localStorage.setItem('language', locale.value) + console.log(locale.value) + const localeMap: Record = { + ENGLISH: 'en', + CHINESE_SIMPLIFIED: 'zh-CN' + } + setUserLanguage(localeMap[locale.value]) } diff --git a/src/views/setting/useSettingsForm.ts b/src/views/setting/useSettingsForm.ts index ddf0777..76ea11c 100644 --- a/src/views/setting/useSettingsForm.ts +++ b/src/views/setting/useSettingsForm.ts @@ -26,11 +26,24 @@ interface UseSettingsFormOptions { locale: Ref } +// 前端 UI 使用的语言值(用于下拉选择) const languageLocaleMap: Record = { english: 'ENGLISH', chinese: 'CHINESE_SIMPLIFIED' } +// 后端 API 使用的语言值 +const backendLanguageMap: Record<'ENGLISH' | 'CHINESE_SIMPLIFIED', 'en' | 'zh-CN'> = { + ENGLISH: 'en', + CHINESE_SIMPLIFIED: 'zh-CN' +} + +// 后端语言值转换为前端语言值 +const backendToFrontendLanguage: Record<'en' | 'zh-CN', LanguageValue> = { + en: 'english', + 'zh-CN': 'chinese' +} + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const createDefaultData = (): SettingsData => ({ @@ -58,8 +71,15 @@ const normalizeLanguage = (language: string | null | undefined): LanguageValue = return '' as LanguageValue } - const normalized = language.trim().toLowerCase() - return normalized.includes('chinese') ? 'chinese' : 'english' + // 后端返回 'en' 或 'zh-CN' + const trimmed = language.trim() + if (trimmed === 'en') { + return 'english' + } else if (trimmed === 'zh-CN') { + return 'chinese' + } + + return '' as LanguageValue } const buildSettingsDataFromProfile = (profile: Partial): SettingsData => ({ @@ -308,14 +328,20 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { return } - const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email || '' + // 将前端语言值转换为后端格式 + let backendLanguage = '' + if (draftData.value.language) { + const i18nLocale = languageLocaleMap[draftData.value.language as LanguageValue] + backendLanguage = backendLanguageMap[i18nLocale] + } + const nextData: UserProfile = { firstName: draftData.value.firstName.trim(), lastName: draftData.value.lastName.trim(), username: draftData.value.username.trim(), - email: nextEmail, + email: securityDraft.value.newEmail.trim(), roles: draftData.value.roles as string[], - language: draftData.value.language, + language: backendLanguage, // 发送后端格式:'en' 或 'zh-CN' region: draftData.value.region, newPassword: '', oldPassword: '', @@ -337,26 +363,34 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { try { await updateUserProfile(nextData) + + // 将后端返回的语言值转换为前端格式 + const frontendLanguage = backendLanguage + ? backendToFrontendLanguage[backendLanguage as 'en' | 'zh-CN'] + : '' + 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 | '', + language: frontendLanguage, region: nextData.region as any } sourceData.value = cloneSettingsData(settingsData) console.log(nextData) - if (nextData.language && nextData.language !== previousLanguage) { - syncAppLanguage(nextData.language as LanguageValue) + if (frontendLanguage && frontendLanguage !== previousLanguage) { + syncAppLanguage(frontendLanguage as LanguageValue) } draftData.value = cloneSettingsData(sourceData.value) securityDraft.value = createEmptySecurityDraft() resetEmailVerificationState() isEditing.value = false + isEditingEmail.value = false + isEditingPassword.value = false ElMessage.success(t('Settings.messages.settingsUpdated')) } catch (error) { console.warn(error) From 9948b18c26ccfca6854b8e8b0aa56c48b5d5394c Mon Sep 17 00:00:00 2001 From: zhangyahui Date: Thu, 28 May 2026 14:20:23 +0800 Subject: [PATCH 5/5] =?UTF-8?q?bugfix:=20=E8=AE=BE=E7=BD=AE=E9=A1=B5?= =?UTF-8?q?=E9=82=AE=E7=AE=B1&=E8=B5=84=E6=BA=90=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/setting/useSettingsForm.ts | 4 ++-- src/views/wardrobe/Assets.vue | 9 +++++---- src/views/wardrobe/Orders.vue | 23 +++++++++++++++++++++-- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/views/setting/useSettingsForm.ts b/src/views/setting/useSettingsForm.ts index 76ea11c..6ff417d 100644 --- a/src/views/setting/useSettingsForm.ts +++ b/src/views/setting/useSettingsForm.ts @@ -341,7 +341,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { username: draftData.value.username.trim(), email: securityDraft.value.newEmail.trim(), roles: draftData.value.roles as string[], - language: backendLanguage, // 发送后端格式:'en' 或 'zh-CN' + language: backendLanguage, region: draftData.value.region, newPassword: '', oldPassword: '', @@ -373,7 +373,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { firstName: nextData.firstName, lastName: nextData.lastName, username: nextData.username, - email: nextData.email, + email: nextData.email || draftData.value.email, roles: nextData.roles as RoleValue[], language: frontendLanguage, region: nextData.region as any diff --git a/src/views/wardrobe/Assets.vue b/src/views/wardrobe/Assets.vue index 0bce7d6..73c1072 100644 --- a/src/views/wardrobe/Assets.vue +++ b/src/views/wardrobe/Assets.vue @@ -32,7 +32,7 @@ class="assets-toolbar__download flex flex-center" :class="{ disabled: selectedCount < 1 }" v-loading="downloadingSelected" - @click="handleDownloadSelected" + @click="handleDownloadSelected(null)" > {{ t('Wardrobe.assets.downloadSelected') }} @@ -67,7 +67,7 @@ download :url="item.thumbnailUrl" :name="item.listingName" - @download.stop="handleDownloadSelected(item)" + @download="handleDownloadSelected(item)" :showPrice="false" > @@ -323,7 +323,7 @@ } const downloadingSelected = ref(false) - const handleDownloadSelected = (assets) => { + const handleDownloadSelected = (assets = null) => { const items = assets ? [assets] : dataList.value.filter((item) => item.checked) downloadingSelected.value = true @@ -350,7 +350,8 @@ }) .catch((error) => { console.error('Download failed:', error) - }).finally(() => { + }) + .finally(() => { downloadingSelected.value = false }) } diff --git a/src/views/wardrobe/Orders.vue b/src/views/wardrobe/Orders.vue index a0dca3d..5408ec3 100644 --- a/src/views/wardrobe/Orders.vue +++ b/src/views/wardrobe/Orders.vue @@ -97,7 +97,7 @@ import { computed, onMounted, ref, shallowRef } from 'vue' import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' - import { fetchMyOrders ,fetchDownloadItemsByGet} from '@/api/user' + import { fetchMyOrders, fetchDownloadItemsByGet } from '@/api/user' import ScItem from '@/views/shoppingCart/sc-item.vue' import Empty from './Empty.vue' @@ -257,7 +257,26 @@ } const handleDownload = (order) => { - console.log(order) + const ids = order.items.map((item) => item.id) + fetchDownloadItemsByGet({ ids }).then((res) => { + console.log(res) + const disposition = res.headers['content-disposition'] + const fileName = disposition?.split('filename=')[1]?.replace(/"/g, '') || 'download.zip' + const blob = res.data + const url = window.URL.createObjectURL(blob) + + const link = document.createElement('a') + link.href = url + + const timestamp = new Date().getTime() + link.download = fileName || `wardrobe_download_${timestamp}.zip` + + document.body.appendChild(link) + link.click() + + document.body.removeChild(link) + window.URL.revokeObjectURL(url) + }) } const handleRouteBrand = (order: OrderRecord) => {