This commit is contained in:
李志鹏
2026-05-28 14:57:00 +08:00
8 changed files with 419 additions and 217 deletions

View File

@@ -108,3 +108,20 @@ export const verifyEmailCode = (verifyCode: string): Promise<ApiResponse> => {
data: { verifyCode } data: { verifyCode }
}) })
} }
// 获取用户语言设置
export const getUserLanguage = (): Promise<ApiResponse> => {
return request({
url: '/buyer/profile/getLanguage',
method: 'post'
})
}
// 设置语言
export const setUserLanguage = (language: string): Promise<ApiResponse> => {
return request({
url: '/buyer/profile/setLanguage',
method: 'post',
data: { language }
})
}

View File

@@ -1,4 +1,16 @@
import { createRouter, createWebHistory } from 'vue-router' 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<string, string> = {
'en': 'ENGLISH',
'zh-CN': 'CHINESE_SIMPLIFIED'
}
// 语言同步状态缓存(避免每次路由切换都请求)
let languageSynced = false
/** /**
* 路由缓存机制: * 路由缓存机制:
@@ -83,6 +95,52 @@ router.beforeEach((to, from, next) => {
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 export default router

View File

@@ -87,6 +87,8 @@
import myEvent from '@/utils/myEvent' import myEvent from '@/utils/myEvent'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useUserInfoStore } from '@/stores/userInfo' import { useUserInfoStore } from '@/stores/userInfo'
import { setUserLanguage } from '@/api/user'
const { t, locale } = useI18n() const { t, locale } = useI18n()
const userInfoStore = useUserInfoStore() const userInfoStore = useUserInfoStore()
const router = useRouter() const router = useRouter()
@@ -153,6 +155,12 @@
const onLanguageClick = () => { const onLanguageClick = () => {
locale.value = locale.value === 'ENGLISH' ? 'CHINESE_SIMPLIFIED' : 'ENGLISH' locale.value = locale.value === 'ENGLISH' ? 'CHINESE_SIMPLIFIED' : 'ENGLISH'
localStorage.setItem('language', locale.value) localStorage.setItem('language', locale.value)
console.log(locale.value)
const localeMap: Record<string, string> = {
ENGLISH: 'en',
CHINESE_SIMPLIFIED: 'zh-CN'
}
setUserLanguage(localeMap[locale.value])
} }
</script> </script>

View File

@@ -9,13 +9,26 @@
<div class="security-inline-row"> <div class="security-inline-row">
<div class="security-label inline">{{ t('Settings.security.email') }}</div> <div class="security-label inline">{{ t('Settings.security.email') }}</div>
<div class="security-static">{{ email }}</div> <div class="security-static">{{ email }}</div>
<button v-show="isEditing" type="button" class="small-btn" @click="emit('reset-email')"> <button
v-show="isEditing && !isEditingEmail"
type="button"
class="small-btn"
@click="emit('edit-email')"
>
{{ t('Settings.buttons.edit') }}
</button>
<button
v-show="isEditing && isEditingEmail"
type="button"
class="small-btn"
@click="emit('reset-email')"
>
{{ t('Settings.buttons.cancel') }} {{ t('Settings.buttons.cancel') }}
</button> </button>
</div> </div>
</div> </div>
<div v-show="isEditing" class="security-row"> <div v-show="isEditing && isEditingEmail" class="security-row">
<div class="security-label">{{ t('Settings.security.newEmail') }}</div> <div class="security-label">{{ t('Settings.security.newEmail') }}</div>
<div class="outlined-field verify-field"> <div class="outlined-field verify-field">
<el-input <el-input
@@ -43,13 +56,26 @@
<div class="security-inline-row"> <div class="security-inline-row">
<div class="security-label inline">{{ t('Settings.security.password') }}</div> <div class="security-label inline">{{ t('Settings.security.password') }}</div>
<div class="security-static password-mask">.........</div> <div class="security-static password-mask">.........</div>
<button v-show="isEditing" type="button" class="small-btn" @click="emit('reset-password')"> <button
v-show="isEditing && !isEditingPassword"
type="button"
class="small-btn"
@click="emit('edit-password')"
>
{{ t('Settings.buttons.edit') }}
</button>
<button
v-show="isEditing && isEditingPassword"
type="button"
class="small-btn"
@click="emit('reset-password')"
>
{{ t('Settings.buttons.cancel') }} {{ t('Settings.buttons.cancel') }}
</button> </button>
</div> </div>
</div> </div>
<div v-show="isEditing" class="security-row"> <div v-show="isEditing && isEditingPassword" class="security-row">
<div class="security-label">{{ t('Settings.security.newPassword') }}</div> <div class="security-label">{{ t('Settings.security.newPassword') }}</div>
<div class="outlined-field"> <div class="outlined-field">
<el-input <el-input
@@ -63,7 +89,7 @@
<div class="security-tip">{{ t('Settings.security.passwordTip') }}</div> <div class="security-tip">{{ t('Settings.security.passwordTip') }}</div>
</div> </div>
<div v-show="isEditing" class="security-row"> <div v-show="isEditing && isEditingPassword" class="security-row">
<div class="security-label">{{ t('Settings.security.currentPassword') }}</div> <div class="security-label">{{ t('Settings.security.currentPassword') }}</div>
<div class="outlined-field"> <div class="outlined-field">
<el-input <el-input
@@ -80,55 +106,59 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import SettingsSection from './SettingsSection.vue' import SettingsSection from './SettingsSection.vue'
defineProps<{ defineProps<{
email: string email: string
newEmail: string newEmail: string
newPassword: string newPassword: string
currentPassword: string currentPassword: string
isEditing: boolean isEditing: boolean
isEditingEmail: boolean
isEditingPassword: boolean
isEmailVerified: boolean isEmailVerified: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'update:newEmail', value: string): void (event: 'update:newEmail', value: string): void
(event: 'update:newPassword', value: string): void (event: 'update:newPassword', value: string): void
(event: 'update:currentPassword', value: string): void (event: 'update:currentPassword', value: string): void
(event: 'edit-email'): void
(event: 'edit-password'): void
(event: 'reset-email'): void (event: 'reset-email'): void
(event: 'reset-password'): void (event: 'reset-password'): void
(event: 'verify-email'): void (event: 'verify-email'): void
}>() }>()
const { t } = useI18n() const { t } = useI18n()
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.field-text() { .field-text() {
font-family: 'KaiseiOpti-Regular'; font-family: 'KaiseiOpti-Regular';
font-size: 1.6rem; font-size: 1.6rem;
line-height: 2.4rem; line-height: 2.4rem;
color: #232323; color: #232323;
} }
.field-frame() { .field-frame() {
width: 100%; width: 100%;
min-height: 4rem; min-height: 4rem;
border: 0.1rem solid #979797; border: 0.1rem solid #979797;
} }
.control-wrapper() { .control-wrapper() {
box-shadow: none; box-shadow: none;
border-radius: 0; border-radius: 0;
padding: 0 2rem; padding: 0 2rem;
} }
.security-row + .security-row { .security-row + .security-row {
margin-top: 2.8rem; margin-top: 2.8rem;
} }
.security-label { .security-label {
margin: 0 0 0.8rem; margin: 0 0 0.8rem;
font-family: 'KaiseiOpti-Medium'; font-family: 'KaiseiOpti-Medium';
font-size: 1.4rem; font-size: 1.4rem;
@@ -141,33 +171,33 @@ const { t } = useI18n()
margin-bottom: 0; margin-bottom: 0;
flex-shrink: 0; flex-shrink: 0;
} }
} }
.security-static { .security-static {
.field-text(); .field-text();
display: flex; display: flex;
align-items: center; align-items: center;
flex: 1; flex: 1;
min-height: 2.4rem; min-height: 2.4rem;
padding: 0.1rem 0 0; padding: 0.1rem 0 0;
} }
.security-inline-row { .security-inline-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2.8rem; gap: 2.8rem;
min-height: 3.2rem; min-height: 3.2rem;
} }
.security-tip { .security-tip {
margin-top: 0.6rem; margin-top: 0.6rem;
font-family: 'KaiseiOpti-Regular'; font-family: 'KaiseiOpti-Regular';
font-size: 1.2rem; font-size: 1.2rem;
line-height: 1.6rem; line-height: 1.6rem;
color: #9f9f9f; color: #9f9f9f;
} }
.outlined-field { .outlined-field {
.field-frame(); .field-frame();
:deep(.el-input) { :deep(.el-input) {
@@ -179,9 +209,9 @@ const { t } = useI18n()
.control-wrapper(); .control-wrapper();
min-height: 4rem; min-height: 4rem;
} }
} }
.verify-field { .verify-field {
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: 0.8rem; margin-top: 0.8rem;
@@ -189,9 +219,9 @@ const { t } = useI18n()
:deep(.el-input) { :deep(.el-input) {
flex: 1; flex: 1;
} }
} }
.verify-btn { .verify-btn {
border: none; border: none;
min-width: 11rem; min-width: 11rem;
height: 2.8rem; height: 2.8rem;
@@ -209,20 +239,20 @@ const { t } = useI18n()
background: #232323; background: #232323;
border-left-color: #232323; border-left-color: #232323;
} }
} }
.password-mask { .password-mask {
font-family: 'KaiseiOpti-Bold'; font-family: 'KaiseiOpti-Bold';
letter-spacing: 0.08rem; letter-spacing: 0.08rem;
} }
.inner-divider { .inner-divider {
height: 1px; height: 1px;
margin: 2rem 0; margin: 2rem 0;
background-color: #c4c4c4; background-color: #c4c4c4;
} }
.small-btn { .small-btn {
width: 10rem; width: 10rem;
height: 3.2rem; height: 3.2rem;
align-self: flex-start; align-self: flex-start;
@@ -234,9 +264,15 @@ const { t } = useI18n()
letter-spacing: -0.03em; letter-spacing: -0.03em;
color: #232323; color: #232323;
cursor: pointer; cursor: pointer;
} /*
&.edit-btn {
border-color: #232323;
background: #232323;
color: #ffffff;
} */
}
.verified-tip { .verified-tip {
color: #6f7f68; color: #6f7f68;
} }
</style> </style>

