feat: 服装分类

This commit is contained in:
2026-05-21 13:44:57 +08:00
parent 5476a1f69d
commit 81b907562e
8 changed files with 321 additions and 61 deletions

27
src/api/user.ts Normal file
View File

@@ -0,0 +1,27 @@
export interface WardrobeItem {
buyerId: number
categories: string[]
designFor: 'female' | 'male' | 'all'
page: number
size: number
}
export const fetchMyWardrobe = (data: WardrobeItem): Promise<ApiResponse> => {
return request({
url: '/buyer/buyer/order/assets/page',
method: 'post',
data
})
}
export interface OrderItem {
status: number
page: number
size: number
}
export const fetchMyOrders = (data: OrderItem): Promise<ApiResponse> => {
return request({
url: '/buyer/buyer/order/page',
method: 'post',
data
})
}

View File

@@ -127,5 +127,73 @@ export default {
singapore: 'Singapore',
unitedKingdom: 'United Kingdom'
}
},
Wardrobe: {
title: 'My Wardrobe',
subtitle: 'Your digital pieces, all in one place',
common: {
all: 'All',
currencyHkd: 'HKD'
},
tabs: {
ariaLabel: 'Wardrobe tabs',
assets: 'Assets',
orders: 'Orders'
},
sort: {
label: 'Sort by',
placeholder: 'Select',
default: 'Default',
dateAdded: 'Date Added',
selectedFirst: 'Selected First'
},
assets: {
filters: 'Filters',
clear: 'Clear',
categories: 'Categories',
gender: 'Gender',
selectedCount: '{count} Selected',
selectAll: 'Select All',
deselectAll: 'Deselect All',
downloadSelected: 'Download Selected',
genders: {
male: 'Male',
female: 'Female'
}
},
orders: {
moreItems: '+{count} more',
statuses: {
all: 'All',
paid: 'Paid',
unpaid: 'Unpaid',
cancelled: 'Canceled'
},
statusBadges: {
paid: 'PAID',
unpaid: 'UNPAID',
cancelled: 'CANCELED'
},
actions: {
invoice: 'Invoice',
completePayment: 'Complete Payment',
buyAgain: 'Buy Again'
}
},
empty: {
title: 'Nothing in Wardrobe yet',
description: 'Explore the digital item and add pieces to your collection.',
action: 'Explore Digital Items'
}
},
ClothesCategories: {
blouses: 'Blouse',
dress: 'Dress',
trousers: 'Trousers',
skirt: 'Skirt',
tops: 'Tops',
bottoms: 'Bottoms',
outwear: 'Outwear',
others: 'Others'
}
}

View File

@@ -127,5 +127,73 @@ export default {
singapore: '新加坡',
unitedKingdom: '英国'
}
},
Wardrobe: {
title: '我的衣橱',
subtitle: '你的数字单品尽在此处',
common: {
all: '全部',
currencyHkd: 'HKD'
},
tabs: {
ariaLabel: '衣橱标签页',
assets: '资产',
orders: '订单'
},
sort: {
label: '排序',
placeholder: '请选择',
default: '默认',
dateAdded: '添加日期',
selectedFirst: '已选优先'
},
assets: {
filters: '筛选',
clear: '清除',
categories: '类别',
gender: '性别',
selectedCount: '已选择 {count} 件',
selectAll: '全选',
deselectAll: '取消全选',
downloadSelected: '下载已选',
genders: {
male: '男',
female: '女'
}
},
orders: {
moreItems: '还有 {count} 件',
statuses: {
all: '全部',
paid: '已支付',
unpaid: '待支付',
cancelled: '已取消'
},
statusBadges: {
paid: '已支付',
unpaid: '待支付',
cancelled: '已取消'
},
actions: {
invoice: '发票',
completePayment: '完成付款',
buyAgain: '再次购买'
}
},
empty: {
title: '衣橱暂无内容',
description: '探索数字单品,并添加到你的收藏。',
action: '探索数字单品'
}
},
ClothesCategories: {
blouses: '衬衫',
dress: '连衣裙',
trousers: '裤子',
skirt: '短裙',
tops: '上装',
bottoms: '下装',
outwear: '外套',
others: '其他'
}
}

View File

