feat: notification页面
This commit is contained in:
@@ -29,6 +29,11 @@ const router = createRouter({
|
||||
component: () => import('@/views/setting/index.vue'),
|
||||
meta: { cache: true }
|
||||
},
|
||||
{
|
||||
path: '/notifications',
|
||||
name: 'notifications',
|
||||
component: () => import('@/views/notifications/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)',
|
||||
name: '404',
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
}
|
||||
const onNotifications = () => {
|
||||
hideProfilePopover()
|
||||
console.log('notifications')
|
||||
router.push('/notifications')
|
||||
}
|
||||
const onSettings = () => {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user