2
0

pages_messages.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import React, { useState, useEffect } from 'react';
  2. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  3. import { Button, Table, Space, Modal, Form, Input, Select, message } from 'antd';
  4. import { io, Socket } from 'socket.io-client';
  5. import type { TableProps } from 'antd';
  6. import dayjs from 'dayjs';
  7. import 'dayjs/locale/zh-cn';
  8. import { MessageAPI , UserAPI } from './api/index.ts';
  9. import type { UserMessage } from '../share/types.ts';
  10. import { MessageStatusNameMap , MessageStatus, MessageType } from '../share/types.ts';
  11. import { useAuth } from "./hooks_sys.tsx";
  12. export const MessagesPage = () => {
  13. const { token } = useAuth();
  14. const [socket, setSocket] = useState<Socket | null>(null);
  15. const [isSocketConnected, setIsSocketConnected] = useState(false);
  16. const queryClient = useQueryClient();
  17. const [form] = Form.useForm();
  18. const [isModalVisible, setIsModalVisible] = useState(false);
  19. const [searchParams, setSearchParams] = useState({
  20. page: 1,
  21. pageSize: 10,
  22. type: undefined,
  23. status: undefined,
  24. search: undefined
  25. });
  26. // 获取消息列表
  27. const { data: messages, isLoading } = useQuery({
  28. queryKey: ['messages', searchParams],
  29. queryFn: () => MessageAPI.getMessages(searchParams),
  30. });
  31. // 获取用户列表
  32. const { data: users } = useQuery({
  33. queryKey: ['users'],
  34. queryFn: () => UserAPI.getUsers({ page: 1, limit: 1000 }),
  35. });
  36. // 获取未读消息数
  37. const { data: unreadCount } = useQuery({
  38. queryKey: ['unreadCount'],
  39. queryFn: () => MessageAPI.getUnreadCount(),
  40. });
  41. // 标记消息为已读
  42. const markAsReadMutation = useMutation({
  43. mutationFn: (id: number) => MessageAPI.markAsRead(id),
  44. onSuccess: () => {
  45. queryClient.invalidateQueries({ queryKey: ['messages'] });
  46. queryClient.invalidateQueries({ queryKey: ['unreadCount'] });
  47. message.success('标记已读成功');
  48. },
  49. });
  50. // 删除消息
  51. const deleteMutation = useMutation({
  52. mutationFn: (id: number) => MessageAPI.deleteMessage(id),
  53. onSuccess: () => {
  54. queryClient.invalidateQueries({ queryKey: ['messages'] });
  55. message.success('删除成功');
  56. },
  57. });
  58. // 发送消息
  59. // 初始化Socket.IO连接
  60. useEffect(() => {
  61. if (!token) return;
  62. const newSocket = io('/', {
  63. path: '/socket.io',
  64. transports: ['websocket'],
  65. autoConnect: false,
  66. query: {
  67. socket_token: token
  68. }
  69. });
  70. newSocket.on('connect', () => {
  71. setIsSocketConnected(true);
  72. message.success('实时消息连接已建立');
  73. });
  74. newSocket.on('disconnect', () => {
  75. setIsSocketConnected(false);
  76. message.warning('实时消息连接已断开');
  77. });
  78. newSocket.on('error', (err) => {
  79. message.error(`实时消息错误: ${err}`);
  80. });
  81. newSocket.connect();
  82. setSocket(newSocket);
  83. return () => {
  84. newSocket.disconnect();
  85. };
  86. }, [token]);
  87. const sendMessageMutation = useMutation({
  88. mutationFn: async (data: any) => {
  89. // 优先使用Socket.IO发送
  90. if (isSocketConnected && socket) {
  91. return new Promise((resolve, reject) => {
  92. socket.emit('message:send', data, (response: any) => {
  93. if (response.error) {
  94. reject(new Error(response.error));
  95. } else {
  96. resolve(response.data);
  97. }
  98. });
  99. });
  100. }
  101. // 回退到HTTP API
  102. return MessageAPI.sendMessage(data);
  103. },
  104. onSuccess: () => {
  105. queryClient.invalidateQueries({ queryKey: ['messages'] });
  106. queryClient.invalidateQueries({ queryKey: ['unreadCount'] });
  107. message.success('发送成功');
  108. setIsModalVisible(false);
  109. form.resetFields();
  110. },
  111. });
  112. const columns: TableProps<UserMessage>['columns'] = [
  113. {
  114. title: '标题',
  115. dataIndex: 'title',
  116. key: 'title',
  117. },
  118. {
  119. title: '类型',
  120. dataIndex: 'type',
  121. key: 'type',
  122. },
  123. {
  124. title: '发送人',
  125. dataIndex: 'sender_name',
  126. key: 'sender_name',
  127. },
  128. {
  129. title: '状态',
  130. dataIndex: 'user_status',
  131. key: 'user_status',
  132. render: (user_status: MessageStatus) => (
  133. <span style={{ color: user_status === MessageStatus.UNREAD ? 'red' : 'green' }}>
  134. {MessageStatusNameMap[user_status]}
  135. </span>
  136. ),
  137. },
  138. {
  139. title: '发送时间',
  140. dataIndex: 'created_at',
  141. key: 'created_at',
  142. render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm'),
  143. },
  144. {
  145. title: '操作',
  146. key: 'action',
  147. render: (_: any, record) => (
  148. <Space size="middle">
  149. <Button
  150. type="link"
  151. onClick={() => markAsReadMutation.mutate(record.id)}
  152. disabled={record.user_status === MessageStatus.READ}
  153. >
  154. 标记已读
  155. </Button>
  156. <Button
  157. type="link"
  158. danger
  159. onClick={() => deleteMutation.mutate(record.id)}
  160. >
  161. 删除
  162. </Button>
  163. </Space>
  164. ),
  165. },
  166. ];
  167. const handleSearch = (values: any) => {
  168. setSearchParams({
  169. ...searchParams,
  170. ...values,
  171. page: 1
  172. });
  173. };
  174. const handleTableChange = (pagination: any) => {
  175. setSearchParams({
  176. ...searchParams,
  177. page: pagination.current,
  178. pageSize: pagination.pageSize
  179. });
  180. };
  181. const handleSendMessage = (values: any) => {
  182. sendMessageMutation.mutate(values);
  183. };
  184. return (
  185. <div className="p-4">
  186. <div className="flex justify-between items-center mb-4">
  187. <h1 className="text-2xl font-bold">消息管理</h1>
  188. <div className="flex items-center space-x-4">
  189. {unreadCount && unreadCount.count > 0 && (
  190. <span className="text-red-500">{unreadCount.count}条未读</span>
  191. )}
  192. <Button type="primary" onClick={() => setIsModalVisible(true)}>
  193. 发送消息
  194. </Button>
  195. </div>
  196. </div>
  197. <div className="bg-white p-4 rounded shadow">
  198. <Form layout="inline" onFinish={handleSearch} className="mb-4">
  199. <Form.Item name="type" label="类型">
  200. <Select
  201. style={{ width: 120 }}
  202. allowClear
  203. options={[
  204. { value: MessageType.SYSTEM, label: '系统消息' },
  205. { value: MessageType.ANNOUNCE, label: '公告' },
  206. { value: MessageType.PRIVATE, label: '个人消息' },
  207. ]}
  208. />
  209. </Form.Item>
  210. <Form.Item name="status" label="状态">
  211. <Select
  212. style={{ width: 120 }}
  213. allowClear
  214. options={[
  215. { value: MessageStatus.UNREAD, label: '未读' },
  216. { value: MessageStatus.READ, label: '已读' },
  217. ]}
  218. />
  219. </Form.Item>
  220. <Form.Item name="search" label="搜索">
  221. <Input placeholder="输入标题或内容" />
  222. </Form.Item>
  223. <Form.Item>
  224. <Button type="primary" htmlType="submit">
  225. 搜索
  226. </Button>
  227. </Form.Item>
  228. </Form>
  229. <Table
  230. columns={columns}
  231. dataSource={messages?.data}
  232. loading={isLoading}
  233. rowKey="id"
  234. pagination={{
  235. current: searchParams.page,
  236. pageSize: searchParams.pageSize,
  237. total: messages?.pagination?.total,
  238. showSizeChanger: true,
  239. }}
  240. onChange={handleTableChange}
  241. />
  242. </div>
  243. <Modal
  244. title="发送消息"
  245. visible={isModalVisible}
  246. onCancel={() => setIsModalVisible(false)}
  247. footer={null}
  248. width={800}
  249. >
  250. <Form
  251. form={form}
  252. layout="vertical"
  253. onFinish={handleSendMessage}
  254. >
  255. <Form.Item
  256. name="title"
  257. label="标题"
  258. rules={[{ required: true, message: '请输入标题' }]}
  259. >
  260. <Input placeholder="请输入消息标题" />
  261. </Form.Item>
  262. <Form.Item
  263. name="type"
  264. label="消息类型"
  265. rules={[{ required: true, message: '请选择消息类型' }]}
  266. >
  267. <Select
  268. options={[
  269. { value: MessageType.SYSTEM, label: '系统消息' },
  270. { value: MessageType.ANNOUNCE, label: '公告' },
  271. { value: MessageType.PRIVATE, label: '个人消息' },
  272. ]}
  273. />
  274. </Form.Item>
  275. <Form.Item
  276. name="receiver_ids"
  277. label="接收人"
  278. rules={[{ required: true, message: '请选择接收人' }]}
  279. >
  280. <Select
  281. mode="multiple"
  282. placeholder="请选择接收人"
  283. options={users?.data?.map((user: any) => ({
  284. value: user.id,
  285. label: user.username,
  286. }))}
  287. />
  288. </Form.Item>
  289. <Form.Item
  290. name="content"
  291. label="内容"
  292. rules={[{ required: true, message: '请输入消息内容' }]}
  293. >
  294. <Input.TextArea rows={6} placeholder="请输入消息内容" />
  295. </Form.Item>
  296. <Form.Item>
  297. <Button
  298. type="primary"
  299. htmlType="submit"
  300. loading={sendMessageMutation.status === 'pending'}
  301. icon={isSocketConnected ? <span style={{color:'green'}}>●</span> : <span style={{color:'red'}}>●</span>}
  302. >
  303. 发送
  304. </Button>
  305. </Form.Item>
  306. </Form>
  307. </Modal>
  308. </div>
  309. );
  310. };