新增消息管理页面,整合消息API,支持消息列表展示、未读消息统计、消息标记为已读及删除功能,提升用户消息管理体验。
This commit is contained in:
@@ -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;
|
||||
|
||||
283
client/admin/pages_messages.tsx
Normal file
283
client/admin/pages_messages.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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 />
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user