feat: 消息通知
This commit is contained in:
@@ -1,166 +1,206 @@
|
||||
<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>
|
||||
<div class="notifications-view mini-scrollbar" @scroll="handleScroll">
|
||||
<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>
|
||||
<section class="notifications-content">
|
||||
<NotificationsList
|
||||
:items="notifications"
|
||||
:unread-count="unreadCount"
|
||||
@toggle-item="handleToggleItem"
|
||||
@mark-all-as-read="handleMarkAllAsRead"
|
||||
/>
|
||||
<div v-if="loading" class="loading-indicator">Loading...</div>
|
||||
<div v-if="!hasMore && notifications.length > 0" class="no-more-data">No more notifications</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import NotificationsList from './components/NotificationsList.vue'
|
||||
import type { NotificationRecord } from './types'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import NotificationsList from './components/NotificationsList.vue'
|
||||
import type { NotificationRecord } from './types'
|
||||
import { fetchAllMessageList, markMessageAsRead, markAllMessagesAsRead } from '@/api/notification'
|
||||
|
||||
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 notifications = ref<NotificationRecord[]>([])
|
||||
const loading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
|
||||
const unreadCount = computed(() => notifications.value.filter((item) => item.isUnread).length)
|
||||
const unreadCount = computed(() => notifications.value.filter((item) => item.isUnread).length)
|
||||
|
||||
const handleToggleItem = (id: NotificationRecord['id']) => {
|
||||
const targetItem = notifications.value.find((item) => item.id === id)
|
||||
const handleToggleItem = async (id: NotificationRecord['id']) => {
|
||||
const targetItem = notifications.value.find((item) => item.id === id)
|
||||
|
||||
if (!targetItem) return
|
||||
if (!targetItem) return
|
||||
|
||||
const nextExpanded = !targetItem.isExpanded
|
||||
const nextExpanded = !targetItem.isExpanded
|
||||
|
||||
notifications.value = notifications.value.map((item) => ({
|
||||
...item,
|
||||
isUnread: item.id === id ? false : item.isUnread,
|
||||
isExpanded: item.id === id ? nextExpanded : false
|
||||
}))
|
||||
}
|
||||
// 如果消息是未读状态,调用API标记为已读
|
||||
if (targetItem.isUnread) {
|
||||
try {
|
||||
await markMessageAsRead(id)
|
||||
} catch (error) {
|
||||
console.error('Failed to mark message as read:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMarkAllAsRead = () => {
|
||||
notifications.value = notifications.value.map((item) => ({
|
||||
...item,
|
||||
isUnread: false
|
||||
}))
|
||||
}
|
||||
notifications.value = notifications.value.map((item) => ({
|
||||
...item,
|
||||
isUnread: item.id === id ? false : item.isUnread,
|
||||
isExpanded: item.id === id ? nextExpanded : false
|
||||
}))
|
||||
}
|
||||
|
||||
const handleMarkAllAsRead = async () => {
|
||||
try {
|
||||
await markAllMessagesAsRead()
|
||||
notifications.value = notifications.value.map((item) => ({
|
||||
...item,
|
||||
isUnread: false
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Failed to mark all messages as read:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const params = ref({
|
||||
page: 1,
|
||||
size: 15
|
||||
})
|
||||
|
||||
const handleFetchMessageList = async () => {
|
||||
if (loading.value || !hasMore.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchAllMessageList(params.value)
|
||||
|
||||
// Transform API data to match NotificationRecord interface
|
||||
const newNotifications: NotificationRecord[] = res.content.map((item: any) => ({
|
||||
id: String(item.id),
|
||||
title: item.title,
|
||||
date: item.createTime,
|
||||
content: item.content,
|
||||
isUnread: item.isRead === 0,
|
||||
isExpanded: false
|
||||
}))
|
||||
|
||||
if (params.value.page === 1) {
|
||||
notifications.value = newNotifications
|
||||
} else {
|
||||
notifications.value = [...notifications.value, ...newNotifications]
|
||||
}
|
||||
|
||||
// Check if there are more pages
|
||||
hasMore.value = params.value.page < res.pages
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch notifications:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = (event: Event) => {
|
||||
const target = event.target as HTMLElement
|
||||
const scrollTop = target.scrollTop
|
||||
const scrollHeight = target.scrollHeight
|
||||
const clientHeight = target.clientHeight
|
||||
|
||||
// Load more when scrolled to 80% of the content
|
||||
if (scrollTop + clientHeight >= scrollHeight * 0.8 && !loading.value && hasMore.value) {
|
||||
params.value.page++
|
||||
handleFetchMessageList()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleFetchMessageList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.notifications-view {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
.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 {
|
||||
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__content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.notifications-hero__title {
|
||||
margin: 0;
|
||||
color: #232323;
|
||||
font-size: 4rem;
|
||||
line-height: 1;
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
}
|
||||
.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-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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.loading-indicator,
|
||||
.no-more-data {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #979797;
|
||||
font-size: 1.4rem;
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
}
|
||||
|
||||
.notifications-hero__title {
|
||||
font-size: 3rem;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.notifications-hero {
|
||||
min-height: 12rem;
|
||||
padding: 2.8rem 1.6rem;
|
||||
}
|
||||
|
||||
.notifications-hero__subtitle {
|
||||
margin-top: 0.8rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.notifications-hero__title {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.notifications-content {
|
||||
width: calc(100% - 3.2rem);
|
||||
padding: 3.2rem 0 4.8rem;
|
||||
}
|
||||
}
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user