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

@@ -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<ApiResponse> => {
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<OrdersPageResponse> => {
return request({
url: '/buyer/buyer/order/page',
method: 'get',
params: data
})
}
export const fetchMyOrders = (data: OrderItem): Promise<OrdersPageResponse> => {
return request({
url: '/buyer/buyer/order/page',
method: 'get',
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',
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',
@@ -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'
}
}
}

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: '安全',
@@ -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: '加拿大'
}
}

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,14 +23,14 @@
: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">
{{ t('Settings.security.verifiedTip') }}

View File

@@ -1,159 +1,165 @@
<template>
<div class="setting-wrapper mini-scrollbar">
<div class="banner">
<div class="title">{{ t('Settings.title') }}</div>
<div class="slogan">{{ t('Settings.slogan') }}</div>
</div>
<div class="setting-wrapper mini-scrollbar">
<div class="banner">
<div class="title">{{ t('Settings.title') }}</div>
<div class="slogan">{{ t('Settings.slogan') }}</div>
</div>
<div class="setting-content">
<ProfileSection
v-model:first-name="draftData.firstName"
v-model:last-name="draftData.lastName"
v-model:username="draftData.username"
v-model:role-model="roleModel"
:display-data="displayData"
:full-name="fullName"
:is-editing="isEditing"
:role-options="roleList"
/>
<div class="setting-content">
<ProfileSection
v-model:first-name="draftData.firstName"
v-model:last-name="draftData.lastName"
v-model:username="draftData.username"
v-model:role-model="roleModel"
:display-data="displayData"
:full-name="fullName"
:is-editing="isEditing"
:role-options="roleList"
/>
<div class="gap" />
<div class="gap" />
<SecuritySection
v-model:new-email="securityDraft.newEmail"
v-model:new-password="securityDraft.newPassword"
v-model:current-password="securityDraft.currentPassword"
:email="displayData.email"
:is-editing="isEditing"
:is-email-verified="isEmailVerified"
@reset-email="resetSecurityEmail"
@reset-password="resetSecurityPassword"
@verify-email="handleVerifyEmail"
/>
<SecuritySection
v-model:new-email="securityDraft.newEmail"
v-model:new-password="securityDraft.newPassword"
v-model:current-password="securityDraft.currentPassword"
:email="displayData.email"
:is-editing="isEditing"
:is-email-verified="isEmailVerified"
@reset-email="resetSecurityEmail"
@reset-password="resetSecurityPassword"
@verify-email="handleVerifyEmail"
/>
<div class="gap" />
<div class="gap" />
<RegionSection
v-model:language="draftData.language"
v-model:region="draftData.region"
:display-language-label="displayLanguageLabel"
:display-region-label="displayRegionLabel"
:is-editing="isEditing"
:language-options="languageList"
:region-options="regionList"
/>
<RegionSection
v-model:language="draftData.language"
v-model:region="draftData.region"
:display-language-label="displayLanguageLabel"
:display-region-label="displayRegionLabel"
:is-editing="isEditing"
:language-options="languageList"
:region-options="regionList"
/>
<div class="gap bottom-gap" />
<div class="gap bottom-gap" />
<SettingsActions
:is-editing="isEditing"
:saving="saving"
@edit="handleEdit"
@save="handleSave"
@discard="handleDiscard"
/>
</div>
<SettingsActions
:is-editing="isEditing"
:saving="saving"
@edit="handleEdit"
@save="handleSave"
@discard="handleDiscard"
/>
</div>
<Footer />
<Footer />
<EmailVerificationDialog
:visible="isVerificationDialogVisible"
:email="verificationTargetEmail"
:saving="saving"
@close="closeVerificationDialog"
@resend="handleSendVerifyCode"
@submit="handleVerificationSubmit"
/>
</div>
<EmailVerificationDialog
:visible="isVerificationDialogVisible"
:email="verificationTargetEmail"
:saving="saving"
@close="closeVerificationDialog"
@resend="handleSendVerifyCode"
@submit="handleVerificationSubmit"
/>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import EmailVerificationDialog from './components/EmailVerificationDialog.vue'
import ProfileSection from './components/ProfileSection.vue'
import RegionSection from './components/RegionSection.vue'
import SecuritySection from './components/SecuritySection.vue'
import SettingsActions from './components/SettingsActions.vue'
import { useSettingsForm } from './useSettingsForm'
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import EmailVerificationDialog from './components/EmailVerificationDialog.vue'
import ProfileSection from './components/ProfileSection.vue'
import RegionSection from './components/RegionSection.vue'
import SecuritySection from './components/SecuritySection.vue'
import SettingsActions from './components/SettingsActions.vue'
import { useSettingsForm } from './useSettingsForm'
const { t, locale } = useI18n({ useScope: 'global' })
const { t, locale } = useI18n({ useScope: 'global' })
const {
draftData,
securityDraft,
isEditing,
saving,
isVerificationDialogVisible,
verificationTargetEmail,
roleList,
languageList,
regionList,
displayData,
isEmailVerified,
displayLanguageLabel,
displayRegionLabel,
fullName,
roleModel,
handleEdit,
handleDiscard,
handleSave,
resetSecurityEmail,
resetSecurityPassword,
handleVerifyEmail,
handleSendVerifyCode,
handleVerificationSubmit,
closeVerificationDialog
} = useSettingsForm({ t, locale })
const {
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
} = useSettingsForm({ t, locale })
onMounted(() => {
loadUserProfile()
})
</script>
<style lang="less" scoped>
.setting-wrapper {
height: 100%;
overflow-y: auto;
background: #ffffff;
}
.setting-wrapper {
height: 100%;
overflow-y: auto;
background: #ffffff;
}
.banner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 14.8rem;
row-gap: 1.2rem;
background: linear-gradient(rgba(255, 255, 255, 0.91), rgba(255, 255, 255, 0.91)),
linear-gradient(90deg, #f2eee8 0%, #fbfaf8 40%, #f1ede7 100%);
}
.banner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 14.8rem;
row-gap: 1.2rem;
background: linear-gradient(rgba(255, 255, 255, 0.91), rgba(255, 255, 255, 0.91)),
linear-gradient(90deg, #f2eee8 0%, #fbfaf8 40%, #f1ede7 100%);
}
.title {
font-family: 'KaiseiOpti-Bold';
font-size: 4rem;
line-height: 3.6rem;
color: #232323;
}
.title {
font-family: 'KaiseiOpti-Bold';
font-size: 4rem;
line-height: 3.6rem;
color: #232323;
}
.slogan {
font-family: 'KaiseiOpti-Regular';
font-size: 1.6rem;
line-height: 2.4rem;
color: #585858;
}
.slogan {
font-family: 'KaiseiOpti-Regular';
font-size: 1.6rem;
line-height: 2.4rem;
color: #585858;
}
.setting-content {
padding: 4rem 18rem 7rem;
}
.setting-content {
padding: 4rem 18rem 7rem;
}
.gap {
height: 0.05rem;
margin-top: 6rem;
margin-bottom: 4rem;
background-color: #c4c4c4;
.gap {
height: 0.05rem;
margin-top: 6rem;
margin-bottom: 4rem;
background-color: #c4c4c4;
&.bottom-gap {
margin-top: 4rem;
}
}
&.bottom-gap {
margin-top: 4rem;
}
}
:deep(.el-select-dropdown__item) {
padding: 0 2rem !important;
}
:deep(.el-select-dropdown__item) {
padding: 0 2rem !important;
}
</style>

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,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<string>
t: Translate
locale: Ref<string>
}
const languageLocaleMap: Record<LanguageValue, 'ENGLISH' | 'CHINESE_SIMPLIFIED'> = {
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<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: '',
currentPassword: ''
newEmail: '',
newPassword: '',
currentPassword: ''
})
export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
const sourceData = ref<SettingsData>(createDefaultData())
const draftData = ref<SettingsData>(cloneSettingsData(sourceData.value))
const securityDraft = ref<SecurityDraft>(createEmptySecurityDraft())
const isEditing = shallowRef(false)
const saving = shallowRef(false)
const isVerificationDialogVisible = shallowRef(false)
const verificationTargetEmail = shallowRef('')
const verifiedEmail = shallowRef('')
const sourceData = ref<SettingsData>(createDefaultData())
const draftData = ref<SettingsData>(cloneSettingsData(sourceData.value))
const securityDraft = ref<SecurityDraft>(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<RoleValue[]>({
get: () => displayData.value.roles,
set: (value) => {
if (isEditing.value) {
draftData.value.roles = value
return
}
const roleModel = computed<RoleValue[]>({
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<UserProfile>
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
}
}

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()
})