feat: 新增已读所有消息

This commit is contained in:
tim
2025-07-14 17:11:08 +08:00
parent b9a79b5e23
commit e69a2213fa

View File

@@ -1,8 +1,20 @@
<template> <template>
<div class="message-page"> <div class="message-page">
<div class="message-tabs"> <div class="message-page-header">
<div :class="['message-tab-item', { selected: selectedTab === 'all' }]" @click="selectedTab = 'all'">消息</div> <div class="message-tabs">
<div :class="['message-tab-item', { selected: selectedTab === 'unread' }]" @click="selectedTab = 'unread'">未读</div> <div :class="['message-tab-item', { selected: selectedTab === 'all' }]" @click="selectedTab = 'all'">消息</div>
<div :class="['message-tab-item', { selected: selectedTab === 'unread' }]" @click="selectedTab = 'unread'">未读
</div>
</div>
<div class="message-page-header-right">
<div class="message-page-header-right-item" @click="markAllRead">
<i class="fas fa-bolt message-page-header-right-item-button-icon"></i>
<span class="message-page-header-right-item-button-text">
已读所有消息
</span>
</div>
</div>
</div> </div>
<div v-if="isLoadingMessage" class="loading-message"> <div v-if="isLoadingMessage" class="loading-message">
@@ -11,189 +23,196 @@
<BasePlaceholder v-else-if="filteredNotifications.length === 0" text="暂时没有消息 :)" icon="fas fa-inbox" /> <BasePlaceholder v-else-if="filteredNotifications.length === 0" text="暂时没有消息 :)" icon="fas fa-inbox" />
<div class="timeline-container" v-if="filteredNotifications.length > 0" > <div class="timeline-container" v-if="filteredNotifications.length > 0">
<BaseTimeline :items="filteredNotifications"> <BaseTimeline :items="filteredNotifications">
<template #item="{ item }"> <template #item="{ item }">
<div class="notif-content" :class="{ read: item.read }"> <div class="notif-content" :class="{ read: item.read }">
<span v-if="!item.read" class="unread-dot"></span> <span v-if="!item.read" class="unread-dot"></span>
<span class="notif-type"> <span class="notif-type">
<template v-if="item.type === 'COMMENT_REPLY' && item.parentComment"> <template v-if="item.type === 'COMMENT_REPLY' && item.parentComment">
<NotificationContainer :item="item" :markRead="markRead"> <NotificationContainer :item="item" :markRead="markRead">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.comment.author.id}`">{{ item.comment.author.username }} </router-link> 对我的评论
<span>
<router-link class="notif-content-text" @click="markRead(item.id)" <router-link class="notif-content-text" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.parentComment.id}`"> :to="`/users/${item.comment.author.id}`">{{ item.comment.author.username }} </router-link> 对我的评论
{{ sanitizeDescription(item.parentComment.content) }} <span>
</router-link> <router-link class="notif-content-text" @click="markRead(item.id)"
</span> 回复了 <span> :to="`/posts/${item.post.id}#comment-${item.parentComment.id}`">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"> {{ sanitizeDescription(item.parentComment.content) }}
{{ sanitizeDescription(item.comment.content) }} </router-link>
</router-link> </span> 回复了 <span>
</span> <router-link class="notif-content-text" @click="markRead(item.id)"
</NotificationContainer> :to="`/posts/${item.post.id}#comment-${item.comment.id}`">
</template> {{ sanitizeDescription(item.comment.content) }}
<template v-else-if="item.type === 'COMMENT_REPLY' && !item.parentComment"> </router-link>
<NotificationContainer :item="item" :markRead="markRead"> </span>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.comment.author.id}`">{{ item.comment.author.username }} </router-link> 对我的文章 </NotificationContainer>
<span> </template>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`"> <template v-else-if="item.type === 'COMMENT_REPLY' && !item.parentComment">
{{ sanitizeDescription(item.post.title) }} <NotificationContainer :item="item" :markRead="markRead">
</router-link> <router-link class="notif-content-text" @click="markRead(item.id)"
</span> 回复了 <span> :to="`/users/${item.comment.author.id}`">{{ item.comment.author.username }} </router-link> 对我的文章
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"> <span>
{{ sanitizeDescription(item.comment.content) }} <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
</router-link> {{ sanitizeDescription(item.post.title) }}
</span> </router-link>
</NotificationContainer> </span> 回复了 <span>
</template> <router-link class="notif-content-text" @click="markRead(item.id)"
<template v-else-if="item.type === 'REACTION' && item.post && !item.comment"> :to="`/posts/${item.post.id}#comment-${item.comment.id}`">
<NotificationContainer :item="item" :markRead="markRead"> {{ sanitizeDescription(item.comment.content) }}
<span class="notif-user">{{ item.fromUser.username }} </span> 对我的文章 </router-link>
<span> </span>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`"> </NotificationContainer>
{{ sanitizeDescription(item.post.title) }} </template>
</router-link> <template v-else-if="item.type === 'REACTION' && item.post && !item.comment">
<NotificationContainer :item="item" :markRead="markRead">
<span class="notif-user">{{ item.fromUser.username }} </span> 对我的文章
<span>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
{{ sanitizeDescription(item.post.title) }}
</router-link>
</span> </span>
进行了表态 进行了表态
</NotificationContainer> </NotificationContainer>
</template> </template>
<template v-else-if="item.type === 'REACTION' && item.comment"> <template v-else-if="item.type === 'REACTION' && item.comment">
<NotificationContainer :item="item" :markRead="markRead"> <NotificationContainer :item="item" :markRead="markRead">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`">{{ item.fromUser.username }} </router-link> 对我的评论 <router-link class="notif-content-text" @click="markRead(item.id)"
<span> :to="`/users/${item.fromUser.id}`">{{ item.fromUser.username }} </router-link> 对我的评论
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"> <span>
<router-link class="notif-content-text" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`">
{{ sanitizeDescription(item.comment.content) }}
</router-link>
</span>
进行了表态
</NotificationContainer>
</template>
<template v-else-if="item.type === 'POST_VIEWED'">
<NotificationContainer :item="item" :markRead="markRead">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`">
{{ item.fromUser.username }}
</router-link>
查看了您的帖子
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
{{ sanitizeDescription(item.post.title) }}
</router-link>
</NotificationContainer>
</template>
<template v-else-if="item.type === 'POST_UPDATED'">
<NotificationContainer :item="item" :markRead="markRead">
您关注的帖子
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
{{ sanitizeDescription(item.post.title) }}
</router-link>
下面有新评论
<router-link class="notif-content-text" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`">
{{ sanitizeDescription(item.comment.content) }} {{ sanitizeDescription(item.comment.content) }}
</router-link> </router-link>
</span> </NotificationContainer>
进行了表态 </template>
</NotificationContainer> <template v-else-if="item.type === 'USER_FOLLOWED'">
</template> <NotificationContainer :item="item" :markRead="markRead">
<template v-else-if="item.type === 'POST_VIEWED'"> <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`">
<NotificationContainer :item="item" :markRead="markRead"> {{ item.fromUser.username }}
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`"> </router-link>
{{ item.fromUser.username }} 开始关注你了
</router-link> </NotificationContainer>
查看了您的帖子 </template>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`"> <template v-else-if="item.type === 'USER_UNFOLLOWED'">
{{ sanitizeDescription(item.post.title) }} <NotificationContainer :item="item" :markRead="markRead">
</router-link> <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`">
</NotificationContainer> {{ item.fromUser.username }}
</template> </router-link>
<template v-else-if="item.type === 'POST_UPDATED'"> 取消关注你了
<NotificationContainer :item="item" :markRead="markRead"> </NotificationContainer>
您关注的帖子 </template>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`"> <template v-else-if="item.type === 'FOLLOWED_POST'">
{{ sanitizeDescription(item.post.title) }} <NotificationContainer :item="item" :markRead="markRead">
</router-link> 你关注的
下面有新评论 <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"> {{ item.fromUser.username }}
{{ sanitizeDescription(item.comment.content) }} </router-link>
</router-link> 发布了文章
</NotificationContainer> <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
</template> {{ sanitizeDescription(item.post.title) }}
<template v-else-if="item.type === 'USER_FOLLOWED'"> </router-link>
<NotificationContainer :item="item" :markRead="markRead"> </NotificationContainer>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`"> </template>
{{ item.fromUser.username }} <template v-else-if="item.type === 'POST_SUBSCRIBED'">
</router-link> <NotificationContainer :item="item" :markRead="markRead">
开始关注你了 <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`">
</NotificationContainer> {{ item.fromUser.username }}
</template> </router-link>
<template v-else-if="item.type === 'USER_UNFOLLOWED'"> 订阅了你的文章
<NotificationContainer :item="item" :markRead="markRead"> <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`"> {{ sanitizeDescription(item.post.title) }}
{{ item.fromUser.username }} </router-link>
</router-link> </NotificationContainer>
取消关注你了 </template>
</NotificationContainer> <template v-else-if="item.type === 'POST_UNSUBSCRIBED'">
</template> <NotificationContainer :item="item" :markRead="markRead">
<template v-else-if="item.type === 'FOLLOWED_POST'"> <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`">
<NotificationContainer :item="item" :markRead="markRead"> {{ item.fromUser.username }}
你关注的 </router-link>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`"> 取消订阅了你的文章
{{ item.fromUser.username }} <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
</router-link> {{ sanitizeDescription(item.post.title) }}
发布了文章 </router-link>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`"> </NotificationContainer>
{{ sanitizeDescription(item.post.title) }} </template>
</router-link> <template v-else-if="item.type === 'POST_REVIEW_REQUEST' && item.fromUser">
</NotificationContainer> <NotificationContainer :item="item" :markRead="markRead">
</template> <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`">
<template v-else-if="item.type === 'POST_SUBSCRIBED'"> {{ item.fromUser.username }}
<NotificationContainer :item="item" :markRead="markRead"> </router-link>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`"> 发布了帖子
{{ item.fromUser.username }} <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
</router-link> {{ sanitizeDescription(item.post.title) }}
订阅了你的文章 </router-link>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`"> 请审核
{{ sanitizeDescription(item.post.title) }} </NotificationContainer>
</router-link> </template>
</NotificationContainer> <template v-else-if="item.type === 'POST_REVIEW_REQUEST'">
</template> <NotificationContainer :item="item" :markRead="markRead">
<template v-else-if="item.type === 'POST_UNSUBSCRIBED'"> 您发布的帖子
<NotificationContainer :item="item" :markRead="markRead"> <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`"> {{ sanitizeDescription(item.post.title) }}
{{ item.fromUser.username }} </router-link>
</router-link> 已提交审核
取消订阅了你的文章 </NotificationContainer>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`"> </template>
{{ sanitizeDescription(item.post.title) }} <template v-else-if="item.type === 'POST_REVIEWED' && item.approved">
</router-link> <NotificationContainer :item="item" :markRead="markRead">
</NotificationContainer> 您发布的帖子
</template> <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
<template v-else-if="item.type === 'POST_REVIEW_REQUEST' && item.fromUser"> {{ sanitizeDescription(item.post.title) }}
<NotificationContainer :item="item" :markRead="markRead"> </router-link>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`"> 已审核通过
{{ item.fromUser.username }} <template #actions>
</router-link> <div class="mark-read-button">
发布了帖子 <button class="mark-read-button-item" v-if="!item.read" @click="markRead(item.id)">标记为已读</button>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`"> <button class="read-button-item" v-else @click="markRead(item.id)">已读</button>
{{ sanitizeDescription(item.post.title) }} </div>
</router-link> </template>
请审核 </NotificationContainer>
</NotificationContainer> </template>
</template> <template v-else-if="item.type === 'POST_REVIEWED' && item.approved === false">
<template v-else-if="item.type === 'POST_REVIEW_REQUEST'"> <NotificationContainer :item="item" :markRead="markRead">
<NotificationContainer :item="item" :markRead="markRead"> 您发布的帖子
您发布的帖子 <router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`"> {{ sanitizeDescription(item.post.title) }}
{{ sanitizeDescription(item.post.title) }} </router-link>
</router-link> 已被管理员拒绝
已提交审核 </NotificationContainer>
</NotificationContainer> </template>
</template> <template v-else>
<template v-else-if="item.type === 'POST_REVIEWED' && item.approved"> <NotificationContainer :item="item" :markRead="markRead">
<NotificationContainer :item="item" :markRead="markRead"> {{ formatType(item.type) }}
您发布的帖子 </NotificationContainer>
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`"> </template>
{{ sanitizeDescription(item.post.title) }} </span>
</router-link> <span class="notif-time">{{ TimeManager.format(item.createdAt) }}</span>
已审核通过 </div>
<template #actions>
<div class="mark-read-button">
<button class="mark-read-button-item" v-if="!item.read" @click="markRead(item.id)">标记为已读</button>
<button class="read-button-item" v-else @click="markRead(item.id)">已读</button>
</div>
</template>
</NotificationContainer>
</template>
<template v-else-if="item.type === 'POST_REVIEWED' && item.approved === false">
<NotificationContainer :item="item" :markRead="markRead">
您发布的帖子
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
{{ sanitizeDescription(item.post.title) }}
</router-link>
已被管理员拒绝
</NotificationContainer>
</template>
<template v-else>
<NotificationContainer :item="item" :markRead="markRead">
{{ formatType(item.type) }}
</NotificationContainer>
</template>
</span>
<span class="notif-time">{{ TimeManager.format(item.createdAt) }}</span>
</div>
</template> </template>
</BaseTimeline> </BaseTimeline>
</div> </div>
@@ -238,6 +257,14 @@ export default {
} }
} }
const markAllRead = async () => {
const ok = await markNotificationsRead(notifications.value.map(n => n.id))
if (ok) {
notifications.value.forEach(n => n.read = true)
toast.success('已读所有消息')
}
}
const iconMap = { const iconMap = {
POST_VIEWED: 'fas fa-eye', POST_VIEWED: 'fas fa-eye',
COMMENT_REPLY: 'fas fa-reply', COMMENT_REPLY: 'fas fa-reply',
@@ -291,100 +318,100 @@ export default {
} }
const data = await res.json() const data = await res.json()
for (const n of data) { for (const n of data) {
if (n.type === 'COMMENT_REPLY') { if (n.type === 'COMMENT_REPLY') {
notifications.value.push({ notifications.value.push({
...n, ...n,
src: n.comment.author.avatar, src: n.comment.author.avatar,
iconClick: () => { iconClick: () => {
markRead(n.id)
router.push(`/users/${n.comment.author.id}`)
}
})
} else if (n.type === 'REACTION') {
notifications.value.push({
...n,
emoji: reactionEmojiMap[n.reactionType],
iconClick: () => {
if (n.fromUser) {
markRead(n.id) markRead(n.id)
router.push(`/users/${n.comment.author.id}`) router.push(`/users/${n.fromUser.id}`)
} }
}) }
} else if (n.type === 'REACTION') { })
notifications.value.push({ } else if (n.type === 'POST_VIEWED') {
...n, notifications.value.push({
emoji: reactionEmojiMap[n.reactionType], ...n,
iconClick: () => { src: n.fromUser ? n.fromUser.avatar : null,
if (n.fromUser) { icon: n.fromUser ? undefined : iconMap[n.type],
markRead(n.id) iconClick: () => {
router.push(`/users/${n.fromUser.id}`) if (n.fromUser) {
}
}
})
} else if (n.type === 'POST_VIEWED') {
notifications.value.push({
...n,
src: n.fromUser ? n.fromUser.avatar : null,
icon: n.fromUser ? undefined : iconMap[n.type],
iconClick: () => {
if (n.fromUser) {
markRead(n.id)
router.push(`/users/${n.fromUser.id}`)
}
}
})
} else if (n.type === 'POST_UPDATED') {
notifications.value.push({
...n,
src: n.comment.author.avatar,
iconClick: () => {
markRead(n.id) markRead(n.id)
router.push(`/users/${n.comment.author.id}`) router.push(`/users/${n.fromUser.id}`)
} }
}) }
} else if (n.type === 'USER_FOLLOWED' || n.type === 'USER_UNFOLLOWED') { })
notifications.value.push({ } else if (n.type === 'POST_UPDATED') {
...n, notifications.value.push({
icon: iconMap[n.type], ...n,
iconClick: () => { src: n.comment.author.avatar,
if (n.fromUser) { iconClick: () => {
markRead(n.id) markRead(n.id)
router.push(`/users/${n.fromUser.id}`) router.push(`/users/${n.comment.author.id}`)
} }
})
} else if (n.type === 'USER_FOLLOWED' || n.type === 'USER_UNFOLLOWED') {
notifications.value.push({
...n,
icon: iconMap[n.type],
iconClick: () => {
if (n.fromUser) {
markRead(n.id)
router.push(`/users/${n.fromUser.id}`)
} }
}) }
} else if (n.type === 'FOLLOWED_POST') { })
notifications.value.push({ } else if (n.type === 'FOLLOWED_POST') {
...n, notifications.value.push({
icon: iconMap[n.type], ...n,
iconClick: () => { icon: iconMap[n.type],
if (n.post) { iconClick: () => {
markRead(n.id) if (n.post) {
router.push(`/posts/${n.post.id}`) markRead(n.id)
} router.push(`/posts/${n.post.id}`)
} }
}) }
} else if (n.type === 'POST_SUBSCRIBED' || n.type === 'POST_UNSUBSCRIBED') { })
notifications.value.push({ } else if (n.type === 'POST_SUBSCRIBED' || n.type === 'POST_UNSUBSCRIBED') {
...n, notifications.value.push({
icon: iconMap[n.type], ...n,
iconClick: () => { icon: iconMap[n.type],
if (n.post) { iconClick: () => {
markRead(n.id) if (n.post) {
router.push(`/posts/${n.post.id}`) markRead(n.id)
} router.push(`/posts/${n.post.id}`)
} }
}) }
} else if (n.type === 'POST_REVIEW_REQUEST') { })
notifications.value.push({ } else if (n.type === 'POST_REVIEW_REQUEST') {
...n, notifications.value.push({
src: n.fromUser ? n.fromUser.avatar : null, ...n,
icon: n.fromUser ? undefined : iconMap[n.type], src: n.fromUser ? n.fromUser.avatar : null,
iconClick: () => { icon: n.fromUser ? undefined : iconMap[n.type],
if (n.post) { iconClick: () => {
markRead(n.id) if (n.post) {
router.push(`/posts/${n.post.id}`) markRead(n.id)
} router.push(`/posts/${n.post.id}`)
} }
}) }
} else { })
notifications.value.push({ } else {
...n, notifications.value.push({
icon: iconMap[n.type], ...n,
}) icon: iconMap[n.type],
} })
} }
}
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@@ -431,7 +458,8 @@ export default {
markRead, markRead,
TimeManager, TimeManager,
selectedTab, selectedTab,
filteredNotifications filteredNotifications,
markAllRead
} }
} }
} }
@@ -452,6 +480,41 @@ export default {
overflow-y: auto; overflow-y: auto;
} }
.message-page-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.message-page-header-right {
display: flex;
flex-direction: row;
align-items: center;
}
.message-page-header-right-item {
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
color: var(--primary-color);
padding-right: 10px;
gap: 5px;
}
.message-page-header-right-item-button-icon {
font-size: 12px;
}
.message-page-header-right-item-button-text {
font-size: 12px;
}
.message-page-header-right-item-button-text:hover {
text-decoration: underline;
}
.timeline-container { .timeline-container {
padding: 10px 20px; padding: 10px 20px;
height: 100%; height: 100%;
@@ -507,7 +570,6 @@ export default {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;
margin-bottom: 20px;
} }
.message-tab-item { .message-tab-item {
@@ -519,6 +581,4 @@ export default {
color: var(--primary-color); color: var(--primary-color);
border-bottom: 2px solid var(--primary-color); border-bottom: 2px solid var(--primary-color);
} }
</style> </style>