Explorar el Código

feat(metadata): merge department

Lind hace 3 años
padre
commit
6d4e8ede89

+ 2 - 0
package.json

@@ -78,6 +78,7 @@
     "braft-editor": "^2.3.9",
     "classnames": "^2.2.6",
     "dexie": "^3.0.3",
+    "event-source-polyfill": "^1.0.25",
     "isomorphic-form-data": "^2.0.0",
     "jetlinks-store": "^0.0.3",
     "lodash": "^4.17.11",
@@ -101,6 +102,7 @@
   },
   "devDependencies": {
     "@ant-design/pro-cli": "^2.0.2",
+    "@types/event-source-polyfill": "^1.0.0",
     "@types/express": "^4.17.0",
     "@types/history": "^4.7.2",
     "@types/jest": "^26.0.0",

+ 2 - 2
src/app.tsx

@@ -210,8 +210,8 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
 
 export function patchRoutes(routes: any) {
   if (extraRoutes && extraRoutes.length) {
-    console.log(getRoutes(extraRoutes));
-    routes.routes[1].routes = [...routes.routes[1].routes, ...getRoutes(extraRoutes)];
+    const basePath = routes.routes.find((_route: any) => _route.path === '/')!;
+    basePath.routes = [...basePath.routes, ...getRoutes(extraRoutes)];
   }
 }
 

+ 1 - 0
src/locales/zh-CN/pages.ts

@@ -181,6 +181,7 @@ export default {
   'pages.device.productDetail.protocolName': '消息协议',
   'pages.device.productDetail.transportProtocol': '链接协议',
   'pages.device.productDetail.createTime': '创建时间',
+  'pages.device.productDetail.updateTime': '创建时间',
   'pages.device.productDetail.base': '配置信息',
   'pages.device.productDetail.base.save': '保存',
   'pages.device.productDetail.metadata': '物模型',

+ 141 - 0
src/pages/device/Instance/Export/index.tsx

@@ -0,0 +1,141 @@
+import { FormItem, FormLayout, Select } from '@formily/antd';
+import { createForm } from '@formily/core';
+import { createSchemaField, FormProvider } from '@formily/react';
+import { Alert, Modal, Radio } from 'antd';
+import 'antd/lib/tree-select/style/index.less';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/device/Instance';
+import type { DeviceInstance } from '../typings';
+import SystemConst from '@/utils/const';
+import Token from '@/utils/token';
+import encodeQuery from '@/utils/encodeQuery';
+
+interface Props {
+  visible: boolean;
+  close: () => void;
+  data?: DeviceInstance;
+}
+
+const Export = (props: Props) => {
+  const { visible, close } = props;
+  const [productList, setProductList] = useState<any[]>([]);
+  const SchemaField = createSchemaField({
+    components: {
+      Radio,
+      Select,
+      FormItem,
+      FormLayout,
+    },
+  });
+
+  useEffect(() => {
+    service.getProductList({ paging: false }).then((resp) => {
+      if (resp.status === 200) {
+        const list = resp.result.map((item: { name: any; id: any }) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        console.log(list);
+        setProductList(list);
+      }
+    });
+  }, []);
+
+  const form = createForm();
+  const schema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-component': 'FormLayout',
+        'x-component-props': {
+          labelCol: 4,
+          wrapperCol: 18,
+          labelAlign: 'right',
+        },
+        properties: {
+          product: {
+            type: 'string',
+            title: '产品',
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            enum: [...productList],
+          },
+          fileType: {
+            type: 'number',
+            title: '文件格式',
+            default: 'xlsx',
+            enum: [
+              {
+                label: 'xlsx',
+                value: 'xlsx',
+              },
+              {
+                label: 'csv',
+                value: 'csv',
+              },
+            ],
+            'x-decorator': 'FormItem',
+            'x-component': 'Radio.Group',
+            'x-component-props': {
+              optionType: 'button',
+              buttonStyle: 'solid',
+            },
+          },
+        },
+      },
+    },
+  };
+  const downloadTemplate = async () => {
+    const values = (await form.submit()) as any;
+    const formElement = document.createElement('form');
+    formElement.style.display = 'display:none;';
+    formElement.method = 'GET';
+    if (values.product) {
+      formElement.action = `/${SystemConst.API_BASE}/device/instance/${values.product}/export.${values.fileType}`;
+    } else {
+      formElement.action = `/${SystemConst.API_BASE}/device/instance/export.${values.fileType}`;
+    }
+    const params = encodeQuery(props.data);
+    Object.keys(params).forEach((key: string) => {
+      const inputElement = document.createElement('input');
+      inputElement.type = 'hidden';
+      inputElement.name = key;
+      inputElement.value = params[key];
+      formElement.appendChild(inputElement);
+    });
+    const inputElement = document.createElement('input');
+    inputElement.type = 'hidden';
+    inputElement.name = ':X_Access_Token';
+    inputElement.value = Token.get();
+    formElement.appendChild(inputElement);
+
+    document.body.appendChild(formElement);
+    formElement.submit();
+    document.body.removeChild(formElement);
+  };
+  return (
+    <Modal
+      visible={visible}
+      onCancel={() => close()}
+      width="35vw"
+      title="导出"
+      onOk={() => {
+        downloadTemplate();
+      }}
+    >
+      <Alert
+        message="不勾选产品,默认导出所有设备的基础数据,勾选单个产品可导出下属的详细数据"
+        type="warning"
+        showIcon
+        closable
+      />
+      <div style={{ marginTop: '20px' }}>
+        <FormProvider form={form}>
+          <SchemaField schema={schema} />
+        </FormProvider>
+      </div>
+    </Modal>
+  );
+};
+export default Export;

+ 276 - 0
src/pages/device/Instance/Import/index.tsx

