Browse Source

在管理后台菜单栏的消息管理后面新增"机构管理"菜单项

yourname 7 months ago
parent
commit
aa8406ed27

+ 113 - 6
client/admin/api.ts

@@ -2,18 +2,53 @@ import axios from 'axios';
 import { getGlobalConfig } from './utils.ts';
 import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
 import 'dayjs/locale/zh-cn';
-import type { 
+import type {
   User, FileLibrary, FileCategory, ThemeSettings,
- SystemSetting, SystemSettingGroupData, 
- LoginLocation, LoginLocationDetail,
- Message, UserMessage, KnowInfo
+  SystemSetting, SystemSettingGroupData,
+  LoginLocation, LoginLocationDetail,
+  Message, UserMessage, KnowInfo,
+  Organization
 } from '../share/types.ts';
 
-
-
 // 定义API基础URL
 const API_BASE_URL = '/api';
 
+// 通用API响应类型
+export interface ApiResponse<T = any> {
+  code: number;
+  message: string;
+  data: T;
+}
+
+// 创建axios实例
+const request = axios.create({
+  baseURL: API_BASE_URL,
+  timeout: 10000,
+  headers: {
+    'Content-Type': 'application/json'
+  }
+});
+
+// ===================
+// 机构管理API
+// ===================
+
+export interface OrganizationAPI {
+  getOrganizations: () => Promise<ApiResponse<Organization[]>>;
+  getOrganizationTree: () => Promise<ApiResponse<Organization[]>>;
+  createOrganization: (org: Omit<Organization, 'id'>) => Promise<ApiResponse<Organization>>;
+  updateOrganization: (id: number, org: Partial<Organization>) => Promise<ApiResponse<Organization>>;
+  deleteOrganization: (id: number) => Promise<ApiResponse<void>>;
+}
+
+export const organizationAPI: OrganizationAPI = {
+  getOrganizations: () => request.get('/sys/organizations'),
+  getOrganizationTree: () => request.get('/sys/organizations/tree'),
+  createOrganization: (org) => request.post('/sys/organizations', org),
+  updateOrganization: (id, org) => request.put(`/sys/organizations/${id}`, org),
+  deleteOrganization: (id) => request.delete(`/sys/organizations/${id}`)
+};
+
 // 获取OSS完整URL
 export const getOssUrl = (path: string): string => {
   // 获取全局配置中的OSS_HOST,如果不存在使用默认值
@@ -434,6 +469,23 @@ export const ThemeAPI = {
   }
 };
 
+  // 机构管理API
+  export const getOrganizations = async (): Promise<ApiResponse<Organization[]>> => {
+    return request.get('/api/sys/organizations/tree');
+  };
+
+  export const createOrganization = async (data: Omit<Organization, 'id' | 'children'>): Promise<ApiResponse<Organization>> => {
+    return request.post('/api/sys/organizations', data);
+  };
+
+  export const updateOrganization = async (id: number, data: Partial<Organization>): Promise<ApiResponse<Organization>> => {
+    return request.put(`/api/sys/organizations/${id}`, data);
+  };
+
+  export const deleteOrganization = async (id: number): Promise<ApiResponse<void>> => {
+    return request.delete(`/api/sys/organizations/${id}`);
+  };
+
 // 图表数据API接口类型
 interface ChartDataResponse<T> {
   message: string;
@@ -738,7 +790,62 @@ export const KnowInfoAPI = {
   }
 };
 
+// 机构管理API接口类型
+interface OrganizationTreeResponse {
+  message: string;
+  data: Organization[];
+}
+
+interface OrganizationResponse {
+  message: string;
+  data: Organization;
+}
+
+interface OrganizationDeleteResponse {
+  message: string;
+  id: number;
+}
+
 export const SystemAPI = {
+  // 获取机构树
+  getOrganizations: async (): Promise<OrganizationTreeResponse> => {
+    try {
+      const response = await axios.get(`${API_BASE_URL}/sys/organizations/tree`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 创建机构
+  createOrganization: async (orgData: Partial<Organization>): Promise<OrganizationResponse> => {
+    try {
+      const response = await axios.post(`${API_BASE_URL}/sys/organizations`, orgData);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 更新机构
+  updateOrganization: async (id: number, orgData: Partial<Organization>): Promise<OrganizationResponse> => {
+    try {
+      const response = await axios.put(`${API_BASE_URL}/sys/organizations/${id}`, orgData);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 删除机构
+  deleteOrganization: async (id: number): Promise<OrganizationDeleteResponse> => {
+    try {
+      const response = await axios.delete(`${API_BASE_URL}/sys/organizations/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
   // 获取所有系统设置
   getSettings: async (): Promise<SystemSettingGroupData[]> => {
     try {

+ 251 - 0
client/admin/pages_organization.tsx

@@ -0,0 +1,251 @@
+
+import React, { useState, useEffect } from 'react';
+import {
+  Layout, Menu, Button, Table, Space,
+  Form, Input, Select, TreeSelect, message, Modal,
+  Card, Spin, Row, Col, Breadcrumb, Avatar,
+  Dropdown, ConfigProvider, theme, Typography,
+  Switch, Badge, Image, Upload, Divider, Descriptions,
+  Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer, Tree
+} from 'antd';
+
+import 'dayjs/locale/zh-cn';
+import {
+  organizationAPI,
+} from './api.ts';
+
+import type { Organization } from '../share/types.ts';
+  
+const { Title } = Typography;
+
+// 系统管理页面 - 机构管理
+export const OrganizationPage = () => {
+const [organizations, setOrganizations] = useState<Organization[]>([]);
+const [treeData, setTreeData] = useState<Organization[]>([]);
+const [loading, setLoading] = useState(false);
+const [modalVisible, setModalVisible] = useState(false);
+const [currentOrg, setCurrentOrg] = useState<Organization | null>(null);
+const [form] = Form.useForm();
+
+// 加载机构数据
+useEffect(() => {
+    loadOrganizations();
+}, []);
+
+const loadOrganizations = async () => {
+    setLoading(true);
+    try {
+    const res = await organizationAPI.getOrganizations();
+    setTreeData(res.data);
+    // 将树形数据转换为扁平列表
+    const flattenOrganizations = flattenTree(res.data);
+    setOrganizations(flattenOrganizations);
+    } catch (err) {
+    message.error('加载机构数据失败');
+    console.error(err);
+    } finally {
+    setLoading(false);
+    }
+};
+
+// 树形数据扁平化
+// 转换Organization为TreeSelect需要的格式
+const convertToTreeData = (orgs: Organization[]): Array<{value: string; title: string; children?: any}> => {
+    return orgs.map(org => ({
+    value: String(org.id),
+    title: org.name,
+    children: org.children ? convertToTreeData(org.children) : undefined,
+    ...org
+    }));
+};
+
+const flattenTree = (tree: Organization[]): Organization[] => {
+    return tree.reduce((acc, node) => {
+    acc.push({
+        id: node.id,
+        name: node.name,
+        code: node.code,
+        parentId: node.parentId,
+        description: node.description
+    });
+    if (node.children) {
+        acc.push(...flattenTree(node.children));
+    }
+    return acc;
+    }, [] as Organization[]);
+};
+
+// 打开创建机构弹窗
+const handleCreate = () => {
+    setCurrentOrg(null);
+    setModalVisible(true);
+    form.resetFields();
+};
+
+// 打开编辑机构弹窗
+const handleEdit = (record: Organization) => {
+    setCurrentOrg(record);
+    setModalVisible(true);
+    form.setFieldsValue(record);
+};
+
+// 删除机构
+const handleDelete = async (id: number) => {
+    Modal.confirm({
+    title: '确认删除',
+    content: '确定要删除该机构吗?',
+    onOk: async () => {
+        try {
+        await organizationAPI.deleteOrganization(id);
+        message.success('删除成功');
+        loadOrganizations();
+        } catch (err) {
+        message.error('删除失败');
+        }
+    }
+    });
+};
+
+// 提交表单
+const handleSubmit = async () => {
+    try {
+    const values = await form.validateFields();
+    if (currentOrg?.id) {
+        // 更新机构
+        await organizationAPI.updateOrganization(currentOrg.id, values);
+        message.success('更新成功');
+    } else {
+        // 创建机构
+        await organizationAPI.createOrganization(values);
+        message.success('创建成功');
+    }
+    setModalVisible(false);
+    loadOrganizations();
+    } catch (err) {
+    console.error(err);
+    }
+};
+
+const columns = [
+    {
+    title: '机构名称',
+    dataIndex: 'name',
+    key: 'name',
+    },
+    {
+    title: '机构编码',
+    dataIndex: 'code',
+    key: 'code',
+    },
+    {
+    title: '描述',
+    dataIndex: 'description',
+    key: 'description',
+    },
+    {
+    title: '操作',
+    key: 'action',
+    render: (_: any, record: any) => (
+        <Space size="middle">
+        <Button size="small" onClick={() => handleEdit(record)}>
+            编辑
+        </Button>
+        <Button
+            size="small"
+            danger
+            onClick={() => handleDelete(record.id)}
+        >
+            删除
+        </Button>
+        </Space>
+    ),
+    },
+];
+
+return (
+    <div>
+    <Title level={2}>机构管理</Title>
+    
+    <div className="bg-white rounded-lg shadow p-6">
+        <div className="flex justify-between mb-4">
+        <Button type="primary" onClick={handleCreate}>
+            新增机构
+        </Button>
+        </div>
+
+        <Tabs defaultActiveKey="1">
+        <Tabs.TabPane tab="树形展示" key="1">
+            <TreeSelect
+            treeData={treeData}
+            placeholder="请选择机构"
+            treeDefaultExpandAll
+            className="w-full"
+            dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+            fieldNames={{ label: 'name', value: 'id', children: 'children' }}
+            onSelect={(value, node) => {
+                const org = organizations.find(o => o.id === value);
+                if (org) handleEdit(org);
+            }}
+            />
+        </Tabs.TabPane>
+        <Tabs.TabPane tab="表格展示" key="2">
+            <Table
+            dataSource={organizations}
+            rowKey="id"
+            loading={loading}
+            pagination={false}
+            columns={columns}
+            />
+        </Tabs.TabPane>
+        </Tabs>
+    </div>
+
+    {/* 新增/编辑弹窗 */}
+    <Modal
+        title={currentOrg?.id ? '编辑机构' : '新增机构'}
+        open={modalVisible}
+        onOk={handleSubmit}
+        onCancel={() => setModalVisible(false)}
+        confirmLoading={loading}
+    >
+        <Form form={form} layout="vertical">
+        <Form.Item
+            name="name"
+            label="机构名称"
+            rules={[{ required: true, message: '请输入机构名称' }]}
+        >
+            <Input placeholder="请输入机构名称" />
+        </Form.Item>
+
+        <Form.Item
+            name="code"
+            label="机构编码"
+            rules={[{ required: true, message: '请输入机构编码' }]}
+        >
+            <Input placeholder="请输入机构编码" />
+        </Form.Item>
+
+        <Form.Item
+            name="parentId"
+            label="上级机构"
+        >
+            <TreeSelect
+            treeData={treeData}
+            placeholder="请选择上级机构"
+            allowClear
+            treeDefaultExpandAll
+            dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+            />
+        </Form.Item>
+
+        <Form.Item
+            name="description"
+            label="机构描述"
+        >
+            <Input.TextArea placeholder="请输入机构描述" rows={3} />
+        </Form.Item>
+        </Form>
+    </Modal>
+    </div>
+);
+};

+ 6 - 3
client/admin/pages_sys.tsx

@@ -1,11 +1,11 @@
 import React, { useState, useEffect } from 'react';
-import { 
+import {
   Layout, Menu, Button, Table, Space,
-  Form, Input, Select, message, Modal,
+  Form, Input, Select, TreeSelect, message, Modal,
   Card, Spin, Row, Col, Breadcrumb, Avatar,
   Dropdown, ConfigProvider, theme, Typography,
   Switch, Badge, Image, Upload, Divider, Descriptions,
-  Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
+  Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer, Tree
 } from 'antd';
 import {
   UploadOutlined,
@@ -38,8 +38,10 @@ import { getEnumOptions } from './utils.ts';
 import {
   FileAPI,
   UserAPI,
+  organizationAPI,
 } from './api.ts';
 
+import type { Organization } from '../share/types.ts';
 
 // 配置 dayjs 插件
 dayjs.extend(weekday);
@@ -506,6 +508,7 @@ export const FileLibraryPage = () => {
       setUploadLoading(false);
     }
   };
+  
 
   // 处理文件上传
   const uploadProps = {

+ 11 - 0
client/admin/web_app.tsx

@@ -66,6 +66,7 @@ import {ThemeSettingsPage} from './pages_theme_settings.tsx'
 import { ChartDashboardPage } from './pages_chart.tsx';
 import { LoginMapPage } from './pages_map.tsx';
 import { LoginPage } from './pages_login_reg.tsx';
+import { OrganizationPage } from './pages_organization.tsx'
 import { ProtectedRoute } from './components_protected_route.tsx';
 
 // 配置 dayjs 插件
@@ -167,6 +168,11 @@ const MainLayout = () => {
       icon: <BellOutlined />,
       label: '消息管理',
     },
+    {
+      key: '/organizations',
+      icon: <TeamOutlined />,
+      label: '机构管理',
+    },
     {
       key: '/settings',
       icon: <SettingOutlined />,
@@ -539,6 +545,11 @@ const App = () => {
           element: <MessagesPage />,
           errorElement: <ErrorPage />
         },
+        {
+          path: 'organizations',
+          element: <OrganizationPage />,
+          errorElement: <ErrorPage />
+        },
       ],
     },
   ]);

+ 21 - 0
client/share/types.ts

@@ -537,3 +537,24 @@ export interface UserMessage {
   message?: Message;
   sender?: User;
 }
+
+// 机构类型定义
+export interface Organization {
+  /** 机构ID */
+  id: number;
+  
+  /** 机构名称 */
+  name: string;
+  
+  /** 机构编码 */
+  code: string;
+  
+  /** 父机构ID */
+  parentId?: number;
+  
+  /** 机构描述 */
+  description?: string;
+  
+  /** 子机构列表 */
+  children?: Organization[];
+}

+ 188 - 0
server/routes_sys.ts

@@ -1061,3 +1061,191 @@ export function createSystemSettingsRoutes(withAuth: WithAuth) {
 
   return settingsRoutes;
 }
+
+import { Organization } from "../client/share/types.ts";
+
+// 创建机构管理路由
+export function createOrganizationRoutes(withAuth: WithAuth) {
+  const organizationRoutes = new Hono<{ Variables: Variables }>();
+
+  // 获取机构树形列表
+  organizationRoutes.get("/tree", withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+
+      // 获取所有机构数据
+      const organizations = await apiClient.database
+        .table("organizations")
+        .where("is_deleted", DeleteStatus.NOT_DELETED)
+        .orderBy("id", "asc");
+
+      // 构建树形结构
+      const buildTree = (parentId?: number): Organization[] => {
+        return organizations
+          .filter((org: any) => org.parent_id === parentId)
+          .map((org: any): Organization => ({
+            id: org.id,
+            name: org.name,
+            code: org.code,
+            parentId: org.parent_id,
+            description: org.description,
+            children: buildTree(org.id)
+          }));
+      };
+
+      const treeData = buildTree();
+
+      return c.json({
+        message: "获取机构树成功",
+        data: treeData
+      });
+    } catch (error) {
+      log.api("获取机构树失败:", error);
+      return c.json({ error: "获取机构树失败" }, 500);
+    }
+  });
+
+  // 创建新机构
+  organizationRoutes.post("/", withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+      const user = c.get('user');
+      const orgData = (await c.req.json()) as Partial<Organization>;
+
+      // 验证必填字段
+      if (!orgData.name) {
+        return c.json({ error: "机构名称不能为空" }, 400);
+      }
+
+      if (!orgData.code) {
+        return c.json({ error: "机构编码不能为空" }, 400);
+      }
+
+      // 插入新机构
+      const [id] = await apiClient.database.table("organizations").insert({
+        ...orgData,
+        created_by: user?.id,
+        updated_by: user?.id,
+        is_deleted: DeleteStatus.NOT_DELETED,
+        created_at: apiClient.database.fn.now(),
+        updated_at: apiClient.database.fn.now()
+      });
+
+      // 获取创建的机构
+      const [createdOrg] = await apiClient.database
+        .table("organizations")
+        .where("id", id);
+
+      return c.json({
+        message: "机构创建成功",
+        data: createdOrg
+      });
+    } catch (error) {
+      log.api("创建机构失败:", error);
+      return c.json({ error: "创建机构失败" }, 500);
+    }
+  });
+
+  // 更新机构信息
+  organizationRoutes.put("/:id", withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+      const user = c.get('user');
+      const id = Number(c.req.param("id"));
+      const orgData = (await c.req.json()) as Partial<Organization>;
+
+      if (!id || isNaN(id)) {
+        return c.json({ error: "无效的机构ID" }, 400);
+      }
+
+      // 验证必填字段
+      if (!orgData.name) {
+        return c.json({ error: "机构名称不能为空" }, 400);
+      }
+
+      // 检查机构是否存在
+      const [existingOrg] = await apiClient.database
+        .table("organizations")
+        .where({ id, is_deleted: DeleteStatus.NOT_DELETED });
+
+      if (!existingOrg) {
+        return c.json({ error: "机构不存在" }, 404);
+      }
+
+      // 更新机构信息
+      await apiClient.database
+        .table("organizations")
+        .where("id", id)
+        .update({
+          ...orgData,
+          updated_by: user?.id,
+          updated_at: apiClient.database.fn.now()
+        });
+
+      // 获取更新后的机构
+      const [updatedOrg] = await apiClient.database
+        .table("organizations")
+        .where("id", id);
+
+      return c.json({
+        message: "机构更新成功",
+        data: updatedOrg
+      });
+    } catch (error) {
+      log.api("更新机构失败:", error);
+      return c.json({ error: "更新机构失败" }, 500);
+    }
+  });
+
+  // 删除机构(软删除)
+  organizationRoutes.delete("/:id", withAuth, async (c) => {
+    try {
+      const apiClient = c.get('apiClient');
+      const user = c.get('user');
+      const id = Number(c.req.param("id"));
+
+      if (!id || isNaN(id)) {
+        return c.json({ error: "无效的机构ID" }, 400);
+      }
+
+      // 检查机构是否存在
+      const [existingOrg] = await apiClient.database
+        .table("organizations")
+        .where({ id, is_deleted: DeleteStatus.NOT_DELETED });
+
+      if (!existingOrg) {
+        return c.json({ error: "机构不存在" }, 404);
+      }
+
+      // 检查是否有子机构
+      const [hasChildren] = await apiClient.database
+        .table("organizations")
+        .where({ parent_id: id, is_deleted: DeleteStatus.NOT_DELETED })
+        .limit(1);
+
+      if (hasChildren) {
+        return c.json({ error: "该机构下有子机构,不能删除" }, 400);
+      }
+
+      // 软删除机构
+      await apiClient.database
+        .table("organizations")
+        .where("id", id)
+        .update({
+          is_deleted: DeleteStatus.DELETED,
+          updated_by: user?.id,
+          updated_at: apiClient.database.fn.now()
+        });
+
+      return c.json({
+        message: "机构删除成功"
+      });
+    } catch (error) {
+      log.api("删除机构失败:", error);
+      return c.json({ error: "删除机构失败" }, 500);
+    }
+  });
+
+  return organizationRoutes;
+}
+

+ 13 - 0
新需求.md

@@ -0,0 +1,13 @@
+1
+
+2
+
+
+3
+
+5.13
+
+
+5.12
+
+5.11