feat: add user notification center
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export interface UserNoticeItem {
|
||||
id: number
|
||||
type: string
|
||||
type_desc: string
|
||||
icon: string
|
||||
color: string
|
||||
title: string
|
||||
content: string
|
||||
is_read: boolean
|
||||
create_time: number | string
|
||||
target_url: string
|
||||
source_user: {
|
||||
id: number
|
||||
nickname: string
|
||||
avatar: string
|
||||
} | null
|
||||
}
|
||||
|
||||
export function getUserNotices(data?: { page_no?: number; page_size?: number }) {
|
||||
return request.get<{
|
||||
lists: UserNoticeItem[]
|
||||
count: number
|
||||
page_no: number
|
||||
page_size: number
|
||||
}>({ url: '/notice/lists', data })
|
||||
}
|
||||
|
||||
export function getUserNoticeSummary() {
|
||||
return request.get<{
|
||||
notice_unread_count: number
|
||||
chat_unread_count: number
|
||||
unread_count: number
|
||||
}>({ url: '/notice/summary' })
|
||||
}
|
||||
|
||||
export function markUserNoticeRead(data: { id: number }) {
|
||||
return request.post({ url: '/notice/read', data })
|
||||
}
|
||||
|
||||
export function markAllUserNoticesRead() {
|
||||
return request.post({ url: '/notice/readAll' })
|
||||
}
|
||||
@@ -167,6 +167,16 @@
|
||||
"auth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message_notice/message_notice",
|
||||
"style": {
|
||||
"navigationStyle": "default",
|
||||
"navigationBarTitleText": "消息通知"
|
||||
},
|
||||
"meta": {
|
||||
"auth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/private_chat/room",
|
||||
"style": {
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<z-paging ref="paging" v-model="noticeList" use-page-scroll @query="queryList">
|
||||
<view class="message-notice-page">
|
||||
<view class="message-notice-page__header">
|
||||
<view>
|
||||
<text class="message-notice-page__title">消息通知</text>
|
||||
<text class="message-notice-page__subtitle">互动、积分与系统消息都会在这里显示</text>
|
||||
</view>
|
||||
<text v-if="summary.notice_unread_count > 0" class="message-notice-page__read-all" @tap="handleReadAll">
|
||||
全部已读
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="private-entry" @tap="openPrivateChat">
|
||||
<view class="private-entry__icon"><u-icon name="chat" size="34" color="#1468f5" /></view>
|
||||
<view class="private-entry__body">
|
||||
<view class="private-entry__top">
|
||||
<text class="private-entry__title">私聊消息</text>
|
||||
<text v-if="summary.chat_unread_count > 0" class="private-entry__badge">
|
||||
{{ summary.chat_unread_count > 99 ? '99+' : summary.chat_unread_count }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="private-entry__desc">查看与好友的聊天消息</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" size="24" color="#9aa4b2" />
|
||||
</view>
|
||||
|
||||
<view class="message-notice-page__section-title">
|
||||
<text>互动与系统消息</text>
|
||||
<text v-if="summary.notice_unread_count > 0" class="message-notice-page__unread-count">
|
||||
{{ summary.notice_unread_count }} 条未读
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="noticeList.length === 0" class="message-notice-empty">
|
||||
<u-icon name="bell" size="56" color="#cbd5e1" />
|
||||
<text>暂时没有新的通知</text>
|
||||
</view>
|
||||
|
||||
<view v-for="item in noticeList" :key="item.id" class="notice-card"
|
||||
:class="{ 'notice-card--unread': !item.is_read }" @tap="handleNoticeTap(item)">
|
||||
<view class="notice-card__icon" :style="{ backgroundColor: `${item.color}16` }">
|
||||
<u-icon :name="item.icon" size="30" :color="item.color" />
|
||||
</view>
|
||||
<view class="notice-card__body">
|
||||
<view class="notice-card__top">
|
||||
<text class="notice-card__title">{{ item.title }}</text>
|
||||
<text class="notice-card__time">{{ formatTime(item.create_time) }}</text>
|
||||
</view>
|
||||
<text class="notice-card__content">{{ item.content }}</text>
|
||||
<text class="notice-card__type">{{ item.type_desc }}</text>
|
||||
</view>
|
||||
<view v-if="!item.is_read" class="notice-card__dot" />
|
||||
<u-icon v-if="item.target_url" name="arrow-right" size="22" color="#aeb7c4" />
|
||||
</view>
|
||||
</view>
|
||||
</z-paging>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { reactive, ref, shallowRef } from 'vue'
|
||||
import {
|
||||
getUserNotices,
|
||||
getUserNoticeSummary,
|
||||
markAllUserNoticesRead,
|
||||
markUserNoticeRead,
|
||||
type UserNoticeItem
|
||||
} from '@/api/notice'
|
||||
|
||||
const paging = shallowRef()
|
||||
const noticeList = ref<UserNoticeItem[]>([])
|
||||
const summary = reactive({
|
||||
notice_unread_count: 0,
|
||||
chat_unread_count: 0,
|
||||
unread_count: 0
|
||||
})
|
||||
|
||||
const queryList = async (pageNo: number, pageSize: number) => {
|
||||
try {
|
||||
const { lists = [] } = await getUserNotices({ page_no: pageNo, page_size: pageSize })
|
||||
paging.value.complete(lists)
|
||||
} catch {
|
||||
paging.value.complete(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadSummary = async () => {
|
||||
try {
|
||||
const result = await getUserNoticeSummary()
|
||||
summary.notice_unread_count = Number(result?.notice_unread_count) || 0
|
||||
summary.chat_unread_count = Number(result?.chat_unread_count) || 0
|
||||
summary.unread_count = Number(result?.unread_count) || 0
|
||||
} catch {
|
||||
// 消息列表仍可正常加载。
|
||||
}
|
||||
}
|
||||
|
||||
const openPrivateChat = () => {
|
||||
uni.navigateTo({ url: '/pages/private_chat/list' })
|
||||
}
|
||||
|
||||
const handleNoticeTap = async (item: UserNoticeItem) => {
|
||||
if (!item.is_read) {
|
||||
try {
|
||||
await markUserNoticeRead({ id: item.id })
|
||||
item.is_read = true
|
||||
summary.notice_unread_count = Math.max(0, summary.notice_unread_count - 1)
|
||||
summary.unread_count = Math.max(0, summary.unread_count - 1)
|
||||
} catch {
|
||||
// 已读更新失败不阻断跳转。
|
||||
}
|
||||
}
|
||||
navigateToTarget(item.target_url)
|
||||
}
|
||||
|
||||
const handleReadAll = async () => {
|
||||
try {
|
||||
await markAllUserNoticesRead()
|
||||
noticeList.value.forEach((item) => {
|
||||
item.is_read = true
|
||||
})
|
||||
summary.unread_count = Math.max(0, summary.unread_count - summary.notice_unread_count)
|
||||
summary.notice_unread_count = 0
|
||||
uni.showToast({ title: '全部消息已读', icon: 'success' })
|
||||
} catch {
|
||||
uni.showToast({ title: '操作失败,请稍后重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const navigateToTarget = (url: string) => {
|
||||
if (!url) return
|
||||
const tabPages = [
|
||||
'/pages/index/index',
|
||||
'/packages_match/pages/worldcup',
|
||||
'/pages/crypto/crypto',
|
||||
'/pages/lottery_analysis/lottery_analysis',
|
||||
'/pages/community/community',
|
||||
'/pages/user/user'
|
||||
]
|
||||
const pagePath = url.split('?')[0]
|
||||
if (tabPages.includes(pagePath)) {
|
||||
uni.switchTab({ url: pagePath })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
const formatTime = (value: string | number) => {
|
||||
const timestamp = typeof value === 'number'
|
||||
? value
|
||||
: new Date(String(value).replace(/-/g, '/')).getTime() / 1000
|
||||
if (!Number.isFinite(timestamp)) return ''
|
||||
const diff = Date.now() / 1000 - timestamp
|
||||
if (diff < 60) return '刚刚'
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`
|
||||
if (diff < 604800) return `${Math.floor(diff / 86400)}天前`
|
||||
const date = new Date(timestamp * 1000)
|
||||
return `${date.getMonth() + 1}-${date.getDate()}`
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
paging.value?.reload()
|
||||
loadSummary()
|
||||
})
|
||||
|
||||
loadSummary()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-notice-page {
|
||||
min-height: 100vh;
|
||||
padding: 28rpx 24rpx 52rpx;
|
||||
box-sizing: border-box;
|
||||
background: #f6f8fb;
|
||||
|
||||
&__header,
|
||||
&__section-title,
|
||||
.private-entry,
|
||||
.private-entry__top,
|
||||
.notice-card,
|
||||
.notice-card__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__header {
|
||||
justify-content: space-between;
|
||||
margin: 0 2rpx 20rpx;
|
||||
}
|
||||
|
||||
&__title,
|
||||
&__subtitle,
|
||||
&__section-title text,
|
||||
.private-entry__title,
|
||||
.private-entry__desc,
|
||||
.notice-card__title,
|
||||
.notice-card__content,
|
||||
.notice-card__type,
|
||||
.notice-card__time,
|
||||
.message-notice-empty text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: #172033;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
margin-top: 8rpx;
|
||||
color: #8b96a7;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
&__read-all {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 20rpx;
|
||||
color: #1468f5;
|
||||
background: #eaf2ff;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
justify-content: space-between;
|
||||
margin: 28rpx 4rpx 14rpx;
|
||||
color: #172033;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__unread-count {
|
||||
color: #ef4444;
|
||||
font-size: 20rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.private-entry,
|
||||
.notice-card {
|
||||
position: relative;
|
||||
border: 1rpx solid #e8edf4;
|
||||
border-radius: 18rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(31, 54, 92, 0.04);
|
||||
}
|
||||
|
||||
.private-entry {
|
||||
padding: 22rpx;
|
||||
|
||||
&__icon,
|
||||
.notice-card__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
background: #eaf2ff;
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 18rpx;
|
||||
}
|
||||
|
||||
&__top {
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: #1f2937;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 7rpx;
|
||||
color: #8b96a7;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
min-width: 30rpx;
|
||||
height: 30rpx;
|
||||
padding: 0 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 16rpx;
|
||||
color: #fff;
|
||||
background: #ef4444;
|
||||
font-size: 18rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-card {
|
||||
margin-bottom: 14rpx;
|
||||
padding: 22rpx 20rpx;
|
||||
|
||||
&--unread {
|
||||
border-color: #cfe0ff;
|
||||
background: linear-gradient(90deg, #f8fbff 0%, #fff 36%);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 16rpx;
|
||||
padding-right: 10rpx;
|
||||
}
|
||||
|
||||
&__top {
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
color: #253044;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__time {
|
||||
flex-shrink: 0;
|
||||
color: #a0a9b6;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: -webkit-box;
|
||||
margin-top: 8rpx;
|
||||
overflow: hidden;
|
||||
color: #647084;
|
||||
font-size: 23rpx;
|
||||
line-height: 34rpx;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
&__type {
|
||||
margin-top: 8rpx;
|
||||
color: #9aa4b2;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.message-notice-empty {
|
||||
padding: 100rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
text {
|
||||
margin-top: 20rpx;
|
||||
color: #9aa4b2;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
<view class="chat-nav__btn" @tap="goBack">
|
||||
<u-icon name="arrow-left" size="34" color="#111827" />
|
||||
</view>
|
||||
<text class="chat-nav__title">消息通知</text>
|
||||
<text class="chat-nav__title">私聊消息</text>
|
||||
<view class="chat-nav__btn" @tap="loadSessions(true)">
|
||||
<u-icon name="reload" size="30" color="#111827" />
|
||||
</view>
|
||||
|
||||
@@ -84,8 +84,8 @@
|
||||
@tap="handleMenuTap(item)">
|
||||
<view class="user-list__icon">
|
||||
<u-icon :name="item.icon" color="#1468f5" size="30" />
|
||||
<view v-if="item.key === 'notice' && stats.chat_unread_count > 0" class="user-list__badge">
|
||||
<text>{{ stats.chat_unread_count > 99 ? '99+' : stats.chat_unread_count }}</text>
|
||||
<view v-if="item.key === 'notice' && stats.message_unread_count > 0" class="user-list__badge">
|
||||
<text>{{ stats.message_unread_count > 99 ? '99+' : stats.message_unread_count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="user-list__label">{{ item.label }}</text>
|
||||
@@ -136,14 +136,15 @@ const stats = reactive({
|
||||
fans_count: 0,
|
||||
collect_count: 0,
|
||||
user_points: 0,
|
||||
chat_unread_count: 0
|
||||
chat_unread_count: 0,
|
||||
message_unread_count: 0
|
||||
})
|
||||
|
||||
const contentMenus = [
|
||||
{ key: 'posts', label: '我的帖子', icon: 'file-text', url: '/pages/my_posts/my_posts' },
|
||||
{ key: 'comments', label: '我的评论', icon: 'chat', url: '/pages/my_comments/my_comments' },
|
||||
{ key: 'collection', label: '我的收藏', icon: 'star', url: '/pages/collection/collection' },
|
||||
{ key: 'notice', label: '消息通知', icon: 'bell', url: '/pages/private_chat/list' }
|
||||
{ key: 'notice', label: '消息通知', icon: 'bell', url: '/pages/message_notice/message_notice' }
|
||||
]
|
||||
|
||||
const accountMenus = [
|
||||
@@ -214,6 +215,7 @@ const fetchStats = async () => {
|
||||
stats.collect_count = res.data.collect_count || 0
|
||||
stats.user_points = res.data.user_points || 0
|
||||
stats.chat_unread_count = Number(res.data.chat_unread_count) || 0
|
||||
stats.message_unread_count = Number(res.data.message_unread_count) || stats.chat_unread_count
|
||||
}
|
||||
} catch (e) {
|
||||
// 静默处理
|
||||
|
||||
Reference in New Issue
Block a user