From a4861da21abfa4ff5e81385afc2ab7b7dbd581d0 Mon Sep 17 00:00:00 2001 From: zhangyahui Date: Wed, 27 May 2026 17:29:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B5=84=E6=96=99=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/user.ts | 96 +++- src/lang/en.ts | 72 ++- src/lang/zh-cn.ts | 135 ++--- src/utils/area.ts | 86 +++ src/utils/request.ts | 10 + .../setting/components/RegionSection.vue | 4 +- .../setting/components/SecuritySection.vue | 4 +- src/views/setting/index.vue | 272 +++++----- src/views/setting/types.ts | 9 +- src/views/setting/useSettingsForm.ts | 501 ++++++++++-------- src/views/shoppingCart/sc-item.vue | 3 +- src/views/wardrobe/Assets.vue | 38 +- src/views/wardrobe/Orders.vue | 85 +-- 13 files changed, 794 insertions(+), 521 deletions(-) create mode 100644 src/utils/area.ts diff --git a/src/api/user.ts b/src/api/user.ts index ae2967d..986aa49 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,34 +1,90 @@ import request from '@/utils/request' export interface WardrobeItem { - buyerId: number - categories: string[] - designFor: 'female' | 'male' | 'all' - page: number - size: number + buyerId: number + categories: string[] + designFor: 'female' | 'male' | 'all' + page: number + size: number } +// 获取我的衣橱assets export const fetchMyWardrobe = (data: WardrobeItem): Promise => { - return request({ - url: '/buyer/buyer/order/assets/page', - method: 'post', - data - }) + return request({ + url: '/buyer/buyer/order/assets/page', + method: 'post', + data + }) } export interface OrderItem { - status?: number // 0未支付 1已支付 2已取消 不传查全部 - page: number - size: number + status?: number // 0未支付 1已支付 2已取消 不传查全部 + page: number + size: number } export interface OrdersPageResponse { - content: any[] + content: any[] +} +// 获取我的衣橱 orders +export const fetchMyOrders = (data: OrderItem): Promise => { + return request({ + url: '/buyer/buyer/order/page', + method: 'get', + params: data + }) } -export const fetchMyOrders = (data: OrderItem): Promise => { - return request({ - url: '/buyer/buyer/order/page', - method: 'get', - params: data - }) +export interface Download { + ids: string[] +} +// 下载资源 +export const fetchDownloadItemsByGet = (params: Download): Promise => { + 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 => { + return request({ + url: '/buyer/profile/getProfile', + method: 'post' + }) +} + +// 设置用户信息 +export const updateUserProfile = (data: UserProfile): Promise => { + return request({ + url: '/buyer/profile/setProfile', + method: 'post', + data + }) } diff --git a/src/lang/en.ts b/src/lang/en.ts index d7e3b4f..f081c3f 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -46,18 +46,21 @@ export default { submit: 'Submit', enterNewPassword: 'Enter a new password for
{email}', passwordsDoNotMatch: 'Passwords do not match', - logOffTip: 'Are you sure to log off?', + logOffTip: 'Are you sure to log off?' }, RegisterSuccess: { title1: 'Welcome to Stylish Parade!', title2: 'Please switch to the Login tab to log in.', title3: 'What awaits you in Stylish Parade', 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', - 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', - 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: { title: 'Settings', @@ -73,7 +76,7 @@ export default { usernamePlaceholder: 'Username', usernameTip: 'Your public username on Stylish Parade.', role: 'ROLE', - roleTip: 'Select up to 2 labels that suit you.', + roleTip: 'Select up to 2 labels that suit you.' }, security: { title: 'Security', @@ -139,7 +142,7 @@ export default { }, languages: { english: 'English', - chinese: 'Chinese', + chinese: 'Chinese' }, regions: { hongKongSar: 'Hong Kong SAR', @@ -152,7 +155,7 @@ export default { title: 'My Wardrobe', subtitle: 'Your digital pieces, all in one place', common: { - all: 'All', + all: 'All' }, tabs: { ariaLabel: 'Wardrobe tabs', @@ -226,33 +229,33 @@ export default { info3: 'This architecture is designed to elevate your exposure through profound "propositional expression," ensuring that soulful, story-driven designs achieve higher market premiums and superior sales conversion.' } }, - footer:{ + footer: { About: 'About', PrivacyPolicy: 'Privacy Policy', TermsOfUse: 'Terms of Use', Disclaimer: 'Disclaimer', - SiteMap: 'Site Map', + SiteMap: 'Site Map' }, - brand:{ + brand: { title: 'Brand', description: "Every brand, every story — discover who's behind the collections.", search: 'Search brand', noFound: 'Brand No Found', noFoundTip: 'Try using another keywords.', searchHistory: 'Searching History', - brandItem:{ - viewProfile: 'View Profile', + brandItem: { + viewProfile: 'View Profile' } }, - brandDetail:{ + brandDetail: { addShoppingTip: 'Please log in first.', merchantInfo: { Contact: 'Contact', - About: 'About', + About: 'About' }, - All: 'All', + All: 'All' }, - digitalItem:{ + digitalItem: { BestSelling: 'Best Selling', Price: 'Price: Low to High', SelectedFirst: 'Selected First', @@ -267,11 +270,11 @@ export default { Filters: 'Filters', Clear: 'Clear', Categories: 'Categories', - Gender: 'Gender', + Gender: 'Gender' } }, checked: { - All: 'All', + All: 'All' }, MainHeader: { Home: 'Home', @@ -281,7 +284,7 @@ export default { HiName: 'Hi, {name}', MyWardrobe: 'My Wardrobe', Notifications: 'Notifications', - Settings: 'Settings', + Settings: 'Settings' }, ShoppingCart: { title: 'Shopping Cart', @@ -300,9 +303,9 @@ export default { selected: 'Selected', brands: 'Brands', item: 'item', - checkoutSelected: 'Checkout Selected', + checkoutSelected: 'Checkout Selected' }, - digitalDetail:{ + digitalDetail: { Sketch: 'Sketch', Illustration: 'Illustration', Product: 'Product', @@ -311,13 +314,30 @@ export default { ReleaseIn: 'Release in', CopyrightLicenseNotice: 'Copyright & License Notice', 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', - AddToCart: 'Add to Cart', + AddToCart: 'Add to Cart' }, - addShoppingCart:{ + addShoppingCart: { title: 'Added to your Shopping Cart', 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' } -} \ No newline at end of file +} diff --git a/src/lang/zh-cn.ts b/src/lang/zh-cn.ts index 8958ee7..b77df76 100644 --- a/src/lang/zh-cn.ts +++ b/src/lang/zh-cn.ts @@ -46,7 +46,7 @@ export default { submit: '提交', enterNewPassword: '请输入新密码
{email}', passwordsDoNotMatch: '两次输入密码不一致', - logOffTip: '确定退出登录吗?', + logOffTip: '确定退出登录吗?' }, RegisterSuccess: { title1: '欢迎来到 Stylish Parade!', @@ -57,7 +57,7 @@ export default { item2title: '创意数字作品', item2tip: '解锁一个增长的数字作品库,刷新你的创意。', item3title: '时尚社区', - item3tip: '加入一个全球的时尚社区,与设计师分享创意。', + item3tip: '加入一个全球的时尚社区,与设计师分享创意。' }, Settings: { title: '设置', @@ -73,7 +73,7 @@ export default { usernamePlaceholder: '请输入用户名', usernameTip: '这是你在 Stylish Parade 上公开显示的用户名。', role: '身份标签', - roleTip: '最多选择 2 个符合你的标签。', + roleTip: '最多选择 2 个符合你的标签。' }, security: { title: '安全', @@ -139,7 +139,7 @@ export default { }, languages: { english: '英文', - chinese: '中文', + chinese: '中文' }, regions: { hongKongSar: '中国香港特别行政区', @@ -152,7 +152,7 @@ export default { title: '我的衣橱', subtitle: '你的数字单品尽在此处', common: { - all: '全部', + all: '全部' }, tabs: { ariaLabel: '衣橱标签页', @@ -216,62 +216,62 @@ export default { }, collectionStory: { back: '返回首页', - title: "我们在寻找", - description: "值得被听见的时尚之声", - button: "如有兴趣,请联系我们", + title: '我们在寻找', + description: '值得被听见的时尚之声', + button: '如有兴趣,请联系我们', joinUs: { title: '加入我们的设计师社区,', - info: "加入我们的远见者社区,发表你的系列故事。", - info2: "我们目前正在寻找深度整合 AiDA 创意工作流程的系列作品,特别是那些通过强大的核心理念和富有感染力的灵感而产生共鸣的作品。", - info3: "这一架构旨在通过深刻的‘命题式表达’提升你的曝光度,确保那些有灵魂、由故事驱动的设计能获得更高的市场溢价和卓越的销售转化率。" + info: '加入我们的远见者社区,发表你的系列故事。', + info2: '我们目前正在寻找深度整合 AiDA 创意工作流程的系列作品,特别是那些通过强大的核心理念和富有感染力的灵感而产生共鸣的作品。', + info3: '这一架构旨在通过深刻的‘命题式表达’提升你的曝光度,确保那些有灵魂、由故事驱动的设计能获得更高的市场溢价和卓越的销售转化率。' } }, - footer:{ + footer: { About: '关于我们', PrivacyPolicy: '隐私政策', TermsOfUse: '条款与条件', Disclaimer: '免责声明', - SiteMap: '地图', + SiteMap: '地图' }, - brand:{ - title: "品牌", - description: "每一个品牌,每一个故事 — 发现系列作品背后的缔造者。", - search: "搜索品牌", - noFound: "未找到品牌", - noFoundTip: "请尝试使用其他关键词。", - searchHistory: "搜索历史", + brand: { + title: '品牌', + description: '每一个品牌,每一个故事 — 发现系列作品背后的缔造者。', + search: '搜索品牌', + noFound: '未找到品牌', + noFoundTip: '请尝试使用其他关键词。', + searchHistory: '搜索历史', brandItem: { - viewProfile: "查看简介" + viewProfile: '查看简介' } }, - brandDetail:{ - addShoppingTip: "请先登录。", + brandDetail: { + addShoppingTip: '请先登录。', merchantInfo: { - Contact: "联系方式", - About: "关于我们" + Contact: '联系方式', + About: '关于我们' }, - All: "全部" + All: '全部' }, digitalItem: { - BestSelling: "畅销优先", - Price: "价格:从低到高", - SelectedFirst: "已选优先", - DateAdded: "添加日期", - NewestFirst: "最新优先", - title: "数字藏品", - info: "收藏于个人档案中的虚拟时装作品", - sortBy: "排序方式", - noData: "暂无数字藏品", - noDataTip: "请尝试调整筛选条件或刷新页面。", + BestSelling: '畅销优先', + Price: '价格:从低到高', + SelectedFirst: '已选优先', + DateAdded: '添加日期', + NewestFirst: '最新优先', + title: '数字藏品', + info: '收藏于个人档案中的虚拟时装作品', + sortBy: '排序方式', + noData: '暂无数字藏品', + noDataTip: '请尝试调整筛选条件或刷新页面。', MerchantInfo: { - Filters: "筛选", - Clear: "清空", - Categories: "分类", - Gender: "适用性别" + Filters: '筛选', + Clear: '清空', + Categories: '分类', + Gender: '适用性别' } }, checked: { - All: "全部" + All: '全部' }, MainHeader: { Home: '首页', @@ -281,7 +281,7 @@ export default { HiName: '你好,{name}', MyWardrobe: '我的衣橱', Notifications: '通知', - Settings: '设置', + Settings: '设置' }, ShoppingCart: { title: '购物车', @@ -300,24 +300,41 @@ export default { selected: '已选', brands: '品牌', item: '数字藏品', - checkoutSelected: '结账已选', + checkoutSelected: '结账已选' }, - digitalDetail:{ - Sketch: "草图", - Illustration: "插画", - Product: "产品", - EditorialVisual: "编辑视觉", - Back: "返回", - ReleaseIn: "发布于", - CopyrightLicenseNotice: "版权与许可声明", - LicenseIncludedInAsset: "资产包含许可", - LicenseIncludedInAssetInfo: "本平台所有产品均为数字资产,非实物商品。购买仅授予使用许可;版权及知识产权仍归原作者所有,除非另有说明。", - BuyNow: "立即购买", - AddToCart: "加入购物车" + digitalDetail: { + Sketch: '草图', + Illustration: '插画', + Product: '产品', + EditorialVisual: '编辑视觉', + Back: '返回', + ReleaseIn: '发布于', + CopyrightLicenseNotice: '版权与许可声明', + LicenseIncludedInAsset: '资产包含许可', + LicenseIncludedInAssetInfo: + '本平台所有产品均为数字资产,非实物商品。购买仅授予使用许可;版权及知识产权仍归原作者所有,除非另有说明。', + BuyNow: '立即购买', + AddToCart: '加入购物车' }, - addShoppingCart:{ - title: "已添加到您的购物车", - statement: "仅限数字资产。不包含实体产品。", - button: "去购物车" + addShoppingCart: { + title: '已添加到您的购物车', + statement: '仅限数字资产。不包含实体产品。', + button: '去购物车' + }, + area: { + chinaMainland: '中国大陆', + hongKongSar: '中国香港特别行政区', + macauSar: '中国澳门特别行政区', + taiwan: '中国台湾', + japan: '日本', + southKorea: '韩国', + singapore: '新加坡', + unitedStates: '美国', + unitedKingdom: '英国', + france: '法国', + italy: '意大利', + germany: '德国', + australia: '澳大利亚', + canada: '加拿大' } } diff --git a/src/utils/area.ts b/src/utils/area.ts new file mode 100644 index 0000000..a761010 --- /dev/null +++ b/src/utils/area.ts @@ -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' + } +] diff --git a/src/utils/request.ts b/src/utils/request.ts index 6eef519..c0240df 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -65,6 +65,16 @@ service.interceptors.response.use( if (response.config.url.includes('llm/streamChat')) { 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) diff --git a/src/views/setting/components/RegionSection.vue b/src/views/setting/components/RegionSection.vue index c640893..f1e9bfb 100644 --- a/src/views/setting/components/RegionSection.vue +++ b/src/views/setting/components/RegionSection.vue @@ -27,7 +27,7 @@
{{ t('Settings.region.region') }}
- {{ displayRegionLabel }} + {{ t(`area.${displayRegionLabel}`) }}
diff --git a/src/views/setting/components/SecuritySection.vue b/src/views/setting/components/SecuritySection.vue index 2d6d865..5fea7a3 100644 --- a/src/views/setting/components/SecuritySection.vue +++ b/src/views/setting/components/SecuritySection.vue @@ -23,14 +23,14 @@ :placeholder="t('Settings.security.newEmailPlaceholder')" @update:model-value="emit('update:newEmail', String($event))" /> - + -->
{{ t('Settings.security.verifiedTip') }} diff --git a/src/views/setting/index.vue b/src/views/setting/index.vue index 8b4c55f..9f3b995 100644 --- a/src/views/setting/index.vue +++ b/src/views/setting/index.vue @@ -1,159 +1,165 @@ diff --git a/src/views/setting/types.ts b/src/views/setting/types.ts index 0d8abf5..130f173 100644 --- a/src/views/setting/types.ts +++ b/src/views/setting/types.ts @@ -13,20 +13,19 @@ export const roleValues = [ ] 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 LanguageValue = (typeof languageValues)[number] -export type RegionValue = (typeof regionValues)[number] export interface SettingsData { firstName: string lastName: string email: string username: string - role: RoleValue[] - language: LanguageValue - region: RegionValue + roles: RoleValue[] + language: LanguageValue | '' + region: string | '' } export interface SecurityDraft { diff --git a/src/views/setting/useSettingsForm.ts b/src/views/setting/useSettingsForm.ts index 02e6c1c..491e0a1 100644 --- a/src/views/setting/useSettingsForm.ts +++ b/src/views/setting/useSettingsForm.ts @@ -1,288 +1,335 @@ import { computed, ref, shallowRef, watch, type Ref } from 'vue' import { ElMessage } from 'element-plus' +import { fetchUserProfile, type UserProfile, updateUserProfile } from '@/api/user' +import regionList from '@/utils/area' import { - languageValues, - regionValues, - roleValues, - type LanguageValue, - type RegionValue, - type RoleValue, - type SecurityDraft, - type SettingsData + languageValues, + roleValues, + type LanguageValue, + type RoleValue, + type SecurityDraft, + type SettingsData } from './types' type Translate = (key: string, ...args: unknown[]) => string interface UseSettingsFormOptions { - t: Translate - locale: Ref + t: Translate + locale: Ref } const languageLocaleMap: Record = { - english: 'ENGLISH', - chinese: 'CHINESE_SIMPLIFIED' + english: 'ENGLISH', + chinese: 'CHINESE_SIMPLIFIED' } const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const createDefaultData = (): SettingsData => ({ - firstName: 'Alexandra', - lastName: 'Chen', - email: 'alex.chen@gmail.com', - username: '@alexandra_chen', - role: ['student', 'graphicDesigner'], - language: 'english', - region: 'hongKongSar' + firstName: '', + lastName: '', + email: '', + username: '', + roles: [] as RoleValue[], + language: '', + region: '' }) const cloneSettingsData = (data: SettingsData): SettingsData => ({ - firstName: data.firstName, - lastName: data.lastName, - email: data.email, - username: data.username, - role: [...data.role], - language: data.language, - region: data.region + firstName: data.firstName, + lastName: data.lastName, + email: data.email, + username: data.username, + roles: [...data.roles], + language: data.language, + 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): 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 => ({ - newEmail: '', - newPassword: '', - currentPassword: '' + newEmail: '', + newPassword: '', + currentPassword: '' }) export function useSettingsForm({ t, locale }: UseSettingsFormOptions) { - const sourceData = ref(createDefaultData()) - const draftData = ref(cloneSettingsData(sourceData.value)) - const securityDraft = ref(createEmptySecurityDraft()) - const isEditing = shallowRef(false) - const saving = shallowRef(false) - const isVerificationDialogVisible = shallowRef(false) - const verificationTargetEmail = shallowRef('') - const verifiedEmail = shallowRef('') + const sourceData = ref(createDefaultData()) + const draftData = ref(cloneSettingsData(sourceData.value)) + const securityDraft = ref(createEmptySecurityDraft()) + const isEditing = shallowRef(false) + const saving = shallowRef(false) + const isVerificationDialogVisible = shallowRef(false) + const verificationTargetEmail = shallowRef('') + const verifiedEmail = shallowRef('') - const roleList = computed(() => - roleValues.map((value) => ({ - name: t(`Settings.roles.${value}`), - value - })) - ) + const roleList = computed(() => + roleValues.map((value) => ({ + name: t(`Settings.roles.${value}`), + value + })) + ) - const languageList = computed(() => - languageValues.map((value) => ({ - label: t(`Settings.languages.${value}`), - value - })) - ) + const languageList = computed(() => + languageValues.map((value) => ({ + label: t(`Settings.languages.${value}`), + value + })) + ) - const regionList = computed(() => - regionValues.map((value) => ({ - label: t(`Settings.regions.${value}`), - value - })) - ) + const displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value)) + const normalizedNewEmail = computed(() => securityDraft.value.newEmail.trim()) + const hasNewEmailChange = computed( + () => + normalizedNewEmail.value.length > 0 && + normalizedNewEmail.value !== sourceData.value.email + ) + const isEmailVerified = computed( + () => hasNewEmailChange.value && verifiedEmail.value === normalizedNewEmail.value + ) + const displayLanguageLabel = computed(() => + 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 displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value)) - const normalizedNewEmail = computed(() => securityDraft.value.newEmail.trim()) - const hasNewEmailChange = computed( - () => normalizedNewEmail.value.length > 0 && normalizedNewEmail.value !== sourceData.value.email - ) - const isEmailVerified = computed( - () => hasNewEmailChange.value && verifiedEmail.value === normalizedNewEmail.value - ) - const displayLanguageLabel = computed(() => t(`Settings.languages.${displayData.value.language}`)) - const displayRegionLabel = computed(() => t(`Settings.regions.${displayData.value.region}`)) + const fullName = computed(() => { + const data = displayData.value + return `${data.firstName} ${data.lastName}`.trim() + }) - const fullName = computed(() => { - const data = displayData.value - return `${data.firstName} ${data.lastName}`.trim() - }) + const roleModel = computed({ + get: () => displayData.value.roles, + set: (value) => { + if (isEditing.value) { + draftData.value.roles = value + return + } - const roleModel = computed({ - get: () => displayData.value.role, - set: (value) => { - if (isEditing.value) { - draftData.value.role = value - return - } + sourceData.value.roles = value + } + }) - sourceData.value.role = value - } - }) + const resetEmailVerificationState = () => { + isVerificationDialogVisible.value = false + verificationTargetEmail.value = '' + verifiedEmail.value = '' + } - const resetEmailVerificationState = () => { - isVerificationDialogVisible.value = false - verificationTargetEmail.value = '' - verifiedEmail.value = '' - } + const syncAppLanguage = (language: LanguageValue) => { + const nextLocale = languageLocaleMap[language] + locale.value = nextLocale + localStorage.setItem('language', nextLocale) + } - const syncAppLanguage = (language: LanguageValue) => { - const nextLocale = languageLocaleMap[language] - locale.value = nextLocale - localStorage.setItem('language', nextLocale) - } + const loadUserProfile = async () => { + try { + const profile = (await fetchUserProfile()) as Partial + const nextData = buildSettingsDataFromProfile(profile) + sourceData.value = cloneSettingsData(nextData) + draftData.value = cloneSettingsData(sourceData.value) - const resetDraftState = () => { - draftData.value = cloneSettingsData(sourceData.value) - securityDraft.value = createEmptySecurityDraft() - resetEmailVerificationState() - } + if (sourceData.value.language) { + syncAppLanguage(sourceData.value.language as LanguageValue) + } + } catch (error) { + console.warn(error) + } + } - const handleEdit = () => { - resetDraftState() - isEditing.value = true - } + const resetDraftState = () => { + draftData.value = cloneSettingsData(sourceData.value) + securityDraft.value = createEmptySecurityDraft() + resetEmailVerificationState() + } - const resetSecurityEmail = () => { - securityDraft.value.newEmail = '' - resetEmailVerificationState() - } + const handleEdit = () => { + resetDraftState() + isEditing.value = true + } - const resetSecurityPassword = () => { - securityDraft.value.newPassword = '' - securityDraft.value.currentPassword = '' - } + const resetSecurityEmail = () => { + securityDraft.value.newEmail = '' + resetEmailVerificationState() + } - const handleDiscard = () => { - resetDraftState() - isEditing.value = false - } + const resetSecurityPassword = () => { + securityDraft.value.newPassword = '' + securityDraft.value.currentPassword = '' + } - const closeVerificationDialog = () => { - isVerificationDialogVisible.value = false - verificationTargetEmail.value = '' - } + const handleDiscard = () => { + resetDraftState() + isEditing.value = false + } - const handleVerifyEmail = () => { - const nextEmail = normalizedNewEmail.value + const closeVerificationDialog = () => { + isVerificationDialogVisible.value = false + verificationTargetEmail.value = '' + } - if (!nextEmail) { - ElMessage.warning(t('Settings.messages.enterNewEmailFirst')) - return - } + const handleVerifyEmail = () => { + const nextEmail = normalizedNewEmail.value - if (!emailPattern.test(nextEmail)) { - ElMessage.warning(t('Settings.messages.invalidEmail')) - return - } + if (!nextEmail) { + ElMessage.warning(t('Settings.messages.enterNewEmailFirst')) + return + } - if (nextEmail === sourceData.value.email) { - ElMessage.warning(t('Settings.messages.sameEmail')) - return - } + if (!emailPattern.test(nextEmail)) { + ElMessage.warning(t('Settings.messages.invalidEmail')) + return + } - if (verifiedEmail.value === nextEmail) { - ElMessage.success(t('Settings.messages.alreadyVerified')) - return - } + if (nextEmail === sourceData.value.email) { + ElMessage.warning(t('Settings.messages.sameEmail')) + return + } - verificationTargetEmail.value = nextEmail - handleSendVerifyCode() - isVerificationDialogVisible.value = true - } + if (verifiedEmail.value === nextEmail) { + ElMessage.success(t('Settings.messages.alreadyVerified')) + return + } - const handleSendVerifyCode = () => { - ElMessage.success(t('Settings.messages.verificationCodeSent')) - } + verificationTargetEmail.value = nextEmail + handleSendVerifyCode() + } - const handleVerificationSubmit = (code: string) => { - if (code.length !== 6) { - ElMessage.warning(t('Settings.messages.enterVerificationCode')) - return - } + const handleSendVerifyCode = () => { + // AccountSendVerifyCode({ + // email: verificationTargetEmail.value, + // operationType: 'FORGET_PWD' + // }).then((res) => { + // console.log(res) + // ElMessage.success(t('Settings.messages.verificationCodeSent')) + // isVerificationDialogVisible.value = true + // }) + } - verifiedEmail.value = verificationTargetEmail.value - closeVerificationDialog() - ElMessage.success(t('Settings.messages.verificationCompleted')) - } + const handleVerificationSubmit = (code: string) => { + if (code.length !== 6) { + ElMessage.warning(t('Settings.messages.enterVerificationCode')) + return + } - const buildNextData = (): SettingsData => { - const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email + verifiedEmail.value = verificationTargetEmail.value + closeVerificationDialog() + ElMessage.success(t('Settings.messages.verificationCompleted')) + } - return { - firstName: draftData.value.firstName.trim(), - lastName: draftData.value.lastName.trim(), - username: draftData.value.username.trim(), - email: nextEmail, - role: [...draftData.value.role], - language: draftData.value.language, - region: draftData.value.region - } - } + const buildNextData = (): SettingsData => { + const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email - const handleSave = async () => { - if (hasNewEmailChange.value && !isEmailVerified.value) { - ElMessage.warning(t('Settings.messages.verifyEmailBeforeSave')) - return - } + 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 nextData = buildNextData() - const previousLanguage = sourceData.value.language - saving.value = true + const handleSave = async () => { + if (hasNewEmailChange.value && !isEmailVerified.value) { + ElMessage.warning(t('Settings.messages.verifyEmailBeforeSave')) + return + } - try { - sourceData.value = cloneSettingsData(nextData) + const nextData = buildNextData() + const previousLanguage = sourceData.value.language + saving.value = true - if (nextData.language !== previousLanguage) { - syncAppLanguage(nextData.language) - } + try { + await updateUserProfile(nextData) + sourceData.value = cloneSettingsData(nextData) + console.log(nextData) - draftData.value = cloneSettingsData(sourceData.value) - securityDraft.value = createEmptySecurityDraft() - resetEmailVerificationState() - isEditing.value = false - ElMessage.success(t('Settings.messages.settingsUpdated')) - } catch (error) { - console.warn(error) - } finally { - saving.value = false - } - } + if (nextData.language && nextData.language !== previousLanguage) { + syncAppLanguage(nextData.language as LanguageValue) + } - watch( - () => securityDraft.value.newEmail, - (value) => { - const trimmedValue = value.trim() + draftData.value = cloneSettingsData(sourceData.value) + securityDraft.value = createEmptySecurityDraft() + resetEmailVerificationState() + isEditing.value = false + ElMessage.success(t('Settings.messages.settingsUpdated')) + } catch (error) { + console.warn(error) + } finally { + saving.value = false + } + } - if (verifiedEmail.value && trimmedValue !== verifiedEmail.value) { - verifiedEmail.value = '' - } + watch( + () => securityDraft.value.newEmail, + (value) => { + const trimmedValue = value.trim() - if ( - isVerificationDialogVisible.value && - verificationTargetEmail.value && - trimmedValue !== verificationTargetEmail.value - ) { - closeVerificationDialog() - } - } - ) + if (verifiedEmail.value && trimmedValue !== verifiedEmail.value) { + verifiedEmail.value = '' + } - return { - sourceData, - draftData, - securityDraft, - isEditing, - saving, - isVerificationDialogVisible, - verificationTargetEmail, - roleList, - languageList, - regionList, - displayData, - isEmailVerified, - displayLanguageLabel, - displayRegionLabel, - fullName, - roleModel, - handleEdit, - handleDiscard, - handleSave, - resetSecurityEmail, - resetSecurityPassword, - handleVerifyEmail, - handleSendVerifyCode, - handleVerificationSubmit, - closeVerificationDialog - } + if ( + isVerificationDialogVisible.value && + verificationTargetEmail.value && + trimmedValue !== verificationTargetEmail.value + ) { + closeVerificationDialog() + } + } + ) + + return { + sourceData, + draftData, + securityDraft, + isEditing, + saving, + isVerificationDialogVisible, + verificationTargetEmail, + roleList, + languageList, + regionList, + displayData, + isEmailVerified, + displayLanguageLabel, + displayRegionLabel, + fullName, + roleModel, + handleEdit, + handleDiscard, + handleSave, + resetSecurityEmail, + resetSecurityPassword, + handleVerifyEmail, + handleSendVerifyCode, + handleVerificationSubmit, + closeVerificationDialog, + loadUserProfile + } } diff --git a/src/views/shoppingCart/sc-item.vue b/src/views/shoppingCart/sc-item.vue index 00d9e35..08031a0 100644 --- a/src/views/shoppingCart/sc-item.vue +++ b/src/views/shoppingCart/sc-item.vue @@ -34,6 +34,7 @@
${{ info.amount }} HKD
@@ -66,6 +67,7 @@ download :url="item.thumbnailUrl" :name="item.listingName" + @download.stop="handleDownloadSelected(item)" :showPrice="false" >
@@ -92,7 +94,7 @@ import { useClothesCategories } from '@/utils/ClothesCategory' import Empty from './Empty.vue' import FilterSidebar from './FilterSidebar.vue' - import { fetchMyWardrobe } from '@/api/user' + import { fetchMyWardrobe, fetchDownloadItemsByGet } from '@/api/user' import { useUserInfoStore } from '@/stores' import { debounce } from 'lodash-es' @@ -320,9 +322,37 @@ } } - const handleDownloadSelected = () => { - const items = dataList.value.filter((item) => item.checked) - console.log(items) + const downloadingSelected = ref(false) + const handleDownloadSelected = (assets) => { + 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 = () => { diff --git a/src/views/wardrobe/Orders.vue b/src/views/wardrobe/Orders.vue index 0423187..a0dca3d 100644 --- a/src/views/wardrobe/Orders.vue +++ b/src/views/wardrobe/Orders.vue @@ -84,6 +84,7 @@ :show-brand="false" is-order order-actions-layout + @download="handleDownload(order)" />
@@ -96,7 +97,7 @@ import { computed, onMounted, ref, shallowRef } from 'vue' import { useI18n } from 'vue-i18n' 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 Empty from './Empty.vue' @@ -181,14 +182,10 @@ 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) => { - 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) => { @@ -221,7 +218,7 @@ } const getOrderItemInfo = (item: OrderItem, order: OrderRecord) => ({ - status: item.status, + status: order.status, title: item.listingName, brand: order.shopName, tags: item.productCategory, @@ -230,6 +227,43 @@ 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 () => { 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(() => { fetchAllOrders() })