Merge branch 'main' of http://18.167.251.121:10003/aidlab/Aida_Purchaser_Front
This commit is contained in:
5
src/assets/icons/eye.svg
Normal file
5
src/assets/icons/eye.svg
Normal 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 |
BIN
src/assets/images/digitalItem/digital_item_banner.png
Normal file
BIN
src/assets/images/digitalItem/digital_item_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 615 KiB |
@@ -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";
|
||||
|
||||
64
src/components/checked.vue
Normal file
64
src/components/checked.vue
Normal 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>
|
||||
@@ -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',
|
||||
|
||||
115
src/views/digitalItem/commodity-list.vue
Normal file
115
src/views/digitalItem/commodity-list.vue
Normal 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>
|
||||
101
src/views/digitalItem/index.vue
Normal file
101
src/views/digitalItem/index.vue
Normal 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>
|
||||
135
src/views/digitalItem/merchant-info.vue
Normal file
135
src/views/digitalItem/merchant-info.vue
Normal 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>
|
||||
@@ -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()
|
||||
|
||||
136
src/views/notifications/components/NotificationListItem.vue
Normal file
136
src/views/notifications/components/NotificationListItem.vue
Normal 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>
|
||||
111
src/views/notifications/components/NotificationsList.vue
Normal file
111
src/views/notifications/components/NotificationsList.vue
Normal 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>
|
||||
166
src/views/notifications/index.vue
Normal file
166
src/views/notifications/index.vue
Normal 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>
|
||||
8
src/views/notifications/types.ts
Normal file
8
src/views/notifications/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface NotificationRecord {
|
||||
id: string
|
||||
title: string
|
||||
date: string
|
||||
content: string
|
||||
isUnread: boolean
|
||||
isExpanded: boolean
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user