This commit is contained in:
李志鹏
2026-04-23 11:49:00 +08:00
15 changed files with 1330 additions and 646 deletions

5
src/assets/icons/eye.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg width="19" height="13" viewBox="0 0 19 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 6.49998C3.33333 1.97224 10.9 -4.36659 18.5 6.49998" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 6.49998C15.6667 11.0277 8.1 17.3666 0.5 6.49998" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9.5" cy="6.49998" r="2.5" stroke="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

View File

@@ -34,7 +34,7 @@ const {} = toRefs(data);
<div class="commodity-item">
<img :src="props.url" alt="">
<div class="detail">
<div calss="text">
<div class="text">
<div class="name">
{{ props.name }}
</div>
@@ -44,7 +44,7 @@ const {} = toRefs(data);
</div>
<div class="btn" @click="addShopping">
<div class="text">
<SvgIcon name="add" size="24"></SvgIcon>
<SvgIcon name="add" size="26"></SvgIcon>
</div>
</div>
</div>
@@ -62,13 +62,14 @@ const {} = toRefs(data);
display: flex;
justify-content: space-between;
align-items: center;
.text{
> .text{
color: #232323;
> .name{
font-family: "KaiseiOpti-Regular";
font-weight: 400;
font-size: var(--commodity-name-fontSize,1.6rem);
line-height: var(--commodity-name-lineHeight,2.3rem);
margin-bottom: var(--commodity-name-marginBottom,0rem);
}
> .price{
font-family: "KaiseiOpti-Regular";

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs, computed } from "vue";
const props = defineProps({
list:{
type:Array,
default:()=>[]
},
selected:{
type:String,
default:()=>''
}
})
const emit = defineEmits([
'update:selected'
])
const checkList = computed(()=>{
return [props.selected]
})
const handleChange = (val) => {
if (val.length > 1) {
emit('update:selected', val[val.length - 1])
}
}
let data = reactive({
})
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<el-checkbox-group v-model="checkList" @change="handleChange">
<el-checkbox
v-for="item in props.list"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</template>
<style lang="less" scoped>
.el-checkbox-group{
display: flex;
flex-direction: column;
gap: 1.2rem;
}
label{
--el-checkbox-font-size: 1.6rem;
--el-checkbox-checked-text-color: #232323;
--el-checkbox-font-weight: 400;
--el-checkbox-height: 2rem;
--el-checkbox-checked-bg-color: #232323;
--el-checkbox-checked-input-border-color: #232323;
--el-checkbox-input-border: 1px solid #232323;
font-family: "KaiseiOpti-Regular";
line-height: 2rem;
.el-checkbox__label{
padding-left: 1.4rem;
}
}
</style>

View File

@@ -23,6 +23,11 @@ const router = createRouter({
name: 'brand',
component: () => import('../views/brand/index.vue')
},
{
path: '/digitalItem',
name: 'digitalItem',
component: () => import('../views/digitalItem/index.vue'),
},
{
path: '/settings',
name: 'settings',
@@ -34,6 +39,11 @@ const router = createRouter({
name: 'shoppingCart',
component: () => import('@/views/shoppingCart/index.vue')
},
{
path: '/notifications',
name: 'notifications',
component: () => import('@/views/notifications/index.vue')
},
{
path: '/:pathMatch(.*)',
name: '404',

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import img from "@/assets/images/collectionStory/Rectangle.png";
//const props = defineProps({
//})
const emit = defineEmits([
'addShopping'
])
let data = reactive({
})
const list = 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",
},
])
const type = ref('All')
const addShopping = (item) => {
emit('addShopping', item)
}
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="commodityList">
<div class="list">
<div class="item" v-for="item in list" :key="item.url">
<CommodityItem :url="item.url" :name="item.title" :price="item.price" @addShopping="addShopping(item)"></CommodityItem>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.commodityList{
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
.list{
width: 100%;
flex: 1;
display: grid;
align-content: start;
grid-template-columns: repeat(3, 1fr);
overflow-y: auto;
/* 垂直线(右边框) */
.item{
position: relative;
padding: 1.2rem;
--commodity-marginBottom: 2rem;
--commodity-name-fontSize: 2rem;
--commodity-name-marginBottom: .8rem;
--commodity-price-fontSize: 1.6rem;
}
.item::before {
content: '';
position: absolute;
right: 0;
top: 0;
height: 100%;
border-right: 0.5px solid #585858;
z-index: 1;
}
/* 水平线(下边框) */
.item::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
border-bottom: 0.5px solid #585858;
z-index: 1;
}
/* 移除最后一列的右边框 */
.item:nth-child(3n)::before {
display: none;
}
}
}
</style>

View File

@@ -0,0 +1,101 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import CommodityList from "./commodity-list.vue";
import MerchantInfo from "./merchant-info.vue";
//const props = defineProps({
//})
//const emit = defineEmits([
//])
let data = reactive({
})
const addShopping = (item) => {}
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="digitalItem">
<div class="header-img">
<img src="@/assets/images/digitalItem/digital_item_banner.png" alt="">
<div class="text">
<div class="title">Digital Item</div>
<p class="info">Virtual fashion creations collected in your personal archive</p>
</div>
</div>
<div class="content">
<div class="merchant-info">
<MerchantInfo></MerchantInfo>
</div>
<div class="commodity-list">
<CommodityList @addShopping="addShopping"></CommodityList>
</div>
</div>
<Footer></Footer>
</div>
</template>
<style lang="less" scoped>
.digitalItem{
width: 100%;
height: 100%;
position: relative;
overflow-y: auto;
.header-img{
width: 100%;
position: relative;
>img{
width: 100%;
}
> .text{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
> .title{
font-family: KaiseiOpti-Bold;
color: #232323;
font-weight: 700;
font-size: 4rem;
line-height: 2.3rem;
letter-spacing: 0%;
text-align: center;
}
> .info{
font-family: KaiseiOpti-Regular;
color: #585858;
font-size: 1.6rem;
line-height: 140%;
margin-top: 1.2rem;
text-align: center;
}
}
}
.content{
display: flex;
height: auto;
align-items: flex-start;
border-top: 0.5px solid #585858;
margin-top: 6rem;
.merchant-info{
width: 38.5rem;
padding-left: 10.2rem;
height: var(--app-view-height);
overflow-y: auto;
position: sticky;
top: 0;
&::-webkit-scrollbar{
width: 0;
height: 0;
}
}
.commodity-list{
flex: 1;
border-left: 0.5px solid #585858;
border-right: 0.5px solid #585858;
margin-right: 9rem;
}
}
}
</style>

View File

@@ -0,0 +1,135 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
//const props = defineProps({
//})
//const emit = defineEmits([
//])
let data = reactive({
})
const categoriesList = ref([
{
label: 'All',
value: 'All'
},
{
label: 'Outwear',
value: 'Outwear'
},
{
label: 'Dress',
value: 'Dress'
},
{
label: 'Trousers',
value: 'Trousers'
},
{
label: 'Blouse',
value: 'Blouse'
},
{
label: 'Skirt',
value: 'Skirt'
},
{
label: 'Accessories',
value: 'Accessories'
},
]);
const genderList = ref([
{
label: 'All',
value: 'All'
},
{
label: 'Male',
value: 'Male'
},
{
label: 'Female',
value: 'Female'
},
])
const categories = ref('All')
const gender = ref('All')
const clearFilters = () => {
categories.value = 'All'
gender.value = 'All'
}
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="filters">
<div class="title">
<div class="left">Filters</div>
<div class="right" @click="clearFilters">Clear</div>
</div>
<div class="categories">Categories</div>
<div class="line"></div>
<div class="multiple">
<checked :list="categoriesList" v-model:selected="categories" />
</div>
<div class="categories">Gender</div>
<div class="line"></div>
<div class="multiple">
<checked :list="genderList" v-model:selected="gender" />
</div>
</div>
</template>
<style lang="less" scoped>
.filters{
width: 100%;
height: auto;
position: relative;
padding-top: 4rem;
padding-bottom: 4rem;
.title{
margin-bottom: 3rem;
display: flex;
padding: 0 1.2rem;
.left{
margin-right: 12.2rem;
font-family: "KaiseiOpti-Bold";
font-weight: 700;
font-size: 2.4rem;
line-height: 3.5rem;
color: #232323;
}
.right{
text-decoration: underline;
font-family: "KaiseiOpti-Regular";
font-weight: 400;
font-size: 1.6rem;
line-height: 2.4rem;
letter-spacing: -0.48px;
text-align: right;
color: #979797;
cursor: pointer;
}
}
.categories{
font-family: "KaiseiOpti-Bold";
font-weight: 700;
font-size: 1.8rem;
line-height: 2.3rem;
color: #585858;
margin-bottom: 1.1rem;
padding: 0 1.2rem;
}
.line{
border-top: 0.5px solid #C4C4C4;
width: 27.1rem;
margin-bottom: 2.2rem;
}
.multiple{
padding: 0 2.3rem;
margin-bottom: 2.9rem;
}
}
</style>

View File

@@ -120,11 +120,11 @@
}
const onNotifications = () => {
hideProfilePopover()
console.log('notifications')
router.push('/notifications')
}
const onSettings = () => {
hideProfilePopover()
console.log('settings')
router.push('/settings')
}
const onLogout = () => {
hideProfilePopover()

View File

@@ -0,0 +1,136 @@
<template>
<article class="notification-item" :class="{ expanded: item.isExpanded, unread: item.isUnread }">
<button type="button" class="notification-trigger" @click="handleToggle">
<span class="status-dot" :class="{ visible: item.isUnread }" />
<div class="notification-main">
<h3 class="notification-title">{{ item.title }}</h3>
<p class="notification-date">{{ item.date }}</p>
</div>
<span class="notification-arrow" :class="arrowClasses" />
</button>
<div v-if="item.isExpanded" class="notification-content">
{{ item.content }}
</div>
</article>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { NotificationRecord } from '../types'
const props = defineProps<{
item: NotificationRecord
}>()
const emit = defineEmits<{
toggle: [id: NotificationRecord['id']]
}>()
const arrowClasses = computed(() => ({
expanded: props.item.isExpanded
}))
const handleToggle = () => {
emit('toggle', props.item.id)
}
</script>
<style lang="less" scoped>
.notification-item {
border-bottom: 0.05rem solid #ded8d2;
&.expanded {
.notification-title {
color: #585858;
font-family: 'KaiseiOpti-Medium';
}
}
}
.notification-trigger {
width: 100%;
padding: 2rem 0;
display: grid;
grid-template-columns: 0.8rem minmax(0, 1fr) 2.4rem;
align-items: center;
gap: 2.4rem;
border: none;
background: transparent;
cursor: pointer;
text-align: left;
}
.status-dot {
width: 0.8rem;
height: 0.8rem;
border-radius: 50%;
background: transparent;
transition: background-color 0.2s ease;
&.visible {
background: #232323;
}
}
.notification-main {
min-width: 0;
}
.notification-title {
margin: 0;
color: #232323;
font-size: 1.6rem;
line-height: 1.4;
font-family: 'KaiseiOpti-Bold';
}
.notification-date {
margin: 0.8rem 0 0;
color: #9f9f9f;
font-size: 1.2rem;
line-height: 1.4;
font-family: 'KaiseiOpti-Regular';
}
.notification-arrow {
width: 0.9rem;
height: 0.9rem;
justify-self: end;
border-right: 0.1rem solid #8f8f8f;
border-bottom: 0.1rem solid #8f8f8f;
transform: rotate(-45deg);
transition: transform 0.2s ease, border-color 0.2s ease;
&.expanded {
transform: rotate(45deg);
}
}
.notification-content {
padding: 0 5.6rem 2.4rem 3.2rem;
color: #585858;
font-size: 1.4rem;
line-height: 2;
font-family: 'KaiseiOpti-Regular';
}
@media (max-width: 768px) {
.notification-trigger {
gap: 1.6rem;
padding: 1.8rem 0;
grid-template-columns: 0.8rem minmax(0, 1fr) 1.8rem;
}
.notification-title {
font-size: 1.5rem;
}
.notification-content {
padding: 0 3rem 2rem 2.4rem;
font-size: 1.3rem;
line-height: 1.8;
}
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<section class="notifications-panel">
<div class="notifications-toolbar">
<div class="unread-summary">
<span class="unread-label">UNREAD</span>
<span class="unread-count">{{ unreadCount }}</span>
</div>
<button
type="button"
class="mark-all-button"
:disabled="unreadCount === 0"
@click="emit('markAllAsRead')"
>
Mark all as read
</button>
</div>
<div class="notifications-items">
<NotificationListItem
v-for="item in items"
:key="item.id"
:item="item"
@toggle="emit('toggleItem', $event)"
/>
</div>
</section>
</template>
<script setup lang="ts">
import NotificationListItem from './NotificationListItem.vue'
import type { NotificationRecord } from '../types'
defineProps<{
items: NotificationRecord[]
unreadCount: number
}>()
const emit = defineEmits<{
toggleItem: [id: NotificationRecord['id']]
markAllAsRead: []
}>()
</script>
<style lang="less" scoped>
.notifications-panel {
width: 100%;
}
.notifications-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
margin-bottom: 4rem;
}
.unread-summary {
display: inline-flex;
align-items: center;
gap: 1.3rem;
}
.unread-label {
color: #979797;
font-size: 1.4rem;
line-height: 1.4;
letter-spacing: 0.04em;
font-family: 'KaiseiOpti-Regular';
}
.unread-count {
min-width: 3.1rem;
height: 2.4rem;
padding: 0 0.8rem;
border-radius: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
background: #232323;
color: #ffffff;
font-size: 1.4rem;
line-height: 1;
font-family: 'KaiseiOpti-Bold';
}
.mark-all-button {
padding: 0;
border: none;
background: transparent;
color: #979797;
font-size: 1.4rem;
line-height: 1.4;
text-decoration: underline;
text-underline-offset: 0.2rem;
font-family: 'KaiseiOpti-Regular';
cursor: pointer;
&:disabled {
cursor: default;
opacity: 0.55;
}
}
@media (max-width: 768px) {
.notifications-toolbar {
align-items: flex-start;
flex-direction: column;
margin-bottom: 2.8rem;
}
}
</style>

View File

@@ -0,0 +1,166 @@
<template>
<div class="notifications-view mini-scrollbar">
<section class="notifications-hero">
<div class="notifications-hero__content">
<h1 class="notifications-hero__title">Notifications</h1>
<p class="notifications-hero__subtitle">System announcements and updates</p>
</div>
</section>
<section class="notifications-content">
<NotificationsList
:items="notifications"
:unread-count="unreadCount"
@toggle-item="handleToggleItem"
@mark-all-as-read="handleMarkAllAsRead"
/>
</section>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import NotificationsList from './components/NotificationsList.vue'
import type { NotificationRecord } from './types'
const notifications = ref<NotificationRecord[]>([
{
id: 'maintenance-mar-10-primary',
title: 'Platform Maintenance Notice - Mar 10, 2026',
date: 'Mar 6, 2026',
content:
'We will perform scheduled platform maintenance on Mar 10, 2026 to improve checkout stability and notification delivery. During this window, a few account features may respond more slowly than usual.',
isUnread: true,
isExpanded: false
},
{
id: 'maintenance-mar-10-reminder',
title: 'Platform Maintenance Notice - Mar 10, 2026',
date: 'Feb 28, 2026',
content:
'This is an early reminder for the Mar 10 maintenance window. Please avoid making urgent profile or order updates right before the scheduled service period.',
isUnread: true,
isExpanded: false
},
{
id: 'terms-mar-1',
title: 'Updated Terms of Service - Effective Mar 1, 2026',
date: 'Feb 20, 2026',
content:
'We updated our Terms of Service to clarify digital item ownership, payment processing responsibilities, and account conduct expectations. Please review the new terms before your next purchase.',
isUnread: true,
isExpanded: false
},
{
id: 'welcome',
title: 'Welcome to Stylish Parade',
date: 'Jan 4, 2026',
content:
'Thanks for joining Stylish Parade. Explore brand stories, save your favorite pieces, and keep your profile updated so we can recommend the right collections for you.',
isUnread: false,
isExpanded: false
},
{
id: 'holiday-support',
title: 'Platform Maintenance Notice - Mar 10, 2026',
date: 'Dec 20, 2025',
content:
'Our customer support team will have limited availability during the holiday season from Dec 24 to Jan 2. Response times may be longer than usual. The platform will remain fully operational throughout this period. We wish you a wonderful holiday season.',
isUnread: false,
isExpanded: true
}
])
const unreadCount = computed(() => notifications.value.filter((item) => item.isUnread).length)
const handleToggleItem = (id: NotificationRecord['id']) => {
const targetItem = notifications.value.find((item) => item.id === id)
if (!targetItem) return
const nextExpanded = !targetItem.isExpanded
notifications.value = notifications.value.map((item) => ({
...item,
isUnread: item.id === id ? false : item.isUnread,
isExpanded: item.id === id ? nextExpanded : false
}))
}
const handleMarkAllAsRead = () => {
notifications.value = notifications.value.map((item) => ({
...item,
isUnread: false
}))
}
</script>
<style lang="less" scoped>
.notifications-view {
height: 100%;
overflow-y: auto;
background: #ffffff;
}
.notifications-hero {
min-height: 14.8rem;
padding: 3.2rem 2rem;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, rgba(245, 243, 240, 0.92) 0%, rgba(250, 249, 246, 0.95) 100%),
linear-gradient(
90deg,
rgba(227, 221, 212, 0.28) 0%,
rgba(255, 255, 255, 0.24) 50%,
rgba(227, 221, 212, 0.28) 100%
);
border-bottom: 0.05rem solid #dfd8d1;
}
.notifications-hero__content {
text-align: center;
}
.notifications-hero__title {
margin: 0;
color: #232323;
font-size: 4rem;
line-height: 1;
font-family: 'KaiseiOpti-Bold';
}
.notifications-hero__subtitle {
margin: 1.2rem 0 0;
color: #585858;
font-size: 1.6rem;
line-height: 1.4;
font-family: 'KaiseiOpti-Regular';
}
.notifications-content {
width: min(108rem, calc(100% - 4rem));
margin: 0 auto;
padding: 4rem 0 8rem;
}
@media (max-width: 768px) {
.notifications-hero {
min-height: 12rem;
padding: 2.8rem 1.6rem;
}
.notifications-hero__title {
font-size: 3rem;
}
.notifications-hero__subtitle {
margin-top: 0.8rem;
font-size: 1.4rem;
}
.notifications-content {
width: calc(100% - 3.2rem);
padding: 3.2rem 0 4.8rem;
}
}
</style>

View File

@@ -0,0 +1,8 @@
export interface NotificationRecord {
id: string
title: string
date: string
content: string
isUnread: boolean
isExpanded: boolean
}

View File

@@ -29,6 +29,7 @@ const props = defineProps<{
modelValue: string | number | boolean | Array<string | number | boolean> | null
options: Option[] // 按钮选项数组
multiple?: boolean // 是否支持多选,默认为 false
max?: number // 多选时最多可选数量,不传则不限制
}>()
const emit = defineEmits<{
@@ -54,6 +55,9 @@ const selectOption = (value: any) => {
if (index >= 0) {
current.splice(index, 1)
} else {
if (typeof props.max === 'number' && props.max > 0 && current.length >= props.max) {
current.shift()
}
current.push(value)
}
emit('update:modelValue', current)

File diff suppressed because it is too large Load Diff