feat: 消息通知

This commit is contained in:
2026-05-31 09:51:57 +08:00
parent 1cc85f1c46
commit 74b6045220
10 changed files with 637 additions and 243 deletions

View File

@@ -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>