This commit is contained in:
李志鹏
2026-04-23 16:11:14 +08:00
8 changed files with 975 additions and 0 deletions

View File

@@ -141,3 +141,6 @@ button[custom="black"] {
--button-click-color: #fff;
--button-font-size: 1.6rem;
}
.el-select-dropdown__item {
padding: 0 2rem !important;
}

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.32243 7.4255L9.409 10.5121C9.43299 10.5361 9.46897 10.5441 9.50096 10.5321C9.53294 10.5201 9.55293 10.4881 9.55293 10.4561V2.94757C9.55293 2.69968 9.75284 2.49977 10.0007 2.49977C10.2486 2.49977 10.4485 2.69968 10.4485 2.94757V10.4561C10.4485 10.4921 10.4685 10.5201 10.5005 10.5321C10.5325 10.5441 10.5685 10.5401 10.5925 10.5121L13.679 7.4255C13.851 7.25758 14.1468 7.25758 14.3147 7.4255C14.3987 7.50946 14.4467 7.6254 14.4467 7.74535C14.4467 7.86529 14.3987 7.97724 14.3147 8.0652L10.1287 12.2513C10.0607 12.3192 9.93676 12.3192 9.86879 12.2513L5.68272 8.0652C5.59876 7.98124 5.55078 7.86529 5.55078 7.74535C5.55078 7.6254 5.59876 7.51346 5.68272 7.4255C5.85064 7.25758 6.1505 7.25758 6.31843 7.4255H6.32243Z" fill="white"/>
<path d="M17.1966 12.6064C16.9487 12.6064 16.7488 12.8064 16.7488 13.0542V16.2528C16.7488 16.4447 16.5929 16.6046 16.397 16.6046H3.60289C3.41098 16.6046 3.25106 16.4487 3.25106 16.2528V13.0542C3.25106 12.8064 3.05115 12.6064 2.80326 12.6064C2.55538 12.6064 2.35547 12.8064 2.35547 13.0542V16.2528C2.35547 16.9404 2.91521 17.5002 3.60289 17.5002H16.397C17.0847 17.5002 17.6444 16.9404 17.6444 16.2528V13.0542C17.6444 12.8064 17.4445 12.6064 17.1966 12.6064Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -50,6 +50,11 @@ const router = createRouter({
name: 'notifications',
component: () => import('@/views/notifications/index.vue')
},
{
path: '/wardrobe',
name: 'wardrobe',
component: () => import('@/views/wardrobe/index.vue')
},
{
path: '/:pathMatch(.*)',
name: '404',

View File

@@ -117,6 +117,7 @@
const onMyWardrobe = () => {
hideProfilePopover()
console.log('my wardrobe')
router.push('/wardrobe')
}
const onNotifications = () => {
hideProfilePopover()

View File

@@ -0,0 +1,486 @@
<template>
<div class="wardrobe-assets flex">
<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>
</div>
<section class="filter-group">
<h3 class="filter-group__title">Categories</h3>
<div class="filter-group__line"></div>
<div class="filter-group__options">
<button
v-for="option in categories"
:key="option.value"
class="filter-option"
type="button"
:class="{ 'is-active': isCategoryActive(option.value) }"
@click="toggleCategory(option.value)"
>
<span class="filter-option__box">
<span class="filter-option__tick"></span>
</span>
<span class="filter-option__label">{{ option.label }}</span>
</button>
</div>
</section>
<section class="filter-group">
<h3 class="filter-group__title">Gender</h3>
<div class="filter-group__line"></div>
<div class="filter-group__options">
<button
v-for="option in genders"
:key="option.value"
class="filter-option"
type="button"
:class="{ 'is-active': filters.gender === option.value }"
@click="setGender(option.value)"
>
<span class="filter-option__box">
<span class="filter-option__tick"></span>
</span>
<span class="filter-option__label">{{ option.label }}</span>
</button>
</div>
</section>
</div>
</aside>
<section class="wardrobe-assets__content flex flex-1 flex-col">
<div class="assets-toolbar">
<div class="assets-toolbar__selection">
<span class="assets-toolbar__count">{{ selectedCount }} Selected</span>
<div class="assets-toolbar__link">Select All</div>
<div class="assets-toolbar__link">Deselect All</div>
</div>
<div class="assets-toolbar__actions">
<div
class="assets-toolbar__download flex flex-center"
:class="{ disabled: selectedCount < 1 }"
>
<SvgIcon name="download" color="#fff" />
<span>Download Selected</span>
</div>
</div>
</div>
<div v-if="dataList.length" class="data-list-container">
<div class="data-list datalist">
<div class="item" v-for="item in dataList" :key="item.url">
<CommodityItem :url="item.url" :name="item.title" :price="item.price"></CommodityItem>
</div>
</div>
</div>
<div v-else class="assets-empty flex flex-col flex-center">
<img src="@/assets/images/wardrobe/empty-wardrobe.png" class="empty-img" alt="" />
<h2 class="assets-empty__title">Nothing in Wardrobe yet</h2>
<p class="assets-empty__description">
Explore the digital item and add pieces to your collection.
</p>
<div class="assets-empty__button" type="button" @click="goToDigitalItems">
Explore Digital Items
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { computed, ref, reactive, watch } from 'vue'
import { useRouter } from 'vue-router'
import img from '@/assets/images/collectionStory/Rectangle.png'
interface FilterOption {
label: string
value: string
}
const router = useRouter()
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 genders: FilterOption[] = [
{ label: 'All', value: 'all' },
{ label: 'Male', value: 'male' },
{ label: 'Female', value: 'female' }
]
const categoryValues = categories
.filter((option) => option.value !== 'all')
.map((option) => option.value)
const filters = reactive({
categories: ['skirt'] as string[],
gender: 'all'
})
const dataList = ref([
{
url: img,
title: 'Windswept Burden',
price: '$100.00'
},
{
url: img,
title: 'Windswept Burden',
price: '$100.00'
},
{
url: img,
title: 'Windswept Burden',
price: '$100.00'
},
{
url: img,
title: 'Windswept Burden',
price: '$100.00'
},
{
url: img,
title: 'Windswept Burden',
price: '$100.00'
},
{
url: img,
title: 'Windswept Burden',
price: '$100.00'
},
{
url: img,
title: 'Windswept Burden',
price: '$100.00'
},
{
url: img,
title: 'Windswept Burden',
price: '$100.00'
}
])
watch(
() => filters,
(val) => {
console.log(val)
},
{ deep: true }
)
const selectedCount = computed(() => 0)
const allCategoriesSelected = computed(() => {
return (
filters.categories.length === categoryValues.length &&
categoryValues.every((value) => filters.categories.includes(value))
)
})
const isCategoryActive = (value: string) => {
if (value === 'all') {
return allCategoriesSelected.value
}
return filters.categories.includes(value)
}
const toggleCategory = (value: string) => {
if (value === 'all') {
filters.categories = allCategoriesSelected.value ? [] : [...categoryValues]
return
}
if (filters.categories.includes(value)) {
filters.categories = filters.categories.filter((item) => item !== value)
return
}
filters.categories = [...filters.categories, value]
}
const setGender = (value: string) => {
filters.gender = value
}
const clearFilters = () => {
filters.categories = [...categoryValues]
filters.gender = 'all'
}
const goToDigitalItems = () => {
router.push('/digitalItem')
}
</script>
<style lang="less" scoped>
.c-svg {
width: initial;
height: initial;
}
.wardrobe-assets {
--wardrobe-border-color: #d9d4cd;
--wardrobe-border-dark: #c8c0b4;
--wardrobe-text-main: #232323;
--wardrobe-text-secondary: #7a746d;
--wardrobe-text-muted: #a0978b;
height: 100%;
// overflow: hidden;
padding: 0 9rem 0 10rem;
.wardrobe-assets__filters {
width: 26.4rem;
border-right: 0.1rem solid var(--wardrobe-border-color);
// background: #fffcf7;
overflow-y: auto;
.filters-card {
padding: 3rem 2.4rem 4rem;
.filters-card__heading {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 3.2rem;
.filters-card__title {
margin: 0;
font-family: 'KaiseiOpti-Bold';
font-size: 2.4rem;
line-height: 1.2;
color: var(--wardrobe-text-main);
}
.filters-card__clear {
border: 0;
padding: 0;
background: transparent;
font-family: 'KaiseiOpti-Regular';
font-size: 1.4rem;
line-height: 1.3;
color: #9a9185;
text-decoration: underline;
cursor: pointer;
}
}
.filter-group {
& + .filter-group {
margin-top: 3.4rem;
}
.filter-group__title {
margin: 0 0 1rem;
font-family: 'KaiseiOpti-Bold';
font-size: 1.8rem;
line-height: 1.3;
color: #5e5851;
}
.filter-group__line {
width: 100%;
height: 0.1rem;
background: var(--wardrobe-border-color);
margin-bottom: 2rem;
}
.filter-group__options {
display: flex;
flex-direction: column;
gap: 1.2rem;
.filter-option {
display: inline-flex;
align-items: center;
gap: 1.2rem;
width: fit-content;
padding: 0;
border: 0;
background: transparent;
font-family: 'KaiseiOpti-Regular';
font-size: 1.5rem;
line-height: 1.4;
color: #6e665d;
cursor: pointer;
text-align: left;
> .filter-option__box {
width: 1.6rem;
height: 1.6rem;
border: 0.1rem solid var(--wardrobe-border-dark);
display: inline-flex;
align-items: center;
justify-content: center;
background: #ffffff;
flex-shrink: 0;
.filter-option__tick {
width: 0.9rem;
height: 0.5rem;
border-left: 0.18rem solid #ffffff;
border-bottom: 0.18rem solid #ffffff;
transform: rotate(-45deg) translateY(-0.05rem);
opacity: 0;
}
}
&.is-active {
color: var(--wardrobe-text-main);
.filter-option__box {
border-color: var(--wardrobe-text-main);
background: var(--wardrobe-text-main);
.filter-option__tick {
opacity: 1;
}
}
}
}
}
}
}
}
.wardrobe-assets__content {
min-width: 0;
border-right: 0.05rem solid #585858;
.assets-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.8rem 1.2rem;
border-bottom: 0.05rem solid #585858;
.assets-toolbar__selection {
display: flex;
align-items: center;
gap: 1.4rem;
flex-wrap: wrap;
}
.assets-toolbar__selection {
.assets-toolbar__count {
position: relative;
padding-left: 1.8rem;
font-family: 'KaiseiOpti-Regular';
font-size: 1.4rem;
line-height: 1.2;
color: #57524b;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
width: 1rem;
height: 1rem;
background: #232323;
transform: translateY(-50%);
}
}
.assets-toolbar__link {
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
font-family: 'KaiseiOpti-Regular';
font-size: 1.4rem;
line-height: 1.2;
color: #a0978b;
text-decoration: underline;
}
}
.assets-toolbar__actions {
display: flex;
align-items: center;
.assets-toolbar__download {
height: 4.4rem;
padding: 0 3rem;
border: 0.1rem solid #232323;
background: #232323;
font-family: 'KaiseiOpti-Regular';
font-size: 1.3rem;
color: #fff;
cursor: pointer;
column-gap: 1.2rem;
&.disabled {
background-color: #979797;
border-color: #979797;
}
}
}
}
.data-list-container {
overflow-y: auto;
.datalist {
width: 100%;
flex: 1;
display: grid;
align-content: start;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 28rem), 1fr));
padding: 0.05rem 0 0 0.05rem;
.item {
width: 100%;
min-width: 0;
padding: 1.2rem;
border: 0.05rem solid #585858;
margin-left: -0.05rem;
margin-top: -0.05rem;
:deep(.commodity-item) {
width: 100%;
}
}
}
}
.assets-empty {
flex: 1;
color: #979797;
.empty-img {
width: 14.2rem;
height: 18.8rem;
}
.assets-empty__title {
font-family: 'KaiseiOpti-Bold';
font-size: 1.6rem;
margin: 2.4rem 0 0.8rem;
}
.assets-empty__description {
font-family: 'KaiseiOpti-Regular';
font-size: 1.4rem;
}
.assets-empty__button {
margin-top: 3rem;
height: 4.4rem;
line-height: 4.4rem;
padding: 0 3.8rem;
border: 0.1rem solid #c4c4c4;
font-family: 'KaiseiOpti-Regular';
font-size: 1.6rem;
color: #585858;
cursor: pointer;
text-transform: uppercase;
}
}
}
}
</style>

