Przeglądaj źródła

feat(场景联动): 添加场景联动列表以及卡片,新增页面;

xieyonghong 3 lat temu
rodzic
commit
287b367895

+ 1 - 1
src/components/CheckButton/index.less

@@ -1,4 +1,4 @@
-@import '../../../node_modules/antd/lib/style/themes/variable';
+@import '~antd/es/style/themes/default.less';
 
 .box {
   display: flex;

+ 49 - 0
src/components/ProTableCard/CardItems/scene.tsx

@@ -0,0 +1,49 @@
+import React from 'react';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { TableCard } from '@/components';
+import '@/style/common.less';
+import '../index.less';
+import type { SceneItem } from '@/pages/rule-engine/Scene/typings';
+
+export interface DeviceCardProps extends SceneItem {
+  tools: React.ReactNode[];
+}
+
+const defaultImage = require('/public/images/scene.png');
+
+export default (props: DeviceCardProps) => {
+  return (
+    <TableCard
+      showMask={false}
+      actions={props.tools}
+      status={props.state.value}
+      statusText={props.state.text}
+      statusNames={{
+        online: StatusColorEnum.processing,
+        offline: StatusColorEnum.error,
+        notActive: StatusColorEnum.warning,
+      }}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img width={88} height={88} src={defaultImage} alt={''} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+          </div>
+          <div className={'card-item-content'}>
+            <div>
+              <label>触发方式</label>
+              <div className={'ellipsis'}>{'test'}</div>
+            </div>
+            <div>
+              <label>说明</label>
+              <div className={'ellipsis'}>{props.describe || '--'}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 157 - 0
src/pages/rule-engine/Scene/Save/action/action.tsx

@@ -0,0 +1,157 @@
+import { Button, InputNumber, Select } from 'antd';
+import { useEffect, useState } from 'react';
+import { useRequest } from 'umi';
+import { queryMessageType, queryMessageConfig, queryMessageTemplate } from './service';
+import MessageContent from './messageContent';
+
+interface ActionProps {
+  title?: string;
+  onRemove: () => void;
+}
+
+export default (props: ActionProps) => {
+  const [type1, setType1] = useState('message');
+  const [configValue, setConfigValue] = useState<string | undefined>(undefined);
+  const [templateValue, setTemplateValue] = useState<string | undefined>(undefined);
+  const [templateData, setTemplateData] = useState<any>(undefined);
+
+  const { data: messageType, run: queryMessageTypes } = useRequest(queryMessageType, {
+    manual: true,
+    formatResult: (res) => res.result,
+  });
+
+  const {
+    data: messageConfig,
+    run: queryMessageConfigs,
+    loading: messageConfigLoading,
+  } = useRequest(queryMessageConfig, {
+    manual: true,
+    formatResult: (res) => res.result,
+  });
+
+  const {
+    data: messageTemplate,
+    run: queryMessageTemplates,
+    loading: messageTemplateLoading,
+  } = useRequest(queryMessageTemplate, {
+    manual: true,
+    formatResult: (res) => res.result,
+  });
+
+  const MessageNodes = (
+    <>
+      <Select
+        options={messageType}
+        fieldNames={{ value: 'id', label: 'name' }}
+        placeholder={'请选择通知方式'}
+        style={{ width: 140 }}
+        onSelect={async (key: string) => {
+          setConfigValue(undefined);
+          setTemplateValue(undefined);
+          setTemplateData(undefined);
+          await queryMessageConfigs({
+            terms: [{ column: 'type$IN', value: key }],
+          });
+        }}
+      />
+      <Select
+        value={configValue}
+        options={messageConfig}
+        loading={messageConfigLoading}
+        fieldNames={{ value: 'id', label: 'name' }}
+        onSelect={async (key: string) => {
+          setConfigValue(key);
+          setTemplateValue(undefined);
+          setTemplateData(undefined);
+          await queryMessageTemplates({
+            terms: [{ column: 'configId', value: key }],
+          });
+        }}
+        style={{ width: 160 }}
+        placeholder={'请选择通知配置'}
+      />
+      <Select
+        value={templateValue}
+        options={messageTemplate}
+        loading={messageTemplateLoading}
+        fieldNames={{ value: 'id', label: 'name' }}
+        style={{ width: 160 }}
+        placeholder={'请选择通知模板'}
+        onSelect={async (key: string, nodeData: any) => {
+          setTemplateData(nodeData);
+          setTemplateValue(key);
+        }}
+      />
+    </>
+  );
+
+  const DeviceNodes = (
+    <>
+      <Select options={[]} placeholder={'请选择产品'} style={{ width: 220 }} />
+      <Select
+        defaultValue={'1'}
+        options={[
+          { label: '固定设备', value: '1' },
+          { label: '按标签', value: '2' },
+          { label: '按关系', value: '3' },
+        ]}
+        style={{ width: 120 }}
+      />
+      <Select options={[]} placeholder={'请选择'} style={{ width: 180 }} />
+      <Select
+        defaultValue={'1'}
+        options={[
+          { label: '设置属性', value: '1' },
+          { label: '功能调用', value: '2' },
+          { label: '读取属性', value: '3' },
+        ]}
+        style={{ width: 120 }}
+      />
+    </>
+  );
+
+  useEffect(() => {
+    if (type1 === 'message') {
+      queryMessageTypes();
+    }
+  }, [type1]);
+
+  const TimeTypeAfter = (
+    <Select
+      defaultValue={'second'}
+      options={[
+        { label: '秒', value: 'second' },
+        { label: '分', value: 'minute' },
+        { label: '小时', value: 'hour' },
+      ]}
+    />
+  );
+
+  return (
+    <div className={'actions-item'}>
+      <div className={'actions-item-title'}>
+        执行动作 {props.title} <Button onClick={props.onRemove}>删除</Button>
+      </div>
+      <div style={{ display: 'flex', gap: 12 }}>
+        <Select
+          options={[
+            { label: '消息通知', value: 'message' },
+            { label: '设备输出', value: 'device' },
+            { label: '延迟执行', value: 'delay' },
+          ]}
+          value={type1}
+          onSelect={(key: string) => {
+            setType1(key);
+          }}
+          style={{ width: 100 }}
+        />
+        {type1 === 'message' && MessageNodes}
+        {type1 === 'device' && DeviceNodes}
+        {type1 === 'delay' && (
+          <InputNumber addonAfter={TimeTypeAfter} style={{ width: 150 }} min={0} max={9999} />
+        )}
+      </div>
+      {type1 === 'message' && <MessageContent {...props} template={templateData} />}
+    </div>
+  );
+};

+ 18 - 0
src/pages/rule-engine/Scene/Save/action/index.less

@@ -0,0 +1,18 @@
+.actions-items {
+  padding: 26px 24px 12px 24px;
+  background-color: #fafafa;
+
+  .actions-item {
+    .actions-item-title {
+      padding-bottom: 18px;
+    }
+
+    &:not(:first-child) {
+      margin-top: 18px;
+    }
+
+    .template-variable {
+      margin-top: 12px;
+    }
+  }
+}

+ 26 - 0
src/pages/rule-engine/Scene/Save/action/index.tsx

@@ -0,0 +1,26 @@
+import ActionItem from './action';
+import './index.less';
+import { ProFormList } from '@ant-design/pro-form';
+
+export default () => {
+  return (
+    <div className={'actions-items'}>
+      <ProFormList
+        name={'actions'}
+        creatorButtonProps={{
+          creatorButtonText: '新增',
+        }}
+      >
+        {(meta, index, action) => {
+          return (
+            <ActionItem
+              onRemove={() => {
+                action.remove?.(index);
+              }}
+            />
+          );
+        }}
+      </ProFormList>
+    </div>
+  );
+};

+ 76 - 0
src/pages/rule-engine/Scene/Save/action/messageContent.tsx

@@ -0,0 +1,76 @@
+import { Col, Form, Row, Select } from 'antd';
+import { ItemGroup } from '@/pages/rule-engine/Scene/Save/components';
+import { ProFormText, ProFormSelect, ProFormDatePicker } from '@ant-design/pro-form';
+
+interface MessageContentProps {
+  type?: string;
+  template?: any;
+}
+
+const rowGutter = 12;
+
+export default (props: MessageContentProps) => {
+  const inputNodeByType = (data: any) => {
+    switch (data.type) {
+      case 'enum':
+        return (
+          <ProFormSelect
+            name={['variables', data.id]}
+            placeholder={`请选择${name}`}
+            style={{ width: '100%' }}
+          />
+        );
+      case 'timmer':
+        return (
+          <ProFormDatePicker
+            name={['variables', data.id]}
+            placeholder={'请选择时间'}
+            style={{ width: '100%' }}
+          />
+        );
+      case 'number':
+        return (
+          <ProFormText
+            name={['variables', data.id]}
+            placeholder={`请输入${name}`}
+            style={{ width: '100%' }}
+          />
+        );
+      default:
+        return <ProFormText name={['variables', data.id]} placeholder={`请输入${name}`} />;
+    }
+  };
+
+  return (
+    <>
+      {props.template && (
+        <div className={'template-variable'}>
+          {props.template.variableDefinitions ? (
+            <Row gutter={rowGutter}>
+              {props.template.variableDefinitions.map((item: any) => {
+                // const rules = !item.required ? [{ required: true, message: '请输入'+ item.name }] : undefined
+                return (
+                  <Col span={12} key={item.id}>
+                    <Form.Item label={item.name} required={!item.required}>
+                      <ItemGroup>
+                        <Select
+                          defaultValue={'1'}
+                          options={[
+                            { label: '手动输入', value: '1' },
+                            { label: '内置参数', value: '2' },
+                          ]}
+                          style={{ width: 120 }}
+                        />
+                        {inputNodeByType(item)}
+                      </ItemGroup>
+                    </Form.Item>
+                  </Col>
+                );
+              })}
+            </Row>
+          ) : null}
+        </div>
+      )}
+    </>
+  );
+};

+ 19 - 0
src/pages/rule-engine/Scene/Save/action/service.ts

@@ -0,0 +1,19 @@
+import { request } from '@@/plugin-request/request';
+import SystemConst from '@/utils/const';
+
+export const queryMessageType = () =>
+  request(`${SystemConst.API_BASE}/notifier/config/types`, { method: 'GET' });
+
+// 通知配置
+export const queryMessageConfig = (data: any) =>
+  request(`${SystemConst.API_BASE}/notifier/config/_query/no-paging?paging=false`, {
+    method: 'POST',
+    data,
+  });
+
+// 通知模板
+export const queryMessageTemplate = (data: any) =>
+  request(`${SystemConst.API_BASE}/notifier/template/_query/no-paging?paging=false`, {
+    method: 'POST',
+    data,
+  });

+ 7 - 0
src/pages/rule-engine/Scene/Save/components/ItemGroup/index.less

@@ -0,0 +1,7 @@
+.group-item-compact {
+  display: flex;
+
+  &:nth-last-child(1) {
+    flex: 1;
+  }
+}

+ 10 - 0
src/pages/rule-engine/Scene/Save/components/ItemGroup/index.tsx

@@ -0,0 +1,10 @@
+import React from 'react';
+import './index.less';
+
+interface ItemGroupProps {
+  children?: React.ReactNode;
+}
+
+export default (props: ItemGroupProps) => {
+  return <div className={'group-item-compact'}>{props.children}</div>;
+};

+ 49 - 0
src/pages/rule-engine/Scene/Save/components/TimeSelect/index.tsx

@@ -0,0 +1,49 @@
+import { TreeSelect } from 'antd';
+import React, { useCallback, useEffect, useState } from 'react';
+
+type OptionsItemType = {
+  label: string;
+  value: string | number;
+};
+
+interface TimeSelectProps {
+  options?: OptionsItemType[];
+  value?: string;
+  onChange?: (value: string[]) => void;
+  style?: React.CSSProperties;
+}
+
+export default (props: TimeSelectProps) => {
+  const [checkedKeys, setCheckedKeys] = useState<any[]>([]);
+
+  const onChange = useCallback(
+    (keys: string[], _, extra) => {
+      if (extra.triggerValue === 'all') {
+        const newKeys = extra.checked ? ['all', ...props.options!.map((item) => item.value)] : [];
+        setCheckedKeys(newKeys);
+      } else {
+        const noAllKeys = keys.filter((key) => key !== 'all');
+        const newKeys = noAllKeys.length === props.options!.length ? ['all', ...keys] : noAllKeys;
+
+        setCheckedKeys(newKeys);
+      }
+    },
+    [checkedKeys, props.options],
+  );
+
+  useEffect(() => {}, [checkedKeys]);
+
+  return (
+    <TreeSelect
+      treeCheckable
+      value={checkedKeys}
+      onChange={onChange}
+      style={props.style}
+      treeData={
+        props.options && props.options.length
+          ? [{ label: '全部', value: 'all' }, ...props.options]
+          : []
+      }
+    />
+  );
+};

+ 89 - 0
src/pages/rule-engine/Scene/Save/components/TimingTrigger/index.tsx

@@ -0,0 +1,89 @@
+import { Select, Input, TimePicker, InputNumber } from 'antd';
+import { TimeSelect } from '@/pages/rule-engine/Scene/Save/components';
+import { useState } from 'react';
+
+export default () => {
+  const [type1, setType1] = useState(1);
+  const [type2, setType2] = useState(1);
+
+  const type1Select = async (key: number) => {
+    setType1(key);
+    if (key !== 3) {
+      // TODO 请求后端返回天数
+    }
+  };
+
+  const type2Select = (key: number) => {
+    setType2(key);
+  };
+
+  const TimeTypeAfter = (
+    <Select
+      defaultValue={'second'}
+      options={[
+        { label: '秒', value: 'second' },
+        { label: '分', value: 'minute' },
+        { label: '小时', value: 'hour' },
+      ]}
+    />
+  );
+
+  const implementNode =
+    type2 === 1 ? (
+      <>
+        <TimePicker.RangePicker />
+        <span> 每 </span>
+        <InputNumber addonAfter={TimeTypeAfter} style={{ width: 150 }} min={0} max={9999} />
+        <span> 执行一次 </span>
+      </>
+    ) : (
+      <>
+        <TimePicker />
+        <InputNumber addonAfter={TimeTypeAfter} style={{ width: 150 }} min={0} max={9999} />
+        <span> 执行一次 </span>
+      </>
+    );
+
+  return (
+    <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
+      <Select
+        options={[
+          { label: '按周', value: 1 },
+          { label: '按月', value: 2 },
+          { label: 'cron表达式', value: 3 },
+        ]}
+        value={type1}
+        onSelect={type1Select}
+        style={{ width: 120 }}
+      />
+      {type1 !== 3 ? (
+        <>
+          <TimeSelect
+            options={[
+              { label: '周一', value: 1 },
+              { label: '周二', value: 2 },
+              { label: '周三', value: 3 },
+              { label: '周四', value: 4 },
+              { label: '周五', value: 5 },
+              { label: '周六', value: 6 },
+              { label: '周末', value: 7 },
+            ]}
+            style={{ width: 150 }}
+          />
+          <Select
+            options={[
+              { label: '周期执行', value: 1 },
+              { label: '执行一次', value: 2 },
+            ]}
+            value={type2}
+            style={{ width: 150 }}
+            onSelect={type2Select}
+          />
+          {implementNode}
+        </>
+      ) : (
+        <Input placeholder={'请输入cron表达式'} style={{ width: 400 }} />
+      )}
+    </div>
+  );
+};

+ 24 - 0
src/pages/rule-engine/Scene/Save/components/TriggerWay/index.less

@@ -0,0 +1,24 @@
+@import '~antd/es/style/themes/default.less';
+
+.trigger-way-warp {
+  display: flex;
+  gap: 20px;
+  width: 100%;
+
+  .trigger-way-item {
+    padding: 20px 16px;
+    border: 1px solid #e0e4e8;
+    border-radius: 2px;
+    cursor: pointer;
+    transition: all 0.3s;
+
+    &:hover {
+      color: @primary-color-hover;
+    }
+
+    &.active {
+      color: @primary-color-active;
+      border-color: @primary-color-active;
+    }
+  }
+}

+ 65 - 0
src/pages/rule-engine/Scene/Save/components/TriggerWay/index.tsx

@@ -0,0 +1,65 @@
+import { useEffect, useState } from 'react';
+import classNames from 'classnames';
+import './index.less';
+
+interface TriggerWayProps {
+  value?: string;
+  onChange?: (type: string) => void;
+}
+
+enum TriggerWayType {
+  manual = 'manual',
+  timing = 'timer',
+  device = 'device',
+}
+
+export default (props: TriggerWayProps) => {
+  const [type, setType] = useState(props.value || '');
+
+  useEffect(() => {
+    setType(props.value || '');
+  }, [props.value]);
+
+  const onSelect = (_type: string) => {
+    setType(_type);
+
+    if (props.onChange) {
+      props.onChange(_type);
+    }
+  };
+
+  return (
+    <div className={'trigger-way-warp'}>
+      <div
+        className={classNames('trigger-way-item', {
+          active: type === TriggerWayType.manual,
+        })}
+        onClick={() => {
+          onSelect(TriggerWayType.manual);
+        }}
+      >
+        手动触发
+      </div>
+      <div
+        className={classNames('trigger-way-item', {
+          active: type === TriggerWayType.timing,
+        })}
+        onClick={() => {
+          onSelect(TriggerWayType.timing);
+        }}
+      >
+        定时触发
+      </div>
+      <div
+        className={classNames('trigger-way-item', {
+          active: type === TriggerWayType.device,
+        })}
+        onClick={() => {
+          onSelect(TriggerWayType.device);
+        }}
+      >
+        设备触发
+      </div>
+    </div>
+  );
+};

+ 4 - 0
src/pages/rule-engine/Scene/Save/components/index.ts

@@ -0,0 +1,4 @@
+export { default as TimeSelect } from './TimeSelect';
+export { default as TimingTrigger } from './TimingTrigger';
+export { default as TriggerWay } from './TriggerWay';
+export { default as ItemGroup } from './ItemGroup';

+ 100 - 1
src/pages/rule-engine/Scene/Save/index.tsx

@@ -1 +1,100 @@
-export default () => {};
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card, Input, Radio } from 'antd';
+import { useIntl, useLocation } from 'umi';
+import { useEffect, useState } from 'react';
+import { TriggerWay, TimingTrigger } from './components';
+import Actions from './action';
+import { PermissionButton } from '@/components';
+import ProForm from '@ant-design/pro-form';
+
+export default () => {
+  const intl = useIntl();
+  const location = useLocation();
+  const [form] = ProForm.useForm();
+
+  const [actionType, setActionType] = useState(1);
+  const { getOtherPermission } = PermissionButton.usePermission('rule-engine/Scene');
+
+  const getDetail = async () => {
+    // TODO 回显数据
+  };
+
+  useEffect(() => {
+    const params = new URLSearchParams(location.search);
+    const id = params.get('id');
+    if (id) {
+      getDetail();
+    }
+  }, [location]);
+
+  const saveData = async () => {
+    const formData = await form.validateFields();
+    console.log(formData);
+  };
+
+  return (
+    <PageContainer>
+      <Card>
+        <ProForm form={form} layout={'vertical'}>
+          <ProForm.Item
+            name="name"
+            label={intl.formatMessage({
+              id: 'pages.table.name',
+              defaultMessage: '名称',
+            })}
+            required={true}
+            rules={[
+              { required: true, message: '请输入名称' },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ]}
+          >
+            <Input placeholder={'请输入名称'} />
+          </ProForm.Item>
+          <ProForm.Item label={'触发方式'} required>
+            <TriggerWay />
+            <TimingTrigger />
+          </ProForm.Item>
+          <ProForm.Item
+            label={
+              <>
+                <span>执行动作</span>
+                <Radio.Group
+                  value={actionType}
+                  optionType="button"
+                  buttonStyle="solid"
+                  size={'small'}
+                  style={{ marginLeft: 12 }}
+                  onChange={(e) => {
+                    setActionType(e.target.value);
+                  }}
+                  options={[
+                    { label: '串行', value: 1 },
+                    { label: '并行', value: 2 },
+                  ]}
+                />
+              </>
+            }
+            tooltip={
+              <div style={{ width: 200 }}>
+                <div>并行:满足任意条件时会触发执行动作</div>
+                <div>穿行:满足所有执行条件才会触发执行动作</div>
+              </div>
+            }
+            required
+          >
+            <Actions />
+          </ProForm.Item>
+          <ProForm.Item name={'describe'} label={'说明'}>
+            <Input.TextArea rows={4} maxLength={200} showCount placeholder={'请输入说明'} />
+          </ProForm.Item>
+        </ProForm>
+        <PermissionButton isPermission={getOtherPermission(['add', 'update'])} onClick={saveData}>
+          保存
+        </PermissionButton>
+      </Card>
+    </PageContainer>
+  );
+};

+ 10 - 0
src/pages/rule-engine/Scene/Save/trigger/index.tsx

@@ -0,0 +1,10 @@
+import { Form } from 'antd';
+import TriggerWay from '../components/TriggerWay';
+
+export default () => {
+  return (
+    <Form.Item>
+      <TriggerWay />
+    </Form.Item>
+  );
+};

+ 12 - 4
src/pages/rule-engine/Scene/index.tsx

@@ -1,5 +1,5 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { useRef, useState } from 'react';
+import React, { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import type { SceneItem } from '@/pages/rule-engine/Scene/typings';
 import { Badge, message } from 'antd';
@@ -8,7 +8,10 @@ import { useIntl } from '@@/plugin-locale/localeExports';
 import { PermissionButton, ProTableCard } from '@/components';
 import { statusMap } from '@/pages/device/Instance';
 import SearchComponent from '@/components/SearchComponent';
+import SceneCard from '@/components/ProTableCard/CardItems/scene';
 import Service from './service';
+import { useHistory } from 'umi';
+import { getMenuPathByCode } from '@/utils/menu';
 
 export const service = new Service('rule-engine/scene');
 
@@ -17,8 +20,9 @@ const Scene = () => {
   const actionRef = useRef<ActionType>();
   const { permission } = PermissionButton.usePermission('rule-engine/Scene');
   const [searchParams, setSearchParams] = useState<any>({});
+  const history = useHistory();
 
-  const Tools = (record: any, type: 'card' | 'table') => {
+  const Tools = (record: any, type: 'card' | 'table'): React.ReactNode[] => {
     return [
       <PermissionButton
         key={'update'}
@@ -189,7 +193,7 @@ const Scene = () => {
           setSearchParams(data);
         }}
       />
-      <ProTableCard
+      <ProTableCard<SceneItem>
         columns={columns}
         actionRef={actionRef}
         params={searchParams}
@@ -213,7 +217,10 @@ const Scene = () => {
             icon={<PlusOutlined />}
             type="primary"
             isPermission={permission.add}
-            onClick={() => {}}
+            onClick={() => {
+              const url = getMenuPathByCode('rule-engine/Scene/Save');
+              history.push(url);
+            }}
           >
             {intl.formatMessage({
               id: 'pages.data.option.add',
@@ -221,6 +228,7 @@ const Scene = () => {
             })}
           </PermissionButton>,
         ]}
+        cardRender={(record) => <SceneCard {...record} tools={Tools(record, 'card')} />}
       />
     </PageContainer>
   );

+ 6 - 3
src/pages/rule-engine/Scene/typings.d.ts

@@ -1,4 +1,4 @@
-import type { BaseItem, State } from '@/utils/typings';
+import type { State } from '@/utils/typings';
 
 type Action = {
   executor: string;
@@ -10,9 +10,12 @@ type Trigger = {
   device: Record<string, unknown>;
 };
 
-type SceneItem = {
+interface SceneItem {
   parallel: boolean;
   state: State;
   actions: Action[];
   triggers: Trigger[];
-} & BaseItem;
+  id: string;
+  name: string;
+  describe: string;
+}

+ 4 - 1
src/pages/system/Menu/Detail/edit.tsx

@@ -159,7 +159,10 @@ export default (props: EditProps) => {
                       defaultMessage: '名称',
                     })}
                     required={true}
-                    rules={[{ required: true, message: '请输入名称' }]}
+                    rules={[
+                      { required: true, message: '请输入名称' },
+                      { max: 64, message: '最多可输入64个字符' },
+                    ]}
                   >
                     <Input disabled={disabled} placeholder={'请输入名称'} />
                   </Form.Item>

+ 6 - 2
src/utils/menu/index.ts

@@ -1,7 +1,8 @@
 // 路由components映射
 import type { IRouteProps } from 'umi';
 import type { MenuItem } from '@/pages/system/Menu/typing';
-import { BUTTON_PERMISSION, getDetailNameByCode, MENUS_CODE, MENUS_CODE_TYPE } from './router';
+import { getDetailNameByCode, MENUS_CODE } from './router';
+import type { BUTTON_PERMISSION, MENUS_CODE_TYPE } from './router';
 
 /** localStorage key */
 export const MENUS_DATA_CACHE = 'MENUS_DATA_CACHE';
@@ -29,6 +30,9 @@ const extraRouteObj = {
       { code: 'Playback', name: '回放' },
     ],
   },
+  'rule-engine/Scene': {
+    children: [{ code: 'Save', name: '详情' }],
+  },
 };
 
 /**
@@ -257,7 +261,7 @@ export const getButtonPermission = (
  * 通过缓存的数据取出相应的路由url
  * @param code
  */
-export const getMenuPathByCode = (code: string): string => {
+export const getMenuPathByCode = (code: MENUS_CODE_TYPE): string => {
   const menusStr = localStorage.getItem(MENUS_DATA_CACHE) || '{}';
   const menusData = JSON.parse(menusStr);
   return menusData[code];

+ 1 - 0
src/utils/menu/router.ts

@@ -60,6 +60,7 @@ export enum MENUS_CODE {
   'rule-engine/Instance' = 'rule-engine/Instance',
   'rule-engine/SQLRule' = 'rule-engine/SQLRule',
   'rule-engine/Scene' = 'rule-engine/Scene',
+  'rule-engine/Scene/Save' = 'rule-engine/Scene/Save',
   'simulator/Device' = 'simulator/Device',
   'system/DataSource' = 'system/DataSource',
   'system/Department/Assets' = 'system/Department/Assets',