Forráskód Böngészése

表单管理页面列表对接

wyn 5 hónapja
szülő
commit
75535b636b

+ 15 - 0
menu-config.json

@@ -330,6 +330,21 @@
           "order": 4,
           "children": null,
           "description": "组件文件: src/components/IsolationWork.tsx, subMenu: 流程设计"
+        },
+        {
+          "id": 65,
+          "parentId": 6,
+          "name": "表单管理",
+          "path": "/jobTicket/form",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:document",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 5,
+          "children": null,
+          "description": "组件文件: src/components/FormManagement.tsx"
         }
       ],
       "description": "隔离作业 - 组件文件: src/components/IsolationWork.tsx"

+ 43 - 3
src/Dashboard.tsx

@@ -11,6 +11,7 @@ import ProfileSettings from './components/ProfileSettings';
 import CockpitDashboard from './components/CockpitDashboard';
 import LockCabinetDetail from './components/lockCabinet/LockCabinetDetail';
 import NotificationManagement from './components/NotificationManagement';
+import FormManagement from './components/FormManagement';
 import { authApi } from './api';
 import { toast } from 'sonner';
 import { Toaster } from 'sonner';
@@ -104,6 +105,7 @@ export default function Dashboard() {
     
     // 隔离作业相关
     if (path === '/jobTicket' || path.startsWith('/jobTicket') || path === '/isolation' || path.startsWith('/isolation') || path === '/CustomWorkflow' || path.startsWith('/CustomWorkflow') || path === '/sopm' || path.startsWith('/sopm')) {
+      if (path.includes('/form') || path.endsWith('/form')) return 'formManagement';
       if (path.includes('job') || path.endsWith('/job')) return 'workManagement';
       if (path.includes('sop') || path.endsWith('/sop')) return 'sopManagement';
       if (path.includes('design') || path.endsWith('/design')) return 'processDesign';
@@ -180,6 +182,7 @@ export default function Dashboard() {
             { key: 'sopManagement', icon: BookOpen, path: '/isolation/record', name: 'SOP管理' },
             { key: 'workManagement', icon: Activity, path: '/isolation/list', name: '作业管理' },
             { key: 'processDesign', icon: Workflow, path: '/jobTicket/design', name: '流程设计' },
+            { key: 'formManagement', icon: FileText, path: '/jobTicket/form', name: '表单管理' },
           ],
         }
       };
@@ -371,6 +374,7 @@ export default function Dashboard() {
               return;
             }
             let childKey: string = mapBackendPathToFrontendKey(child.path) || '';
+            console.log('处理子菜单:', { path: child.path, mappedKey: childKey });
             if (!childKey) {
               // 如果无法映射,尝试根据路径推断
               // 优先处理 clientSystem/IsolationWork 路径
@@ -386,6 +390,21 @@ export default function Dashboard() {
                 } else {
                   childKey = 'isolationWork';
                 }
+              } else if (child.path.includes('/jobTicket') || child.path.startsWith('/jobTicket')) {
+                // 处理 /jobTicket 路径
+                if (child.path.includes('/form') || child.path.endsWith('/form')) {
+                  childKey = 'formManagement';
+                } else if (child.path.includes('/processDesign') || child.path.endsWith('/design')) {
+                  childKey = 'processDesign';
+                } else if (child.path.includes('/sop') || child.path.endsWith('/sop')) {
+                  childKey = 'sopManagement';
+                } else if (child.path.includes('/job') || child.path.endsWith('/job')) {
+                  childKey = 'workManagement';
+                } else if (child.path.includes('/step') || child.path.includes('/template')) {
+                  childKey = 'processTemplate';
+                } else {
+                  childKey = 'isolationWork';
+                }
               } else if (child.path.includes('/dept')) {
                 childKey = 'departmentManagement';
               } else if (child.path.includes('/menu')) {
@@ -403,6 +422,7 @@ export default function Dashboard() {
                 childKey = child.path.replace(/[^a-zA-Z0-9]/g, '_') || `child_${child.id}`;
               }
             }
+            console.log('最终 childKey:', childKey);
             
             // 检查是否已经添加过(去重)
             const subMenuKey = `${frontendKey}_${childKey}`;