View File

@@ -25,7 +25,11 @@
v-model:current-password="securityDraft.currentPassword" v-model:current-password="securityDraft.currentPassword"
:email="displayData.email" :email="displayData.email"
:is-editing="isEditing" :is-editing="isEditing"
:is-editing-email="isEditingEmail"
:is-editing-password="isEditingPassword"
:is-email-verified="isEmailVerified" :is-email-verified="isEmailVerified"
@edit-email="handleEditEmail"
@edit-password="handleEditPassword"
@reset-email="resetSecurityEmail" @reset-email="resetSecurityEmail"
@reset-password="resetSecurityPassword" @reset-password="resetSecurityPassword"
@verify-email="handleVerifyEmail" @verify-email="handleVerifyEmail"
@@ -98,9 +102,13 @@
fullName, fullName,
roleModel, roleModel,
needsEmailVerification, needsEmailVerification,
isEditingEmail,
isEditingPassword,
handleEdit, handleEdit,
handleDiscard, handleDiscard,
handleSave, handleSave,
handleEditEmail,
handleEditPassword,
resetSecurityEmail, resetSecurityEmail,
resetSecurityPassword, resetSecurityPassword,
handleVerifyEmail, handleVerifyEmail,

View File

@@ -26,11 +26,24 @@ interface UseSettingsFormOptions {
locale: Ref<string> locale: Ref<string>
} }
// 前端 UI 使用的语言值(用于下拉选择)
const languageLocaleMap: Record<LanguageValue, 'ENGLISH' | 'CHINESE_SIMPLIFIED'> = { const languageLocaleMap: Record<LanguageValue, 'ENGLISH' | 'CHINESE_SIMPLIFIED'> = {
english: 'ENGLISH', english: 'ENGLISH',
chinese: 'CHINESE_SIMPLIFIED' 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 emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const createDefaultData = (): SettingsData => ({ const createDefaultData = (): SettingsData => ({
@@ -58,8 +71,15 @@ const normalizeLanguage = (language: string | null | undefined): LanguageValue =
return '' as LanguageValue return '' as LanguageValue
} }
const normalized = language.trim().toLowerCase() // 后端返回 'en' 或 'zh-CN'
return normalized.includes('chinese') ? 'chinese' : 'english' const trimmed = language.trim()
if (trimmed === 'en') {
return 'english'
} else if (trimmed === 'zh-CN') {
return 'chinese'
}
return '' as LanguageValue
} }
const buildSettingsDataFromProfile = (profile: Partial<UserProfile>): SettingsData => ({ const buildSettingsDataFromProfile = (profile: Partial<UserProfile>): SettingsData => ({
@@ -88,6 +108,8 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
const verificationTargetEmail = shallowRef('') const verificationTargetEmail = shallowRef('')
const verifiedEmail = shallowRef('') const verifiedEmail = shallowRef('')
const verificationCode = shallowRef('') const verificationCode = shallowRef('')
const isEditingEmail = shallowRef(false)
const isEditingPassword = shallowRef(false)
const roleList = computed(() => const roleList = computed(() =>
roleValues.map((value) => ({ roleValues.map((value) => ({
@@ -178,6 +200,8 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
draftData.value = cloneSettingsData(sourceData.value) draftData.value = cloneSettingsData(sourceData.value)
securityDraft.value = createEmptySecurityDraft() securityDraft.value = createEmptySecurityDraft()
resetEmailVerificationState() resetEmailVerificationState()
isEditingEmail.value = false
isEditingPassword.value = false
} }
const handleEdit = () => { const handleEdit = () => {
@@ -188,11 +212,21 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
const resetSecurityEmail = () => { const resetSecurityEmail = () => {
securityDraft.value.newEmail = '' securityDraft.value.newEmail = ''
resetEmailVerificationState() resetEmailVerificationState()
isEditingEmail.value = false
} }
const resetSecurityPassword = () => { const resetSecurityPassword = () => {
securityDraft.value.newPassword = '' securityDraft.value.newPassword = ''
securityDraft.value.currentPassword = '' securityDraft.value.currentPassword = ''
isEditingPassword.value = false
}
const handleEditEmail = () => {
isEditingEmail.value = true
}
const handleEditPassword = () => {
isEditingPassword.value = true
} }
const handleDiscard = () => { const handleDiscard = () => {
@@ -294,15 +328,24 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
return 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 = { const nextData: UserProfile = {
firstName: draftData.value.firstName.trim(), firstName: draftData.value.firstName.trim(),
lastName: draftData.value.lastName.trim(), lastName: draftData.value.lastName.trim(),
username: draftData.value.username.trim(), username: draftData.value.username.trim(),
email: nextEmail, email: securityDraft.value.newEmail.trim(),
roles: draftData.value.roles as string[], roles: draftData.value.roles as string[],
language: draftData.value.language, language: backendLanguage,
region: draftData.value.region region: draftData.value.region,
newPassword: '',
oldPassword: '',
verifyCode: ''
} }
// 如果改邮箱或改密码,需要添加验证码和密码信息 // 如果改邮箱或改密码,需要添加验证码和密码信息
@@ -320,26 +363,34 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
try { try {
await updateUserProfile(nextData) await updateUserProfile(nextData)
// 将后端返回的语言值转换为前端格式
const frontendLanguage = backendLanguage
? backendToFrontendLanguage[backendLanguage as 'en' | 'zh-CN']
: ''
const settingsData: SettingsData = { const settingsData: SettingsData = {
firstName: nextData.firstName, firstName: nextData.firstName,
lastName: nextData.lastName, lastName: nextData.lastName,
username: nextData.username, username: nextData.username,
email: nextData.email, email: nextData.email || draftData.value.email,
roles: nextData.roles as RoleValue[], roles: nextData.roles as RoleValue[],
language: nextData.language as LanguageValue | '', language: frontendLanguage,
region: nextData.region as any region: nextData.region as any
} }
sourceData.value = cloneSettingsData(settingsData) sourceData.value = cloneSettingsData(settingsData)
console.log(nextData) console.log(nextData)
if (nextData.language && nextData.language !== previousLanguage) { if (frontendLanguage && frontendLanguage !== previousLanguage) {
syncAppLanguage(nextData.language as LanguageValue) syncAppLanguage(frontendLanguage as LanguageValue)
} }
draftData.value = cloneSettingsData(sourceData.value) draftData.value = cloneSettingsData(sourceData.value)
securityDraft.value = createEmptySecurityDraft() securityDraft.value = createEmptySecurityDraft()
resetEmailVerificationState() resetEmailVerificationState()
isEditing.value = false isEditing.value = false
isEditingEmail.value = false
isEditingPassword.value = false
ElMessage.success(t('Settings.messages.settingsUpdated')) ElMessage.success(t('Settings.messages.settingsUpdated'))
} catch (error) { } catch (error) {
console.warn(error) console.warn(error)
@@ -385,11 +436,15 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
fullName, fullName,
roleModel, roleModel,
needsEmailVerification, needsEmailVerification,
isEditingEmail,
isEditingPassword,
handleEdit, handleEdit,
handleDiscard, handleDiscard,
handleSave, handleSave,
resetSecurityEmail, resetSecurityEmail,
resetSecurityPassword, resetSecurityPassword,
handleEditEmail,
handleEditPassword,
handleVerifyEmail, handleVerifyEmail,
handleSendVerifyCode, handleSendVerifyCode,
handleVerificationSubmit, handleVerificationSubmit,

View File

@@ -32,7 +32,7 @@
class="assets-toolbar__download flex flex-center" class="assets-toolbar__download flex flex-center"
:class="{ disabled: selectedCount < 1 }" :class="{ disabled: selectedCount < 1 }"
v-loading="downloadingSelected" v-loading="downloadingSelected"
@click="handleDownloadSelected" @click="handleDownloadSelected(null)"
> >
<SvgIcon name="downloadBtn" color="#fff" /> <SvgIcon name="downloadBtn" color="#fff" />
<span>{{ t('Wardrobe.assets.downloadSelected') }}</span> <span>{{ t('Wardrobe.assets.downloadSelected') }}</span>
@@ -67,7 +67,7 @@
download download
:url="item.thumbnailUrl" :url="item.thumbnailUrl"
:name="item.listingName" :name="item.listingName"
@download.stop="handleDownloadSelected(item)" @download="handleDownloadSelected(item)"
:showPrice="false" :showPrice="false"
></CommodityItem> ></CommodityItem>
</div> </div>
@@ -323,7 +323,7 @@
} }
const downloadingSelected = ref(false) const downloadingSelected = ref(false)
const handleDownloadSelected = (assets) => { const handleDownloadSelected = (assets = null) => {
const items = assets ? [assets] : dataList.value.filter((item) => item.checked) const items = assets ? [assets] : dataList.value.filter((item) => item.checked)
downloadingSelected.value = true downloadingSelected.value = true
@@ -350,7 +350,8 @@
}) })
.catch((error) => { .catch((error) => {
console.error('Download failed:', error) console.error('Download failed:', error)
}).finally(() => { })
.finally(() => {
downloadingSelected.value = false downloadingSelected.value = false
}) })
} }

View File

@@ -97,7 +97,7 @@
import { computed, onMounted, ref, shallowRef } from 'vue' import { computed, onMounted, ref, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' 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 ScItem from '@/views/shoppingCart/sc-item.vue'
import Empty from './Empty.vue' import Empty from './Empty.vue'
@@ -257,7 +257,26 @@
} }
const handleDownload = (order) => { 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) => { const handleRouteBrand = (order: OrderRecord) => {