@@ -0,0 +1,71 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import i18n from '@/lang'
type Translate = (key: string) => string
const clothesCategoryConfigs = [
{
key: 'blouses',
value: 'blouses'
},
{
key: 'dress',
value: 'dress'
},
{
key: 'trousers',
value: 'trousers'
},
{
key: 'skirt',
value: 'skirt'
},
{
key: 'tops',
value: 'tops'
},
{
key: 'bottoms',
value: 'bottoms'
},
{
key: 'outwear',
value: 'outwear'
},
{
key: 'others',
value: 'others'
}
] as const
export type ClothesCategoryValue = (typeof clothesCategoryConfigs)[number]['value']
export interface ClothesCategory {
name: string
label: string
value: ClothesCategoryValue
}
export const createClothesCategories = (t: Translate = i18n.global.t): ClothesCategory[] =>
clothesCategoryConfigs.map(({ key, value }) => ({
name: t(`ClothesCategories.${key}`),
label: t(`ClothesCategories.${key}`),
value
}))
export const ClothesCategories: ClothesCategory[] = clothesCategoryConfigs.map(({ key, value }) => ({
get name() {
return i18n.global.t(`ClothesCategories.${key}`)
},
get label() {
return i18n.global.t(`ClothesCategories.${key}`)
},
value
}))
export const useClothesCategories = () => {
const { t } = useI18n({ useScope: 'global' })
return computed(() => createClothesCategories(t))
}

View File

