新增消息管理页面,整合消息API,支持消息列表展示、未读消息统计、消息标记为已读及删除功能,提升用户消息管理体验。

This commit is contained in:
zyh
2025-04-10 09:07:31 +00:00
parent 948cd5263e
commit d5c31777d2
4 changed files with 383 additions and 3 deletions

View File

@@ -3,7 +3,9 @@ import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types
import 'dayjs/locale/zh-cn';
import type {
User, FileLibrary, FileCategory, ThemeSettings,
SystemSetting, SystemSettingGroupData, LoginLocation, LoginLocationDetail
SystemSetting, SystemSettingGroupData,
LoginLocation, LoginLocationDetail,
Message, UserMessage
} from '../share/types.ts';
@@ -502,6 +504,90 @@ export const ChartAPI = {
}
};
// 消息API接口类型
interface MessagesResponse {
data: UserMessage[];
pagination: {
total: number;
current: number;
pageSize: number;
totalPages: number;
};
}
interface MessageResponse {
data: Message;
message?: string;
}
interface MessageCountResponse {
count: number;
}
// 消息API
export const MessageAPI = {
// 获取消息列表
getMessages: async (params?: {
page?: number,
pageSize?: number,
type?: string,
status?: string,
search?: string
}): Promise<MessagesResponse> => {
try {
const response = await axios.get(`${API_BASE_URL}/messages`, { params });
return response.data;
} catch (error) {
throw error;
}
},
// 发送消息
sendMessage: async (data: {
title: string,
content: string,
type: string,
receiver_ids: number[]
}): Promise<MessageResponse> => {
try {
const response = await axios.post(`${API_BASE_URL}/messages`, data);
return response.data;
} catch (error) {
throw error;
}
},
// 获取未读消息数
getUnreadCount: async (): Promise<MessageCountResponse> => {
try {
const response = await axios.get(`${API_BASE_URL}/messages/count/unread`);
return response.data;
} catch (error) {
throw error;
}
},
// 标记消息为已读
markAsRead: async (id: number): Promise<MessageResponse> => {
try {
const response = await axios.post(`${API_BASE_URL}/messages/${id}/read`);
return response.data;
} catch (error) {
throw error;
}
},
// 删除消息
deleteMessage: async (id: number): Promise<MessageResponse> => {
try {
const response = await axios.delete(`${API_BASE_URL}/messages/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
};
// 地图相关API的接口类型定义
export interface LoginLocationResponse {
message: string;

View File

@@ -0,0 +1,283 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient, UseMutationResult } from '@tanstack/react-query';
import { Button, Table, Space, Modal, Form, Input, Select, message } from 'antd';
import type { TableProps } from 'antd';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import { MessageAPI } from './api.ts';
import { UserAPI } from './api.ts';
import type { UserMessage } from '../share/types.ts';
import { MessageStatusNameMap , MessageStatus} from '../share/types.ts';
export const MessagesPage = () => {
const queryClient = useQueryClient();
const [form] = Form.useForm();
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
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('删除成功');
},
});
// 发送消息
const sendMessageMutation = useMutation({
mutationFn: (data: any) => 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: 'status',
key: 'status',
render: (status: MessageStatus) => (
<span style={{ color: status === MessageStatus.UNREAD ? 'red' : 'green' }}>
{MessageStatusNameMap[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.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: 'SYSTEM', label: '系统消息' },
{ value: 'NOTICE', label: '公告' },
{ value: 'PERSONAL', label: '个人消息' },
]}
/>
</Form.Item>
<Form.Item name="status" label="状态">
<Select
style={{ width: 120 }}
allowClear
options={[
{ value: 'UNREAD', label: '未读' },
{ value: '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: 'SYSTEM', label: '系统消息' },
{ value: 'NOTICE', label: '公告' },
{ value: 'PERSONAL', 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'}
>
</Button>
</Form.Item>
</Form>
</Modal>
</div>
);
};

View File

@@ -74,7 +74,8 @@ import {
KnowInfoPage,
FileLibraryPage
} from './pages_sys.tsx';
import {
import { MessagesPage } from './pages_messages.tsx';
import {
SettingsPage,
ThemeSettingsPage,
} from './pages_settings.tsx';
@@ -184,6 +185,11 @@ const MainLayout = () => {
icon: <TeamOutlined />,
label: '用户管理',
},
{
key: '/messages',
icon: <BellOutlined />,
label: '消息管理',
},
{
key: '/settings',
icon: <SettingOutlined />,
@@ -555,6 +561,11 @@ const App = () => {
element: <FileLibraryPage />,
errorElement: <ErrorPage />
},
{
path: 'messages',
element: <MessagesPage />,
errorElement: <ErrorPage />
},
],
},
]);

View File

@@ -28,7 +28,7 @@ export function createUserRoutes(withAuth: WithAuth) {
}
const total = await query.clone().count()
const users = await query.select('id', 'username', 'nickname', 'email', 'phone', 'role', 'created_at')
const users = await query.select('id', 'username', 'nickname', 'email', 'phone', 'created_at')
.limit(pageSize).offset(offset)
return c.json({