Quellcode durchsuchen

feat(分屏展示): 左侧树添加懒加载以及虚拟滚动

xieyonghong vor 4 Jahren
Ursprung
Commit
9615602d6e
46 geänderte Dateien mit 6697 neuen und 4613 gelöschten Zeilen
  1. 8 8
      package.json
  2. BIN
      public/images/access-config-diaabled.png
  3. BIN
      public/images/access-config-enabled.png
  4. 149 20
      src/components/Player/ScreenPlayer.tsx
  5. 2 1
      src/components/Player/index.less
  6. 24 0
      src/components/Player/service.ts
  7. 97 0
      src/components/ProTableCard/CardItems/AccessConfig/index.less
  8. 63 0
      src/components/ProTableCard/CardItems/AccessConfig/index.tsx
  9. 4 2
      src/components/ProTableCard/TableCard.tsx
  10. 25 19
      src/components/SearchComponent/index.tsx
  11. 3 3
      src/components/SearchComponent/typings.d.ts
  12. 117 0
      src/pages/device/Instance/Detail/Config/Edit.tsx
  13. 136 150
      src/pages/device/Instance/Detail/Config/index.tsx
  14. 25 15
      src/pages/device/Instance/Detail/Info/index.tsx
  15. 69 52
      src/pages/device/Instance/Detail/Config/Tags/index.tsx
  16. 63 0
      src/pages/device/Instance/Detail/Tags/index.tsx
  17. 0 106
      src/pages/device/Product/Detail/Access/AccessConfig/index.less
  18. 7 45
      src/pages/device/Product/Detail/Access/AccessConfig/index.tsx
  19. 40 25
      src/pages/device/Product/Detail/Access/index.tsx
  20. 40 72
      src/pages/link/AccessConfig/Detail/Access/index.tsx
  21. 1 0
      src/pages/link/AccessConfig/Detail/index.tsx
  22. 10 39
      src/pages/link/AccessConfig/index.less
  23. 5 46
      src/pages/link/AccessConfig/index.tsx
  24. 7 5
      src/pages/media/Cascade/Save/index.tsx
  25. 1 1
      src/pages/media/Cascade/index.tsx
  26. 1 1
      src/pages/media/SplitScreen/index.less
  27. 16 8
      src/pages/media/SplitScreen/index.tsx
  28. 1 1
      src/pages/media/SplitScreen/service.ts
  29. 13 33
      src/pages/media/SplitScreen/tree.tsx
  30. 32 23
      src/pages/media/Stream/Detail/index.tsx
  31. 82 0
      src/pages/notice/Config/Debug/index.tsx
  32. 31 16
      src/pages/notice/Config/Detail/index.tsx
  33. 74 0
      src/pages/notice/Config/Log/index.tsx
  34. 42 130
      src/pages/notice/Config/index.tsx
  35. 7 0
      src/pages/notice/Config/service.ts
  36. 1 0
      src/pages/notice/Config/typings.d.ts
  37. 184 0
      src/pages/notice/Template/Debug/index.tsx
  38. 204 60
      src/pages/notice/Template/Detail/index.tsx
  39. 74 0
      src/pages/notice/Template/Log/index.tsx
  40. 67 516
      src/pages/notice/Template/index.tsx
  41. 34 0
      src/pages/notice/Template/service.ts
  42. 8 0
      src/pages/notice/Template/typings.d.ts
  43. 1 1
      src/pages/system/Menu/Detail/edit.tsx
  44. 1 1
      src/pages/system/Menu/service.ts
  45. 4 0
      src/utils/BaseService.ts
  46. 4924 3214
      yarn.lock

+ 8 - 8
package.json

@@ -62,20 +62,20 @@
     "@ant-design/pro-descriptions": "^1.6.8",
     "@ant-design/pro-form": "^1.18.3",
     "@ant-design/pro-layout": "^6.27.2",
-    "@formily/antd": "2.0.0-rc.17",
-    "@formily/core": "2.0.0-rc.17",
-    "@formily/json-schema": "2.0.0-rc.17",
-    "@formily/react": "2.0.0-rc.17",
-    "@formily/reactive": "2.0.0-rc.17",
-    "@formily/reactive-react": "2.0.0-rc.17",
-    "@formily/shared": "2.0.0-rc.17",
+    "@formily/antd": "2.0.18",
+    "@formily/core": "2.0.18",
+    "@formily/json-schema": "2.0.18",
+    "@formily/react": "2.0.18",
+    "@formily/reactive": "2.0.18",
+    "@formily/reactive-react": "2.0.18",
+    "@formily/shared": "2.0.18",
     "@jetlinks/pro-list": "^1.10.8",
     "@jetlinks/pro-table": "^2.63.11",
     "@liveqing/liveplayer": "^2.6.4",
     "@types/react-syntax-highlighter": "^13.5.2",
     "@umijs/route-utils": "^1.0.36",
     "ahooks": "^2.10.9",
-    "antd": "^4.18.8",
+    "antd": "^4.19.5",
     "braft-editor": "^2.3.9",
     "classnames": "^2.3.1",
     "dexie": "^3.0.3",

BIN
public/images/access-config-diaabled.png


BIN
public/images/access-config-enabled.png


+ 149 - 20
src/components/Player/ScreenPlayer.tsx

@@ -1,7 +1,7 @@
 import { useCallback, useEffect, useRef, useState } from 'react';
 import classNames from 'classnames';
 import LivePlayer from './index';