@@ -0,0 +1,276 @@
+import { FormItem, FormLayout, Select } from '@formily/antd';
+import { createForm, onFieldValueChange } from '@formily/core';
+import { createSchemaField, FormProvider } from '@formily/react';
+import { Badge, Button, Checkbox, message, Modal, Radio, Space, Upload } from 'antd';
+import 'antd/lib/tree-select/style/index.less';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/device/Instance';
+import type { DeviceInstance } from '../typings';
+import FUpload from '@/components/Upload';
+import { UploadOutlined } from '@ant-design/icons';
+import SystemConst from '@/utils/const';
+import Token from '@/utils/token';
+import { EventSourcePolyfill } from 'event-source-polyfill';
+
+interface Props {
+  visible: boolean;
+  close: () => void;
+  data?: DeviceInstance;
+}
+
+const FileFormat = (props: any) => {
+  const [data, setData] = useState<{ autoDeploy: boolean; fileType: 'xlsx' | 'csv' }>({
+    autoDeploy: false,
+    fileType: 'xlsx',
+  });
+  return (
+    <Space>
+      <Radio.Group
+        defaultValue="xlsx"
+        buttonStyle="solid"
+        onChange={(e) => {
+          setData({
+            ...data,
+            fileType: e.target.value,
+          });
+          props.onChange({
+            ...data,
+            fileType: e.target.value,
+          });
+        }}
+      >
+        <Radio.Button value="xlsx">xlsx</Radio.Button>
+        <Radio.Button value="csv">csv</Radio.Button>
+      </Radio.Group>
+      <Checkbox
+        onChange={(e) => {
+          setData({
+            ...data,
+            autoDeploy: e.target.checked,
+          });
+          props.onChange({
+            ...data,
+            autoDeploy: e.target.checked,
+          });
+        }}
+      >
+        自动启用
+      </Checkbox>
+    </Space>
+  );
+};
+
+const downloadTemplate = (type: string, product: string) => {
+  const formElement = document.createElement('form');
+  formElement.style.display = 'display:none;';
+  formElement.method = 'GET';
+  formElement.action = `/${SystemConst.API_BASE}/device-instance/${product}/template.${type}`;
+  const inputElement = document.createElement('input');
+  inputElement.type = 'hidden';
+  inputElement.name = ':X_Access_Token';
+  inputElement.value = Token.get();
+  formElement.appendChild(inputElement);
+  document.body.appendChild(formElement);
+  formElement.submit();
+  document.body.removeChild(formElement);
+};
+
+const NormalUpload = (props: any) => {
+  return (
+    <div>
+      <Space>
+        <Upload
+          action={`/${SystemConst.API_BASE}/file/static`}
+          headers={{
+            'X-Access-Token': Token.get(),
+          }}
+          onChange={(info) => {
+            if (info.file.status === 'done') {
+              message.success('上传成功');
+              const resp: any = info.file.response || { result: '' };
+              props.onChange(resp?.result || '');
+            }
+          }}
+          showUploadList={false}
+        >
+          <Button icon={<UploadOutlined />}>上传文件</Button>
+        </Upload>
+        <div style={{ marginLeft: 20 }}>
+          下载模板
+          <a
+            style={{ marginLeft: 10 }}
+            onClick={() => {
+              downloadTemplate('xlsx', props.product);
+            }}
+          >
+            .xlsx
+          </a>
+          <a
+            style={{ marginLeft: 10 }}
+            onClick={() => {
+              downloadTemplate('csv', props.product);
+            }}
+          >
+            .csv
+          </a>
+        </div>
+      </Space>
+    </div>
+  );
+};
+
+const Import = (props: Props) => {
+  const { visible, close } = props;
+  const [productList, setProductList] = useState<any[]>([]);
+  const [importLoading, setImportLoading] = useState(false);
+  const [flag, setFlag] = useState<boolean>(true);
+  const [count, setCount] = useState<number>(0);
+  const [errMessage, setErrMessage] = useState<string>('');
+
+  const SchemaField = createSchemaField({
+    components: {
+      Radio,
+      Select,
+      FormItem,
+      FormLayout,
+      FUpload,
+      FileFormat,
+      NormalUpload,
+    },
+  });
+
+  useEffect(() => {
+    service.getProductList({ paging: false }).then((resp) => {
+      if (resp.status === 200) {
+        const list = resp.result.map((item: { name: any; id: any }) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        setProductList(list);
+      }
+    });
+  }, []);
+
+  const form = createForm({
+    effects() {
+      onFieldValueChange('product', (field) => {
+        form.setFieldState('*(fileType, upload)', (state) => {
+          state.visible = !!field.value;
+        });
+        form.setFieldState('*(fileType)', (state) => {
+          state.componentProps = {
+            product: field.value,
+          };
+        });
+      });
+    },
+  });
+
+  const schema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-component': 'FormLayout',
+        'x-component-props': {
+          labelCol: 4,
+          wrapperCol: 18,
+          labelAlign: 'right',
+        },
+        properties: {
+          product: {
+            type: 'string',
+            title: '产品',
+            required: true,
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            enum: [...productList],
+          },
+          fileType: {
+            title: '文件格式',
+            'x-visible': false,
+            'x-decorator': 'FormItem',
+            'x-component': 'FileFormat',
+          },
+          upload: {
+            type: 'string',
+            title: '文件上传',
+            'x-visible': false,
+            'x-decorator': 'FormItem',
+            'x-component': 'NormalUpload',
+          },
+        },
+      },
+    },
+  };
+
+  const submitData = () => {
+    const values = form.getFormState().values;
+    if (!!values?.upload) {
+      setCount(0);
+      setErrMessage('');
+      setFlag(true);
+      const autoDeploy = !!values?.fileType?.autoDeploy || false;
+      setImportLoading(true);
+      let dt = 0;
+      const source = new EventSourcePolyfill(
+        `/${SystemConst.API_BASE}/device/instance/${values.product}/import?fileUrl=${
+          values?.upload
+        }&autoDeploy=${autoDeploy}&:X_Access_Token=${Token.get()}`,
+      );
+      source.onmessage = (e: any) => {
+        const res = JSON.parse(e.data);
+        if (res.success) {
+          close();
+          const temp = res.result.total;
+          dt += temp;
+          setCount(dt);
+        } else {
+          setErrMessage(res.message);
+        }
+      };
+      source.onerror = () => {
+        setFlag(false);
+        source.close();
+      };
+      source.onopen = () => {};
+    } else {
+      message.error('请先上传文件');
+    }
+  };
+  return (
+    <Modal
+      visible={visible}
+      onCancel={() => close()}
+      width="35vw"
+      title="导出"
+      onOk={() => submitData()}
+      footer={[
+        <Button key="cancel" onClick={() => close()}>
+          取消
+        </Button>,
+        <Button key="ok" type="primary" onClick={() => submitData()}>
+          确认
+        </Button>,
+      ]}
+    >
+      <div style={{ marginTop: '20px' }}>
+        <FormProvider form={form}>
+          <SchemaField schema={schema} />
+        </FormProvider>
+      </div>
+      {importLoading && (
+        <div style={{ marginLeft: 20 }}>
+          {flag ? (
+            <Badge status="processing" text="进行中" />
+          ) : (
+            <Badge status="success" text="已完成" />
+          )}
+          <span style={{ marginLeft: 15 }}>总数量:{count}</span>
+          <p style={{ color: 'red' }}>{errMessage}</p>
+        </div>
+      )}
+    </Modal>
+  );
+};
+export default Import;

+ 93 - 0
src/pages/device/Instance/Process/index.tsx

@@ -0,0 +1,93 @@
+import { Badge, Modal } from 'antd';
+import { useEffect, useState } from 'react';
+import { EventSourcePolyfill } from 'event-source-polyfill';
+
+interface Props {
+  api: string;
+  closeVisible: () => void;
+  action: string;
+}
+
+interface State {
+  source: any;
+}
+
+const Process = (props: Props) => {
+  const initState: State = {
+    source: {},
+  };
+  const [eventSource, setSource] = useState<any>(initState.source);
+  const [count, setCount] = useState<number>(0);
+  const [flag, setFlag] = useState<boolean>(true);
+  const [errMessage, setErrMessage] = useState<string>('');
+  const { action } = props;
+
+  const getData = () => {
+    let dt = 0;
+
+    const source = new EventSourcePolyfill(props.api);
+    setSource(source);
+    source.onmessage = (e: any) => {
+      const res = JSON.parse(e.data);
+      switch (action) {
+        case 'active':
+          if (res.success) {
+            dt += res.total;
+            setCount(dt);
+          }
+          break;
+        case 'sync':
+          dt += res;
+          setCount(dt);
+          break;
+        case 'import':
+          if (res.success) {
+            const temp = res.result.total;
+            dt += temp;
+            setCount(dt);
+          } else {
+            setErrMessage(res.message);
+          }
+          break;
+        default:
+          break;
+      }
+    };
+    source.onerror = () => {
+      setFlag(false);
+      source.close();
+    };
+    source.onopen = () => {};
+  };
+  useEffect(() => {
+    getData();
+  }, []);
+  return (
+    <Modal
+      title="当前进度"
+      visible
+      confirmLoading={flag}
+      okText="确认"
+      onOk={() => {
+        props.closeVisible();
+        setCount(0);
+        eventSource.close();
+      }}
+      cancelText="关闭"
+      onCancel={() => {
+        props.closeVisible();
+        setCount(0);
+        eventSource.close();
+      }}
+    >
+      {flag ? (
+        <Badge status="processing" text="进行中" />
+      ) : (
+        <Badge status="success" text="已完成" />
+      )}
+      <p>总数量:{count}</p>
+      <p style={{ color: 'red' }}>{errMessage}</p>
+    </Modal>
+  );
+};
+export default Process;

+ 140 - 0
src/pages/device/Instance/Save/index.tsx

