Files
d8d-admin-mobile-starter-pu…/client/admin/pages_messages.tsx

337 lines
9.5 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Button, Table, Space, Modal, Form, Input, Select, message } from 'antd';
import { io, Socket } from 'socket.io-client';
import type { TableProps } from 'antd';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import { MessageAPI , UserAPI } from './api/index.ts';
import type { UserMessage } from '../share/types.ts';
import { MessageStatusNameMap , MessageStatus, MessageType } from '../share/types.ts';
import { useAuth } from "./hooks_sys.tsx";
export const MessagesPage = () => {
const { token } = useAuth();
const [socket, setSocket] = useState<Socket | null>(null);
const [isSocketConnected, setIsSocketConnected] = useState(false);
const queryClient = useQueryClient();
const [form] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState(false);
const [searchParams, setSearchParams] = useState({
page: 1,
pageSize: 10,
type: undefined,
status: undefined,
search: undefined
});
// 获取消息列表
const { data: messages, isLoading } = useQuery({
queryKey: ['messages', searchParams],
queryFn: () => MessageAPI.getMessages(searchParams),
});
// 获取用户列表
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: () => UserAPI.getUsers({ page: 1, limit: 1000 }),
});
// 获取未读消息数
const { data: unreadCount } = useQuery({
queryKey: ['unreadCount'],
queryFn: () => MessageAPI.getUnreadCount(),
});
// 标记消息为已读
const markAsReadMutation = useMutation({
mutationFn: (id: number) => MessageAPI.markAsRead(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['messages'] });
queryClient.invalidateQueries({ queryKey: ['unreadCount'] });
message.success('标记已读成功');
},
});
// 删除消息
const deleteMutation = useMutation({
mutationFn: (id: number) => MessageAPI.deleteMessage(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['messages'] });
message.success('删除成功');
},
});
// 发送消息
// 初始化Socket.IO连接
useEffect(() => {
if (!token) return;
const newSocket = io('/', {
path: '/socket.io',
transports: ['websocket'],
autoConnect: false,
query: {
socket_token: token
}
});
newSocket.on('connect', () => {
setIsSocketConnected(true);
message.success('实时消息连接已建立');
});
newSocket.on('disconnect', () => {
setIsSocketConnected(false);
message.warning('实时消息连接已断开');
});
newSocket.on('error', (err) => {
message.error(`实时消息错误: ${err}`);
});
newSocket.connect();
setSocket(newSocket);
return () => {
newSocket.disconnect();
};
}, [token]);
const sendMessageMutation = useMutation({
mutationFn: async (data: any) => {
// 优先使用Socket.IO发送
if (isSocketConnected && socket) {
return new Promise((resolve, reject) => {
socket.emit('message:send', data, (response: any) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response.data);
}
});
});
}
// 回退到HTTP API
return MessageAPI.sendMessage(data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['messages'] });
queryClient.invalidateQueries({ queryKey: ['unreadCount'] });
message.success('发送成功');
setIsModalVisible(false);
form.resetFields();
},
});
const columns: TableProps<UserMessage>['columns'] = [
{
title: '标题',
dataIndex: 'title',
key: 'title',
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
},
{
title: '发送人',
dataIndex: 'sender_name',
key: 'sender_name',
},
{
title: '状态',
dataIndex: 'user_status',
key: 'user_status',
render: (user_status: MessageStatus) => (
<span style={{ color: user_status === MessageStatus.UNREAD ? 'red' : 'green' }}>
{MessageStatusNameMap[user_status]}
</span>
),
},
{
title: '发送时间',
dataIndex: 'created_at',
key: 'created_at',
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm'),
},
{
title: '操作',
key: 'action',
render: (_: any, record) => (
<Space size="middle">
<Button
type="link"
onClick={() => markAsReadMutation.mutate(record.id)}
disabled={record.user_status === MessageStatus.READ}
>
</Button>
<Button
type="link"
danger
onClick={() => deleteMutation.mutate(record.id)}
>
</Button>
</Space>
),
},
];
const handleSearch = (values: any) => {
setSearchParams({
...searchParams,
...values,
page: 1
});
};
const handleTableChange = (pagination: any) => {
setSearchParams({
...searchParams,
page: pagination.current,
pageSize: pagination.pageSize
});
};
const handleSendMessage = (values: any) => {
sendMessageMutation.mutate(values);
};
return (
<div className="p-4">
<div className="flex justify-between items-center mb-4">
<h1 className="text-2xl font-bold"></h1>
<div className="flex items-center space-x-4">
{unreadCount && unreadCount.count > 0 && (
<span className="text-red-500">{unreadCount.count}</span>
)}
<Button type="primary" onClick={() => setIsModalVisible(true)}>
</Button>
</div>
</div>
<div className="bg-white p-4 rounded shadow">
<Form layout="inline" onFinish={handleSearch} className="mb-4">
<Form.Item name="type" label="类型">
<Select
style={{ width: 120 }}
allowClear
options={[
{ value: MessageType.SYSTEM, label: '系统消息' },
{ value: MessageType.ANNOUNCE, label: '公告' },
{ value: MessageType.PRIVATE, label: '个人消息' },
]}
/>
</Form.Item>
<Form.Item name="status" label="状态">
<Select
style={{ width: 120 }}
allowClear
options={[
{ value: MessageStatus.UNREAD, label: '未读' },
{ value: MessageStatus.READ, label: '已读' },
]}
/>
</Form.Item>
<Form.Item name="search" label="搜索">
<Input placeholder="输入标题或内容" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
</Form>
<Table
columns={columns}
dataSource={messages?.data}
loading={isLoading}
rowKey="id"
pagination={{
current: searchParams.page,
pageSize: searchParams.pageSize,
total: messages?.pagination?.total,
showSizeChanger: true,
}}
onChange={handleTableChange}
/>
</div>
<Modal
title="发送消息"
visible={isModalVisible}
onCancel={() => setIsModalVisible(false)}
footer={null}
width={800}
>
<Form
form={form}
layout="vertical"
onFinish={handleSendMessage}
>
<Form.Item
name="title"
label="标题"
rules={[{ required: true, message: '请输入标题' }]}
>
<Input placeholder="请输入消息标题" />
</Form.Item>
<Form.Item
name="type"
label="消息类型"
rules={[{ required: true, message: '请选择消息类型' }]}
>
<Select
options={[
{ value: MessageType.SYSTEM, label: '系统消息' },
{ value: MessageType.ANNOUNCE, label: '公告' },
{ value: MessageType.PRIVATE, label: '个人消息' },
]}
/>
</Form.Item>
<Form.Item
name="receiver_ids"
label="接收人"
rules={[{ required: true, message: '请选择接收人' }]}
>
<Select
mode="multiple"
placeholder="请选择接收人"
options={users?.data?.map((user: any) => ({
value: user.id,
label: user.username,
}))}
/>
</Form.Item>
<Form.Item
name="content"
label="内容"
rules={[{ required: true, message: '请输入消息内容' }]}
>
<Input.TextArea rows={6} placeholder="请输入消息内容" />
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
loading={sendMessageMutation.status === 'pending'}
icon={isSocketConnected ? <span style={{color:'green'}}></span> : <span style={{color:'red'}}></span>}
>
</Button>
</Form.Item>
</Form>
</Modal>
</div>
);
};