View File

@@ -0,0 +1,257 @@
<script setup lang="ts">
import { shallowRef } from 'vue'
import { useRouter } from 'vue-router'
type OrderStatus = 'all' | 'paid' | 'unpaid' | 'cancelled'
interface StatusOption {
key: OrderStatus
label: string
}
const router = useRouter()
const statusOptions: StatusOption[] = [
{ key: 'all', label: 'All' },
{ key: 'paid', label: 'Paid' },
{ key: 'unpaid', label: 'Unpaid' },
{ key: 'cancelled', label: 'Cancelled' }
]
const activeStatus = shallowRef<OrderStatus>('all')
const goToDigitalItems = () => {
router.push('/digitalItem')
}
</script>
<template>
<div class="wardrobe-orders">
<div class="orders-toolbar">
<button
v-for="status in statusOptions"
:key="status.key"
class="orders-toolbar__chip"
type="button"
:class="{ 'is-active': activeStatus === status.key }"
@click="activeStatus = status.key"
>
{{ status.label }}
</button>
</div>
<div class="orders-empty">
<div class="orders-empty__illustration" aria-hidden="true">
<div class="orders-empty__hook"></div>
<div class="orders-empty__hanger"></div>
<div class="orders-empty__body"></div>
<div class="orders-empty__hem"></div>
</div>
<h2 class="orders-empty__title">Nothing in Wardrobe yet</h2>
<p class="orders-empty__description">
Explore the digital item and add pieces to your collection.
</p>
<button class="orders-empty__button" type="button" @click="goToDigitalItems">
Explore Digital Items
</button>
</div>
</div>
</template>
<style lang="less" scoped>
.wardrobe-orders {
--wardrobe-border-color: #d9d4cd;
--wardrobe-border-dark: #c8c0b4;
height: 100%;
display: flex;
flex-direction: column;
background: #fffdf8;
> .orders-toolbar {
display: flex;
align-items: center;
gap: 1rem;
padding: 2.2rem 3rem 0;
flex-wrap: wrap;
> .orders-toolbar__chip {
height: 3rem;
padding: 0 1.4rem;
border: 0.1rem solid var(--wardrobe-border-color);
border-radius: 999rem;
background: #ffffff;
font-family: 'KaiseiOpti-Regular';
font-size: 1.2rem;
line-height: 1;
color: #8b8277;
cursor: pointer;
&.is-active {
background: #232323;
border-color: #232323;
color: #ffffff;
}
}
}
> .orders-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem 7rem;
text-align: center;
> .orders-empty__illustration {
position: relative;
width: 8.8rem;
height: 10.4rem;
margin-bottom: 2.4rem;
opacity: 0.55;
> .orders-empty__hook {
position: absolute;
left: 50%;
top: 0;
width: 1.6rem;
height: 1.6rem;
border: 0.14rem solid #b8b1a5;
border-bottom-color: transparent;
border-left-color: transparent;
border-radius: 50%;
transform: translateX(-10%) rotate(-18deg);
}
> .orders-empty__hanger {
position: absolute;
left: 50%;
top: 1.2rem;
width: 4rem;
height: 2rem;
border-top: 0.14rem solid #b8b1a5;
border-left: 0.14rem solid #b8b1a5;
border-right: 0.14rem solid #b8b1a5;
transform: translateX(-50%) skewY(-12deg);
}
> .orders-empty__body {
position: absolute;
left: 50%;
top: 3.2rem;
width: 4.8rem;
height: 4.8rem;
border: 0.14rem solid #b8b1a5;
border-top: 0;
transform: translateX(-50%);
&::before,
&::after {
content: '';
position: absolute;
top: 0.2rem;
width: 1.7rem;
height: 2.6rem;
border: 0.14rem solid #b8b1a5;
border-bottom: 0;
}
&::before {
left: -1.2rem;
transform: skewY(22deg);
}
&::after {
right: -1.2rem;
transform: skewY(-22deg);
}
}
> .orders-empty__hem {
position: absolute;
left: 50%;
bottom: 0.8rem;
width: 4.6rem;
height: 2rem;
border-left: 0.14rem solid #b8b1a5;
border-right: 0.14rem solid #b8b1a5;
border-bottom: 0.14rem solid #b8b1a5;
transform: translateX(-50%);
&::before,
&::after {
content: '';
position: absolute;
top: 0;
width: 1.3rem;
height: 100%;
border-bottom: 0.14rem solid #b8b1a5;
}
&::before {
left: -0.1rem;
transform: skewX(24deg);
transform-origin: left bottom;
}
&::after {
right: -0.1rem;
transform: skewX(-24deg);
transform-origin: right bottom;
}
}
}
> .orders-empty__title {
margin: 0;
font-family: 'KaiseiOpti-Bold';
font-size: 1.8rem;
line-height: 1.3;
color: #8c8479;
}
> .orders-empty__description {
max-width: 32rem;
margin: 1rem 0 0;
font-family: 'KaiseiOpti-Regular';
font-size: 1.3rem;
line-height: 1.7;
color: #b1a99e;
}
> .orders-empty__button {
margin-top: 2rem;
height: 3.8rem;
padding: 0 2.4rem;
border: 0.1rem solid var(--wardrobe-border-color);
background: #ffffff;
font-family: 'KaiseiOpti-Regular';
font-size: 1.2rem;
line-height: 1;
letter-spacing: 0.06rem;
text-transform: uppercase;
color: #6d655b;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
&:hover {
background: #f8f3ea;
border-color: var(--wardrobe-border-dark);
}
}
}
}
@media (max-width: 960px) {
.wardrobe-orders {
> .orders-toolbar {
padding: 2rem 2rem 0;
}
> .orders-empty {
padding: 5rem 2rem 7rem;
}
}
}
</style>