@@ -591,6 +611,11 @@ export default function Dashboard() {
         // 硬件管理的子菜单
         setActiveMenu('hardwareManagement');
         setActiveSubMenu(menuKey);
+      } else if (menuKey === 'formManagement' || menuKey === 'processDesign' || menuKey === 'sopManagement' || 
+                 menuKey === 'workManagement' || menuKey === 'processTemplate') {
+        // 隔离作业的子菜单
+        setActiveMenu('isolationWork');
+        setActiveSubMenu(menuKey);
       } else if (filteredSubMenuConfig[menuKey] && filteredSubMenuConfig[menuKey].length > 0) {
         // 其他菜单,设置第一个子菜单
         setActiveMenu(menuKey);
@@ -648,6 +673,11 @@ export default function Dashboard() {
         // 硬件管理的子菜单
         setActiveMenu('hardwareManagement');
         setActiveSubMenu(menuKey);
+      } else if (menuKey === 'formManagement' || menuKey === 'processDesign' || menuKey === 'sopManagement' || 
+                 menuKey === 'workManagement' || menuKey === 'processTemplate') {
+        // 隔离作业的子菜单
+        setActiveMenu('isolationWork');
+        setActiveSubMenu(menuKey);
       } else if (filteredSubMenuConfig[menuKey] && filteredSubMenuConfig[menuKey].length > 0) {
         // 其他菜单,设置第一个子菜单
         setActiveMenu(menuKey);
@@ -1017,9 +1047,19 @@ export default function Dashboard() {
         ) : activeMenu === 'locationManagement' ? (
           <SegregationPointManagement />
         ) : activeMenu === 'isolationWork' ? (
-          <IsolationWork subMenu={
-            filteredSubMenuConfig[activeMenu]?.find(item => item.key === activeSubMenu)?.name || activeSubMenu
-          } />
+          (() => {
+            // 调试信息
+            console.log('检查隔离作业子菜单:', { activeMenu, activeSubMenu, location: location.pathname });
+            // 支持 'formManagement' 和 'form' 两种 key
+            if (activeSubMenu === 'formManagement' || activeSubMenu === 'form' || location.pathname.includes('/form')) {
+              console.log('✅ 渲染 FormManagement 组件', { activeMenu, activeSubMenu });
+              return <FormManagement />;
+            } else {
+              const subMenuName = filteredSubMenuConfig[activeMenu]?.find(item => item.key === activeSubMenu)?.name || activeSubMenu;
+              console.log('⚠️ 渲染 IsolationWork 组件', { activeMenu, activeSubMenu, subMenuName });
+              return <IsolationWork subMenu={subMenuName} />;
+            }
+          })()
         ) : activeMenu === 'notificationManagement' ? (
           <NotificationManagement />
         ) : (

+ 71 - 0
src/api/bpm/form.ts

@@ -0,0 +1,71 @@
+import { request } from '../../utils/axios';
+
+export interface FormVO {
+  id?: number;
+  name: string;
+  conf: string;
+  fields: string[];
+  status: number;
+  remark?: string;
+  createTime?: string;
+}
+
+export interface FormPageParams {
+  pageNo: number;
+  pageSize: number;
+  name?: string;
+}
+
+export interface FormPageResponse {
+  list: FormVO[];
+  total: number;
+}
+
+// 创建工作流的表单定义
+export const createForm = async (data: FormVO): Promise<void> => {
+  await request.post({
+    url: '/bpm/form/create',
+    data: data
+  });
+};
+
+// 更新工作流的表单定义
+export const updateForm = async (data: FormVO): Promise<void> => {
+  await request.put({
+    url: '/bpm/form/update',
+    data: data
+  });
+};
+
+// 删除工作流的表单定义
+export const deleteForm = async (id: number): Promise<void> => {
+  await request.delete({
+    url: `/bpm/form/delete?id=${id}`
+  });
+};
+
+// 获得工作流的表单定义
+export const getForm = async (id: number): Promise<FormVO> => {
+  const response = await request.get({
+    url: `/bpm/form/get?id=${id}`
+  });
+  return response as FormVO;
+};
+
+// 获得工作流的表单定义分页
+export const getFormPage = async (params: FormPageParams): Promise<FormPageResponse> => {
+  const response = await request.get({
+    url: '/bpm/form/page',
+    params
+  });
+  return response as FormPageResponse;
+};
+
+// 获得动态表单的精简列表
+export const getFormSimpleList = async (): Promise<FormVO[]> => {
+  const response = await request.get({
+    url: '/bpm/form/simple-list'
+  });
+  return response as FormVO[];
+};
+

+ 306 - 0
src/components/FormManagement.tsx

@@ -0,0 +1,306 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { Search, Plus, RefreshCw, Edit2, Trash2, Copy, Eye, X } from 'lucide-react';
+import { Button, Input, Space, Table as AntdTable, Modal, message } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import { toast } from 'sonner';
+import * as FormApi from '../api/bpm/form';
+import { DICT_TYPE, getDictLabel } from '../utils/dict';
+import { dateFormatter } from '../utils/formatTime';
+import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
+
+export default function FormManagement() {
+  const navigate = useNavigate();
+  const location = useLocation();
+  
+  // 调试信息
+  useEffect(() => {
+    console.log('FormManagement 组件已加载');
+  }, []);
+  
+  const [loading, setLoading] = useState(true);
+  const [list, setList] = useState<FormApi.FormVO[]>([]);
+  const [total, setTotal] = useState(0);
+  const [queryParams, setQueryParams] = useState<FormApi.FormPageParams>({
+    pageNo: 1,
+    pageSize: 10,
+    name: undefined
+  });
+  
+  // 详情弹窗
+  const [detailVisible, setDetailVisible] = useState(false);
+  const [detailData, setDetailData] = useState<FormCreateData>({
+    rule: [],
+    option: {}
+  });
+
+  /** 查询列表 */
+  const getList = async () => {
+    setLoading(true);
+    try {
+      console.log('FormManagement: 开始获取表单列表', queryParams);
+      const data = await FormApi.getFormPage(queryParams);
+      console.log('FormManagement: 获取到数据', data);
+      setList(data.list || []);
+      setTotal(data.total || 0);
+    } catch (error: any) {
+      console.error('FormManagement: 获取表单列表失败', error);
+      toast.error(error.message || '获取表单列表失败');
+      // 确保即使失败也显示空列表
+      setList([]);
+      setTotal(0);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    getList();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [queryParams.pageNo, queryParams.pageSize, queryParams.name]);
+
+  /** 搜索按钮操作 */
+  const handleQuery = () => {
+    setQueryParams({ ...queryParams, pageNo: 1 });
+    getList();
+  };
+
+  /** 重置按钮操作 */
+  const resetQuery = () => {
+    setQueryParams({
+      pageNo: 1,
+      pageSize: 10,
+      name: undefined
+    });
+  };
+
+  /** 添加/修改操作 */
+  const openForm = (type: string, id?: number) => {
+    const query: Record<string, string> = { type };
+    if (id !== undefined && (typeof id === 'number' || typeof id === 'string')) {
+      query.id = String(id);
+    }
+    // 跳转到表单编辑器(这里需要根据实际路由配置)
+    navigate(`/bpm/form/editor?${new URLSearchParams(query).toString()}`);
+  };
+
+  /** 删除按钮操作 */
+  const handleDelete = async (id: number) => {
+    Modal.confirm({
+      title: '确认删除',
+      content: '确定要删除这条表单数据吗?',
+      okText: '确定',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await FormApi.deleteForm(id);
+          toast.success('删除成功');
+          await getList();
+        } catch (error: any) {
+          toast.error(error.message || '删除失败');
+        }
+      }
+    });
+  };
+
+  /** 详情操作 */
+  const openDetail = async (rowId: number) => {
+    try {
+      const data = await FormApi.getForm(rowId);
+      setConfAndFields2(setDetailData, data.conf, data.fields);
+      setDetailVisible(true);
+    } catch (error: any) {
+      toast.error(error.message || '获取表单详情失败');
+    }
+  };
+
+  // 表格列配置
+  const columns: ColumnsType<FormApi.FormVO> = [
+    {
+      title: '编号',
+      dataIndex: 'id',
+      width: 80,
+      align: 'center',
+    },
+    {
+      title: '表单名',
+      dataIndex: 'name',
+      width: 200,
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      width: 100,
+      align: 'center',
+      render: (status: number) => {
+        const label = getDictLabel(DICT_TYPE.COMMON_STATUS, status);
+        const colorType = status === 0 ? 'success' : 'danger';
+        return (
+          <span
+            className={`inline-flex px-3 py-1 rounded-lg text-xs ${
+              colorType === 'success'
+                ? 'bg-green-100 text-green-700'
+                : 'bg-red-100 text-red-700'
+            }`}
+          >
+            {label}
+          </span>
+        );
+      },
+    },
+    {
+      title: '备注',
+      dataIndex: 'remark',
+      ellipsis: true,
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createTime',
+      width: 180,
+      align: 'center',
+      render: (text: string) => dateFormatter(text),
+    },
+    {
+      title: '操作',
+      width: 280,
+      align: 'center',
+      fixed: 'right',
+      render: (_: any, record: FormApi.FormVO) => (
+        <Space size="small">
+          <Button
+            type="link"
+            size="small"
+            icon={<Copy className="w-4 h-4" />}
+            onClick={() => openForm('copy', record.id)}
+          >
+            复制
+          </Button>
+          <Button
+            type="link"
+            size="small"
+            icon={<Edit2 className="w-4 h-4" />}
+            onClick={() => openForm('update', record.id)}
+          >
+            编辑
+          </Button>
+          <Button
+            type="link"
+            size="small"
+            icon={<Eye className="w-4 h-4" />}
+            onClick={() => openDetail(record.id!)}
+          >
+            详情
+          </Button>
+          <Button
+            type="link"
+            size="small"
+            danger
+            icon={<Trash2 className="w-4 h-4" />}
+            onClick={() => handleDelete(record.id!)}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div className="space-y-6">
+      {/* 搜索工作栏 */}
+      <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-4 lg:p-5">
+        <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap">
+          <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
+            <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">表单名:</label>
+              <Input
+                value={queryParams.name || ''}
+                onChange={(e) => setQueryParams({ ...queryParams, name: e.target.value || undefined })}
+                onPressEnter={handleQuery}
+                placeholder="请输入表单名"
+                className="min-w-[200px] max-w-[300px]"
+                allowClear
+              />
+            </div>
+          </div>
+
+          <Space className="flex-shrink-0">
+            <Button
+              type="primary"
+              icon={<Search className="w-4 h-4" />}
+              onClick={handleQuery}
+            >
+              搜索
+            </Button>
+            <Button
+              icon={<RefreshCw className="w-4 h-4" />}
+              onClick={resetQuery}
+            >
+              重置
+            </Button>
+            <Button
+              type="primary"
+              icon={<Plus className="w-4 h-4" />}
+              onClick={() => openForm('create')}
+            >
+              新增
+            </Button>
+          </Space>
+        </div>
+      </div>
+
+      {/* 列表 */}
+      <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
+        <AntdTable
+          loading={loading}
+          columns={columns}
+          dataSource={list}
+          rowKey="id"
+          pagination={{
+            current: queryParams.pageNo,
+            pageSize: queryParams.pageSize,
+            total: total,
+            showSizeChanger: true,
+            showTotal: (total) => `共 ${total} 条记录`,
+            onChange: (page, pageSize) => {
+              setQueryParams({ ...queryParams, pageNo: page, pageSize: pageSize || 10 });
+            },
+          }}
+          scroll={{ x: 'max-content' }}
+        />
+      </div>
+
+      {/* 表单详情的弹窗 */}
+      <Modal
+        title="表单详情"
+        open={detailVisible}
+        onCancel={() => setDetailVisible(false)}
+        footer={[
+          <Button key="close" onClick={() => setDetailVisible(false)}>
+            关闭
+          </Button>
+        ]}
+        width={800}
+      >
+        <div className="p-4">
+          {/* 这里可以集成 form-create 组件来展示表单 */}
+          <div className="space-y-4">
+            <div>
+              <label className="text-sm font-medium text-gray-700">表单配置:</label>
+              <pre className="mt-2 p-4 bg-gray-50 rounded-lg text-xs overflow-auto max-h-96">
+                {JSON.stringify(detailData.option, null, 2)}
+              </pre>
+            </div>
+            <div>
+              <label className="text-sm font-medium text-gray-700">表单字段:</label>
+              <pre className="mt-2 p-4 bg-gray-50 rounded-lg text-xs overflow-auto max-h-96">
+                {JSON.stringify(detailData.rule, null, 2)}
+              </pre>
+            </div>
+          </div>
+        </div>
+      </Modal>
+    </div>
+  );
+}
+

+ 129 - 0
src/utils/formCreate.ts

@@ -0,0 +1,129 @@
+/**
+ * 表单创建工具函数
+ * 用于处理动态表单的配置和字段设置
+ */
+
+import type { Dispatch, SetStateAction } from 'react';
+
+/**
+ * 表单配置和字段数据类型
+ */
+export interface FormCreateData {
+  rule: any[];
+  option: Record<string, any>;
+}
+
+/**
+ * 设置表单配置和字段(React 版本)
+ * @param setState React 的 setState 函数或 ref 的 setter
+ * @param conf 表单配置字符串(JSON 格式)
+ * @param fields 表单字段数组(JSON 格式字符串或数组)
+ */
+export function setConfAndFields2(
+  setState: Dispatch<SetStateAction<FormCreateData>> | { current: FormCreateData },
+  conf: string | Record<string, any>,
+  fields: string | string[] | any[]
+): void {
+  let parsedConf: Record<string, any> = {};
+  let parsedFields: any[] = [];
+
+  // 解析配置
+  if (typeof conf === 'string') {
+    try {
+      parsedConf = JSON.parse(conf);
+    } catch (e) {
+      console.error('解析表单配置失败:', e);
+      parsedConf = {};
+    }
+  } else if (conf && typeof conf === 'object') {
+    parsedConf = conf;
+  }
+
+  // 解析字段
+  if (typeof fields === 'string') {
+    try {
+      parsedFields = JSON.parse(fields);
+    } catch (e) {
+      console.error('解析表单字段失败:', e);
+      parsedFields = [];
+    }
+  } else if (Array.isArray(fields)) {
+    parsedFields = fields.map((field) => {
+      if (typeof field === 'string') {
+        try {
+          return JSON.parse(field);
+        } catch (e) {
+          return field;
+        }
+      }
+      return field;
+    });
+  }
+
+  // 更新状态
+  const newData: FormCreateData = {
+    rule: parsedFields,
+    option: parsedConf
+  };
+
+  // 处理 React setState 或 ref
+  if (typeof setState === 'function') {
+    setState(newData);
+  } else if (setState && typeof setState === 'object' && 'current' in setState) {
+    setState.current = newData;
+  }
+}
+
+/**
+ * 设置表单配置和字段(兼容 Vue ref 风格的函数)
+ * 用于直接修改 ref 对象的属性
+ * @param refObject 包含 rule 和 option 属性的对象引用
+ * @param conf 表单配置字符串(JSON 格式)
+ * @param fields 表单字段数组(JSON 格式字符串或数组)
+ */
+export function setConfAndFields(
+  refObject: { rule: any[]; option: Record<string, any> },
+  conf: string | Record<string, any>,
+  fields: string | string[] | any[]
+): void {
+  let parsedConf: Record<string, any> = {};
+  let parsedFields: any[] = [];
+
+  // 解析配置
+  if (typeof conf === 'string') {
+    try {
+      parsedConf = JSON.parse(conf);
+    } catch (e) {
+      console.error('解析表单配置失败:', e);
+      parsedConf = {};
+    }
+  } else if (conf && typeof conf === 'object') {
+    parsedConf = conf;
+  }
+
+  // 解析字段
+  if (typeof fields === 'string') {
+    try {
+      parsedFields = JSON.parse(fields);
+    } catch (e) {
+      console.error('解析表单字段失败:', e);
+      parsedFields = [];
+    }
+  } else if (Array.isArray(fields)) {
+    parsedFields = fields.map((field) => {
+      if (typeof field === 'string') {
+        try {
+          return JSON.parse(field);
+        } catch (e) {
+          return field;
+        }
+      }
+      return field;
+    });
+  }
+
+  // 直接修改对象属性
+  refObject.rule = parsedFields;
+  refObject.option = parsedConf;
+}
+

+ 9 - 1
src/utils/permission.ts

@@ -218,7 +218,15 @@ export const mapMenuPathToKey = (path: string): string | null => {
     return 'isolationWork';
   }
   
- 
+  // 隔离作业相关 - jobTicket 路径
+  if (path === '/jobTicket' || path.startsWith('/jobTicket/')) {
+    if (path.includes('/form') || path.endsWith('/form')) return 'formManagement';
+    if (path.includes('/processDesign') || path.endsWith('/design')) return 'processDesign';
+    if (path.includes('/sop') || path.endsWith('/sop')) return 'sopManagement';
+    if (path.includes('/job') || path.endsWith('/job')) return 'workManagement';
+    if (path.includes('/step') || path.includes('/template')) return 'processTemplate';
+    return 'isolationWork';
+  }
   
   // 点位管理
   if (path === '/points' || path.startsWith('/points/')) {