@@ -0,0 +1,140 @@
+import { message, Modal } from 'antd';
+import { createForm } from '@formily/core';
+import { Form, FormItem, FormLayout, Input, Radio, Select } from '@formily/antd';
+import { createSchemaField } from '@formily/react';
+import type { ISchema } from '@formily/json-schema';
+import FUpload from '@/components/Upload';
+import { service } from '@/pages/device/Instance';
+import 'antd/lib/tree-select/style/index.less';
+import type { DeviceInstance } from '../typings';
+import { useEffect, useState } from 'react';
+
+interface Props {
+  visible: boolean;
+  close: () => void;
+  data?: DeviceInstance;
+}
+
+const Save = (props: Props) => {
+  const { visible, close, data } = props;
+  const [productList, setProductList] = useState<any[]>([]);
+  const form = createForm({
+    initialValues: data,
+  });
+
+  useEffect(() => {
+    service.getProductList({ paging: false }).then((resp) => {
+      if (resp.status === 200) {
+        const list = resp.result.map((item: { name: any; id: any }) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        setProductList(list);
+      }
+    });
+  }, []);
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      Radio,
+      FUpload,
+      FormLayout,
+    },
+  });
+
+  const handleSave = async () => {
+    const values = (await form.submit()) as any;
+    const productId = values.productId;
+    if (productId) {
+      const product = productList.find((i) => i.value === productId);
+      values.productName = product.label;
+    }
+    const resp = (await service.update(values)) as any;
+    if (resp.status === 200) {
+      message.success('保存成功');
+      props.close();
+    }
+  };
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-component': 'FormLayout',
+        'x-component-props': {
+          labelCol: 4,
+          wrapperCol: 18,
+        },
+        properties: {
+          id: {
+            title: 'ID',
+            'x-disabled': !!data?.id,
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              tooltip: <div>若不填写,系统将自动生成唯一ID</div>,
+            },
+            // 'x-validator': `{{(value) => {
+            //     return service.isExists(value).then(resp => {
+            //         if(resp.status === 200){
+            //             if(!resp.result){
+            //                 resolve('已存在该设备ID')
+            //             }
+            //         } else {
+            //             resolve('')
+            //         }
+            //     })
+            //   }}}`
+          },
+          name: {
+            title: '名称',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入名称',
+              },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+          },
+          productId: {
+            title: '所属产品',
+            required: true,
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            enum: [...productList],
+          },
+          describe: {
+            title: '描述',
+            'x-component': 'Input.TextArea',
+            'x-decorator': 'FormItem',
+            'x-component-props': {
+              showCount: true,
+              maxLength: 200,
+            },
+          },
+        },
+      },
+    },
+  };
+  return (
+    <Modal
+      visible={visible}
+      onCancel={() => close()}
+      width="30vw"
+      title={data?.id ? '编辑' : '新增'}
+      onOk={handleSave}
+    >
+      <Form form={form}>
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+export default Save;

+ 254 - 35
src/pages/device/Instance/index.tsx

@@ -1,22 +1,33 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
 import type { DeviceInstance } from '@/pages/device/Instance/typings';
 import moment from 'moment';
-import { Badge, message, Popconfirm, Tooltip } from 'antd';
-import { useRef } from 'react';
-import BaseCrud from '@/components/BaseCrud';
+import { Badge, Button, Dropdown, Menu, message, Popconfirm, Tooltip } from 'antd';
+import { useRef, useState } from 'react';
 import { Link } from 'umi';
 import {
-  CloseCircleOutlined,
-  EditOutlined,
-  EyeOutlined,
-  PlayCircleOutlined,
+  CheckCircleOutlined,
+  DeleteOutlined,
+  ExportOutlined,
+  ImportOutlined,
+  PlusOutlined,
+  SearchOutlined,
+  StopOutlined,
+  SyncOutlined,
 } from '@ant-design/icons';
-import { CurdModel } from '@/components/BaseCrud/model';
 import { model } from '@formily/reactive';
 import Service from '@/pages/device/Instance/service';
 import type { MetadataItem } from '@/pages/device/Product/typings';
 import { useIntl } from '@@/plugin-locale/localeExports';
+import Save from './Save';
+import Export from './Export';
+import Import from './Import';
+import Process from './Process';
+import encodeQuery from '@/utils/encodeQuery';
+import SearchComponent from '@/components/SearchComponent';
+import SystemConst from '@/utils/const';
+import Token from '@/utils/token';
 
 const statusMap = new Map();
 statusMap.set('在线', 'success');
@@ -43,7 +54,17 @@ export const InstanceModel = model<{
 export const service = new Service('device/instance');
 const Instance = () => {
   const actionRef = useRef<ActionType>();
+  const [visible, setVisible] = useState<boolean>(false);
+  const [exportVisible, setExportVisible] = useState<boolean>(false);
+  const [importVisible, setImportVisible] = useState<boolean>(false);
+  const [operationVisible, setOperationVisible] = useState<boolean>(false);
+  const [type, setType] = useState<'active' | 'sync'>('active');
+  const [api, setApi] = useState<string>('');
+  const [current, setCurrent] = useState<DeviceInstance>();
+  const [searchParams, setSearchParams] = useState<any>({});
+  const [bindKeys, setBindKeys] = useState<any[]>([]);
   const intl = useIntl();
+
   const columns: ProColumns<DeviceInstance>[] = [
     {
       dataIndex: 'index',
@@ -146,19 +167,22 @@ const Instance = () => {
             })}
             key={'detail'}
           >
-            <EyeOutlined />
+            <SearchOutlined />
           </Tooltip>
         </Link>,
-        <a key="editable" onClick={() => CurdModel.update(record)}>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.edit',
-              defaultMessage: '编辑',
-            })}
-          >
-            <EditOutlined />
-          </Tooltip>
-        </a>,
+        // <a key="editable" onClick={() => {
+        //   setVisible(true)
+        //   setCurrent(record)
+        // }}>
+        //   <Tooltip
+        //     title={intl.formatMessage({
+        //       id: 'pages.data.option.edit',
+        //       defaultMessage: '编辑',
+        //     })}
+        //   >
+        //     <EditOutlined />
+        //   </Tooltip>
+        // </a>,
 
         <a href={record.id} target="_blank" rel="noopener noreferrer" key="view">
           <Popconfirm
