浏览代码

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

zyh 8 月之前
父节点
当前提交
d5c31777d2
共有 4 个文件被更改,包括 383 次插入3 次删除
  1. 87 1
      client/admin/api.ts
  2. 283 0
      client/admin/pages_messages.tsx
  3. 12 1
      client/admin/web_app.tsx
  4. 1 1
      server/routes_users.ts

+ 87 - 1
client/admin/api.ts

@@ -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 - 0
client/admin/pages_messages.tsx

@@ -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>
+  );
+};

+ 12 - 1
client/admin/web_app.tsx

@@ -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 />
+        },
       ],
     },
   ]);

+ 1 - 1
server/routes_users.ts

@@ -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({