Procházet zdrojové kódy

fix(merge): merge notice

lind před 3 roky
rodič
revize
38a8026c3f
51 změnil soubory, kde provedl 1625 přidání a 944 odebrání
  1. binární
      public/images/network.png
  2. 7 0
      src/components/AIcon/index.tsx
  3. 5 3
      src/components/FBraftEditor/index.tsx
  4. 32 33
      src/components/Footer/index.tsx
  5. 4 3
      src/components/ProTableCard/CardItems/AccessConfig/index.less
  6. 7 7
      src/components/ProTableCard/CardItems/AccessConfig/index.tsx
  7. 16 9
      src/components/ProTableCard/CardItems/cascade.tsx
  8. 80 0
      src/components/ProTableCard/CardItems/networkCard.tsx
  9. 1 2
      src/components/ProTableCard/CardItems/ruleInstance.tsx
  10. 6 5
      src/components/Upload/Image/index.tsx
  11. 1 0
      src/components/index.ts
  12. 19 8
      src/pages/device/Category/Save/index.tsx
  13. 63 13
      src/pages/device/Category/index.tsx
  14. 84 95
      src/pages/device/Instance/Detail/Config/index.tsx
  15. 14 8
      src/pages/device/Instance/Detail/Log/index.tsx
  16. 47 34
      src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx
  17. 7 6
      src/pages/device/Instance/Detail/Tags/index.tsx
  18. 16 14
      src/pages/device/Instance/Detail/index.tsx
  19. 19 10
      src/pages/device/Instance/Save/index.tsx
  20. 24 16
      src/pages/device/Instance/index.tsx
  21. 3 3
      src/pages/device/Instance/service.ts
  22. 7 7
      src/pages/device/Product/Detail/Access/AccessConfig/index.tsx
  23. 12 12
      src/pages/device/Product/Detail/Access/index.tsx
  24. 20 12
      src/pages/device/Product/Save/index.tsx
  25. 15 15
      src/pages/device/Product/index.tsx
  26. 17 23
      src/pages/device/components/Metadata/Base/Edit/index.tsx
  27. 44 24
      src/pages/device/components/Metadata/Base/index.tsx
  28. 13 10
      src/pages/device/components/Metadata/Cat/index.tsx
  29. 33 27
      src/pages/link/AccessConfig/index.tsx
  30. 11 11
      src/pages/link/Type/Detail/index.tsx
  31. 102 23
      src/pages/link/Type/index.tsx
  32. 21 10
      src/pages/media/Cascade/Channel/index.tsx
  33. 26 12
      src/pages/media/Cascade/index.tsx
  34. 49 32
      src/pages/media/Device/Channel/index.tsx
  35. 20 4
      src/pages/media/Device/Save/index.tsx
  36. 14 15
      src/pages/media/Device/index.tsx
  37. 1 1
      src/pages/notice/Config/Debug/index.tsx
  38. 1 1
      src/pages/notice/Config/Log/index.tsx
  39. 4 2
      src/pages/notice/Config/index.tsx
  40. 2 2
      src/pages/notice/Config/service.ts
  41. 6 3
      src/pages/notice/Template/Debug/index.tsx
  42. 1 11
      src/pages/notice/Template/Detail/doc/DingTalkRebot.tsx
  43. 355 202
      src/pages/notice/Template/Detail/index.tsx
  44. 2 2
      src/pages/notice/Template/Log/index.tsx
  45. 8 4
      src/pages/notice/Template/index.tsx
  46. 79 13
      src/pages/notice/Template/service.ts
  47. 73 64
      src/pages/rule-engine/Instance/index.tsx
  48. 39 2
      src/pages/system/Department/index.tsx
  49. 12 1
      src/pages/system/Department/save.tsx
  50. 6 6
      src/pages/system/Role/Detail/UserManage/BindUser.tsx
  51. 177 124
      src/pages/system/User/Save/index.tsx

binární
public/images/network.png


+ 7 - 0
src/components/AIcon/index.tsx

@@ -0,0 +1,7 @@
+import {createFromIconfontCN} from '@ant-design/icons';
+
+const AIcon = createFromIconfontCN({
+  scriptUrl: '/icons/iconfont.js', // 在 iconfont.cn 上生成
+});
+
+export default AIcon;

+ 5 - 3
src/components/FBraftEditor/index.tsx

@@ -1,11 +1,12 @@
-import { connect, mapProps } from '@formily/react';
-import BraftEditor, { BraftEditorProps, EditorState } from 'braft-editor';
+import {connect, mapProps} from '@formily/react';
+import BraftEditor, {BraftEditorProps, EditorState} from 'braft-editor';
 import 'braft-editor/dist/index.css';
