feat: 资料更新

This commit is contained in:
2026-05-27 17:29:35 +08:00
parent 3e64912804
commit a4861da21a
13 changed files with 794 additions and 521 deletions

View File

@@ -7,6 +7,7 @@ export interface WardrobeItem {
page: number page: number
size: number size: number
} }
// 获取我的衣橱assets
export const fetchMyWardrobe = (data: WardrobeItem): Promise<ApiResponse> => { export const fetchMyWardrobe = (data: WardrobeItem): Promise<ApiResponse> => {
return request({ return request({
url: '/buyer/buyer/order/assets/page', url: '/buyer/buyer/order/assets/page',
@@ -24,7 +25,7 @@ export interface OrderItem {
export interface OrdersPageResponse { export interface OrdersPageResponse {
content: any[] content: any[]
} }
// 获取我的衣橱 orders
export const fetchMyOrders = (data: OrderItem): Promise<OrdersPageResponse> => { export const fetchMyOrders = (data: OrderItem): Promise<OrdersPageResponse> => {
return request({ return request({
url: '/buyer/buyer/order/page', url: '/buyer/buyer/order/page',
@@ -32,3 +33,58 @@ export const fetchMyOrders = (data: OrderItem): Promise<OrdersPageResponse> => {
params: data params: data
}) })
} }
export interface Download {
ids: string[]
}
// 下载资源
export const fetchDownloadItemsByGet = (params: Download): Promise<ApiResponse> => {
return request({
url: '/buyer/listing/mall/main-product/download',
method: 'get',
responseType: 'blob',
params,
paramsSerializer: (p: any) => {
const usp = new URLSearchParams()
if (p && p.ids && Array.isArray(p.ids)) {
p.ids.forEach((id: any) => usp.append('ids', String(id)))
} else if (p) {
Object.keys(p).forEach((k) => {
const v = (p as any)[k]
if (Array.isArray(v)) {
v.forEach((x) => usp.append(k, String(x)))
} else if (v !== undefined && v !== null) {
usp.append(k, String(v))
}
})
}
return usp.toString()
}
})
}
export interface UserProfile {
firstName: string
lastName: string
username: string
roles: string[]
region: string
language: string
email: string
}
// 获取用户信息
export const fetchUserProfile = (): Promise<ApiResponse> => {
return request({
url: '/buyer/profile/getProfile',
method: 'post'
})
}
// 设置用户信息
export const updateUserProfile = (data: UserProfile): Promise<ApiResponse> => {
return request({
url: '/buyer/profile/setProfile',
method: 'post',
data
})
}

View File

@@ -46,18 +46,21 @@ export default {
submit: 'Submit', submit: 'Submit',
enterNewPassword: 'Enter a new password for<br/><span>{email}</span>', enterNewPassword: 'Enter a new password for<br/><span>{email}</span>',
passwordsDoNotMatch: 'Passwords do not match', passwordsDoNotMatch: 'Passwords do not match',
logOffTip: 'Are you sure to log off?', logOffTip: 'Are you sure to log off?'
}, },
RegisterSuccess: { RegisterSuccess: {
title1: 'Welcome to Stylish Parade!', title1: 'Welcome to Stylish Parade!',
title2: 'Please switch to the Login tab to log in.', title2: 'Please switch to the Login tab to log in.',
title3: 'What awaits you in Stylish Parade', title3: 'What awaits you in Stylish Parade',
item1title: 'Behind the design', item1title: 'Behind the design',
item1tip: 'Discover how designers bring ideas to life with AiDA — from first sketch to final look.', item1tip:
'Discover how designers bring ideas to life with AiDA — from first sketch to final look.',
item2title: 'Creative digital works', item2title: 'Creative digital works',
item2tip: 'Unlock a growing library of inspiring digital works to refresh your creative mind.', item2tip:
'Unlock a growing library of inspiring digital works to refresh your creative mind.',
item3title: 'A fashion community', item3title: 'A fashion community',
item3tip: 'Join a space where fashion speaks — exchange ideas and connect with creators worldwide.', item3tip:
'Join a space where fashion speaks — exchange ideas and connect with creators worldwide.'
}, },
Settings: { Settings: {
title: 'Settings', title: 'Settings',
@@ -73,7 +76,7 @@ export default {
usernamePlaceholder: 'Username', usernamePlaceholder: 'Username',
usernameTip: 'Your public username on Stylish Parade.', usernameTip: 'Your public username on Stylish Parade.',
role: 'ROLE', role: 'ROLE',
roleTip: 'Select up to 2 labels that suit you.', roleTip: 'Select up to 2 labels that suit you.'
}, },
security: { security: {
title: 'Security', title: 'Security',
@@ -139,7 +142,7 @@ export default {
}, },
languages: { languages: {
english: 'English', english: 'English',
chinese: 'Chinese', chinese: 'Chinese'
}, },
regions: { regions: {
hongKongSar: 'Hong Kong SAR', hongKongSar: 'Hong Kong SAR',
@@ -152,7 +155,7 @@ export default {
title: 'My Wardrobe', title: 'My Wardrobe',
subtitle: 'Your digital pieces, all in one place', subtitle: 'Your digital pieces, all in one place',
common: { common: {
all: 'All', all: 'All'
}, },
tabs: { tabs: {
ariaLabel: 'Wardrobe tabs', ariaLabel: 'Wardrobe tabs',
@@ -231,7 +234,7 @@ export default {
PrivacyPolicy: 'Privacy Policy', PrivacyPolicy: 'Privacy Policy',
TermsOfUse: 'Terms of Use', TermsOfUse: 'Terms of Use',
Disclaimer: 'Disclaimer', Disclaimer: 'Disclaimer',
SiteMap: 'Site Map', SiteMap: 'Site Map'
}, },
brand: { brand: {
title: 'Brand', title: 'Brand',
@@ -241,16 +244,16 @@ export default {
noFoundTip: 'Try using another keywords.', noFoundTip: 'Try using another keywords.',
searchHistory: 'Searching History', searchHistory: 'Searching History',
brandItem: { brandItem: {
viewProfile: 'View Profile', viewProfile: 'View Profile'
} }
}, },
brandDetail: { brandDetail: {
addShoppingTip: 'Please log in first.', addShoppingTip: 'Please log in first.',
merchantInfo: { merchantInfo: {
Contact: 'Contact', Contact: 'Contact',
About: 'About', About: 'About'
}, },
All: 'All', All: 'All'
}, },
digitalItem: { digitalItem: {
BestSelling: 'Best Selling', BestSelling: 'Best Selling',
@@ -267,11 +270,11 @@ export default {
Filters: 'Filters', Filters: 'Filters',
Clear: 'Clear', Clear: 'Clear',
Categories: 'Categories', Categories: 'Categories',
Gender: 'Gender', Gender: 'Gender'
} }
}, },
checked: { checked: {
All: 'All', All: 'All'
}, },
MainHeader: { MainHeader: {
Home: 'Home', Home: 'Home',
@@ -281,7 +284,7 @@ export default {
HiName: 'Hi, {name}', HiName: 'Hi, {name}',
MyWardrobe: 'My Wardrobe', MyWardrobe: 'My Wardrobe',
Notifications: 'Notifications', Notifications: 'Notifications',
Settings: 'Settings', Settings: 'Settings'
}, },
ShoppingCart: { ShoppingCart: {
title: 'Shopping Cart', title: 'Shopping Cart',
@@ -300,7 +303,7 @@ export default {
selected: 'Selected', selected: 'Selected',
brands: 'Brands', brands: 'Brands',
item: 'item', item: 'item',
checkoutSelected: 'Checkout Selected', checkoutSelected: 'Checkout Selected'
}, },
digitalDetail: { digitalDetail: {
Sketch: 'Sketch', Sketch: 'Sketch',
@@ -311,13 +314,30 @@ export default {
ReleaseIn: 'Release in', ReleaseIn: 'Release in',
CopyrightLicenseNotice: 'Copyright & License Notice', CopyrightLicenseNotice: 'Copyright & License Notice',
LicenseIncludedInAsset: 'License Included in Asset', LicenseIncludedInAsset: 'License Included in Asset',
LicenseIncludedInAssetInfo: 'All products on this platform are digital assets, not physical goods. Purchase grants a usage license only; copyright and intellectual property rights remain with the original creator, unless otherwise stated.', LicenseIncludedInAssetInfo:
'All products on this platform are digital assets, not physical goods. Purchase grants a usage license only; copyright and intellectual property rights remain with the original creator, unless otherwise stated.',
BuyNow: 'Buy Now', BuyNow: 'Buy Now',
AddToCart: 'Add to Cart', AddToCart: 'Add to Cart'
}, },
addShoppingCart: { addShoppingCart: {
title: 'Added to your Shopping Cart', title: 'Added to your Shopping Cart',
statement: 'Digital Assets Only. No physical product included.', statement: 'Digital Assets Only. No physical product included.',
button: 'Set Shopping Cart', button: 'Set Shopping Cart'
},
area: {
chinaMainland: 'China Mainland',
hongKongSar: 'Hong Kong SAR',
macauSar: 'Macau SAR',
taiwan: 'Taiwan',
japan: 'Japan',
southKorea: 'South Korea',
singapore: 'Singapore',
unitedStates: 'United States',
unitedKingdom: 'United Kingdom',
france: 'France',
italy: 'Italy',
germany: 'Germany',
australia: 'Australia',
canada: 'Canada'
} }
} }

View File

@@ -46,7 +46,7 @@ export default {
submit: '提交', submit: '提交',
enterNewPassword: '请输入新密码<br/><span>{email}</span>', enterNewPassword: '请输入新密码<br/><span>{email}</span>',
passwordsDoNotMatch: '两次输入密码不一致', passwordsDoNotMatch: '两次输入密码不一致',
logOffTip: '确定退出登录吗?', logOffTip: '确定退出登录吗?'
}, },
RegisterSuccess: { RegisterSuccess: {
title1: '欢迎来到 Stylish Parade', title1: '欢迎来到 Stylish Parade',
@@ -57,7 +57,7 @@ export default {
item2title: '创意数字作品', item2title: '创意数字作品',
item2tip: '解锁一个增长的数字作品库,刷新你的创意。', item2tip: '解锁一个增长的数字作品库,刷新你的创意。',
item3title: '时尚社区', item3title: '时尚社区',
item3tip: '加入一个全球的时尚社区,与设计师分享创意。', item3tip: '加入一个全球的时尚社区,与设计师分享创意。'
}, },
Settings: { Settings: {
title: '设置', title: '设置',
@@ -73,7 +73,7 @@ export default {
usernamePlaceholder: '请输入用户名', usernamePlaceholder: '请输入用户名',
usernameTip: '这是你在 Stylish Parade 上公开显示的用户名。', usernameTip: '这是你在 Stylish Parade 上公开显示的用户名。',
role: '身份标签', role: '身份标签',
roleTip: '最多选择 2 个符合你的标签。', roleTip: '最多选择 2 个符合你的标签。'
}, },
security: { security: {
title: '安全', title: '安全',
@@ -139,7 +139,7 @@ export default {
}, },
languages: { languages: {
english: '英文', english: '英文',
chinese: '中文', chinese: '中文'
}, },
regions: { regions: {
hongKongSar: '中国香港特别行政区', hongKongSar: '中国香港特别行政区',
@@ -152,7 +152,7 @@ export default {
title: '我的衣橱', title: '我的衣橱',
subtitle: '你的数字单品尽在此处', subtitle: '你的数字单品尽在此处',
common: { common: {
all: '全部', all: '全部'
}, },
tabs: { tabs: {
ariaLabel: '衣橱标签页', ariaLabel: '衣橱标签页',
@@ -216,14 +216,14 @@ export default {
}, },
collectionStory: { collectionStory: {
back: '返回首页', back: '返回首页',
title: "我们在寻找", title: '我们在寻找',
description: "值得被听见的时尚之声", description: '值得被听见的时尚之声',
button: "如有兴趣,请联系我们", button: '如有兴趣,请联系我们',
joinUs: { joinUs: {
title: '加入我们的设计师社区,', title: '加入我们的设计师社区,',
info: "加入我们的远见者社区,发表你的系列故事。", info: '加入我们的远见者社区,发表你的系列故事。',
info2: "我们目前正在寻找深度整合 AiDA 创意工作流程的系列作品,特别是那些通过强大的核心理念和富有感染力的灵感而产生共鸣的作品。", info2: '我们目前正在寻找深度整合 AiDA 创意工作流程的系列作品,特别是那些通过强大的核心理念和富有感染力的灵感而产生共鸣的作品。',
info3: "这一架构旨在通过深刻的‘命题式表达’提升你的曝光度,确保那些有灵魂、由故事驱动的设计能获得更高的市场溢价和卓越的销售转化率。" info3: '这一架构旨在通过深刻的‘命题式表达’提升你的曝光度,确保那些有灵魂、由故事驱动的设计能获得更高的市场溢价和卓越的销售转化率。'
} }
}, },
footer: { footer: {
@@ -231,47 +231,47 @@ export default {
PrivacyPolicy: '隐私政策', PrivacyPolicy: '隐私政策',
TermsOfUse: '条款与条件', TermsOfUse: '条款与条件',
Disclaimer: '免责声明', Disclaimer: '免责声明',
SiteMap: '地图', SiteMap: '地图'
}, },
brand: { brand: {
title: "品牌", title: '品牌',
description: "每一个品牌,每一个故事 — 发现系列作品背后的缔造者。", description: '每一个品牌,每一个故事 — 发现系列作品背后的缔造者。',
search: "搜索品牌", search: '搜索品牌',
noFound: "未找到品牌", noFound: '未找到品牌',
noFoundTip: "请尝试使用其他关键词。", noFoundTip: '请尝试使用其他关键词。',
searchHistory: "搜索历史", searchHistory: '搜索历史',
brandItem: { brandItem: {
viewProfile: "查看简介" viewProfile: '查看简介'
} }
}, },
brandDetail: { brandDetail: {
addShoppingTip: "请先登录。", addShoppingTip: '请先登录。',
merchantInfo: { merchantInfo: {
Contact: "联系方式", Contact: '联系方式',
About: "关于我们" About: '关于我们'
}, },
All: "全部" All: '全部'
}, },
digitalItem: { digitalItem: {
BestSelling: "畅销优先", BestSelling: '畅销优先',
Price: "价格:从低到高", Price: '价格:从低到高',
SelectedFirst: "已选优先", SelectedFirst: '已选优先',
DateAdded: "添加日期", DateAdded: '添加日期',
NewestFirst: "最新优先", NewestFirst: '最新优先',
title: "数字藏品", title: '数字藏品',
info: "收藏于个人档案中的虚拟时装作品", info: '收藏于个人档案中的虚拟时装作品',
sortBy: "排序方式", sortBy: '排序方式',
noData: "暂无数字藏品", noData: '暂无数字藏品',
noDataTip: "请尝试调整筛选条件或刷新页面。", noDataTip: '请尝试调整筛选条件或刷新页面。',
MerchantInfo: { MerchantInfo: {
Filters: "筛选", Filters: '筛选',
Clear: "清空", Clear: '清空',
Categories: "分类", Categories: '分类',
Gender: "适用性别" Gender: '适用性别'
} }
}, },
checked: { checked: {
All: "全部" All: '全部'
}, },
MainHeader: { MainHeader: {
Home: '首页', Home: '首页',
@@ -281,7 +281,7 @@ export default {
HiName: '你好,{name}', HiName: '你好,{name}',
MyWardrobe: '我的衣橱', MyWardrobe: '我的衣橱',
Notifications: '通知', Notifications: '通知',
Settings: '设置', Settings: '设置'
}, },
ShoppingCart: { ShoppingCart: {
title: '购物车', title: '购物车',
@@ -300,24 +300,41 @@ export default {
selected: '已选', selected: '已选',
brands: '品牌', brands: '品牌',
item: '数字藏品', item: '数字藏品',
checkoutSelected: '结账已选', checkoutSelected: '结账已选'
}, },
digitalDetail: { digitalDetail: {
Sketch: "草图", Sketch: '草图',
Illustration: "插画", Illustration: '插画',
Product: "产品", Product: '产品',
EditorialVisual: "编辑视觉", EditorialVisual: '编辑视觉',
Back: "返回", Back: '返回',
ReleaseIn: "发布于", ReleaseIn: '发布于',
CopyrightLicenseNotice: "版权与许可声明", CopyrightLicenseNotice: '版权与许可声明',
LicenseIncludedInAsset: "资产包含许可", LicenseIncludedInAsset: '资产包含许可',
LicenseIncludedInAssetInfo: "本平台所有产品均为数字资产,非实物商品。购买仅授予使用许可;版权及知识产权仍归原作者所有,除非另有说明。", LicenseIncludedInAssetInfo:
BuyNow: "立即购买", '本平台所有产品均为数字资产,非实物商品。购买仅授予使用许可;版权及知识产权仍归原作者所有,除非另有说明。',
AddToCart: "加入购物车" BuyNow: '立即购买',
AddToCart: '加入购物车'
}, },
addShoppingCart: { addShoppingCart: {
title: "已添加到您的购物车", title: '已添加到您的购物车',
statement: "仅限数字资产。不包含实体产品。", statement: '仅限数字资产。不包含实体产品。',
button: "去购物车" button: '去购物车'
},
area: {
chinaMainland: '中国大陆',
hongKongSar: '中国香港特别行政区',
macauSar: '中国澳门特别行政区',
taiwan: '中国台湾',
japan: '日本',
southKorea: '韩国',
singapore: '新加坡',
unitedStates: '美国',
unitedKingdom: '英国',
france: '法国',
italy: '意大利',
germany: '德国',
australia: '澳大利亚',
canada: '加拿大'
} }
} }

86
src/utils/area.ts Normal file
View File

@@ -0,0 +1,86 @@
export default [
{
key: 'chinaMainland',
name: 'China Mainland',
label: 'China Mainland',
value: 'China Mainland'
},
{
key: 'hongKongSar',
name: 'Hong Kong SAR',
label: 'Hong Kong SAR',
value: 'Hong Kong SAR'
},
{
key: 'macauSar',
name: 'Macau SAR',
label: 'Macau SAR',
value: 'Macau SAR'
},
{
key: 'taiwan',
name: 'Taiwan',
label: 'Taiwan',
value: 'Taiwan'
},
{
key: 'japan',
name: 'Japan',
label: 'Japan',
value: 'Japan'
},
{
key: 'southKorea',
name: 'South Korea',
label: 'South Korea',
value: 'South Korea'
},
{
key: 'singapore',
name: 'Singapore',
label: 'Singapore',
value: 'Singapore'
},
{
key: 'unitedStates',
name: 'United States',
label: 'United States',
value: 'United States'
},
{
key: 'unitedKingdom',
name: 'United Kingdom',
label: 'United Kingdom',
value: 'United Kingdom'
},
{
key: 'france',
name: 'France',
label: 'France',
value: 'France'
},
{
key: 'italy',
name: 'Italy',
label: 'Italy',
value: 'Italy'
},
{
key: 'germany',
name: 'Germany',
label: 'Germany',
value: 'Germany'
},
{
key: 'australia',
name: 'Australia',
label: 'Australia',
value: 'Australia'
},
{
key: 'canada',
name: 'Canada',
label: 'Canada',
value: 'Canada'
}
]

View File

@@ -65,6 +65,16 @@ service.interceptors.response.use(
if (response.config.url.includes('llm/streamChat')) { if (response.config.url.includes('llm/streamChat')) {
return response return response
} }
// 如果是二进制下载blob/arraybuffer直接返回原始 response 以便调用方处理文件
if (
response.config.responseType === 'blob' ||
response.config.responseType === 'arraybuffer' ||
response.headers['content-type'] === 'application/octet-stream'
) {
removePending(response.config)
if (response.config.loading) closeLoading()
return response
}
// 已完成请求的删除请求中数组 // 已完成请求的删除请求中数组
removePending(response.config) removePending(response.config)

View File

@@ -27,7 +27,7 @@
<div class="region-row"> <div class="region-row">
<div class="security-label">{{ t('Settings.region.region') }}</div> <div class="security-label">{{ t('Settings.region.region') }}</div>
<div v-show="!isEditing" class="security-static field-box"> <div v-show="!isEditing" class="security-static field-box">
{{ displayRegionLabel }} {{ t(`area.${displayRegionLabel}`) }}
</div> </div>
<div v-show="isEditing" class="outlined-field select-field"> <div v-show="isEditing" class="outlined-field select-field">
<el-select <el-select
@@ -38,7 +38,7 @@
<el-option <el-option
v-for="item in regionOptions" v-for="item in regionOptions"
:key="item.value" :key="item.value"
:label="item.label" :label="t(`area.${item.key}`)"
:value="item.value" :value="item.value"
/> />
</el-select> </el-select>

View File

@@ -23,14 +23,14 @@
:placeholder="t('Settings.security.newEmailPlaceholder')" :placeholder="t('Settings.security.newEmailPlaceholder')"
@update:model-value="emit('update:newEmail', String($event))" @update:model-value="emit('update:newEmail', String($event))"
/> />
<button <!-- <button
type="button" type="button"
class="verify-btn" class="verify-btn"
:class="{ verified: isEmailVerified }" :class="{ verified: isEmailVerified }"
@click="emit('verify-email')" @click="emit('verify-email')"
> >
{{ 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') }}

View File

@@ -68,6 +68,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import EmailVerificationDialog from './components/EmailVerificationDialog.vue' import EmailVerificationDialog from './components/EmailVerificationDialog.vue'
import ProfileSection from './components/ProfileSection.vue' import ProfileSection from './components/ProfileSection.vue'
@@ -102,8 +103,13 @@ const {
handleVerifyEmail, handleVerifyEmail,
handleSendVerifyCode, handleSendVerifyCode,
handleVerificationSubmit, handleVerificationSubmit,
closeVerificationDialog closeVerificationDialog,
loadUserProfile
} = useSettingsForm({ t, locale }) } = useSettingsForm({ t, locale })
onMounted(() => {
loadUserProfile()
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -13,20 +13,19 @@ export const roleValues = [
] as const ] as const
export const languageValues = ['english', 'chinese'] as const export const languageValues = ['english', 'chinese'] as const
export const regionValues = ['hongKongSar', 'mainlandChina', 'singapore', 'unitedKingdom'] as const
export type RoleValue = (typeof roleValues)[number] export type RoleValue = (typeof roleValues)[number]
export type LanguageValue = (typeof languageValues)[number] export type LanguageValue = (typeof languageValues)[number]
export type RegionValue = (typeof regionValues)[number]
export interface SettingsData { export interface SettingsData {
firstName: string firstName: string
lastName: string lastName: string
email: string email: string
username: string username: string
role: RoleValue[] roles: RoleValue[]
language: LanguageValue language: LanguageValue | ''
region: RegionValue region: string | ''
} }
export interface SecurityDraft { export interface SecurityDraft {

View File

@@ -1,11 +1,11 @@
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 regionList from '@/utils/area'
import { import {
languageValues, languageValues,
regionValues,
roleValues, roleValues,
type LanguageValue, type LanguageValue,
type RegionValue,
type RoleValue, type RoleValue,
type SecurityDraft, type SecurityDraft,
type SettingsData type SettingsData
@@ -26,13 +26,13 @@ const languageLocaleMap: Record<LanguageValue, 'ENGLISH' | 'CHINESE_SIMPLIFIED'>
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const createDefaultData = (): SettingsData => ({ const createDefaultData = (): SettingsData => ({
firstName: 'Alexandra', firstName: '',
lastName: 'Chen', lastName: '',
email: 'alex.chen@gmail.com', email: '',
username: '@alexandra_chen', username: '',
role: ['student', 'graphicDesigner'], roles: [] as RoleValue[],
language: 'english', language: '',
region: 'hongKongSar' region: ''
}) })
const cloneSettingsData = (data: SettingsData): SettingsData => ({ const cloneSettingsData = (data: SettingsData): SettingsData => ({
@@ -40,11 +40,30 @@ const cloneSettingsData = (data: SettingsData): SettingsData => ({
lastName: data.lastName, lastName: data.lastName,
email: data.email, email: data.email,
username: data.username, username: data.username,
role: [...data.role], roles: [...data.roles],
language: data.language, language: data.language,
region: data.region region: data.region
}) })
const normalizeLanguage = (language: string | null | undefined): LanguageValue => {
if (!language) {
return '' as LanguageValue
}
const normalized = language.trim().toLowerCase()
return normalized.includes('chinese') ? 'chinese' : 'english'
}
const buildSettingsDataFromProfile = (profile: Partial<UserProfile>): SettingsData => ({
firstName: profile.firstName || '',
lastName: profile.lastName || '',
username: profile.username || '',
email: profile.email || '',
roles: profile.roles || [],
language: normalizeLanguage(profile.language),
region: profile.region
})
const createEmptySecurityDraft = (): SecurityDraft => ({ const createEmptySecurityDraft = (): SecurityDraft => ({
newEmail: '', newEmail: '',
newPassword: '', newPassword: '',
@@ -75,23 +94,27 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
})) }))
) )
const regionList = computed(() =>
regionValues.map((value) => ({
label: t(`Settings.regions.${value}`),
value
}))
)
const displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value)) const displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value))
const normalizedNewEmail = computed(() => securityDraft.value.newEmail.trim()) const normalizedNewEmail = computed(() => securityDraft.value.newEmail.trim())
const hasNewEmailChange = computed( const hasNewEmailChange = computed(
() => normalizedNewEmail.value.length > 0 && normalizedNewEmail.value !== sourceData.value.email () =>
normalizedNewEmail.value.length > 0 &&
normalizedNewEmail.value !== sourceData.value.email
) )
const isEmailVerified = computed( const isEmailVerified = computed(
() => hasNewEmailChange.value && verifiedEmail.value === normalizedNewEmail.value () => hasNewEmailChange.value && verifiedEmail.value === normalizedNewEmail.value
) )
const displayLanguageLabel = computed(() => t(`Settings.languages.${displayData.value.language}`)) const displayLanguageLabel = computed(() =>
const displayRegionLabel = computed(() => t(`Settings.regions.${displayData.value.region}`)) displayData.value.language ? t(`Settings.languages.${displayData.value.language}`) : ''
)
const displayRegionLabel = computed(() => {
if (displayData.value.region) {
const regionItem = regionList.find((item) => item.value === displayData.value.region)
return regionItem.key
} else {
return ''
}
})
const fullName = computed(() => { const fullName = computed(() => {
const data = displayData.value const data = displayData.value
@@ -99,14 +122,14 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
}) })
const roleModel = computed<RoleValue[]>({ const roleModel = computed<RoleValue[]>({
get: () => displayData.value.role, get: () => displayData.value.roles,
set: (value) => { set: (value) => {
if (isEditing.value) { if (isEditing.value) {
draftData.value.role = value draftData.value.roles = value
return return
} }
sourceData.value.role = value sourceData.value.roles = value
} }
}) })
@@ -122,6 +145,21 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
localStorage.setItem('language', nextLocale) localStorage.setItem('language', nextLocale)
} }
const loadUserProfile = async () => {
try {
const profile = (await fetchUserProfile()) as Partial<UserProfile>
const nextData = buildSettingsDataFromProfile(profile)
sourceData.value = cloneSettingsData(nextData)
draftData.value = cloneSettingsData(sourceData.value)
if (sourceData.value.language) {
syncAppLanguage(sourceData.value.language as LanguageValue)
}
} catch (error) {
console.warn(error)
}
}
const resetDraftState = () => { const resetDraftState = () => {
draftData.value = cloneSettingsData(sourceData.value) draftData.value = cloneSettingsData(sourceData.value)
securityDraft.value = createEmptySecurityDraft() securityDraft.value = createEmptySecurityDraft()
@@ -178,11 +216,17 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
verificationTargetEmail.value = nextEmail verificationTargetEmail.value = nextEmail
handleSendVerifyCode() handleSendVerifyCode()
isVerificationDialogVisible.value = true
} }
const handleSendVerifyCode = () => { const handleSendVerifyCode = () => {
ElMessage.success(t('Settings.messages.verificationCodeSent')) // AccountSendVerifyCode({
// email: verificationTargetEmail.value,
// operationType: 'FORGET_PWD'
// }).then((res) => {
// console.log(res)
// ElMessage.success(t('Settings.messages.verificationCodeSent'))
// isVerificationDialogVisible.value = true
// })
} }
const handleVerificationSubmit = (code: string) => { const handleVerificationSubmit = (code: string) => {
@@ -204,7 +248,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
lastName: draftData.value.lastName.trim(), lastName: draftData.value.lastName.trim(),
username: draftData.value.username.trim(), username: draftData.value.username.trim(),
email: nextEmail, email: nextEmail,
role: [...draftData.value.role], roles: [...draftData.value.roles],
language: draftData.value.language, language: draftData.value.language,
region: draftData.value.region region: draftData.value.region
} }
@@ -221,10 +265,12 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
saving.value = true saving.value = true
try { try {
await updateUserProfile(nextData)
sourceData.value = cloneSettingsData(nextData) sourceData.value = cloneSettingsData(nextData)
console.log(nextData)
if (nextData.language !== previousLanguage) { if (nextData.language && nextData.language !== previousLanguage) {
syncAppLanguage(nextData.language) syncAppLanguage(nextData.language as LanguageValue)
} }
draftData.value = cloneSettingsData(sourceData.value) draftData.value = cloneSettingsData(sourceData.value)
@@ -283,6 +329,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
handleVerifyEmail, handleVerifyEmail,
handleSendVerifyCode, handleSendVerifyCode,
handleVerificationSubmit, handleVerificationSubmit,
closeVerificationDialog closeVerificationDialog,
loadUserProfile
} }
} }

View File

@@ -34,6 +34,7 @@
<div class="amount" v-show="!disabled">${{ info.amount }}<span> HKD</span></div> <div class="amount" v-show="!disabled">${{ info.amount }}<span> HKD</span></div>
<SvgIcon <SvgIcon
v-if="showDownload" v-if="showDownload"
@click.stop="emit('download', info)"
class="download" class="download"
name="download" name="download"
size="32" size="32"
@@ -52,7 +53,7 @@
import { FormatDate } from '@/utils/tools' import { FormatDate } from '@/utils/tools'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
const emit = defineEmits(['remove']) const emit = defineEmits(['remove', 'download'])
const props = defineProps({ const props = defineProps({
showTags: { type: Boolean, default: true }, showTags: { type: Boolean, default: true },
showDate: { type: Boolean, default: true }, showDate: { type: Boolean, default: true },

View File

@@ -31,6 +31,7 @@
<div <div
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"
@click="handleDownloadSelected" @click="handleDownloadSelected"
> >
<SvgIcon name="downloadBtn" color="#fff" /> <SvgIcon name="downloadBtn" color="#fff" />
@@ -66,6 +67,7 @@
download download
:url="item.thumbnailUrl" :url="item.thumbnailUrl"
:name="item.listingName" :name="item.listingName"
@download.stop="handleDownloadSelected(item)"
:showPrice="false" :showPrice="false"
></CommodityItem> ></CommodityItem>
</div> </div>
@@ -92,7 +94,7 @@
import { useClothesCategories } from '@/utils/ClothesCategory' import { useClothesCategories } from '@/utils/ClothesCategory'
import Empty from './Empty.vue' import Empty from './Empty.vue'
import FilterSidebar from './FilterSidebar.vue' import FilterSidebar from './FilterSidebar.vue'
import { fetchMyWardrobe } from '@/api/user' import { fetchMyWardrobe, fetchDownloadItemsByGet } from '@/api/user'
import { useUserInfoStore } from '@/stores' import { useUserInfoStore } from '@/stores'
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
@@ -320,9 +322,37 @@
} }
} }
const handleDownloadSelected = () => { const downloadingSelected = ref(false)
const items = dataList.value.filter((item) => item.checked) const handleDownloadSelected = (assets) => {
console.log(items) const items = assets ? [assets] : dataList.value.filter((item) => item.checked)
downloadingSelected.value = true
const ids = items.map((item) => item.listingId)
fetchDownloadItemsByGet({ ids })
.then((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)
})
.catch((error) => {
console.error('Download failed:', error)
}).finally(() => {
downloadingSelected.value = false
})
} }
const handleAssetsScroll = () => { const handleAssetsScroll = () => {

View File

@@ -84,6 +84,7 @@
:show-brand="false" :show-brand="false"
is-order is-order
order-actions-layout order-actions-layout
@download="handleDownload(order)"
/> />
</div> </div>
</article> </article>
@@ -96,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 } 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'
@@ -181,14 +182,10 @@
return t('Wardrobe.orders.actions.buyAgain') return t('Wardrobe.orders.actions.buyAgain')
} }
const getOrderStatusValue = (status: unknown): ActualOrderStatus => {
if (status === 0 || status === '0' || status === 'unpaid') return 'unpaid'
if (status === 2 || status === '2' || status === 'cancelled') return 'cancelled'
return 'paid'
}
const getOrderStatus = (order: OrderRecord) => { const getOrderStatus = (order: OrderRecord) => {
return getOrderStatusValue(order.status) if (Number(order.status) === 0) return 'unpaid'
if (Number(order.status) === 2) return 'cancelled'
if (Number(order.status) === 1) return 'paid'
} }
const formatOrderUpdateTime = (dateStr: string) => { const formatOrderUpdateTime = (dateStr: string) => {
@@ -221,7 +218,7 @@
} }
const getOrderItemInfo = (item: OrderItem, order: OrderRecord) => ({ const getOrderItemInfo = (item: OrderItem, order: OrderRecord) => ({
status: item.status, status: order.status,
title: item.listingName, title: item.listingName,
brand: order.shopName, brand: order.shopName,
tags: item.productCategory, tags: item.productCategory,
@@ -230,6 +227,43 @@
cover: item.thumbnailUrl cover: item.thumbnailUrl
}) })
const resetOrders = () => {
ordersRequestId.value += 1
orders.value = []
expandedOrderId.value = ''
isLoadingOrders.value = false
hasMoreOrders.value = true
orderParams.value.page = 1
if (ordersScrollRef.value) {
ordersScrollRef.value.scrollTop = 0
}
}
const setActiveStatus = (status: OrderStatus) => {
activeStatus.value = status
resetOrders()
fetchAllOrders()
}
const handleOrdersScroll = () => {
const el = ordersScrollRef.value
if (!el) return
const reachBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 120
if (reachBottom) {
fetchAllOrders()
}
}
const handleDownload = (order) => {
console.log(order)
}
const handleRouteBrand = (order: OrderRecord) => {
ROUTER.push(`/brand/${order.sellerId}`)
}
const fetchAllOrders = async () => { const fetchAllOrders = async () => {
if (isLoadingOrders.value || !hasMoreOrders.value) return if (isLoadingOrders.value || !hasMoreOrders.value) return
@@ -267,39 +301,6 @@
} }
} }
const resetOrders = () => {
ordersRequestId.value += 1
orders.value = []
expandedOrderId.value = ''
isLoadingOrders.value = false
hasMoreOrders.value = true
orderParams.value.page = 1
if (ordersScrollRef.value) {
ordersScrollRef.value.scrollTop = 0
}
}
const setActiveStatus = (status: OrderStatus) => {
activeStatus.value = status
resetOrders()
fetchAllOrders()
}
const handleOrdersScroll = () => {
const el = ordersScrollRef.value
if (!el) return
const reachBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 120
if (reachBottom) {
fetchAllOrders()
}
}
const handleRouteBrand = (order: OrderRecord) => {
ROUTER.push(`/brand/${order.sellerId}`)
}
onMounted(() => { onMounted(() => {
fetchAllOrders() fetchAllOrders()
}) })