-import { Radio } from 'antd';
+import { Radio, Dropdown, Menu, Popover, Input, Button, Empty, Tooltip } from 'antd';
 import { useFullscreen } from 'ahooks';
 import './index.less';
 import {
@@ -9,9 +9,12 @@ import {
   CaretLeftOutlined,
   CaretRightOutlined,
   CaretUpOutlined,
+  DeleteOutlined,
   MinusOutlined,
   PlusOutlined,
+  QuestionCircleOutlined,
 } from '@ant-design/icons';
+import Service from './service';
 
 type Player = {
   id?: string;
@@ -37,10 +40,18 @@ interface ScreenProps {
   showScreen?: boolean;
 }
 
+const service = new Service();
+
+const DEFAULT_SAVE_CODE = 'screen_save';
+
 export default (props: ScreenProps) => {
   const [screen, setScreen] = useState(1);
   const [players, setPlayers] = useState<Player[]>([]);
   const [playerActive, setPlayerActive] = useState(0);
+  const [historyList, setHistoryList] = useState<any>([]);
+  const [historyTitle, setHistoryTitle] = useState('');
+  const [visible, setVisible] = useState(false);
+
   const fullscreenRef = useRef(null);
   const [isFullscreen, { setFull }] = useFullscreen(fullscreenRef);
 
@@ -62,6 +73,42 @@ export default (props: ScreenProps) => {
     [players, playerActive, screen],
   );
 
+  const handleHistory = (item: any) => {
+    const log = JSON.parse(item.content || '{}');
+    setScreen(log.screen);
+    setPlayers(log.players);
+  };
+
+  const getHistory = async () => {
+    const resp = await service.history.query(DEFAULT_SAVE_CODE);
+    if (resp.status === 200) {
+      setHistoryList(resp.result);
+    }
+  };
+
+  const deleteHistory = async (id: string) => {
+    const resp = await service.history.remove(DEFAULT_SAVE_CODE, id);
+    if (resp.status === 200) {
+      getHistory();
+      setVisible(false);
+    }
+  };
+
+  const saveHistory = useCallback(async () => {
+    const param = {
+      name: historyTitle,
+      content: JSON.stringify({
+        screen: screen,
+        players: players,
+      }),
+    };
+    const resp = await service.history.save(DEFAULT_SAVE_CODE, param);
+    if (resp.status === 200) {
+      setVisible(false);
+      getHistory();
+    }
+  }, [players, historyTitle, screen]);
+
   useEffect(() => {
     const arr = new Array(screen).fill(1).map(() => ({ id: '', url: '' }));
     setPlayers(arr);
@@ -75,32 +122,114 @@ export default (props: ScreenProps) => {
     }
   }, [props.url]);
 
+  useEffect(() => {
+    if (props.showScreen !== false) {
+      getHistory();
+    }
+  }, []);
+
   const screenClass = `screen-${screen}`;
 
+  const DropdownMenu = (
+    <Menu>
+      {historyList.length ? (
+        historyList.map((item: any) => {
+          return (
+            <Menu.Item
+              key={item.id}
+              onClick={() => {
+                handleHistory(item);
+              }}
+            >
+              {item.name}
+              <DeleteOutlined
+                onClick={() => {
+                  deleteHistory(item.id);
+                }}
+              />
+            </Menu.Item>
+          );
+        })
+      ) : (
+        <Empty />
+      )}
+    </Menu>
+  );
+
   return (
     <div className={'live-player-warp'}>
       <div className={'live-player-content'}>
         <div className={'player-screen-tool'}>
           {props.showScreen !== false && (
-            <Radio.Group
-              options={[
-                { label: '单屏', value: 1 },
-                { label: '四分屏', value: 4 },
-                { label: '九分屏', value: 9 },
-                { label: '全屏', value: 0 },
-              ]}
-              value={screen}
-              onChange={(e) => {
-                if (e.target.value) {
-                  setScreen(e.target.value);
-                } else {
-                  // 全屏操作
-                  setFull();
-                }
-              }}
-              optionType={'button'}
-              buttonStyle={'solid'}
-            />
+            <>
+              <div></div>
+              <div>
+                <Radio.Group
+                  options={[
+                    { label: '单屏', value: 1 },
+                    { label: '四分屏', value: 4 },
+                    { label: '九分屏', value: 9 },
+                    { label: '全屏', value: 0 },
+                  ]}
+                  value={screen}
+                  onChange={(e) => {
+                    if (e.target.value) {
+                      setScreen(e.target.value);
+                    } else {
+                      // 全屏操作
+                      setFull();
+                    }
+                  }}
+                  optionType={'button'}
+                  buttonStyle={'solid'}
+                />
+                {/*<Tooltip*/}
+                {/*  title={''}*/}
+                {/*>*/}
+                {/*  <QuestionCircleOutlined />*/}
+                {/*</Tooltip>*/}
+              </div>
+              <div className={'screen-tool-save'}>
+                <Popover
+                  content={
+                    <div style={{ width: 300 }}>
+                      <Input.TextArea
+                        rows={3}
+                        onChange={(e) => {
+                          setHistoryTitle(e.target.value);
+                        }}
+                      />
+                      <Button
+                        type={'primary'}
+                        onClick={saveHistory}
+                        style={{ width: '100%', marginTop: 16 }}
+                      >
+                        保存
+                      </Button>
+                    </div>
+                  }
+                  title="分屏名称"
+                  trigger="click"
+                  visible={visible}
+                  onVisibleChange={(v) => {
+                    setVisible(v);
+                  }}
+                >
+                  <Dropdown.Button
+                    type={'primary'}
+                    overlay={DropdownMenu}
+                    onClick={() => {
+                      setVisible(true);
+                    }}
+                  >
+                    保存
+                  </Dropdown.Button>
+                </Popover>
+                <Tooltip title={'可保存分屏配置记录'}>
+                  <QuestionCircleOutlined style={{ marginLeft: 8 }} />
+                </Tooltip>
+              </div>
+            </>
           )}
         </div>
         <div className={'player-body'}>

+ 2 - 1
src/components/Player/index.less

@@ -12,7 +12,8 @@
 
     .player-screen-tool {
       display: flex;
-      justify-content: center;
+      align-items: center;
+      justify-content: space-between;
       margin-bottom: 20px;
 
       .ant-radio-button-wrapper {

+ 24 - 0
src/components/Player/service.ts

@@ -0,0 +1,24 @@
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service {
+  private api = `/${SystemConst.API_BASE}/user/settings`;
+
+  public history = {
+    query: (type: string) =>
+      request(`${this.api}/${type}`, {
+        method: 'GET',
+      }),
+    save: (type: string, data: Record<string, unknown>) =>
+      request(`${this.api}/${type}`, {
+        method: 'POST',
+        data,
+      }),
+    remove: (type: string, key: string) =>
+      request(`${this.api}/${type}/${key}`, {
+        method: 'DELETE',
+      }),
+  };
+}
+
+export default Service;

+ 97 - 0
src/components/ProTableCard/CardItems/AccessConfig/index.less

@@ -0,0 +1,97 @@
+@import '~antd/es/style/themes/default.less';
+
+.tableCardDisabled {
+  width: 100%;
+  background: url('/images/access-config-diaabled.png') no-repeat;
+  background-size: 100% 100%;
+}
+
+.tableCardEnabled {
+  width: 100%;
+  background: url('/images/access-config-enabled.png') no-repeat;
+  background-size: 100% 100%;
+}
+
+.active {
+  border: 1px solid @primary-color-active;
+}
+
+.context-access {
+  display: flex;
+  width: 100%;
+  .card {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    margin-left: 20px;
+    .header {
+      .title {
+        width: 70%;
+        overflow: hidden;
+        font-weight: 700;
+        font-size: 18px;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+      .title::before {
+        display: none;
+      }
+      .desc {
+        width: 70%;
+        margin-top: 10px;
+        overflow: hidden;
+        color: #666;
+        font-weight: 400;
+        font-size: 12px;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+    }
+
+    .container {
+      display: flex;
+      width: 100%;
+      min-height: 50px;
+      margin-top: 10px;
+
+      .server,
+      .procotol {
+        width: calc(50% - 20px);
+        margin-right: 10px;
+        .subTitle {
+          width: 100%;
+          margin-bottom: 5px;
+          overflow: hidden;
+          color: rgba(0, 0, 0, 0.75);
+          font-size: 12px;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+        .subItem {
+          width: 100%;
+          height: 20px;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+      }
+      .procotol {
+        .desc {
+          width: 100%;
+          overflow: hidden;
+          color: #666;
+          font-weight: 400;
+          font-size: 12px;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+      }
+    }
+  }
+}
+
+:global {
+  .ant-pagination-item {
+    display: none;
+  }
+}

+ 63 - 0
src/components/ProTableCard/CardItems/AccessConfig/index.tsx

@@ -0,0 +1,63 @@
+import React from 'react';
+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 './index.less';
+
+export interface AccessConfigCardProps extends AccessItem {
+  detail?: React.ReactNode;
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+  showTool?: boolean;
+  activeStyle?: string;
+}
+
+const defaultImage = require('/public/images/device-access.png');
+
+export default (props: AccessConfigCardProps) => {
+  return (
+    <TableCard
+      showMask={false}
+      actions={props.actions}
+      status={props.state.value}
+      statusText={props.state.text}
+      statusNames={{
+        enabled: StatusColorEnum.processing,
+        disabled: StatusColorEnum.error,
+      }}
+      showTool={props.showTool}
+      contentClassName={props.state.value === 'disabled' ? 'tableCardDisabled' : 'tableCardEnabled'}
+      className={props.activeStyle}
+    >
+      <div className="context-access">
+        <div>
+          <img width={88} height={88} src={defaultImage} alt={''} />
+        </div>
+        <div className="card">
+          <div className="header">
+            <div className="title">
+              <Tooltip title={props.name}>{props.name || '--'}</Tooltip>
+            </div>
+            <div className="desc">{props.description || '--'}</div>
+          </div>
+          <div className="container">
+            <div className="server">
+              <div className="subTitle">{props?.channelInfo?.name || '--'}</div>
+              {props.channelInfo?.addresses.slice(0, 2).map((i: any, index: number) => (
+                <div className="subItem" key={i.address + `_address${index}`}>
+                  <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
+                </div>
+              ))}
+            </div>
+            <div className="procotol">
+              <div className="subTitle">{props?.protocolDetail?.name || '--'}</div>
+              <div className="desc">{props.protocolDetail?.description || '--'}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 4 - 2
src/components/ProTableCard/TableCard.tsx

@@ -1,7 +1,8 @@
 import React, { useState } from 'react';
 import classNames from 'classnames';
 import { BadgeStatus } from '@/components';
-import { StatusColorEnum, StatusColorType } from '@/components/BadgeStatus';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import type { StatusColorType } from '@/components/BadgeStatus';
 import './index.less';
 
 export interface TableCardProps {
@@ -15,6 +16,7 @@ export interface TableCardProps {
   statusNames?: Record<string | number, StatusColorType>;
   children?: React.ReactNode;
   actions?: React.ReactNode[];
+  contentClassName?: string;
 }
 
 function getAction(actions: React.ReactNode[]) {
@@ -72,7 +74,7 @@ export default (props: TableCardProps) => {
     <div className={classNames('iot-card', { hover: maskShow }, props.className)}>
       <div className={'card-warp'}>
         <div
-          className={'card-content'}
+          className={classNames('card-content', props.contentClassName)}
           onMouseEnter={() => {
             setMaskShow(true);
           }}

+ 25 - 19
src/components/SearchComponent/index.tsx

@@ -55,12 +55,12 @@ const server2Ui = (source: SearchTermsServer): SearchTermsUI => ({
 });
 
 interface Props<T> {
-  /** @name 搜索条件 */
+  /** @name "搜索条件" */
   field: ProColumns<T>[];
   onSearch: (params: { terms: SearchTermsServer }) => void;
   target?: string;
   /**
-   *  @name 固定查询参数
+   *  @name "固定查询参数"
    *  eg: 1: {[{ column: 'test', value: 'admin' }]}
    *      2: {[
    *            {
@@ -133,6 +133,7 @@ const sortField = (field: ProColumns[]) => {
 
 const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
   const { field, target, onSearch, defaultParam, enableSave = true } = props;
+
   const intl = useIntl();
   const [expand, setExpand] = useState<boolean>(true);
   const initForm = server2Ui([{ terms: [initTerm] }]);
@@ -141,6 +142,17 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
   const [initParams, setInitParams] = useState<SearchTermsUI>(initForm);
   const [history, setHistory] = useState([]);
 
+  /**
+   * 过滤不参与搜索的数据
+   */
+  const filterSearchTerm = (): ProColumns<T>[] =>
+    field
+      .filter((item) => item.dataIndex)
+      .filter((item) => !item.hideInSearch)
+      .filter((item) => !['index', 'option'].includes(item.dataIndex as string));
+  // 处理后的搜索条件
+  const processedField = sortField(filterSearchTerm());
+
   const form = useMemo(
     () =>
       createForm<SearchTermsUI>({
@@ -150,7 +162,7 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
           onFormInit((form1) => {
             if (expand) {
               form1.setValues({
-                terms1: [{ column: sortField(field)[0]?.dataIndex, termType: 'like' }],
+                terms1: [{ column: processedField[0]?.dataIndex, termType: 'like' }],
               });
             }
           });
@@ -211,13 +223,6 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
     }
   };
 
-  const filterSearchTerm = (): EnumData[] =>
-    field
-      .filter((item) => item.dataIndex)
-      .filter((item) => !item.hideInSearch)
-      .filter((item) => !['index', 'option'].includes(item.dataIndex as string))
-      .map((i) => ({ label: i.title, value: i.dataIndex } as EnumData));
-
   const createGroup = (name: string): ISchema => ({
     'x-decorator': 'FormItem',
     'x-decorator-props': {
@@ -258,7 +263,7 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
           'x-component-props': {
             placeholder: '请选择',
           },
-          enum: filterSearchTerm(),
+          enum: filterSearchTerm().map((i) => ({ label: i.title, value: i.dataIndex } as EnumData)),
         },
         termType: {
           type: 'enum',
@@ -319,21 +324,22 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
 
   const handleExpand = () => {
     const value = form.values;
-    const _field = sortField(field);
 
     if (expand) {
       value.terms1 = [0, 1, 2].map((i) => ({
         termType: 'like',
-        column: (_field[i]?.dataIndex as string) || null,
+        column: (processedField[i]?.dataIndex as string) || null,
         type: 'or',
       }));
       value.terms2 = [3, 4, 5].map((i) => ({
         termType: 'like',
-        column: (_field[i]?.dataIndex as string) || null,
+        column: (processedField[i]?.dataIndex as string) || null,
         type: 'or',
       }));
     } else {
-      value.terms1 = [{ termType: 'like', column: (_field[0].dataIndex as string) || null }];
+      value.terms1 = [
+        { termType: 'like', column: (processedField[0].dataIndex as string) || null },
+      ];
       value.terms2 = [];
     }
     setInitParams(value);
@@ -349,6 +355,7 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
   const handleHistory = (item: SearchHistory) => {
     const log = JSON.parse(item.content) as SearchTermsUI;
     form.setValues(log);
+    // @ts-ignore
     setExpand(!(log.terms1?.length > 1 || log.terms2?.length > 1));
   };
 
@@ -399,13 +406,12 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
     // 处理默认查询参数
     if (defaultParam && defaultParam?.length > 0) {
       if ('terms' in defaultParam[0]) {
-        console.log(defaultParam, 'terms');
         _value = _value.concat(defaultParam as SearchTermsServer);
       } else if ('value' in defaultParam[0]) {
-        console.log(defaultParam, 'value');
         _value = _value.concat([{ terms: defaultParam }]);
       }
     }
+
     return _value
       .filter((i) => i.terms?.length > 0)
       .map((term) => {
@@ -422,8 +428,8 @@ const SearchComponent = <T extends Record<string, any>>(props: Props<T>) => {
 
   const handleSearch = async () => {
     const value = form.values;
-    const filterTerms = (data: Partial<Term>[]) =>
-      data && data.filter((item) => item.column != null).filter((item) => item.value != null);
+    const filterTerms = (data: Partial<Term>[] | undefined) =>
+      data && data.filter((item) => item.column != null).filter((item) => item.value !== undefined);
     const _terms = _.cloneDeep(value);
     _terms.terms1 = filterTerms(_terms.terms1);
     _terms.terms2 = filterTerms(_terms.terms2);

+ 3 - 3
src/components/SearchComponent/typings.d.ts

@@ -6,13 +6,13 @@ type Term = {
 };
 
 type SearchTermsUI = {
-  terms1: Partial<Term>[];
+  terms1: Partial<Term>[] | undefined;
   type: 'or' | 'and';
-  terms2: Partial<Term>[];
+  terms2: Partial<Term>[] | undefined;
 };
 
 type SearchTermsServer = {
-  terms: Partial<Term>[];
+  terms: Partial<Term>[] | undefined;
   type?: 'or' | 'and';
 }[];
 

+ 117 - 0
src/pages/device/Instance/Detail/Config/Edit.tsx

@@ -0,0 +1,117 @@
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import type { ISchema } from '@formily/json-schema';
+import { Form, FormGrid, FormItem, Input, Password, PreviewText } from '@formily/antd';
+import { Button, Drawer, message, Space } from 'antd';
+import { useParams } from 'umi';
+
+const componentMap = {
+  string: 'Input',
+  password: 'Password',
+};
+
+interface Props {
+  close: () => void;
+  metadata: any[];
+}
+
+const Edit = (props: Props) => {
+  const { metadata } = props;
+  const params = useParams<{ id: string }>();
+  const id = InstanceModel.detail?.id || params?.id;
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: InstanceModel.detail?.configuration,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Password,
+      FormGrid,
+      PreviewText,
+    },
+  });
+
+  const configToSchema = (data: any[]) => {
+    const config = {};
+    data.forEach((item) => {
+      config[item.property] = {
+        type: 'string',
+        title: item.name,
+        'x-decorator': 'FormItem',
+        'x-component': componentMap[item.type.type],
+        'x-decorator-props': {
+          tooltip: item.description,
+        },
+      };
+    });
+    return config;
+  };
+
+  const renderConfigCard = () => {
+    return metadata?.map((item: any) => {
+      const itemSchema: ISchema = {
+        type: 'object',
+        properties: {
+          grid: {
+            type: 'void',
+            'x-component': 'FormGrid',
+            'x-component-props': {
+              minColumns: [1],
+              maxColumns: [1],
+            },
+            properties: configToSchema(item.properties),
+          },
+        },
+      };
+
+      return (
+        <>
+          <PreviewText.Placeholder value="-">
+            <Form form={form} layout="vertical">
+              <SchemaField schema={itemSchema} />
+            </Form>
+          </PreviewText.Placeholder>
+        </>
+      );
+    });
+  };
+  return (
+    <Drawer
+      title="编辑配置"
+      placement="right"
+      onClose={() => {
+        props.close();
+      }}
+      visible
+      extra={
+        <Space>
+          <Button
+            type="primary"
+            onClick={async () => {
+              const values = (await form.submit()) as any;
+              const resp = await service.modify(id || '', {
+                id,
+                configuration: { ...values },
+              });
+              if (resp.status === 200) {
+                message.success('操作成功!');
+                props.close();
+              }
+            }}
+          >
+            保存
+          </Button>
+        </Space>
+      }
+    >
+      {renderConfigCard()}
+    </Drawer>
+  );
+};
+
+export default Edit;

+ 136 - 150
src/pages/device/Instance/Detail/Config/index.tsx

@@ -1,19 +1,15 @@
-import { Card, Divider, Empty, message, Popconfirm, Space, Tooltip } from 'antd';
+import { Button, Descriptions, message, Popconfirm, Space, Tooltip } from 'antd';
 import { InstanceModel, service } from '@/pages/device/Instance';
 import { useEffect, useState } from 'react';
-import { createSchemaField } from '@formily/react';
-import type { ConfigMetadata, ConfigProperty } from '@/pages/device/Product/typings';
-import type { ISchema } from '@formily/json-schema';
-import { Form, FormGrid, FormItem, FormLayout, Input, Password, PreviewText } from '@formily/antd';
-import { createForm } from '@formily/core';
+import type { ConfigMetadata } from '@/pages/device/Product/typings';
 import { history, useParams } from 'umi';
-import Tags from '@/pages/device/Instance/Detail/Config/Tags';
-import Icon from '@ant-design/icons';
-
-const componentMap = {
-  string: 'Input',
-  password: 'Password',
-};
+import {
+  CheckOutlined,
+  EditOutlined,
+  QuestionCircleOutlined,
+  UndoOutlined,
+} from '@ant-design/icons';
+import Edit from './Edit';
 
 const Config = () => {
   const params = useParams<{ id: string }>();
@@ -29,13 +25,7 @@ const Config = () => {
   }, []);
 
   const [metadata, setMetadata] = useState<ConfigMetadata[]>([]);
-  const [state, setState] = useState<boolean>(false);
-
-  const form = createForm({
-    validateFirst: true,
-    readPretty: state,
-    initialValues: InstanceModel.detail?.configuration,
-  });
+  const [visible, setVisible] = useState<boolean>(false);
 
   const id = InstanceModel.detail?.id || params?.id;
 
@@ -52,146 +42,142 @@ const Config = () => {
       service.getConfigMetadata(id).then((config) => {
         setMetadata(config?.result);
       });
-      setState(
-        !!(
-          InstanceModel.detail?.configuration &&
-          Object.keys(InstanceModel.detail?.configuration).length > 0
-        ),
-      );
     }
-
-    return () => {};
   }, [id]);
 
-  const SchemaField = createSchemaField({
-    components: {
-      FormItem,
-      Input,
-      Password,
-      FormGrid,
-      PreviewText,
-    },
-  });
-
-  const configToSchema = (data: ConfigProperty[]) => {
-    const config = {};
-    data.forEach((item) => {
-      config[item.property] = {
-        type: 'string',
-        title: item.name,
-        'x-decorator': 'FormItem',
-        'x-component': componentMap[item.type.type],
-        'x-decorator-props': {
-          tooltip: item.description,
-        },
-      };
-    });
-    return config;
+  const isExit = (property: string) => {
+    return (
+      InstanceModel.detail?.cachedConfiguration &&
+      InstanceModel.detail?.cachedConfiguration[property] !== undefined &&
+      InstanceModel.detail?.configuration &&
+      InstanceModel.detail?.configuration[property] !==
+        InstanceModel.detail?.cachedConfiguration[property]
+    );
   };
 
-  const renderConfigCard = () => {
-    return metadata ? (
-      metadata?.map((item) => {
-        const itemSchema: ISchema = {
-          type: 'object',
-          properties: {
-            grid: {
-              type: 'void',
-              'x-component': 'FormGrid',
-              'x-component-props': {
-                minColumns: [2],
-                maxColumns: [2],
-              },
-              properties: configToSchema(item.properties),
-            },
-          },
-        };
-
+  const renderComponent = (item: any) => {
+    if (InstanceModel.detail?.configuration) {
+      const config = InstanceModel.detail?.configuration;
+      if (item.type.type === 'password' && config[item.property]?.length > 0) {
+        return '••••••';
+      }
+      if (isExit(item.property)) {
         return (
-          <>
-            <Divider />
-            <Card
-              title={item.name}
-              extra={
-                <Space>
-                  <a
-                    onClick={async () => {
-                      if (!state) {
-                        const values = (await form.submit()) as any;
-                        const resp = await service.modify(id || '', {
-                          id,
-                          configuration: { ...values },
-                        });
-                        if (resp.status === 200) {
-                          getDetail();
-                        }
-                      }
-                      setState(!state);
-                    }}
-                  >
-                    {state ? '编辑' : '保存'}
-                  </a>
-                  {InstanceModel.detail.state?.value !== 'notActive' && (
-                    <Popconfirm
-                      title="确认重新应用该配置?"
-                      onConfirm={async () => {
-                        const resp = await service.deployDevice(id || '');
-                        if (resp.status === 200) {
-                          message.success('操作成功');
-                          getDetail();
-                        }
-                      }}
-                    >
-                      <a>应用配置</a>
-                      <Tooltip title="修改配置后需重新应用后才能生效。">
-                        <Icon type="question-circle-o" />
-                      </Tooltip>
-                    </Popconfirm>
-                  )}
-                  {InstanceModel.detail?.aloneConfiguration && (
-                    <Popconfirm
-                      title="确认恢复默认配置?"
-                      onConfirm={async () => {
-                        const resp = await service.configurationReset(id || '');
-                        if (resp.status === 200) {
-                          message.success('恢复默认配置成功');
-                          getDetail();
-                        }
-                      }}
-                    >
-                      <a>恢复默认</a>
-                      <Tooltip
-                        title={`该设备单独编辑过配置信息,点击此将恢复成默认的配置信息,请谨慎操作。`}
-                      >
-                        <Icon type="question-circle-o" />
-                      </Tooltip>
-                    </Popconfirm>
-                  )}
-                </Space>
-              }
-            >
-              <PreviewText.Placeholder value="-">
-                <Form form={form}>
-                  <FormLayout labelCol={6} wrapperCol={16}>
-                    <SchemaField schema={itemSchema} />
-                  </FormLayout>
-                </Form>
-              </PreviewText.Placeholder>
-            </Card>
-          </>
+          <div>
+            <span style={{ marginRight: '10px' }}>{config[item.property]}</span>
+            <Tooltip title={`有效值:${config[item.property]}`}>
+              <QuestionCircleOutlined />
+            </Tooltip>
+          </div>
         );
-      })
-    ) : (
-      <Empty />
-    );
+      } else {
+        return <span>{config[item.property]}</span>;
+      }
+    } else {
+      return '--';
+    }
   };
 
   return (
-    <>
-      {renderConfigCard()}
-      <Divider />
-      <Tags />
-    </>
+    <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);
+              }}
+            >
+              <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">
+                  <UndoOutlined />
+                  恢复默认
+                </Button>
+                <Tooltip
+                  title={`该设备单独编辑过配置信息,点击此将恢复成默认的配置信息,请谨慎操作。`}
+                >
+                  <QuestionCircleOutlined />
+                </Tooltip>
+              </Popconfirm>
+            )}
+          </Space>,
+        ]}
+      >
+        {(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>
+      {visible && (
+        <Edit
+          metadata={metadata || []}
+          close={() => {
+            setVisible(false);
+            getDetail();
+          }}
+        />
+      )}
+    </div>
   );
 };
 

+ 25 - 15
src/pages/device/Instance/Detail/Info/index.tsx

@@ -1,4 +1,4 @@
-import { Card, Descriptions } from 'antd';
+import { Button, Card, Descriptions } from 'antd';
 import { InstanceModel } from '@/pages/device/Instance';
 import moment from 'moment';
 import { observer } from '@formily/react';
@@ -7,6 +7,8 @@ import Config from '@/pages/device/Instance/Detail/Config';
 import Save from '../../Save';
 import { useState } from 'react';
 import type { DeviceInstance } from '../../typings';
+import { EditOutlined } from '@ant-design/icons';
+import Tags from '@/pages/device/Instance/Detail/Tags';
 
 const Info = observer(() => {
   const intl = useIntl();
@@ -14,19 +16,25 @@ const Info = observer(() => {
 
   return (
     <>
-      <Card
-        title={'设备信息'}
-        extra={
-          <a
-            onClick={() => {
-              setVisible(true);
-            }}
-          >
-            编辑
-          </a>
-        }
-      >
-        <Descriptions size="small" column={3} bordered>
+      <Card>
+        <Descriptions
+          size="small"
+          column={3}
+          bordered
+          title={[
+            <span key={1}>设备信息</span>,
+            <Button
+              key={2}
+              type={'link'}
+              onClick={() => {
+                setVisible(true);
+              }}
+            >
+              <EditOutlined />
+              编辑
+            </Button>,
+          ]}
+        >
           <Descriptions.Item
             label={intl.formatMessage({
               id: 'pages.table.deviceId',
@@ -102,8 +110,10 @@ const Info = observer(() => {
             {InstanceModel.detail?.description}
           </Descriptions.Item>
         </Descriptions>
+        {InstanceModel.detail?.configuration &&
+          Object.keys(InstanceModel.detail?.configuration).length > 0 && <Config />}
+        {InstanceModel.detail?.tags && InstanceModel.detail?.tags.length > 0 && <Tags />}
       </Card>
-      <Config />
       <Save
         model={'edit'}
         data={{ ...InstanceModel?.detail, describe: InstanceModel?.detail?.description || '' }}

+ 69 - 52
src/pages/device/Instance/Detail/Config/Tags/index.tsx

@@ -1,31 +1,18 @@
-import { createSchemaField, FormProvider } from '@formily/react';
-import { Editable, FormItem, Input, ArrayTable } from '@formily/antd';
 import { createForm } from '@formily/core';
-import { Card, message } from 'antd';
-import { useIntl } from '@@/plugin-locale/localeExports';
+import { createSchemaField, FormProvider } from '@formily/react';
 import { InstanceModel, service } from '@/pages/device/Instance';
-import { useEffect, useState } from 'react';
+import { ArrayTable, FormItem, Input } from '@formily/antd';
+import { message, Modal } from 'antd';
+import { useIntl } from 'umi';
 
-const SchemaField = createSchemaField({
-  components: {
-    FormItem,
-    Editable,
-    Input,
-    ArrayTable,
-  },
-});
+interface Props {
+  close: () => void;
+  tags: any[];
+}
 
-const Tags = () => {
+const Edit = (props: Props) => {
+  const { tags } = props;
   const intl = useIntl();
-  const [tags, setTags] = useState<any[]>([]);
-
-  const tag = InstanceModel.detail?.tags;
-
-  useEffect(() => {
-    if (tag) {
-      setTags([...tag] || []);
-    }
-  }, [tag]);
 
   const form = createForm({
     initialValues: {
@@ -33,6 +20,14 @@ const Tags = () => {
     },
   });
 
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      ArrayTable,
+    },
+  });
+
   const schema = {
     type: 'object',
     properties: {
@@ -56,7 +51,7 @@ const Tags = () => {
                   type: 'string',
                   'x-decorator': 'FormItem',
                   'x-component': 'Input',
-                  'x-disabled': true,
+                  // 'x-disabled': true
                 },
               },
             },
@@ -73,7 +68,7 @@ const Tags = () => {
               properties: {
                 name: {
                   type: 'string',
-                  'x-decorator': 'Editable',
+                  'x-decorator': 'FormItem',
                   'x-component': 'Input',
                 },
               },
@@ -91,47 +86,69 @@ const Tags = () => {
               properties: {
                 value: {
                   type: 'string',
-                  'x-decorator': 'Editable',
+                  'x-decorator': 'FormItem',
                   'x-component': 'Input',
                 },
               },
             },
+            column4: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                width: 100,
+                title: '操作',
+                dataIndex: 'operations',
+              },
+              properties: {
+                item: {
+                  type: 'void',
+                  'x-component': 'FormItem',
+                  properties: {
+                    remove: {
+                      type: 'void',
+                      'x-component': 'ArrayTable.Remove',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+        properties: {
+          add: {
+            type: 'void',
+            'x-component': 'ArrayTable.Addition',
+            title: '添加',
           },
         },
       },
     },
   };
+
   return (
-    <Card
-      title={intl.formatMessage({
-        id: 'pages.device.instanceDetail.tags',
-        defaultMessage: '标签',
-      })}
-      extra={
-        <a
-          onClick={async () => {
-            const values = (await form.submit()) as any;
-            if (values?.tags) {
-              const resp = await service.saveTags(InstanceModel.detail?.id || '', values.tags);
-              if (resp.status === 200) {
-                InstanceModel.detail = { ...InstanceModel.detail, tags: values.tags };
-                message.success('操作成功!');
-              }
-            }
-          }}
-        >
-          {intl.formatMessage({
-            id: 'pages.device.instanceDetail.save',
-            defaultMessage: '保存',
-          })}
-        </a>
-      }
+    <Modal
+      title="编辑标签"
+      onCancel={() => {
+        props.close();
+      }}
+      visible
+      width={1000}
+      onOk={async () => {
+        const values: any = (await form.submit()) as any;
+        const list = (values?.tags || []).filter((item: any) => item?.id);
+        const resp = await service.saveTags(InstanceModel.detail?.id || '', list);
+        if (resp.status === 200) {
+          InstanceModel.detail = { ...InstanceModel.detail, tags: values.tags };
+          message.success('操作成功!');
+          props.close();
+        }
+      }}
     >
       <FormProvider form={form}>
         <SchemaField schema={schema} />
       </FormProvider>
-    </Card>
+    </Modal>
   );
 };
 
-export default Tags;
+export default Edit;

+ 63 - 0
src/pages/device/Instance/Detail/Tags/index.tsx

@@ -0,0 +1,63 @@
+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 = () => {
+  const intl = useIntl();
+  const [tags, setTags] = useState<any[]>([]);
+  const [visible, setVisible] = useState<boolean>(false);
+
+  const tag = InstanceModel.detail?.tags;
+
+  useEffect(() => {
+    if (tag) {
+      setTags([...tag] || []);
+    }
+  }, [tag]);
+
+  return (
+    <div style={{ width: '100%', marginTop: '20px' }}>
+      <Descriptions
+        style={{ marginBottom: 20 }}
+        bordered
+        size="small"
+        title={
+          <span>
+            {intl.formatMessage({
+              id: 'pages.device.instanceDetail.tags',
+              defaultMessage: '标签',
+            })}
+            <Button
+              type="link"
+              onClick={() => {
+                setVisible(true);
+              }}
+            >
+              <EditOutlined />
+              编辑
+            </Button>
+          </span>
+        }
+      >
+        {(tags || [])?.map((item: any) => (
+          <Descriptions.Item label={`${item.name}(${item.key})`} key={item.key}>
+            {item.value || '--'}
+          </Descriptions.Item>
+        ))}
+      </Descriptions>
+      {visible && (
+        <Edit
+          close={() => {
+            setVisible(false);
+          }}
+          tags={tags}
+        />
+      )}
+    </div>
+  );
+};
+
+export default Tags;

+ 0 - 106
src/pages/device/Product/Detail/Access/AccessConfig/index.less

@@ -1,109 +1,3 @@
-// .box {
-//   display: flex;
-//   justify-content: space-between;
-// }
-
-// .images {
-//   width: 64px;
-//   height: 64px;
-//   color: white;
-//   font-size: 18px;
-//   line-height: 64px;
-//   text-align: center;
-//   background: linear-gradient(
-//     128.453709216706deg,
-//     rgba(255, 255, 255, 1) 4%,
-//     rgba(113, 187, 255, 1) 43%,
-//     rgba(24, 144, 255, 1) 100%
-//   );
-//   border: 1px solid rgba(242, 242, 242, 1);
-//   border-radius: 50%;
-// }
-
-// .content {
-//   display: flex;
-//   flex-direction: column;
-//   width: calc(100% - 80px);
-// }
-
-// .top {
-//   display: flex;
-// }
-
-// .desc {
-//   width: 100%;
-//   margin-top: 10px;
-//   overflow: hidden;
-//   color: rgba(0, 0, 0, 0.55);
-//   font-weight: 400;
-//   font-size: 13px;
-//   white-space: nowrap;
-//   text-overflow: ellipsis;
-// }
-
-.context {
-  display: flex;
-  width: 100%;
-
-  .card {
-    display: flex;
-    flex-direction: column;
-    width: 100%;
-    margin-left: 10px;
-
-    .header {
-      .title {
-        width: 90%;
-        overflow: hidden;
-        font-weight: 700;
-        font-size: 18px;
-        white-space: nowrap;
-        text-overflow: ellipsis;
-      }
-
-      .desc {
-        width: 100%;
-        margin-top: 10px;
-        overflow: hidden;
-        color: #666;
-        font-weight: 400;
-        font-size: 12px;
-        white-space: nowrap;
-        text-overflow: ellipsis;
-      }
-    }
-
-    .container {
-      display: flex;
-      width: 100%;
-      height: 80px;
-      margin-top: 10px;
-
-      .server,
-      .procotol {
-        width: calc(50% - 20px);
-        margin-right: 10px;
-
-        .subTitle {
-          width: 100%;
-          overflow: hidden;
-          color: rgba(0, 0, 0, 0.75);
-          font-size: 12px;
-          white-space: nowrap;
-          text-overflow: ellipsis;
-        }
-
-        p {
-          width: 100%;
-          overflow: hidden;
-          white-space: nowrap;
-          text-overflow: ellipsis;
-        }
-      }
-    }
-  }
-}
-
 :global {
   .ant-pagination-item {
     display: none;

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

@@ -1,15 +1,13 @@
 import { useEffect, useState } from 'react';
-import { Badge, Button, Col, message, Modal, Pagination, Row } from 'antd';
+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 styles from './index.less';
 import Service from '@/pages/device/Product/service';
-import { TableCard } from '@/components';
-import { StatusColorEnum } from '@/components/BadgeStatus';
 
-const defaultImage = require('/public/images/device-access.png');
+import AccessConfigCard from '@/components/ProTableCard/CardItems/AccessConfig';
 
 interface Props {
   close: () => void;
@@ -145,51 +143,15 @@ const AccessConfig = (props: Props) => {
           <Col
             key={item.name}
             span={12}
-            // style={{
-            //   width: '100%',
-            //   borderColor: currrent?.id === item.id ? 'var(--ant-primary-color-active)' : 'red',
-            // }}
             onClick={() => {
               setCurrrent(item);
             }}
           >
-            <TableCard
-              showMask={false}
-              status={item.state.value}
-              statusText={item.state.text}
-              statusNames={{
-                enabled: StatusColorEnum.processing,
-                disabled: StatusColorEnum.error,
-              }}
-            >
-              <div className={styles.context}>
-                <div>
-                  <img width={88} height={88} src={defaultImage} alt={''} />
-                </div>
-                <div className={styles.card}>
-                  <div className={styles.header}>
-                    <div className={styles.title}>{item.name || '--'}</div>
-                    <div className={styles.desc}>{item.description || '--'}</div>
-                  </div>
-                  <div className={styles.container}>
-                    <div className={styles.server}>
-                      <div className={styles.subTitle}>{item?.channelInfo?.name || '--'}</div>
-                      <div style={{ width: '100%' }}>
-                        {item.channelInfo?.addresses.map((i: any) => (
-                          <p key={i.address}>
-                            <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
-                          </p>
-                        ))}
-                      </div>
-                    </div>
-                    <div className={styles.procotol}>
-                      <div className={styles.subTitle}>{item?.protocolDetail?.name || '--'}</div>
-                      <p>{item.protocolDetail?.description || '--'}</p>
-                    </div>
-                  </div>
-                </div>
-              </div>
-            </TableCard>
+            <AccessConfigCard
+              {...item}
+              showTool={false}
+              activeStyle={currrent?.id === item.id ? 'active' : ''}
+            />
           </Col>
         ))}
       </Row>

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

@@ -12,6 +12,7 @@ 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 = {
   string: 'Input',
@@ -286,6 +287,24 @@ const Access = () => {
                 minColumns: 1,
                 columnGap: 48,
               },
+              title: (
+                <TitleComponent
+                  data={
+                    <span>
+                      {item.name}
+                      <Tooltip title="此配置来自于该产品接入方式所选择的协议">
+                        <QuestionCircleOutlined />
+                      </Tooltip>
+                    </span>
+                  }
+                />
+              ),
+              'x-decorator': 'FormItem',
+              'x-decorator-props': {
+                gridSpan: 1,
+                labelAlign: 'left',
+                layout: 'vertical',
+              },
               properties: configToSchema(item.properties),
             },
           },
@@ -345,20 +364,24 @@ const Access = () => {
           <Col span={12}>
             <div className={styles.config}>
               <div className={styles.item}>
-                <div className={styles.title}>
-                  接入方式
-                  <Button
-                    size="small"
-                    type="primary"
-                    ghost
-                    style={{ marginLeft: 20 }}
-                    onClick={() => {
-                      setConfigVisible(true);
-                    }}
-                  >
-                    更换
-                  </Button>
-                </div>
+                <TitleComponent
+                  data={
+                    <span>
+                      接入方式
+                      <Button
+                        size="small"
+                        type="primary"
+                        ghost
+                        style={{ marginLeft: 20 }}
+                        onClick={() => {
+                          setConfigVisible(true);
+                        }}
+                      >
+                        更换
+                      </Button>
+                    </span>
+                  }
+                />
                 <div className={styles.context}>
                   {providers.find((i) => i.id === access?.provider)?.name || '--'}
                 </div>
@@ -372,7 +395,7 @@ const Access = () => {
               </div>
 
               <div className={styles.item}>
-                <div className={styles.title}>消息协议</div>
+                <TitleComponent data={'消息协议'} />
                 <div className={styles.context}>{access?.protocolDetail?.name || '--'}</div>
                 {config?.document && (
                   <div className={styles.context}>
@@ -382,7 +405,7 @@ const Access = () => {
               </div>
 
               <div className={styles.item}>
-                <div className={styles.title}>连接信息</div>
+                <TitleComponent data={'连接信息'} />
                 {(networkList.find((i) => i.id === access?.channelId)?.addresses || []).length > 0
                   ? (networkList.find((i) => i.id === access?.channelId)?.addresses || []).map(
                       (item: any) => (
@@ -398,15 +421,7 @@ const Access = () => {
                   : '暂无连接信息'}
               </div>
 
-              <div className={styles.item}>
-                <div className={styles.title}>
-                  认证配置
-                  <Tooltip title="此配置来自于该产品接入方式所选择的协议">
-                    <QuestionCircleOutlined />
-                  </Tooltip>
-                </div>
-                {renderConfigCard()}
-              </div>
+              <div className={styles.item}>{renderConfigCard()}</div>
             </div>
           </Col>
           <Col span={12}>

+ 40 - 72
src/pages/link/AccessConfig/Detail/Access/index.tsx

@@ -16,7 +16,7 @@ import { useEffect, useState } from 'react';
 import styles from './index.less';
 import { service } from '@/pages/link/AccessConfig';
 import encodeQuery from '@/utils/encodeQuery';
-import { useHistory, useLocation } from 'umi';
+import { useHistory } from 'umi';
 import ReactMarkdown from 'react-markdown';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 import { ExclamationCircleFilled } from '@ant-design/icons';
@@ -24,12 +24,9 @@ import { ExclamationCircleFilled } from '@ant-design/icons';
 interface Props {
   change: () => void;
   data: any;
+  provider: any;
 }
 
-type LocationType = {
-  id?: string;
-};
-
 const Access = (props: Props) => {
   const [form] = Form.useForm();
 
@@ -38,11 +35,9 @@ const Access = (props: Props) => {
   const [current, setCurrent] = useState<number>(0);
   const [networkList, setNetworkList] = useState<any[]>([]);
   const [procotolList, setProcotolList] = useState<any[]>([]);
-  const [access, setAccess] = useState<any>({});
   const [procotolCurrent, setProcotolCurrent] = useState<string>('');
   const [networkCurrent, setNetworkCurrent] = useState<string>('');
   const [config, setConfig] = useState<any>();
-  const [providers, setProviders] = useState<any[]>([]);
 
   const MetworkTypeMapping = new Map();
   MetworkTypeMapping.set('websocket-server', 'WEB_SOCKET_SERVER');
@@ -78,48 +73,32 @@ const Access = (props: Props) => {
     });
   };
 
-  const queryProviders = () => {
-    service.getProviders().then((resp) => {
-      if (resp.status === 200) {
-        setProviders(resp.result);
-      }
-    });
-  };
-
   useEffect(() => {
-    if (props.data?.id) {
-      queryNetworkList(props.data?.id);
+    if (props.provider?.id) {
+      queryNetworkList(props.provider?.id);
       setCurrent(0);
     }
-  }, [props.data]);
-
-  const location = useLocation<LocationType>();
-
-  const params = new URLSearchParams(location.search);
+  }, [props.provider]);
 
   useEffect(() => {
-    if (params.get('id')) {
-      service.detail(params.get('id') || '').then((resp) => {
-        setAccess(resp.result);
-        setProcotolCurrent(resp.result?.protocol);
-        setNetworkCurrent(resp.result?.channelId);
-        form.setFieldsValue({
-          name: resp.result?.name,
-          description: resp.result?.description,
-        });
-        queryProviders();
-        setCurrent(0);
-        queryNetworkList(resp.result?.provider);
+    if (props.data?.id) {
+      setProcotolCurrent(props.data?.protocol);
+      setNetworkCurrent(props.data?.channelId);
+      form.setFieldsValue({
+        name: props.data?.name,
+        description: props.data?.description,
       });
+      setCurrent(0);
+      queryNetworkList(props.data?.provider);
     }
-  }, []);
+  }, [props.data]);
 
   const next = () => {
     if (current === 0) {
       if (!networkCurrent) {
         message.error('请选择网络组件!');
       } else {
-        queryProcotolList(props.data?.id || access?.provider);
+        queryProcotolList(props.provider?.id);
         setCurrent(current + 1);
       }
     }
@@ -128,7 +107,7 @@ const Access = (props: Props) => {
         message.error('请选择消息协议!');
       } else {
         service
-          .getConfigView(procotolCurrent, ProcotoleMapping.get(props.data?.id || access?.provider))
+          .getConfigView(procotolCurrent, ProcotoleMapping.get(props.provider?.id))
           .then((resp) => {
             if (resp.status === 200) {
               setConfig(resp.result);
@@ -301,7 +280,7 @@ const Access = (props: Props) => {
                 placeholder="请输入名称"
                 onSearch={(value: string) => {
                   queryNetworkList(
-                    props.data?.id || access?.provider,
+                    props.provider?.id,
                     encodeQuery({
                       terms: {
                         name$LIKE: `%${value}%`,
@@ -318,7 +297,7 @@ const Access = (props: Props) => {
                   const tab: any = window.open(`${origin}/#${url}`);
                   tab!.onTabSaveSuccess = (value: any) => {
                     if (value.status === 200) {
-                      queryNetworkList(props.data?.id || access?.provider);
+                      queryNetworkList(props.provider?.id);
                     }
                   };
                 }}
@@ -377,7 +356,7 @@ const Access = (props: Props) => {
                         const tab: any = window.open(`${origin}/#${url}`);
                         tab!.onTabSaveSuccess = (value: any) => {
                           if (value.status === 200) {
-                            queryNetworkList(props.data?.id || access?.provider);
+                            queryNetworkList(props.provider?.id);
                           }
                         };
                       }}
@@ -403,7 +382,7 @@ const Access = (props: Props) => {
                 placeholder="请输入名称"
                 onSearch={(value: string) => {
                   queryProcotolList(
-                    props.data?.id || access?.provider,
+                    props.provider?.id,
                     encodeQuery({
                       terms: {
                         name$LIKE: `%${value}%`,
@@ -416,10 +395,11 @@ const Access = (props: Props) => {
               <Button
                 type="primary"
                 onClick={() => {
-                  const tab: any = window.open(`${origin}/#/link/Protocol?save=true`);
+                  const url = getMenuPathByCode(MENUS_CODE['link/Protocol?save=true']);
+                  const tab: any = window.open(`${origin}/#${url}`);
                   tab!.onTabSaveSuccess = (value: any) => {
                     if (value) {
-                      queryProcotolList(props.data?.id || access?.provider);
+                      queryProcotolList(props.provider?.id);
                     }
                   };
                 }}
@@ -436,7 +416,7 @@ const Access = (props: Props) => {
                       style={{
                         width: '100%',
                         borderColor:
-                          networkCurrent === item.id ? 'var(--ant-primary-color-active)' : '',
+                          procotolCurrent === item.id ? 'var(--ant-primary-color-active)' : '',
                       }}
                       hoverable
                       onClick={() => {
@@ -458,10 +438,11 @@ const Access = (props: Props) => {
                     暂无数据
                     <a
                       onClick={() => {
-                        const tab: any = window.open(`${origin}/#/link/Protocol?save=true`);
+                        const url = getMenuPathByCode(MENUS_CODE['link/Protocol?save=true']);
+                        const tab: any = window.open(`${origin}/#${url}`);
                         tab!.onTabSaveSuccess = (value: any) => {
                           if (value) {
-                            queryProcotolList(props.data?.id || access?.provider);
+                            queryProcotolList(props.provider?.id);
                           }
                         };
                       }}
@@ -502,14 +483,14 @@ const Access = (props: Props) => {
                       try {
                         const values = await form.validateFields();
                         // 编辑还是保存
-                        if (!params.get('id')) {
+                        if (!props.data?.id) {
                           service
                             .save({
                               name: values.name,
                               description: values.description,
-                              provider: props.data.id,
+                              provider: props.provider.id,
                               protocol: procotolCurrent,
-                              transport: ProcotoleMapping.get(props.data.id),
+                              transport: ProcotoleMapping.get(props.provider.id),
                               channel: 'network', // 网络组件
                               channelId: networkCurrent,
                             })
@@ -526,12 +507,10 @@ const Access = (props: Props) => {
                         } else {
                           service
                             .update({
-                              id: access?.id,
+                              ...props.data,
                               name: values.name,
                               description: values.description,
-                              provider: access?.provider,
                               protocol: procotolCurrent,
-                              transport: access?.transport,
                               channel: 'network', // 网络组件
                               channelId: networkCurrent,
                             })
@@ -560,30 +539,19 @@ const Access = (props: Props) => {
               <div className={styles.config}>
                 <div className={styles.item}>
                   <div className={styles.title}>接入方式</div>
-                  <div className={styles.context}>
-                    {props.data?.name ||
-                      providers.find((i) => i.id === access?.provider)?.name ||
-                      '--'}
-                  </div>
-                  <div className={styles.context}>
-                    {((props.data?.description ||
-                      providers.find((i) => i.id === access?.provider)?.description) && (
-                      <span>
-                        {props.data?.description ||
-                          providers.find((i) => i.id === access?.provider)?.description}
-                      </span>
-                    )) ||
-                      '--'}
-                  </div>
+                  <div className={styles.context}>{props.provider?.name || '--'}</div>
+                  <div className={styles.context}>{props.provider?.description || '--'}</div>
                 </div>
                 <div className={styles.item}>
                   <div className={styles.title}>消息协议</div>
                   <div className={styles.context}>
                     {procotolList.find((i) => i.id === procotolCurrent)?.name || '--'}
                   </div>
-                  <div className={styles.context}>
-                    {config?.document ? <ReactMarkdown>{config?.document}</ReactMarkdown> : '--'}
-                  </div>
+                  {config?.document && (
+                    <div className={styles.context}>
+                      {<ReactMarkdown>{config?.document}</ReactMarkdown> || '--'}
+                    </div>
+                  )}
                 </div>
                 <div className={styles.item}>
                   <div className={styles.title}>网络组件</div>
@@ -604,8 +572,8 @@ const Access = (props: Props) => {
                 {config?.routes && config?.routes?.length > 0 && (
                   <div className={styles.item}>
                     <div style={{ fontWeight: '600', marginBottom: 10 }}>
-                      {access?.provider === 'mqtt-server-gateway' ||
-                      access?.provider === 'mqtt-client-gateway'
+                      {props.data?.provider === 'mqtt-server-gateway' ||
+                      props.data?.provider === 'mqtt-client-gateway'
                         ? 'topic'
                         : 'URL信息'}
                     </div>

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

@@ -56,6 +56,7 @@ const Detail = () => {
         return (
           <Access
             data={data}
+            provider={provider}
             change={() => {
               setVisible(true);
             }}

+ 10 - 39
src/pages/link/AccessConfig/index.less

@@ -1,43 +1,14 @@
-// .box {
-//   display: flex;
-//   justify-content: space-between;
-// }
-
-// .images {
-//   width: 64px;
-//   height: 64px;
-//   color: white;
-//   font-size: 18px;
-//   line-height: 64px;
-//   text-align: center;
-//   background: linear-gradient(
-//     128.453709216706deg,
-//     rgba(255, 255, 255, 1) 4%,
-//     rgba(113, 187, 255, 1) 43%,
-//     rgba(24, 144, 255, 1) 100%
-//   );
-//   border: 1px solid rgba(242, 242, 242, 1);
-//   border-radius: 50%;
-// }
-
-// .content {
-//   display: flex;
-//   flex-direction: column;
-//   width: calc(100% - 80px);
-// }
-
-// .top {
-//   display: flex;
-//   justify-content: space-between;
-
-//   .left {
-//     display: flex;
-//   }
+.tableCardDisabled {
+  width: 100%;
+  background: url('/images/access-config-diaabled.png') no-repeat;
+  background-size: 100% 100%;
+}
 
-//   .action a {
-//     margin: 0 5px;
-//   }
-// }
+.tableCardEnabled {
+  width: 100%;
+  background: url('/images/access-config-enabled.png') no-repeat;
+  background-size: 100% 100%;
+}
 
 .context {
   display: flex;

+ 5 - 46
src/pages/link/AccessConfig/index.tsx

@@ -1,17 +1,13 @@
-import { TableCard } from '@/components';
 import SearchComponent from '@/components/SearchComponent';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
-import { StatusColorEnum } from '@/components/BadgeStatus';
 import { PageContainer } from '@ant-design/pro-layout';
 import type { ProColumns } from '@jetlinks/pro-table';
-import { Badge, Button, Card, Col, Empty, message, Pagination, Popconfirm, Row } from 'antd';
+import { Button, Card, Col, Empty, message, Pagination, Popconfirm, Row } from 'antd';
 import { useEffect, useState } from 'react';
 import { useHistory } from 'umi';
-import styles from './index.less';
 import Service from './service';
 import { CheckCircleOutlined, DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
-
-const defaultImage = require('/public/images/device-access.png');
+import AccessConfigCard from '@/components/ProTableCard/CardItems/AccessConfig';
 
 export const service = new Service('gateway/device');
 
@@ -72,7 +68,6 @@ const AccessConfig = () => {
       <Card>
         <SearchComponent
           field={columns}
-          // pattern={'simple'}
           enableSave={false}
           onSearch={(data: any) => {
             const dt = {
@@ -96,8 +91,8 @@ const AccessConfig = () => {
           <Row gutter={[16, 16]} style={{ marginTop: 10 }}>
             {(dataSource?.data || []).map((item: any) => (
               <Col key={item.id} span={12}>
-                <TableCard
-                  showMask={false}
+                <AccessConfigCard
+                  {...item}
                   actions={[
                     <Button
                       key="edit"
@@ -166,43 +161,7 @@ const AccessConfig = () => {
                       </Popconfirm>
                     </Button>,
                   ]}
-                  status={item.state.value}
-                  statusText={item.state.text}
-                  statusNames={{
-                    enabled: StatusColorEnum.processing,
-                    disabled: StatusColorEnum.error,
-                  }}
-                >
-                  <div className={styles.context}>
-                    <div>
-                      <img width={88} height={88} src={defaultImage} alt={''} />
-                    </div>
-                    <div className={styles.card}>
-                      <div className={styles.header}>
-                        <div className={styles.title}>{item.name || '--'}</div>
-                        <div className={styles.desc}>{item.description || '--'}</div>
-                      </div>
-                      <div className={styles.container}>
-                        <div className={styles.server}>
-                          <div className={styles.subTitle}>{item?.channelInfo?.name || '--'}</div>
-                          <div style={{ width: '100%' }}>
-                            {item.channelInfo?.addresses.map((i: any, index: number) => (
-                              <p key={i.address + `_address${index}`}>
-                                <Badge color={i.health === -1 ? 'red' : 'green'} text={i.address} />
-                              </p>
-                            ))}
-                          </div>
-                        </div>
-                        <div className={styles.procotol}>
-                          <div className={styles.subTitle}>
-                            {item?.protocolDetail?.name || '--'}
-                          </div>
-                          <p>{item.protocolDetail?.description || '--'}</p>
-                        </div>
-                      </div>
-                    </div>
-                  </div>
-                </TableCard>
+                />
               </Col>
             ))}
           </Row>

+ 7 - 5
src/pages/media/Cascade/Save/index.tsx

@@ -27,12 +27,14 @@ const Save = () => {
   const id = location?.query?.id || '';
 
   const checkSIP = (_: any, value: { host: string; port: number }) => {
-    if (!value) {
-      return Promise.reject(new Error('请输入SIP'));
-    } else if (Number(value.port) < 1 || Number(value.port) > 65535) {
-      return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
-    } else if (!testIP(value.host)) {
+    if (!value || !value.host) {
+      return Promise.reject(new Error('请输入API HOST'));
+    } else if (value?.host && !testIP(value.host)) {
       return Promise.reject(new Error('请输入正确的IP地址'));
+    } else if (!value?.port) {
+      return Promise.reject(new Error('请输入端口'));
+    } else if ((value?.port && Number(value.port) < 1) || Number(value.port) > 65535) {
+      return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
     }
     return Promise.resolve();
   };

+ 1 - 1
src/pages/media/Cascade/index.tsx

@@ -15,7 +15,7 @@ import type { CascadeItem } from '@/pages/media/Cascade/typings';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import SearchComponent from '@/components/SearchComponent';
 import { ProTableCard } from '@/components';
-import CascadeCard from '@/components//ProTableCard/CardItems/cascade';
+import CascadeCard from '@/components/ProTableCard/CardItems/cascade';
 import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
 import { useHistory } from 'umi';
 import Service from './service';

+ 1 - 1
src/pages/media/SplitScreen/index.less

@@ -68,7 +68,7 @@
       }
 
       .player-tools {
-        flex-basis: 290px;
+        flex-basis: 280px;
         padding: 50px 12px 0 12px;
       }
     }

+ 16 - 8
src/pages/media/SplitScreen/index.tsx

@@ -1,26 +1,34 @@
 // 视频分屏
 import { PageContainer } from '@ant-design/pro-layout';
-import { Card } from 'antd';
+import { Card, message } from 'antd';
 import LeftTree from './tree';
 import { ScreenPlayer } from '@/components';
 import { ptzStop, ptzTool } from './service';
 import { useState } from 'react';
+import { ptzStart } from './service';
 import './index.less';
 
 const SplitScreen = () => {
   const [deviceId, setDeviceId] = useState('');
-  const [channelId] = useState('');
-  const [url] = useState('');
+  const [channelId, setChannelId] = useState('');
+  const [url, setUrl] = useState('');
+
+  const mediaStart = async (dId: string, cId: string) => {
+    setChannelId(dId);
+    setDeviceId(cId);
+    const resp = await ptzStart(dId, cId);
+    if (resp.status === 200) {
+      setUrl(resp.result.mp4);
+    } else {
+      message.error(resp.result);
+    }
+  };
 
   return (
     <PageContainer>
       <Card>
         <div className="split-screen">
-          <LeftTree
-            onSelect={(id) => {
-              setDeviceId(id);
-            }}
-          />
+          <LeftTree onSelect={mediaStart} />
           <div className="right-content">
             <ScreenPlayer
               id={deviceId}

+ 1 - 1
src/pages/media/SplitScreen/service.ts

@@ -9,7 +9,7 @@ export const getMediaTree = (data?: any) =>
 
 // 开始直播
 export const ptzStart = (deviceId: string, channelId: string) =>
-  request(`${url}/device/${deviceId}/${channelId}/_pzt/_start`, { method: 'POST' });
+  request(`${url}/device/${deviceId}/${channelId}/_start`, { method: 'POST' });
 
 // 云台控制-停止
 export const ptzStop = (deviceId: string, channelId: string) =>

+ 13 - 33
src/pages/media/SplitScreen/tree.tsx

@@ -1,11 +1,10 @@
-import { Input, Tree } from 'antd';
+import { Tree } from 'antd';
 import { getMediaTree, queryChannel } from './service';
-import React, { useCallback, useEffect, useState, useRef } from 'react';
-import { SearchOutlined, VideoCameraOutlined } from '@ant-design/icons';
-import { debounce } from 'lodash';
+import React, { useEffect, useState } from 'react';
+import { VideoCameraOutlined } from '@ant-design/icons';
 
 type LeftTreeTYpe = {
-  onSelect?: (deviceId: string) => void;
+  onSelect?: (deviceId: string, channelId: string) => void;
 };
 
 interface DataNode {
@@ -23,7 +22,6 @@ interface DataNode {
 
 const LeftTree = (props: LeftTreeTYpe) => {
   const [treeData, setTreeData] = useState<DataNode[]>([]);
-  const treeDataCache = useRef<DataNode[]>([]);
 
   /**
    * 是否为子节点
@@ -132,36 +130,18 @@ const LeftTree = (props: LeftTreeTYpe) => {
     });
   };
 
-  const ThreeNodeSearch = useCallback(
-    (e: any) => {
-      const value = e.target.value;
-      // 缓存treeData数据
-      if (!treeDataCache.current.length) {
-        treeDataCache.current = treeData;
-      }
-
-      if (value) {
-        setTreeData(treeData.filter((node) => node.name.includes(value)));
-      } else {
-        setTreeData(treeDataCache.current);
-        treeDataCache.current = [];
-      }
-    },
-    [treeData],
-  );
-
   useEffect(() => {
     getDeviceList();
   }, []);
 
   return (
     <div className="left-content">
-      <Input
-        className={'left-search'}
-        placeholder={'请输入设备名称'}
-        suffix={<SearchOutlined />}
-        onChange={debounce(ThreeNodeSearch, 300)}
-      />
+      {/*<Input*/}
+      {/*  className={'left-search'}*/}
+      {/*  placeholder={'请输入设备名称'}*/}
+      {/*  suffix={<SearchOutlined />}*/}
+      {/*  onChange={debounce(ThreeNodeSearch, 300)}*/}
+      {/*/>*/}
       <Tree
         showIcon
         showLine={{ showLeafIcon: false }}
@@ -170,9 +150,9 @@ const LeftTree = (props: LeftTreeTYpe) => {
           title: 'name',
           key: 'id',
         }}
-        onSelect={(key) => {
-          if (props.onSelect) {
-            props.onSelect(key[0] as string);
+        onSelect={(_, { node }: any) => {
+          if (props.onSelect && node.isLeaf) {
+            props.onSelect(node.deviceId, node.channelId);
           }
         }}
         loadData={onLoadData}

+ 32 - 23
src/pages/media/Stream/Detail/index.tsx

@@ -171,13 +171,20 @@ const Detail = () => {
   const checkSIP = (_: any, value: { host: string; port: number }) => {
     if (!value || !value.host) {
       return Promise.reject(new Error('请输入API HOST'));
-    } else if ((value?.port && Number(value.port) < 1) || Number(value.port) > 65535) {
-      return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
     } else if (value?.host && !testIP(value.host)) {
       return Promise.reject(new Error('请输入正确的IP地址'));
+    } else if (!value?.port) {
+      return Promise.reject(new Error('请输入端口'));
+    } else if ((value?.port && Number(value.port) < 1) || Number(value.port) > 65535) {
+      return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
     }
     return Promise.resolve();
   };
+
+  const testPort = (value: any) => {
+    return (value && Number(value) < 1) || Number(value) > 65535;
+  };
+
   const checkRIP = (
     _: any,
     value: {
@@ -189,25 +196,29 @@ const Detail = () => {
   ) => {
     if (!value || !value.rtpIp) {
       return Promise.reject(new Error('请输入RTP IP'));
-    } else if (value.rtpIp && !testIP(value.rtpIp)) {
+    } else if (value?.rtpIp && !testIP(value.rtpIp)) {
       return Promise.reject(new Error('请输入正确的IP地址'));
+    } else if (!value.dynamicRtpPort) {
+      if (value.rtpIp && !testIP(value.rtpIp)) {
+        return Promise.reject(new Error('请输入正确的IP地址'));
+      }
+      if (!value?.rtpPort) {
+        return Promise.reject(new Error('请输入端口'));
+      }
+      if (testPort(value?.rtpPort)) {
+        return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
+      }
     } else if (value.dynamicRtpPort) {
       if (value.dynamicRtpPortRange) {
-        if (value.dynamicRtpPortRange?.[0]) {
-          if (
-            Number(value.dynamicRtpPortRange?.[0]) < 1 ||
-            Number(value.dynamicRtpPortRange?.[0]) > 65535
-          ) {
-            return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
-          }
+        if (!value.dynamicRtpPortRange?.[0]) {
+          return Promise.reject(new Error('请输入起始端口'));
+        } else if (testPort(value.dynamicRtpPortRange?.[0])) {
+          return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
         }
-        if (value.dynamicRtpPortRange?.[1]) {
-          if (
-            Number(value.dynamicRtpPortRange?.[1]) < 1 ||
-            Number(value.dynamicRtpPortRange?.[1]) > 65535
-          ) {
-            return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
-          }
+        if (!value.dynamicRtpPortRange?.[1]) {
+          return Promise.reject(new Error('请输入终止端口'));
+        } else if (testPort(value.dynamicRtpPortRange?.[1])) {
+          return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
         }
         if (
           value.dynamicRtpPortRange?.[0] &&
@@ -216,10 +227,8 @@ const Detail = () => {
         ) {
           return Promise.reject(new Error('终止端口需大于等于起始端'));
         }
-      }
-    } else {
-      if ((value.rtpPort && Number(value.rtpPort) < 1) || Number(value.rtpPort) > 65535) {
-        return Promise.reject(new Error('端口请输入1~65535之间的正整数'));
+      } else if (!value.dynamicRtpPortRange) {
+        return Promise.reject(new Error('请输入端口'));
       }
     }
     return Promise.resolve();
@@ -316,7 +325,7 @@ const Detail = () => {
                   </span>
                 }
                 name="api"
-                rules={[{ validator: checkSIP }]}
+                rules={[{ required: true }, { validator: checkSIP }]}
               >
                 <SipComponent />
               </Form.Item>
@@ -335,7 +344,7 @@ const Detail = () => {
                   </span>
                 }
                 name="rtp"
-                rules={[{ validator: checkRIP }]}
+                rules={[{ required: true }, { validator: checkRIP }]}
               >
                 <RTPComponent />
               </Form.Item>

+ 82 - 0
src/pages/notice/Config/Debug/index.tsx

@@ -0,0 +1,82 @@
+import { Button, Modal } from 'antd';
+import { useMemo } from 'react';
+import { createForm } from '@formily/core';
+import { createSchemaField, observer } from '@formily/react';
+import { Form, FormItem, Input, Select } from '@formily/antd';
+import { ISchema } from '@formily/json-schema';
+import { state } from '@/pages/notice/Template';
+import { useLocation } from 'umi';
+import { useAsyncDataSource } from '@/utils/util';
+
+const Debug = observer(() => {
+  const location = useLocation<{ id: string }>();
+  const id = (location as any).query?.id;
+
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        effects() {},
+      }),
+    [],
+  );
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+    },
+  });
+
+  console.log(id, 'testt');
+
+  const getTemplate = () => {};
+  // const getConfig = () =>
+  //   configService
+  //     .queryNoPagingPost({
+  //       terms: [{column: 'type$IN', value: id}],
+  //     })
+  //     .then((resp: any) => {
+  //       return resp.result?.map((item) => ({
+  //         label: item.name,
+  //         value: item.id,
+  //       }));
+  //     });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      configId: {
+        title: '通知模版',
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        'x-reactions': '{{useAsyncDataSource(getTemplate)}}',
+      },
+      bianliang: {
+        title: '变量',
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+      },
+    },
+  };
+  return (
+    <Modal
+      width="40vw"
+      visible={state.debug}
+      onCancel={() => (state.debug = false)}
+      footer={
+        <Button type="primary" onClick={() => (state.debug = false)}>
+          关闭
+        </Button>
+      }
+    >
+      <Form form={form} layout={'vertical'}>
+        <SchemaField schema={schema} scope={{ getTemplate, useAsyncDataSource }} />
+      </Form>
+    </Modal>
+  );
+});
+export default Debug;

+ 31 - 16
src/pages/notice/Config/Detail/index.tsx

@@ -1,9 +1,9 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { createForm, onFieldValueChange } from '@formily/core';
-import { Card, Col, Input, Row } from 'antd';
+import { Card, Col, Input, message, Row } from 'antd';
 import { ISchema } from '@formily/json-schema';
-import { useMemo } from 'react';
-import { createSchemaField } from '@formily/react';
+import { useEffect, useMemo } from 'react';
+import { createSchemaField, observer } from '@formily/react';
 import {
   FormButtonGroup,
   FormItem,
@@ -25,8 +25,9 @@ import { useAsyncDataSource } from '@/utils/util';
 import { useParams } from 'umi';
 import { typeList } from '@/pages/notice';
 import FUpload from '@/components/Upload';
+import { state } from '@/pages/notice/Config';
 
-const Detail = () => {
+const Detail = observer(() => {
   const { id } = useParams<{ id: string }>();
 
   const form = useMemo(
@@ -37,8 +38,8 @@ const Detail = () => {
           onFieldValueChange('type', async (field, f) => {
             const type = field.value;
             if (!type) return;
-            f.setFieldState('provider', (state) => {
-              state.value = undefined;
+            f.setFieldState('provider', (state1) => {
+              state1.value = undefined;
               // state.dataSource = providerRef.current
               //   .find((item) => type === item.id)
               //   ?.providerInfos.map((i) => ({ label: i.name, value: i.id }));
@@ -54,6 +55,12 @@ const Detail = () => {
     [id],
   );
 
+  useEffect(() => {
+    if (state.current) {
+      form.setValues(state.current);
+    }
+  }, []);
+
   const SchemaField = createSchemaField({
     components: {
       FormItem,
@@ -113,7 +120,7 @@ const Detail = () => {
             type: 'void',
             properties: {
               corpId: {
-                title: 'corpID',
+                title: 'corpId',
                 'x-component': 'Input',
                 'x-decorator': 'FormItem',
                 // 企业消息
@@ -139,8 +146,8 @@ const Detail = () => {
                   },
                 },
               },
-              AppId: {
-                title: 'appId',
+              appId: {
+                title: 'appID',
                 'x-component': 'Input',
                 'x-decorator': 'FormItem',
                 'x-reactions': {
@@ -152,8 +159,8 @@ const Detail = () => {
                   },
                 },
               },
-              AppSecret: {
-                title: 'appSecret',
+              appSecret: {
+                title: 'AppSecret',
                 'x-component': 'Input',
                 'x-decorator': 'FormItem',
                 'x-reactions': {
@@ -172,7 +179,7 @@ const Detail = () => {
             type: 'void',
             'x-visible': id === 'dingTalk',
             properties: {
-              AppKey: {
+              appKey: {
                 title: 'AppKey',
                 'x-component': 'Input',
                 'x-decorator': 'FormItem',
@@ -186,7 +193,7 @@ const Detail = () => {
                   },
                 },
               },
-              AppSecret: {
+              appSecret: {
                 title: 'AppSecret',
                 'x-component': 'Input',
                 'x-decorator': 'FormItem',
@@ -200,7 +207,7 @@ const Detail = () => {
                   },
                 },
               },
-              Webhook: {
+              webhook: {
                 title: 'webHook',
                 'x-component': 'Input',
                 'x-decorator': 'FormItem',
@@ -301,6 +308,14 @@ const Detail = () => {
     },
   };
 
+  const handleSave = async () => {
+    const data: ConfigItem = await form.submit();
+    const response: any = await service.save(data);
+    if (response?.status === 200) {
+      message.success('保存成功');
+      history.back();
+    }
+  };
   return (
     <PageContainer>
       <Card>
@@ -310,7 +325,7 @@ const Detail = () => {
               <SchemaField scope={{ useAsyncDataSource, getTypes }} schema={schema} />
               <FormButtonGroup.Sticky>
                 <FormButtonGroup.FormItem>
-                  <Submit>保存</Submit>
+                  <Submit onSubmit={handleSave}>保存</Submit>
                 </FormButtonGroup.FormItem>
               </FormButtonGroup.Sticky>
             </Form>
@@ -322,6 +337,6 @@ const Detail = () => {
       </Card>
     </PageContainer>
   );
-};
+});
 
 export default Detail;

+ 74 - 0
src/pages/notice/Config/Log/index.tsx

@@ -0,0 +1,74 @@
+import { Modal } from 'antd';
+import { observer } from '@formily/react';
+import { service, state } from '..';
+import ProTable, { ProColumns } from '@jetlinks/pro-table';
+import SearchComponent from '@/components/SearchComponent';
+import { useLocation } from 'umi';
+import { InfoCircleOutlined } from '@ant-design/icons';
+
+const Log = observer(() => {
+  const location = useLocation<{ id: string }>();
+  const id = (location as any).query?.id;
+
+  const columns: ProColumns<LogItem>[] = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+    },
+    {
+      dataIndex: 'sendTime',
+      title: '发送时间',
+    },
+    {
+      dataIndex: 'state',
+      title: '状态',
+    },
+    {
+      dataIndex: 'action',
+      title: '操作',
+      render: (text, record) => [
+        <a
+          onClick={() => {
+            Modal.info({
+              title: '详情信息',
+              content: (
+                <div>
+                  <p>some messages...some messages...</p>
+                  <p>some messages...some messages...</p>
+                  {JSON.stringify(record)}
+                </div>
+              ),
+              onOk() {},
+            });
+          }}
+        >
+          <InfoCircleOutlined />
+        </a>,
+      ],
+    },
+  ];
+  return (
+    <Modal onCancel={() => (state.log = false)} title="通知记录" width={'70vw'} visible={state.log}>
+      <SearchComponent
+        defaultParam={[{ column: 'type$IN', value: id }]}
+        field={columns}
+        onSearch={(data) => {
+          // actionRef.current?.reset?.();
+          // setParam(data);
+          console.log(data);
+        }}
+        enableSave={false}
+      />
+      <ProTable<LogItem>
+        search={false}
+        pagination={{
+          pageSize: 5,
+        }}
+        columns={columns}
+        request={async (params) => service.query(params)}
+      ></ProTable>
+    </Modal>
+  );
+});
+
+export default Log;

+ 42 - 130
src/pages/notice/Config/index.tsx

@@ -4,8 +4,8 @@ import {
   ArrowDownOutlined,
   BarsOutlined,
   BugOutlined,
+  DeleteOutlined,
   EditOutlined,
-  MinusOutlined,
   PlusOutlined,
 } from '@ant-design/icons';
 import { Button, message, Popconfirm, Tooltip } from 'antd';
@@ -18,12 +18,21 @@ import { createForm, onFieldValueChange } from '@formily/core';
 import { observer } from '@formily/react';
 import SearchComponent from '@/components/SearchComponent';
 import ProTable from '@jetlinks/pro-table';
-import { history } from '@@/core/history';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
-import { useLocation } from 'umi';
+import { history, useLocation } from 'umi';
+import { model } from '@formily/reactive';
+import moment from 'moment';
 
 export const service = new Service('notifier/config');
 
+export const state = model<{
+  current?: ConfigItem;
+  debug?: boolean;
+  log?: boolean;
+}>({
+  debug: false,
+  log: false,
+});
 const Config = observer(() => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
@@ -40,113 +49,15 @@ const Config = observer(() => {
     const DForm = form;
     if (!DForm?.values) return;
     DForm.setValuesIn('provider', DForm.values.provider);
-    // const resp = await service.getMetadata(
-    //   DForm?.values?.type,
-    //   // eslint-disable-next-line @typescript-eslint/no-use-before-define
-    //   currentType || DForm.values?.provider,
-    // );
-    // const properties = resp.result?.properties as ConfigMetadata[];
-    // setConfigSchema({
-    //   type: 'object',
-    //   properties: properties?.reduce((previousValue, currentValue) => {
-    //     if (currentValue.type?.type === 'array') {
-    //       // 单独处理邮件的其他配置功能
-    //       previousValue[currentValue.property] = {
-    //         type: 'array',
-    //         title: '其他配置',
-    //         'x-component': 'ArrayItems',
-    //         'x-decorator': 'FormItem',
-    //         items: {
-    //           type: 'object',
-    //           properties: {
-    //             space: {
-    //               type: 'void',
-    //               'x-component': 'Space',
-    //               properties: {
-    //                 sort: {
-    //                   type: 'void',
-    //                   'x-decorator': 'FormItem',
-    //                   'x-component': 'ArrayItems.SortHandle',
-    //                 },
-    //                 name: {
-    //                   type: 'string',
-    //                   title: 'key',
-    //                   'x-decorator': 'FormItem',
-    //                   'x-component': 'Input',
-    //                 },
-    //                 value: {
-    //                   type: 'string',
-    //                   title: 'value',
-    //                   'x-decorator': 'FormItem',
-    //                   'x-component': 'Input',
-    //                 },
-    //                 description: {
-    //                   type: 'string',
-    //                   title: '备注',
-    //                   'x-decorator': 'FormItem',
-    //                   'x-component': 'Input',
-    //                 },
-    //                 remove: {
-    //                   type: 'void',
-    //                   'x-decorator': 'FormItem',
-    //                   'x-component': 'ArrayItems.Remove',
-    //                 },
-    //               },
-    //             },
-    //           },
-    //         },
-    //         properties: {
-    //           add: {
-    //             type: 'void',
-    //             title: '添加条目',
-    //             'x-component': 'ArrayItems.Addition',
-    //           },
-    //         },
-    //       };
-    //     } else {
-    //       previousValue[currentValue.property] = {
-    //         title: currentValue.name,
-    //         type: 'string',
-    //         'x-component': 'Input',
-    //         'x-decorator': 'FormItem',
-    //       };
-    //     }
-    //     return previousValue;
-    //   }, {}),
-    // });
-    // DForm.setValues(CurdModel.current);
-    // setLoading(false);
   };
-  // const schema: ISchema = {
-  //   type: 'object',
-  //   properties: {
-  //     name: {
-  //       title: '名称',
-  //       'x-component': 'Input',
-  //       'x-decorator': 'FormItem',
-  //     },
-  //     type: {
-  //       title: '类型',
-  //       'x-component': 'Select',
-  //       'x-decorator': 'FormItem',
-  //       'x-reactions': ['{{useAsyncDataSource(getTypes)}}'],
-  //     },
-  //     provider: {
-  //       title: '服务商',
-  //       'x-component': 'Select',
-  //       'x-decorator': 'FormItem',
-  //     },
-  //     configuration: configSchema,
-  //   },
-  // };
 
   const formEvent = () => {
     onFieldValueChange('type', async (field, f) => {
       const type = field.value;
       if (!type) return;
-      f.setFieldState('provider', (state) => {
-        state.value = undefined;
-        state.dataSource = providerRef.current
+      f.setFieldState('provider', (state1) => {
+        state1.value = undefined;
+        state1.dataSource = providerRef.current
           .find((item) => type === item.id)
           ?.providerInfos.map((i) => ({ label: i.name, value: i.id }));
       });
@@ -191,7 +102,7 @@ const Config = observer(() => {
       dataIndex: 'type',
       title: intl.formatMessage({
         id: 'pages.notice.config.type',
-        defaultMessage: '通知类型',
+        defaultMessage: '通知方式',
       }),
     },
     {
@@ -214,10 +125,8 @@ const Config = observer(() => {
           key="edit"
           onClick={async () => {
             // setLoading(true);
-            CurdModel.update(record);
-            form.setValues(record);
-            await createSchema();
-            CurdModel.model = 'edit';
+            state.current = record;
+            history.push(getMenuPathByParams(MENUS_CODE['notice/Config/Detail'], id));
           }}
         >
           <Tooltip
@@ -229,7 +138,15 @@ const Config = observer(() => {
             <EditOutlined />
           </Tooltip>
         </a>,
-        <a onClick={() => downloadObject(record, '通知配置')} key="download">
+        <a
+          onClick={() =>
+            downloadObject(
+              record,
+              `通知配置${record.name}-${moment(new Date()).format('YYYY/MM/DD HH:mm:ss')}`,
+            )
+          }
+          key="download"
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.data.option.download',
@@ -239,7 +156,12 @@ const Config = observer(() => {
             <ArrowDownOutlined />
           </Tooltip>
         </a>,
-        <a key="debug">
+        <a
+          key="debug"
+          onClick={() => {
+            state.debug = true;
+          }}
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.notice.option.debug',
@@ -249,7 +171,12 @@ const Config = observer(() => {
             <BugOutlined />
           </Tooltip>
         </a>,
-        <a key="record">
+        <a
+          key="record"
+          onClick={() => {
+            state.log = true;
+          }}
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.data.option.record',
@@ -279,7 +206,7 @@ const Config = observer(() => {
                 defaultMessage: '删除',
               })}
             >
-              <MinusOutlined />
+              <DeleteOutlined />
             </Tooltip>
           </Popconfirm>
         </a>,
@@ -299,6 +226,7 @@ const Config = observer(() => {
   return (
     <PageContainer className={'page-title-show'}>
       <SearchComponent
+        defaultParam={[{ column: 'type$IN', value: id }]}
         field={columns}
         onSearch={(data) => {
           actionRef.current?.reset?.();
@@ -313,6 +241,7 @@ const Config = observer(() => {
         toolBarRender={() => [
           <Button
             onClick={() => {
+              state.current = undefined;
               history.push(getMenuPathByParams(MENUS_CODE['notice/Config/Detail'], id));
             }}
             key="button"
@@ -327,23 +256,6 @@ const Config = observer(() => {
         ]}
         request={async (params) => service.query(params)}
       />
-
-      {/*<BaseCrud*/}
-      {/*  columns={columns}*/}
-      {/*  service={service}*/}
-      {/*  title={intl.formatMessage({*/}
-      {/*    id: 'pages.notice.config',*/}
-      {/*    defaultMessage: '通知配置',*/}
-      {/*  })}*/}
-      {/*  modelConfig={{*/}
-      {/*    width: '50vw',*/}
-      {/*    loading: loading,*/}
-      {/*  }}*/}
-      {/*  schema={schema}*/}
-      {/*  form={form}*/}
-      {/*  schemaConfig={{ scope: { useAsyncDataSource, getTypes } }}*/}
-      {/*  actionRef={actionRef}*/}
-      {/*/>*/}
     </PageContainer>
   );
 });

+ 7 - 0
src/pages/notice/Config/service.ts

@@ -1,5 +1,6 @@
 import BaseService from '@/utils/BaseService';
 import { request } from 'umi';
+import SystemConst from '@/utils/const';
 
 class Service extends BaseService<ConfigItem> {
   public getTypes = () =>
@@ -11,6 +12,12 @@ class Service extends BaseService<ConfigItem> {
     request(`${this.uri}/${type}/${provider}/metadata`, {
       method: 'GET',
     });
+
+  public getTemplate = (configId: string) =>
+    request(`${SystemConst.API_BASE}/notifier/template/${configId}/_query/no-paging`);
+
+  public getTemplateVariable = (templateId: string) =>
+    request(`${SystemConst.API_BASE}/notifier/template/${templateId}/detail`);
 }
 
 export default Service;

+ 1 - 0
src/pages/notice/Config/typings.d.ts

@@ -31,5 +31,6 @@ type ConfigMetadata = {
     type: string;
     expands?: Record<string, any>;
   };
+  properties: ConfigProperty[];
   scopes: any[];
 };

+ 184 - 0
src/pages/notice/Template/Debug/index.tsx

@@ -0,0 +1,184 @@
+import { Button, Modal } from 'antd';
+import { useEffect, useMemo } from 'react';
+import { createForm, Field, onFieldReact, onFieldValueChange } from '@formily/core';
+import { createSchemaField, observer } from '@formily/react';
+import {
+  ArrayTable,
+  DatePicker,
+  Form,
+  FormItem,
+  Input,
+  NumberPicker,
+  PreviewText,
+  Select,
+} from '@formily/antd';
+import { ISchema } from '@formily/json-schema';
+import { configService, state } from '@/pages/notice/Template';
+import { useLocation } from 'umi';
+import { useAsyncDataSource } from '@/utils/util';
+import { Store } from 'jetlinks-store';
+import FUpload from '@/components/Upload';
+
+const Debug = observer(() => {
+  const location = useLocation<{ id: string }>();
+  const id = (location as any).query?.id;
+
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        effects() {
+          onFieldValueChange('configId', async (field, form1) => {
+            const value = (field as Field).value;
+            const configs = Store.get('notice-config');
+            const target = configs.find((item: { id: any }) => item.id === value);
+            console.log(target, 'target');
+            // 从缓存中获取通知配置信息
+            if (target && target.variableDefinitions) {
+              form1.setValuesIn('variableDefinitions', target.variableDefinitions);
+            }
+            //
+            // 获取 变量列表
+            // 然后set 值
+          });
+          onFieldReact('variableDefinitions.*.type', (field) => {
+            const value = (field as Field).value;
+            const format = field.query('.value').take() as any;
+            switch (value) {
+              case 'date':
+                format.setComponent(DatePicker);
+                break;
+              case 'string':
+                format.setComponent(Input);
+                break;
+              case 'number':
+                format.setComponent(NumberPicker);
+                break;
+              case 'file':
+                format.setComponent(FUpload, {
+                  type: 'file',
+                });
+                break;
+              case 'other':
+                format.setComponent(Input);
+                break;
+            }
+          });
+        },
+      }),
+    [id],
+  );
+
+  useEffect(() => {
+    const data = state.current;
+    form.setValuesIn('variableDefinitions', data?.variableDefinitions);
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      ArrayTable,
+      PreviewText,
+    },
+  });
+
+  const getConfig = () =>
+    configService
+      .queryNoPagingPost({
+        terms: [{ column: 'type$IN', value: id }],
+      })
+      .then((resp: any) => {
+        // 缓存通知配置
+        Store.set('notice-config', resp.result);
+        return resp.result?.map((item: { name: any; id: any }) => ({
+          label: item.name,
+          value: item.id,
+        }));
+      });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      configId: {
+        title: '配置',
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Select',
+        'x-reactions': '{{useAsyncDataSource(getConfig)}}',
+      },
+      variableDefinitions: {
+        title: '变量',
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'ArrayTable',
+        'x-component-props': {
+          pagination: { pageSize: 9999 },
+          scroll: { x: '100%' },
+        },
+        items: {
+          type: 'object',
+          properties: {
+            column1: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '变量', width: '120px' },
+              properties: {
+                id: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'PreviewText.Input',
+                  'x-disabled': true,
+                },
+              },
+            },
+            column2: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '名称', width: '120px' },
+              properties: {
+                name: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'PreviewText.Input',
+                  'x-disabled': true,
+                },
+              },
+            },
+            column3: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '值', width: '120px' },
+              properties: {
+                value: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+  };
+  return (
+    <Modal
+      title="调试"
+      width="40vw"
+      visible={state.debug}
+      onCancel={() => (state.debug = false)}
+      footer={
+        <Button type="primary" onClick={() => (state.debug = false)}>
+          关闭
+        </Button>
+      }
+    >
+      <Form form={form} layout={'vertical'}>
+        <SchemaField schema={schema} scope={{ getConfig, useAsyncDataSource }} />
+      </Form>
+    </Modal>
+  );
+});
+export default Debug;

+ 204 - 60
src/pages/notice/Template/Detail/index.tsx

@@ -1,8 +1,10 @@
 import {
+  ArrayItems,
   ArrayTable,
   Editable,
   Form,
   FormButtonGroup,
+  FormGrid,
   FormItem,
   Input,
   NumberPicker,
@@ -24,12 +26,37 @@ import { useParams } from 'umi';
 import { PageContainer } from '@ant-design/pro-layout';
 import { Card, Col, message, Row } from 'antd';
 import { typeList } from '@/pages/notice';
-import { service, state } from '@/pages/notice/Template';
+import { configService, service, state } from '@/pages/notice/Template';
 import FBraftEditor from '@/components/FBraftEditor';
+import { useAsyncDataSource } from '@/utils/util';
 
 const Detail = observer(() => {
   const { id } = useParams<{ id: string }>();
 
+  const getConfig = () =>
+    configService
+      .queryNoPagingPost({
+        terms: [{ column: 'type$IN', value: id }],
+      })
+      .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 = (configId: string) => service.weixin.getDepartments(configId);
+  const getWeixinTags = (configId: string) => service.weixin.getTags(configId);
+  const getWeixinUser = (configId: string) => service.weixin.getUserByDepartment(configId);
+
+  const getAliyunSigns = (configId: string) => service.aliyun.getSigns(configId);
+  const getAliyunTemplates = (configId: string) => service.aliyun.getTemplates(configId);
+
   // 正则提取${}里面的值
   const pattern = /(?<=\$\{).*?(?=\})/g;
   const form = useMemo(
@@ -40,6 +67,7 @@ const Detail = observer(() => {
           onFieldInit('template.message', (field, form1) => {
             if (id === 'email') {
               field.setComponent(FBraftEditor);
+              // form1.setValuesIn('template.message', 'testtt')
             }
             console.log(form1);
             ///给FBraftEditor 设置初始值
@@ -49,43 +77,57 @@ const Detail = observer(() => {
             if (id === 'email' && form1.modified) {
               value = value?.toHTML();
             }
-            console.log(value, 'test');
             const idList = value
-              .match(pattern)
+              ?.match(pattern)
               ?.filter((i: string) => i)
-              .map((item: string) => ({ id: item }));
+              .map((item: string) => ({ id: item, type: 'string', format: '--' }));
             if (form1.modified) {
               form1.setValuesIn('variableDefinitions', idList);
             }
           });
-          onFieldValueChange('variableDefinitions.*.type', () => {
-            // const value = (field as Field).value;
-            // console.log(value, 'value');
-            // const format = field.query('.format').take() as DataField;
-            // console.log(field.query('.format'), field.query('.format').take(), 'values')
-            // switch (format.value) {
-            //   case 'date':
-            //     break;
-            //   case 'string':
-            //     format.setComponent(Input);
-            //     format.setDataSource([])
-            //     break;
-            //   case 'number':
-            //     format.setComponent(Input);
-            //     // format.setValue('%.xf');
-            //     break;
-            //   case 'file':
-            //     format.setComponent(Select);
-            //     format.setDataSource([
-            //       {label: '视频', value: 'video'},
-            //       {label: '图片', value: 'img'},
-            //       {label: '全部', value: 'any'},
-            //       {label: '', value: ''},
-            //     ])
-            //
-            //     break;
-            // }
+          onFieldValueChange('variableDefinitions.*.type', (field) => {
+            const value = (field as Field).value;
+            const format = field.query('.format').take() as any;
+            switch (value) {
+              case 'date':
+                format.setComponent(Select);
+                format.setDataSource([
+                  { label: 'String类型的UTC时间戳 (毫秒)', value: 'string' },
+                  { label: 'yyyy-MM-dd', value: 'yyyy-MM-dd' },
+                  { label: 'yyyy-MM-dd HH:mm:ss', value: 'yyyy-MM-dd HH:mm:ss' },
+                  { label: 'yyyy-MM-dd HH:mm:ss EE', value: 'yyyy-MM-dd HH:mm:ss EE' },
+                  { label: 'yyyy-MM-dd HH:mm:ss zzz', value: 'yyyy-MM-dd HH:mm:ss zzz' },
+                ]);
+                format.setValue('string');
+                break;
+              case 'string':
+                format.setComponent(PreviewText.Input);
+                format.setValue('--');
+                break;
+              case 'number':
+                format.setComponent(Input);
+                format.setValue('%.xf');
+                break;
+              case 'file':
+                format.setComponent(Select);
+                format.setDataSource([
+                  { label: '视频', value: 'video' },
+                  { label: '图片', value: 'img' },
+                  { label: '全部', value: 'any' },
+                ]);
+                format.setValue('any');
+                break;
+              case 'other':
+                format.setComponent(PreviewText.Input);
+                format.setValue('--');
+                break;
+            }
           });
+          // onFieldValueChange('configId', (field, form1) => {
+          //   const value = (field as Field).value;
+          //
+          //
+          // })
         },
       }),
     [id],
@@ -104,13 +146,15 @@ const Detail = observer(() => {
       Select,
       Switch,
       Radio,
-      ArrayTable,
       Editable,
       PreviewText,
       Space,
       FUpload,
       NumberPicker,
       FBraftEditor,
+      ArrayItems,
+      FormGrid,
+      ArrayTable,
     },
   });
 
@@ -197,11 +241,12 @@ const Detail = observer(() => {
         type: 'string',
         'x-decorator': 'FormItem',
         'x-component': 'Select',
-        enum: [
-          { label: '测试配置1', value: 'test1' },
-          { label: '测试配置2', value: 'test2' },
-          { label: '测试配置3', value: 'test3' },
-        ],
+        // enum: [
+        //   {label: '测试配置1', value: 'test1'},
+        //   {label: '测试配置2', value: 'test2'},
+        //   {label: '测试配置3', value: 'test3'},
+        // ],
+        'x-reactions': '{{useAsyncDataSource(getConfig)}}',
         'x-visible': id !== 'email',
       },
       template: {
@@ -224,35 +269,56 @@ const Detail = observer(() => {
               },
               toUser: {
                 title: '收信人ID',
-                'x-component': 'Input',
+                'x-component': 'Select',
                 'x-decorator': 'FormItem',
                 'x-decorator-props': {
                   tooltip: '请输入收信人ID',
                 },
                 'x-component-props': {
                   placeholder: '请输入收信人ID',
+                  mode: 'tags',
+                },
+                'x-reactions': {
+                  dependencies: ['configId'],
+                  fulfill: {
+                    run: '{{useAsyncDataSource(getWeixinUser($deps[0]))}}',
+                  },
                 },
               },
               toParty: {
                 title: '收信部门ID',
-                'x-component': 'Input',
+                'x-component': 'Select',
                 'x-decorator': 'FormItem',
                 'x-decorator-props': {
                   tooltip: '请输入收信部门ID',
                 },
                 'x-component-props': {
                   placeholder: '请输入收信部门ID',
+                  mode: 'tags',
+                },
+                'x-reactions': {
+                  dependencies: ['configId'],
+                  fulfill: {
+                    run: '{{useAsyncDataSource(getWeixinDept($deps[0]))}}',
+                  },
                 },
               },
               toTag: {
                 title: '标签推送',
-                'x-component': 'Input',
+                'x-component': 'Select',
                 'x-decorator': 'FormItem',
                 'x-decorator-props': {
                   tooltip: '标签推送',
                 },
                 'x-component-props': {
                   placeholder: '请输入标签推送,多个标签用,号分隔',
+                  mode: 'tags',
+                },
+                'x-reactions': {
+                  dependencies: ['configId'],
+                  fulfill: {
+                    run: '{{useAsyncDataSource(getWeixinTags($deps[0]))}}',
+                  },
                 },
               },
             },
@@ -287,7 +353,7 @@ const Detail = observer(() => {
                   },
                   userIdList: {
                     title: '收信人ID',
-                    'x-component': 'Input',
+                    'x-component': 'Select',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
                       tooltip: '请输入收信人ID',
@@ -295,10 +361,16 @@ const Detail = observer(() => {
                     'x-component-props': {
                       placeholder: '请输入收信人ID',
                     },
+                    'x-reactions': {
+                      dependencies: ['configId'],
+                      fulfill: {
+                        run: '{{useAsyncDataSource(getDingTalkUser($deps[0]))}}',
+                      },
+                    },
                   },
                   departmentIdList: {
                     title: '收信部门ID',
-                    'x-component': 'Input',
+                    'x-component': 'Select',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
                       tooltip: '请输入收信部门ID',
@@ -306,6 +378,12 @@ const Detail = observer(() => {
                     'x-component-props': {
                       placeholder: '请输入AgentID',
                     },
+                    'x-reactions': {
+                      dependencies: ['configId'],
+                      fulfill: {
+                        run: '{{useAsyncDataSource(getDingTalkDept($deps[0]))}}',
+                      },
+                    },
                   },
                 },
                 'x-reactions': {
@@ -458,7 +536,7 @@ const Detail = observer(() => {
                 properties: {
                   code: {
                     title: '模版ID',
-                    'x-component': 'Input',
+                    'x-component': 'Select',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
                       tooltip: '请输入模版ID',
@@ -466,10 +544,16 @@ const Detail = observer(() => {
                     'x-component-props': {
                       placeholder: '请输入模版ID',
                     },
+                    'x-reactions': {
+                      dependencies: ['configId'],
+                      fulfill: {
+                        run: '{{useAsyncDataSource(getAliyunTemplates($deps[0]))}}',
+                      },
+                    },
                   },
                   phoneNumber: {
                     title: '收信人',
-                    'x-component': 'Input',
+                    'x-component': 'Select',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
                       tooltip: '请输入收信人',
@@ -480,7 +564,7 @@ const Detail = observer(() => {
                   },
                   signName: {
                     title: '签名',
-                    'x-component': 'Input',
+                    'x-component': 'Select',
                     'x-decorator': 'FormItem',
                     'x-decorator-props': {
                       tooltip: '请输入签名',
@@ -488,6 +572,12 @@ const Detail = observer(() => {
                     'x-component-props': {
                       placeholder: '请输入签名',
                     },
+                    'x-reactions': {
+                      dependencies: ['configId'],
+                      fulfill: {
+                        run: '{{useAsyncDataSource(getAliyunSigns($deps[0]))}}',
+                      },
+                    },
                   },
                   // code	String	短信-模板ID
                   // signName	String	短信-签名
@@ -525,12 +615,55 @@ const Detail = observer(() => {
               //   },
               // },
               attachments: {
-                'x-component': 'FUpload',
+                type: 'array',
+                title: '附件信息',
                 'x-decorator': 'FormItem',
-                title: '附件',
-                'x-component-props': {
-                  type: 'file',
-                  placeholder: '请上传文件',
+                'x-component': 'ArrayItems',
+                'x-decorator-props': {
+                  style: {
+                    width: '100%',
+                  },
+                },
+                items: {
+                  type: 'object',
+
+                  'x-component': 'FormGrid',
+                  'x-component-props': {
+                    maxColumns: 24,
+                    minColumns: 24,
+                  },
+
+                  properties: {
+                    file: {
+                      'x-component': 'FUpload',
+                      'x-decorator': 'FormItem',
+                      'x-decorator-props': {
+                        style: {
+                          width: '100%',
+                        },
+                        gridSpan: 23,
+                      },
+                      'x-component-props': {
+                        type: 'file',
+                        placeholder: '请上传文件',
+                      },
+                    },
+                    remove: {
+                      type: 'void',
+                      'x-decorator': 'FormItem',
+                      'x-component': 'ArrayItems.Remove',
+                      'x-decorator-props': {
+                        gridSpan: 1,
+                      },
+                    },
+                  },
+                },
+                properties: {
+                  add: {
+                    type: 'void',
+                    'x-component': 'ArrayItems.Addition',
+                    title: '添加附件',
+                  },
                 },
               },
               // subject: {
@@ -587,6 +720,7 @@ const Detail = observer(() => {
                 name: {
                   type: 'string',
                   'x-decorator': 'FormItem',
+                  required: true,
                   'x-component': 'Input',
                 },
               },
@@ -600,11 +734,13 @@ const Detail = observer(() => {
                   type: 'string',
                   'x-decorator': 'FormItem',
                   'x-component': 'Select',
+                  required: true,
                   enum: [
+                    { label: '字符串', value: 'string' },
                     { label: '时间', value: 'date' },
                     { label: '数字', value: 'number' },
-                    { label: '字符串', value: 'string' },
                     { label: '文件', value: 'file' },
+                    { label: '其他', value: 'other' },
                   ],
                 },
               },
@@ -613,18 +749,12 @@ const Detail = observer(() => {
               type: 'void',
               'x-component': 'ArrayTable.Column',
               'x-component-props': { title: '格式', width: '150px' },
+              required: true,
               properties: {
                 format: {
                   type: 'string',
                   'x-decorator': 'FormItem',
-                  'x-component': 'Select',
-                  enum: [
-                    { label: 'String类型的UTC时间戳 (毫秒)', value: 'string' },
-                    { label: 'yyyy-MM-dd', value: 'yyyy-MM-dd' },
-                    { label: 'yyyy-MM-dd HH:mm:ss', value: 'yyyy-MM-dd HH:mm:ss' },
-                    { label: 'yyyy-MM-dd HH:mm:ss EE', value: 'yyyy-MM-dd HH:mm:ss EE' },
-                    { label: 'yyyy-MM-dd HH:mm:ss zzz', value: 'yyyy-MM-dd HH:mm:ss zzz' },
-                  ],
+                  'x-component': 'PreviewText.Input',
                 },
               },
             },
@@ -647,7 +777,21 @@ const Detail = observer(() => {
         <Row>
           <Col span={10}>
             <Form className={styles.form} form={form} layout={'vertical'}>
-              <SchemaField schema={schema} />
+              <SchemaField
+                schema={schema}
+                scope={{
+                  getConfig,
+                  getDingTalkDept,
+                  getDingTalkDeptTree,
+                  getDingTalkUser,
+                  getWeixinDept,
+                  getWeixinTags,
+                  getWeixinUser,
+                  getAliyunSigns,
+                  getAliyunTemplates,
+                  useAsyncDataSource,
+                }}
+              />
               <FormButtonGroup.Sticky>
                 <FormButtonGroup.FormItem>
                   <Submit onSubmit={handleSave}>保存</Submit>

+ 74 - 0
src/pages/notice/Template/Log/index.tsx

@@ -0,0 +1,74 @@
+import { Modal } from 'antd';
+import { observer } from '@formily/react';
+import { service, state } from '..';
+import ProTable, { ProColumns } from '@jetlinks/pro-table';
+import SearchComponent from '@/components/SearchComponent';
+import { useLocation } from 'umi';
+import { InfoCircleOutlined } from '@ant-design/icons';
+
+const Log = observer(() => {
+  const location = useLocation<{ id: string }>();
+  const id = (location as any).query?.id;
+
+  const columns: ProColumns<LogItem>[] = [
+    {
+      dataIndex: 'config',
+      title: '通知配置',
+    },
+    {
+      dataIndex: 'sendTime',
+      title: '发送时间',
+    },
+    {
+      dataIndex: 'state',
+      title: '状态',
+    },
+    {
+      dataIndex: 'action',
+      title: '操作',
+      render: (text, record) => [
+        <a
+          onClick={() => {
+            Modal.info({
+              title: '详情信息',
+              content: (
+                <div>
+                  <p>这是通知记录的详细信息。。。。。</p>
+                  <p>这是通知记录的详细信息。。。。。</p>
+                  {JSON.stringify(record)}
+                </div>
+              ),
+              onOk() {},
+            });
+          }}
+        >
+          <InfoCircleOutlined />
+        </a>,
+      ],
+    },
+  ];
+  return (
+    <Modal onCancel={() => (state.log = false)} title="通知记录" width={'70vw'} visible={state.log}>
+      <SearchComponent
+        defaultParam={[{ column: 'type$IN', value: id }]}
+        field={columns}
+        onSearch={(data) => {
+          // actionRef.current?.reset?.();
+          // setParam(data);
+          console.log(data);
+        }}
+        enableSave={false}
+      />
+      <ProTable<LogItem>
+        search={false}
+        pagination={{
+          pageSize: 5,
+        }}
+        columns={columns}
+        request={async (params) => service.query(params)}
+      ></ProTable>
+    </Modal>
+  );
+});
+
+export default Log;

+ 67 - 516
src/pages/notice/Template/index.tsx

@@ -10,21 +10,34 @@ import {
   DeleteOutlined,
   EditOutlined,
   PlusOutlined,
+  UnorderedListOutlined,
 } from '@ant-design/icons';
-import { Button, Tooltip } from 'antd';
+import { Button, Popconfirm, Tooltip } from 'antd';
 import { useIntl } from '@@/plugin-locale/localeExports';
 // import type { ISchema } from '@formily/json-schema';
 import Service from '@/pages/notice/Template/service';
+import ConfigService from '@/pages/notice/Config/service';
 import SearchComponent from '@/components/SearchComponent';
 // import Detail from '@/pages/notice/Template/Detail';
 import { history, useLocation } from 'umi';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 import { model } from '@formily/reactive';
+import Debug from './Debug';
+import Log from '@/pages/notice/Template/Log';
+import { downloadObject } from '@/utils/util';
+import moment from 'moment';
 
 export const service = new Service('notifier/template');
+
+export const configService = new ConfigService('notifier/config');
 export const state = model<{
   current?: TemplateItem;
-}>({});
+  debug?: boolean;
+  log?: boolean;
+}>({
+  debug: false,
+  log: false,
+});
 const Template = () => {
   const intl = useIntl();
   const location = useLocation<{ id: string }>();
@@ -43,15 +56,12 @@ const Template = () => {
       dataIndex: 'type',
       title: intl.formatMessage({
         id: 'pages.notice.config.type',
-        defaultMessage: '通知类型',
+        defaultMessage: '通知方式',
       }),
     },
     {
-      dataIndex: 'provider',
-      title: intl.formatMessage({
-        id: 'pages.table.provider',
-        defaultMessage: '服务商',
-      }),
+      dataIndex: 'description',
+      title: '说明',
     },
     {
       title: intl.formatMessage({
@@ -78,17 +88,34 @@ const Template = () => {
             <EditOutlined />
           </Tooltip>
         </a>,
-        <a key="delete">
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.remove',
-              defaultMessage: '删除',
-            })}
-          >
-            <DeleteOutlined />
-          </Tooltip>
-        </a>,
-        <a key="download">
+        <Popconfirm
+          key="delete"
+          title="确认删除?"
+          onConfirm={async () => {
+            await service.remove(record.id);
+            actionRef.current?.reload();
+          }}
+        >
+          <a key="delete">
+            <Tooltip
+              title={intl.formatMessage({
+                id: 'pages.data.option.remove',
+                defaultMessage: '删除',
+              })}
+            >
+              <DeleteOutlined />
+            </Tooltip>
+          </a>
+        </Popconfirm>,
+        <a
+          key="download"
+          onClick={() => {
+            downloadObject(
+              record,
+              `${record.name}-${moment(new Date()).format('YYYY/MM/DD HH:mm:ss')}`,
+            );
+          }}
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.data.option.download',
@@ -98,7 +125,13 @@ const Template = () => {
             <ArrowDownOutlined />
           </Tooltip>
         </a>,
-        <a key="debug">
+        <a
+          key="debug"
+          onClick={() => {
+            state.debug = true;
+            state.current = record;
+          }}
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.notice.option.debug',
@@ -108,509 +141,25 @@ const Template = () => {
             <BugOutlined />
           </Tooltip>
         </a>,
+        <a
+          key="log"
+          onClick={() => {
+            state.log = true;
+          }}
+        >
+          <Tooltip title="通知记录">
+            <UnorderedListOutlined />
+          </Tooltip>
+        </a>,
       ],
     },
   ];
-  // const providerRef = useRef<NetworkType[]>([]);
-
-  // const getTypes = async () =>
-  //   service.getTypes().then((resp) => {
-  //     providerRef.current = resp.result;
-  //     return resp.result.map((item: NetworkType) => ({
-  //       label: item.name,
-  //       value: item.id,
-  //     }));
-  //   });
-  //
-  // const formEvent = () => {
-  //   onFieldValueChange('type', async (field, f) => {
-  //     const type = field.value;
-  //     if (!type) return;
-  //     f.setFieldState('provider', (state) => {
-  //       state.value = undefined;
-  //       state.dataSource = providerRef.current
-  //         .find((item) => type === item.id)
-  //         ?.providerInfos.map((i) => ({ label: i.name, value: i.id }));
-  //     });
-  //   });
-  // };
-  //
-  // const handleNetwork = (field: Field) => {
-  //   const provider = field.query('...provider').get('value');
-  //   const defaultMessage = {
-  //     MQTT_CLIENT:
-  //       'qos1 /device/${#deviceId}\n' +
-  //       '\n' +
-  //       '${T(com.alibaba.fastjson.JSON).toJSONString(#this)}',
-  //     HTTP_CLIENT:
-  //       'POST http://[host]:[port]/api\n' +
-  //       'Content-Type: application/json\n' +
-  //       '\n' +
-  //       '${T(com.alibaba.fastjson.JSON).toJSONString(#this)}',
-  //   };
-  //   field.value = defaultMessage[provider];
-  // };
-  //
-  // const schema: ISchema = {
-  //   type: 'object',
-  //   properties: {
-  //     name: {
-  //       type: 'string',
-  //       title: '名称',
-  //       required: true,
-  //       'x-component': 'Input',
-  //       'x-decorator': 'FormItem',
-  //     },
-  //     grid: {
-  //       type: 'void',
-  //       'x-component': 'FormGrid',
-  //       'x-decorator': 'FormItem',
-  //       properties: {
-  //         type: {
-  //           type: 'string',
-  //           title: '通知类型',
-  //           'x-component': 'Select',
-  //           'x-decorator': 'FormItem',
-  //           'x-decorator-props': {
-  //             labelCol: 8,
-  //             wrapperCol: 12,
-  //           },
-  //           'x-reactions': ['{{useAsyncDataSource(getTypes)}}'],
-  //         },
-  //         provider: {
-  //           type: 'string',
-  //           title: '服务商',
-  //           'x-component': 'Select',
-  //           'x-decorator-props': {
-  //             labelCol: 4,
-  //             wrapperCol: 16,
-  //           },
-  //           'x-decorator': 'FormItem',
-  //         },
-  //       },
-  //     },
-  //     template: {
-  //       type: 'object',
-  //       properties: {
-  //         voice: {
-  //           type: 'void',
-  //           properties: {
-  //             ttsCode: {
-  //               type: 'string',
-  //               title: '模版ID',
-  //               'x-component': 'Input',
-  //               'x-decorator': 'FormItem',
-  //             },
-  //             calledShowNumbers: {
-  //               type: 'string',
-  //               title: '被叫显号',
-  //               'x-component': 'Input',
-  //               'x-decorator': 'FormItem',
-  //             },
-  //             CalledNumber: {
-  //               type: 'string',
-  //               title: '被叫号码',
-  //               'x-component': 'Input',
-  //               'x-decorator': 'FormItem',
-  //             },
-  //             PlayTimes: {
-  //               type: 'string',
-  //               title: '播放次数',
-  //               'x-component': 'Input',
-  //               'x-decorator': 'FormItem',
-  //             },
-  //           },
-  //           'x-visible': false,
-  //           'x-reactions': {
-  //             dependencies: ['...type'],
-  //             fulfill: {
-  //               state: {
-  //                 visible: '{{$deps[0]==="voice"}}',
-  //               },
-  //             },
-  //           },
-  //         },
-  //         sms: {
-  //           type: 'void',
-  //           properties: {
-  //             test: {
-  //               type: 'void',
-  //               properties: {
-  //                 text: {
-  //                   type: 'string',
-  //                   title: '应用ID',
-  //                   'x-component': 'Input.TextArea',
-  //                   'x-decorator': 'FormItem',
-  //                 },
-  //                 sendTo: {
-  //                   type: 'string',
-  //                   title: '收件人',
-  //                   'x-component': 'Input.TextArea',
-  //                   'x-decorator': 'FormItem',
-  //                   'x-component-props': {
-  //                     placeholder: '多个收件人以 , 分割',
-  //                   },
-  //                 },
-  //               },
-  //               'x-visible': false,
-  //               'x-reactions': {
-  //                 dependencies: ['...provider'],
-  //                 fulfill: {
-  //                   state: {
-  //                     visible: '{{$deps[0]==="test"}}',
-  //                   },
-  //                 },
-  //               },
-  //             },
-  //             aliyunSms: {
-  //               type: 'void',
-  //               properties: {
-  //                 code: {
-  //                   type: 'string',
-  //                   title: '模版编码',
-  //                   'x-component': 'Input.TextArea',
-  //                   'x-decorator': 'FormItem',
-  //                   required: true,
-  //                   'x-component-props': {
-  //                     placeholder: '阿里云短信模版编码',
-  //                   },
-  //                 },
-  //                 signName: {
-  //                   type: 'string',
-  //                   title: '签名',
-  //                   'x-component': 'Input.TextArea',
-  //                   'x-decorator': 'FormItem',
-  //                   required: true,
-  //                   'x-component-props': {
-  //                     placeholder: '阿里云短信模版签名',
-  //                   },
-  //                 },
-  //                 phoneNumber: {
-  //                   type: 'string',
-  //                   title: '收件人',
-  //                   'x-component': 'Input.TextArea',
-  //                   'x-decorator': 'FormItem',
-  //                   'x-component-props': {
-  //                     placeholder: '短信接收者,暂只支持单个联系人',
-  //                   },
-  //                 },
-  //               },
-  //               'x-visible': false,
-  //               'x-reactions': {
-  //                 dependencies: ['...provider'],
-  //                 fulfill: {
-  //                   state: {
-  //                     visible: '{{$deps[0]==="aliyunSms"}}',
-  //                   },
-  //                 },
-  //               },
-  //             },
-  //           },
-  //           'x-visible': false,
-  //           'x-reactions': {
-  //             dependencies: ['...type'],
-  //             fulfill: {
-  //               state: {
-  //                 visible: '{{$deps[0]==="sms"}}',
-  //               },
-  //             },
-  //           },
-  //         },
-  //         email: {
-  //           type: 'void',
-  //           properties: {
-  //             subject: {
-  //               type: 'string',
-  //               title: '标题',
-  //               'x-component': 'Input',
-  //               'x-decorator': 'FormItem',
-  //             },
-  //             sendTo: {
-  //               type: 'string',
-  //               title: '收件人',
-  //               'x-component': 'Input.TextArea',
-  //               'x-decorator': 'FormItem',
-  //               'x-component-props': {
-  //                 placeholder: '多个收件人以  ,  分隔',
-  //               },
-  //             },
-  //             attachments: {
-  //               type: 'string',
-  //               title: '附件',
-  //               'x-component': 'FUpload',
-  //               'x-decorator': 'FormItem',
-  //               'x-component-props': {
-  //                 type: 'multi',
-  //               },
-  //             },
-  //             emailEditor: {
-  //               type: 'string',
-  //               title: '正文',
-  //               'x-component': 'FBraftEditor',
-  //               'x-decorator': 'FormItem',
-  //               'x-component-props': {
-  //                 style: {
-  //                   height: '300px',
-  //                 },
-  //                 contentStyle: {
-  //                   height: '200px',
-  //                   // overflowY: 'auto',
-  //                 },
-  //               },
-  //             },
-  //           },
-  //           'x-visible': false,
-  //           'x-reactions': {
-  //             dependencies: ['...type'],
-  //             fulfill: {
-  //               state: {
-  //                 visible: '{{$deps[0]==="email"}}',
-  //               },
-  //             },
-  //           },
-  //         },
-  //         weixin: {
-  //           type: 'void',
-  //           properties: {
-  //             agentId: {
-  //               type: 'string',
-  //               title: '应用ID',
-  //               'x-component': 'Input',
-  //               'x-decorator': 'FormItem',
-  //             },
-  //             toUser: {
-  //               type: 'string',
-  //               title: '收信人ID',
-  //               'x-component': 'Input',
-  //               'x-decorator': 'FormItem',
-  //             },
-  //             toParty: {
-  //               type: 'string',
-  //               title: '收信部门ID',
-  //               'x-component': 'Input',
-  //               'x-decorator': 'FormItem',
-  //             },
-  //             toTag: {
-  //               type: 'string',
-  //               title: '按标签推送',
-  //               'x-component': 'Input',
-  //               'x-decorator': 'FormItem',
-  //             },
-  //             message: {
-  //               type: 'string',
-  //               title: '内容',
-  //               'x-component': 'Input.TextArea',
-  //               'x-decorator': 'FormItem',
-  //             },
-  //           },
-  //           'x-visible': false,
-  //           'x-reactions': {
-  //             dependencies: ['...type'],
-  //             fulfill: {
-  //               state: {
-  //                 visible: '{{$deps[0]==="weixin"}}',
-  //               },
-  //             },
-  //           },
-  //         },
-  //         dingTalk: {
-  //           type: 'void',
-  //           properties: {
-  //             dingTalkRobotWebHook: {
-  //               type: 'void',
-  //               properties: {
-  //                 messageType: {
-  //                   title: '消息类型',
-  //                   type: 'string',
-  //                   'x-component': 'Select',
-  //                   'x-decorator': 'FormItem',
-  //                   enum: ['text', 'markdown', 'link'],
-  //                 },
-  //                 text: {
-  //                   type: 'object',
-  //                   properties: {
-  //                     content: {
-  //                       title: '通知内容',
-  //                       type: 'string',
-  //                       'x-decorator': 'FormItem',
-  //                       'x-component': 'Input.TextArea',
-  //                     },
-  //                   },
-  //                   'x-visible': false,
-  //                   'x-reactions': {
-  //                     dependencies: ['.messageType'],
-  //                     fulfill: {
-  //                       state: {
-  //                         visible: '{{$deps[0]==="text"}}',
-  //                       },
-  //                     },
-  //                   },
-  //                 },
-  //                 markdown: {
-  //                   type: 'object',
-  //                   properties: {
-  //                     title: {
-  //                       title: '标题',
-  //                       type: 'string',
-  //                       'x-decorator': 'FormItem',
-  //                       'x-component': 'Input',
-  //                     },
-  //                     text: {
-  //                       title: '内容',
-  //                       type: 'string',
-  //                       'x-decorator': 'FormItem',
-  //                       'x-component': 'Input.TextArea',
-  //                     },
-  //                   },
-  //                   'x-visible': false,
-  //                   'x-reactions': {
-  //                     dependencies: ['.messageType'],
-  //                     fulfill: {
-  //                       state: {
-  //                         visible: '{{$deps[0]==="markdown"}}',
-  //                       },
-  //                     },
-  //                   },
-  //                 },
-  //                 link: {
-  //                   type: 'object',
-  //                   properties: {
-  //                     title: {
-  //                       title: '标题',
-  //                       type: 'string',
-  //                       'x-decorator': 'FormItem',
-  //                       'x-component': 'Input',
-  //                     },
-  //                     text: {
-  //                       title: '内容',
-  //                       type: 'string',
-  //                       'x-decorator': 'FormItem',
-  //                       'x-component': 'Input.TextArea',
-  //                     },
-  //                     picUrl: {
-  //                       title: '图片连接',
-  //                       'x-decorator': 'FormItem',
-  //                       'x-component': 'FUpload',
-  //                     },
-  //                     messageUrl: {
-  //                       title: '内容连接',
-  //                       'x-decorator': 'FormItem',
-  //                       'x-component': 'Input.TextArea',
-  //                     },
-  //                   },
-  //                   'x-visible': false,
-  //                   'x-reactions': {
-  //                     dependencies: ['.messageType'],
-  //                     fulfill: {
-  //                       state: {
-  //                         visible: '{{$deps[0]==="link"}}',
-  //                       },
-  //                     },
-  //                   },
-  //                 },
-  //               },
-  //               'x-visible': false,
-  //               'x-reactions': {
-  //                 dependencies: ['...provider'],
-  //                 fulfill: {
-  //                   state: {
-  //                     visible: '{{$deps[0]==="dingTalkRobotWebHook"}}',
-  //                   },
-  //                 },
-  //               },
-  //             },
-  //             dingTalkMessage: {
-  //               type: 'void',
-  //               properties: {
-  //                 agentId: {
-  //                   type: 'string',
-  //                   title: '应用ID',
-  //                   'x-component': 'Input',
-  //                   'x-decorator': 'FormItem',
-  //                 },
-  //                 userIdList: {
-  //                   type: 'string',
-  //                   title: '收信人ID',
-  //                   'x-component': 'Input',
-  //                   'x-decorator': 'FormItem',
-  //                 },
-  //                 departmentIdList: {
-  //                   type: 'string',
-  //                   title: '收信部门ID',
-  //                   'x-component': 'Input',
-  //                   'x-decorator': 'FormItem',
-  //                 },
-  //                 toAllUser: {
-  //                   type: 'string',
-  //                   title: '全部用户',
-  //                   'x-component': 'Select',
-  //                   'x-decorator': 'FormItem',
-  //                   enum: [
-  //                     { label: '是', value: true },
-  //                     { label: '否', value: false },
-  //                   ],
-  //                 },
-  //                 message: {
-  //                   type: 'string',
-  //                   title: '内容',
-  //                   'x-component': 'Input.TextArea',
-  //                   'x-decorator': 'FormItem',
-  //                 },
-  //               },
-  //               'x-visible': false,
-  //               'x-reactions': {
-  //                 dependencies: ['...provider'],
-  //                 fulfill: {
-  //                   state: {
-  //                     visible: '{{$deps[0]==="dingTalkMessage"}}',
-  //                   },
-  //                 },
-  //               },
-  //             },
-  //           },
-  //           'x-visible': false,
-  //           'x-reactions': {
-  //             dependencies: ['...type'],
-  //             fulfill: {
-  //               state: {
-  //                 visible: '{{$deps[0]==="dingTalk"}}',
-  //               },
-  //             },
-  //           },
-  //         },
-  //         network: {
-  //           type: 'void',
-  //           properties: {
-  //             text: {
-  //               type: 'string',
-  //               title: '消息',
-  //               'x-component': 'Input.TextArea',
-  //               'x-decorator': 'FormItem',
-  //               'x-component-props': {
-  //                 rows: 5,
-  //               },
-  //               'x-reactions': '{{handleNetwork}}',
-  //             },
-  //           },
-  //           'x-visible': false,
-  //           'x-reactions': {
-  //             dependencies: ['...type'],
-  //             fulfill: {
-  //               state: {
-  //                 visible: '{{$deps[0]==="network"}}',
-  //               },
-  //             },
-  //           },
-  //         },
-  //       },
-  //     },
-  //   },
-  // };
 
   const [param, setParam] = useState({});
   return (
     <PageContainer className={'page-title-show'}>
       <SearchComponent
+        defaultParam={[{ column: 'type$IN', value: id }]}
         field={columns}
         onSearch={(data) => {
           actionRef.current?.reset?.();
@@ -618,6 +167,7 @@ const Template = () => {
         }}
       />
       <ProTable<TemplateItem>
+        rowKey="id"
         search={false}
         params={param}
         columns={columns}
@@ -643,7 +193,8 @@ const Template = () => {
         ]}
         request={async (params) => service.query(params)}
       />
-      {/*<Detail />*/}
+      <Debug />
+      <Log />
     </PageContainer>
   );
 };

+ 34 - 0
src/pages/notice/Template/service.ts

@@ -12,6 +12,40 @@ class Service extends BaseService<TemplateItem> {
     request(`${this.uri}/${type}/${provider}/metadata`, {
       method: 'GET',
     });
+
+  public getConfigs = (data: any) =>
+    request(`${SystemConst.API_BASE}/notifier/config/_query`, {
+      method: 'POST',
+      data,
+    });
+
+  public sendMessage = (notifierId: string) =>
+    request(`${SystemConst.API_BASE}/notifier/${notifierId}/_send`, {
+      method: 'POST',
+    });
+
+  dingTalk = {
+    getDepartments: (id: string) =>
+      request(`${SystemConst.API_BASE}/notifier/dingtalk/corp/${id}/departments`),
+    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`),
+  };
+
+  weixin = {
+    getTags: (id: string) => request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/tags`),
+    getDepartments: (id: string) =>
+      request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/departments`),
+    getUserByDepartment: (id: string) =>
+      request(`${SystemConst.API_BASE}/notifier/wechat/corp/${id}/users`),
+  };
+
+  aliyun = {
+    getSigns: (id: string) => request(`${SystemConst.API_BASE}/notifier/sms/aliyun/${id}/signs`),
+    getTemplates: (id: string) =>
+      request(`${SystemConst.API_BASE}/notifier/sms/aliyun/${id}/templates`),
+  };
 }
 
 export default Service;

+ 8 - 0
src/pages/notice/Template/typings.d.ts

@@ -6,4 +6,12 @@ type TemplateItem = {
   provider: string;
   creatorId: string;
   createTime: number;
+  variableDefinitions: any;
+};
+
+type LogItem = {
+  id: string;
+  config: string;
+  sendTime: number;
+  state: string;
 };

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

@@ -41,7 +41,7 @@ export default (props: EditProps) => {
 
   const { data: permissions, run: queryPermissions } = useRequest(service.queryPermission, {
     manual: true,
-    formatResult: (response) => response.result.data,
+    formatResult: (response) => response.result,
   });
 
   const { data: menuThree, run: queryMenuThree } = useRequest(service.queryMenuThree, {

+ 1 - 1
src/pages/system/Menu/service.ts

@@ -21,7 +21,7 @@ class Service extends BaseService<MenuItem> {
    * @param data
    */
   queryPermission = (data: any) =>
-    request(`${SystemConst.API_BASE}/permission/_query`, { method: 'POST', data });
+    request(`${SystemConst.API_BASE}/permission/_query/no-paging`, { method: 'POST', data });
 
   queryDetail = (id: string) => request(`${this.uri}/${id}`, { method: 'GET' });
 

+ 4 - 0
src/utils/BaseService.ts

@@ -23,6 +23,10 @@ class BaseService<T> implements IBaseService<T> {
     return request(`${this.uri}/_query/`, { data, method: 'POST' });
   }
 
+  queryNoPagingPost(data: any): Promise<unknown> {
+    return request(`${this.uri}/_query/no-paging?paging=false`, { data, method: 'POST' });
+  }
+
   queryNoPaging(params: any): Promise<unknown> {
     return request(`${this.uri}/_query/no-paging?paging=false`, { params, method: 'GET' });
   }

Datei-Diff unterdrückt, da er zu groß ist
+ 4924 - 3214
yarn.lock