-import { useState } from 'react';
+import {useState} from 'react';
 
 interface Props extends BraftEditorProps {
   value: any;
   onChange: (data: any) => void;
+  placeholder?: string;
 }
 
 const FBraftEditor = connect((props: Props) => {
@@ -18,6 +19,7 @@ const FBraftEditor = connect((props: Props) => {
       {
         // @ts-ignore
         <BraftEditor
+          placeholder={props.placeholder}
           value={editorState}
           onChange={(state) => {
             setEditorState(state);

+ 32 - 33
src/components/Footer/index.tsx

@@ -1,37 +1,36 @@
-import { useIntl } from 'umi';
-import { GithubOutlined } from '@ant-design/icons';
-import { DefaultFooter } from '@ant-design/pro-layout';
+// import {useIntl} from 'umi';
+// import {GithubOutlined} from '@ant-design/icons';
+// import {DefaultFooter} from '@ant-design/pro-layout';
 
 export default () => {
-  const intl = useIntl();
-  const defaultMessage = intl.formatMessage({
-    id: 'app.copyright.produced',
-    defaultMessage: 'Jetlinks Team',
-  });
+  // const intl = useIntl();
+  // const defaultMessage = intl.formatMessage({
+  //   id: 'app.copyright.produced',
+  //   defaultMessage: 'Jetlinks Team',
+  // });
 
-  return (
-    <DefaultFooter
-      copyright={`2021 ${defaultMessage}`}
-      links={[
-        {
-          key: 'Jetlinks',
-          title: 'Jetlinks',
-          href: 'https://pro.ant.design',
-          blankTarget: true,
-        },
-        {
-          key: 'github',
-          title: <GithubOutlined />,
-          href: 'https://github.com/jetlinks',
-          blankTarget: true,
-        },
-        {
-          key: 'Ant Design',
-          title: 'Ant Design',
-          href: 'https://ant.design',
-          blankTarget: true,
-        },
-      ]}
-    />
-  );
+  // const footer = <DefaultFooter
+  //   copyright={`2021 ${defaultMessage}`}
+  //   links={[
+  //     {
+  //       key: 'Jetlinks',
+  //       title: 'Jetlinks',
+  //       href: 'https://pro.ant.design',
+  //       blankTarget: true,
+  //     },
+  //     {
+  //       key: 'github',
+  //       title: <GithubOutlined/>,
+  //       href: 'https://github.com/jetlinks',
+  //       blankTarget: true,
+  //     },
+  //     {
+  //       key: 'Ant Design',
+  //       title: 'Ant Design',
+  //       href: 'https://ant.design',
+  //       blankTarget: true,
+  //     },
+  //   ]}
+  // />
+  return null;
 };

+ 4 - 3
src/components/ProTableCard/CardItems/AccessConfig/index.less

@@ -24,12 +24,13 @@
     .card {
       display: flex;
       flex-direction: column;
-      width: 100%;
+      flex-grow: 1;
+      width: 0;
       margin-left: 20px;
 
       .header {
         .title {
-          width: 70%;
+          width: calc(100% - 70px);
           overflow: hidden;
           font-weight: 700;
           font-size: 18px;
@@ -42,7 +43,7 @@
         }
 
         .desc {
-          width: 70%;
+          width: 100%;
           margin-top: 10px;
           overflow: hidden;
           color: #666;

+ 7 - 7
src/components/ProTableCard/CardItems/AccessConfig/index.tsx

@@ -1,9 +1,9 @@
 import React from 'react';
-import { StatusColorEnum } from '@/components/BadgeStatus';
-import { TableCard } from '@/components';
+import {StatusColorEnum} from '@/components/BadgeStatus';
+import {TableCard} from '@/components';
 import '@/style/common.less';
-import { Badge, Tooltip } from 'antd';
-import type { AccessItem } from '@/pages/link/AccessConfig/typings';
+import {Badge, Tooltip} from 'antd';
+import type {AccessItem} from '@/pages/link/AccessConfig/typings';
 import './index.less';
 import classNames from 'classnames';
 
@@ -38,9 +38,9 @@ export default (props: AccessConfigCardProps) => {
         </div>
         <div className="card">
           <div className="header">
-            <div className="title">
-              <Tooltip title={props.name}>{props.name || '--'}</Tooltip>
-            </div>
+            <Tooltip title={props.name}>
+              <div className="title ellipsis">{props.name || '--'}</div>
+            </Tooltip>
             <div className="desc">{props.description || '--'}</div>
           </div>
           <div className="container">

+ 16 - 9
src/components/ProTableCard/CardItems/cascade.tsx

@@ -1,10 +1,10 @@
 import React from 'react';
-import type { CascadeItem } from '@/pages/media/Cascade/typings';
-import { StatusColorEnum } from '@/components/BadgeStatus';
-import { TableCard } from '@/components';
+import type {CascadeItem} from '@/pages/media/Cascade/typings';
+import {StatusColorEnum} from '@/components/BadgeStatus';
+import {TableCard} from '@/components';
 import '@/style/common.less';
 import '../index.less';
-import { Badge } from 'antd';
+import {Badge} from 'antd';
 
 export interface CascadeCardProps extends CascadeItem {
   detail?: React.ReactNode;
@@ -35,11 +35,18 @@ export default (props: CascadeCardProps) => {
             <span className={'card-item-header-name ellipsis'}>{props.name}</span>
           </div>
           <div>通道数量: {props?.count || 0}</div>
-          <div>
-            <Badge
-              status={props.onlineStatus?.value === 'offline' ? 'error' : 'success'}
-              text={`sip:${props.sipConfigs[0]?.sipId}@${props.sipConfigs[0]?.hostAndPort}`}
-            />
+          <div style={{ display: 'flex', width: '100%' }}>
+            <Badge status={props.onlineStatus?.value === 'offline' ? 'error' : 'success'} />
+            <div
+              style={{
+                width: '90%',
+                overflow: 'hidden',
+                whiteSpace: 'nowrap',
+                textOverflow: 'ellipsis',
+              }}
+            >
+              sip:{props.sipConfigs[0]?.sipId}@{props.sipConfigs[0]?.hostAndPort}
+            </div>
           </div>
         </div>
       </div>

+ 80 - 0
src/components/ProTableCard/CardItems/networkCard.tsx

@@ -0,0 +1,80 @@
+import React from 'react';
+import { TableCard } from '@/components';
+import '@/style/common.less';
+import '../index.less';
+import { NetworkItem } from '@/pages/link/Type/typings';
+import { networkMap } from '@/pages/link/Type';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import { Tooltip } from 'antd';
+
+export interface NoticeCardProps extends NetworkItem {
+  detail?: React.ReactNode;
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+}
+
+const image = require('/public/images/network.png');
+
+export default (props: NoticeCardProps) => {
+  const createDetail = () => {
+    const record = props;
+    if (record.shareCluster) {
+      const publicHost = record.configuration.publicHost;
+      const publicPort = record.configuration.publicPort;
+      return publicHost ? (
+        <>
+          {networkMap[record.type]}
+          {publicHost}:{publicPort}
+        </>
+      ) : null;
+    } else {
+      const log = record.cluster?.map(
+        (item) => `${item.configuration.publicHost}:${item.configuration.publicPort}`,
+      );
+      return (
+        <>
+          {log.map((item) => (
+            <div key={item}>
+              `${networkMap[record.type]}${item}`
+            </div>
+          ))}
+        </>
+      );
+    }
+  };
+  return (
+    <TableCard
+      actions={props.actions}
+      status={props.state.value}
+      statusText={props.state.text}
+      statusNames={{
+        disabled: StatusColorEnum.error,
+        enabled: StatusColorEnum.processing,
+      }}
+      showMask={false}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img width={88} height={88} src={image} alt={props.type} />
+        </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'}>{props.type}</div>
+            </div>
+            <div>
+              <label>详情</label>
+              <Tooltip title={createDetail()}>
+                <div className={'ellipsis'}>{createDetail()}</div>
+              </Tooltip>
+            </div>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 1 - 2
src/components/ProTableCard/CardItems/ruleInstance.tsx

@@ -21,8 +21,7 @@ export default (props: RuleInstanceCardProps) => {
       statusText={props.state.text}
       statusNames={{
         started: StatusColorEnum.success,
-        stopped: StatusColorEnum.error,
-        disable: StatusColorEnum.processing,
+        disable: StatusColorEnum.error,
       }}
     >
       <div className={'pro-table-card-item'}>

+ 6 - 5
src/components/Upload/Image/index.tsx

@@ -1,10 +1,10 @@
-import { message, Upload } from 'antd';
-import React, { useEffect, useState } from 'react';
-import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
+import {message, Upload} from 'antd';
+import React, {useEffect, useState} from 'react';
+import {LoadingOutlined, PlusOutlined} from '@ant-design/icons';
 import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
-import type { UploadChangeParam } from 'antd/lib/upload/interface';
-import type { RcFile } from 'antd/es/upload';
+import type {UploadChangeParam} from 'antd/lib/upload/interface';
+import type {RcFile} from 'antd/es/upload';
 import './index.less';
 
 interface UploadImageProps {
@@ -69,6 +69,7 @@ export default ({ onChange, value, ...extraProps }: UploadImageProps) => {
           showUploadList={false}
           onChange={handleChange}
           beforeUpload={beforeUpload}
+          accept={imageTypes && imageTypes.length ? imageTypes.toString() : ''}
           {...extraProps}
         >
           <div className={'upload-image-content'} style={extraProps.style}>

+ 1 - 0
src/components/index.ts

@@ -6,3 +6,4 @@ export { default as BadgeStatus } from './BadgeStatus';
 export { default as Player } from './Player';
 export { default as ScreenPlayer } from './Player/ScreenPlayer';
 export { default as Modal } from './Modal';
+export { default as AIcon } from './AIcon';

+ 19 - 8
src/pages/device/Category/Save/index.tsx

@@ -13,21 +13,22 @@ import {
   Upload,
 } from '@formily/antd';
 import React from 'react';
-import { createForm } from '@formily/core';
-import { createSchemaField } from '@formily/react';
+import {createForm} from '@formily/core';
+import {createSchemaField} from '@formily/react';
 import FUpload from '@/components/Upload';
 import * as ICONS from '@ant-design/icons';
-import { message, Modal } from 'antd';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import type { ISchema } from '@formily/json-schema';
-import type { CategoryItem } from '@/pages/visualization/Category/typings';
-import { service, state } from '@/pages/device/Category';
-import type { Response } from '@/utils/typings';
+import {message, Modal} from 'antd';
+import {useIntl} from '@@/plugin-locale/localeExports';
+import type {ISchema} from '@formily/json-schema';
+import type {CategoryItem} from '@/pages/visualization/Category/typings';
+import {service, state} from '@/pages/device/Category';
+import type {Response} from '@/utils/typings';
 
 interface Props {
   visible: boolean;
   close: () => void;
   data: Partial<CategoryItem>;
+  reload?: () => void;
 }
 
 const Save = (props: Props) => {
@@ -70,6 +71,9 @@ const Save = (props: Props) => {
       : ((await service.save(value as any)) as Response<CategoryItem>);
     if (resp.status === 200) {
       message.success('操作成功!');
+      if (props.reload) {
+        props.reload();
+      }
     } else {
       message.error('操作失败');
     }
@@ -103,6 +107,9 @@ const Save = (props: Props) => {
         }),
         'x-decorator': 'FormItem',
         'x-component': 'Input',
+        'x-component-props': {
+          placeholder: '请输入名称',
+        },
         required: true,
         name: 'name',
       },
@@ -113,6 +120,9 @@ const Save = (props: Props) => {
         }),
         'x-decorator': 'FormItem',
         'x-component': 'NumberPicker',
+        'x-component-props': {
+          placeholder: '请输入分类排序',
+        },
         name: 'sortIndex',
       },
       description: {
@@ -125,6 +135,7 @@ const Save = (props: Props) => {
         'x-component': 'Input.TextArea',
         'x-component-props': {
           rows: 3,
+          placeholder: '请输入描述',
         },
         name: 'description',
       },

+ 63 - 13
src/pages/device/Category/index.tsx

@@ -1,17 +1,17 @@
-import { PageContainer } from '@ant-design/pro-layout';
+import {PageContainer} from '@ant-design/pro-layout';
 import Service from '@/pages/device/Category/service';
-import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import type {ActionType, ProColumns} from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
-import { Button, message, Popconfirm, Tooltip } from 'antd';
-import { useRef, useState } from 'react';
-import { useIntl } from '@@/plugin-locale/localeExports';
+import {DeleteOutlined, EditOutlined, PlusOutlined} from '@ant-design/icons';
+import {Button, message, Popconfirm, Tooltip} from 'antd';
+import {useRef, useState} from 'react';
+import {useIntl} from '@@/plugin-locale/localeExports';
 import Save from '@/pages/device/Category/Save';
-import { model } from '@formily/reactive';
-import { observer } from '@formily/react';
-import type { Response } from '@/utils/typings';
+import {model} from '@formily/reactive';
+import {observer} from '@formily/react';
+import type {Response} from '@/utils/typings';
 import SearchComponent from '@/components/SearchComponent';
-import { getButtonPermission } from '@/utils/menu';
+import {getButtonPermission} from '@/utils/menu';
 
 export const service = new Service('device/category');
 
@@ -24,9 +24,33 @@ export const state = model<{
   current: {},
   parentId: undefined,
 });
+
+export const getSortIndex = (data: CategoryItem[], pId?: string): number => {
+  let sortIndex = 0;
+  if (data.length) {
+    if (!pId) {
+      return data.sort((a, b) => b.sortIndex - a.sortIndex)[0].sortIndex + 1;
+    }
+    data.some((department) => {
+      if (department.id === pId && department.children) {
+        const sortArray = department.children.sort((a, b) => b.sortIndex - a.sortIndex);
+        sortIndex = sortArray[0].sortIndex + 1;
+        return true;
+      } else if (department.children) {
+        sortIndex = getSortIndex(department.children, pId);
+        return !!sortIndex;
+      }
+      return false;
+    });
+  }
+  return sortIndex;
+};
+
 const Category = observer(() => {
   const actionRef = useRef<ActionType>();
   const [param, setParam] = useState({});
+  const [sortParam, setSortParam] = useState<any>({ name: 'sortIndex', order: 'asc' });
+  const [treeData, setTreeData] = useState<any[]>([]);
 
   const intl = useIntl();
 
@@ -41,6 +65,8 @@ const Category = observer(() => {
     {
       title: '分类排序',
       dataIndex: 'sortIndex',
+      valueType: 'digit',
+      sorter: true,
       // render: (text) => (
       //   <Space>{text}<EditOutlined onClick={() => {
 
@@ -89,7 +115,11 @@ const Category = observer(() => {
           key={'add-next'}
           onClick={() => {
             state.visible = true;
+            const sortIndex = getSortIndex(treeData, record.id);
             state.parentId = record.id;
+            state.current = {
+              sortIndex,
+            };
           }}
           disabled={getButtonPermission('device/Category', ['update', 'add'])}
         >
@@ -106,9 +136,9 @@ const Category = observer(() => {
           disabled={getButtonPermission('device/Category', ['delete'])}
           type="link"
           style={{ padding: 0 }}
+          key={'delete'}
         >
           <Popconfirm
-            key={'delete'}
             onConfirm={async () => {
               const resp = (await service.remove(record.id)) as Response<any>;
               if (resp.status === 200) {
@@ -147,7 +177,12 @@ const Category = observer(() => {
         params={param}
         search={false}
         request={async (params) => {
-          const response = await service.queryTree({ paging: false, ...params });
+          const response = await service.queryTree({
+            paging: false,
+            sorts: [sortParam],
+            ...params,
+          });
+          setTreeData(response.result);
           return {
             code: response.message,
             result: {
@@ -161,10 +196,23 @@ const Category = observer(() => {
         }}
         rowKey="id"
         columns={columns}
+        onChange={(_, f, sorter: any) => {
+          if (sorter.order) {
+            setSortParam({ name: sorter.columnKey, order: sorter.order.replace('end', '') });
+          } else {
+            setSortParam({ name: 'sortIndex', value: 'asc' });
+          }
+        }}
         headerTitle={
           <Button
             disabled={getButtonPermission('device/Category', ['add'])}
-            onClick={() => (state.visible = true)}
+            onClick={() => {
+              const sortIndex = getSortIndex(treeData, '');
+              state.current = {
+                sortIndex,
+              };
+              state.visible = true;
+            }}
             key="button"
             icon={<PlusOutlined />}
             type="primary"
@@ -185,6 +233,8 @@ const Category = observer(() => {
           state.visible = false;
           state.current = {};
           state.parentId = undefined;
+        }}
+        reload={() => {
           actionRef.current?.reload();
         }}
       />

+ 84 - 95
src/pages/device/Instance/Detail/Config/index.tsx

@@ -1,14 +1,9 @@
-import { Button, Descriptions, message, Popconfirm, Space, Tooltip } from 'antd';
-import { InstanceModel, service } from '@/pages/device/Instance';
-import { useEffect, useState } from 'react';
-import type { ConfigMetadata } from '@/pages/device/Product/typings';
-import { history, useParams } from 'umi';
-import {
-  CheckOutlined,
-  EditOutlined,
-  QuestionCircleOutlined,
-  UndoOutlined,
-} from '@ant-design/icons';
+import {Button, Descriptions, message, Popconfirm, Space, Tooltip} from 'antd';
+import {InstanceModel, service} from '@/pages/device/Instance';
+import {useEffect, useState} from 'react';
+import type {ConfigMetadata} from '@/pages/device/Product/typings';
+import {history, useParams} from 'umi';
+import {CheckOutlined, EditOutlined, QuestionCircleOutlined, UndoOutlined,} from '@ant-design/icons';
 import Edit from './Edit';
 
 const Config = () => {
@@ -71,7 +66,7 @@ const Config = () => {
           </div>
         );
       } else {
-        return <span>{config[item.property]}</span>;
+        return <div>{config[item.property]}</div>;
       }
     } else {
       return '--';
@@ -80,94 +75,88 @@ const Config = () => {
 
   return (
     <div style={{ width: '100%', marginTop: '20px' }} className="config">
-      <Descriptions
-        layout="vertical"
-        title={[
-          <span key={1}>配置</span>,
-          <Space key={2}>
-            <Button
-              type="link"
-              onClick={async () => {
-                setVisible(true);
+      <div style={{ display: 'flex', marginBottom: 20 }}>
+        <div style={{ fontSize: 16, fontWeight: 700 }}>配置</div>
+        <Space>
+          <Button
+            type="link"
+            onClick={async () => {
+              setVisible(true);
+            }}
+          >
+            <EditOutlined />
+            编辑
+          </Button>
+          {InstanceModel.detail.state?.value !== 'notActive' && (
+            <Popconfirm
+              title="确认重新应用该配置?"
+              onConfirm={async () => {
+                const resp = await service.deployDevice(id || '');
+                if (resp.status === 200) {
+                  message.success('操作成功');
+                  getDetail();
+                }
               }}
             >
-              <EditOutlined />
-              编辑
-            </Button>
-            {InstanceModel.detail.state?.value !== 'notActive' && (
-              <Popconfirm
-                title="确认重新应用该配置?"
-                onConfirm={async () => {
-                  const resp = await service.deployDevice(id || '');
-                  if (resp.status === 200) {
-                    message.success('操作成功');
-                    getDetail();
-                  }
-                }}
-              >
-                <Button type="link">
-                  <CheckOutlined />
-                  应用配置
-                </Button>
-                <Tooltip title="修改配置后需重新应用后才能生效。">
-                  <QuestionCircleOutlined />
-                </Tooltip>
-              </Popconfirm>
-            )}
-            {InstanceModel.detail?.aloneConfiguration && (
-              <Popconfirm
-                title="确认恢复默认配置?"
-                onConfirm={async () => {
-                  const resp = await service.configurationReset(id || '');
-                  if (resp.status === 200) {
-                    message.success('恢复默认配置成功');
-                    getDetail();
-                  }
-                }}
+              <Button type="link">
+                <CheckOutlined />
+                应用配置
+              </Button>
+              <Tooltip title="修改配置后需重新应用后才能生效。">
+                <QuestionCircleOutlined />
+              </Tooltip>
+            </Popconfirm>
+          )}
+          {InstanceModel.detail?.aloneConfiguration && (
+            <Popconfirm
+              title="确认恢复默认配置?"
+              onConfirm={async () => {
+                const resp = await service.configurationReset(id || '');
+                if (resp.status === 200) {
+                  message.success('恢复默认配置成功');
+                  getDetail();
+                }
+              }}
+            >
+              <Button type="link">
+                <UndoOutlined />
+                恢复默认
+              </Button>
+              <Tooltip
+                title={`该设备单独编辑过配置信息,点击此将恢复成默认的配置信息,请谨慎操作。`}
               >
-                <Button type="link">
-                  <UndoOutlined />
-                  恢复默认
-                </Button>
-                <Tooltip
-                  title={`该设备单独编辑过配置信息,点击此将恢复成默认的配置信息,请谨慎操作。`}
-                >
-                  <QuestionCircleOutlined />
-                </Tooltip>
-              </Popconfirm>
-            )}
-          </Space>,
-        ]}
-      >
+                <QuestionCircleOutlined />
+              </Tooltip>
+            </Popconfirm>
+          )}
+        </Space>
+      </div>
+      <div style={{ paddingLeft: 10 }}>
         {(metadata || []).map((i) => (
-          <Descriptions.Item key={i.name} label={<h4>{i.name}</h4>} span={3}>
-            <div style={{ width: '100%' }}>
-              <Descriptions column={2} bordered size="small">
-                {(i?.properties || []).map((item: any) => (
-                  <Descriptions.Item
-                    span={1}
-                    label={
-                      item.description ? (
-                        <div>
-                          <span style={{ marginRight: '10px' }}>{item.name}</span>
-                          <Tooltip title={item.description}>
-                            <QuestionCircleOutlined />
-                          </Tooltip>
-                        </div>
-                      ) : (
-                        item.name
-                      )
-                    }
-                    key={item.property}
-                  >
-                    {renderComponent(item)}
-                  </Descriptions.Item>
-                ))}
-              </Descriptions>
-            </div>
-          </Descriptions.Item>
+          <Descriptions size="small" column={3} key={i.name} bordered title={<h4>{i.name}</h4>}>
+            {(i?.properties || []).map((item: any) => (
+              <Descriptions.Item
+                span={1}
+                label={
+                  item?.description ? (
+                    <div>
+                      <span style={{ marginRight: '10px' }}>{item.name}</span>
+                      <Tooltip title={item.description}>
+                        <QuestionCircleOutlined />
+                      </Tooltip>
+                    </div>
+                  ) : (
+                    item.name
+                  )
+                }
+                key={item.property}
+              >
+                {renderComponent(item)}
+              </Descriptions.Item>
+            ))}
+          </Descriptions>
         ))}
-      </Descriptions>
+      </div>
       {visible && (
         <Edit
           metadata={metadata || []}

+ 14 - 8
src/pages/device/Instance/Detail/Log/index.tsx

@@ -1,11 +1,11 @@
-import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import type {ActionType, ProColumns} from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import type { LogItem } from '@/pages/device/Instance/Detail/Log/typings';
-import { Card, Modal, Tooltip } from 'antd';
-import { SearchOutlined } from '@ant-design/icons';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import { InstanceModel, service } from '@/pages/device/Instance';
-import { useEffect, useRef, useState } from 'react';
+import type {LogItem} from '@/pages/device/Instance/Detail/Log/typings';
+import {Card, Input, Modal, Tooltip} from 'antd';
+import {SearchOutlined} from '@ant-design/icons';
+import {useIntl} from '@@/plugin-locale/localeExports';
+import {InstanceModel, service} from '@/pages/device/Instance';
+import {useEffect, useRef, useState} from 'react';
 import SearchComponent from '@/components/SearchComponent';
 
 const Log = () => {
@@ -70,7 +70,13 @@ const Log = () => {
         return [
           <a
             key="editable"
-            onClick={() => Modal.info({ title: '详细信息', content: <pre>{content}</pre> })}
+            onClick={() =>
+              Modal.info({
+                title: '详细信息',
+                width: 700,
+                content: <Input.TextArea bordered={false} rows={15} value={content} />,
+              })
+            }
           >
             <Tooltip title="查看">
               <SearchOutlined />

+ 47 - 34
src/pages/device/Instance/Detail/MetadataMap/EditableTable/index.tsx

@@ -1,7 +1,7 @@
-import React, { useContext, useEffect, useState } from 'react';
-import { Form, Input, message, Pagination, Select, Table } from 'antd';
-import { service } from '@/pages/device/Instance';
-import { QuestionCircleOutlined } from '@ant-design/icons';
+import React, {useContext, useEffect, useState} from 'react';
+import {Form, Input, message, Pagination, Select, Table} from 'antd';
+import {service} from '@/pages/device/Instance';
+import _ from 'lodash';
 
 const EditableContext: any = React.createContext(null);
 
@@ -42,7 +42,11 @@ const EditableCell = ({
   const save = async () => {
     try {
       const values = await form.validateFields();
-      handleSave({ ...record, ...values });
+      if (values) {
+        handleSave({ ...record, ...values });
+      } else {
+        console.log(values);
+      }
     } catch (errInfo) {
       console.log('Save failed:', errInfo);
     }
@@ -62,6 +66,7 @@ const EditableCell = ({
         <Select
           onChange={save}
           showSearch
+          allowClear
           optionFilterProp="children"
           filterOption={(input: string, option: any) =>
             (option?.children || '').toLowerCase()?.indexOf(input.toLowerCase()) >= 0
@@ -111,6 +116,7 @@ const EditableTable = (props: Props) => {
     total: properties.length,
   });
   const [protocolMetadata, setProtocolMetadata] = useState<any[]>([]);
+  const [pmList, setPmList] = useState<any[]>([]);
 
   const components = {
     body: {
@@ -132,12 +138,19 @@ const EditableTable = (props: Props) => {
       data.map((i: any) => {
         obj[i?.originalId] = i?.metadataId || '';
       });
-      const list = properties.map((item) => {
-        return {
-          ...item,
-          metadataId: obj[item.id] || '',
-        };
-      });
+      if (protocolMetadata.length > 0) {
+        setPmList(protocolMetadata.filter((i) => !_.map(data, 'metadataId').includes(i.id)));
+      } else {
+        setPmList([]);
+      }
+      const list = (JSON.parse(props?.data?.metadata || '{}')?.properties || []).map(
+        (item: any) => {
+          return {
+            ...item,
+            metadataId: obj[item.id] || '',
+          };
+        },
+      );
       setProperties([...list]);
       setDataSource({
         data: list.slice(
@@ -152,18 +165,20 @@ const EditableTable = (props: Props) => {
   };
 
   useEffect(() => {
-    service
-      .queryProtocolMetadata(
-        props.type === 'device' ? props.data?.protocol : props.data?.messageProtocol,
-        props.type === 'device' ? props.data?.transport : props.data?.transportProtocol,
-      )
-      .then((resp) => {
-        if (resp.status === 200) {
-          setProtocolMetadata(JSON.parse(resp.result || '{}')?.properties || []);
-          initData();
-        }
-      });
-  }, []);
+    if (props.data && Object.keys(props.data).length > 0) {
+      service
+        .queryProtocolMetadata(
+          props.type === 'device' ? props.data?.protocol : props.data?.messageProtocol,
+          props.type === 'device' ? props.data?.transport : props.data?.transportProtocol,
+        )
+        .then((resp) => {
+          if (resp.status === 200) {
+            setProtocolMetadata(JSON.parse(resp.result || '{}')?.properties || []);
+            initData();
+          }
+        });
+    }
+  }, [props.data]);
 
   const handleSave = async (row: any) => {
     const newData = [...dataSource.data];
@@ -231,7 +246,7 @@ const EditableTable = (props: Props) => {
         editable: col.editable,
         dataIndex: col.dataIndex,
         title: col.title,
-        list: protocolMetadata,
+        list: pmList,
         handleSave: handleSave,
       }),
     };
@@ -253,16 +268,14 @@ const EditableTable = (props: Props) => {
             });
           }}
         />
-        <div>
-          <div style={{ color: 'rgba(0, 0, 0, .65)' }}>
-            <QuestionCircleOutlined style={{ margin: 5 }} />
-            该设备已脱离产品物模型映射,修改产品物模型映射对该设备物模型映射无影响
-          </div>
-          <div style={{ color: 'rgba(0, 0, 0, .65)' }}>
-            <QuestionCircleOutlined style={{ margin: 5 }} />
-            设备会默认继承产品的物模型映射,修改设备物模型映射后将脱离产品物模型映射
-          </div>
-        </div>
+        {/* <div style={{ color: 'rgba(0, 0, 0, .65)' }}>
+                    <QuestionCircleOutlined style={{ margin: 5 }} />
+                    {
+                        props?.data?.independentMetadata ?
+                            '该设备已脱离产品物模型映射,修改产品物模型映射对该设备物模型映射无影响' :
+                            '设备会默认继承产品的物模型映射,修改设备物模型映射后将脱离产品物模型映射'
+                    }
+                </div> */}
       </div>
       <Table
         components={components}

+ 7 - 6
src/pages/device/Instance/Detail/Tags/index.tsx

@@ -1,8 +1,8 @@
-import { Button, Descriptions } from 'antd';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import { InstanceModel } from '@/pages/device/Instance';
-import { useEffect, useState } from 'react';
-import { EditOutlined } from '@ant-design/icons';
+import {Button, Descriptions} from 'antd';
+import {useIntl} from '@@/plugin-locale/localeExports';
+import {InstanceModel} from '@/pages/device/Instance';
+import {useEffect, useState} from 'react';
+import {EditOutlined} from '@ant-design/icons';
 import Edit from './Edit';
 
 const Tags = () => {
@@ -23,6 +23,7 @@ const Tags = () => {
       <Descriptions
         style={{ marginBottom: 20 }}
         bordered
+        column={3}
         size="small"
         title={
           <span>
@@ -43,7 +44,7 @@ const Tags = () => {
         }
       >
         {(tags || [])?.map((item: any) => (
-          <Descriptions.Item label={`${item.name}(${item.key})`} key={item.key}>
+          <Descriptions.Item span={1} label={`${item.name}(${item.key})`} key={item.key}>
             {item.value || '--'}
           </Descriptions.Item>
         ))}

+ 16 - 14
src/pages/device/Instance/Detail/index.tsx

@@ -1,10 +1,10 @@
-import { PageContainer } from '@ant-design/pro-layout';
-import { InstanceModel, service } from '@/pages/device/Instance';
-import { history, useParams } from 'umi';
-import { Badge, Button, Card, Descriptions, Divider, message, Tooltip } from 'antd';
-import type { ReactNode } from 'react';
-import { useEffect, useState } from 'react';
-import { observer } from '@formily/react';
+import {PageContainer} from '@ant-design/pro-layout';
+import {InstanceModel, service} from '@/pages/device/Instance';
+import {history, useParams} from 'umi';
+import {Badge, Button, Card, Descriptions, Divider, message, Popconfirm, Tooltip} from 'antd';
+import type {ReactNode} from 'react';
+import {useEffect, useState} from 'react';
+import {observer} from '@formily/react';
 import Log from '@/pages/device/Instance/Detail/Log';
 // import Alarm from '@/pages/device/components/Alarm';
 import Info from '@/pages/device/Instance/Detail/Info';
@@ -13,13 +13,13 @@ import Running from '@/pages/device/Instance/Detail/Running';
 import ChildDevice from '@/pages/device/Instance/Detail/ChildDevice';
 import Diagnose from '@/pages/device/Instance/Detail/Diagnose';
 import MetadataMap from '@/pages/device/Instance/Detail/MetadataMap';
-import { useIntl } from '@@/plugin-locale/localeExports';
+import {useIntl} from '@@/plugin-locale/localeExports';
 import Metadata from '../../components/Metadata';
-import type { DeviceMetadata } from '@/pages/device/Product/typings';
+import type {DeviceMetadata} from '@/pages/device/Product/typings';
 import MetadataAction from '@/pages/device/components/Metadata/DataBaseAction';
-import { Store } from 'jetlinks-store';
+import {Store} from 'jetlinks-store';
 import SystemConst from '@/utils/const';
-import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import {getMenuPathByParams, MENUS_CODE} from '@/utils/menu';
 import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
 
 export const deviceStatus = new Map();
@@ -70,9 +70,11 @@ const InstanceDetail = observer(() => {
           <Metadata
             type="device"
             tabAction={
-              <Tooltip title="重置后将使用产品的物模型配置">
-                <Button onClick={resetMetadata}>重置操作</Button>
-              </Tooltip>
+              <Popconfirm title="确认重置?" onConfirm={resetMetadata}>
+                <Tooltip title="重置后将使用产品的物模型配置">
+                  <Button>重置操作</Button>
+                </Tooltip>
+              </Popconfirm>
             }
           />
         </Card>

+ 19 - 10
src/pages/device/Instance/Save/index.tsx

@@ -1,10 +1,10 @@
-import { Col, Form, Input, message, Row, Select } from 'antd';
-import { service } from '@/pages/device/Instance';
-import type { DeviceInstance } from '../typings';
-import { useEffect, useState } from 'react';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import { Modal, UploadImage } from '@/components';
-import { debounce } from 'lodash';
+import {Col, Form, Input, message, Row, Select} from 'antd';
+import {service} from '@/pages/device/Instance';
+import type {DeviceInstance} from '../typings';
+import {useEffect, useState} from 'react';
+import {useIntl} from '@@/plugin-locale/localeExports';
+import {Modal, UploadImage} from '@/components';
+import {debounce} from 'lodash';
 
 interface Props {
   visible: boolean;
@@ -157,7 +157,7 @@ const Save = (props: Props) => {
             >
               <Input
                 disabled={props.model === 'edit'}
-                placeholder={intlFormat('pages.form.tip.input', '请输入')}
+                placeholder={`${intlFormat('pages.form.tip.input', '请输入')}ID`}
               />
             </Form.Item>
             <Form.Item
@@ -183,7 +183,12 @@ const Save = (props: Props) => {
               ]}
               required
             >
-              <Input placeholder={intlFormat('pages.form.tip.input', '请输入')} />
+              <Input
+                placeholder={
+                  intlFormat('pages.form.tip.input', '请输入') +
+                  intlFormat('pages.table.name', '名称')
+                }
+              />
             </Form.Item>
           </Col>
         </Row>
@@ -213,6 +218,7 @@ const Save = (props: Props) => {
                     productName: node.label,
                   });
                 }}
+                placeholder={'请选择所属产品'}
                 filterOption={(input, option) => option.label.includes(input)}
               />
             </Form.Item>
@@ -225,7 +231,10 @@ const Save = (props: Props) => {
           <Col span={24}>
             <Form.Item label={intlFormat('pages.table.description', '说明')} name={'describe'}>
               <Input.TextArea
-                placeholder={intlFormat('pages.form.tip.input', '请输入')}
+                placeholder={
+                  intlFormat('pages.form.tip.input', '请输入') +
+                  intlFormat('pages.table.description', '说明')
+                }
                 rows={4}
                 style={{ width: '100%' }}
                 maxLength={200}

+ 24 - 16
src/pages/device/Instance/index.tsx

@@ -1,10 +1,10 @@
-import { PageContainer } from '@ant-design/pro-layout';
-import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import type { DeviceInstance } from '@/pages/device/Instance/typings';
+import {PageContainer} from '@ant-design/pro-layout';
+import type {ActionType, ProColumns} from '@jetlinks/pro-table';
+import type {DeviceInstance} from '@/pages/device/Instance/typings';
 import moment from 'moment';
-import { Badge, Button, Dropdown, Menu, message, Popconfirm, Tooltip } from 'antd';
-import { useRef, useState } from 'react';
-import { useHistory } from 'umi';
+import {Badge, Button, Dropdown, Menu, message, Popconfirm, Tooltip} from 'antd';
+import {useRef, useState} from 'react';
+import {useHistory} from 'umi';
 import {
   CheckCircleOutlined,
   DeleteOutlined,
@@ -16,20 +16,20 @@ import {
   StopOutlined,
   SyncOutlined,
 } from '@ant-design/icons';
-import { model } from '@formily/reactive';
+import {model} from '@formily/reactive';
 import Service from '@/pages/device/Instance/service';
-import type { MetadataItem } from '@/pages/device/Product/typings';
-import { useIntl } from '@@/plugin-locale/localeExports';
+import type {MetadataItem} from '@/pages/device/Product/typings';
+import {useIntl} from '@@/plugin-locale/localeExports';
 import Save from './Save';
 import Export from './Export';
 import Import from './Import';
 import Process from './Process';
 import SearchComponent from '@/components/SearchComponent';
-import { ProTableCard } from '@/components';
+import {ProTableCard} from '@/components';
 import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
 import DeviceCard from '@/components/ProTableCard/CardItems/device';
-import { getButtonPermission, getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import {getButtonPermission, getMenuPathByParams, MENUS_CODE} from '@/utils/menu';
 
 export const statusMap = new Map();
 statusMap.set('在线', 'success');
@@ -72,30 +72,29 @@ const Instance = () => {
     <Button
       type={'link'}
       style={{ padding: 0 }}
+      key={'detail'}
       onClick={() => {
         InstanceModel.current = record;
         const url = getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id);
         history.push(url);
       }}
-      disabled={getButtonPermission('device/Instance', ['view'])}
     >
       <Tooltip
         title={intl.formatMessage({
           id: 'pages.data.option.detail',
           defaultMessage: '查看',
         })}
-        key={'detail'}
       >
         <EyeOutlined />
       </Tooltip>
     </Button>,
     <Button
       type={'link'}
+      key={'state'}
       style={{ padding: 0 }}
       disabled={getButtonPermission('device/Product', ['action'])}
     >
       <Popconfirm
-        key={'state'}
         title={intl.formatMessage({
           id: `pages.data.option.${
             record.state.value !== 'notActive' ? 'disabled' : 'enabled'
@@ -130,6 +129,7 @@ const Instance = () => {
 
     <Button
       type={'link'}
+      key={'delete'}
       style={{ padding: 0 }}
       disabled={getButtonPermission('device/Instance', ['delete'])}
     >
@@ -140,7 +140,6 @@ const Instance = () => {
               ? 'pages.data.option.remove.tips'
               : 'pages.device.instance.deleteTip',
         })}
-        key={'delete'}
         onConfirm={async () => {
           if (record.state.value === 'notActive') {
             await service.remove(record.id);
@@ -192,6 +191,15 @@ const Instance = () => {
       dataIndex: 'productName',
       width: 200,
       ellipsis: true,
+      valueType: 'select',
+      request: async () => {
+        const res = await service.getProductList();
+        if (res.status === 200) {
+          return res.result.map((pItem: any) => ({ label: pItem.name, value: pItem.id }));
+        }
+        return [];
+      },
+      filterMultiple: true,
     },
     {
       title: intl.formatMessage({
@@ -474,10 +482,10 @@ const Instance = () => {
               <Button
                 disabled={getButtonPermission('device/Instance', ['action'])}
                 type={'link'}
+                key={'state'}
                 style={{ padding: 0 }}
               >
                 <Popconfirm
-                  key={'state'}
                   title={intl.formatMessage({
                     id: `pages.data.option.${
                       record.state.value !== 'notActive' ? 'disabled' : 'enabled'

+ 3 - 3
src/pages/device/Instance/service.ts

@@ -9,7 +9,7 @@ class Service extends BaseService<DeviceInstance> {
   public detail = (id: string) => request(`${this.uri}/${id}/detail`, { method: 'GET' });
 
   // 查询产品列表
-  public getProductList = (params: any) =>
+  public getProductList = (params?: any) =>
     request(`/${SystemConst.API_BASE}/device/product/_query/no-paging`, { method: 'GET', params });
 
   // 批量删除设备
@@ -195,7 +195,7 @@ class Service extends BaseService<DeviceInstance> {
     });
   // 执行功能
   public executeFunctions = (deviceId: string, functionId: string, data: any) =>
-    request(`/${SystemConst.API_BASE}device/invoked/${deviceId}/function/${functionId}`, {
+    request(`/${SystemConst.API_BASE}/device/invoked/${deviceId}/function/${functionId}`, {
       method: 'POST',
       data,
     });
@@ -207,7 +207,7 @@ class Service extends BaseService<DeviceInstance> {
     });
   // 设置属性
   public settingProperties = (deviceId: string, data: any) =>
-    request(`/${SystemConst.API_BASE}/device/setting/${deviceId}/property`, {
+    request(`/${SystemConst.API_BASE}//device-instance/${deviceId}/property`, {
       method: 'POST',
       data,
     });

+ 7 - 7
src/pages/device/Product/Detail/Access/AccessConfig/index.tsx

@@ -1,9 +1,9 @@
-import { useEffect, useState } from 'react';
-import { Button, Col, message, Modal, Pagination, Row } from 'antd';
-import { service } from '@/pages/link/AccessConfig';
-import { productModel } from '@/pages/device/Product';
+import {useEffect, useState} from 'react';
+import {Button, Col, message, Modal, Pagination, Row} from 'antd';
+import {service} from '@/pages/link/AccessConfig';
+import {productModel} from '@/pages/device/Product';
 import SearchComponent from '@/components/SearchComponent';
-import type { ProColumns } from '@jetlinks/pro-table';
+import type {ProColumns} from '@jetlinks/pro-table';
 import styles from './index.less';
 import Service from '@/pages/device/Product/service';
 
@@ -41,7 +41,7 @@ const AccessConfig = (props: Props) => {
     service
       .queryList({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
       .then((resp) => {
-        setDataSource(resp.result);
+        setDataSource(resp?.result);
       });
   };
 
@@ -140,7 +140,7 @@ const AccessConfig = (props: Props) => {
         </div>
       </div>
       <Row gutter={[16, 16]}>
-        {dataSource.data.map((item: any) => (
+        {(dataSource?.data || []).map((item: any) => (
           <Col
             key={item.name}
             span={12}

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

@@ -1,17 +1,17 @@
-import { Badge, Button, Col, Empty, message, Row, Table, Tooltip } from 'antd';
-import { service } from '@/pages/link/AccessConfig';
-import { productModel, service as productService } from '@/pages/device/Product';
+import {Badge, Button, Col, Empty, message, Row, Table, Tooltip} from 'antd';
+import {service} from '@/pages/link/AccessConfig';
+import {productModel, service as productService} from '@/pages/device/Product';
 import styles from './index.less';
-import type { SetStateAction } from 'react';
-import { useEffect, useState } from 'react';
+import type {SetStateAction} from 'react';
+import {useEffect, useState} from 'react';
 import AccessConfig from './AccessConfig';
 import ReactMarkdown from 'react-markdown';
-import { Form, FormGrid, FormItem, FormLayout, Input, Password, PreviewText } from '@formily/antd';
-import type { ISchema } from '@formily/json-schema';
-import type { ConfigProperty } from '@/pages/device/Product/typings';
-import { createSchemaField } from '@formily/react';
-import { createForm } from '@formily/core';
-import { QuestionCircleOutlined } from '@ant-design/icons';
+import {Form, FormGrid, FormItem, FormLayout, Input, Password, PreviewText} from '@formily/antd';
+import type {ISchema} from '@formily/json-schema';
+import type {ConfigProperty} from '@/pages/device/Product/typings';
+import {createSchemaField} from '@formily/react';
+import {createForm} from '@formily/core';
+import {QuestionCircleOutlined} from '@ant-design/icons';
 import TitleComponent from '@/components/TitleComponent';
 
 const componentMap = {
@@ -59,7 +59,7 @@ const Access = () => {
 
   const queryAccess = (id: string) => {
     service.queryList({ pageSize: 1000 }).then((resp) => {
-      const dt = resp.result.data.find((i: any) => i.id === id);
+      const dt = resp.result?.data.find((i: any) => i.id === id);
       setAccess(dt);
       if (dt) {
         queryNetworkList(dt?.provider);

+ 20 - 12
src/pages/device/Product/Save/index.tsx

@@ -1,11 +1,11 @@
-import { useEffect, useState } from 'react';
-import { service } from '@/pages/device/Product';
-import type { ProductItem } from '@/pages/device/Product/typings';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import { RadioCard, UploadImage } from '@/components';
-import { Col, Form, Input, message, Modal, Row, TreeSelect } from 'antd';
-import { useRequest } from 'umi';
-import { debounce } from 'lodash';
+import {useEffect, useState} from 'react';
+import {service} from '@/pages/device/Product';
+import type {ProductItem} from '@/pages/device/Product/typings';
+import {useIntl} from '@@/plugin-locale/localeExports';
+import {RadioCard, UploadImage} from '@/components';
+import {Col, Form, Input, message, Modal, Row, TreeSelect} from 'antd';
+import {useRequest} from 'umi';
+import {debounce} from 'lodash';
 
 interface Props {
   visible: boolean;
@@ -169,7 +169,7 @@ const Save = (props: Props) => {
             >
               <Input
                 disabled={props.model === 'edit'}
-                placeholder={intlFormat('pages.form.tip.input', '请输入')}
+                placeholder={`${intlFormat('pages.form.tip.input', '请输入')}ID`}
               />
             </Form.Item>
             <Form.Item
@@ -195,7 +195,12 @@ const Save = (props: Props) => {
               ]}
               required
             >
-              <Input placeholder={intlFormat('pages.form.tip.input', '请输入')} />
+              <Input
+                placeholder={
+                  intlFormat('pages.form.tip.input', '请输入') +
+                  intlFormat('pages.table.name', '名称')
+                }
+              />
             </Form.Item>
           </Col>
         </Row>
@@ -210,7 +215,7 @@ const Save = (props: Props) => {
                   });
                 }}
                 filterTreeNode={(input, treeNode) => treeNode.name.includes(input)}
-                placeholder={intlFormat('pages.form.tip.select', '请选择')}
+                placeholder={`${intlFormat('pages.form.tip.select', '请选择')}分类`}
                 fieldNames={{
                   label: 'name',
                   value: 'id',
@@ -264,7 +269,10 @@ const Save = (props: Props) => {
           <Col span={24}>
             <Form.Item label={intlFormat('pages.table.description', '说明')} name={'describe'}>
               <Input.TextArea
-                placeholder={intlFormat('pages.form.tip.input', '请输入')}
+                placeholder={
+                  intlFormat('pages.form.tip.input', '请输入') +
+                  intlFormat('pages.table.description', '说明')
+                }
                 rows={4}
                 style={{ width: '100%' }}
                 maxLength={200}

+ 15 - 15
src/pages/device/Product/index.tsx

@@ -1,28 +1,28 @@
-import { PageContainer } from '@ant-design/pro-layout';
-import { Badge, Button, message, Popconfirm, Space, Tooltip, Upload } from 'antd';
-import type { ProductItem } from '@/pages/device/Product/typings';
+import {PageContainer} from '@ant-design/pro-layout';
+import {Badge, Button, message, Popconfirm, Space, Tooltip, Upload} from 'antd';
+import type {ProductItem} from '@/pages/device/Product/typings';
 import {
   DeleteOutlined,
   DownloadOutlined,
   EditOutlined,
   EyeOutlined,
-  PlayCircleOutlined,
   PlusOutlined,
   StopOutlined,
 } from '@ant-design/icons';
 import Service from '@/pages/device/Product/service';
-import { observer } from '@formily/react';
-import { model } from '@formily/reactive';
-import { useHistory } from 'umi';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import { useEffect, useRef, useState } from 'react';
+import {observer} from '@formily/react';
+import {model} from '@formily/reactive';
+import {useHistory} from 'umi';
+import {useIntl} from '@@/plugin-locale/localeExports';
+import type {ActionType, ProColumns} from '@jetlinks/pro-table';
+import {useEffect, useRef, useState} from 'react';
 import Save from '@/pages/device/Product/Save';
 import SearchComponent from '@/components/SearchComponent';
-import { getButtonPermission, getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
-import { ProTableCard } from '@/components';
+import {getButtonPermission, getMenuPathByParams, MENUS_CODE} from '@/utils/menu';
+import {ProTableCard} from '@/components';
 import ProductCard from '@/components/ProTableCard/CardItems/product';
-import { downloadObject } from '@/utils/util';
+import {downloadObject} from '@/utils/util';
+import AIcon from '../../../components/AIcon';
 
 export const service = new Service('device-product');
 export const statusMap = {
@@ -191,7 +191,7 @@ const Product = observer(() => {
             defaultMessage: record.state ? '禁用' : '启用',
           })}
         >
-          {record.state ? <StopOutlined /> : <PlayCircleOutlined />}
+          {record.state ? <StopOutlined /> : <AIcon type={'icon-fabu'} />}
         </Tooltip>
       </Button>
     </Popconfirm>,
@@ -481,7 +481,7 @@ const Product = observer(() => {
                   type={'link'}
                   disabled={getButtonPermission('device/Product', ['action'])}
                 >
-                  {record.state ? <StopOutlined /> : <PlayCircleOutlined />}
+                  {record.state ? <StopOutlined /> : <AIcon type={'icon-fabu'} />}
                   {intl.formatMessage({
                     id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}`,
                     defaultMessage: record.state ? '禁用' : '启用',

+ 17 - 23
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -1,8 +1,8 @@
-import { Button, Drawer, Dropdown, Menu, message } from 'antd';
-import { createSchemaField, observer } from '@formily/react';
+import {Button, Drawer, Dropdown, Menu, message} from 'antd';
+import {createSchemaField, observer} from '@formily/react';
 import MetadataModel from '../model';
-import type { Field, IFieldState } from '@formily/core';
-import { createForm, registerValidateRules } from '@formily/core';
+import type {Field, IFieldState} from '@formily/core';
+import {createForm, registerValidateRules} from '@formily/core';
 import {
   ArrayItems,
   Editable,
@@ -15,34 +15,28 @@ import {
   Select,
   Space,
 } from '@formily/antd';
-import type { ISchema } from '@formily/json-schema';
-import {
-  DataTypeList,
-  DateTypeList,
-  EventLevel,
-  FileTypeList,
-  PropertySource,
-} from '@/pages/device/data';
-import { useMemo } from 'react';
-import { productModel } from '@/pages/device/Product';
-import { service } from '@/pages/device/components/Metadata';
-import { Store } from 'jetlinks-store';
-import type { MetadataItem } from '@/pages/device/Product/typings';
+import type {ISchema} from '@formily/json-schema';
+import {DataTypeList, DateTypeList, EventLevel, FileTypeList, PropertySource,} from '@/pages/device/data';
+import {useMemo} from 'react';
+import {productModel} from '@/pages/device/Product';
+import {service} from '@/pages/device/components/Metadata';
+import {Store} from 'jetlinks-store';
+import type {MetadataItem} from '@/pages/device/Product/typings';
 
 import JsonParam from '@/components/Metadata/JsonParam';
 import ArrayParam from '@/components/Metadata/ArrayParam';
 import EnumParam from '@/components/Metadata/EnumParam';
 import BooleanEnum from '@/components/Metadata/BooleanParam';
 import ConfigParam from '@/components/Metadata/ConfigParam';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import { lastValueFrom } from 'rxjs';
+import {useIntl} from '@@/plugin-locale/localeExports';
+import {lastValueFrom} from 'rxjs';
 import SystemConst from '@/utils/const';
 import DB from '@/db';
 import _ from 'lodash';
-import { InstanceModel } from '@/pages/device/Instance';
+import {InstanceModel} from '@/pages/device/Instance';
 import FRuleEditor from '@/components/FRuleEditor';
-import { action } from '@formily/reactive';
-import { asyncUpdateMedata, updateMetadata } from '../../metadata';
+import {action} from '@formily/reactive';
+import {asyncUpdateMedata, updateMetadata} from '../../metadata';
 
 interface Props {
   type: 'product' | 'device';
@@ -582,7 +576,7 @@ const Edit = observer((props: Props) => {
             },
           },
           type: {
-            title: '属性类型',
+            title: MetadataModel.type === 'tags' ? '标签类型' : '属性类型',
             required: true,
             'x-decorator': 'FormItem',
             'x-component': 'Select',

+ 44 - 24
src/pages/device/components/Metadata/Base/index.tsx

@@ -1,22 +1,22 @@
-import type { ProColumns } from '@jetlinks/pro-table';
+import type {ProColumns} from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import { useCallback, useEffect, useState } from 'react';
-import { useParams } from 'umi';
+import {useCallback, useEffect, useState} from 'react';
+import {useParams} from 'umi';
 import DB from '@/db';
-import type { MetadataItem, MetadataType } from '@/pages/device/Product/typings';
+import type {MetadataItem, MetadataType} from '@/pages/device/Product/typings';
 import MetadataMapping from './columns';
-import { Button, message, Popconfirm, Tooltip } from 'antd';
-import { DeleteOutlined, EditOutlined, ImportOutlined, PlusOutlined } from '@ant-design/icons';
+import {Button, message, Popconfirm, Tooltip} from 'antd';
+import {DeleteOutlined, EditOutlined, ImportOutlined, PlusOutlined} from '@ant-design/icons';
 import Edit from './Edit';
-import { observer } from '@formily/react';
+import {observer} from '@formily/react';
 import MetadataModel from './model';
-import { Store } from 'jetlinks-store';
+import {Store} from 'jetlinks-store';
 import SystemConst from '@/utils/const';
-import { useIntl } from '@@/plugin-locale/localeExports';
+import {useIntl} from '@@/plugin-locale/localeExports';
 import PropertyImport from '@/pages/device/Product/Detail/PropertyImport';
-import { productModel } from '@/pages/device/Product';
-import { InstanceModel } from '@/pages/device/Instance';
-import { asyncUpdateMedata, removeMetadata } from '../metadata';
+import {productModel} from '@/pages/device/Product';
+import {InstanceModel} from '@/pages/device/Instance';
+import {asyncUpdateMedata, removeMetadata} from '../metadata';
 
 interface Props {
   type: MetadataType;
@@ -51,6 +51,21 @@ const BaseMetadata = observer((props: Props) => {
     }
   };
 
+  const limitsMap = new Map<string, any>();
+  limitsMap.set('events-add', 'eventNotInsertable');
+  limitsMap.set('events-updata', 'eventNotModifiable');
+  limitsMap.set('properties-add', 'propertyNotInsertable');
+  limitsMap.set('properties-updata', 'propertyNotModifiable');
+
+  const operateLimits = (action: 'add' | 'updata', types: MetadataType) => {
+    return (
+      target === 'device' &&
+      (typeMap.get('device')?.features || []).find(
+        (item: { id: string; name: string }) => item.id === limitsMap.get(`${types}-${action}`),
+      )
+    );
+  };
+
   const actions: ProColumns<MetadataItem>[] = [
     {
       title: '操作',
@@ -58,8 +73,10 @@ const BaseMetadata = observer((props: Props) => {
       align: 'center',
       width: 200,
       render: (_: unknown, record: MetadataItem) => [
-        <a
+        <Button
           key="editable"
+          type="link"
+          disabled={operateLimits('updata', type)}
           onClick={() => {
             MetadataModel.edit = true;
             MetadataModel.item = record;
@@ -70,7 +87,7 @@ const BaseMetadata = observer((props: Props) => {
           <Tooltip title="编辑">
             <EditOutlined />
           </Tooltip>
-        </a>,
+        </Button>,
         <a key="delete">
           <Popconfirm
             title="确认删除?"
@@ -144,16 +161,18 @@ const BaseMetadata = observer((props: Props) => {
           },
         }}
         toolBarRender={() => [
-          <Button
-            onClick={() => {
-              MetadataModel.importMetadata = true;
-            }}
-            key="button"
-            icon={<ImportOutlined />}
-            type="ghost"
-          >
-            导入属性
-          </Button>,
+          props.type === 'properties' && (
+            <Button
+              onClick={() => {
+                MetadataModel.importMetadata = true;
+              }}
+              key="button"
+              icon={<ImportOutlined />}
+              type="ghost"
+            >
+              导入属性
+            </Button>
+          ),
           <Button
             onClick={() => {
               MetadataModel.edit = true;
@@ -161,6 +180,7 @@ const BaseMetadata = observer((props: Props) => {
               MetadataModel.type = type;
               MetadataModel.action = 'add';
             }}
+            disabled={operateLimits('add', type)}
             key="button"
             icon={<PlusOutlined />}
             type="primary"

+ 13 - 10
src/pages/device/components/Metadata/Cat/index.tsx

@@ -1,14 +1,15 @@
-import { Drawer, Tabs } from 'antd';
-import { useEffect, useState } from 'react';
-import { productModel, service } from '@/pages/device/Product';
+import {Drawer, Tabs} from 'antd';
+import {useEffect, useState} from 'react';
+import {productModel, service} from '@/pages/device/Product';
 import MonacoEditor from 'react-monaco-editor';
+import {observer} from '@formily/react';
 
 interface Props {
   visible: boolean;
   close: () => void;
 }
 
-const Cat = (props: Props) => {
+const Cat = observer((props: Props) => {
   const [codecs, setCodecs] = useState<{ id: string; name: string }[]>();
   const metadata = productModel.current?.metadata as string;
   const [value, setValue] = useState(metadata);
@@ -23,11 +24,13 @@ const Cat = (props: Props) => {
   const convertMetadata = (key: string) => {
     if (key === 'alink') {
       setValue('');
-      service.convertMetadata('to', 'alink', JSON.parse(metadata)).subscribe({
-        next: (data) => {
-          setValue(JSON.stringify(data));
-        },
-      });
+      if (metadata) {
+        service.convertMetadata('to', 'alink', JSON.parse(metadata)).subscribe({
+          next: (data) => {
+            setValue(JSON.stringify(data));
+          },
+        });
+      }
     } else {
       setValue(metadata);
     }
@@ -65,6 +68,6 @@ const Cat = (props: Props) => {
       </Tabs>
     </Drawer>
   );
-};
+});
 
 export default Cat;

+ 33 - 27
src/pages/link/AccessConfig/index.tsx

@@ -1,19 +1,19 @@
 import SearchComponent from '@/components/SearchComponent';
-import { getButtonPermission, getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
-import { PageContainer } from '@ant-design/pro-layout';
-import type { ProColumns } from '@jetlinks/pro-table';
-import { Button, Card, Col, Empty, message, Pagination, Popconfirm, Row } from 'antd';
-import { useEffect, useState } from 'react';
-import { useHistory } from 'umi';
+import {getButtonPermission, getMenuPathByCode, MENUS_CODE} from '@/utils/menu';
+import {PageContainer} from '@ant-design/pro-layout';
+import type {ProColumns} from '@jetlinks/pro-table';
+import {Button, Card, Col, Empty, message, Pagination, Popconfirm, Row, Tooltip} from 'antd';
+import {useEffect, useState} from 'react';
+import {useHistory} from 'umi';
 import Service from './service';
-import { CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
+import {CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined} from '@ant-design/icons';
 import AccessConfigCard from '@/components/ProTableCard/CardItems/AccessConfig';
 
 export const service = new Service('gateway/device');
 
 const AccessConfig = () => {
   const history = useHistory();
-  const [param, setParam] = useState<any>({ pageSize: 10 });
+  const [param, setParam] = useState<any>({ pageSize: 10, terms: [] });
 
   const columns: ProColumns<any>[] = [
     {
@@ -148,28 +148,34 @@ const AccessConfig = () => {
                         )}
                       </Popconfirm>
                     </Button>,
-                    <Button
+                    <Tooltip
                       key="delete"
-                      type="link"
-                      disabled={getButtonPermission('link/AccessConfig', ['delete'])}
+                      title={
+                        getButtonPermission('link/AccessConfig', ['delete']) ? '没有权限' : '删除'
+                      }
                     >
-                      <Popconfirm
-                        title={'确认删除?'}
-                        onConfirm={() => {
-                          service.remove(item.id).then((resp: any) => {
-                            if (resp.status === 200) {
-                              message.success('操作成功!');
-                              handleSearch(param);
-                            } else {
-                              message.error(resp.message);
-                            }
-                          });
-                        }}
+                      <Button
+                        type="link"
+                        disabled={getButtonPermission('link/AccessConfig', ['delete'])}
                       >
-                        <DeleteOutlined />
-                        删除
-                      </Popconfirm>
-                    </Button>,
+                        <Popconfirm
+                          title={'确认删除?'}
+                          onConfirm={() => {
+                            service.remove(item.id).then((resp: any) => {
+                              if (resp.status === 200) {
+                                message.success('操作成功!');
+                                handleSearch(param);
+                              } else {
+                                message.error(resp?.message || '操作失败');
+                              }
+                            });
+                          }}
+                        >
+                          <DeleteOutlined />
+                          删除
+                        </Popconfirm>
+                      </Button>
+                    </Tooltip>,
                   ]}
                 />
               </Col>

+ 11 - 11
src/pages/link/Type/Detail/index.tsx

@@ -1,5 +1,5 @@
-import { PageContainer } from '@ant-design/pro-layout';
-import { createSchemaField, observer } from '@formily/react';
+import {PageContainer} from '@ant-design/pro-layout';
+import {createSchemaField, observer} from '@formily/react';
 import {
   ArrayCollapse,
   Form,
@@ -14,17 +14,17 @@ import {
   Select,
   Submit,
 } from '@formily/antd';
-import type { ISchema } from '@formily/json-schema';
-import { useEffect, useMemo, useRef } from 'react';
-import type { Field } from '@formily/core';
-import { createForm, onFieldValueChange } from '@formily/core';
-import { Card, message } from 'antd';
+import type {ISchema} from '@formily/json-schema';
+import {useEffect, useMemo, useRef} from 'react';
+import type {Field} from '@formily/core';
+import {createForm, onFieldValueChange} from '@formily/core';
+import {Card, message} from 'antd';
 import styles from './index.less';
-import { useAsyncDataSource } from '@/utils/util';
-import { service } from '../index';
+import {useAsyncDataSource} from '@/utils/util';
+import {service} from '../index';
 import _ from 'lodash';
 import FAutoComplete from '@/components/FAutoComplete';
-import { Store } from 'jetlinks-store';
+import {Store} from 'jetlinks-store';
 
 /**
  *  根据类型过滤配置信息
@@ -776,7 +776,7 @@ const Save = observer(() => {
     }
   };
   return (
-    <PageContainer onBack={() => history.back()}>
+    <PageContainer className={'page-title-show'} onBack={() => history.back()}>
       <Card>
         <Form form={form} layout="vertical" style={{ padding: 30 }}>
           <SchemaField

+ 102 - 23
src/pages/link/Type/index.tsx

@@ -1,22 +1,17 @@
-import { useRef, useState } from 'react';
-import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import ProTable from '@jetlinks/pro-table';
-import { Badge, Button, message, Popconfirm, Tooltip } from 'antd';
-import {
-  CloseCircleOutlined,
-  DeleteOutlined,
-  EditOutlined,
-  PlayCircleOutlined,
-  PlusOutlined,
-} from '@ant-design/icons';
-import { PageContainer } from '@ant-design/pro-layout';
-import type { NetworkItem } from '@/pages/link/Type/typings';
-import { useIntl } from '@@/plugin-locale/localeExports';
+import {useRef, useState} from 'react';
+import type {ActionType, ProColumns} from '@jetlinks/pro-table';
+import {Badge, Button, message, Popconfirm, Tooltip} from 'antd';
+import {CloseCircleOutlined, DeleteOutlined, EditOutlined, PlayCircleOutlined, PlusOutlined,} from '@ant-design/icons';
+import {PageContainer} from '@ant-design/pro-layout';
+import type {NetworkItem} from '@/pages/link/Type/typings';
+import {useIntl} from '@@/plugin-locale/localeExports';
 import SearchComponent from '@/components/SearchComponent';
-import { getButtonPermission, getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
-import { history } from 'umi';
+import {getButtonPermission, getMenuPathByParams, MENUS_CODE} from '@/utils/menu';
+import {history} from 'umi';
 import Service from '@/pages/link/service';
-import { Store } from 'jetlinks-store';
+import {Store} from 'jetlinks-store';
+import {ProTableCard} from '@/components';
+import NetworkCard from '@/components/ProTableCard/CardItems/networkCard';
 
 export const service = new Service('network/config');
 
@@ -29,6 +24,16 @@ const pageJump = (id?: string) => {
   history.push(`${getMenuPathByParams(MENUS_CODE['link/Type/Detail'], id)}`);
 };
 
+export const networkMap = {
+  UDP: 'udp://',
+  TCP_SERVER: 'tcp://',
+  WEB_SOCKET_SERVER: 'ws://',
+  MQTT_CLIENT: 'mqtt://',
+  HTTP_SERVER: 'http://',
+  MQTT_SERVER: 'mqtt://',
+  COAP_SERVER: 'coap://',
+};
+
 const Network = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
@@ -60,11 +65,12 @@ const Network = () => {
         if (record.shareCluster) {
           const publicHost = record.configuration.publicHost;
           const publicPort = record.configuration.publicPort;
-          return (
+          return publicHost ? (
             <>
-              公网: {publicHost}:{publicPort}
+              {networkMap[record.type]}
+              {publicHost}:{publicPort}
             </>
-          );
+          ) : null;
         } else {
           const log = record.cluster?.map(
             (item) => `${item.configuration.publicHost}:${item.configuration.publicPort}`,
@@ -72,7 +78,9 @@ const Network = () => {
           return (
             <>
               {log.map((item) => (
-                <div key={item}>公网:{item}</div>
+                <div key={item}>
+                  `${networkMap[record.type]}${item}`
+                </div>
               ))}
             </>
           );
@@ -114,7 +122,7 @@ const Network = () => {
             pageJump(record.id);
           }}
         >
-          <Tooltip title="查看">
+          <Tooltip title="编辑">
             <EditOutlined />
           </Tooltip>
         </Button>,
@@ -198,7 +206,7 @@ const Network = () => {
           setParam(data);
         }}
       />
-      <ProTable<NetworkItem>
+      <ProTableCard<NetworkItem>
         actionRef={actionRef}
         params={param}
         columns={columns}
@@ -222,6 +230,77 @@ const Network = () => {
         request={async (params) =>
           service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
         }
+        gridColumn={3}
+        cardRender={(record) => (
+          <NetworkCard
+            {...record}
+            actions={[
+              <Button
+                key="edit"
+                onClick={() => {
+                  Store.set('current-network-data', record);
+                  pageJump(record.id);
+                }}
+              >
+                <EditOutlined />
+                编辑
+              </Button>,
+              <Tooltip title={record.state.value === 'enabled' ? '禁用' : '启用'}>
+                <Popconfirm
+                  disabled={getButtonPermission('link/Type', ['action'])}
+                  title={`确认${record.state.value === 'enabled' ? '禁用' : '启用'}?`}
+                  onConfirm={async () => {
+                    // await service.update({
+                    //   id: record.id,
+                    //   status: record.status ? 0 : 1,
+                    // });
+                    const map = {
+                      disabled: 'start',
+                      enabled: 'shutdown',
+                    };
+                    await service.changeState(record.id, map[record.state.value]);
+                    message.success(
+                      intl.formatMessage({
+                        id: 'pages.data.option.success',
+                        defaultMessage: '操作成功!',
+                      }),
+                    );
+                    actionRef.current?.reload();
+                  }}
+                >
+                  <Button
+                    type="link"
+                    style={{ padding: 0 }}
+                    disabled={getButtonPermission('link/Type', ['action'])}
+                    key="changeState"
+                  >
+                    {record.state.value === 'enabled' ? (
+                      <CloseCircleOutlined />
+                    ) : (
+                      <PlayCircleOutlined />
+                    )}
+                    {record.state.value === 'enabled' ? '禁用' : '启用'}
+                  </Button>
+                </Popconfirm>
+              </Tooltip>,
+              <Popconfirm
+                key="delete"
+                title="确认删除?"
+                onConfirm={async () => {
+                  const response: any = await service.remove(record.id);
+                  if (response.status === 200) {
+                    message.success('删除成功');
+                    actionRef.current?.reload();
+                  }
+                }}
+              >
+                <Button key="delete">
+                  <DeleteOutlined />
+                </Button>
+              </Popconfirm>,
+            ]}
+          />
+        )}
       />
     </PageContainer>
   );

+ 21 - 10
src/pages/media/Cascade/Channel/index.tsx

@@ -19,6 +19,7 @@ const Channel = () => {
   const [selectedRowKey, setSelectedRowKey] = useState<string[]>([]);
   const id = location?.query?.id || '';
   const [data, setData] = useState<string>('');
+  const [popVisible, setPopvisible] = useState<string>('');
 
   const unbind = async (list: string[]) => {
     const resp = await service.unbindChannel(id, list);
@@ -43,9 +44,11 @@ const Channel = () => {
           style={{ marginTop: 10, width: '100%' }}
           onClick={async () => {
             if (!!data) {
-              const resp: any = service.editBindInfo(record.id, { gbChannelId: data });
+              const resp: any = await service.editBindInfo(record.id, { gbChannelId: data });
               if (resp.status === 200) {
+                message.success('操作成功');
                 actionRef.current?.reload();
+                setPopvisible('');
               }
             } else {
               message.error('请输入国标ID');
@@ -68,17 +71,23 @@ const Channel = () => {
       title: '通道名称',
     },
     {
-      dataIndex: 'channelId',
+      dataIndex: 'gbChannelId',
       title: '国标ID',
       tooltip: '国标级联有18位、20位两种格式。在当前页面修改不会修改视频设备-通道页面中的国标ID',
       render: (text: any, record: any) => (
         <span>
           {text}
-          <Popover trigger="click" content={content(record)} title="编辑通道ID">
+          <Popover
+            visible={popVisible === record.id}
+            trigger="click"
+            content={content(record)}
+            title="编辑国标ID"
+          >
             <a
               style={{ marginLeft: 10 }}
               onClick={() => {
                 setData('');
+                setPopvisible(record.id);
               }}
             >
               <EditOutlined />
@@ -119,7 +128,7 @@ const Channel = () => {
           key={'unbinds'}
           title="确认解绑"
           onConfirm={() => {
-            unbind([record.id]);
+            unbind([record.channelId]);
           }}
         >
           <a>
@@ -136,7 +145,7 @@ const Channel = () => {
     <PageContainer>
       <SearchComponent<any>
         field={columns}
-        target="unbind-channel"
+        target="bind-channel"
         onSearch={(params) => {
           actionRef.current?.reload();
           setParam({
@@ -157,7 +166,7 @@ const Channel = () => {
             sorts: [{ name: 'createTime', order: 'desc' }],
           })
         }
-        rowKey="id"
+        rowKey="channelId"
         rowSelection={{
           selectedRowKeys: selectedRowKey,
           onChange: (selectedRowKeys) => {
@@ -195,18 +204,20 @@ const Channel = () => {
           >
             绑定通道
           </Button>,
-          <Button
-            onClick={() => {
+          <Popconfirm
+            title={'确认解绑'}
+            onConfirm={() => {
               if (selectedRowKey.length > 0) {
                 unbind(selectedRowKey);
+                setSelectedRowKey([]);
               } else {
                 message.error('请先选择需要解绑的通道列表');
               }
             }}
             key="unbind"
           >
-            批量解绑
-          </Button>,
+            <Button>批量解绑</Button>
+          </Popconfirm>,
         ]}
       />
       {visible && (

+ 26 - 12
src/pages/media/Cascade/index.tsx

@@ -68,21 +68,29 @@ const Cascade = () => {
         选择通道
       </Tooltip>
     </Button>,
-    <Button type={'link'} key={'share'} disabled={record.status.value === 'disabled'}>
-      <Popconfirm
+    <Tooltip
+      key={'share'}
+      title={record.status.value === 'disabled' ? '禁用状态下不可推送' : '推送'}
+    >
+      <Button
+        type={'link'}
         key={'share'}
-        title="确认推送!"
-        onConfirm={() => {
-          setCurrent(record);
-          setVisible(true);
-        }}
+        disabled={
+          getButtonPermission('media/Cascade', ['push']) || record.status.value === 'disabled'
+        }
       >
-        <Tooltip title={record.status.value === 'disabled' ? '禁用状态下不可推送' : '推送'}>
+        <Popconfirm
+          title="确认推送!"
+          onConfirm={() => {
+            setCurrent(record);
+            setVisible(true);
+          }}
+        >
           <ShareAltOutlined />
           推送
-        </Tooltip>
-      </Popconfirm>
-    </Button>,
+        </Popconfirm>
+      </Button>
+    </Tooltip>,
     <Button
       type={'link'}
       key={'operate'}
@@ -262,7 +270,13 @@ const Cascade = () => {
           title={record.status.value === 'disabled' ? '禁用状态下不可推送' : '推送'}
           key={'share'}
         >
-          <Button type="link" style={{ padding: 0 }} disabled={record.status.value === 'disabled'}>
+          <Button
+            type="link"
+            style={{ padding: 0 }}
+            disabled={
+              getButtonPermission('media/Cascade', ['push']) || record.status.value === 'disabled'
+            }
+          >
             <Popconfirm
               onConfirm={() => {
                 setVisible(true);

+ 49 - 32
src/pages/media/Device/Channel/index.tsx

@@ -20,7 +20,7 @@ import Save from './Save';
 import Service from './service';
 import { ProviderValue } from '../index';
 import Live from './Live';
-import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import { getButtonPermission, getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 import Tree from './Tree';
 
 export const service = new Service('media');
@@ -128,14 +128,15 @@ export default () => {
             defaultMessage: '编辑',
           })}
         >
-          <a
+          <Button
             onClick={() => {
               setCurrent(record);
               setVisible(true);
             }}
+            disabled={getButtonPermission('media/Device', 'update')}
           >
             <EditOutlined />
-          </a>
+          </Button>
         </Tooltip>,
         <Tooltip key={'live'} title={'播发'}>
           <a
@@ -161,22 +162,27 @@ export default () => {
           </a>
         </Tooltip>,
         type === ProviderValue.FIXED ? (
-          <Tooltip key={'updateChannel'} title="删除">
-            <Popconfirm
-              key="delete"
-              title={intl.formatMessage({
-                id: 'page.table.isDelete',
-                defaultMessage: '是否删除?',
-              })}
-              onConfirm={async () => {
-                deleteItem(record.id);
-              }}
-            >
-              <Button type={'link'} style={{ padding: '4px' }}>
+          <Popconfirm
+            key="delete"
+            title={intl.formatMessage({
+              id: 'page.table.isDelete',
+              defaultMessage: '是否删除?',
+            })}
+            onConfirm={async () => {
+              deleteItem(record.id);
+            }}
+            disabled={getButtonPermission('media/Device', 'delete')}
+          >
+            <Tooltip title="删除">
+              <Button
+                type={'link'}
+                style={{ padding: 0 }}
+                disabled={getButtonPermission('media/Device', 'delete')}
+              >
                 <DeleteOutlined />
               </Button>
-            </Popconfirm>
-          </Tooltip>
+            </Tooltip>
+          </Popconfirm>
         ) : null,
       ],
     },
@@ -232,21 +238,32 @@ export default () => {
             rowKey="id"
             search={false}
             headerTitle={[
-              <Button
-                onClick={() => {
-                  setCurrent(undefined);
-                  setVisible(true);
-                }}
-                key="button"
-                disabled={type === ProviderValue.GB281}
-                icon={<PlusOutlined />}
-                type="primary"
-              >
-                {intl.formatMessage({
-                  id: 'pages.data.option.add',
-                  defaultMessage: '新增',
-                })}
-              </Button>,
+              type === ProviderValue.GB281 ? (
+                <Tooltip key="button" title={'接入方式为GB/T28281时,不支持新增'}>
+                  <Button disabled>
+                    {intl.formatMessage({
+                      id: 'pages.data.option.add',
+                      defaultMessage: '新增',
+                    })}
+                  </Button>
+                </Tooltip>
+              ) : (
+                <Button
+                  onClick={() => {
+                    setCurrent(undefined);
+                    setVisible(true);
+                  }}
+                  key="button"
+                  disabled={getButtonPermission('media/Device', 'add')}
+                  icon={<PlusOutlined />}
+                  type="primary"
+                >
+                  {intl.formatMessage({
+                    id: 'pages.data.option.add',
+                    defaultMessage: '新增',
+                  })}
+                </Button>
+              ),
             ]}
           />
         </div>

+ 20 - 4
src/pages/media/Device/Save/index.tsx

@@ -178,7 +178,23 @@ export default (props: SaveProps) => {
                 label={'ID'}
                 name={'id'}
                 required
-                rules={[{ required: true, message: '请输入ID' }, {}]}
+                rules={[
+                  { required: true, message: '请输入ID' },
+                  {
+                    pattern: /^[a-zA-Z0-9_\-]+$/,
+                    message: intl.formatMessage({
+                      id: 'pages.form.tip.id',
+                      defaultMessage: '请输入英文或者数字或者-或者_',
+                    }),
+                  },
+                  {
+                    max: 64,
+                    message: intl.formatMessage({
+                      id: 'pages.form.tip.max64',
+                      defaultMessage: '最多输入64个字符',
+                    }),
+                  },
+                ]}
               >
                 <Input placeholder={'请输入ID'} disabled={props.model === 'edit'} />
               </Form.Item>
@@ -213,11 +229,11 @@ export default (props: SaveProps) => {
                     placeholder={'请选择所属产品'}
                     style={{ width: props.model === 'edit' ? '100%' : 'calc(100% - 36px)' }}
                     onSelect={(_: any, node: any) => {
-                      const pasd = node.configuration ? node.configuration.access_pwd : '';
+                      const pwd = node.configuration ? node.configuration.access_pwd : '';
                       form.setFieldsValue({
-                        password: pasd,
+                        password: pwd,
                       });
-                      setOldPassword(pasd);
+                      setOldPassword(pwd);
                     }}
                   />
                 </Form.Item>

+ 14 - 15
src/pages/media/Device/index.tsx

@@ -127,27 +127,26 @@ const Device = () => {
         defaultMessage: '设备厂家',
       }),
     },
-    {
-      dataIndex: 'model',
-      title: intl.formatMessage({
-        id: 'pages.media.device.model',
-        defaultMessage: '型号',
-      }),
-    },
-    {
-      dataIndex: 'firmware',
-      title: intl.formatMessage({
-        id: 'pages.media.device.firmware',
-        defaultMessage: '固件版本',
-      }),
-    },
+    // {
+    //   dataIndex: 'model',
+    //   title: intl.formatMessage({
+    //     id: 'pages.media.device.model',
+    //     defaultMessage: '型号',
+    //   }),
+    // },
+    // {
+    //   dataIndex: 'firmware',
+    //   title: intl.formatMessage({
+    //     id: 'pages.media.device.firmware',
+    //     defaultMessage: '固件版本',
+    //   }),
+    // },
     {
       dataIndex: 'state',
       title: intl.formatMessage({
         id: 'pages.searchTable.titleStatus',
         defaultMessage: '状态',
       }),
-      valueType: 'select',
       render: (_, record) => (
         <BadgeStatus
           status={record.state.value}

+ 1 - 1
src/pages/notice/Config/Debug/index.tsx

@@ -164,7 +164,7 @@ const Debug = observer(() => {
     const list = Store.get('notice-template-list');
     const _template = list.find((item: any) => item.id === templateId);
 
-    const resp = await service.debug(state?.current.id, {
+    const resp = await service.debug(state?.current.id, templateId, {
       template: _template,
       context: data.variableDefinitions?.reduce(
         (previousValue: any, currentValue: { id: any; value: any }) => {

+ 1 - 1
src/pages/notice/Config/Log/index.tsx

@@ -57,7 +57,7 @@ const Log = observer(() => {
       visible={state.log && !!state.current?.id}
     >
       <SearchComponent
-        defaultParam={[{ column: 'type$IN', value: id }]}
+        defaultParam={[{ column: 'notifyType$IN', value: id }]}
         field={columns}
         onSearch={(data) => {
           actionRef.current?.reset?.();

+ 4 - 2
src/pages/notice/Config/index.tsx

@@ -242,7 +242,9 @@ const Config = observer(() => {
           </Space>
         }
         gridColumn={3}
-        request={async (params) => service.query(params)}
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
         cardRender={(record) => (
           <NoticeConfig
             {...record}
@@ -308,7 +310,7 @@ const Config = observer(() => {
         )}
       />
       <Debug />
-      <Log />
+      {state.log && <Log />}
     </PageContainer>
   );
 });

+ 2 - 2
src/pages/notice/Config/service.ts

@@ -28,8 +28,8 @@ class Service extends BaseService<ConfigItem> {
       data,
     });
 
-  public debug = (id: string, data: Record<string, any>) =>
-    request(`${SystemConst.API_BASE}/notifier/${id}/_send`, {
+  public debug = (id: string, templateId: string, data: Record<string, any>) =>
+    request(`${SystemConst.API_BASE}/notifier/${id}/${templateId}/_send`, {
       method: 'POST',
       data,
     });

+ 6 - 3
src/pages/notice/Template/Debug/index.tsx

@@ -86,7 +86,10 @@ const Debug = observer(() => {
   const getConfig = () =>
     configService
       .queryNoPagingPost({
-        terms: [{ column: 'type$IN', value: id }],
+        terms: [
+          { column: 'type$IN', value: id },
+          { column: 'provider', value: state.current?.provider },
+        ],
       })
       .then((resp: any) => {
         // 缓存通知配置
@@ -166,10 +169,10 @@ const Debug = observer(() => {
   };
 
   const start = async () => {
-    const data: { templateId: string; variableDefinitions: any } = await form.submit();
+    const data: { configId: string; variableDefinitions: any } = await form.submit();
     // 应该取选择的配置信息
     if (!state.current) return;
-    const resp = await service.debug(state?.current.id, {
+    const resp = await service.debug(data.configId, state?.current.id, {
       template: state.current,
       context: data.variableDefinitions?.reduce(
         (previousValue: any, currentValue: { id: any; value: any }) => {

+ 1 - 11
src/pages/notice/Template/Detail/doc/DingTalkRebot.tsx

@@ -1,9 +1,4 @@
-import { Image } from 'antd';
-
 const DingTalkRebot = () => {
-  const text = require('/public/images/notice/doc/template/dingTalk-rebot/01-text.jpg');
-  const markdown = require('/public/images/notice/doc/template/dingTalk-rebot/02-markdown.jpg');
-  const link = require('/public/images/notice/doc/template/dingTalk-rebot/03-link.jpg');
   const b = '{name}';
   return (
     <div>
@@ -26,12 +21,7 @@ const DingTalkRebot = () => {
         <div> 1、绑定配置</div>
         <div> 绑定通知配置</div>
         <div> 2、消息类型</div>
-        <div> 目前支持text、markdown、link3种,对应的发送效果示例,如下图:</div>
-        <div>
-          <Image width="100%" src={text} />
-          <Image width="100%" src={markdown} />
-          <Image width="100%" src={link} />
-        </div>
+        <div> 目前支持text、markdown、link3种。</div>
         <div> 3. 模板内容</div>
         <div>
           支持填写带变量的动态模板。变量填写规范示例:${b}

+ 355 - 202
src/pages/notice/Template/Detail/index.tsx

@@ -36,6 +36,7 @@ import DingTalkRebot from '@/pages/notice/Template/Detail/doc/DingTalkRebot';
 import AliyunVoice from '@/pages/notice/Template/Detail/doc/AliyunVoice';
 import AliyunSms from '@/pages/notice/Template/Detail/doc/AliyunSms';
 import Email from '@/pages/notice/Template/Detail/doc/Email';
+import { Store } from 'jetlinks-store';
 
 export const docMap = {
   weixin: {
@@ -63,6 +64,37 @@ const Detail = observer(() => {
   // 正则提取${}里面的值
   const pattern = /(?<=\$\{).*?(?=\})/g;
 
+  const getConfig = (provider1: string) =>
+    configService
+      .queryNoPagingPost({
+        terms: [
+          { column: 'type$IN', value: id },
+          { column: 'provider', value: provider1 },
+        ],
+      })
+      .then((resp: any) => {
+        return resp.result?.map((item: any) => ({
+          label: item.name,
+          value: item.id,
+        }));
+      });
+
+  //需要复杂联动才可以完成
+  const getWeixinDept = (configId: string) => service.weixin.getDepartments(configId);
+  const getWeixinTags = (configId: string) => service.weixin.getTags(configId);
+  const getWeixinUser = (configId: string) => service.weixin.getUser(configId);
+
+  const getDingTalkDept = (configId: string) => service.dingTalk.getDepartments(configId);
+  const getDingTalkDeptTree = (configId: string) => service.dingTalk.getDepartmentsTree(configId);
+  const getDingTalkUser = (configId: string) => service.dingTalk.getUser(configId);
+
+  const getWeixinOfficialTags = (configId: string) => service.weixin.getOfficialTags(configId);
+  const getWeixinOfficialTemplates = (configId: string) =>
+    service.weixin.getOfficialTemplates(configId);
+
+  const getAliyunSigns = (configId: string) => service.aliyun.getSigns(configId);
+  const getAliyunTemplates = (configId: string) => service.aliyun.getTemplates(configId);
+
   const form = useMemo(
     () =>
       createForm({
@@ -70,14 +102,111 @@ const Detail = observer(() => {
         effects() {
           onFieldInit('template.message', (field) => {
             if (id === 'email') {
-              field.setComponent(FBraftEditor);
+              field.setComponent(FBraftEditor, {
+                placeholder:
+                  '变量格式:${name};\n 示例:尊敬的${name},${time}有设备触发告警,请注意处理',
+              });
             }
           });
           onFieldValueChange('provider', (field, form1) => {
             const value = field.value;
             setProvider(value);
-            form1.setValuesIn('configId', null);
-            form1.setValuesIn('template', null);
+            // form1.setValuesIn('configId', null);
+            // form1.setValuesIn('template', null);
+            // 设置绑定配置的数据
+            form1.setFieldState('configId', async (state1) => {
+              state1.dataSource = await getConfig(value);
+            });
+          });
+          onFieldValueChange('configId', (field, form1) => {
+            const value = field.value;
+            // 判断provider
+            if (!value) return;
+            switch (form1.values.provider) {
+              case 'corpMessage':
+                form1.setFieldState('template.toUser', async (state8) => {
+                  state8.dataSource = await getWeixinUser(value);
+                });
+                form1.setFieldState('template.toParty', async (state9) => {
+                  state9.dataSource = await getWeixinDept(value);
+                });
+                form1.setFieldState('template.toTag', async (state10) => {
+                  state10.dataSource = await getWeixinTags(value);
+                });
+                break;
+              case 'officialMessage':
+                form1.setFieldState('template.tagid', async (state1) => {
+                  state1.dataSource = await getWeixinOfficialTags(value);
+                });
+                form1.setFieldState('template.wxTemplateId', async (state2) => {
+                  const list = await getWeixinOfficialTemplates(value);
+                  Store.set('wxTemplate', list);
+                  state2.dataSource = list;
+                });
+                break;
+              case 'dingTalkMessage':
+                form1.setFieldState('template.userIdList', async (state3) => {
+                  state3.dataSource = await getDingTalkUser(value);
+                });
+                form1.setFieldState('template.departmentIdList', async (state4) => {
+                  const list = await getDingTalkDept(value);
+                  Store.set('wxTemplate', list);
+                  state4.dataSource = list;
+                });
+                break;
+              case 'aliyun':
+                // 阿里云语音
+                form1.setFieldState('template.ttsCode', async (state5) => {
+                  const list = await getAliyunTemplates(value);
+                  Store.set('AliyunTemplate', list);
+                  state5.dataSource = list;
+                });
+                break;
+              case 'aliyunSms':
+                // 阿里云短信
+                form1.setFieldState('template.code', async (state6) => {
+                  const list = await getAliyunTemplates(value);
+                  Store.set('AliyunTemplate', list);
+                  state6.dataSource = list;
+                });
+
+                form1.setFieldState('template.signName', async (state7) => {
+                  // const list =
+                  // Store.set('AliyunTemplate', list);
+                  state7.dataSource = await getAliyunSigns(value);
+                });
+
+                break;
+              default:
+                break;
+            }
+          });
+          onFieldValueChange('template.wxTemplateId', (field, form1) => {
+            const value = field.value;
+            // 处理消息模版。
+            const template = Store.get('wxTemplate');
+            const data = template?.find((i: { id: any }) => i.id === value);
+            if (data) {
+              form1.setFieldState('template.title', (state1) => {
+                state1.value = data.title;
+                state1.disabled = true;
+              });
+              form1.setFieldState('template.message', (state1) => {
+                state1.value = data.content;
+                state1.disabled = true;
+              });
+            }
+          });
+          onFieldValueChange('template.code', (field, form1) => {
+            const value = field.value;
+            const template = Store.get('AliyunTemplate');
+            const data = template?.find((i: { templateCode: any }) => i.templateCode === value);
+            if (data) {
+              form1.setFieldState('template.message', (state1) => {
+                state1.value = data.templateContent;
+                state1.disabled = true;
+              });
+            }
           });
           onFieldValueChange('template.message', (field, form1) => {
             const value = (field as Field).value;
@@ -134,38 +263,6 @@ const Detail = observer(() => {
     [id],
   );
 
-  const getConfig = () =>
-    configService
-      .queryNoPagingPost({
-        terms: [
-          { column: 'type$IN', value: id },
-          { column: 'provider', value: form.values?.provider },
-        ],
-      })
-      .then((resp: any) => {
-        return resp.result?.map((item: any) => ({
-          label: item.name,
-          value: item.id,
-        }));
-      });
-
-  const getDingTalkDept = (configId: string) => service.dingTalk.getDepartments(configId);
-  const getDingTalkDeptTree = (configId: string) => service.dingTalk.getDepartmentsTree(configId);
-  const getDingTalkUser = (configId: string, departmentId: string) =>
-    service.dingTalk.getUserByDepartment(configId, departmentId);
-
-  //需要复杂联动才可以完成
-  const getWeixinDept = () => service.weixin.getDepartments(form?.values.configId);
-  const getWeixinTags = () => service.weixin.getTags(form?.values.configId);
-  const getWeixinUser = () => service.weixin.getUser(form?.values.configId);
-
-  const getWeixinOfficialTags = (configId: string) => service.weixin.getOfficialTags(configId);
-  const getWeixinOfficialTemplates = (configId: string) =>
-    service.weixin.getOfficialTemplates(configId);
-
-  const getAliyunSigns = (configId: string) => service.aliyun.getSigns(configId);
-  const getAliyunTemplates = (configId: string) => service.aliyun.getTemplates(configId);
-
   useEffect(() => {
     if (state.current) {
       form.setValues(state.current);
@@ -201,6 +298,8 @@ const Detail = observer(() => {
     // r如果是text 的话。template.message=>template.text.content
     // 如果是markdown 的话。 template.message=>template.markdown.text
     // 如果是link的话。 template.message =>template.markdown.text
+
+    // 微信服务号: template.message =>template.content
     if (data.provider === 'dingTalkRobotWebHook') {
       const type = data.template.messageType;
       // emplate.messageType
@@ -284,7 +383,7 @@ const Detail = observer(() => {
         //   {label: '测试配置2', value: 'test2'},
         //   {label: '测试配置3', value: 'test3'},
         // ],
-        'x-reactions': '{{useAsyncDataSource(getConfig)}}',
+        // 'x-reactions': '{{useAsyncDataSource(getConfig)}}',
         'x-visible': id !== 'email',
       },
       template: {
@@ -308,34 +407,37 @@ const Detail = observer(() => {
                       placeholder: '请输入AgentID',
                     },
                   },
-                  toUser: {
-                    title: '收信人ID',
-                    'x-component': 'Select',
-                    'x-decorator': 'FormItem',
-                    'x-decorator-props': {
-                      tooltip: '请输入收信人ID',
-                    },
-                    'x-component-props': {
-                      placeholder: '请输入收信人ID',
-                      mode: 'tags',
-                    },
-                    'x-reactions': '{{useAsyncDataSource(getWeixinUser)}}',
-                  },
-                  toParty: {
-                    title: '收信部门ID',
-                    'x-component': 'Select',
-                    'x-decorator': 'FormItem',
+                  layout: {
+                    type: 'void',
+                    'x-decorator': 'FormGrid',
                     'x-decorator-props': {
-                      tooltip: '请输入收信部门ID',
+                      maxColumns: 2,
+                      minColumns: 2,
                     },
-                    'x-component-props': {
-                      placeholder: '请输入收信部门ID',
-                      mode: 'tags',
-                    },
-                    'x-reactions': {
-                      dependencies: ['configId'],
-                      fulfill: {
-                        run: '{{useAsyncDataSource(getWeixinDept($deps[0]))}}',
+                    properties: {
+                      toUser: {
+                        title: '收信人',
+                        'x-component': 'Select',
+                        'x-decorator': 'FormItem',
+                        'x-decorator-props': {
+                          tooltip: '请输入收信人ID',
+                          gridSpan: 1,
+                        },
+                        'x-component-props': {
+                          placeholder: '请输入收信人ID',
+                        },
+                      },
+                      toParty: {
+                        title: '收信部门',
+                        'x-component': 'Select',
+                        'x-decorator': 'FormItem',
+                        'x-decorator-props': {
+                          tooltip: '请输入收信部门ID',
+                          gridSpan: 1,
+                        },
+                        'x-component-props': {
+                          placeholder: '请输入收信部门ID',
+                        },
                       },
                     },
                   },
@@ -348,13 +450,6 @@ const Detail = observer(() => {
                     },
                     'x-component-props': {
                       placeholder: '请输入标签推送,多个标签用,号分隔',
-                      mode: 'tags',
-                    },
-                    'x-reactions': {
-                      dependencies: ['configId'],
-                      fulfill: {
-                        run: '{{useAsyncDataSource(getWeixinTags($deps[0]))}}',
-                      },
                     },
                   },
                 },
@@ -387,35 +482,39 @@ const Detail = observer(() => {
                     'x-component-props': {
                       placeholder: '请选择用户标签',
                     },
-                    'x-reactions': {
-                      dependencies: ['configId'],
-                      fulfill: {
-                        run: '{{useAsyncDataSource(getWeixinOfficialTags($deps[0]))}}',
-                      },
-                    },
                   },
-                  wxTemplateId: {
-                    title: '消息模版',
-                    type: 'string',
-                    'x-decorator': 'FormItem',
-                    'x-component': 'Select',
-                    'x-component-props': {
-                      placeholder: '请选择消息模版',
+                  layout: {
+                    type: 'void',
+                    'x-decorator': 'FormGrid',
+                    'x-decorator-props': {
+                      maxColumns: 2,
+                      minColumns: 2,
                     },
-                    'x-reactions': {
-                      dependencies: ['configId'],
-                      fulfill: {
-                        run: '{{useAsyncDataSource(getWeixinOfficialTemplates($deps[0]))}}',
+                    properties: {
+                      wxTemplateId: {
+                        title: '消息模版',
+                        type: 'string',
+                        'x-decorator': 'FormItem',
+                        'x-component': 'Select',
+                        'x-component-props': {
+                          placeholder: '请选择消息模版',
+                        },
+                        'x-decorator-props': {
+                          gridSpan: 1,
+                        },
+                      },
+                      url: {
+                        title: '模版跳转链接',
+                        type: 'string',
+                        'x-decorator': 'FormItem',
+                        'x-component': 'Input',
+                        'x-component-props': {
+                          placeholder: '请输入模版跳转链接',
+                        },
+                        'x-decorator-props': {
+                          gridSpan: 1,
+                        },
                       },
-                    },
-                  },
-                  url: {
-                    title: '模版跳转链接',
-                    type: 'string',
-                    'x-decorator': 'FormItem',
-                    'x-component': 'Input',
-                    'x-component-props': {
-                      placeholder: '请输入模版跳转链接',
                     },
                   },
                   toMiniProgram: {
@@ -435,22 +534,38 @@ const Detail = observer(() => {
                   miniProgram: {
                     type: 'void',
                     properties: {
-                      miniProgramId: {
-                        title: '跳转小程序AppId',
-                        type: 'string',
-                        'x-decorator': 'FormItem',
-                        'x-component': 'Input',
-                        'x-component-props': {
-                          placeholder: '请输入跳转小程序AppId',
+                      layout: {
+                        type: 'void',
+                        'x-decorator': 'FormGrid',
+                        'x-decorator-props': {
+                          maxColumns: 2,
+                          minColumns: 2,
                         },
-                      },
-                      miniProgramPath: {
-                        title: '跳转小程序具体路径',
-                        type: 'string',
-                        'x-decorator': 'FormItem',
-                        'x-component': 'Input',
-                        'x-component-props': {
-                          placeholder: '请输入跳转小程序具体路径',
+                        properties: {
+                          miniProgramId: {
+                            title: '跳转小程序AppId',
+                            type: 'string',
+                            'x-decorator': 'FormItem',
+                            'x-component': 'Input',
+                            'x-component-props': {
+                              placeholder: '请输入跳转小程序AppId',
+                            },
+                            'x-decorator-props': {
+                              gridSpan: 1,
+                            },
+                          },
+                          miniProgramPath: {
+                            title: '跳转小程序具体路径',
+                            type: 'string',
+                            'x-decorator': 'FormItem',
+                            'x-component': 'Input',
+                            'x-component-props': {
+                              placeholder: '请输入跳转小程序具体路径',
+                            },
+                            'x-decorator-props': {
+                              gridSpan: 1,
+                            },
+                          },
                         },
                       },
                     },
@@ -472,12 +587,12 @@ const Detail = observer(() => {
                       placeholder: '这里是回显内容',
                     },
                   },
-                  content: {
-                    title: '模版内容',
-                    type: 'string',
-                    'x-decorator': 'FormItem',
-                    'x-component': 'Input.TextArea',
-                  },
+                  // content: {
+                  //   title: '模版内容',
+                  //   type: 'string',
+                  //   'x-decorator': 'FormItem',
+                  //   'x-component': 'Input.TextArea',
+                  // },
                 },
                 'x-reactions': {
                   dependencies: ['provider'],
@@ -499,6 +614,7 @@ const Detail = observer(() => {
                 properties: {
                   agentId: {
                     title: 'AgentID',
+                    required: true,
                     'x-component': 'Input',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
@@ -508,47 +624,49 @@ const Detail = observer(() => {
                       placeholder: '请输入AgentID',
                     },
                   },
-                  toAllUser: {
-                    title: '通知全部用户',
-                    type: 'boolean',
-                    'x-component': 'Radio.Group',
-                    'x-decorator': 'FormItem',
-                    enum: [
-                      { label: '是', value: true },
-                      { label: '否', value: false },
-                    ],
-                  },
-                  userIdList: {
-                    title: '收信人ID',
-                    'x-component': 'Select',
-                    'x-decorator': 'FormItem',
+                  layout: {
+                    type: 'void',
+                    'x-decorator': 'FormGrid',
                     'x-decorator-props': {
-                      tooltip: '请输入收信人ID',
-                    },
-                    'x-component-props': {
-                      placeholder: '请输入收信人ID',
+                      maxColumns: 2,
+                      minColumns: 2,
                     },
-                    'x-reactions': {
-                      dependencies: ['configId'],
-                      fulfill: {
-                        run: '{{useAsyncDataSource(getDingTalkUser($deps[0]))}}',
+                    properties: {
+                      userIdList: {
+                        title: '收信人',
+                        'x-component': 'Select',
+                        'x-decorator': 'FormItem',
+                        'x-decorator-props': {
+                          tooltip: '请输入收信人ID',
+                          gridSpan: 1,
+                        },
+                        'x-component-props': {
+                          placeholder: '请输入收信人ID',
+                        },
+                        'x-reactions': {
+                          dependencies: ['configId'],
+                          fulfill: {
+                            run: '{{useAsyncDataSource(getDingTalkUser($deps[0]))}}',
+                          },
+                        },
                       },
-                    },
-                  },
-                  departmentIdList: {
-                    title: '收信部门ID',
-                    'x-component': 'Select',
-                    'x-decorator': 'FormItem',
-                    'x-decorator-props': {
-                      tooltip: '请输入收信部门ID',
-                    },
-                    'x-component-props': {
-                      placeholder: '请输入AgentID',
-                    },
-                    'x-reactions': {
-                      dependencies: ['configId'],
-                      fulfill: {
-                        run: '{{useAsyncDataSource(getDingTalkDept($deps[0]))}}',
+                      departmentIdList: {
+                        title: '收信部门',
+                        'x-component': 'Select',
+                        'x-decorator': 'FormItem',
+                        'x-decorator-props': {
+                          tooltip: '请输入收信部门ID',
+                          gridSpan: 1,
+                        },
+                        'x-component-props': {
+                          placeholder: '请输入AgentID',
+                        },
+                        'x-reactions': {
+                          dependencies: ['configId'],
+                          fulfill: {
+                            run: '{{useAsyncDataSource(getDingTalkDept($deps[0]))}}',
+                          },
+                        },
                       },
                     },
                   },
@@ -569,6 +687,7 @@ const Detail = observer(() => {
                     title: '消息类型',
                     'x-component': 'Select',
                     'x-decorator': 'FormItem',
+                    required: true,
                     enum: [
                       { label: 'markdown', value: 'markdown' },
                       { label: 'text', value: 'text' },
@@ -579,6 +698,7 @@ const Detail = observer(() => {
                     type: 'object',
                     properties: {
                       title: {
+                        required: true,
                         title: '标题',
                         'x-component': 'Input',
                         'x-decorator': 'FormItem',
@@ -597,6 +717,7 @@ const Detail = observer(() => {
                     type: 'object',
                     properties: {
                       title: {
+                        required: true,
                         title: '标题',
                         'x-component': 'Input',
                         'x-decorator': 'FormItem',
@@ -647,30 +768,38 @@ const Detail = observer(() => {
                 'x-visible': id === 'voice',
                 type: 'void',
                 properties: {
-                  // ttsCode	String	语音-模版ID
-                  // calledShowNumbers	String	语音-被叫显号
-                  // CalledNumber	String	语音-被叫号码
-                  // PlayTimes	String	语音-播放次数
-                  ttsCode: {
-                    title: '模版ID',
-                    'x-component': 'Input',
-                    'x-decorator': 'FormItem',
-                    'x-decorator-props': {
-                      tooltip: '请输入模版ID',
-                    },
-                    'x-component-props': {
-                      placeholder: '请输入模版ID',
-                    },
-                  },
-                  calledShowNumbers: {
-                    title: '被叫号码',
-                    'x-component': 'Input',
-                    'x-decorator': 'FormItem',
+                  layout: {
+                    type: 'void',
+                    'x-decorator': 'FormGrid',
                     'x-decorator-props': {
-                      tooltip: '请输入calledShowNumbers',
+                      maxColumns: 2,
+                      minColumns: 2,
                     },
-                    'x-component-props': {
-                      placeholder: '请输入calledShowNumbers',
+                    properties: {
+                      ttsCode: {
+                        title: '模版ID',
+                        'x-component': 'Select',
+                        'x-decorator': 'FormItem',
+                        'x-decorator-props': {
+                          tooltip: '请输入模版ID',
+                          gridSpan: 1,
+                        },
+                        'x-component-props': {
+                          placeholder: '请输入模版ID',
+                        },
+                      },
+                      calledShowNumbers: {
+                        title: '被叫号码',
+                        'x-component': 'Input',
+                        'x-decorator': 'FormItem',
+                        'x-decorator-props': {
+                          tooltip: '请输入calledShowNumbers',
+                          gridSpan: 1,
+                        },
+                        'x-component-props': {
+                          placeholder: '请输入calledShowNumbers',
+                        },
+                      },
                     },
                   },
                   calledNumber: {
@@ -701,32 +830,44 @@ const Detail = observer(() => {
                 'x-visible': id === 'sms',
                 type: 'void',
                 properties: {
-                  code: {
-                    title: '模版ID',
-                    'x-component': 'Select',
-                    'x-decorator': 'FormItem',
+                  layout: {
+                    type: 'void',
+                    'x-decorator': 'FormGrid',
                     'x-decorator-props': {
-                      tooltip: '请输入模版ID',
+                      maxColumns: 2,
+                      minColumns: 2,
                     },
-                    'x-component-props': {
-                      placeholder: '请输入模版ID',
-                    },
-                    'x-reactions': {
-                      dependencies: ['configId'],
-                      fulfill: {
-                        run: '{{useAsyncDataSource(getAliyunTemplates($deps[0]))}}',
+                    properties: {
+                      code: {
+                        title: '模版',
+                        'x-component': 'Select',
+                        'x-decorator': 'FormItem',
+                        'x-decorator-props': {
+                          tooltip: '阿里云短信平台自定义的模版名称',
+                          gridSpan: 1,
+                        },
+                        'x-component-props': {
+                          placeholder: '请选择模版',
+                        },
+                        'x-reactions': {
+                          dependencies: ['configId'],
+                          fulfill: {
+                            run: '{{useAsyncDataSource(getAliyunTemplates($deps[0]))}}',
+                          },
+                        },
+                      },
+                      phoneNumber: {
+                        title: '收信人',
+                        'x-component': 'Input',
+                        'x-decorator': 'FormItem',
+                        'x-decorator-props': {
+                          tooltip: '请输入收信人',
+                          gridSpan: 1,
+                        },
+                        'x-component-props': {
+                          placeholder: '请输入收信人',
+                        },
                       },
-                    },
-                  },
-                  phoneNumber: {
-                    title: '收信人',
-                    'x-component': 'Select',
-                    'x-decorator': 'FormItem',
-                    'x-decorator-props': {
-                      tooltip: '请输入收信人',
-                    },
-                    'x-component-props': {
-                      placeholder: '请输入收信人',
                     },
                   },
                   signName: {
@@ -759,7 +900,10 @@ const Detail = observer(() => {
                 'x-decorator': 'FormItem',
                 title: '标题',
                 'x-decorator-props': {
-                  tip: '请输入邮件标题',
+                  tip: '邮件标题',
+                },
+                'x-component-props': {
+                  placeholder: '请输入标题',
                 },
               },
               sendTo: {
@@ -767,7 +911,7 @@ const Detail = observer(() => {
                 'x-decorator': 'FormItem',
                 title: '收件人',
                 'x-decorator-props': {
-                  tip: '请输入收件人邮箱,多个收件人用换行分隔',
+                  tip: '多个收件人用换行分隔 \n最大支持1000个号码',
                 },
               },
               attachments: {
@@ -830,7 +974,16 @@ const Detail = observer(() => {
         'x-component': 'Input.TextArea',
         'x-decorator': 'FormItem',
         'x-decorator-props': {
-          tooltip: '请输入模版内容',
+          tooltip: '发送的内容,支持录入变量',
+        },
+        required: true,
+        'x-reactions': {
+          dependencies: ['provider'],
+          fulfill: {
+            state: {
+              hidden: '{{$deps[0]==="aliyun"}}',
+            },
+          },
         },
         'x-component-props': {
           rows: 5,

+ 2 - 2
src/pages/notice/Template/Log/index.tsx

@@ -56,10 +56,10 @@ const Log = observer(() => {
       onCancel={() => (state.log = false)}
       title="通知记录"
       width={'70vw'}
-      visible={state.log}
+      visible={state.log && !!state.current?.id}
     >
       <SearchComponent
-        defaultParam={[{ column: 'type$IN', value: id }]}
+        defaultParam={[{ column: 'notifyType$IN', value: id }]}
         field={columns}
         onSearch={(data) => {
           actionRef.current?.reset?.();

+ 8 - 4
src/pages/notice/Template/index.tsx

@@ -23,6 +23,7 @@ import { downloadObject } from '@/utils/util';
 import moment from 'moment';
 import { ProTableCard } from '@/components';
 import NoticeCard, { typeList } from '@/components/ProTableCard/CardItems/noticeTemplate';
+import { observer } from '@formily/react';
 
 export const service = new Service('notifier/template');
 
@@ -35,7 +36,7 @@ export const state = model<{
   debug: false,
   log: false,
 });
-const Template = () => {
+const Template = observer(() => {
   const intl = useIntl();
   const location = useLocation<{ id: string }>();
   const id = (location as any).query?.id;
@@ -262,6 +263,7 @@ const Template = () => {
                 key="log"
                 onClick={() => {
                   state.log = true;
+                  state.current = record;
                 }}
               >
                 <UnorderedListOutlined />
@@ -282,11 +284,13 @@ const Template = () => {
             ]}
           />
         )}
-        request={async (params) => service.query(params)}
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
       />
       <Debug />
-      <Log />
+      {state.log && <Log />}
     </PageContainer>
   );
-};
+});
 export default Template;

+ 79 - 13
src/pages/notice/Template/service.ts

@@ -31,8 +31,8 @@ class Service extends BaseService<TemplateItem> {
       data,
     });
 
-  public debug = (id: string, data: Record<string, any>) =>
-    request(`${SystemConst.API_BASE}/notifier/${id}/_send`, {
+  public debug = (id: string, templateId: string, data: Record<string, any>) =>
+    request(`${SystemConst.API_BASE}/notifier/${id}/${templateId}/_send`, {
       method: 'POST',
       data,
     });
@@ -44,28 +44,94 @@ class Service extends BaseService<TemplateItem> {
 
   dingTalk = {
     getDepartments: (id: string) =>
-      request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/departments`),
+      request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/departments`).then(
+        (resp: any) => {
+          return resp.result?.map((item: any) => ({
+            label: item.name,
+            value: item.id,
+          }));
+        },
+      ),
     getDepartmentsTree: (id: string) =>
-      request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/departments/tree`),
-    getUserByDepartment: (id: string, departmentId: string) =>
-      request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/${departmentId}/users`),
+      request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/departments/tree`).then(
+        (resp: any) => {
+          return resp.result?.map((item: any) => ({
+            label: item.name,
+            value: item.id,
+          }));
+        },
+      ),
+    getUser: (id: string) =>
+      request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/users`).then((resp: any) => {
+        return resp.result?.map((item: any) => ({
+          label: item.name,
+          value: item.id,
+        }));
+      }),
   };
 
   weixin = {
-    getTags: (id: string) => request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/tags`),
+    getTags: (id: string) =>
+      request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/tags`).then((resp: any) => {
+        return resp.result?.map((item: any) => ({
+          label: item.name,
+          value: item.id,
+        }));
+      }),
     getDepartments: (id: string) =>
-      request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/departments`),
-    getUser: (id: string) => request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/users`),
+      request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/departments`).then(
+        (resp: any) => {
+          return resp.result?.map((item: any) => ({
+            label: item.name,
+            value: item.id,
+          }));
+        },
+      ),
+    getUser: (id: string) =>
+      request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/users`).then((resp: any) => {
+        return resp.result?.map((item: any) => ({
+          label: item.name,
+          value: item.id,
+        }));
+      }),
     getOfficialTags: (configId: string) =>
-      request(`${SystemConst.API_BASE}/notifier/wechat/official/${configId}/tags`),
+      request(`${SystemConst.API_BASE}/notifier/wechat/official/${configId}/tags`).then(
+        (resp: any) => {
+          return resp.result?.map((item: any) => ({
+            label: item.name,
+            value: item.id,
+          }));
+        },
+      ),
     getOfficialTemplates: (configId: string) =>
-      request(`${SystemConst.API_BASE}/notifier/wechat/official/${configId}/templates`),
+      request(`${SystemConst.API_BASE}/notifier/wechat/official/${configId}/templates`).then(
+        (resp: any) => {
+          return resp.result?.map((item: any) => ({
+            ...item,
+            label: item.title,
+            value: item.id,
+          }));
+        },
+      ),
   };
 
   aliyun = {
-    getSigns: (id: string) => request(`${SystemConst.API_BASE}/notifier/sms/aliyun/${id}/signs`),
+    getSigns: (id: string) =>
+      request(`${SystemConst.API_BASE}/notifier/sms/aliyun/${id}/signs`).then((resp: any) => {
+        return resp.result?.map((item: any) => ({
+          ...item,
+          label: item.signName,
+          value: item.signName,
+        }));
+      }),
     getTemplates: (id: string) =>
-      request(`${SystemConst.API_BASE}/notifier/sms/aliyun/${id}/templates`),
+      request(`${SystemConst.API_BASE}/notifier/sms/aliyun/${id}/templates`).then((resp: any) => {
+        return resp.result?.map((item: any) => ({
+          ...item,
+          label: item.templateName,
+          value: item.templateCode,
+        }));
+      }),
   };
 }
 

+ 73 - 64
src/pages/rule-engine/Instance/index.tsx

@@ -117,40 +117,47 @@ const Instance = () => {
         </Tooltip>
       </Popconfirm>
     </Button>,
-    <Button
-      type={'link'}
+    <Tooltip
       key={'delete'}
-      style={{ padding: 0 }}
-      disabled={getButtonPermission('rule-engine/Instance', ['delete'])}
+      title={
+        record.state.value !== 'disable'
+          ? '已启用不能删除'
+          : intl.formatMessage({
+              id: 'pages.data.option.remove',
+              defaultMessage: '删除',
+            })
+      }
     >
-      <Popconfirm
-        title={record.state.value === 'disable' ? '确认删除' : '未停止不能删除'}
-        key={'delete'}
-        onConfirm={async () => {
-          if (record.state.value === 'disable') {
-            await service.remove(record.id);
-            message.success(
-              intl.formatMessage({
-                id: 'pages.data.option.success',
-                defaultMessage: '操作成功!',
-              }),
-            );
-            actionRef.current?.reload();
-          } else {
-            message.error('未停止不能删除');
-          }
-        }}
+      <Button
+        type={'link'}
+        style={{ padding: 0 }}
+        disabled={
+          getButtonPermission('rule-engine/Instance', ['delete']) ||
+          record.state.value !== 'disable'
+        }
       >
-        <Tooltip
-          title={intl.formatMessage({
-            id: 'pages.data.option.remove',
-            defaultMessage: '删除',
-          })}
+        <Popconfirm
+          title={record.state.value === 'disable' ? '确认删除' : '未停止不能删除'}
+          key={'delete'}
+          onConfirm={async () => {
+            if (record.state.value === 'disable') {
+              await service.remove(record.id);
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            } else {
+              message.error('未停止不能删除');
+            }
+          }}
         >
           <DeleteOutlined />
-        </Tooltip>
-      </Popconfirm>
-    </Button>,
+        </Popconfirm>
+      </Button>
+    </Tooltip>,
   ];
 
   const columns: ProColumns<InstanceItem>[] = [
@@ -223,7 +230,6 @@ const Instance = () => {
         <Button
           type="link"
           style={{ padding: 0 }}
-          disabled={getButtonPermission('rule-engine/Instance', ['view'])}
           key={'view'}
           onClick={() => {
             window.open(`/${SystemConst.API_BASE}/rule-editor/index.html#flow/${record.id}`);
@@ -279,40 +285,47 @@ const Instance = () => {
             </Tooltip>
           </Popconfirm>
         </Button>,
-        <Button
-          disabled={getButtonPermission('rule-engine/Instance', ['delete'])}
-          type={'link'}
+        <Tooltip
           key={'delete'}
-          style={{ padding: 0 }}
+          title={
+            record.state.value !== 'disable'
+              ? '已启用不能删除'
+              : intl.formatMessage({
+                  id: 'pages.data.option.remove',
+                  defaultMessage: '删除',
+                })
+          }
         >
-          <Popconfirm
-            title={record.state.value === 'disable' ? '确认删除' : '未禁用不能删除'}
-            key={'delete'}
-            onConfirm={async () => {
-              if (record.state.value === 'disable') {
-                await service.remove(record.id);
-                message.success(
-                  intl.formatMessage({
-                    id: 'pages.data.option.success',
-                    defaultMessage: '操作成功!',
-                  }),
-                );
-                actionRef.current?.reload();
-              } else {
-                message.error('未禁用不能删除');
-              }
-            }}
+          <Button
+            disabled={
+              getButtonPermission('rule-engine/Instance', ['delete']) ||
+              record.state.value !== 'disable'
+            }
+            type={'link'}
+            style={{ padding: 0 }}
           >
-            <Tooltip
-              title={intl.formatMessage({
-                id: 'pages.data.option.remove',
-                defaultMessage: '删除',
-              })}
+            <Popconfirm
+              title={record.state.value === 'disable' ? '确认删除' : '未禁用不能删除'}
+              key={'delete'}
+              onConfirm={async () => {
+                if (record.state.value === 'disable') {
+                  await service.remove(record.id);
+                  message.success(
+                    intl.formatMessage({
+                      id: 'pages.data.option.success',
+                      defaultMessage: '操作成功!',
+                    }),
+                  );
+                  actionRef.current?.reload();
+                } else {
+                  message.error('未禁用不能删除');
+                }
+              }}
             >
               <DeleteOutlined />
-            </Tooltip>
-          </Popconfirm>
-        </Button>,
+            </Popconfirm>
+          </Button>
+        </Tooltip>,
       ],
     },
   ];
@@ -373,11 +386,7 @@ const Instance = () => {
               <div
                 style={{ padding: 8, fontSize: 24 }}
                 onClick={() => {
-                  if (!getButtonPermission('rule-engine/Instance', ['view'])) {
-                    window.open(
-                      `/${SystemConst.API_BASE}/rule-editor/index.html#flow/${record.id}`,
-                    );
-                  }
+                  window.open(`/${SystemConst.API_BASE}/rule-editor/index.html#flow/${record.id}`);
                 }}
               >
                 <EyeOutlined />

+ 39 - 2
src/pages/system/Department/index.tsx

@@ -36,13 +36,37 @@ export const State = model<ModelType>({
   parentId: undefined,
 });
 
+export const getSortIndex = (data: DepartmentItem[], pId?: string): number => {
+  let sortIndex = 0;
+  if (data.length) {
+    if (!pId) {
+      return data.sort((a, b) => b.sortIndex - a.sortIndex)[0].sortIndex + 1;
+    }
+    data.some((department) => {
+      if (department.id === pId && department.children) {
+        const sortArray = department.children.sort((a, b) => b.sortIndex - a.sortIndex);
+        sortIndex = sortArray[0].sortIndex + 1;
+        return true;
+      } else if (department.children) {
+        sortIndex = getSortIndex(department.children, pId);
+        return !!sortIndex;
+      }
+      return false;
+    });
+  }
+  return sortIndex;
+};
+
 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 [sortParam, setSortParam] = useState<any>({ name: 'sortIndex', order: 'asc' });
+
   const rowKeys = useRef<React.Key[]>([]);
+
   /**
    * 根据部门ID删除数据
    * @param id
@@ -296,7 +320,7 @@ export default observer(() => {
         request={async (params) => {
           const response = await service.queryOrgThree({
             paging: false,
-            sorts: [{ name: 'sortIndex', order: 'asc' }],
+            sorts: [sortParam],
             ...params,
           });
           setTreeData(response.result);
@@ -311,6 +335,13 @@ export default observer(() => {
             status: response.status,
           };
         }}
+        onChange={(_, f, sorter: any) => {
+          if (sorter.order) {
+            setSortParam({ name: sorter.columnKey, order: sorter.order.replace('end', '') });
+          } else {
+            setSortParam({ name: 'sortIndex', value: 'asc' });
+          }
+        }}
         rowKey="id"
         expandable={{
           expandedRowKeys: [...rowKeys.current],
@@ -325,7 +356,9 @@ export default observer(() => {
         headerTitle={
           <Button
             disabled={getButtonPermission('system/Department', ['add'])}
-            onClick={() => (State.visible = true)}
+            onClick={() => {
+              State.visible = true;
+            }}
             key="button"
             icon={<PlusOutlined />}
             type="primary"
@@ -338,6 +371,10 @@ export default observer(() => {
         }
       />
       <Save<DepartmentItem>
+        parentChange={(pId) => {
+          console.log(getSortIndex(treeData, pId));
+          return getSortIndex(treeData, pId);
+        }}
         title={
           State.current.parentId
             ? intl.formatMessage({

+ 12 - 1
src/pages/system/Department/save.tsx

@@ -1,6 +1,7 @@
 // Modal 弹窗,用于新增、修改数据
 import React from 'react';
-import { createForm } from '@formily/core';
+import type { Field } from '@formily/core';
+import { createForm, onFieldReact } from '@formily/core';
 import { createSchemaField } from '@formily/react';
 import {
   ArrayTable,
@@ -29,12 +30,14 @@ import type BaseService from '@/utils/BaseService';
 export interface SaveModalProps<T> extends Omit<ModalProps, 'onOk' | 'onCancel'> {
   service: BaseService<T>;
   data?: Partial<T>;
+  reload?: () => void;
   /**
    * Model关闭事件
    * @param type 是否为请求接口后关闭,用于外部table刷新数据
    */
   onCancel?: (type: boolean, id?: React.Key) => void;
   schema: ISchema;
+  parentChange: (value?: string) => number;
 }
 
 const Save = <T extends object>(props: SaveModalProps<T>) => {
@@ -68,6 +71,14 @@ const Save = <T extends object>(props: SaveModalProps<T>) => {
   const form = createForm({
     validateFirst: true,
     initialValues: data || {},
+    effects: () => {
+      onFieldReact('sortIndex', (field) => {
+        if (props.parentChange) {
+          const sortIndex = props.parentChange(field.query('parentId').value());
+          (field as Field).value = !!sortIndex ? sortIndex : sortIndex + 1;
+        }
+      });
+    },
   });
 
   /**

+ 6 - 6
src/pages/system/Role/Detail/UserManage/BindUser.tsx

@@ -1,9 +1,9 @@
-import type { ProColumns } from '@jetlinks/pro-table';
+import type {ProColumns} from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import { message, Modal } from 'antd';
-import { useRef, useState } from 'react';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import { service } from '@/pages/system/User/index';
+import {message, Modal} from 'antd';
+import {useRef, useState} from 'react';
+import {useIntl} from '@@/plugin-locale/localeExports';
+import {service} from '@/pages/system/User/index';
 import Service from '@/pages/system/Role/service';
 import SearchComponent from '@/components/SearchComponent';
 
@@ -55,7 +55,7 @@ const BindUser = (props: Props) => {
   return (
     <Modal
       title="添加"
-      width={900}
+      width={990}
       visible={props.visible}
       onCancel={() => {
         props.cancel();

+ 177 - 124
src/pages/system/User/Save/index.tsx

@@ -6,7 +6,16 @@ import { createSchemaField } from '@formily/react';
 import React, { useEffect, useState } from 'react';
 import * as ICONS from '@ant-design/icons';
 import { PlusOutlined } from '@ant-design/icons';
-import { Form, FormItem, Input, Password, Select, Switch, TreeSelect } from '@formily/antd';
+import {
+  Form,
+  FormGrid,
+  FormItem,
+  Input,
+  Password,
+  Select,
+  Switch,
+  TreeSelect,
+} from '@formily/antd';
 import type { ISchema } from '@formily/json-schema';
 import { action } from '@formily/reactive';
 import type { Response } from '@/utils/typings';
@@ -75,6 +84,7 @@ const Save = (props: Props) => {
       Switch,
       Select,
       TreeSelect,
+      FormGrid,
     },
     scope: {
       icon(name: any) {
@@ -86,72 +96,93 @@ const Save = (props: Props) => {
   const schema: ISchema = {
     type: 'object',
     properties: {
-      name: {
-        title: intl.formatMessage({
-          id: 'pages.system.name',
-          defaultMessage: '姓名',
-        }),
-        type: 'string',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        name: 'name',
-        'x-validator': [
-          {
-            max: 64,
-            message: '最多可输入64个字符',
-          },
-          {
-            required: true,
-            message: '请输入姓名',
-          },
-        ],
-        // required: true,
-      },
-      username: {
-        title: intl.formatMessage({
-          id: 'pages.system.username',
-          defaultMessage: '用户名',
-        }),
-        type: 'string',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        'x-component-props': {
-          disabled: model === 'edit',
+      layout: {
+        type: 'void',
+        'x-decorator': 'FormGrid',
+        'x-decorator-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
         },
-        'x-validator': [
-          {
-            max: 50,
-            message: '最多可输入50个字符',
-          },
-          {
-            required: true,
-            message: '请输入用户名',
+        properties: {
+          name: {
+            title: intl.formatMessage({
+              id: 'pages.system.name',
+              defaultMessage: '姓名',
+            }),
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-component-props': {
+              placeholder: '请输入姓名',
+            },
+            name: 'name',
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入姓名',
+              },
+            ],
+            // required: true,
           },
-          {
-            triggerType: 'onBlur',
-            validator: (value: string) => {
-              return new Promise((resolve) => {
-                service
-                  .validateField('username', value)
-                  .then((resp) => {
-                    if (resp.status === 200) {
-                      if (resp.result.passed) {
+          username: {
+            title: intl.formatMessage({
+              id: 'pages.system.username',
+              defaultMessage: '用户名',
+            }),
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-component-props': {
+              disabled: model === 'edit',
+              placeholder: '请输入用户名',
+            },
+            'x-validator': [
+              {
+                max: 50,
+                message: '最多可输入50个字符',
+              },
+              {
+                required: true,
+                message: '请输入用户名',
+              },
+              {
+                triggerType: 'onBlur',
+                validator: (value: string) => {
+                  return new Promise((resolve) => {
+                    service
+                      .validateField('username', value)
+                      .then((resp) => {
+                        if (resp.status === 200) {
+                          if (resp.result.passed) {
+                            resolve('');
+                          } else {
+                            resolve(resp.result.reason);
+                          }
+                        }
                         resolve('');
-                      } else {
-                        resolve(resp.result.reason);
-                      }
-                    }
-                    resolve('');
-                  })
-                  .catch(() => {
-                    return '验证失败!';
+                      })
+                      .catch(() => {
+                        return '验证失败!';
+                      });
                   });
-              });
-            },
+                },
+              },
+            ],
+            name: 'username',
+            required: true,
           },
-        ],
-        name: 'username',
-        required: true,
+        },
       },
       password: {
         type: 'string',
@@ -163,7 +194,7 @@ const Save = (props: Props) => {
         'x-component': 'Password',
         'x-component-props': {
           checkStrength: true,
-          placeholder: '********',
+          placeholder: '请输入密码',
         },
         maxLength: 128,
         minLength: 6,
@@ -204,7 +235,7 @@ const Save = (props: Props) => {
         'x-component': 'Password',
         'x-component-props': {
           checkStrength: true,
-          placeholder: '********',
+          placeholder: '请再次输入密码',
         },
         maxLength: 128,
         minLength: 6,
@@ -236,70 +267,92 @@ const Save = (props: Props) => {
         'x-decorator-props': {},
         name: 'confirmPassword',
       },
-      roleIdList: {
-        title: '角色',
-        'x-decorator': 'FormItem',
-        'x-component': 'Select',
-        'x-component-props': {
-          mode: 'multiple',
-          showArrow: true,
-          filterOption: (input: string, option: any) =>
-            option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
-        },
-        'x-reactions': ['{{useAsyncDataSource(getRole)}}'],
+      layout2: {
+        type: 'void',
+        'x-decorator': 'FormGrid',
         'x-decorator-props': {
-          addonAfter: (
-            <a
-              onClick={() => {
-                const tab: any = window.open(`${origin}/#/system/role?save=true`);
-                tab!.onTabSaveSuccess = (value: any) => {
-                  form.setFieldState('roleIdList', (state) => {
-                    state.dataSource = state.dataSource?.concat([
-                      { label: value.name, value: value.id },
-                    ]);
-                  });
-                };
-              }}
-            >
-              <PlusOutlined />
-            </a>
-          ),
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
         },
-      },
-      orgIdList: {
-        title: '部门',
-        'x-decorator': 'FormItem',
-        'x-component': 'TreeSelect',
-        'x-component-props': {
-          multiple: true,
-          showArrow: true,
-          showCheckedStrategy: ATreeSelect.SHOW_ALL,
-          filterOption: (input: string, option: any) =>
-            option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
-          fieldNames: {
-            label: 'name',
-            value: 'id',
+        properties: {
+          roleIdList: {
+            title: '角色',
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-component-props': {
+              mode: 'multiple',
+              showArrow: true,
+              placeholder: '请选择角色',
+              filterOption: (input: string, option: any) =>
+                option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+            },
+            'x-reactions': ['{{useAsyncDataSource(getRole)}}'],
+            'x-decorator-props': {
+              gridSpan: 1,
+              addonAfter: (
+                <a
+                  onClick={() => {
+                    const tab: any = window.open(`${origin}/#/system/role?save=true`);
+                    tab!.onTabSaveSuccess = (value: any) => {
+                      form.setFieldState('roleIdList', (state) => {
+                        state.dataSource = state.dataSource?.concat([
+                          { label: value.name, value: value.id },
+                        ]);
+                        state.value = [...state.value, value.id];
+                      });
+                    };
+                  }}
+                >
+                  <PlusOutlined />
+                </a>
+              ),
+            },
+          },
+          orgIdList: {
+            title: '部门',
+            'x-decorator': 'FormItem',
+            'x-component': 'TreeSelect',
+            'x-component-props': {
+              multiple: true,
+              showArrow: true,
+              placeholder: '请选择角色',
+              showCheckedStrategy: ATreeSelect.SHOW_ALL,
+              filterOption: (input: string, option: any) =>
+                option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+              fieldNames: {
+                label: 'name',
+                value: 'id',
+              },
+              treeNodeFilterProp: 'name',
+            },
+            'x-decorator-props': {
+              gridSpan: 1,
+              addonAfter: (
+                <a
+                  onClick={() => {
+                    const tab: any = window.open(`${origin}/#/system/department?save=true`);
+                    tab!.onTabSaveSuccess = (value: any) => {
+                      form.setFieldState('orgIdList', async (state) => {
+                        state.dataSource = await getOrg().then((resp) =>
+                          resp.result?.map((item: Record<string, unknown>) => ({
+                            ...item,
+                            label: item.name,
+                            value: item.id,
+                          })),
+                        );
+                        state.value = [...state.value, value.id];
+                      });
+                    };
+                  }}
+                >
+                  <PlusOutlined />
+                </a>
+              ),
+            },
+            'x-reactions': ['{{useAsyncDataSource(getOrg)}}'],
           },
-          treeNodeFilterProp: 'name',
-        },
-        'x-decorator-props': {
-          addonAfter: (
-            <a
-              onClick={() => {
-                const tab: any = window.open(`${origin}/#/system/department?save=true`);
-                tab!.onTabSaveSuccess = (value: any) => {
-                  console.log(value, 'value');
-                  form.setFieldState('orgIdList', (state) => {
-                    state.dataSource = state.dataSource?.concat({ name: value.name, id: value.id });
-                  });
-                };
-              }}
-            >
-              <PlusOutlined />
-            </a>
-          ),
         },
-        'x-reactions': ['{{useAsyncDataSource(getOrg)}}'],
       },
     },
   };