View File

@@ -0,0 +1,219 @@
<template>
<div class="wardrobe-page">
<section 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>
</section>
<section class="wardrobe-shell">
<div class="wardrobe-tabs">
<div class="wardrobe-tabs__nav" role="tablist" aria-label="Wardrobe tabs">
<button
v-for="tab in tabs"
:key="tab.key"
class="wardrobe-tabs__item"
:class="{ 'is-active': activeTab === tab.key }"
type="button"
role="tab"
:aria-selected="activeTab === tab.key"
@click="activeTab = tab.key"
>
{{ tab.label }}
</button>
</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">
<el-option
v-for="option in sortOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
</div>
<component :is="activePanel" class="wardrobe-shell__panel" />
</section>
</div>
</template>
<script setup lang="ts">
import { computed, shallowRef } from 'vue'
import Assets from './Assets.vue'
import Orders from './Orders.vue'
type WardrobeTab = 'assets' | 'orders'
interface TabItem {
key: WardrobeTab
label: string
}
interface SortOption {
label: string
value: number
}
const tabs: TabItem[] = [
{
key: 'assets',
label: 'Assets'
},
{
key: 'orders',
label: 'Orders'
}
]
const sortOptions: SortOption[] = [
{
label: 'Default',
value: 0
},
{
label: 'Date Added',
value: 1
},
{
label: 'Selected First',
value: 2
}
]
const activeTab = shallowRef<WardrobeTab>('assets')
const activeSort = shallowRef(1)
const activePanel = computed(() => {
return activeTab.value === 'assets' ? Assets : Orders
})
</script>
<style lang="less" scoped>
.wardrobe-page {
--wardrobe-border-color: #d9d4cd;
--wardrobe-text-main: #232323;
--wardrobe-text-secondary: #7a746d;
--wardrobe-surface: #fffdf8;
height: 100%;
display: flex;
flex-direction: column;
background: #ffffff;
overflow: hidden;
.wardrobe-hero {
height: 14.8rem;
background-color: #f5f5f5;
.wardrobe-hero__title {
margin: 0;
font-family: 'KaiseiOpti-Bold';
font-size: 4rem;
color: #232323;
line-height: 3.6rem;
}
.wardrobe-hero__subtitle {
margin-top: 1rem;
font-family: 'KaiseiOpti-Regular';
font-size: 1.4rem;
line-height: 2.4rem;
color: #585858;
}
}
> .wardrobe-shell {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
> .wardrobe-tabs {
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
padding: 0 9rem;
border-bottom: 0.1rem solid var(--wardrobe-border-color);
background: #ffffff;
> .wardrobe-tabs__nav {
display: flex;
align-items: center;
> .wardrobe-tabs__item {
position: relative;
height: 6rem;
padding: 0 1.8rem;
border: 0;
background: transparent;
font-family: 'KaiseiOpti-Regular';
font-size: 1.4rem;
line-height: 1;
color: #7d766f;
cursor: pointer;
width: 13.9rem;
&::after {
content: '';
position: absolute;
left: 1.8rem;
right: 1.8rem;
bottom: -0.1rem;
height: 0.2rem;
background: transparent;
transition: background-color 0.2s ease;
}
&.is-active {
font-family: 'KaiseiOpti-Bold';
color: var(--wardrobe-text-main);
&::after {
background: var(--wardrobe-text-main);
}
}
}
}
> .wardrobe-tabs__sort {
display: flex;
align-items: center;
gap: 1rem;
flex-shrink: 0;
> .wardrobe-tabs__sort-label {
font-family: 'KaiseiOpti-Regular';
font-size: 1.4rem;
line-height: 2.4rem;
color: #7d766f;
white-space: nowrap;
}
:deep(.el-select) {
width: 13rem;
}
:deep(.el-select__wrapper) {
min-height: 3.6rem;
padding: 0 1.2rem;
background: #ffffff;
box-shadow: none;
}
:deep(.el-select__selected-item) {
font-family: 'KaiseiOpti-Regular';
font-size: 1.4rem;
color: #232323;
}
}
}
> .wardrobe-shell__panel {
flex: 1;
min-height: 0;
}
}
}
</style>