@@ -167,10 +191,11 @@ const Instance = () => {
               defaultMessage: '确认禁用?',
             })}
             onConfirm={async () => {
-              await service.update({
-                id: record.id,
-                // status: record.state?.value ? 0 : 1,
-              });
+              if (record.state.value !== 'notActive') {
+                await service.undeployDevice(record.id);
+              } else {
+                await service.deployDevice(record.id);
+              }
               message.success(
                 intl.formatMessage({
                   id: 'pages.data.option.success',
@@ -182,11 +207,33 @@ const Instance = () => {
           >
             <Tooltip
               title={intl.formatMessage({
-                id: `pages.data.option.${record.state.value ? 'disabled' : 'enabled'}`,
-                defaultMessage: record.state.value ? '禁用' : '启用',
+                id: `pages.data.option.${
+                  record.state.value !== 'notActive' ? 'disabled' : 'enabled'
+                }`,
+                defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
               })}
             >
-              {record.state.value ? <CloseCircleOutlined /> : <PlayCircleOutlined />}
+              {record.state.value !== 'notActive' ? <StopOutlined /> : <CheckCircleOutlined />}
+            </Tooltip>
+          </Popconfirm>
+        </a>,
+
+        <a key={'delete'}>
+          <Popconfirm
+            title="确认删除"
+            onConfirm={async () => {
+              await service.remove(record.id);
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            }}
+          >
+            <Tooltip title={'删除'}>
+              <DeleteOutlined />
             </Tooltip>
           </Popconfirm>
         </a>,
@@ -194,20 +241,192 @@ const Instance = () => {
     },
   ];
 
-  const schema = {};
+  const menu = (
+    <Menu>
+      <Menu.Item key="1">
+        <Button
+          icon={<ExportOutlined />}
+          type="default"
+          onClick={() => {
+            setExportVisible(true);
+          }}
+        >
+          批量导出设备
+        </Button>
+      </Menu.Item>
+      <Menu.Item key="2">
+        <Button
+          icon={<ImportOutlined />}
+          onClick={() => {
+            setImportVisible(true);
+          }}
+        >
+          批量导入设备
+        </Button>
+      </Menu.Item>
+      <Menu.Item key="4">
+        <Popconfirm
+          title={'确认激活全部设备?'}
+          onConfirm={() => {
+            setType('active');
+            const activeAPI = `/${
+              SystemConst.API_BASE
+            }/device-instance/deploy?:X_Access_Token=${Token.get()}`;
+            setApi(activeAPI);
+            setOperationVisible(true);
+          }}
+        >
+          <Button icon={<CheckCircleOutlined />} type="primary" ghost>
+            激活全部设备
+          </Button>
+        </Popconfirm>
+      </Menu.Item>
+      <Menu.Item key="5">
+        <Button
+          icon={<SyncOutlined />}
+          type="primary"
+          onClick={() => {
+            setType('sync');
+            const syncAPI = `/${
+              SystemConst.API_BASE
+            }/device-instance/state/_sync?:X_Access_Token=${Token.get()}`;
+            setApi(syncAPI);
+            setOperationVisible(true);
+          }}
+        >
+          同步设备状态
+        </Button>
+      </Menu.Item>
+      {bindKeys.length > 0 && (
+        <Menu.Item key="3">
+          <Popconfirm
+            title="确认删除选中设备?"
+            onConfirm={() => {
+              service.batchDeleteDevice(bindKeys).then((resp) => {
+                if (resp.status === 200) {
+                  message.success('操作成功');
+                  actionRef.current?.reset?.();
+                }
+              });
+            }}
+            okText="确认"
+            cancelText="取消"
+          >
+            <Button icon={<DeleteOutlined />} type="primary" danger>
+              删除选中设备
+            </Button>
+          </Popconfirm>
+        </Menu.Item>
+      )}
+      {bindKeys.length > 0 && (
+        <Menu.Item key="6">
+          <Popconfirm
+            title="确认禁用选中设备?"
+            onConfirm={() => {
+              service.batchUndeployDevice(bindKeys).then((resp) => {
+                if (resp.status === 200) {
+                  message.success('操作成功');
+                  actionRef.current?.reset?.();
+                }
+              });
+            }}
+            okText="确认"
+            cancelText="取消"
+          >
+            <Button icon={<StopOutlined />} type="primary" danger>
+              禁用选中设备
+            </Button>
+          </Popconfirm>
+        </Menu.Item>
+      )}
+    </Menu>
+  );
 
   return (
     <PageContainer>
-      <BaseCrud
+      <SearchComponent<DeviceInstance>
+        field={columns}
+        target="device-instance"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setSearchParams(data);
+        }}
+        onReset={() => {
+          // 重置分页及搜索参数
+          actionRef.current?.reset?.();
+          setSearchParams({});
+        }}
+      />
+      <ProTable<DeviceInstance>
         columns={columns}
-        service={service}
-        title={intl.formatMessage({
-          id: 'pages.device.instance',
-          defaultMessage: '设备管理',
-        })}
-        schema={schema}
         actionRef={actionRef}
+        params={searchParams}
+        options={{ fullScreen: true }}
+        request={(params) => service.query(encodeQuery({ ...params, sorts: { id: 'ascend' } }))}
+        rowKey="id"
+        search={false}
+        pagination={{ pageSize: 10 }}
+        rowSelection={{
+          selectedRowKeys: bindKeys,
+          onChange: (selectedRowKeys, selectedRows) => {
+            setBindKeys(selectedRows.map((item) => item.id));
+          },
+        }}
+        toolBarRender={() => [
+          <Button
+            onClick={() => {
+              setVisible(true);
+              setCurrent(undefined);
+            }}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </Button>,
+          <Dropdown key={'more'} overlay={menu} placement="bottom">
+            <Button>批量操作</Button>
+          </Dropdown>,
+        ]}
+      />
+      <Save
+        data={current}
+        close={() => {
+          setVisible(false);
+          actionRef.current?.reload();
+        }}
+        visible={visible}
       />
+      <Export
+        data={searchParams}
+        close={() => {
+          setExportVisible(false);
+          actionRef.current?.reload();
+        }}
+        visible={exportVisible}
+      />
+      <Import
+        data={current}
+        close={() => {
+          setImportVisible(false);
+          actionRef.current?.reload();
+        }}
+        visible={importVisible}
+      />
+      {operationVisible && (
+        <Process
+          api={api}
+          action={type}
+          closeVisible={() => {
+            setOperationVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
     </PageContainer>
   );
 };

+ 51 - 0
src/pages/device/Instance/service.ts

@@ -8,6 +8,57 @@ import { filter, groupBy, map } from 'rxjs/operators';
 class Service extends BaseService<DeviceInstance> {
   public detail = (id: string) => request(`${this.uri}/${id}/detail`, { method: 'GET' });
 
+  // 查询产品列表
+  public getProductList = (params: any) =>
+    request(`/${SystemConst.API_BASE}/device/product/_query/no-paging`, { method: 'GET', params });
+
+  // 批量删除设备
+  public batchDeleteDevice = (params: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/batch/_delete`, {
+      method: 'PUT',
+      data: params,
+    });
+
+  // 启用设备
+  public deployDevice = (deviceId: string, params?: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/deploy`, {
+      method: 'POST',
+      data: params,
+    });
+
+  // 禁用设备
+  public undeployDevice = (deviceId: string, params?: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${deviceId}/undeploy`, {
+      method: 'POST',
+      data: params,
+    });
+
+  // 批量激活设备
+  public batchDeployDevice = (params: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/batch/_deploy`, {
+      method: 'PUT',
+      data: params,
+    });
+
+  // 批量注销设备
+  public batchUndeployDevice = (params: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/batch/_undeploy`, {
+      method: 'PUT',
+      data: params,
+    });
+
+  // 激活所有设备
+  public deployAllDevice = (params?: any) =>
+    request(`/${SystemConst.API_BASE}/device-instance/deploy`, { method: 'GET', params });
+
+  // 同步设备
+  public syncDevice = () =>
+    request(`/${SystemConst.API_BASE}/device-instance/state/_sync`, { method: 'GET' });
+
+  //验证设备ID是否重复
+  public isExists = (id: string) =>
+    request(`/${SystemConst.API_BASE}/device-instance/${id}/exists`, { method: 'GET' });
+
   public getConfigMetadata = (id: string) =>
     request(`${this.uri}/${id}/config-metadata`, { method: 'GET' });
 

+ 199 - 106
src/pages/device/Product/Detail/BaseInfo/index.tsx

@@ -1,124 +1,217 @@
-import { createSchemaField } from '@formily/react';
 import { productModel, service } from '@/pages/device/Product';
-import { Form, FormItem, FormGrid, Password, FormLayout, PreviewText, Input } from '@formily/antd';
-import { createForm } from '@formily/core';
-import { Card, Empty } from 'antd';
-import type { ISchema } from '@formily/json-schema';
-import type { SetStateAction } from 'react';
-import { useEffect, useState } from 'react';
-import type { ConfigMetadata, ConfigProperty } from '@/pages/device/Product/typings';
+import { Button, Descriptions } from 'antd';
+import { useState } from 'react';
 import { useParams } from 'umi';
 import { useIntl } from '@@/plugin-locale/localeExports';
+import { EditOutlined } from '@ant-design/icons';
+import { getDateFormat } from '@/utils/util';
+import Save from '@/pages/device/Product/Save';
 
-const componentMap = {
-  string: 'Input',
-  password: 'Password',
-};
+// const componentMap = {
+//   string: 'Input',
+//   password: 'Password',
+// };
 const BaseInfo = () => {
   const intl = useIntl();
   const param = useParams<{ id: string }>();
-  const [metadata, setMetadata] = useState<ConfigMetadata[]>([]);
-  const [state, setState] = useState<boolean>(false);
+  // const [metadata, setMetadata] = useState<ConfigMetadata[]>([]);
+  // const [state, setState] = useState<boolean>(false);
+  const [visible, setVisible] = useState(false);
 
-  const form = createForm({
-    validateFirst: true,
-    readPretty: state,
-    initialValues: productModel.current?.configuration,
-  });
+  // const form = createForm({
+  //   validateFirst: true,
+  //   readPretty: state,
+  //   initialValues: productModel.current?.configuration,
+  // });
 
-  useEffect(() => {
-    if (param.id) {
-      service
-        .getConfigMetadata(param.id)
-        .then((config: { result: SetStateAction<ConfigMetadata[]> }) => {
-          setMetadata(config.result);
-        });
-    }
-  }, [param.id]);
+  // useEffect(() => {
+  //   if (param.id) {
+  //     service
+  //       .getConfigMetadata(param.id)
+  //       .then((config: { result: SetStateAction<ConfigMetadata[]> }) => {
+  //         setMetadata(config.result);
+  //       });
+  //   }
+  // }, [param.id]);
 
-  const SchemaField = createSchemaField({
-    components: {
-      Password,
-      FormGrid,
-      PreviewText,
-      FormItem,
-      Input,
-    },
-  });
+  // const SchemaField = createSchemaField({
+  //   components: {
+  //     Password,
+  //     FormGrid,
+  //     PreviewText,
+  //     FormItem,
+  //     Input,
+  //   },
+  // });
+  //
+  // 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 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,
-        },
-      };
+  const getDetailInfo = () => {
+    service.getProductDetail(param?.id).subscribe((data) => {
+      if (data) {
+        productModel.current = data;
+      }
     });
-    return config;
   };
 
-  const renderConfigCard = () => {
-    return metadata && metadata.length > 0 ? (
-      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 renderConfigCard = () => {
+  //   return metadata && metadata.length > 0 ? (
+  //     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),
+  //           },
+  //         },
+  //       };
+  //
+  //       return (
+  //         <Card
+  //           key={item.name}
+  //           title={item.name}
+  //           extra={
+  //             <a onClick={() => setState(!state)}>
+  //               {state ? (
+  //                 <>
+  //                   {intl.formatMessage({
+  //                     id: 'pages.data.option.edit',
+  //                     defaultMessage: '编辑',
+  //                   })}
+  //                 </>
+  //               ) : (
+  //                 <>
+  //                   {intl.formatMessage({
+  //                     id: 'pages.device.productDetail.base.save',
+  //                     defaultMessage: '保存',
+  //                   })}
+  //                 </>
+  //               )}
+  //             </a>
+  //           }
+  //         >
+  //           <PreviewText.Placeholder value='-'>
+  //             <Form form={form}>
+  //               <FormLayout labelCol={6} wrapperCol={16}>
+  //                 <SchemaField schema={itemSchema} />
+  //               </FormLayout>
+  //             </Form>
+  //           </PreviewText.Placeholder>
+  //         </Card>
+  //       );
+  //     })
+  //   ) : (
+  //     <Empty description={'暂无配置'} />
+  //   );
+  // };
 
-        return (
-          <Card
-            key={item.name}
-            title={item.name}
-            extra={
-              <a onClick={() => setState(!state)}>
-                {state ? (
-                  <>
-                    {intl.formatMessage({
-                      id: 'pages.data.option.edit',
-                      defaultMessage: '编辑',
-                    })}
-                  </>
-                ) : (
-                  <>
-                    {intl.formatMessage({
-                      id: 'pages.device.productDetail.base.save',
-                      defaultMessage: '保存',
-                    })}
-                  </>
-                )}
-              </a>
-            }
+  return (
+    <>
+      <Descriptions
+        size="small"
+        column={3}
+        title={[
+          <span key={1}>产品信息</span>,
+          <Button
+            key={2}
+            type={'link'}
+            onClick={() => {
+              setVisible(true);
+            }}
           >
-            <PreviewText.Placeholder value="-">
-              <Form form={form}>
-                <FormLayout labelCol={6} wrapperCol={16}>
-                  <SchemaField schema={itemSchema} />
-                </FormLayout>
-              </Form>
-            </PreviewText.Placeholder>
-          </Card>
-        );
-      })
-    ) : (
-      <Empty description={'暂无配置'} />
-    );
-  };
-
-  return <>{renderConfigCard()}</>;
+            <EditOutlined />
+          </Button>,
+        ]}
+        bordered
+      >
+        <Descriptions.Item
+          label={intl.formatMessage({
+            id: 'pages.device.category',
+            defaultMessage: '产品ID',
+          })}
+        >
+          {productModel.current?.id}
+        </Descriptions.Item>
+        <Descriptions.Item
+          label={intl.formatMessage({
+            id: 'pages.device.productDetail.classifiedName',
+            defaultMessage: '所属品类',
+          })}
+        >
+          {productModel.current?.classifiedName}
+        </Descriptions.Item>
+        <Descriptions.Item
+          label={intl.formatMessage({
+            id: 'pages.device.productDetail.protocolName',
+            defaultMessage: '消息协议',
+          })}
+        >
+          {productModel.current?.protocolName}
+        </Descriptions.Item>
+        <Descriptions.Item
+          label={intl.formatMessage({
+            id: 'pages.device.productDetail.transportProtocol',
+            defaultMessage: '链接协议',
+          })}
+        >
+          {productModel.current?.transportProtocol}
+        </Descriptions.Item>
+        <Descriptions.Item
+          label={intl.formatMessage({
+            id: 'pages.device.productDetail.updateTime',
+            defaultMessage: '更新时间',
+          })}
+        >
+          {getDateFormat(productModel.current?.updateTime)}
+        </Descriptions.Item>
+        <Descriptions.Item
+          label={intl.formatMessage({
+            id: 'pages.device.productDetail.createTime',
+            defaultMessage: '创建时间',
+          })}
+        >
+          {getDateFormat(productModel.current?.createTime)}
+        </Descriptions.Item>
+        <Descriptions.Item
+          span={3}
+          label={intl.formatMessage({
+            id: 'pages.device.productDetail.metadata.describe',
+            defaultMessage: '描述',
+          })}
+        >
+          {productModel.current?.describe}
+        </Descriptions.Item>
+      </Descriptions>
+      <Save
+        model={'edit'}
+        data={productModel.current}
+        close={() => {
+          setVisible(false);
+        }}
+        reload={getDetailInfo}
+        visible={visible}
+      />
+    </>
+  );
 };
 export default BaseInfo;

+ 35 - 64
src/pages/device/Product/Detail/index.tsx

@@ -1,6 +1,17 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { history, useParams } from 'umi';
-import { Button, Card, Descriptions, Space, Tabs, Badge, message, Spin, Tooltip } from 'antd';
+import { history, Link, useParams } from 'umi';
+import {
+  Badge,
+  Button,
+  Card,
+  Descriptions,
+  message,
+  Space,
+  Spin,
+  Switch,
+  Tabs,
+  Tooltip,
+} from 'antd';
 import BaseInfo from '@/pages/device/Product/Detail/BaseInfo';
 import { observer } from '@formily/react';
 import { productModel, service } from '@/pages/device/Product';
@@ -9,10 +20,11 @@ import { useIntl } from '@@/plugin-locale/localeExports';
 import Metadata from '@/pages/device/components/Metadata';
 import Alarm from '@/pages/device/components/Alarm';
 import type { DeviceMetadata } from '@/pages/device/Product/typings';
-import { Link } from 'umi';
 import { Store } from 'jetlinks-store';
 import MetadataAction from '@/pages/device/components/Metadata/DataBaseAction';
 import { QuestionCircleOutlined } from '@ant-design/icons';
+import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import encodeQuery from '@/utils/encodeQuery';
 
 const ProductDetail = observer(() => {
   const intl = useIntl();
@@ -59,6 +71,11 @@ const ProductDetail = observer(() => {
           MetadataAction.insert(metadata);
         }
       });
+      service.instanceCount(encodeQuery({ terms: { productId: param?.id } })).then((res: any) => {
+        if (res.status === 200) {
+          productModel.current!.count = res.result;
+        }
+      });
     }
 
     return () => {
@@ -109,73 +126,27 @@ const ProductDetail = observer(() => {
       content={
         <Spin spinning={loading}>
           <Descriptions size="small" column={2}>
-            <Descriptions.Item
-              label={intl.formatMessage({
-                id: 'pages.device.category',
-                defaultMessage: '产品ID',
-              })}
-            >
-              {productModel.current?.id}
-            </Descriptions.Item>
-            <Descriptions.Item
-              label={intl.formatMessage({
-                id: 'pages.table.productName',
-                defaultMessage: '产品名称',
-              })}
-            >
-              {productModel.current?.name}
-            </Descriptions.Item>
-            <Descriptions.Item
-              label={intl.formatMessage({
-                id: 'pages.device.productDetail.classifiedName',
-                defaultMessage: '所属品类',
-              })}
-            >
-              {productModel.current?.classifiedName}
-            </Descriptions.Item>
-            <Descriptions.Item
-              label={intl.formatMessage({
-                id: 'pages.device.productDetail.protocolName',
-                defaultMessage: '消息协议',
-              })}
-            >
-              {productModel.current?.protocolName}
-            </Descriptions.Item>
-            <Descriptions.Item
-              label={intl.formatMessage({
-                id: 'pages.device.productDetail.transportProtocol',
-                defaultMessage: '链接协议',
-              })}
-            >
-              {productModel.current?.transportProtocol}
-            </Descriptions.Item>
             <Descriptions.Item label={'设备数量'}>
-              <Link to={'/device/instance'}> {productModel.current?.count}</Link>
-            </Descriptions.Item>
-            <Descriptions.Item
-              label={intl.formatMessage({
-                id: 'pages.device.productDetail.createTime',
-                defaultMessage: '创建时间',
-              })}
-            >
-              {productModel.current?.createTime}
+              <Link to={getMenuPathByCode(MENUS_CODE['device/Instance'])}>
+                {' '}
+                {productModel.current?.count || 0}
+              </Link>
             </Descriptions.Item>
           </Descriptions>
         </Spin>
       }
-      extra={[
-        statusMap[productModel.current?.state || 0].component,
-        <Button
-          key="2"
-          onClick={() => {
+      title={productModel.current?.name}
+      subTitle={
+        <Switch
+          key={2}
+          checkedChildren="启用"
+          unCheckedChildren="停用"
+          onChange={() => {
             changeDeploy(statusMap[productModel.current?.state || 0].action);
           }}
-        >
-          {intl.formatMessage({
-            id: `pages.device.productDetail.${statusMap[productModel.current?.state || 0].key}`,
-            defaultMessage: statusMap[productModel.current?.state || 1].name,
-          })}
-        </Button>,
+        />
+      }
+      extra={[
         <Button key="1" type="primary" onClick={() => changeDeploy('deploy')}>
           {intl.formatMessage({
             id: 'pages.device.productDetail.setting',
@@ -185,7 +156,7 @@ const ProductDetail = observer(() => {
       ]}
     >
       <Card>
-        <Tabs tabPosition="left" defaultActiveKey="base">
+        <Tabs defaultActiveKey="base">
           <Tabs.TabPane
             tab={intl.formatMessage({
               id: 'pages.device.productDetail.base',

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

@@ -1,7 +1,7 @@
 import { message, Modal } from 'antd';
 import type { Field } from '@formily/core';
 import { createForm, onFieldValueChange } from '@formily/core';
-import { TreeSelect, Form, FormItem, FormLayout, Input, Radio, Select } from '@formily/antd';
+import { Form, FormItem, FormLayout, Input, Radio, Select, TreeSelect } from '@formily/antd';
 import { createSchemaField } from '@formily/react';
 import type { ISchema } from '@formily/json-schema';
 import FUpload from '@/components/Upload';
@@ -9,11 +9,14 @@ import { service } from '@/pages/device/Product';
 import { action } from '@formily/reactive';
 import 'antd/lib/tree-select/style/index.less';
 import type { ProductItem } from '@/pages/device/Product/typings';
+import { useIntl } from '@@/plugin-locale/localeExports';
 
 interface Props {
   visible: boolean;
   close: () => void;
+  reload: () => void;
   data?: ProductItem;
+  model: 'add' | 'edit';
 }
 
 /**
@@ -36,6 +39,8 @@ const treeToArray = (tree: any) => {
 
 const Save = (props: Props) => {
   const { visible, close, data } = props;
+  const intl = useIntl();
+
   const handleData = () => {
     // 特殊处理deviceType字段
     if (data) {
@@ -119,6 +124,7 @@ const Save = (props: Props) => {
     const resp = (await service.update(values)) as any;
     if (resp.status === 200) {
       message.success('保存成功');
+      props.reload();
       props.close();
     }
   };
@@ -145,6 +151,9 @@ const Save = (props: Props) => {
             'x-decorator-props': {
               tooltip: <div>若不填写,系统将自动生成唯一ID</div>,
             },
+            'x-component-props': {
+              disabled: props.model === 'edit',
+            },
           },
           name: {
             title: '名称',
@@ -223,7 +232,16 @@ const Save = (props: Props) => {
     },
   };
   return (
-    <Modal visible={visible} onCancel={() => close()} width="30vw" title="新增" onOk={handleSave}>
+    <Modal
+      visible={visible}
+      onCancel={() => close()}
+      width="30vw"
+      title={intl.formatMessage({
+        id: `pages.data.option.${props.model}`,
+        defaultMessage: '新增',
+      })}
+      onOk={handleSave}
+    >
       <Form form={form}>
         <SchemaField schema={schema} scope={{ useAsyncDataSource }} />
       </Form>

+ 99 - 40
src/pages/device/Product/index.tsx

@@ -1,14 +1,14 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import { Badge, Button, message, Space, Tooltip } from 'antd';
+import { Badge, Button, message, Popconfirm, Space, Tooltip } from 'antd';
 import type { ProductItem } from '@/pages/device/Product/typings';
 import {
-  CloseCircleOutlined,
+  DeleteOutlined,
   DownloadOutlined,
   EditOutlined,
   EyeOutlined,
-  MinusOutlined,
   PlayCircleOutlined,
   PlusOutlined,
+  StopOutlined,
 } from '@ant-design/icons';
 import Service from '@/pages/device/Product/service';
 import { observer } from '@formily/react';
@@ -16,11 +16,12 @@ import { model } from '@formily/reactive';
 import { Link } from 'umi';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import { useRef, useState } from 'react';
 import ProTable from '@jetlinks/pro-table';
-import { lastValueFrom } from 'rxjs';
+import { useRef, useState } from 'react';
 import encodeQuery from '@/utils/encodeQuery';
 import Save from '@/pages/device/Product/Save';
+import SearchComponent from '@/components/SearchComponent';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
 
 export const service = new Service('device-product');
 export const statusMap = {
@@ -38,6 +39,8 @@ const Product = observer(() => {
   const [current, setCurrent] = useState<ProductItem>();
   const actionRef = useRef<ActionType>();
   const intl = useIntl();
+  const [param, setParam] = useState({});
+
   const status = {
     1: (
       <Badge
@@ -58,31 +61,54 @@ const Product = observer(() => {
       />
     ),
   };
+
+  const deleteItem = async (id: string) => {
+    const response: any = await service.remove(id);
+    if (response.status === 200) {
+      message.success(
+        intl.formatMessage({
+          id: 'pages.data.option.success',
+          defaultMessage: '操作成功!',
+        }),
+      );
+    }
+    actionRef.current?.reload();
+  };
+
+  /**
+   * table 查询参数
+   * @param data
+   */
+  const searchFn = (data: any) => {
+    setParam({
+      terms: data,
+    });
+  };
+
+  const changeDeploy = (id: string, state: 'deploy' | 'undeploy') => {
+    service.changeDeploy(id, state).subscribe((res) => {
+      if (res) {
+        actionRef?.current?.reload();
+      }
+    });
+  };
+
   const columns: ProColumns<ProductItem>[] = [
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
       title: 'ID',
       dataIndex: 'id',
     },
     {
-      title: '产品名称',
+      title: '名称',
       dataIndex: 'name',
     },
     {
-      title: '状态',
-      render: (_, row) => <Space size={0}>{status[row.state]}</Space>,
-    },
-    {
-      title: '设备数量',
-      dataIndex: 'count',
+      title: '设备类型',
+      dataIndex: 'classifiedName',
     },
     {
-      title: '设备分类',
-      dataIndex: 'classifiedName',
+      title: '状态',
+      render: (_, row) => <Space size={0}>{status[row.state]}</Space>,
     },
     {
       title: intl.formatMessage({
@@ -104,7 +130,7 @@ const Product = observer(() => {
             onClick={() => {
               productModel.current = record;
             }}
-            to={`/device/product/detail/${record.id}`}
+            to={`${getMenuPathByParams(MENUS_CODE['device/Product/Detail'], record.id)}`}
             key="link"
           >
             <EyeOutlined />
@@ -147,46 +173,76 @@ const Product = observer(() => {
             />
           </a>
         </Tooltip>,
-        <Tooltip
+        <Popconfirm
+          key={'state'}
           title={intl.formatMessage({
-            id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}`,
-            defaultMessage: record.state ? '禁用' : '启用',
+            id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}.tips`,
+            defaultMessage: '是否删除该菜单',
           })}
-          key={'state'}
+          onConfirm={() => {
+            changeDeploy(record.id, record.state ? 'undeploy' : 'deploy');
+          }}
         >
-          <a key="state">{record.state ? <CloseCircleOutlined /> : <PlayCircleOutlined />}</a>
-        </Tooltip>,
-        <Tooltip
+          <Tooltip
+            title={intl.formatMessage({
+              id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}`,
+              defaultMessage: record.state ? '禁用' : '启用',
+            })}
+          >
+            <a key="state">{record.state ? <StopOutlined /> : <PlayCircleOutlined />}</a>
+          </Tooltip>
+        </Popconfirm>,
+        <Popconfirm
+          key="unBindUser"
           title={intl.formatMessage({
-            id: 'pages.data.option.remove',
-            defaultMessage: '删除',
+            id: 'page.system.menu.table.delete',
+            defaultMessage: '是否删除该菜单',
           })}
-          key={'remove'}
+          onConfirm={() => {
+            deleteItem(record.id);
+          }}
         >
-          <a key="delete">
-            <MinusOutlined />
-          </a>
-        </Tooltip>,
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.remove.tips',
+              defaultMessage: '删除',
+            })}
+            key={'remove'}
+          >
+            <a key="delete">
+              <DeleteOutlined />
+            </a>
+          </Tooltip>
+        </Popconfirm>,
       ],
     },
   ];
 
   return (
     <PageContainer>
+      <SearchComponent field={columns} onSearch={searchFn} />
       <ProTable<ProductItem>
         columns={columns}
         actionRef={actionRef}
         options={{ fullScreen: true }}
-        request={async (params = {}) => {
-          return await lastValueFrom(
-            service.queryZipCount(encodeQuery({ ...params, sorts: { id: 'ascend' } })),
-          );
-        }}
+        // request={async (params = {}) => {
+        //   return await lastValueFrom(
+        //     service.queryZipCount(encodeQuery({ ...params, sorts: { id: 'ascend' } })),
+        //   );
+        // }}
+        params={param}
+        request={(params = {}) =>
+          service.query(encodeQuery({ ...params, sorts: { createTime: 'ascend' } }))
+        }
         rowKey="id"
+        search={false}
         pagination={{ pageSize: 10 }}
         toolBarRender={() => [
           <Button
-            onClick={() => setVisible(true)}
+            onClick={() => {
+              setCurrent(undefined);
+              setVisible(true);
+            }}
             key="button"
             icon={<PlusOutlined />}
             type="primary"
@@ -199,9 +255,12 @@ const Product = observer(() => {
         ]}
       />
       <Save
+        model={!current ? 'add' : 'edit'}
         data={current}
         close={() => {
           setVisible(false);
+        }}
+        reload={() => {
           actionRef.current?.reload();
         }}
         visible={visible}

+ 7 - 7
src/pages/device/Product/service.ts

@@ -18,7 +18,7 @@ class Service extends BaseService<ProductItem> {
         from((i.result as PageResult)?.data).pipe(
           concatMap((t: ProductItem) =>
             from(this.instanceCount(encodeQuery({ terms: { productId: t.id } }))).pipe(
-              map((count) => ({ ...t, count: count.result })),
+              map((count: any) => ({ ...t, count: count.result })),
             ),
           ),
           toArray(),
@@ -74,8 +74,8 @@ class Service extends BaseService<ProductItem> {
         }),
       ),
     ).pipe(
-      filter((resp) => resp.status === 200),
-      map((resp) => resp.result),
+      filter((resp: any) => resp.status === 200),
+      map((resp: any) => resp.result),
     );
 
   public codecs = () =>
@@ -86,8 +86,8 @@ class Service extends BaseService<ProductItem> {
         }),
       ),
     ).pipe(
-      filter((resp) => resp.status === 200),
-      map((resp) => resp.result),
+      filter((resp: any) => resp.status === 200),
+      map((resp: any) => resp.result),
     );
 
   public convertMetadata = (direction: 'from' | 'to', type: string, data: any) =>
