Skip to content

Commit

Permalink
feat(user): 实现消息中心功能
Browse files Browse the repository at this point in the history
- 新增消息类别 'notice'
- 重构消息列表获取方式,使用 EventSource 实现服务器发送事件 (SSE)
- 优化消息展示和交互逻辑
- 调整消息数量显示,最大显示 999 条
  • Loading branch information
aide-cloud committed Jan 8, 2025
1 parent eb3bf0e commit 3452523
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 44 deletions.
15 changes: 10 additions & 5 deletions src/api/user/message.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PaginationReply, PaginationReq } from '../global'
import type { PaginationReply, PaginationReq } from '../global'
import request from '../request'

/**
Expand All @@ -19,7 +19,7 @@ export interface ListMessageReply {
}

export type MessageCategory = 'info' | 'success' | 'warning' | 'error'
export type MessageBiz = 'invitation' | 'invitation_rejected' | 'invitation_accepted'
export type MessageBiz = 'invitation' | 'invitation_rejected' | 'invitation_accepted' | 'notice'
/**
* api.admin.NoticeUserMessage
*/
Expand Down Expand Up @@ -60,6 +60,11 @@ export function getBizName(biz: MessageBiz): MessageBizItem {
label: '邀请被拒绝',
color: '#F56C6C'
}
case 'notice':
return {
label: '通知',
color: '#409EFF'
}
default:
return {
label: '未知',
Expand All @@ -81,7 +86,7 @@ export interface DeleteMessageRepquest {
* @description 接口地址:https://app.apifox.com/link/project/5266863/apis/api-221535243
*/
export function deleteMessage(params: DeleteMessageRepquest): Promise<unknown> {
return request.POST(`/v1/user/messages/read`, params)
return request.POST('/v1/user/messages/read', params)
}

/**
Expand All @@ -96,9 +101,9 @@ export function listMessage(params: ListMessageRequest): Promise<ListMessageRepl
}

export function confirmMessage(id: number): Promise<unknown> {
return request.POST(`/v1/user/messages/confirm`, { id })
return request.POST('/v1/user/messages/confirm', { id })
}

export function cancelMessage(id: number): Promise<unknown> {
return request.POST(`/v1/user/messages/cancel`, { id })
return request.POST('/v1/user/messages/cancel', { id })
}
94 changes: 55 additions & 39 deletions src/components/layout/header-message.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { baseURL, getToken } from '@/api/request'
import { getInvite } from '@/api/user/invite'
import {
type MessageCategory,
Expand All @@ -10,11 +11,11 @@ import {
} from '@/api/user/message'
import { GlobalContext } from '@/utils/context'
import { BellOutlined, CheckOutlined, XOutlined } from '@ant-design/icons'
import { useRequest } from 'ahooks'
import { Badge, Button, Divider, Modal, Popover, Space, Tag, theme as antTheme } from 'antd'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn' // 导入中文语言包
import relativeTime from 'dayjs/plugin/relativeTime'
import { debounce } from 'lodash'
import type React from 'react'
import { useCallback, useContext, useEffect, useState } from 'react'
// 设置 dayjs 的语言为中文
Expand All @@ -30,45 +31,60 @@ export const HeaderMessage: React.FC = () => {
const { setRefreshMyTeamList } = useContext(GlobalContext)
const [msgCnt, setMsgCnt] = useState(0)
const [data, setData] = useState<NoticeUserMessageItem[]>([])
const [refresh, setRefresh] = useState(false)
const [eventSource, setEventSource] = useState<EventSource | null>(null)

const handleRefresh = () => {
setRefresh(!refresh)
}
const connectToSSE = useCallback(() => {
if (eventSource) {
return
}

const url = new URL(`${baseURL}/v1/message/conn`)
url.searchParams.set('token', getToken())
const SSEEvent = new EventSource(url.toString())
setEventSource(SSEEvent)

SSEEvent.onmessage = (event) => {
setData((prev) => [...prev, JSON.parse(event.data || '{}')])
setMsgCnt((prev) => +prev + 1)
}

SSEEvent.onerror = (e) => {
console.log('Error connecting to server.', e)
SSEEvent.close()
}

SSEEvent.onopen = () => {
console.log('SSE connection established.')
}
}, [eventSource])

const { run: getMyMessage } = useRequest(listMessage, {
manual: true,
onSuccess: ({ list, pagination }) => {
setData(list || [])
setMsgCnt(pagination?.total || 0)
}
})

const getMessage = useCallback(() => {
getMyMessage({ pagination: { pageNum: 1, pageSize: 999 } })
}, [getMyMessage])

const handleOnOk = () => {
handleRefresh()
getMessage()
setRefreshMyTeamList?.()
}

// eslint-disable-next-line react-hooks/exhaustive-deps
const fetchData = useCallback(
debounce(async (params) => {
listMessage(params).then(({ list, pagination }) => {
setData(list || [])
setMsgCnt(pagination?.total || 0)
})
}, 500),
[]
)
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
getMessage()
}, [])

useEffect(() => {
fetchData({
pagination: {
pageNum: 1,
pageSize: 999
}
})
const interval = setInterval(() => {
fetchData({
pagination: {
pageNum: 1,
pageSize: 999
}
})
}, 60000)
return () => clearInterval(interval)
}, [fetchData])
if (!eventSource) {
connectToSSE()
}
}, [connectToSSE, eventSource])

const getMessageIcon = (category: MessageCategory) => {
switch (category) {
Expand Down Expand Up @@ -124,7 +140,7 @@ export const HeaderMessage: React.FC = () => {
return confirmMessage(messageItem.id).finally(handleOnOk)
},
async onCancel() {
return cancelMessage(messageItem.id).finally(handleRefresh)
return cancelMessage(messageItem.id).finally(getMessage)
}
})
}
Expand All @@ -140,7 +156,7 @@ export const HeaderMessage: React.FC = () => {
</div>
),
async onOk() {
return deleteMessage({ ids: [messageItem.id] }).finally(handleRefresh)
return deleteMessage({ ids: [messageItem.id] }).finally(getMessage)
}
})
}
Expand All @@ -160,13 +176,13 @@ export const HeaderMessage: React.FC = () => {
<Popover
placement='bottomLeft'
content={
<div className='w-[400px] h-[400px] flex flex-col relative'>
<div className='w-[400px] h-[400px] flex flex-col'>
<div className='p-3 pb-2 flex justify-between'>
<div className='text-base font-bold'>通知</div>
<div className='text-sm text-gray-500'>您有 {msgCnt} 条未读消息</div>
</div>
<Divider className='m-1 p-0' />
<Space direction='vertical' size={4} className='text-[#888] text-lg p-1' wrap>
<div className='text-[#888] text-lg p-1 overflow-auto flex flex-col gap-2 h-[400px]'>
{data.map((item, index) => {
const { color, label } = getBizName(item.biz)
return (
Expand Down Expand Up @@ -199,9 +215,9 @@ export const HeaderMessage: React.FC = () => {
</div>
)
})}
</Space>
</div>
{msgCnt > 0 && (
<div className='absolute bottom-0 left-0 right-0'>
<div>
<Divider className='m-1 p-0' />
<div className='flex-1 overflow-auto overflow-x-hidden py-3'>
<Button className='w-full' type='primary'>
Expand All @@ -213,7 +229,7 @@ export const HeaderMessage: React.FC = () => {
</div>
}
>
<Badge count={msgCnt} overflowCount={99} size='small'>
<Badge count={msgCnt} overflowCount={999} size='small'>
<Button icon={<BellOutlined />} type='text' />
</Badge>
</Popover>
Expand Down

0 comments on commit 3452523

Please sign in to comment.