@@ -3,12 +3,14 @@
<aside class="wardrobe-assets__filters">
<div class="filters-card">
<div class="filters-card__heading">
<h2 class="filters-card__title">Filters</h2>
<button class="filters-card__clear" type="button" @click="clearFilters">Clear</button>
<h2 class="filters-card__title">{{ t('Wardrobe.assets.filters') }}</h2>
<button class="filters-card__clear" type="button" @click="clearFilters">
{{ t('Wardrobe.assets.clear') }}
</button>
</div>
<section class="filter-group">
<h3 class="filter-group__title">Categories</h3>
<h3 class="filter-group__title">{{ t('Wardrobe.assets.categories') }}</h3>
<div class="filter-group__line"></div>
<div class="filter-group__options">
<button
@@ -28,7 +30,7 @@
</section>
<section class="filter-group">
<h3 class="filter-group__title">Gender</h3>
<h3 class="filter-group__title">{{ t('Wardrobe.assets.gender') }}</h3>
<div class="filter-group__line"></div>
<div class="filter-group__options">
<button
@@ -57,10 +59,16 @@
<div class="assets-toolbar__selection">
<div class="select-count flex align-center">
<img src="@/assets/images/wardrobe/select.png" />
<span class="assets-toolbar__count">{{ selectedCount }} Selected</span>
<span class="assets-toolbar__count">
{{ t('Wardrobe.assets.selectedCount', { count: selectedCount }) }}
</span>
</div>
<div class="assets-toolbar__link" @click="handleSelectAll(true)">
{{ t('Wardrobe.assets.selectAll') }}
</div>
<div class="assets-toolbar__link" @click="handleSelectAll(false)">
{{ t('Wardrobe.assets.deselectAll') }}
</div>
<div class="assets-toolbar__link" @click="handleSelectAll(true)">Select All</div>
<div class="assets-toolbar__link" @click="handleSelectAll(false)">Deselect All</div>
</div>
<div class="assets-toolbar__actions">
@@ -70,7 +78,7 @@
@click="handleDownloadSelected"
>
<SvgIcon name="downloadBtn" color="#fff" />
<span>Download Selected</span>
<span>{{ t('Wardrobe.assets.downloadSelected') }}</span>
</div>
</div>
</div>
@@ -106,7 +114,9 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useClothesCategories } from '@/utils/ClothesCategory'
import img from '@/assets/images/collectionStory/Rectangle.png'
import Empty from './Empty.vue'
@@ -116,26 +126,26 @@ interface FilterOption {
}
const router = useRouter()
const { t } = useI18n({ useScope: 'global' })
const clothesCategories = useClothesCategories()
const categories: FilterOption[] = [
{ label: 'All', value: 'all' },
{ label: 'Outerwear', value: 'outerwear' },
{ label: 'Dress', value: 'dress' },
{ label: 'Trousers', value: 'trousers' },
{ label: 'Blouse', value: 'blouse' },
{ label: 'Skirt', value: 'skirt' },
{ label: 'Accessories', value: 'accessories' }
]
const categories = computed<FilterOption[]>(() => [
{ label: t('Wardrobe.common.all'), value: 'all' },
...clothesCategories.value.map((option) => ({
label: option.label,
value: option.value
}))
])
const genders: FilterOption[] = [
{ label: 'All', value: 'all' },
{ label: 'Male', value: 'male' },
{ label: 'Female', value: 'female' }
]
const genders = computed<FilterOption[]>(() => [
{ label: t('Wardrobe.common.all'), value: 'all' },
{ label: t('Wardrobe.assets.genders.male'), value: 'male' },
{ label: t('Wardrobe.assets.genders.female'), value: 'female' }
])
const categoryValues = categories
.filter((option) => option.value !== 'all')
.map((option) => option.value)
const categoryValues = computed(() =>
categories.value.filter((option) => option.value !== 'all').map((option) => option.value)
)
const filters = reactive({
categories: ['skirt'] as string[],
@@ -209,8 +219,8 @@ const selectedCount = computed(() => {
})
const allCategoriesSelected = computed(() => {
return (
filters.categories.length === categoryValues.length &&
categoryValues.every((value) => filters.categories.includes(value))
filters.categories.length === categoryValues.value.length &&
categoryValues.value.every((value) => filters.categories.includes(value))
)
})
@@ -224,7 +234,7 @@ const isCategoryActive = (value: string) => {
const toggleCategory = (value: string) => {
if (value === 'all') {
filters.categories = allCategoriesSelected.value ? [] : [...categoryValues]
filters.categories = allCategoriesSelected.value ? [] : [...categoryValues.value]
return
}
@@ -241,7 +251,7 @@ const setGender = (value: string) => {
}
const clearFilters = () => {
filters.categories = [...categoryValues]
filters.categories = [...categoryValues.value]
filters.gender = 'all'
}
@@ -456,9 +466,9 @@ onUnmounted(() => {
font-family: 'KaiseiOpti-Regular';
.select-count {
column-gap: 1.2rem;
img{
img {
width: 2.4rem;
height: 2.4rem ;
height: 2.4rem;
}
.assets-toolbar__count {
position: relative;
@@ -544,7 +554,6 @@ onUnmounted(() => {
}
}
}
}
}
</style>

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n({ useScope: 'global' })
const emit = defineEmits<{
(event: 'explore'): void
}>()
@@ -8,12 +12,12 @@ const emit = defineEmits<{
<div class="wardrobe-empty flex flex-col flex-center">
<img src="@/assets/images/wardrobe/empty-wardrobe.png" class="wardrobe-empty__image" alt="" />
<h2 class="wardrobe-empty__title">Nothing in Wardrobe yet</h2>
<h2 class="wardrobe-empty__title">{{ t('Wardrobe.empty.title') }}</h2>
<p class="wardrobe-empty__description">
Explore the digital item and add pieces to your collection.
{{ t('Wardrobe.empty.description') }}
</p>
<button class="wardrobe-empty__button" type="button" @click="emit('explore')">
Explore Digital Items
{{ t('Wardrobe.empty.action') }}
</button>
</div>
</template>

View File

@@ -33,17 +33,17 @@
:style="{ backgroundColor: item.color }"
></span>
<span v-if="getExtraCount(order)" class="order-card__extra">
+{{ getExtraCount(order) }} more
{{ t('Wardrobe.orders.moreItems', { count: getExtraCount(order) }) }}
</span>
</div>
<div class="order-card__status" :class="`is-${order.status}`">
{{ order.status.toUpperCase() }}
{{ getStatusBadgeLabel(order.status) }}
</div>
<div class="order-card__amount">
<span>${{ order.amount }}</span>
<small>HKD</small>
<small>{{ t('Wardrobe.common.currencyHkd') }}</small>
</div>
<button
@@ -52,9 +52,7 @@
:class="{ 'is-primary': order.status === 'unpaid' }"
>
<svg-icon v-if="order.status === 'paid'" name="Invoice" size="20" color="#232323" />
<span v-if="order.status === 'paid'">Invoice</span>
<span v-else-if="order.status === 'unpaid'">Complete Payment</span>
<span v-else>Buy Again</span>
<span>{{ getOrderActionLabel(order.status) }}</span>
</button>
</div>
@@ -77,6 +75,7 @@
<script setup lang="ts">
import { computed, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import ScItem from '@/views/shoppingCart/sc-item.vue'
type OrderStatus = 'all' | 'paid' | 'unpaid' | 'cancelled'
@@ -109,13 +108,14 @@ interface OrderRecord {
}
const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
const { t } = useI18n({ useScope: 'global' })
const statusOptions: StatusOption[] = [
{ key: 'all', label: 'All' },
{ key: 'paid', label: 'Paid' },
{ key: 'unpaid', label: 'Unpaid' },
{ key: 'cancelled', label: 'Canceled' }
]
const statusOptions = computed<StatusOption[]>(() => [
{ key: 'all', label: t('Wardrobe.orders.statuses.all') },
{ key: 'paid', label: t('Wardrobe.orders.statuses.paid') },
{ key: 'unpaid', label: t('Wardrobe.orders.statuses.unpaid') },
{ key: 'cancelled', label: t('Wardrobe.orders.statuses.cancelled') }
])
const createOrderItem = (
id: number,
@@ -201,6 +201,16 @@ const toggleOrder = (orderId: string) => {
const getExtraCount = (order: OrderRecord) => {
return Math.max(order.items.length - 2, 0)
}
const getStatusBadgeLabel = (status: ActualOrderStatus) => {
return t(`Wardrobe.orders.statusBadges.${status}`)
}
const getOrderActionLabel = (status: ActualOrderStatus) => {
if (status === 'paid') return t('Wardrobe.orders.actions.invoice')
if (status === 'unpaid') return t('Wardrobe.orders.actions.completePayment')
return t('Wardrobe.orders.actions.buyAgain')
}
</script>
<style lang="less" scoped>

View File

@@ -1,13 +1,13 @@
<template>
<div class="wardrobe-page">
<div class="wardrobe-hero flex flex-col flex-center">
<div class="wardrobe-hero__title">My Wardrobe</div>
<div class="wardrobe-hero__subtitle">Your digital pieces, all in one place</div>
<div class="wardrobe-hero__title">{{ t('Wardrobe.title') }}</div>
<div class="wardrobe-hero__subtitle">{{ t('Wardrobe.subtitle') }}</div>
</div>
<div class="wardrobe-shell">
<div class="wardrobe-tabs">
<div class="wardrobe-tabs__nav" role="tablist" aria-label="Wardrobe tabs">
<div class="wardrobe-tabs__nav" role="tablist" :aria-label="t('Wardrobe.tabs.ariaLabel')">
<button
v-for="tab in tabs"
:key="tab.key"
@@ -23,8 +23,8 @@
</div>
<div v-if="activeTab === 'assets'" class="wardrobe-tabs__sort">
<div class="wardrobe-tabs__sort-label">Sort by</div>
<el-select v-model="activeSort" placeholder="Select">
<div class="wardrobe-tabs__sort-label">{{ t('Wardrobe.sort.label') }}</div>
<el-select v-model="activeSort" :placeholder="t('Wardrobe.sort.placeholder')">
<el-option
v-for="option in sortOptions"
:key="option.value"
@@ -42,6 +42,7 @@
</template>
<script setup lang="ts">
import { computed, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import Assets from './Assets.vue'
import Orders from './Orders.vue'
@@ -57,31 +58,33 @@ interface SortOption {
value: number
}
const tabs: TabItem[] = [
const { t } = useI18n({ useScope: 'global' })
const tabs = computed<TabItem[]>(() => [
{
key: 'assets',
label: 'Assets'
label: t('Wardrobe.tabs.assets')
},
{
key: 'orders',
label: 'Orders'
label: t('Wardrobe.tabs.orders')
}
]
])
const sortOptions: SortOption[] = [
const sortOptions = computed<SortOption[]>(() => [
{
label: 'Default',
label: t('Wardrobe.sort.default'),
value: 0
},
{
label: 'Date Added',
label: t('Wardrobe.sort.dateAdded'),
value: 1
},
{
label: 'Selected First',
label: t('Wardrobe.sort.selectedFirst'),
value: 2
}
]
])
const activeTab = shallowRef<WardrobeTab>('assets')
const activeSort = shallowRef(1)