@@ -99,8 +99,8 @@ class Service extends BaseService<ProductItem> {
         }),
       ),
     ).pipe(
-      filter((resp) => resp.status === 200),
-      map((resp) => resp.result),
+      filter((resp: any) => resp.status === 200),
+      map((resp: any) => resp.result),
     );
 
   public productAlarm = (id: string) =>

+ 8 - 6
src/pages/device/Product/typings.d.ts

@@ -1,5 +1,10 @@
 import type { BaseItem, State } from '@/utils/typings';
 
+type DeviceType = {
+  text: string;
+  value: string;
+};
+
 export type ProductItem = {
   id: string;
   name: string;
@@ -7,13 +12,9 @@ export type ProductItem = {
   classifiedName: string;
   configuration: Record<string, any>;
   createTime: number;
+  updateTime: number;
   creatorId: string;
-  deviceType:
-    | {
-        text: string;
-        value: string;
-      }
-    | string;
+  deviceType: string | DeviceType;
   count?: number;
   messageProtocol: string;
   metadata: string;
@@ -21,6 +22,7 @@ export type ProductItem = {
   protocolName: string;
   state: number;
   transportProtocol: string;
+  describe?: string;
 };
 
 export type ConfigProperty = {

+ 29 - 21
src/pages/system/Department/Assets/deivce/bind.tsx

@@ -1,5 +1,5 @@
 // 资产-产品分类-绑定
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import { DeviceBadge, service } from './index';
 import { message, Modal } from 'antd';
@@ -10,6 +10,7 @@ import { observer } from '@formily/react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { DeviceItem } from '@/pages/system/Department/typings';
 import PermissionModal from '@/pages/system/Department/Assets/permissionModal';
+import SearchComponent from '@/components/SearchComponent';
 
 interface Props {
   reload: () => void;
@@ -22,6 +23,7 @@ const Bind = observer((props: Props) => {
   const param = useParams<{ id: string }>();
   const actionRef = useRef<ActionType>();
   const [perVisible, setPerVisible] = useState(false);
+  const [searchParam, setSearchParam] = useState({});
 
   const columns: ProColumns<DeviceItem>[] = [
     {
@@ -98,13 +100,35 @@ const Bind = observer((props: Props) => {
           }
         }}
       />
+      <SearchComponent<DeviceItem>
+        field={columns}
+        pattern={'simple'}
+        defaultParam={[
+          {
+            column: 'id',
+            termType: 'dim-assets$not',
+            value: {
+              assetType: 'device',
+              targets: [
+                {
+                  type: 'org',
+                  id: param.id,
+                },
+              ],
+            },
+          },
+        ]}
+        onSearch={async (data) => {
+          actionRef.current?.reset?.();
+          setSearchParam(data);
+        }}
+        target="department-assets-device"
+      />
       <ProTable<DeviceItem>
         actionRef={actionRef}
         columns={columns}
         rowKey="id"
-        pagination={{
-          pageSize: 5,
-        }}
+        search={false}
         rowSelection={{
           selectedRowKeys: Models.bindKeys,
           onChange: (selectedRowKeys, selectedRows) => {
@@ -112,23 +136,7 @@ const Bind = observer((props: Props) => {
           },
         }}
         request={(params) => service.queryDeviceList(params)}
-        params={{
-          terms: [
-            {
-              column: 'id',
-              termType: 'dim-assets$not',
-              value: {
-                assetType: 'device',
-                targets: [
-                  {
-                    type: 'org',
-                    id: param.id,
-                  },
-                ],
-              },
-            },
-          ],
-        }}
+        params={searchParam}
       />
     </Modal>
   );

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

@@ -1,8 +1,8 @@
 // 资产分配-产品分类
-import ProTable from '@jetlinks/pro-table';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import { Button, message, Popconfirm, Tooltip, Badge } from 'antd';
+import { Badge, Button, message, Popconfirm, Tooltip } from 'antd';
 import { useRef, useState } from 'react';
 import { useParams } from 'umi';
 import { observer } from '@formily/react';
@@ -182,7 +182,6 @@ export default observer(() => {
       />
       <SearchComponent<DeviceItem>
         field={columns}
-        pattern={'simple'}
         defaultParam={[
           {
             column: 'id',

+ 32 - 21
src/pages/system/Department/Assets/product/bind.tsx

@@ -1,5 +1,5 @@
 // 资产-产品分类-绑定
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import { service } from './index';
 import { message, Modal } from 'antd';
@@ -8,8 +8,9 @@ import Models from './model';
 import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import type { ProductCategoryItem } from '@/pages/system/Department/typings';
 import PermissionModal from '@/pages/system/Department/Assets/permissionModal';
+import type { ProductItem } from '@/pages/system/Department/typings';
+import SearchComponent from '@/components/SearchComponent';
 
 interface Props {
   reload: () => void;
@@ -22,8 +23,9 @@ const Bind = observer((props: Props) => {
   const param = useParams<{ id: string }>();
   const actionRef = useRef<ActionType>();
   const [perVisible, setPerVisible] = useState(false);
+  const [searchParam, setSearchParam] = useState({});
 
-  const columns: ProColumns<ProductCategoryItem>[] = [
+  const columns: ProColumns<ProductItem>[] = [
     {
       dataIndex: 'id',
       title: 'ID',
@@ -86,10 +88,35 @@ const Bind = observer((props: Props) => {
           }
         }}
       />
-      <ProTable<ProductCategoryItem>
+      <SearchComponent<ProductItem>
+        field={columns}
+        pattern={'simple'}
+        defaultParam={[
+          {
+            column: 'id',
+            termType: 'dim-assets$not',
+            value: {
+              assetType: 'product',
+              targets: [
+                {
+                  type: 'org',
+                  id: param.id,
+                },
+              ],
+            },
+          },
+        ]}
+        onSearch={async (data) => {
+          actionRef.current?.reset?.();
+          setSearchParam(data);
+        }}
+        target="department-assets-product"
+      />
+      <ProTable<ProductItem>
         actionRef={actionRef}
         columns={columns}
         rowKey="id"
+        search={false}
         pagination={{
           pageSize: 5,
         }}
@@ -100,23 +127,7 @@ const Bind = observer((props: Props) => {
           },
         }}
         request={(params) => service.queryProductList(params)}
-        params={{
-          terms: [
-            {
-              column: 'id',
-              termType: 'dim-assets$not',
-              value: {
-                assetType: 'product',
-                targets: [
-                  {
-                    type: 'org',
-                    id: param.id,
-                  },
-                ],
-              },
-            },
-          ],
-        }}
+        params={searchParam}
       />
     </Modal>
   );

+ 1 - 2
src/pages/system/Department/Assets/product/index.tsx

@@ -1,6 +1,6 @@
 // 资产分配-产品分类
-import ProTable from '@jetlinks/pro-table';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { Button, message, Popconfirm, Tooltip } from 'antd';
 import { useRef, useState } from 'react';
@@ -123,7 +123,6 @@ export default observer(() => {
       />
       <SearchComponent<ProductItem>
         field={columns}
-        pattern={'simple'}
         defaultParam={[
           {
             column: 'id',

+ 41 - 23
src/pages/system/Department/Assets/productCategory/bind.tsx

@@ -1,15 +1,17 @@
 // 资产-产品分类-绑定
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
-import { service, getTableKeys } from './index';
-import { Modal, message } from 'antd';
+import { getTableKeys, service } from './index';
+import { message, Modal } from 'antd';
 import { useParams } from 'umi';
 import Models from './model';
-import { useRef, useState, useEffect } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import { observer } from '@formily/react';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { ProductCategoryItem } from '@/pages/system/Department/typings';
 import PermissionModal from '@/pages/system/Department/Assets/permissionModal';
+import SearchComponent from '@/components/SearchComponent';
+import { difference } from 'lodash';
 
 interface Props {
   reload: () => void;
@@ -22,6 +24,7 @@ const Bind = observer((props: Props) => {
   const param = useParams<{ id: string }>();
   const actionRef = useRef<ActionType>();
   const [perVisible, setPerVisible] = useState(false);
+  const [searchParam, setSearchParam] = useState({});
 
   const columns: ProColumns<ProductCategoryItem>[] = [
     {
@@ -84,34 +87,49 @@ const Bind = observer((props: Props) => {
           }
         }}
       />
+      <SearchComponent<ProductCategoryItem>
+        field={columns}
+        pattern="simple"
+        defaultParam={[
+          {
+            column: 'id',
+            termType: 'dim-assets$not',
+            value: {
+              assetType: 'deviceCategory',
+              targets: [
+                {
+                  type: 'org',
+                  id: param.id,
+                },
+              ],
+            },
+          },
+        ]}
+        onSearch={async (data) => {
+          actionRef.current?.reset?.();
+          setSearchParam(data);
+        }}
+        target="department-assets-category"
+      />
       <ProTable<ProductCategoryItem>
         actionRef={actionRef}
         columns={columns}
         rowKey="id"
+        search={false}
         pagination={false}
         rowSelection={{
           selectedRowKeys: Models.bindKeys,
-          onChange: (selectedRowKeys, selectedRows) => {
-            Models.bindKeys = getTableKeys(selectedRows);
+          onSelect: (record, selected, selectedRows) => {
+            const keys = getTableKeys(selected ? selectedRows : [record]);
+            if (selected) {
+              Models.bindKeys = keys;
+            } else {
+              // 去除重复的key
+              Models.bindKeys = difference(Models.bindKeys, keys);
+            }
           },
         }}
-        params={{
-          terms: [
-            {
-              column: 'id',
-              termType: 'dim-assets$not',
-              value: {
-                assetType: 'deviceCategory',
-                targets: [
-                  {
-                    type: 'org',
-                    id: param.id,
-                  },
-                ],
-              },
-            },
-          ],
-        }}
+        params={searchParam}
         request={async (params) => {
           const response = await service.queryProductCategoryList(params);
           return {

+ 12 - 5
src/pages/system/Department/Assets/productCategory/index.tsx

@@ -1,8 +1,8 @@
 // 资产分配-产品分类
-import ProTable from '@jetlinks/pro-table';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import { Button, Popconfirm, Tooltip, message } from 'antd';
+import { Button, message, Popconfirm, Tooltip } from 'antd';
 import { useRef, useState } from 'react';
 import { useParams } from 'umi';
 import { observer } from '@formily/react';
@@ -12,6 +12,7 @@ import Models from '@/pages/system/Department/Assets/productCategory/model';
 import Service from '@/pages/system/Department/Assets/service';
 import Bind from './bind';
 import SearchComponent from '@/components/SearchComponent';
+import { difference } from 'lodash';
 
 export const service = new Service<ProductCategoryItem>('assets');
 
@@ -24,6 +25,7 @@ export const getTableKeys = (rows: ProductCategoryItem[]): string[] => {
       keys = [...keys, ...childrenKeys];
     }
   });
+
   return keys;
 };
 
@@ -141,7 +143,6 @@ export default observer(() => {
       />
       <SearchComponent<ProductCategoryItem>
         field={columns}
-        pattern="simple"
         defaultParam={[
           {
             column: 'id',
@@ -188,8 +189,14 @@ export default observer(() => {
         }}
         rowSelection={{
           selectedRowKeys: Models.unBindKeys,
-          onChange: (selectedRowKeys, selectedRows) => {
-            Models.unBindKeys = selectedRows.map((item) => item.id);
+          onSelect: (record, selected, selectedRows) => {
+            const keys = getTableKeys(selected ? selectedRows : [record]);
+            if (selected) {
+              Models.unBindKeys = keys;
+            } else {
+              // 去除重复的key
+              Models.unBindKeys = difference(Models.unBindKeys, keys);
+            }
           },
         }}
         toolBarRender={() => [

+ 16 - 13
src/pages/system/Department/Member/bind.tsx

@@ -1,13 +1,14 @@
 // 部门-用户绑定
-import type { ProColumns, ActionType } from '@jetlinks/pro-table';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import ProTable from '@jetlinks/pro-table';
 import { service } from '@/pages/system/Department/Member';
 import { message, Modal } from 'antd';
 import { useParams } from 'umi';
 import MemberModel from '@/pages/system/Department/Member/model';
 import { observer } from '@formily/react';
-import { useEffect, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import { useIntl } from '@@/plugin-locale/localeExports';
+import SearchComponent from '@/components/SearchComponent';
 
 interface Props {
   reload: () => void;
@@ -18,6 +19,7 @@ interface Props {
 const Bind = observer((props: Props) => {
   const intl = useIntl();
   const param = useParams<{ id: string }>();
+  const [searchParam, setSearchParam] = useState({});
   const actionRef = useRef<ActionType>();
 
   useEffect(() => {
@@ -74,21 +76,22 @@ const Bind = observer((props: Props) => {
       width={990}
       title="绑定"
     >
+      <SearchComponent<UserItem>
+        pattern={'simple'}
+        field={columns}
+        defaultParam={[{ column: 'id$in-dimension$org$not', value: param.id }]}
+        onSearch={async (data) => {
+          actionRef.current?.reset?.();
+          setSearchParam(data);
+        }}
+        target="department-user"
+      />
       <ProTable
         actionRef={actionRef}
         columns={columns}
         rowKey="id"
-        pagination={{
-          pageSize: 5,
-        }}
-        params={{
-          terms: [
-            {
-              column: 'id$in-dimension$org$not',
-              value: param.id,
-            },
-          ],
-        }}
+        search={false}
+        params={searchParam}
         rowSelection={{
           selectedRowKeys: MemberModel.bindUsers,
           onChange: (selectedRowKeys, selectedRows) => {

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

@@ -1,7 +1,7 @@
 // 部门-用户管理
 import { PageContainer } from '@ant-design/pro-layout';
-import ProTable from '@jetlinks/pro-table';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import { Badge, Button, message, Popconfirm, Tooltip } from 'antd';
 import { useRef, useState } from 'react';
@@ -10,7 +10,7 @@ import { observer } from '@formily/react';
 import MemberModel from '@/pages/system/Department/Member/model';
 import type { MemberItem } from '@/pages/system/Department/typings';
 import Service from '@/pages/system/Department/Member/service';
-import { PlusOutlined, DisconnectOutlined } from '@ant-design/icons';
+import { DisconnectOutlined, PlusOutlined } from '@ant-design/icons';
 import Bind from './bind';
 import SearchComponent from '@/components/SearchComponent';
 
@@ -155,9 +155,9 @@ const Member = observer(() => {
         reload={() => actionRef.current?.reload()}
       />
       <SearchComponent<MemberItem>
-        pattern={'simple'}
+        // pattern={'simple'}
         field={columns}
-        defaultParam={[{ column: 'id$in-dimension$org', value: param.id, termType: 'eq' }]}
+        defaultParam={[{ column: 'id$in-dimension$org', value: param.id }]}
         onSearch={async (data) => {
           actionRef.current?.reset?.();
           setSearchParam(data);

+ 27 - 2
src/pages/system/Permission/index.tsx

@@ -258,7 +258,19 @@ const Permission: React.FC = observer(() => {
         'x-decorator': 'FormItem',
         'x-component': 'Input',
         name: 'id',
-        required: true,
+        'x-decorator-props': {
+          tooltip: <div>标识ID需与代码中的标识ID一致</div>,
+        },
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+          {
+            required: true,
+            message: '请输入标识(ID)',
+          },
+        ],
       },
       name: {
         title: intl.formatMessage({
@@ -269,7 +281,16 @@ const Permission: React.FC = observer(() => {
         'x-decorator': 'FormItem',
         'x-component': 'Input',
         name: 'name',
-        required: true,
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+          {
+            required: true,
+            message: '请输入名称',
+          },
+        ],
       },
       status: {
         title: '状态',
@@ -290,6 +311,9 @@ const Permission: React.FC = observer(() => {
         name: 'properties.assetTypes',
         required: false,
         enum: PermissionModel.assetsTypesList,
+        'x-decorator-props': {
+          tooltip: <div>关联资产为角色权限中的权限分配提供数据支持</div>,
+        },
         'x-component-props': {
           showSearch: true,
           mode: 'multiple',
@@ -416,6 +440,7 @@ const Permission: React.FC = observer(() => {
         actionRef={actionRef}
         columns={columns}
         service={service}
+        defaultParams={{ sorts: [{ name: 'modifyTime', order: 'desc' }] }}
         title={intl.formatMessage({
           id: 'pages.system.permission',
           defaultMessage: '',

+ 2 - 2
src/pages/system/Permission/service.ts

@@ -25,8 +25,8 @@ class Service extends BaseService<PermissionItem> {
   public batchAdd = (data: any) =>
     defer(() =>
       from(
-        request(`/${SystemConst.API_BASE}/dimension/_batch`, {
-          method: 'POST',
+        request(`/${SystemConst.API_BASE}/permission`, {
+          method: 'PATCH',
           data,
         }),
       ),

+ 0 - 3
src/utils/menu.ts

@@ -82,8 +82,6 @@ export const MENUS_CODE = {
   'system/Menu': 'system/Menu',
   'system/OpenAPI': 'system/OpenAPI',
   'system/Permission': 'system/Permission',
-  'system/Role/Edit/Info': 'system/Role/Edit/Info',
-  'system/Role/Edit/UserManage': 'system/Role/Edit/UserManage',
   'system/Role/Edit': 'system/Role/Edit',
   'system/Role': 'system/Role',
   'system/Tenant/Detail/Assets': 'system/Tenant/Detail/Assets',
@@ -92,7 +90,6 @@ export const MENUS_CODE = {
   'system/Tenant/Detail/Permission': 'system/Tenant/Detail/Permission',
   'system/Tenant/Detail': 'system/Tenant/Detail',
   'system/Tenant': 'system/Tenant',
-  'system/User/Save': 'system/User/Save',
   'system/User': 'system/User',
   'user/Login': 'user/Login',
   'visualization/Category': 'visualization/Category',

+ 7 - 0
src/utils/util.ts

@@ -34,3 +34,10 @@ export const useAsyncDataSource =
       }),
     );
   };
+
+export const getDateFormat = (
+  date: moment.MomentInput,
+  format: string = 'YYYY-MM-DD HH:mm:ss',
+): string => {
+  return date ? moment(date).format(format) : '-';
+};