This commit is contained in:
李志鹏
2026-05-28 11:31:38 +08:00
14 changed files with 929 additions and 543 deletions

View File

@@ -7,6 +7,7 @@ export interface WardrobeItem {
page: number
size: number
}
// 获取我的衣橱assets
export const fetchMyWardrobe = (data: WardrobeItem): Promise<ApiResponse> => {
return request({
url: '/buyer/buyer/order/assets/page',
@@ -24,7 +25,7 @@ export interface OrderItem {
export interface OrdersPageResponse {
content: any[]
}
// 获取我的衣橱 orders
export const fetchMyOrders = (data: OrderItem): Promise<OrdersPageResponse> => {
return request({
url: '/buyer/buyer/order/page',
@@ -32,3 +33,78 @@ export const fetchMyOrders = (data: OrderItem): Promise<OrdersPageResponse> => {
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 const fetchUserProfile = (): Promise<ApiResponse> => {
return request({
url: '/buyer/profile/getProfile',
method: 'post'
})
}
// 设置用户信息
export interface UserProfile {
firstName: string
lastName: string
username: string
roles: string[]
region: string
language: string
email: string
oldPassword?: string
newPassword?: string
verifyCode?: string
}
export const updateUserProfile = (data: UserProfile): Promise<ApiResponse> => {
return request({
url: '/buyer/profile/setProfile',
method: 'post',
data
})
}
// 获取设置页验证码
export const fetchVerifyCode = (): Promise<ApiResponse> => {
return request({
url: '/buyer/profile/sendEmailChangeCode',
method: 'post'
})
}
// 验证设置页验证码
export const verifyEmailCode = (verifyCode: string): Promise<ApiResponse> => {
return request({
url: '/buyer/profile/verifyEmailChangeCode',
method: 'post',
data: { verifyCode }
})
}

View File

@@ -46,18 +46,21 @@ export default {
submit: 'Submit',
enterNewPassword: 'Enter a new password for<br/><span>{email}</span>',
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',
@@ -104,6 +107,7 @@ export default {
discard: 'DISCARD',
edit: 'EDIT',
saveChange: 'SAVE CHANGE',
verifyEmail: 'VERIFY EMAIL',
saving: 'SAVING...'
},
dialog: {
@@ -114,7 +118,7 @@ export default {
resendCodeIn: 'Resend Code in {time}'
},
messages: {
enterNewEmailFirst: 'Please enter your new email address first',
enterNewEmailFirst: 'Please enter your email address first',
invalidEmail: 'Please enter a valid email address',
sameEmail: 'Please enter a different email address',
alreadyVerified: 'This email has already been verified',
@@ -122,6 +126,11 @@ export default {
enterVerificationCode: 'Please enter the 6-digit verification code',
verificationCompleted: 'Email verification completed',
verifyEmailBeforeSave: 'Please verify your new email before saving',
currentPasswordRequired: 'Please enter your current password',
passwordLengthError: 'Password length must be between {min} and {max} characters',
passwordSpecial: 'Password must contain special characters',
passwordCase: 'Password must include upper/lowercase letters and numbers',
passwordNotSameAsOld: 'New password cannot be the same as current password',
settingsUpdated: 'Settings updated'
},
roles: {
@@ -139,7 +148,7 @@ export default {
},
languages: {
english: 'English',
chinese: 'Chinese',
chinese: 'Chinese'
},
regions: {
hongKongSar: 'Hong Kong SAR',
@@ -152,7 +161,7 @@ export default {
title: 'My Wardrobe',
subtitle: 'Your digital pieces, all in one place',
common: {
all: 'All',
all: 'All'
},
tabs: {
ariaLabel: 'Wardrobe tabs',
@@ -231,7 +240,7 @@ export default {
PrivacyPolicy: 'Privacy Policy',
TermsOfUse: 'Terms of Use',
Disclaimer: 'Disclaimer',
SiteMap: 'Site Map',
SiteMap: 'Site Map'
},
brand: {
title: 'Brand',
@@ -241,16 +250,16 @@ export default {
noFoundTip: 'Try using another keywords.',
searchHistory: 'Searching History',
brandItem: {
viewProfile: 'View Profile',
viewProfile: 'View Profile'
}
},
brandDetail: {
addShoppingTip: 'Please log in first.',
merchantInfo: {
Contact: 'Contact',
About: 'About',
About: 'About'
},
All: 'All',
All: 'All'
},
digitalItem: {
BestSelling: 'Best Selling',
@@ -267,11 +276,11 @@ export default {
Filters: 'Filters',
Clear: 'Clear',
Categories: 'Categories',
Gender: 'Gender',
Gender: 'Gender'
}
},
checked: {
All: 'All',
All: 'All'
},
MainHeader: {
Home: 'Home',
@@ -281,7 +290,7 @@ export default {
HiName: 'Hi, {name}',
MyWardrobe: 'My Wardrobe',
Notifications: 'Notifications',
Settings: 'Settings',
Settings: 'Settings'
},
ShoppingCart: {
title: 'Shopping Cart',
@@ -300,7 +309,7 @@ export default {
selected: 'Selected',
brands: 'Brands',
item: 'item',
checkoutSelected: 'Checkout Selected',
checkoutSelected: 'Checkout Selected'
},
digitalDetail: {
Sketch: 'Sketch',
@@ -311,9 +320,10 @@ 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'
},
Home: {
IndexTitle: 'Were Seeking<br /><span>Fashion Voice</span><br /><span class="small">Worth Featuring.</span>',
@@ -348,6 +358,22 @@ export default {
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'
}
}

View File

@@ -46,7 +46,7 @@ export default {
submit: '提交',
enterNewPassword: '请输入新密码<br/><span>{email}</span>',
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: '安全',
@@ -104,6 +104,7 @@ export default {
discard: '放弃',
edit: '编辑',
saveChange: '保存更改',
verifyEmail: '验证邮箱',
saving: '保存中...'
},
dialog: {
@@ -122,6 +123,11 @@ export default {
enterVerificationCode: '请输入 6 位验证码',
verificationCompleted: '邮箱验证完成',
verifyEmailBeforeSave: '请先完成新邮箱验证再保存',
currentPasswordRequired: '请输入当前密码',
passwordLengthError: '密码长度必须在 {min} 到 {max} 个字符之间',
passwordSpecial: '密码必须包含特殊符号',
passwordCase: '密码必须包含大小写字母和数字',
passwordNotSameAsOld: '新密码不能与旧密码相同',
settingsUpdated: '设置已更新'
},
roles: {
@@ -139,7 +145,7 @@ export default {
},
languages: {
english: '英文',
chinese: '中文',
chinese: '中文'
},
regions: {
hongKongSar: '中国香港特别行政区',
@@ -152,7 +158,7 @@ export default {
title: '我的衣橱',
subtitle: '你的数字单品尽在此处',
common: {
all: '全部',
all: '全部'
},
tabs: {
ariaLabel: '衣橱标签页',
@@ -216,14 +222,14 @@ export default {
},
collectionStory: {
back: '返回首页',
title: "我们在寻找",
description: "值得被听见的时尚之声",
button: "如有兴趣,请联系我们",
title: '我们在寻找',
description: '值得被听见的时尚之声',
button: '如有兴趣,请联系我们',
joinUs: {
title: '加入我们的设计师社区,',
info: "加入我们的远见者社区,发表你的系列故事。",
info2: "我们目前正在寻找深度整合 AiDA 创意工作流程的系列作品,特别是那些通过强大的核心理念和富有感染力的灵感而产生共鸣的作品。",
info3: "这一架构旨在通过深刻的‘命题式表达’提升你的曝光度,确保那些有灵魂、由故事驱动的设计能获得更高的市场溢价和卓越的销售转化率。"
info: '加入我们的远见者社区,发表你的系列故事。',
info2: '我们目前正在寻找深度整合 AiDA 创意工作流程的系列作品,特别是那些通过强大的核心理念和富有感染力的灵感而产生共鸣的作品。',
info3: '这一架构旨在通过深刻的‘命题式表达’提升你的曝光度,确保那些有灵魂、由故事驱动的设计能获得更高的市场溢价和卓越的销售转化率。'
}
},
footer: {
@@ -231,47 +237,47 @@ export default {
PrivacyPolicy: '隐私政策',
TermsOfUse: '条款与条件',
Disclaimer: '免责声明',
SiteMap: '地图',
SiteMap: '地图'
},
brand: {
title: "品牌",
description: "每一个品牌,每一个故事 — 发现系列作品背后的缔造者。",
search: "搜索品牌",
noFound: "未找到品牌",
noFoundTip: "请尝试使用其他关键词。",
searchHistory: "搜索历史",
title: '品牌',
description: '每一个品牌,每一个故事 — 发现系列作品背后的缔造者。',
search: '搜索品牌',
noFound: '未找到品牌',
noFoundTip: '请尝试使用其他关键词。',
searchHistory: '搜索历史',
brandItem: {
viewProfile: "查看简介"
viewProfile: '查看简介'
}
},
brandDetail: {
addShoppingTip: "请先登录。",
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 +287,7 @@ export default {
HiName: '你好,{name}',
MyWardrobe: '我的衣橱',
Notifications: '通知',
Settings: '设置',
Settings: '设置'
},
ShoppingCart: {
title: '购物车',
@@ -300,54 +306,80 @@ export default {
selected: '已选',
brands: '品牌',
item: '数字藏品',
checkoutSelected: '结账',
checkoutSelected: '结账'
},
digitalDetail: {
Sketch: "草图",
Illustration: "插画",
Product: "产品",
EditorialVisual: "编辑视觉",
Back: "返回",
ReleaseIn: "发布于",
CopyrightLicenseNotice: "版权与许可声明",
LicenseIncludedInAsset: "资产包含许可",
LicenseIncludedInAssetInfo: "本平台所有产品均为数字资产,非实物商品。购买仅授予使用许可;版权及知识产权仍归原作者所有,除非另有说明。",
BuyNow: "立即购买",
AddToCart: "加入购物车"
Sketch: '草图',
Illustration: '插画',
Product: '产品',
EditorialVisual: '编辑视觉',
Back: '返回',
ReleaseIn: '发布于',
CopyrightLicenseNotice: '版权与许可声明',
LicenseIncludedInAsset: '资产包含许可',
LicenseIncludedInAssetInfo:
'本平台所有产品均为数字资产,非实物商品。购买仅授予使用许可;版权及知识产权仍归原作者所有,除非另有说明。',
BuyNow: '立即购买',
AddToCart: '加入购物车'
},
addShoppingCart: {
title: '已添加到您的购物车',
statement: '仅限数字资产。不包含实体产品。',
button: '去购物车'
},
area: {
chinaMainland: '中国大陆',
hongKongSar: '中国香港特别行政区',
macauSar: '中国澳门特别行政区',
taiwan: '中国台湾',
japan: '日本',
southKorea: '韩国',
singapore: '新加坡',
unitedStates: '美国',
unitedKingdom: '英国',
france: '法国',
italy: '意大利',
germany: '德国',
australia: '澳大利亚',
canada: '加拿大'
},
Home: {
IndexTitle: '我们正在寻<br />找值得推广<br />的时尚之声。',
IndexTip: '通过了解每件作品背后的故事来探索其收藏品。这是一个精心打造的场所,将设计师、故事和时尚商业紧密相连。',
IndexTip:
'通过了解每件作品背后的故事来探索其收藏品。这是一个精心打造的场所,将设计师、故事和时尚商业紧密相连。',
DesignerTitle: '设计师社区',
DesignerTip: '发现 AiDA 创意社区的设计师。<br />每月我们都会展示他们最杰出的作品。',
SearchBrands: '搜索品牌',
AidaTitle: '使用 AiDA 设计',
AidaTip: '在这个平台上,每一件服装都是设计师创意在 AiDA 软件中绽放的成果。AiDA 是一款能激发您的创造力的工具,它从不掩盖您的才华。让您的创意尽情绽放吧。',
AidaTip:
'在这个平台上,每一件服装都是设计师创意在 AiDA 软件中绽放的成果。AiDA 是一款能激发您的创造力的工具,它从不掩盖您的才华。让您的创意尽情绽放吧。',
TryNow: '立即试用',
DigitalItems: '数字藏品',
DigitalItemsTip1: 'AiDA 能够捕捉您最激昂的想法,并将其转化为生动的形象。<br />“数字愿景”一个充满创造力碰撞与发展的虚拟世界。',
DigitalItemsTip2: 'AiDA 风格的创新,打造出实用性强的日常服饰,这些服饰能够经久耐用。<br/>你的衣橱与现代时尚的节奏保持同步。',
DigitalItemsTip1:
'AiDA 能够捕捉您最激昂的想法,并将其转化为生动的形象。<br />“数字愿景”一个充满创造力碰撞与发展的虚拟世界。',
DigitalItemsTip2:
'AiDA 风格的创新,打造出实用性强的日常服饰,这些服饰能够经久耐用。<br/>你的衣橱与现代时尚的节奏保持同步。',
FooterTip: 'Stylish Parade 是一个为设计师提供的商业平台,它作为 AiDA 商业的商业扩展。',
FooterAidaTip: '使用 AiDA 设计',
Help: "帮助",
FAQ: "常见问题",
MyAccount: "我的账户",
MyOrders: "我的订单",
PaymentInvoices: "支付发票",
CopyrightLicense: "版权与许可声明",
Polices: "政策",
Legal: "法律",
PrivacyPolicy: "隐私政策",
CookiesSettings: "Cookie 设置",
PurchaseConditions: "购买条件",
Company: "公司",
AboutUs: "关于我们",
Offices: "办公室",
JoinWithUs: "加入我们",
Help: '帮助',
FAQ: '常见问题',
MyAccount: '我的账户',
MyOrders: '我的订单',
PaymentInvoices: '支付发票',
CopyrightLicense: '版权与许可声明',
Polices: '政策',
Legal: '法律',
PrivacyPolicy: '隐私政策',
CookiesSettings: 'Cookie 设置',
PurchaseConditions: '购买条件',
Company: '公司',
AboutUs: '关于我们',
Offices: '办公室',
JoinWithUs: '加入我们'
},
addShoppingCart: {
title: "已添加到您的购物车",
statement: "仅限数字资产。不包含实体产品。",
button: "去购物车"
title: '已添加到您的购物车',
statement: '仅限数字资产。不包含实体产品。',
button: '去购物车'
}
}

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')) {
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)

View File

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

View File

@@ -23,18 +23,18 @@
:placeholder="t('Settings.security.newEmailPlaceholder')"
@update:model-value="emit('update:newEmail', String($event))"
/>
<button
<!-- <button
type="button"
class="verify-btn"
:class="{ verified: isEmailVerified }"
@click="emit('verify-email')"
>
{{ isEmailVerified ? t('Settings.security.verified') : t('Settings.security.verify') }}
</button>
</button> -->
</div>
<div v-if="isEmailVerified" class="security-tip verified-tip">
<!-- <div v-if="isEmailVerified" class="security-tip verified-tip">
{{ t('Settings.security.verifiedTip') }}
</div>
</div> -->
</div>
<div class="inner-divider" />

View File

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

View File

@@ -48,8 +48,10 @@
<SettingsActions
:is-editing="isEditing"
:saving="saving"
:needs-email-verification="needsEmailVerification"
@edit="handleEdit"
@save="handleSave"
@verify="handleVerifyEmail"
@discard="handleDiscard"
/>
</div>
@@ -58,7 +60,7 @@
<EmailVerificationDialog
:visible="isVerificationDialogVisible"
:email="verificationTargetEmail"
:email="displayData.email"
:saving="saving"
@close="closeVerificationDialog"
@resend="handleSendVerifyCode"
@@ -68,6 +70,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import EmailVerificationDialog from './components/EmailVerificationDialog.vue'
import ProfileSection from './components/ProfileSection.vue'
@@ -94,6 +97,7 @@ const {
displayRegionLabel,
fullName,
roleModel,
needsEmailVerification,
handleEdit,
handleDiscard,
handleSave,
@@ -102,8 +106,13 @@ const {
handleVerifyEmail,
handleSendVerifyCode,
handleVerificationSubmit,
closeVerificationDialog
closeVerificationDialog,
loadUserProfile
} = useSettingsForm({ t, locale })
onMounted(() => {
loadUserProfile()
})
</script>
<style lang="less" scoped>

View File

@@ -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 {

View File

@@ -1,15 +1,23 @@
import { computed, ref, shallowRef, watch, type Ref } from 'vue'
import { ElMessage } from 'element-plus'
import md5 from 'md5'
import {
fetchUserProfile,
type UserProfile,
updateUserProfile,
verifyEmailCode,
fetchVerifyCode
} from '@/api/user'
import regionList from '@/utils/area'
import {
languageValues,
regionValues,
roleValues,
type LanguageValue,
type RegionValue,
type RoleValue,
type SecurityDraft,
type SettingsData
} from './types'
import { validateCase, validateLength, validateSpecial } from '@/views/login/tools'
type Translate = (key: string, ...args: unknown[]) => string
@@ -26,13 +34,13 @@ const languageLocaleMap: Record<LanguageValue, 'ENGLISH' | '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 => ({
@@ -40,11 +48,30 @@ const cloneSettingsData = (data: SettingsData): SettingsData => ({
lastName: data.lastName,
email: data.email,
username: data.username,
role: [...data.role],
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<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 => ({
newEmail: '',
newPassword: '',
@@ -60,6 +87,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
const isVerificationDialogVisible = shallowRef(false)
const verificationTargetEmail = shallowRef('')
const verifiedEmail = shallowRef('')
const verificationCode = shallowRef('')
const roleList = computed(() =>
roleValues.map((value) => ({
@@ -75,23 +103,31 @@ 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 normalizedNewEmail = computed(() => securityDraft.value.newEmail.trim())
const hasNewEmailChange = computed(
() => normalizedNewEmail.value.length > 0 && normalizedNewEmail.value !== sourceData.value.email
() =>
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 hasNewPasswordChange = computed(() => securityDraft.value.newPassword.length > 0)
const needsEmailVerification = computed(
() => (hasNewEmailChange.value || hasNewPasswordChange.value) && !isEmailVerified.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 fullName = computed(() => {
const data = displayData.value
@@ -99,14 +135,14 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
})
const roleModel = computed<RoleValue[]>({
get: () => displayData.value.role,
get: () => displayData.value.roles,
set: (value) => {
if (isEditing.value) {
draftData.value.role = value
draftData.value.roles = value
return
}
sourceData.value.role = value
sourceData.value.roles = value
}
})
@@ -114,6 +150,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
isVerificationDialogVisible.value = false
verificationTargetEmail.value = ''
verifiedEmail.value = ''
verificationCode.value = ''
}
const syncAppLanguage = (language: LanguageValue) => {
@@ -122,6 +159,21 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
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 = () => {
draftData.value = cloneSettingsData(sourceData.value)
securityDraft.value = createEmptySecurityDraft()
@@ -154,13 +206,36 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
}
const handleVerifyEmail = () => {
const nextEmail = normalizedNewEmail.value
if (!hasNewEmailChange.value && !hasNewPasswordChange.value) return
if (!nextEmail) {
ElMessage.warning(t('Settings.messages.enterNewEmailFirst'))
const nextEmail = normalizedNewEmail.value
const newPassword = securityDraft.value.newPassword
const currentPassword = securityDraft.value.currentPassword
let targetEmail = ''
if (hasNewPasswordChange.value) {
if (validateLength(newPassword)) {
ElMessage.warning(t('Settings.messages.passwordLengthError', { min: 6, max: 20 }))
return
}
if (validateSpecial(newPassword)) {
ElMessage.warning(t('Settings.messages.passwordSpecial'))
return
}
if (validateCase(newPassword)) {
ElMessage.warning(t('Settings.messages.passwordCase'))
return
}
if (newPassword === currentPassword) {
ElMessage.warning(t('Settings.messages.passwordNotSameAsOld'))
return
}
}
if (hasNewEmailChange.value) {
if (!emailPattern.test(nextEmail)) {
ElMessage.warning(t('Settings.messages.invalidEmail'))
return
@@ -176,13 +251,22 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
return
}
verificationTargetEmail.value = nextEmail
targetEmail = nextEmail
} else if (hasNewPasswordChange.value) {
targetEmail = sourceData.value.email
}
if (!targetEmail) return
verificationTargetEmail.value = targetEmail
handleSendVerifyCode()
isVerificationDialogVisible.value = true
}
const handleSendVerifyCode = () => {
fetchVerifyCode().then(() => {
ElMessage.success(t('Settings.messages.verificationCodeSent'))
isVerificationDialogVisible.value = true
})
}
const handleVerificationSubmit = (code: string) => {
@@ -190,24 +274,13 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
ElMessage.warning(t('Settings.messages.enterVerificationCode'))
return
}
// send code to backend and store the code locally so save() can include it
verifyEmailCode(code).then((res) => {
verificationCode.value = code
verifiedEmail.value = verificationTargetEmail.value
closeVerificationDialog()
ElMessage.success(t('Settings.messages.verificationCompleted'))
}
const buildNextData = (): SettingsData => {
const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email
return {
firstName: draftData.value.firstName.trim(),
lastName: draftData.value.lastName.trim(),
username: draftData.value.username.trim(),
email: nextEmail,
role: [...draftData.value.role],
language: draftData.value.language,
region: draftData.value.region
}
})
}
const handleSave = async () => {
@@ -216,15 +289,51 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
return
}
const nextData = buildNextData()
if (hasNewPasswordChange.value && !verificationCode.value) {
ElMessage.warning(t('Settings.messages.verifyEmailBeforeSave'))
return
}
const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email
const nextData: UserProfile = {
firstName: draftData.value.firstName.trim(),
lastName: draftData.value.lastName.trim(),
username: draftData.value.username.trim(),
email: nextEmail,
roles: draftData.value.roles as string[],
language: draftData.value.language,
region: draftData.value.region
}
// 如果改邮箱或改密码,需要添加验证码和密码信息
if (hasNewEmailChange.value || hasNewPasswordChange.value) {
nextData.verifyCode = verificationCode.value
}
if (hasNewPasswordChange.value) {
nextData.oldPassword = md5(securityDraft.value.currentPassword)
nextData.newPassword = md5(securityDraft.value.newPassword)
}
const previousLanguage = sourceData.value.language
saving.value = true
try {
sourceData.value = cloneSettingsData(nextData)
await updateUserProfile(nextData)
const settingsData: SettingsData = {
firstName: nextData.firstName,
lastName: nextData.lastName,
username: nextData.username,
email: nextData.email,
roles: nextData.roles as RoleValue[],
language: nextData.language as LanguageValue | '',
region: nextData.region as any
}
sourceData.value = cloneSettingsData(settingsData)
console.log(nextData)
if (nextData.language !== previousLanguage) {
syncAppLanguage(nextData.language)
if (nextData.language && nextData.language !== previousLanguage) {
syncAppLanguage(nextData.language as LanguageValue)
}
draftData.value = cloneSettingsData(sourceData.value)
@@ -275,6 +384,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
displayRegionLabel,
fullName,
roleModel,
needsEmailVerification,
handleEdit,
handleDiscard,
handleSave,
@@ -283,6 +393,7 @@ export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
handleVerifyEmail,
handleSendVerifyCode,
handleVerificationSubmit,
closeVerificationDialog
closeVerificationDialog,
loadUserProfile
}
}

View File

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

View File

@@ -31,6 +31,7 @@
<div
class="assets-toolbar__download flex flex-center"
:class="{ disabled: selectedCount < 1 }"
v-loading="downloadingSelected"
@click="handleDownloadSelected"
>
<SvgIcon name="downloadBtn" color="#fff" />
@@ -66,6 +67,7 @@
download
:url="item.thumbnailUrl"
:name="item.listingName"
@download.stop="handleDownloadSelected(item)"
:showPrice="false"
></CommodityItem>
</div>
@@ -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 = () => {

View File

@@ -84,6 +84,7 @@
:show-brand="false"
is-order
order-actions-layout
@download="handleDownload(order)"
/>
</div>
</article>
@@ -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()
})