瀏覽代碼

fix(route): merge xyh

Lind 3 年之前
父節點
當前提交
72b66bef30

+ 3 - 3
src/app.tsx

@@ -12,7 +12,7 @@ import type { RequestOptionsInit } from 'umi-request';
 import ReconnectingWebSocket from 'reconnecting-websocket';
 import SystemConst from '@/utils/const';
 import { service as MenuService } from '@/pages/system/Menu';
-import getRoutes, { getMenus, saveMenusCache } from '@/utils/menu';
+import getRoutes, { getMenus, handleRoutes, saveMenusCache } from '@/utils/menu';
 
 const isDev = process.env.NODE_ENV === 'development';
 const loginPath = '/user/login';
@@ -239,8 +239,8 @@ export function render(oldRender: any) {
   if (history.location.pathname !== loginPath) {
     MenuService.queryMenuThree({ paging: false }).then((res) => {
       if (res.status === 200) {
-        extraRoutes = res.result;
-        saveMenusCache(res.result);
+        extraRoutes = handleRoutes(res.result);
+        saveMenusCache(extraRoutes);
       }
       oldRender();
     });

+ 1 - 1
src/components/SearchComponent/GroupNameControl.tsx

@@ -12,7 +12,7 @@ const GroupNameControl = (props: Props) => {
   return (
     <>
       {index === 0 ? (
-        <div style={{ textAlign: 'center', fontWeight: 600 }}>{props?.name || '第一组'}</div>
+        <div style={{ fontWeight: 600 }}>{props?.name || '第一组'}</div>
       ) : (
         <Select
           onChange={props.onChange}

+ 59 - 43
src/components/SearchComponent/index.tsx

@@ -57,6 +57,7 @@ interface Props<T> {
   /** @name 固定查询参数*/
   defaultParam?: Term[];
   pattern?: 'simple' | 'advance';
+  enableSave?: boolean;
 }
 
 const termType = [
@@ -91,7 +92,15 @@ const SchemaField = createSchemaField({
 });
 
 const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
-  const { field, target, onReset, onSearch, defaultParam, pattern = 'advance' } = props;
+  const {
+    field,
+    target,
+    onReset,
+    onSearch,
+    defaultParam,
+    pattern = 'advance',
+    enableSave = true,
+  } = props;
   const intl = useIntl();
   const [expand, setExpand] = useState<boolean>(true);
   const initForm = server2Ui(
@@ -205,15 +214,15 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
       type: 'object',
       'x-component': 'FormGrid',
       'x-component-props': {
-        minColumns: 6,
-        maxColumns: 6,
+        minColumns: 14,
+        maxColumns: 14,
       },
       properties: {
         type: {
           'x-decorator': 'FormItem',
           'x-component': 'GroupNameControl',
           'x-decorator-props': {
-            gridSpan: 1,
+            gridSpan: 2,
           },
           'x-component-props': {
             name: name,
@@ -225,7 +234,7 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
           'x-decorator': 'FormItem',
           'x-component': 'Select',
           'x-decorator-props': {
-            gridSpan: 2,
+            gridSpan: 3,
           },
           'x-component-props': {
             placeholder: '请选择',
@@ -237,14 +246,14 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
           'x-decorator': 'FormItem',
           'x-component': 'Select',
           'x-decorator-props': {
-            gridSpan: 1,
+            gridSpan: 3,
           },
           default: 'like',
           enum: termType,
         },
         value: {
           'x-decorator-props': {
-            gridSpan: 2,
+            gridSpan: 6,
           },
           'x-decorator': 'FormItem',
           'x-component': 'Input',
@@ -420,43 +429,45 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
             >
               搜索
             </Dropdown.Button>
-            <Popover
-              content={
-                <Form style={{ width: '217px' }} form={historyForm}>
-                  <SchemaField
-                    schema={{
-                      type: 'object',
-                      properties: {
-                        alias: {
-                          'x-decorator': 'FormItem',
-                          'x-component': 'Input.TextArea',
-                          'x-validator': [
-                            {
-                              max: 50,
-                              message: '最多可输入50个字符',
-                            },
-                          ],
+            {enableSave && (
+              <Popover
+                content={
+                  <Form style={{ width: '217px' }} form={historyForm}>
+                    <SchemaField
+                      schema={{
+                        type: 'object',
+                        properties: {
+                          alias: {
+                            'x-decorator': 'FormItem',
+                            'x-component': 'Input.TextArea',
+                            'x-validator': [
+                              {
+                                max: 50,
+                                message: '最多可输入50个字符',
+                              },
+                            ],
+                          },
                         },
-                      },
-                    }}
-                  />
-                  <Button onClick={handleSaveLog} type="primary" className={styles.saveLog}>
-                    保存
-                  </Button>
-                </Form>
-              }
-              visible={aliasVisible}
-              onVisibleChange={setAliasVisible}
-              title="搜索名称"
-              trigger="click"
-            >
-              <Button block>
-                {intl.formatMessage({
-                  id: 'pages.data.option.save',
-                  defaultMessage: '保存',
-                })}
-              </Button>
-            </Popover>
+                      }}
+                    />
+                    <Button onClick={handleSaveLog} type="primary" className={styles.saveLog}>
+                      保存
+                    </Button>
+                  </Form>
+                }
+                visible={aliasVisible}
+                onVisibleChange={setAliasVisible}
+                title="搜索名称"
+                trigger="click"
+              >
+                <Button block>
+                  {intl.formatMessage({
+                    id: 'pages.data.option.save',
+                    defaultMessage: '保存',
+                  })}
+                </Button>
+              </Popover>
+            )}
             <Button block onClick={resetForm}>
               重置
             </Button>
@@ -483,6 +494,11 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
           <Button block onClick={resetForm}>
             重置
           </Button>
+          <DoubleRightOutlined
+            onClick={handleExpand}
+            style={{ fontSize: 20 }}
+            rotate={expand ? 90 : -90}
+          />
         </FormButtonGroup.FormItem>
       </div>
     );

+ 21 - 23
src/pages/device/Instance/Detail/Running/Event/index.tsx

@@ -1,16 +1,14 @@
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import { InstanceModel, service } from '@/pages/device/Instance';
+import { service } from '@/pages/device/Instance';
 import { useParams } from 'umi';
 import type { EventMetadata } from '@/pages/device/Product/typings';
 import SearchComponent from '@/components/SearchComponent';
 import moment from 'moment';
 import { Form, Modal } from 'antd';
 import { SearchOutlined } from '@ant-design/icons';
-import { useEffect, useRef, useState } from 'react';
+import { useRef, useState } from 'react';
 import MonacoEditor from 'react-monaco-editor';
-import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
-import { map } from 'rxjs/operators';
 
 interface Props {
   data: Partial<EventMetadata>;
@@ -21,8 +19,8 @@ const EventLog = (props: Props) => {
   const { data } = props;
   const actionRef = useRef<ActionType>();
   const [searchParams, setSearchParams] = useState<any>({ pageSize: 10 });
-  const device = InstanceModel.detail;
-  const [subscribeTopic] = useSendWebsocketMessage();
+  // const device = InstanceModel.detail;
+  // const [subscribeTopic] = useSendWebsocketMessage();
 
   const columns: ProColumns<MetadataLogData>[] = [
     {
@@ -70,24 +68,24 @@ const EventLog = (props: Props) => {
   /**
    * 订阅事件数据
    */
-  const subscribeEvent = () => {
-    const id = `instance-info-event-${device.id}-${device.productId}`;
-    const topic = `/dashboard/device/${device.productId}/events/realTime`;
-    subscribeTopic!(id, topic, {
-      deviceId: device.id,
-    })
-      ?.pipe(map((res) => res.payload))
-      .subscribe((payload: any) => {
-        const { value } = payload;
-        if (value) {
-          actionRef.current?.reload?.();
-        }
-      });
-  };
+  // const subscribeEvent = () => {
+  //   const id = `instance-info-event-${device.id}-${device.productId}`;
+  //   const topic = `/dashboard/device/${device.productId}/events/realTime`;
+  //   subscribeTopic!(id, topic, {
+  //     deviceId: device.id,
+  //   })
+  //     ?.pipe(map((res) => res.payload))
+  //     .subscribe((payload: any) => {
+  //       const { value } = payload;
+  //       if (value) {
+  //         actionRef.current?.reload?.();
+  //       }
+  //     });
+  // };
 
-  useEffect(() => {
-    subscribeEvent();
-  }, []);
+  // useEffect(() => {
+  //   subscribeEvent();
+  // }, []);
 
   const createColumn = (): ProColumns[] =>
     data.valueType?.type === 'object'

+ 6 - 2
src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx

@@ -7,6 +7,7 @@ import { service } from '@/pages/device/Instance';
 import { useParams } from 'umi';
 import PropertyLog from '@/pages/device/Instance/Detail/MetadataLog/Property';
 import EditProperty from '@/pages/device/Instance/Detail/Running/Property/EditProperty';
+import moment from 'moment';
 
 interface Props {
   data: Partial<PropertyMetadata>;
@@ -67,8 +68,11 @@ const Property = (props: Props) => {
       colSpan={{ xs: 12, sm: 8, md: 6, lg: 6, xl: 6 }}
     >
       <Spin spinning={loading}>
-        <div style={{ height: 60, fontWeight: 600, fontSize: '30px' }}>
-          {value?.formatValue || ''}
+        <div>
+          <div style={{ fontWeight: 600, fontSize: '30px' }}>{value?.formatValue || '--'}</div>
+          <div style={{ marginTop: 10 }}>
+            {value?.timestamp ? moment(value?.timestamp).format('YYYY-MM-DD HH:mm:ss') : '--'}
+          </div>
         </div>
       </Spin>
       <EditProperty

+ 1 - 1
src/pages/device/Instance/Detail/index.tsx

@@ -140,7 +140,7 @@ const InstanceDetail = observer(() => {
       history.goBack();
     } else {
       setTab('detail');
-      getDetail(InstanceModel.current?.id || params.id);
+      getDetail(params?.id || InstanceModel.current?.id || '');
     }
     return () => {
       MetadataAction.clean();

+ 25 - 0
src/pages/device/Product/Detail/Access/index.tsx

@@ -60,6 +60,18 @@ const Access = () => {
           {text}
         </Tooltip>
       ),
+      onCell: (record: any, index: number) => {
+        const list = (config?.routes || []).sort((a: any, b: any) => a - b) || [];
+        const arr = list.filter((res: any) => {
+          // 这里gpsNumber是我需要判断的字段名(相同就合并)
+          return res?.group == record?.group;
+        });
+        if (index == 0 || list[index - 1]?.group != record?.group) {
+          return { rowSpan: arr.length };
+        } else {
+          return { rowSpan: 0 };
+        }
+      },
     },
     {
       title: 'topic',
@@ -111,6 +123,18 @@ const Access = () => {
           {text}
         </Tooltip>
       ),
+      onCell: (record: any, index: number) => {
+        const list = (config?.routes || []).sort((a: any, b: any) => a - b) || [];
+        const arr = list.filter((res: any) => {
+          // 这里gpsNumber是我需要判断的字段名(相同就合并)
+          return res?.group == record?.group;
+        });
+        if (index == 0 || list[index - 1]?.group != record?.group) {
+          return { rowSpan: arr.length };
+        } else {
+          return { rowSpan: 0 };
+        }
+      },
     },
     {
       title: '地址',
@@ -242,6 +266,7 @@ const Access = () => {
             <div>
               <Table
                 dataSource={config?.routes || []}
+                bordered
                 columns={config.id === 'MQTT' ? columnsMQTT : columnsHTTP}
                 pagination={false}
                 scroll={{ y: 240 }}

+ 25 - 0
src/pages/link/AccessConfig/Detail/Access/index.tsx

@@ -166,6 +166,18 @@ const Access = (props: Props) => {
           {text}
         </Tooltip>
       ),
+      onCell: (record: any, index: number) => {
+        const list = (config?.routes || []).sort((a: any, b: any) => a - b) || [];
+        const arr = list.filter((res: any) => {
+          // 这里gpsNumber是我需要判断的字段名(相同就合并)
+          return res?.group == record?.group;
+        });
+        if (index == 0 || list[index - 1]?.group != record?.group) {
+          return { rowSpan: arr.length };
+        } else {
+          return { rowSpan: 0 };
+        }
+      },
     },
     {
       title: 'topic',
@@ -217,6 +229,18 @@ const Access = (props: Props) => {
           {text}
         </Tooltip>
       ),
+      onCell: (record: any, index: number) => {
+        const list = (config?.routes || []).sort((a: any, b: any) => a - b) || [];
+        const arr = list.filter((res: any) => {
+          // 这里gpsNumber是我需要判断的字段名(相同就合并)
+          return res?.group == record?.group;
+        });
+        if (index == 0 || list[index - 1]?.group != record?.group) {
+          return { rowSpan: arr.length };
+        } else {
+          return { rowSpan: 0 };
+        }
+      },
     },
     {
       title: '分组',
@@ -556,6 +580,7 @@ const Access = (props: Props) => {
               {config?.routes && config?.routes?.length > 0 && (
                 <div>
                   <Table
+                    bordered
                     dataSource={config?.routes || []}
                     columns={config.id === 'MQTT' ? columnsMQTT : columnsHTTP}
                     pagination={false}

+ 4 - 20
src/pages/link/Protocol/index.tsx

@@ -267,6 +267,7 @@ const Protocol = () => {
                   dependencies: ['..type'],
                   fulfill: {
                     state: {
+                      value: '',
                       visible: '{{["jar","local"].includes($deps[0])}}',
                       componentType: '{{$deps[0]==="jar"?"FileUpload":"Input"}}',
                       componentProps: '{{$deps[0]==="jar"?{type:"file", accept: ".jar, .zip"}:{}}}',
@@ -274,26 +275,6 @@ const Protocol = () => {
                   },
                 },
               },
-              // provider: {
-              //   title: '类名',
-              //   'x-component': 'Input',
-              //   'x-decorator': 'FormItem',
-              //   'x-visible': false,
-              //   'x-validator': [
-              //     {
-              //       required: true,
-              //       message: '请选择类名',
-              //     },
-              //   ],
-              //   'x-reactions': {
-              //     dependencies: ['..type'],
-              //     fulfill: {
-              //       state: {
-              //         visible: '{{["jar","local"].includes($deps[0])}}',
-              //       },
-              //     },
-              //   },
-              // },
             },
           },
           description: {
@@ -341,6 +322,9 @@ const Protocol = () => {
             form.setFieldState('id', (state) => {
               state.disabled = CurdModel.model === 'edit';
             });
+            form.setFieldState('type', (state) => {
+              state.disabled = CurdModel.model === 'edit';
+            });
           });
         }}
         footer={

+ 15 - 18
src/pages/system/Department/Assets/index.tsx

@@ -1,5 +1,4 @@
 // 部门-资产分配
-import { PageContainer } from '@ant-design/pro-layout';
 import { Tabs } from 'antd';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import ProductCategory from './productCategory';
@@ -32,23 +31,21 @@ const Assets = () => {
   const intl = useIntl();
 
   return (
-    <PageContainer>
-      <div style={{ background: '#fff', padding: 12 }}>
-        <Tabs tabPosition="left" defaultActiveKey="ProductCategory">
-          {TabsArray.map((item) => (
-            <Tabs.TabPane
-              tab={intl.formatMessage({
-                id: item.intlTitle,
-                defaultMessage: item.defaultMessage,
-              })}
-              key={item.key}
-            >
-              <item.components />
-            </Tabs.TabPane>
-          ))}
-        </Tabs>
-      </div>
-    </PageContainer>
+    <div style={{ background: '#fff', padding: 12 }}>
+      <Tabs tabPosition="left" defaultActiveKey="ProductCategory">
+        {TabsArray.map((item) => (
+          <Tabs.TabPane
+            tab={intl.formatMessage({
+              id: item.intlTitle,
+              defaultMessage: item.defaultMessage,
+            })}
+            key={item.key}
+          >
+            <item.components />
+          </Tabs.TabPane>
+        ))}
+      </Tabs>
+    </div>
   );
 };
 

+ 15 - 0
src/pages/system/Department/Detail/index.tsx

@@ -0,0 +1,15 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { useLocation } from 'umi';
+import Assets from '../Assets';
+import Member from '../Member';
+
+type LocationType = {
+  type: string;
+};
+
+export default () => {
+  const location = useLocation<LocationType>();
+  const params: any = new URLSearchParams(location.search);
+
+  return <PageContainer>{params.get('type') === 'assets' ? <Assets /> : <Member />}</PageContainer>;
+};

+ 2 - 3
src/pages/system/Department/Member/index.tsx

@@ -1,5 +1,4 @@
 // 部门-用户管理
-import { PageContainer } from '@ant-design/pro-layout';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
@@ -151,7 +150,7 @@ const Member = observer(() => {
   };
 
   return (
-    <PageContainer>
+    <>
       <Bind
         visible={MemberModel.bind}
         onCancel={closeModal}
@@ -216,7 +215,7 @@ const Member = observer(() => {
           </Popconfirm>,
         ]}
       />
-    </PageContainer>
+    </>
   );
 });
 

+ 55 - 4
src/pages/system/Department/index.tsx

@@ -2,6 +2,7 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
+import * as React from 'react';
 import { useEffect, useRef, useState } from 'react';
 import { Link, useIntl, useLocation } from 'umi';
 import { Button, message, Popconfirm, Tooltip } from 'antd';
@@ -20,6 +21,7 @@ import { observer } from '@formily/react';
 import { model } from '@formily/reactive';
 import Save from './save';
 import SearchComponent from '@/components/SearchComponent';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 
 export const service = new Service('organization');
 
@@ -38,7 +40,9 @@ export default observer(() => {
   const actionRef = useRef<ActionType>();
   const intl = useIntl();
   const [param, setParam] = useState({});
-
+  const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([]);
+  const [treeData, setTreeData] = useState<any[]>([]);
+  const rowKeys = useRef<React.Key[]>([]);
   /**
    * 根据部门ID删除数据
    * @param id
@@ -115,7 +119,13 @@ export default observer(() => {
             <PlusCircleOutlined />
           </Tooltip>
         </a>,
-        <Link key="assets" to={`/system/department/${record.id}/assets`}>
+        <Link
+          key="assets"
+          to={`${getMenuPathByParams(
+            MENUS_CODE['system/Department/Detail'],
+            record.id,
+          )}?type=assets`}
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.data.option.assets',
@@ -125,7 +135,10 @@ export default observer(() => {
             <MedicineBoxOutlined />
           </Tooltip>
         </Link>,
-        <Link key="user" to={`/system/department/${record.id}/user`}>
+        <Link
+          key="user"
+          to={`${getMenuPathByParams(MENUS_CODE['system/Department/Detail'], record.id)}?type=user`}
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.system.department.user',
@@ -163,6 +176,26 @@ export default observer(() => {
   const schema: ISchema = {
     type: 'object',
     properties: {
+      parentId: {
+        type: 'string',
+        title: '上级部门',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'TreeSelect',
+        'x-validator': [
+          {
+            required: true,
+            message: '请输入名称',
+          },
+        ],
+        'x-component-props': {
+          fieldNames: {
+            label: 'name',
+            value: 'id',
+          },
+        },
+        enum: treeData,
+      },
       name: {
         type: 'string',
         title: intl.formatMessage({
@@ -189,10 +222,15 @@ export default observer(() => {
           id: 'pages.device.instanceDetail.detail.sort',
           defaultMessage: '排序',
         }),
+        required: true,
         'x-decorator': 'FormItem',
         'x-component': 'NumberPicker',
         'x-validator': [
           {
+            required: true,
+            message: '请输入排序',
+          },
+          {
             pattern: /^[0-9]*[1-9][0-9]*$/,
             message: '请输入大于0的整数',
           },
@@ -236,6 +274,7 @@ export default observer(() => {
             ...params,
             sorts: [{ name: 'createTime', order: 'desc' }],
           });
+          setTreeData(response.result);
           return {
             code: response.message,
             result: {
@@ -248,6 +287,13 @@ export default observer(() => {
           };
         }}
         rowKey="id"
+        expandable={{
+          expandedRowKeys: [...rowKeys.current],
+          onExpandedRowsChange: (keys) => {
+            rowKeys.current = keys as React.Key[];
+            setExpandedRowKeys(keys as React.Key[]);
+          },
+        }}
         pagination={false}
         search={false}
         params={param}
@@ -279,7 +325,12 @@ export default observer(() => {
             : undefined
         }
         service={service}
-        onCancel={(type) => {
+        onCancel={(type, pId) => {
+          if (pId) {
+            expandedRowKeys.push(pId);
+            rowKeys.current.push(pId);
+            setExpandedRowKeys(expandedRowKeys);
+          }
           if (type) {
             actionRef.current?.reload();
           }

+ 9 - 6
src/pages/system/Department/save.tsx

@@ -4,6 +4,7 @@ import { createForm } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import {
   ArrayTable,
+  Checkbox,
   Editable,
   Form,
   FormGrid,
@@ -14,8 +15,8 @@ import {
   Password,
   Select,
   Switch,
+  TreeSelect,
   Upload,
-  Checkbox,
 } from '@formily/antd';
 import { message, Modal } from 'antd';
 import { useIntl } from '@@/plugin-locale/localeExports';
@@ -32,7 +33,7 @@ export interface SaveModalProps<T> extends Omit<ModalProps, 'onOk' | 'onCancel'>
    * Model关闭事件
    * @param type 是否为请求接口后关闭,用于外部table刷新数据
    */
-  onCancel?: (type: boolean) => void;
+  onCancel?: (type: boolean, id?: React.Key) => void;
   schema: ISchema;
 }
 
@@ -55,6 +56,7 @@ const Save = <T extends object>(props: SaveModalProps<T>) => {
       NumberPicker,
       FUpload,
       Checkbox,
+      TreeSelect,
     },
     scope: {
       icon(name: any) {
@@ -71,10 +73,11 @@ const Save = <T extends object>(props: SaveModalProps<T>) => {
   /**
    * 关闭Modal
    * @param type 是否需要刷新外部table数据
+   * @param id 传递上级部门id,用于table展开父节点
    */
-  const modalClose = (type: boolean) => {
+  const modalClose = (type: boolean, id?: string) => {
     if (typeof onCancel === 'function') {
-      onCancel(type);
+      onCancel(type, id);
     }
   };
 
@@ -89,7 +92,7 @@ const Save = <T extends object>(props: SaveModalProps<T>) => {
 
     if (response.status === 200) {
       message.success('操作成功!');
-      modalClose(true);
+      modalClose(true, response.result.parentId);
       if ((window as any).onTabSaveSuccess) {
         (window as any).onTabSaveSuccess(response.result);
         setTimeout(() => window.close(), 300);
@@ -116,7 +119,7 @@ const Save = <T extends object>(props: SaveModalProps<T>) => {
         modalClose(false);
       }}
     >
-      <Form form={form} labelCol={5} wrapperCol={16}>
+      <Form form={form} layout={'vertical'}>
         <SchemaField schema={schema} />
       </Form>
     </Modal>

+ 29 - 3
src/pages/system/Menu/Detail/edit.tsx

@@ -17,15 +17,17 @@ import Permission from '@/pages/system/Menu/components/permission';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { useEffect, useState } from 'react';
 import { service } from '@/pages/system/Menu';
-import { useRequest } from 'umi';
+import { useHistory, useRequest } from 'umi';
 import type { MenuItem } from '@/pages/system/Menu/typing';
 // import { debounce } from 'lodash';
 import Title from '../components/Title';
 import { UploadImage } from '@/components';
 import { QuestionCircleFilled } from '@ant-design/icons';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 
 type EditProps = {
   data: MenuItem;
+  basePath?: string;
   onLoad: (id: string) => void;
 };
 
@@ -34,6 +36,7 @@ export default (props: EditProps) => {
   const [disabled, setDisabled] = useState(true);
   const [show, setShow] = useState(true);
   const [accessSupport, setAccessSupport] = useState('unsupported');
+  const history = useHistory();
 
   const [form] = Form.useForm();
 
@@ -52,20 +55,43 @@ export default (props: EditProps) => {
     formatResult: (response) => response.result,
   });
 
+  /**
+   * 跳转详情页
+   * @param id
+   */
+  const pageJump = (id?: string) => {
+    // 跳转详情
+    history.push(`${getMenuPathByParams(MENUS_CODE['system/Menu/Detail'], id)}`);
+  };
+
   const saveData = async () => {
     const formData = await form.validateFields();
     if (formData) {
-      const response: any = await service.update(formData);
+      const response: any = !props.data.id
+        ? await service.save(formData)
+        : await service.update(formData);
       if (response.status === 200) {
         message.success('操作成功!');
         setDisabled(true);
-        props.onLoad(response.result.id);
+        // 新增后刷新页面,编辑则不需要
+        if (!props.data.id) {
+          pageJump(response.result.id);
+        }
       } else {
         message.error('操作失败!');
       }
     }
   };
 
+  useEffect(() => {
+    console.log(props);
+    if (form && props.basePath) {
+      form.setFieldsValue({
+        url: props.basePath,
+      });
+    }
+  }, []);
+
   // const filterThree = (e: any) => {
   //   const _data: any = {
   //     paging: false,

+ 6 - 4
src/pages/system/Menu/Detail/index.tsx

@@ -4,7 +4,7 @@ import { useIntl } from '@@/plugin-locale/localeExports';
 import { useEffect, useState } from 'react';
 import BaseDetail from './edit';
 import Buttons from './buttons';
-import { useLocation, useRequest } from 'umi';
+import { useLocation, useParams, useRequest } from 'umi';
 import { service } from '@/pages/system/Menu';
 
 type LocationType = {
@@ -16,6 +16,8 @@ export default () => {
   const [tabKey, setTabKey] = useState('detail');
   const [pId, setPid] = useState<string | null>(null);
   const location = useLocation<LocationType>();
+  const params: any = new URLSearchParams(location.search);
+  const param = useParams<{ id?: string }>();
 
   const { data, run: queryData } = useRequest(service.queryDetail, {
     manual: true,
@@ -28,10 +30,9 @@ export default () => {
    * 获取当前菜单详情
    */
   const queryDetail = (editId?: string) => {
-    const params = new URLSearchParams(location.search);
-    const id = editId || params.get('id');
+    const id = editId || param.id;
     const _pId = params.get('pId');
-    if (id) {
+    if (id && id !== ':id') {
       queryData(id);
     }
     if (_pId) {
@@ -73,6 +74,7 @@ export default () => {
               ...data,
               parentId: pId,
             }}
+            basePath={params.get('basePath')}
             onLoad={queryDetail}
           />
         </div>

+ 7 - 4
src/pages/system/Menu/index.tsx

@@ -18,7 +18,7 @@ import SearchComponent from '@/components/SearchComponent';
 import Service from './service';
 import type { MenuItem } from './typing';
 import moment from 'moment';
-import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 
 export const service = new Service('menu');
 
@@ -59,11 +59,14 @@ export default observer(() => {
    * 跳转详情页
    * @param id
    * @param pId
+   * @param basePath
    */
-  const pageJump = (id?: string, pId?: string) => {
+  const pageJump = (id?: string, pId?: string, basePath?: string) => {
     // 跳转详情
     history.push(
-      `${getMenuPathByCode(MENUS_CODE['system/Menu/Detail'])}?id=${id || ''}&pId=${pId || ''}`,
+      `${getMenuPathByParams(MENUS_CODE['system/Menu/Detail'], id)}?pId=${pId || ''}&basePath=${
+        basePath || ''
+      }`,
     );
   };
 
@@ -148,7 +151,7 @@ export default observer(() => {
               parentId: record.id,
             };
             // State.visible = true;
-            pageJump('', record.id);
+            pageJump('', record.id, record.url);
           }}
         >
           <Tooltip

+ 8 - 5
src/pages/system/Permission/Save/index.tsx

@@ -4,7 +4,7 @@ import { createForm, onFormSubmitStart } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import React, { useEffect, useState } from 'react';
 import * as ICONS from '@ant-design/icons';
-import { ArrayTable, Form, FormItem, Input, Editable } from '@formily/antd';
+import { ArrayTable, Editable, Form, FormItem, Input } from '@formily/antd';
 import type { ISchema } from '@formily/json-schema';
 import type { PermissionItem } from '@/pages/system/Permission/typings';
 import { service } from '@/pages/system/Permission';
@@ -225,8 +225,13 @@ const Save = (props: Props) => {
 
   const save = async () => {
     const value = await form.submit<UserItem>();
-    const response = await service.update(value);
-    if (response.status === 200) {
+    let response = undefined;
+    if (props.model === 'add') {
+      response = await service.save(value);
+    } else {
+      response = await service.update(value);
+    }
+    if (response && response.status === 200) {
       message.success(
         intl.formatMessage({
           id: 'pages.data.option.success',
@@ -234,8 +239,6 @@ const Save = (props: Props) => {
         }),
       );
       props.close();
-    } else {
-      message.error('操作失败!');
     }
   };
 

+ 1 - 0
src/pages/system/User/index.tsx

@@ -176,6 +176,7 @@ const User = observer(() => {
       <SearchComponent<UserItem>
         field={columns}
         target="user"
+        pattern="simple"
         onSearch={(data) => {
           // 重置分页数据
           actionRef.current?.reset?.();

+ 164 - 0
src/utils/menu/index.ts

@@ -0,0 +1,164 @@
+// 路由components映射
+import type { IRouteProps } from 'umi';
+import type { MenuItem } from '@/pages/system/Menu/typing';
+import { getDetailNameByCode, MENUS_CODE } from './router';
+
+/** localStorage key */
+export const MENUS_DATA_CACHE = 'MENUS_DATA_CACHE';
+
+const DetailCode = 'detail';
+
+/**
+ * 根据url获取映射的组件
+ * @param files
+ */
+const findComponents = (files: __WebpackModuleApi.RequireContext) => {
+  const modules = {};
+  files.keys().forEach((key) => {
+    // 删除路径开头的./ 以及结尾的 /index;
+    const str = key.replace(/(\.\/|\.tsx)/g, '').replace('/index', '');
+    modules[str] = files(key).default;
+  });
+  return modules;
+};
+
+/**
+ * 扁平化路由树
+ * @param routes
+ */
+export const flatRoute = (routes: MenuItem[]): MenuItem[] => {
+  return routes.reduce<MenuItem[]>((pValue, currValue) => {
+    const menu: MenuItem[] = [];
+    const { children, ...extraRoute } = currValue;
+    menu.push(extraRoute);
+    return [...pValue, ...menu, ...flatRoute(children || [])];
+  }, []);
+};
+
+/**
+ * 获取菜单组件
+ * @param baseCode
+ */
+const findDetailRoute = (baseCode: string, url: string): MenuItem | undefined => {
+  if (baseCode) {
+    const allComponents = findComponents(require.context('@/pages', true, /index(\.tsx)$/));
+    const code = `${baseCode}/Detail`;
+    const path = `${url}/${DetailCode}/:id`;
+    const component = allComponents[code];
+    return component
+      ? ({ path: path, url: path, name: getDetailNameByCode[code], code } as MenuItem)
+      : undefined;
+  }
+  return undefined;
+};
+
+export const handleRoutes = (routes?: MenuItem[], level = 1): MenuItem[] => {
+  return routes
+    ? routes.map((item) => {
+        const detailComponent = findDetailRoute(item.code, item.url);
+        if (detailComponent) {
+          item.children = item.children ? [...item.children, detailComponent] : [detailComponent];
+        }
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
+        if (item.children) {
+          item.children = handleRoutes(item.children, level + 1);
+        }
+        item.level = level;
+        return item;
+      })
+    : [];
+};
+
+/**
+ * 处理为正确的路由格式
+ * @param extraRoutes 后端菜单数据
+ * @param level 路由层级
+ */
+const getRoutes = (extraRoutes: MenuItem[], level = 1): IRouteProps[] => {
+  const allComponents = findComponents(require.context('@/pages', true, /index(\.tsx)$/));
+  return extraRoutes.map((route) => {
+    const component = allComponents[route.code] || null;
+    const _route: IRouteProps = {
+      key: route.url,
+      name: route.name,
+      path: route.url,
+    };
+
+    if (route.children && route.children.length) {
+      const flatRoutes = getRoutes(flatRoute(route.children || []), level + 1);
+      const redirect = flatRoutes.filter((r) => r.component)[0]?.path;
+      _route.children = redirect
+        ? [
+            ...flatRoutes,
+            {
+              path: _route.path,
+              redirect: redirect,
+            },
+          ]
+        : flatRoutes;
+    } else if (component) {
+      _route.component = component;
+    }
+
+    if (level !== 1) {
+      _route.exact = true;
+    }
+
+    return _route;
+  });
+};
+
+export const getMenus = (extraRoutes: IRouteProps[]): any[] => {
+  return extraRoutes.map((route) => {
+    const children = route.children && route.children.length ? route.children : [];
+
+    return {
+      key: route.url,
+      name: route.name,
+      path: route.url,
+      icon: route.icon,
+      hideChildrenInMenu: children && children.some((item: any) => item.url.includes(DetailCode)),
+      exact: route.level !== 1,
+      children: getMenus(children),
+    };
+  });
+};
+
+/** 缓存路由数据,格式为 [{ code: url }] */
+export const saveMenusCache = (routes: MenuItem[]) => {
+  const list: MenuItem[] = flatRoute(routes);
+  const listObject = {};
+  list.forEach((route) => {
+    listObject[route.code] = route.url;
+  });
+  try {
+    localStorage.setItem(MENUS_DATA_CACHE, JSON.stringify(listObject));
+  } catch (e) {
+    console.log(e);
+  }
+};
+
+/**
+ * 通过缓存的数据取出相应的路由url
+ * @param code
+ */
+export const getMenuPathByCode = (code: string): string => {
+  const menusStr = localStorage.getItem(MENUS_DATA_CACHE) || '{}';
+  const menusData = JSON.parse(menusStr);
+  return menusData[code];
+};
+
+/**
+ * 通过缓存的数据取出相应的路由url
+ * @param code 路由Code
+ * @param id 路由携带参数
+ * @param regStr 路由参数code
+ */
+export const getMenuPathByParams = (code: string, id?: string, regStr: string = ':id') => {
+  const menusData = getMenuPathByCode(code);
+  return id ? menusData.replace(regStr, id) : menusData;
+};
+
+export default getRoutes;
+
+export { MENUS_CODE };

+ 29 - 147
src/utils/menu.ts

@@ -1,10 +1,3 @@
-// 路由components映射
-import type { IRouteProps } from 'umi';
-import type { MenuItem } from '@/pages/system/Menu/typing';
-
-/** localStorage key */
-export const MENUS_DATA_CACHE = 'MENUS_DATA_CACHE';
-
 /** 路由Code */
 export const MENUS_CODE = {
   'Analysis/CPU': 'Analysis/CPU',
@@ -22,27 +15,10 @@ export const MENUS_CODE = {
   'device/Category': 'device/Category',
   'device/Command': 'device/Command',
   'device/DataSource': 'device/DataSource',
-  'device/Firmware/Detail/History': 'device/Firmware/Detail/History',
-  'device/Firmware/Detail/Task/Detail': 'device/Firmware/Detail/Task/Detail',
-  'device/Firmware/Detail/Task/Release': 'device/Firmware/Detail/Task/Release',
-  'device/Firmware/Detail/Task/Save': 'device/Firmware/Detail/Task/Save',
-  'device/Firmware/Detail/Task': 'device/Firmware/Detail/Task',
-  'device/Firmware/Detail': 'device/Firmware/Detail',
   'device/Firmware/Save': 'device/Firmware/Save',
   'device/Firmware': 'device/Firmware',
-  'device/Instance/Detail/Config/Tags': 'device/Instance/Detail/Config/Tags',
-  'device/Instance/Detail/Config': 'device/Instance/Detail/Config',
-  'device/Instance/Detail/Functions': 'device/Instance/Detail/Functions',
-  'device/Instance/Detail/Info': 'device/Instance/Detail/Info',
-  'device/Instance/Detail/Log': 'device/Instance/Detail/Log',
-  'device/Instance/Detail/MetadataLog/Event': 'device/Instance/Detail/MetadataLog/Event',
-  'device/Instance/Detail/MetadataLog/Property': 'device/Instance/Detail/MetadataLog/Property',
-  'device/Instance/Detail/Running': 'device/Instance/Detail/Running',
-  'device/Instance/Detail': 'device/Instance/Detail',
   'device/Instance': 'device/Instance',
   'device/Location': 'device/Location',
-  'device/Product/Detail/BaseInfo': 'device/Product/Detail/BaseInfo',
-  'device/Product/Detail': 'device/Product/Detail',
   'device/Product/Save': 'device/Product/Save',
   'device/Product': 'device/Product',
   'device/components/Alarm/Edit': 'device/components/Alarm/Edit',
@@ -64,7 +40,6 @@ export const MENUS_CODE = {
   'link/Type': 'link/Type',
   'link/Type/Save': 'link/Type/Save',
   'link/AccessConfig': 'link/AccessConfig',
-  'link/AccessConfig/Detail': 'link/AccessConfig/Detail',
   'log/Access': 'log/Access',
   'log/System': 'log/System',
   'media/Cascade': 'media/Cascade',
@@ -81,7 +56,6 @@ export const MENUS_CODE = {
   'system/Department/Assets': 'system/Department/Assets',
   'system/Department/Member': 'system/Department/Member',
   'system/Department': 'system/Department',
-  'system/Menu/Detail': 'system/Menu/Detail',
   'system/Menu': 'system/Menu',
   'system/OpenAPI': 'system/OpenAPI',
   'system/Permission': 'system/Permission',
@@ -98,127 +72,35 @@ export const MENUS_CODE = {
   'visualization/Category': 'visualization/Category',
   'visualization/Configuration': 'visualization/Configuration',
   'visualization/Screen': 'visualization/Screen',
+  'device/Firmware/Detail/History': 'device/Firmware/Detail/History',
+  'device/Firmware/Detail/Task/Detail': 'device/Firmware/Detail/Task/Detail',
+  'device/Firmware/Detail/Task/Release': 'device/Firmware/Detail/Task/Release',
+  'device/Firmware/Detail/Task/Save': 'device/Firmware/Detail/Task/Save',
+  'device/Firmware/Detail/Task': 'device/Firmware/Detail/Task',
+  'device/Firmware/Detail': 'device/Firmware/Detail',
+  'device/Instance/Detail/Config/Tags': 'device/Instance/Detail/Config/Tags',
+  'device/Instance/Detail/Config': 'device/Instance/Detail/Config',
+  'device/Instance/Detail/Functions': 'device/Instance/Detail/Functions',
+  'device/Instance/Detail/Info': 'device/Instance/Detail/Info',
+  'device/Instance/Detail/Log': 'device/Instance/Detail/Log',
+  'device/Instance/Detail/MetadataLog/Event': 'device/Instance/Detail/MetadataLog/Event',
+  'device/Instance/Detail/MetadataLog/Property': 'device/Instance/Detail/MetadataLog/Property',
+  'device/Instance/Detail/Running': 'device/Instance/Detail/Running',
+  'device/Instance/Detail': 'device/Instance/Detail',
+  'device/Product/Detail/BaseInfo': 'device/Product/Detail/BaseInfo',
+  'device/Product/Detail': 'device/Product/Detail',
+  'link/AccessConfig/Detail': 'link/AccessConfig/Detail',
+  'system/Menu/Detail': 'system/Menu/Detail',
+  'system/Department/Detail': 'system/Department/Detail',
+  '/link/Type/Detail': '/link/Type/Detail',
 };
 
-/**
- * 根据url获取映射的组件
- * @param files
- */
-const findComponents = (files: __WebpackModuleApi.RequireContext) => {
-  const modules = {};
-  files.keys().forEach((key) => {
-    // 删除路径开头的./ 以及结尾的 /index;
-    const str = key.replace(/(\.\/|\.tsx)/g, '').replace('/index', '');
-    modules[str] = files(key).default;
-  });
-  return modules;
-};
-
-/**
- * 扁平化路由树
- * @param routes
- */
-export const flatRoute = (routes: MenuItem[]): MenuItem[] => {
-  return routes.reduce<MenuItem[]>((pValue, currValue) => {
-    const menu: MenuItem[] = [];
-    const { children, ...extraRoute } = currValue;
-    menu.push(extraRoute);
-    return [...pValue, ...menu, ...flatRoute(children || [])];
-  }, []);
-};
-
-/**
- * 处理为正确的路由格式
- * @param extraRoutes 后端菜单数据
- * @param level 路由层级
- */
-
-const allComponents = findComponents(require.context('@/pages', true, /index(\.tsx)$/));
-const getRoutes = (extraRoutes: MenuItem[], level = 1): IRouteProps[] => {
-  return extraRoutes.map((route) => {
-    const component = allComponents[route.code] || null;
-    const _route: IRouteProps = {
-      key: route.url,
-      name: route.name,
-      path: route.url,
-    };
-
-    if (route.children && route.children.length) {
-      const flatRoutes = getRoutes(flatRoute(route.children || []), level + 1);
-      const redirect = flatRoutes.filter((r) => r.component)[0]?.path;
-
-      _route.children = redirect
-        ? [
-            ...flatRoutes,
-            {
-              path: _route.path,
-              redirect: redirect,
-            },
-          ]
-        : flatRoutes;
-    }
-
-    if (component) {
-      _route.component = component;
-    }
-
-    if (level !== 1) {
-      _route.exact = true;
-    }
-
-    return _route;
-  });
-};
-
-export const getMenus = (extraRoutes: IRouteProps[], level: number = 1): any[] => {
-  return extraRoutes.map((route) => {
-    const children =
-      route.children && route.children.length ? getMenus(route.children, level + 1) : [];
-    const _route = {
-      key: route.url,
-      name: route.name,
-      path: route.url,
-      icon: route.icon,
-      exact: level !== 1 ? true : false,
-      children: children,
-    };
-    return _route;
-  });
-};
-
-/** 缓存路由数据,格式为 [{ code: url }] */
-export const saveMenusCache = (routes: MenuItem[]) => {
-  const list: MenuItem[] = flatRoute(routes);
-  const listObject = {};
-  list.forEach((route) => {
-    listObject[route.code] = route.url;
-  });
-  try {
-    localStorage.setItem(MENUS_DATA_CACHE, JSON.stringify(listObject));
-  } catch (e) {
-    console.log(e);
-  }
-};
-
-/**
- * 通过缓存的数据取出相应的路由url
- * @param code
- */
-export const getMenuPathByCode = (code: string): string => {
-  const menusStr = localStorage.getItem(MENUS_DATA_CACHE) || '{}';
-  const menusData = JSON.parse(menusStr);
-  return menusData[code];
-};
-
-/**
- * 通过缓存的数据取出相应的路由url
- * @param code 路由Code
- * @param id 路由携带参数
- * @param regStr 路由参数code
- */
-export const getMenuPathByParams = (code: string, id?: string, regStr: string = ':id') => {
-  const menusData = getMenuPathByCode(code);
-  return id ? menusData.replace(regStr, id) : menusData;
+export const getDetailNameByCode = {
+  'system/Menu/Detail': '菜单详情',
+  'device/Product/Detail': '产品详情',
+  'device/Instance/Detail': '设备详情',
+  'device/Firmware/Detail': '固件详情',
+  'system/Department/Detail': '部门详情',
+  'link/Type/Detail': '网络组件详情',
+  'link/AccessConfig/Detail': '配置详情',
 };
-
-export default getRoutes;