Переглянути джерело

feat: 边缘网关资源库

100011797 3 роки тому
батько
коміт
e1230e3057

+ 1 - 1
src/components/FMonacoEditor/index.tsx

@@ -2,7 +2,7 @@ import MonacoEditor from 'react-monaco-editor';
 import { connect, mapProps } from '@formily/react';
 import { useState } from 'react';
 
-const JMonacoEditor = (props: any) => {
+export const JMonacoEditor = (props: any) => {
   const [loading, setLoading] = useState(false);
 
   return (

+ 54 - 0
src/components/ProTableCard/CardItems/edge/Resource.tsx

@@ -0,0 +1,54 @@
+import { Ellipsis, TableCard } from '@/components';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import '@/style/common.less';
+import '../../index.less';
+import React from 'react';
+
+export interface ResourceCardProps extends Partial<ResourceItem> {
+  detail?: React.ReactNode;
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+  className?: string;
+  content?: React.ReactNode[];
+  onClick?: () => void;
+  showTool?: boolean;
+}
+
+const defaultImage = require('/public/images/device-type-3-big.png');
+
+export default (props: ResourceCardProps) => {
+  return (
+    <TableCard
+      showMask={false}
+      detail={props.detail}
+      actions={props.actions}
+      status={props.state?.value}
+      statusText={props.state?.text}
+      statusNames={{
+        enabled: StatusColorEnum.success,
+        disabled: StatusColorEnum.error,
+      }}
+    >
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <img width={88} height={88} src={defaultImage} alt={''} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <Ellipsis title={props.name} titleClassName={'card-item-header-name'} />
+          </div>
+          <div className={'card-item-content'}>
+            <div>
+              <label>通讯协议</label>
+              <Ellipsis title={props?.category || ''} />
+            </div>
+            <div>
+              <label>所属边缘网关</label>
+              <Ellipsis title={props?.category || ''} />
+            </div>
+          </div>
+        </div>
+      </div>
+    </TableCard>
+  );
+};

+ 111 - 0
src/pages/edge/Resource/Issue/Result.tsx

@@ -0,0 +1,111 @@
+import SystemConst from '@/utils/const';
+import Token from '@/utils/token';
+import { downloadObject } from '@/utils/util';
+import { Col, Input, Modal, Row } from 'antd';
+import { EventSourcePolyfill } from 'event-source-polyfill';
+import { useEffect, useState } from 'react';
+import { DeviceInstance } from '@/pages/device/Instance/typings';
+
+interface Props {
+  list: Partial<DeviceInstance>[];
+  data: Partial<ResourceItem>;
+  close: () => void;
+}
+
+const Publish = (props: Props) => {
+  const [count, setCount] = useState<number>(0);
+  const [countErr, setCountErr] = useState<number>(0);
+  const [flag, setFlag] = useState<boolean>(true);
+  const [errMessage, setErrMessage] = useState<any[]>([]);
+
+  const getData = () => {
+    let dt = 0;
+    let et = 0;
+    const errMessages: any[] = [];
+    const _terms = {
+      deviceId: (props.list || []).map((item) => item.id),
+      name: props.data.name,
+      targetId: props.data.targetId,
+      targetType: props.data.targetType,
+      category: props.data.category,
+      metadata: encodeURIComponent(props.data?.metadata || ''),
+    };
+    const url = new URLSearchParams();
+    Object.keys(_terms).forEach((key) => {
+      if (Array.isArray(_terms[key]) && _terms[key].length) {
+        _terms[key].map((item: string) => {
+          url.append(key, item);
+        });
+      } else {
+        url.append(key, _terms[key]);
+      }
+    });
+    const source = new EventSourcePolyfill(
+      `/${
+        SystemConst.API_BASE
+      }/edge/operations/entity-template-save/invoke/_batch?:X_Access_Token=${Token.get()}&${url.toString()}`,
+    );
+    source.onmessage = (e: any) => {
+      const res = JSON.parse(e.data);
+      if (res.successful) {
+        dt += 1;
+        setCount(dt);
+      } else {
+        et += 1;
+        setCountErr(et);
+        setFlag(false);
+        if (errMessages.length <= 5) {
+          errMessages.push({ ...res });
+          setErrMessage([...errMessages]);
+        }
+      }
+    };
+    source.onerror = () => {
+      source.close();
+    };
+    source.onopen = () => {};
+  };
+
+  useEffect(() => {
+    getData();
+  }, []);
+
+  return (
+    <Modal
+      title={'下发结果'}
+      maskClosable={false}
+      open
+      onCancel={props.close}
+      onOk={props.close}
+      width={900}
+    >
+      <Row gutter={24} style={{ marginBottom: 20 }}>
+        <Col span={8}>
+          <div>成功: {count}</div>
+          <div>
+            失败: {countErr}
+            {errMessage.length > 0 && (
+              <a
+                style={{ marginLeft: 20 }}
+                onClick={() => {
+                  downloadObject(errMessage || '', '下发失败原因');
+                }}
+              >
+                下载
+              </a>
+            )}
+          </div>
+        </Col>
+        <Col span={8}>下发设备数量: {props.list?.length || 0}</Col>
+        <Col span={8}>已下发数量: {countErr + count}</Col>
+      </Row>
+      {!flag && (
+        <div>
+          <Input.TextArea rows={10} value={JSON.stringify(errMessage)} />
+        </div>
+      )}
+    </Modal>
+  );
+};
+
+export default Publish;

+ 182 - 7
src/pages/edge/Resource/Issue/index.tsx

@@ -1,23 +1,198 @@
-import { Modal } from 'antd';
+import { Badge, Modal } from 'antd';
+import { DeviceInstance } from '@/pages/device/Instance/typings';
+import ProTable, { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { Key, useRef, useState } from 'react';
+import moment from 'moment';
+import { service } from '../index';
+import SearchComponent from '@/components/SearchComponent';
+import { statusMap } from '@/pages/device/Instance';
+import styles from '@/pages/link/AccessConfig/Detail/components/Network/index.less';
+import { InfoCircleOutlined } from '@ant-design/icons';
+import { onlyMessage } from '@/utils/util';
+import Result from './Result';
 interface Props {
-  data: any;
+  data: Partial<ResourceItem>;
   cancel: () => void;
 }
 
 export default (props: Props) => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [searchParams, setSearchParams] = useState<any>({});
+
+  const columns: ProColumns<DeviceInstance>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      width: 200,
+      ellipsis: true,
+      fixed: 'left',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.productName',
+        defaultMessage: '产品名称',
+      }),
+      dataIndex: 'productName',
+      ellipsis: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.table.deviceName',
+        defaultMessage: '设备名称',
+      }),
+      dataIndex: 'name',
+      ellipsis: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.device.instance.registrationTime',
+        defaultMessage: '注册时间',
+      }),
+      dataIndex: 'registryTime',
+      width: '200px',
+      render: (text: any) => (text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : ''),
+      sorter: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.searchTable.titleStatus',
+        defaultMessage: '状态',
+      }),
+      dataIndex: 'state',
+      width: '90px',
+      valueType: 'select',
+      renderText: (record) =>
+        record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '',
+      valueEnum: {
+        notActive: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.notActive',
+            defaultMessage: '禁用',
+          }),
+          status: 'notActive',
+        },
+        offline: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.offLine',
+            defaultMessage: '离线',
+          }),
+          status: 'offline',
+        },
+        online: {
+          text: intl.formatMessage({
+            id: 'pages.device.instance.status.onLine',
+            defaultMessage: '在线',
+          }),
+          status: 'online',
+        },
+      },
+      filterMultiple: false,
+    },
+  ];
+
+  const [data, setData] = useState<Partial<DeviceInstance>[]>([]);
+  const [visible, setVisible] = useState<boolean>(false);
+
   return (
     <Modal
       open
-      title={'下发'}
-      onOk={() => {
-        props.cancel();
+      title={'下发设备'}
+      onOk={async () => {
+        if (data.length) {
+          setVisible(true);
+        } else {
+          onlyMessage('请选择设备', 'error');
+        }
       }}
       onCancel={() => {
         props.cancel();
       }}
-      width={700}
+      width={1000}
     >
-      下发
+      <div className={styles.alert}>
+        <InfoCircleOutlined style={{ marginRight: 10 }} />
+        离线设备无法进行设备模板下发
+      </div>
+      <SearchComponent<DeviceInstance>
+        field={columns}
+        enableSave={false}
+        model="simple"
+        target="edge-resource-issue"
+        onSearch={(param) => {
+          actionRef.current?.reset?.();
+          setSearchParams(param);
+        }}
+      />
+      <ProTable<DeviceInstance>
+        tableAlertRender={false}
+        rowSelection={{
+          type: 'checkbox',
+          onSelect: (selectedRow: any, selected: any) => {
+            let newSelectKeys = [...data];
+            if (selected) {
+              newSelectKeys.push({ ...selectedRow });
+            } else {
+              newSelectKeys = newSelectKeys.filter((item) => item.id !== selectedRow.id);
+            }
+            setData(newSelectKeys);
+          },
+          onSelectAll: (selected: boolean, _: any, changeRows: any) => {
+            let newSelectKeys = [...data];
+            if (selected) {
+              changeRows.forEach((item: any) => {
+                newSelectKeys.push({ ...item });
+              });
+            } else {
+              newSelectKeys = newSelectKeys.filter((a) => {
+                return !changeRows.some((b: any) => b.id === a.id);
+              });
+            }
+            setData(newSelectKeys);
+          },
+          selectedRowKeys: data?.map((item) => item.id) as Key[],
+        }}
+        params={searchParams}
+        toolBarRender={false}
+        rowKey="id"
+        pagination={{
+          pageSize: 10,
+        }}
+        search={false}
+        columnEmptyText={''}
+        columns={columns}
+        actionRef={actionRef}
+        request={(params) =>
+          service.queryDeviceList({
+            ...params,
+            terms: [
+              ...(params?.terms || []),
+              {
+                terms: [
+                  {
+                    termType: 'eq',
+                    column: 'productId$product-info',
+                    value: 'accessProvider is official-edge-gateway',
+                  },
+                ],
+                type: 'and',
+              },
+            ],
+            sorts: [{ name: 'createTime', order: 'desc' }],
+          })
+        }
+      />
+      {visible && (
+        <Result
+          data={props.data}
+          list={data}
+          close={() => {
+            setVisible(false);
+            props.cancel();
+          }}
+        />
+      )}
     </Modal>
   );
 };

+ 23 - 11
src/pages/edge/Resource/Save/index.tsx

@@ -1,13 +1,20 @@
 import { Modal } from 'antd';
-import MonacoEditor from 'react-monaco-editor';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
+import { JMonacoEditor } from '@/components/FMonacoEditor';
+import { service } from '@/pages/edge/Resource';
+import { onlyMessage } from '@/utils/util';
 interface Props {
-  data: any;
+  data: Partial<ResourceItem>;
   cancel: () => void;
+  reload: () => void;
 }
 
 export default (props: Props) => {
-  const [monacoValue, setMonacoValue] = useState<string>(props.data);
+  const [monacoValue, setMonacoValue] = useState<string>(props.data?.metadata || '{}');
+
+  useEffect(() => {
+    setMonacoValue(props.data?.metadata || '{}');
+  }, [props.data]);
 
   const editorDidMountHandle = (editor: any) => {
     editor.getAction('editor.action.formatDocument').run();
@@ -20,21 +27,26 @@ export default (props: Props) => {
     <Modal
       open
       title={'编辑'}
-      onOk={() => {
-        props.cancel();
+      onOk={async () => {
+        if (props.data?.id) {
+          const resp = await service.modify(props.data.id, { metadata: monacoValue });
+          if (resp.status === 200) {
+            props.reload();
+            onlyMessage('操作成功', 'success');
+          }
+        }
       }}
       onCancel={() => {
         props.cancel();
       }}
       width={700}
     >
-      <MonacoEditor
-        width={'100%'}
-        height={400}
-        theme="vs-dark"
+      <JMonacoEditor
+        height={350}
+        theme="vs"
         language={'json'}
         value={monacoValue}
-        onChange={(newValue) => {
+        onChange={(newValue: any) => {
           setMonacoValue(newValue);
         }}
         editorDidMount={editorDidMountHandle}

+ 263 - 384
src/pages/edge/Resource/index.tsx

@@ -1,394 +1,273 @@
 import { PageContainer } from '@ant-design/pro-layout';
-// import {DeviceInstance} from "@/pages/device/Instance/typings";
-// import SearchComponent from "@/components/SearchComponent";
-// import {ActionType, ProColumns} from "@jetlinks/pro-table";
-// import moment from "moment";
-// import {Badge, Button, Tooltip} from "antd";
-// import {service as categoryService} from "@/pages/device/Category";
-// import {InstanceModel, service, statusMap} from "@/pages/device/Instance";
-// import {useIntl} from "@@/plugin-locale/localeExports";
-// import {
-//   DeleteOutlined,
-//   DownSquareOutlined,
-//   EditOutlined,
-//   EyeOutlined,
-//   PlayCircleOutlined,
-//   StopOutlined,
-// } from "@ant-design/icons";
-// import {PermissionButton, ProTableCard} from "@/components";
-// import {useRef, useState} from "react";
-// import DeviceCard from "@/components/ProTableCard/CardItems/device";
-// import {onlyMessage} from "@/utils/util";
-// import {getMenuPathByParams, MENUS_CODE} from "@/utils/menu";
-// import {useHistory} from "umi";
-// import Save from './Save';
+import SearchComponent from '@/components/SearchComponent';
+import { ActionType, ProColumns } from '@jetlinks/pro-table';
+import { Badge } from 'antd';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import {
+  DeleteOutlined,
+  DownSquareOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import { PermissionButton, ProTableCard } from '@/components';
+import { useRef, useState } from 'react';
+import { onlyMessage } from '@/utils/util';
+import Save from './Save';
+import Issue from './Issue';
+import Service from './service';
+import ResourceCard from '@/components/ProTableCard/CardItems/edge/Resource';
+import moment from 'moment';
+
+export const service = new Service('entity/template');
 
 export default () => {
-  // const intl = useIntl();
-  // const actionRef = useRef<ActionType>();
-  // const [searchParams, setSearchParams] = useState<any>({});
-  // const [current, setCurrent] = useState<Partial<DeviceInstance>>({});
-  // const [visible, setVisible] = useState<boolean>(false);
-  // const history = useHistory<Record<string, string>>();
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [searchParams, setSearchParams] = useState<any>({});
+  const [current, setCurrent] = useState<Partial<ResourceItem>>({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const [issueVisible, setIssueVisible] = useState<boolean>(false);
 
-  // const tools = (record: DeviceInstance, type: 'card' | 'list') => [
-  //   type === 'list' && <Button
-  //     type={'link'}
-  //     style={{ padding: 0 }}
-  //     key={'detail'}
-  //     onClick={() => {
-  //       InstanceModel.current = record;
-  //       const url = getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id);
-  //       history.push(url);
-  //     }}
-  //   >
-  //     <Tooltip
-  //       title={intl.formatMessage({
-  //         id: 'pages.data.option.detail',
-  //         defaultMessage: '查看',
-  //       })}
-  //     >
-  //       <EyeOutlined />
-  //     </Tooltip>
-  //   </Button>,
-  //   <PermissionButton
-  //     type={'link'}
-  //     isPermission={true}
-  //     onClick={() => {
-  //       setCurrent(record);
-  //       setVisible(true);
-  //     }}
-  //     tooltip={{
-  //       title: type === 'list' ? '编辑' : '',
-  //     }}
-  //     style={{ padding: 0 }}
-  //     key={'edit'}
-  //   >
-  //     <EditOutlined />
-  //     {type === 'list' ? '' : '编辑'}
-  //   </PermissionButton>,
-  //   <PermissionButton
-  //     type={'link'}
-  //     onClick={() => {
-  //     }}
-  //     tooltip={{
-  //       title: type !== 'list' ? '' : '下发'
-  //     }}
-  //     style={{ padding: 0 }}
-  //     isPermission={true}
-  //     key={'reset'}
-  //   >
-  //     <DownSquareOutlined />
-  //     {type === 'list' ? '' : '下发'}
-  //   </PermissionButton>,
-  //   <PermissionButton
-  //     type={'link'}
-  //     key={'state'}
-  //     style={{ padding: 0 }}
-  //     popConfirm={{
-  //       title: intl.formatMessage({
-  //         id: `pages.data.option.${
-  //           record.state.value !== 'notActive' ? 'disabled' : 'enabled'
-  //         }.tips`,
-  //         defaultMessage: '确认禁用?',
-  //       }),
-  //       onConfirm: () => {
-  //         if (record.state.value !== 'notActive') {
-  //           service.undeployDevice(record.id).then((resp: any) => {
-  //             if (resp.status === 200) {
-  //               onlyMessage(
-  //                 intl.formatMessage({
-  //                   id: 'pages.data.option.success',
-  //                   defaultMessage: '操作成功!',
-  //                 }),
-  //               );
-  //               actionRef.current?.reload();
-  //             }
-  //           });
-  //         } else {
-  //           service.deployDevice(record.id).then((resp: any) => {
-  //             if (resp.status === 200) {
-  //               onlyMessage(
-  //                 intl.formatMessage({
-  //                   id: 'pages.data.option.success',
-  //                   defaultMessage: '操作成功!',
-  //                 }),
-  //               );
-  //               actionRef.current?.reload();
-  //             }
-  //           });
-  //         }
-  //       },
-  //     }}
-  //     isPermission={true}
-  //     tooltip={{
-  //       title: intl.formatMessage({
-  //         id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
-  //         defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
-  //       }),
-  //     }}
-  //   >
-  //     {record.state.value !== 'notActive' ? <StopOutlined /> : <PlayCircleOutlined />}
-  //     {record.state.value !== 'notActive' ? (type === 'list' ? '' : '禁用') : (type === 'list' ? '' : '启用')}
-  //   </PermissionButton>,
-  //   <PermissionButton
-  //     type={'link'}
-  //     key={'delete'}
-  //     style={{ padding: 0 }}
-  //     isPermission={true}
-  //     tooltip={
-  //       record.state.value !== 'notActive'
-  //         ? { title: intl.formatMessage({ id: 'pages.device.instance.deleteTip' }) }
-  //         : undefined
-  //     }
-  //     disabled={record.state.value !== 'notActive'}
-  //     popConfirm={{
-  //       title: intl.formatMessage({
-  //         id: 'pages.data.option.remove.tips',
-  //       }),
-  //       disabled: record.state.value !== 'notActive',
-  //       onConfirm: async () => {
-  //         if (record.state.value === 'notActive') {
-  //           await service.remove(record.id);
-  //           onlyMessage(
-  //             intl.formatMessage({
-  //               id: 'pages.data.option.success',
-  //               defaultMessage: '操作成功!',
-  //             }),
-  //           );
-  //           actionRef.current?.reload();
-  //         } else {
-  //           onlyMessage(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }), 'error');
-  //         }
-  //       },
-  //     }}
-  //   >
-  //     <DeleteOutlined />
-  //   </PermissionButton>,
-  // ];
-  // const columns: ProColumns<DeviceInstance>[] = [
-  //   {
-  //     title: 'ID',
-  //     dataIndex: 'id',
-  //     width: 200,
-  //     ellipsis: true,
-  //     fixed: 'left',
-  //   },
-  //   {
-  //     title: intl.formatMessage({
-  //       id: 'pages.table.deviceName',
-  //       defaultMessage: '设备名称',
-  //     }),
-  //     dataIndex: 'name',
-  //     ellipsis: true,
-  //     width: 200,
-  //   },
-  //   {
-  //     title: intl.formatMessage({
-  //       id: 'pages.table.productName',
-  //       defaultMessage: '产品名称',
-  //     }),
-  //     dataIndex: 'productId',
-  //     width: 200,
-  //     ellipsis: true,
-  //     valueType: 'select',
-  //     request: async () => {
-  //       const res = await service.getProductList();
-  //       if (res.status === 200) {
-  //         return res.result.map((pItem: any) => ({ label: pItem.name, value: pItem.id }));
-  //       }
-  //       return [];
-  //     },
-  //     render: (_, row) => row.productName,
-  //     filterMultiple: true,
-  //   },
-  //   {
-  //     title: intl.formatMessage({
-  //       id: 'pages.device.instance.registrationTime',
-  //       defaultMessage: '注册时间',
-  //     }),
-  //     dataIndex: 'registryTime',
-  //     width: '200px',
-  //     valueType: 'dateTime',
-  //     render: (_: any, row) => {
-  //       return row.registryTime ? moment(row.registryTime).format('YYYY-MM-DD HH:mm:ss') : '';
-  //     },
-  //     sorter: true,
-  //   },
-  //   {
-  //     title: intl.formatMessage({
-  //       id: 'pages.searchTable.titleStatus',
-  //       defaultMessage: '状态',
-  //     }),
-  //     dataIndex: 'state',
-  //     width: '90px',
-  //     valueType: 'select',
-  //     renderText: (record) =>
-  //       record ? <Badge status={statusMap.get(record.value)} text={record.text} /> : '',
-  //     valueEnum: {
-  //       notActive: {
-  //         text: intl.formatMessage({
-  //           id: 'pages.device.instance.status.notActive',
-  //           defaultMessage: '禁用',
-  //         }),
-  //         status: 'notActive',
-  //       },
-  //       offline: {
-  //         text: intl.formatMessage({
-  //           id: 'pages.device.instance.status.offLine',
-  //           defaultMessage: '离线',
-  //         }),
-  //         status: 'offline',
-  //       },
-  //       online: {
-  //         text: intl.formatMessage({
-  //           id: 'pages.device.instance.status.onLine',
-  //           defaultMessage: '在线',
-  //         }),
-  //         status: 'online',
-  //       },
-  //     },
-  //     filterMultiple: false,
-  //   },
-  //   {
-  //     dataIndex: 'classifiedId',
-  //     title: '产品分类',
-  //     valueType: 'treeSelect',
-  //     hideInTable: true,
-  //     fieldProps: {
-  //       fieldNames: {
-  //         label: 'name',
-  //         value: 'id',
-  //       },
-  //     },
-  //     request: () =>
-  //       categoryService
-  //         .queryTree({
-  //           paging: false,
-  //         })
-  //         .then((resp: any) => resp.result),
-  //   },
-  //   {
-  //     dataIndex: 'productId$product-info',
-  //     title: '接入方式',
-  //     valueType: 'select',
-  //     hideInTable: true,
-  //     request: () =>
-  //       service.queryGatewayList().then((resp: any) =>
-  //         resp.result.map((item: any) => ({
-  //           label: item.name,
-  //           value: `accessId is ${item.id}`,
-  //         })),
-  //       ),
-  //   },
-  //   {
-  //     dataIndex: 'deviceType',
-  //     title: '设备类型',
-  //     valueType: 'select',
-  //     hideInTable: true,
-  //     valueEnum: {
-  //       device: {
-  //         text: '直连设备',
-  //         status: 'device',
-  //       },
-  //       childrenDevice: {
-  //         text: '网关子设备',
-  //         status: 'childrenDevice',
-  //       },
-  //       gateway: {
-  //         text: '网关设备',
-  //         status: 'gateway',
-  //       },
-  //     },
-  //   },
-  //   {
-  //     title: intl.formatMessage({
-  //       id: 'pages.table.description',
-  //       defaultMessage: '说明',
-  //     }),
-  //     dataIndex: 'describe',
-  //     width: '15%',
-  //     ellipsis: true,
-  //     hideInSearch: true,
-  //   },
-  //   {
-  //     title: intl.formatMessage({
-  //       id: 'pages.data.option',
-  //       defaultMessage: '操作',
-  //     }),
-  //     valueType: 'option',
-  //     width: 250,
-  //     fixed: 'right',
-  //     render: (text, record) => tools(record, 'list'),
-  //   },
-  // ];
+  const tools = (record: ResourceItem, type: 'card' | 'list') => [
+    <PermissionButton
+      type={'link'}
+      isPermission={true}
+      onClick={() => {
+        setCurrent(record);
+        setVisible(true);
+      }}
+      tooltip={{
+        title: type === 'list' ? '编辑' : '',
+      }}
+      style={{ padding: 0 }}
+      key={'edit'}
+    >
+      <EditOutlined />
+      {type === 'list' ? '' : '编辑'}
+    </PermissionButton>,
+    <PermissionButton
+      type={'link'}
+      onClick={() => {
+        setCurrent(record);
+        setIssueVisible(true);
+      }}
+      tooltip={{
+        title: type !== 'list' ? '' : '下发',
+      }}
+      style={{ padding: 0 }}
+      isPermission={true}
+      key={'reset'}
+    >
+      <DownSquareOutlined />
+      {type === 'list' ? '' : '下发'}
+    </PermissionButton>,
+    <PermissionButton
+      type={'link'}
+      key={'state'}
+      style={{ padding: 0 }}
+      popConfirm={{
+        title: intl.formatMessage({
+          id: `pages.data.option.${record.state?.value}.tips`,
+          defaultMessage: '确认禁用?',
+        }),
+        onConfirm: () => {
+          if (record.state?.value !== 'disabled') {
+            service._stop([record.id]).then((resp: any) => {
+              if (resp.status === 200) {
+                onlyMessage(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            });
+          } else {
+            service._start([record.id]).then((resp: any) => {
+              if (resp.status === 200) {
+                onlyMessage(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            });
+          }
+        },
+      }}
+      isPermission={true}
+      tooltip={{
+        title: type === 'list' ? (record.state.value !== 'disabled' ? '禁用' : '启用') : '',
+      }}
+    >
+      {record.state.value !== 'disabled' ? <StopOutlined /> : <PlayCircleOutlined />}
+      {record.state.value !== 'disabled'
+        ? type === 'list'
+          ? ''
+          : '禁用'
+        : type === 'list'
+        ? ''
+        : '启用'}
+    </PermissionButton>,
+    <PermissionButton
+      type={'link'}
+      key={'delete'}
+      style={{ padding: 0 }}
+      isPermission={true}
+      tooltip={record.state.value !== 'notActive' ? { title: '请先禁用,再删除。' } : undefined}
+      disabled={record.state.value !== 'notActive'}
+      popConfirm={{
+        title: intl.formatMessage({
+          id: 'pages.data.option.remove.tips',
+        }),
+        disabled: record.state.value !== 'notActive',
+        onConfirm: async () => {
+          if (record.state.value === 'notActive') {
+            await service.remove(record.id);
+            onlyMessage(
+              intl.formatMessage({
+                id: 'pages.data.option.success',
+                defaultMessage: '操作成功!',
+              }),
+            );
+            actionRef.current?.reload();
+          } else {
+            onlyMessage(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }), 'error');
+          }
+        },
+      }}
+    >
+      <DeleteOutlined />
+    </PermissionButton>,
+  ];
+  const columns: ProColumns<ResourceItem>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      width: 200,
+      ellipsis: true,
+      fixed: 'left',
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+      width: 150,
+      ellipsis: true,
+    },
+    {
+      title: '通信协议',
+      dataIndex: 'category',
+      width: 150,
+      ellipsis: true,
+    },
+    {
+      title: '所属边缘网关',
+      width: 150,
+      dataIndex: 'sourceId',
+      ellipsis: true,
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createTime',
+      width: 200,
+      valueType: 'dateTime',
+      render: (_: any, row) => {
+        return row.createTime ? moment(row.createTime).format('YYYY-MM-DD HH:mm:ss') : '';
+      },
+      sorter: true,
+    },
+    {
+      title: '状态',
+      dataIndex: 'state',
+      width: '90px',
+      valueType: 'select',
+      renderText: (text) =>
+        text ? (
+          <Badge status={text.value === 'enabled' ? 'success' : 'error'} text={text?.text || ''} />
+        ) : (
+          ''
+        ),
+      valueEnum: {
+        enabled: {
+          text: '正常',
+          status: 'enabled',
+        },
+        disabled: {
+          text: '异常',
+          status: 'disabled',
+        },
+      },
+      filterMultiple: false,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      width: 200,
+      fixed: 'right',
+      render: (text, record) => tools(record, 'list'),
+    },
+  ];
 
   return (
     <PageContainer>
-      开发中。。。。
-      {/*<SearchComponent<DeviceInstance>*/}
-      {/*  field={columns}*/}
-      {/*  target="edge-resource"*/}
-      {/*  onSearch={(data) => {*/}
-      {/*    actionRef.current?.reset?.();*/}
-      {/*    setSearchParams(data);*/}
-      {/*  }}*/}
-      {/*/>*/}
-      {/*<ProTableCard<DeviceInstance>*/}
-      {/*  columns={columns}*/}
-      {/*  scroll={{ x: 1366 }}*/}
-      {/*  actionRef={actionRef}*/}
-      {/*  params={searchParams}*/}
-      {/*  options={{ fullScreen: true }}*/}
-      {/*  columnEmptyText={''}*/}
-      {/*  request={(params) =>*/}
-      {/*    service.query({*/}
-      {/*      ...params,*/}
-      {/*      terms: [*/}
-      {/*        ...(params?.terms || []),*/}
-      {/*        {*/}
-      {/*          terms: [*/}
-      {/*            {*/}
-      {/*              column: 'productId$product-info',*/}
-      {/*              value: 'accessProvider is official-edge-gateway',*/}
-      {/*            },*/}
-      {/*          ],*/}
-      {/*          type: 'and',*/}
-      {/*        },*/}
-      {/*      ],*/}
-      {/*      sorts: [*/}
-      {/*        {*/}
-      {/*          name: 'createTime',*/}
-      {/*          order: 'desc',*/}
-      {/*        },*/}
-      {/*      ],*/}
-      {/*    })*/}
-      {/*  }*/}
-      {/*  rowKey="id"*/}
-      {/*  search={false}*/}
-      {/*  pagination={{ pageSize: 10 }}*/}
-      {/*  cardRender={(record) => (*/}
-      {/*    <DeviceCard*/}
-      {/*      {...record}*/}
-      {/*      detail={*/}
-      {/*        <div*/}
-      {/*          style={{ padding: 8, fontSize: 24 }}*/}
-      {/*          onClick={() => {*/}
-      {/*            InstanceModel.current = record;*/}
-      {/*            const url = getMenuPathByParams(MENUS_CODE['device/Instance/Detail'], record.id);*/}
-      {/*            history.push(url);*/}
-      {/*          }}*/}
-      {/*        >*/}
-      {/*          <EyeOutlined />*/}
-      {/*        </div>*/}
-      {/*      }*/}
-      {/*      actions={tools(record, 'card')}*/}
-      {/*    />*/}
-      {/*  )}*/}
-      {/*/>*/}
-      {/*{*/}
-      {/*  visible && <Save data={current} cancel={() => {setVisible(false)}} />*/}
-      {/*}*/}
+      <SearchComponent<ResourceItem>
+        field={columns}
+        target="edge-resource"
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setSearchParams(data);
+        }}
+      />
+      <ProTableCard<ResourceItem>
+        columns={columns}
+        scroll={{ x: 1366 }}
+        actionRef={actionRef}
+        params={searchParams}
+        options={{ fullScreen: true }}
+        columnEmptyText={''}
+        request={(params) =>
+          service.query({
+            ...params,
+            sorts: [
+              {
+                name: 'createTime',
+                order: 'desc',
+              },
+            ],
+          })
+        }
+        rowKey="id"
+        search={false}
+        pagination={{ pageSize: 10 }}
+        cardRender={(record) => <ResourceCard {...record} actions={tools(record, 'card')} />}
+      />
+      {visible && (
+        <Save
+          data={current}
+          cancel={() => {
+            setVisible(false);
+          }}
+          reload={() => {
+            actionRef.current?.reload();
+            setVisible(false);
+          }}
+        />
+      )}
+      {issueVisible && (
+        <Issue
+          data={current}
+          cancel={() => {
+            setIssueVisible(false);
+          }}
+        />
+      )}
     </PageContainer>
   );
 };

+ 23 - 0
src/pages/edge/Resource/service.ts

@@ -0,0 +1,23 @@
+import BaseService from '@/utils/BaseService';
+import { request } from '@@/plugin-request/request';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<ResourceItem> {
+  _start = (data: any) =>
+    request(`/${SystemConst.API_BASE}/entity/template/start/_batch`, {
+      method: 'POST',
+      data,
+    });
+  _stop = (data: any) =>
+    request(`/${SystemConst.API_BASE}/entity/template/stop/_batch`, {
+      method: 'POST',
+      data,
+    });
+  queryDeviceList = (data: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/detail/_query`, {
+      method: 'POST',
+      data,
+    });
+}
+
+export default Service;

+ 8 - 66
src/pages/edge/Resource/typings.d.ts

@@ -1,74 +1,16 @@
-export type DeviceInstance = {
+type ResourceItem = {
   id: string;
   name: string;
-  describe: string;
-  description: string;
-  productId: string;
-  productName: string;
-  protocolName: string;
-  security: any;
-  deriveMetadata: string;
+  targetId: string;
+  targetType: string;
+  sourceId: string;
+  sourceType: string;
   metadata: string;
-  binds: any;
   state: {
     value: string;
     text: string;
   };
-  creatorId: string;
-  creatorName: string;
-  createTime: number;
-  registryTime: string;
-  disabled?: boolean;
-  aloneConfiguration?: boolean;
-  deviceType: {
-    value: string;
-    text: string;
-  };
-  transportProtocol: string;
-  messageProtocol: string;
-  orgId: string;
-  orgName: string;
-  configuration: Record<string, any>;
-  relations?: any[];
-  cachedConfiguration: any;
-  transport: string;
-  protocol: string;
-  address: string;
-  registerTime: number;
-  onlineTime: string | number;
-  offlineTime: string | number;
-  tags: any;
-  photoUrl: string;
-  independentMetadata?: boolean;
-  accessProvider?: string;
-  accessId?: string;
-  features?: any[];
-  parentId?: string;
-  classifiedName?: string;
-};
-
-type Unit = {
-  id: string;
-  name: string;
-  symbol: string;
-  text: string;
-  type: string;
-  value: string;
-  description: string;
-};
-
-type PropertyData = {
-  data: {
-    value?:
-      | {
-          formatValue: string;
-          property: string;
-          value: any;
-        }
-      | any;
-    timeString: string;
-    timestamp: number;
-    formatValue: string;
-    property: string;
-  };
+  properties: any;
+  category?: string;
+  createTime: string | number;
 };

+ 3 - 2
src/pages/rule-engine/Scene/Save/action/ListItem/Item.tsx

@@ -55,7 +55,8 @@ export default (props: ItemProps) => {
       case 'dingTalk':
         return (
           <div>
-            向<span>{options?.notifierName || data?.notify?.notifierId}</span>
+            向<span className={'notify-text-highlight'}>{options?.orgName || ''}</span>
+            <span className={'notify-text-highlight'}>{options?.sendTo || ''}</span>
             通过
             <span className={'notify-img-highlight'}>
               <img width={18} src={itemNotifyIconMap.get(data?.notify?.notifyType)} />
@@ -189,7 +190,7 @@ export default (props: ItemProps) => {
     if (props?.data?.alarm?.mode === 'trigger') {
       return (
         <div className={'item-options-content'}>
-          满足条件后将触发关联
+          满足条件后将触发
           <a
             onClick={(e) => {
               e.stopPropagation();

+ 34 - 3
src/pages/rule-engine/Scene/Save/action/notify/VariableDefinitions.tsx

@@ -7,6 +7,7 @@ import Tag from './components/variableItem/tag';
 import BuildIn from './components/variableItem/buildIn';
 import InputFile from './components/variableItem/inputFile';
 import { forwardRef, useCallback, useImperativeHandle } from 'react';
+import { onlyMessage } from '@/utils/util';
 
 interface Props {
   name: number;
@@ -122,10 +123,40 @@ export default forwardRef((props: Props, ref) => {
         const formData = await form.validateFields().catch(() => {
           resolve(false);
         });
-        if (formData) {
-          resolve(formData);
+        if (
+          NotifyModel.notify.notifyType &&
+          ['dingTalk', 'weixin'].includes(NotifyModel.notify.notifyType)
+        ) {
+          const arr = NotifyModel.variable.map((item) => {
+            return { type: item.expands?.businessType || item?.type, id: item.id };
+          });
+          const org = arr.find((i) => i.type === 'org')?.id;
+          const user = arr.find((i) => i.type === 'user')?.id;
+          if (org && user) {
+            if (
+              (formData[org]?.source && formData[org]?.value) ||
+              (!formData[org]?.source && formData[org]) ||
+              (formData[user]?.source && (formData[user]?.value || formData[user]?.relation)) ||
+              (!formData[user]?.source && formData[user])
+            ) {
+              resolve(formData);
+            } else {
+              onlyMessage('收信人和收信部门必填一个', 'error');
+              resolve(false);
+            }
+          } else {
+            if (formData) {
+              resolve(formData);
+            } else {
+              resolve(false);
+            }
+          }
         } else {
-          resolve(false);
+          if (formData) {
+            resolve(formData);
+          } else {
+            resolve(false);
+          }
         }
       } else {
         resolve({});

+ 58 - 46
src/pages/system/User/Save/index.tsx

@@ -59,6 +59,7 @@ const Save = (props: Props) => {
 
   registerValidateRules({
     checkStrength(value: string) {
+      if (!value || value.length < 8 || value.length > 64) return true;
       if (/^[0-9]+$/.test(value) || /^[a-zA-Z]+$/.test(value) || /^[~!@#$%^&*]+$/.test(value)) {
         return {
           type: 'error',
@@ -177,6 +178,7 @@ const Save = (props: Props) => {
                 triggerType: 'onBlur',
                 validator: (value: string) => {
                   return new Promise((resolve) => {
+                    if (!value) resolve('');
                     service
                       .validateField('username', value)
                       .then((resp) => {
@@ -214,17 +216,17 @@ const Save = (props: Props) => {
           placeholder: '请输入密码',
         },
         'x-visible': model === 'add',
-        'x-reactions': [
-          {
-            dependencies: ['.confirmPassword'],
-            fulfill: {
-              state: {
-                selfErrors:
-                  '{{$deps[0] && $self.value && $self.value !==$deps[0] ? "两次密码输入不一致" : ""}}',
-              },
-            },
-          },
-        ],
+        // 'x-reactions': [
+        //   {
+        //     dependencies: ['.confirmPassword'],
+        //     fulfill: {
+        //       state: {
+        //         selfErrors:
+        //           '{{$deps[0].length > 8 && $deps[0].length > 8 && $self.value && $self.value !==$deps[0] ? "两次密码输入不一致" : ""}}',
+        //       },
+        //     },
+        //   },
+        // ],
         name: 'password',
         'x-validator': [
           {
@@ -240,40 +242,10 @@ const Save = (props: Props) => {
             message: '请输入密码',
           },
           {
-            checkStrength: true,
-          },
-        ],
-      },
-      confirmPassword: {
-        type: 'string',
-        title: intl.formatMessage({
-          id: 'pages.system.confirmPassword',
-          defaultMessage: '确认密码?',
-        }),
-        'x-decorator': 'FormItem',
-        'x-component': 'Password',
-        'x-component-props': {
-          checkStrength: true,
-          placeholder: '请再次输入密码',
-        },
-        'x-visible': model === 'add',
-        'x-validator': [
-          {
-            max: 64,
-            message: '密码最多可输入64位',
-          },
-          {
-            min: 8,
-            message: '密码不能少于8位',
-          },
-          {
-            required: model === 'add',
-            message: '请输入确认密码',
-          },
-          {
             triggerType: 'onBlur',
             validator: (value: string) => {
               return new Promise((resolve) => {
+                if (!value || value.length < 8 || value.length > 64) resolve('');
                 service
                   .validateField('password', value)
                   .then((resp) => {
@@ -292,11 +264,41 @@ const Save = (props: Props) => {
               });
             },
           },
+          // {
+          //   checkStrength: true,
+          // },
+        ],
+      },
+      confirmPassword: {
+        type: 'string',
+        title: intl.formatMessage({
+          id: 'pages.system.confirmPassword',
+          defaultMessage: '确认密码?',
+        }),
+        'x-decorator': 'FormItem',
+        'x-component': 'Password',
+        'x-component-props': {
+          checkStrength: true,
+          placeholder: '请再次输入密码',
+        },
+        'x-visible': model === 'add',
+        'x-validator': [
+          // {
+          //   max: 64,
+          //   message: '密码最多可输入64位',
+          // },
+          // {
+          //   min: 8,
+          //   message: '密码不能少于8位',
+          // },
           {
-            checkStrength: true,
+            required: model === 'add',
+            message: '请输入确认密码',
           },
+          // {
+          //   checkStrength: true,
+          // },
         ],
-
         'x-reactions': [
           {
             dependencies: ['.password'],
@@ -410,7 +412,12 @@ const Save = (props: Props) => {
             title: '手机号',
             'x-decorator': 'FormItem',
             'x-component': 'Input',
-            'x-validator': 'phone',
+            'x-validator': [
+              {
+                format: 'phone',
+                message: '请输入正确的手机号',
+              },
+            ],
             'x-decorator-props': {
               gridSpan: 1,
             },
@@ -422,7 +429,12 @@ const Save = (props: Props) => {
             title: '邮箱',
             'x-decorator': 'FormItem',
             'x-component': 'Input',
-            'x-validator': 'email',
+            'x-validator': [
+              {
+                format: 'email',
+                message: '请输入正确的邮箱',
+              },
+            ],
             'x-decorator-props': {
               gridSpan: 1,
             },

+ 1 - 1
src/utils/util.ts

@@ -52,7 +52,7 @@ export const downloadFileByUrl = (url: string, name: string, type: string) => {
 export const downloadObject = (record: Record<string, any>, fileName: string) => {
   // 创建隐藏的可下载链接
   const ghostLink = document.createElement('a');
-  ghostLink.download = `${record?.name}${fileName}_${moment(new Date()).format(
+  ghostLink.download = `${record?.name || ''}${fileName}_${moment(new Date()).format(
     'YYYY/MM/DD HH:mm:ss',
   )}.json`;
   ghostLink.style.display = 'none';