feat: 服装分类
This commit is contained in:
27
src/api/user.ts
Normal file
27
src/api/user.ts
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '其他'
|
||||
}
|
||||
}
|
||||
|
||||
71
src/utils/ClothesCategory.ts
Normal file
71
src/utils/ClothesCategory.ts
Normal 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))
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user