2
0

pages_messages.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import React, { useEffect } from 'react';
  2. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  3. import dayjs from 'dayjs';
  4. import 'dayjs/locale/zh-cn';
  5. import { BellIcon } from '@heroicons/react/24/outline';
  6. import { MessageStatus } from '../share/types.ts';
  7. import { io, Socket } from 'socket.io-client';
  8. // 添加通知页面组件
  9. import { MessageAPI } from './api/index.ts';
  10. import { useAuth } from "./hooks.tsx";
  11. export const NotificationsPage = () => {
  12. const { token , user} = useAuth();
  13. const queryClient = useQueryClient();
  14. const [socket, setSocket] = React.useState<Socket | null>(null);
  15. const [isSubscribed, setIsSubscribed] = React.useState(false);
  16. // 获取消息列表
  17. const { data: messages, isLoading } = useQuery({
  18. queryKey: ['messages'],
  19. queryFn: () => MessageAPI.getMessages(),
  20. });
  21. // 初始化Socket.IO连接
  22. useEffect(() => {
  23. if (!token || !user) return;
  24. const newSocket = io('/', {
  25. path: '/socket.io',
  26. transports: ['websocket'],
  27. withCredentials: true,
  28. query: {
  29. socket_token: token
  30. }
  31. });
  32. setSocket(newSocket);
  33. // 订阅消息频道
  34. newSocket.on('connect', () => {
  35. // 订阅个人频道
  36. newSocket.emit('message:subscribe', `user_${user.id}`);
  37. // 订阅系统频道
  38. newSocket.emit('message:subscribe', 'system');
  39. // 订阅公告频道
  40. newSocket.emit('message:subscribe', 'announce');
  41. setIsSubscribed(true);
  42. });
  43. // 处理实时消息
  44. const handleNewMessage = (newMessage: any) => {
  45. queryClient.setQueryData(['messages'], (oldData: any) => {
  46. if (!oldData) return oldData;
  47. return {
  48. ...oldData,
  49. data: [newMessage, ...oldData.data]
  50. };
  51. });
  52. // 更新未读计数
  53. queryClient.setQueryData(['unreadCount'], (oldData: any) => {
  54. if (!oldData) return oldData;
  55. return {
  56. ...oldData,
  57. count: oldData.count + 1
  58. };
  59. });
  60. };
  61. // 处理广播消息
  62. newSocket.on('message:broadcasted', handleNewMessage);
  63. // 处理频道推送消息
  64. newSocket.on('message:received', handleNewMessage);
  65. // 错误处理
  66. newSocket.on('error', (error) => {
  67. console.error('Socket error:', error);
  68. });
  69. return () => {
  70. if (newSocket) {
  71. newSocket.emit('message:unsubscribe', `user_${user.id}`);
  72. newSocket.emit('message:unsubscribe', 'system');
  73. newSocket.emit('message:unsubscribe', 'announce');
  74. newSocket.disconnect();
  75. }
  76. };
  77. }, [queryClient, token]);
  78. // 获取未读消息数量
  79. const { data: unreadCount } = useQuery({
  80. queryKey: ['unreadCount'],
  81. queryFn: () => MessageAPI.getUnreadCount(),
  82. });
  83. // 标记消息为已读
  84. const markAsReadMutation = useMutation({
  85. mutationFn: (id: number) => MessageAPI.markAsRead(id),
  86. onSuccess: () => {
  87. queryClient.invalidateQueries({ queryKey: ['messages'] });
  88. queryClient.invalidateQueries({ queryKey: ['unreadCount'] });
  89. },
  90. });
  91. // 删除消息
  92. const deleteMutation = useMutation({
  93. mutationFn: (id: number) => MessageAPI.deleteMessage(id),
  94. onSuccess: () => {
  95. queryClient.invalidateQueries({ queryKey: ['messages'] });
  96. },
  97. });
  98. const handleMarkAsRead = (id: number) => {
  99. markAsReadMutation.mutate(id);
  100. };
  101. const handleDelete = (id: number) => {
  102. if (confirm('确定要删除这条消息吗?')) {
  103. deleteMutation.mutate(id);
  104. }
  105. };
  106. if (isLoading) {
  107. return (
  108. <div className="p-4">
  109. <h1 className="text-2xl font-bold mb-4">通知</h1>
  110. <div className="flex justify-center items-center h-40">
  111. <div className="w-8 h-8 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin"></div>
  112. </div>
  113. </div>
  114. );
  115. }
  116. return (
  117. <div className="p-4">
  118. <div className="flex items-center justify-between mb-4">
  119. <h1 className="text-2xl font-bold">通知</h1>
  120. {unreadCount && unreadCount.count > 0 ? (
  121. <div className="flex items-center">
  122. <BellIcon className="h-5 w-5 text-red-500 mr-1" />
  123. <span className="text-sm text-red-500">{unreadCount.count}条未读</span>
  124. </div>
  125. ) : null}
  126. </div>
  127. <div className="bg-white rounded-lg shadow divide-y">
  128. {messages?.data.map((message) => (
  129. <div key={message.id} className="p-4">
  130. <div className="flex justify-between items-start">
  131. <h3 className="font-medium">{message.title}</h3>
  132. <div className="flex space-x-2">
  133. {message.user_status === MessageStatus.UNREAD && (
  134. <button
  135. type="button"
  136. onClick={() => handleMarkAsRead(message.id)}
  137. className="text-xs text-blue-600 hover:text-blue-800"
  138. >
  139. 标记已读
  140. </button>
  141. )}
  142. <button
  143. type="button"
  144. onClick={() => handleDelete(message.id)}
  145. className="text-xs text-red-600 hover:text-red-800"
  146. >
  147. 删除
  148. </button>
  149. </div>
  150. </div>
  151. <p className="text-gray-500 text-sm mt-1">{message.content}</p>
  152. <p className="text-xs text-gray-400 mt-2">
  153. {dayjs(message.created_at).format('YYYY-MM-DD HH:mm')}
  154. </p>
  155. </div>
  156. ))}
  157. </div>
  158. </div>
  159. );
  160. };