Jelajahi Sumber

feat(merge): 合并代码

xieyonghong 4 tahun lalu
induk
melakukan
f70314b811
35 mengubah file dengan 20656 tambahan dan 441 penghapusan
  1. 2 2
      .github/workflows/ci.yml
  2. 2 2
      .github/workflows/docker.yml
  3. 1 1
      .gitignore
  4. 1 1
      src/app.tsx
  5. 19 0
      src/components/CheckButton/index.less
  6. 38 0
      src/components/CheckButton/index.tsx
  7. 16 0
      src/components/FRuleEditor/Advance/index.less
  8. 33 0
      src/components/FRuleEditor/Advance/index.tsx
  9. 78 0
      src/components/FRuleEditor/Debug/index.less
  10. 151 0
      src/components/FRuleEditor/Debug/index.tsx
  11. 180 0
      src/components/FRuleEditor/Editor/index.tsx
  12. 38 0
      src/components/FRuleEditor/Operator/index.less
  13. 83 0
      src/components/FRuleEditor/Operator/index.tsx
  14. 11 0
      src/components/FRuleEditor/Operator/service.ts
  15. 10 0
      src/components/FRuleEditor/Operator/typings.d.ts
  16. 2 2
      src/components/FRuleEditor/index.less
  17. 41 133
      src/components/FRuleEditor/index.tsx
  18. 1 0
      src/components/HeaderSearch/index.tsx
  19. 1 0
      src/locales/en-US/pages.ts
  20. 1 0
      src/locales/zh-CN/pages.ts
  21. 26 24
      src/pages/device/Category/Save/index.tsx
  22. 26 22
      src/pages/device/Category/index.tsx
  23. 1 1
      src/pages/device/Category/service.ts
  24. 134 27
      src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx
  25. 70 0
      src/pages/device/Instance/Detail/Running/Property/EditProperty.tsx
  26. 0 0
      src/pages/device/Instance/Detail/Running/Property/PropertyCard copy.tsx
  27. 85 0
      src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx
  28. 7 0
      src/pages/device/Instance/Detail/Running/Property/index.less
  29. 244 0
      src/pages/device/Instance/Detail/Running/Property/index.tsx
  30. 164 159
      src/pages/device/Instance/Detail/Running/index.tsx
  31. 44 8
      src/pages/device/Product/Detail/index.tsx
  32. 0 6
      src/pages/device/Product/index.tsx
  33. 64 52
      src/pages/device/components/Metadata/Base/Edit/index.tsx
  34. 1 1
      src/pages/device/components/Metadata/Base/index.tsx
  35. 19081 0
      yarn.lock

+ 2 - 2
.github/workflows/ci.yml

@@ -7,7 +7,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        node_version: [12.x, 14.x]
+        node_version: [14.x]
         os: [ubuntu-latest, windows-latest, macOS-latest]
     steps:
       - uses: actions/checkout@v1
@@ -16,7 +16,7 @@ jobs:
         with:
           node-version: ${{ matrix.node_version }}
       - run: echo ${{github.ref}}
-      - run: npm install --force
+      - run: yarn install
       - run: yarn run lint
       - run: yarn run tsc
       - run: yarn run build

+ 2 - 2
.github/workflows/docker.yml

@@ -16,8 +16,8 @@ jobs:
           key: jetlinks-ui-pro-repository
       - name: Install 🔧
         run: |
-          npm install --force
-          npm run-script build
+          yarn install
+          yarn run build
           cp -r dist docker/
           docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-pro:$(node -p "require('./package.json').version") ./docker
 

+ 1 - 1
.gitignore

@@ -17,7 +17,7 @@ yarn-error.log
 
 /coverage
 .idea
-yarn.lock
+#yarn.lock
 package-lock.json
 *bak
 .vscode

+ 1 - 1
src/app.tsx

@@ -138,7 +138,7 @@ export const request: RequestConfig = {
         if (resp) {
           notification.error({
             key: 'error',
-            message: JSON.parse(resp).message,
+            message: JSON.parse(resp).message || '服务器内部错误!',
           });
         } else {
           response

+ 19 - 0
src/components/CheckButton/index.less

@@ -0,0 +1,19 @@
+.box {
+  display: flex;
+  .item {
+    width: 30px;
+    height: 30px;
+    font-size: 20px;
+    line-height: 30px;
+    text-align: center;
+    border-top: 1px solid lightgray;
+    border-bottom: 1px solid lightgray;
+    cursor: pointer;
+  }
+  .left {
+    border-left: 1px solid lightgray;
+  }
+  .right {
+    border-right: 1px solid lightgray;
+  }
+}

+ 38 - 0
src/components/CheckButton/index.tsx

@@ -0,0 +1,38 @@
+import { AppstoreFilled, UnorderedListOutlined } from '@ant-design/icons';
+import classnames from 'classnames';
+import styles from './index.less';
+interface Props {
+  value: boolean;
+  change: (value: boolean) => void;
+}
+
+const CheckButton = (props: Props) => {
+  const activeStyle = {
+    border: '1px solid #1d39c4',
+    color: '#1d39c4',
+  };
+
+  return (
+    <div className={styles.box}>
+      <div
+        className={classnames(styles.item, styles.left)}
+        style={props.value ? activeStyle : {}}
+        onClick={() => {
+          props.change(true);
+        }}
+      >
+        <AppstoreFilled />
+      </div>
+      <div
+        className={classnames(styles.item, styles.right)}
+        style={!props.value ? activeStyle : {}}
+        onClick={() => {
+          props.change(false);
+        }}
+      >
+        <UnorderedListOutlined />
+      </div>
+    </div>
+  );
+};
+export default CheckButton;

+ 16 - 0
src/components/FRuleEditor/Advance/index.less

@@ -0,0 +1,16 @@
+.box {
+  display: flex;
+  justify-content: flex-start;
+  width: 100%;
+
+  .left {
+    width: 1000px;
+  }
+
+  .right {
+    width: 30%;
+    margin-left: 10px;
+    padding-left: 10px;
+    border-left: 1px solid lightgray;
+  }
+}

+ 33 - 0
src/components/FRuleEditor/Advance/index.tsx

@@ -0,0 +1,33 @@
+import { Modal } from 'antd';
+import Debug from '../Debug';
+import Operator from '../Operator';
+import styles from './index.less';
+import Editor from '@/components/FRuleEditor/Editor';
+
+interface Props {
+  model: 'advance' | 'simple';
+  onChange: (value: 'advance' | 'simple') => void;
+}
+
+const Advance = (props: Props) => {
+  const { model, onChange } = props;
+  return (
+    <Modal
+      visible={model === 'advance'}
+      width="70vw"
+      title="设置属性规则"
+      onCancel={() => onChange('simple')}
+    >
+      <div className={styles.box}>
+        <div className={styles.left}>
+          <Editor mode="advance" />
+          <Debug />
+        </div>
+        <div className={styles.right}>
+          <Operator />
+        </div>
+      </div>
+    </Modal>
+  );
+};
+export default Advance;

+ 78 - 0
src/components/FRuleEditor/Debug/index.less

@@ -0,0 +1,78 @@
+.container {
+  display: flex;
+  width: 100%;
+  height: 340px;
+  margin-top: 20px;
+
+  .left {
+    flex: auto;
+    max-width: 550px;
+    overflow-y: auto;
+    border: 1px solid lightgray;
+
+    .header {
+      display: flex;
+      align-items: center;
+      width: 100%;
+      height: 40px;
+      border-bottom: 1px solid lightgray;
+      //justify-content: space-around;
+
+      div {
+        display: flex;
+        //width: 100%;
+        align-items: center;
+        justify-content: flex-start;
+        height: 100%;
+
+        .title {
+          margin: 0 10px;
+          font-weight: 600;
+          font-size: 16px;
+        }
+
+        .description {
+          margin-left: 10px;
+          color: lightgray;
+          font-size: 12px;
+        }
+      }
+
+      .action {
+        width: 150px;
+        font-size: 14px;
+      }
+    }
+  }
+
+  .right {
+    flex: auto;
+    border: 1px solid lightgray;
+    border-left: none;
+
+    .header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      width: 100%;
+      height: 40px;
+      border-bottom: 1px solid lightgray;
+
+      .title {
+        display: flex;
+
+        div {
+          margin: 0 10px;
+        }
+      }
+
+      .action {
+        display: flex;
+
+        div {
+          margin: 0 10px;
+        }
+      }
+    }
+  }
+}

+ 151 - 0
src/components/FRuleEditor/Debug/index.tsx

@@ -0,0 +1,151 @@
+import styles from './index.less';
+import { createSchemaField } from '@formily/react';
+import { ArrayTable, Form, FormItem, Input, Select } from '@formily/antd';
+import { useMemo } from 'react';
+import { createForm } from '@formily/core';
+import type { ISchema } from '@formily/json-schema';
+import FAutoComplete from '@/components/FAutoComplete';
+
+const Debug = () => {
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      ArrayTable,
+      FAutoComplete,
+    },
+  });
+  const form = useMemo(() => createForm(), []);
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      array: {
+        type: 'array',
+        'x-decorator': 'FormItem',
+        'x-component': 'ArrayTable',
+        'x-component-props': {
+          pagination: {
+            pageSize: 9999,
+          },
+          style: {
+            maxHeight: 260,
+            overflowY: 'auto',
+          },
+          scroll: { y: 240 },
+        },
+        items: {
+          type: 'object',
+          properties: {
+            column1: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '属性ID',
+              },
+              properties: {
+                id: {
+                  'x-decorator': 'FormItem',
+                  'x-component': 'FAutoComplete',
+                  enum: [1, 2, 4],
+                },
+              },
+            },
+            column2: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '当前值',
+              },
+              properties: {
+                current: {
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                },
+              },
+            },
+            column3: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '上一值',
+              },
+              properties: {
+                last: {
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                },
+              },
+            },
+            column6: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '',
+                dataIndex: 'operations',
+                width: 50,
+                fixed: 'right',
+              },
+              properties: {
+                item: {
+                  type: 'void',
+                  'x-component': 'FormItem',
+                  properties: {
+                    remove: {
+                      type: 'void',
+                      'x-component': 'ArrayTable.Remove',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+        properties: {
+          add: {
+            type: 'void',
+            'x-component': 'ArrayTable.Addition',
+            title: '添加条目',
+          },
+        },
+      },
+    },
+  };
+  return (
+    <div className={styles.container}>
+      <div className={styles.left}>
+        <div className={styles.header}>
+          <div>
+            <div className={styles.title}>
+              属性赋值
+              <div className={styles.description}>请对上方规则使用的属性进行赋值</div>
+            </div>
+          </div>
+        </div>
+        <Form form={form}>
+          <SchemaField schema={schema} />
+        </Form>
+      </div>
+      <div className={styles.right}>
+        <div className={styles.header}>
+          <div className={styles.title}>
+            <div>运行结果</div>
+          </div>
+
+          <div className={styles.action}>
+            <div>
+              <a>开始运行</a>
+              {/*<a>停止运行</a>*/}
+            </div>
+            <div>
+              <a>清空</a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Debug;

+ 180 - 0
src/components/FRuleEditor/Editor/index.tsx

@@ -0,0 +1,180 @@
+import styles from '@/components/FRuleEditor/index.less';
+import { Dropdown, Menu } from 'antd';
+import { FullscreenOutlined, MoreOutlined } from '@ant-design/icons';
+import MonacoEditor, { monaco } from 'react-monaco-editor';
+import { useEffect, useRef } from 'react';
+import type * as monacoEditor from 'monaco-editor';
+import { Store } from 'jetlinks-store';
+import { State } from '@/components/FRuleEditor';
+
+const symbolList = [
+  {
+    key: 'add',
+    value: '+',
+  },
+  {
+    key: 'subtract',
+    value: '-',
+  },
+  {
+    key: 'multiply',
+    value: '*',
+  },
+  {
+    key: 'divide',
+    value: '/',
+  },
+  {
+    key: 'parentheses',
+    value: '()',
+  },
+  {
+    key: 'cubic',
+    value: '^',
+  },
+  {
+    key: 'dayu',
+    value: '>',
+  },
+  {
+    key: 'dayudengyu',
+    value: '>=',
+  },
+  {
+    key: 'dengyudengyu',
+    value: '==',
+  },
+  {
+    key: 'xiaoyudengyu',
+    value: '<=',
+  },
+  {
+    key: 'xiaoyu',
+    value: '<',
+  },
+  {
+    key: 'jiankuohao',
+    value: '<>',
+  },
+  {
+    key: 'andand',
+    value: '&&',
+  },
+  {
+    key: 'huohuo',
+    value: '||',
+  },
+  {
+    key: 'fei',
+    value: '!',
+  },
+  {
+    key: 'and',
+    value: '&',
+  },
+  {
+    key: 'huo',
+    value: '|',
+  },
+  {
+    key: 'bolang',
+    value: '~',
+  },
+];
+
+interface Props {
+  mode?: 'advance' | 'simple';
+  onChange?: (value: 'advance' | 'simple') => void;
+}
+
+const Editor = (props: Props) => {
+  const editorRef = useRef<monacoEditor.editor.IStandaloneCodeEditor>();
+  const editorDidMountHandle = (editor: monacoEditor.editor.IStandaloneCodeEditor) => {
+    editorRef.current = editor;
+  };
+
+  const handleInsertCode = (value: string) => {
+    const editor = editorRef.current;
+    if (!editor) return;
+    const position = editor.getPosition()!;
+    editor?.executeEdits(State.code, [
+      {
+        range: new monaco.Range(
+          position?.lineNumber,
+          position?.column,
+          position?.lineNumber,
+          position?.column,
+        ),
+        text: value,
+      },
+    ]);
+  };
+
+  useEffect(() => {
+    const subscription = Store.subscribe('add-operator-value', handleInsertCode);
+    return () => subscription.unsubscribe();
+  }, []);
+
+  return (
+    <div className={styles.box}>
+      <div className={styles.top}>
+        <div className={styles.left}>
+          {symbolList
+            .filter((t, i) => i <= 3)
+            .map((item) => (
+              <span
+                key={item.key}
+                onClick={() => {
+                  handleInsertCode(item.value);
+                }}
+              >
+                {item.value}
+              </span>
+            ))}
+          <span>
+            <Dropdown
+              overlay={
+                <Menu>
+                  {symbolList
+                    .filter((t, i) => i > 6)
+                    .map((item) => (
+                      <Menu.Item
+                        key={item.key}
+                        onClick={async () => {
+                          handleInsertCode(item.value);
+                        }}
+                      >
+                        {item.value}
+                      </Menu.Item>
+                    ))}
+                </Menu>
+              }
+            >
+              <a className="ant-dropdown-link" onClick={(e) => e.preventDefault()}>
+                <MoreOutlined />
+              </a>
+            </Dropdown>
+          </span>
+        </div>
+        <div className={styles.right}>
+          {props.mode !== 'advance' && (
+            <span>
+              <FullscreenOutlined onClick={() => props.onChange?.('advance')} />
+            </span>
+          )}
+        </div>
+      </div>
+      <MonacoEditor
+        editorDidMount={(editor) => editorDidMountHandle(editor)}
+        language={'javascript'}
+        height={300}
+        onChange={(c) => {
+          State.code = c;
+          Store.set('rule-editor-value', State.code);
+        }}
+        value={State.code}
+      />
+    </div>
+  );
+};
+export default Editor;

+ 38 - 0
src/components/FRuleEditor/Operator/index.less

@@ -0,0 +1,38 @@
+.border {
+  margin-top: 10px;
+  padding: 10px;
+  border-top: 1px solid lightgray;
+}
+
+.box {
+  width: 100%;
+
+  .explain {
+    .border;
+  }
+
+  .tree {
+    .border;
+
+    height: 350px;
+    overflow-y: auto;
+
+    .node {
+      display: flex;
+      justify-content: space-between;
+      width: 220px;
+
+      .add {
+        display: none;
+      }
+
+      &:hover .add {
+        display: block;
+      }
+
+      .parent {
+        display: none;
+      }
+    }
+  }
+}

+ 83 - 0
src/components/FRuleEditor/Operator/index.tsx

@@ -0,0 +1,83 @@
+import { Input, Tree } from 'antd';
+import Service from './service';
+import { useEffect, useRef, useState } from 'react';
+import styles from './index.less';
+import ReactMarkdown from 'react-markdown';
+import { treeFilter } from '@/utils/tree';
+import type { OperatorItem } from './typings';
+import { Store } from 'jetlinks-store';
+
+const service = new Service();
+
+const Operator = () => {
+  const [data, setData] = useState<OperatorItem[]>([]);
+  const [item, setItem] = useState<Partial<OperatorItem>>({});
+  const dataRef = useRef<OperatorItem[]>([]);
+  const getData = async () => {
+    // TODO 从物模型中获取属性数据
+    const properties = {
+      id: 'property',
+      name: '属性',
+      description: '',
+      code: '',
+      children: [
+        {
+          id: 'test',
+          name: '测试数据',
+        },
+      ],
+    };
+    const response = await service.getOperator();
+    if (response.status === 200) {
+      setData([properties, ...response.result]);
+      dataRef.current = [properties, ...response.result];
+    }
+  };
+  useEffect(() => {
+    getData();
+  }, []);
+
+  const search = (value: string) => {
+    if (value) {
+      const nodes = treeFilter(dataRef.current, value, 'name') as OperatorItem[];
+      setData(nodes);
+    } else {
+      setData(dataRef.current);
+    }
+  };
+  return (
+    <div className={styles.box}>
+      <Input.Search onSearch={search} allowClear placeholder="搜索关键字" />
+      <Tree
+        className={styles.tree}
+        onSelect={(k, info) => {
+          setItem(info.node as unknown as OperatorItem);
+        }}
+        fieldNames={{
+          title: 'name',
+          key: 'id',
+        }}
+        titleRender={(node) => (
+          <div className={styles.node}>
+            <div>{node.name}</div>
+            <div className={node.children?.length > 0 ? styles.parent : styles.add}>
+              <a
+                onClick={() => {
+                  Store.set('add-operator-value', node.code);
+                }}
+              >
+                添加
+              </a>
+            </div>
+          </div>
+        )}
+        autoExpandParent={true}
+        treeData={data}
+      />
+      <div className={styles.explain}>
+        <ReactMarkdown>{item.description || ''}</ReactMarkdown>
+      </div>
+    </div>
+  );
+};
+export default Operator;

+ 11 - 0
src/components/FRuleEditor/Operator/service.ts

@@ -0,0 +1,11 @@
+import BaseService from '@/utils/BaseService';
+import type { ProductItem } from '@/pages/device/Product/typings';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<ProductItem> {
+  public getOperator = () =>
+    request(`/${SystemConst.API_BASE}/property-calculate-rule/description`, { method: 'GET' });
+}
+
+export default Service;

+ 10 - 0
src/components/FRuleEditor/Operator/typings.d.ts

@@ -0,0 +1,10 @@
+import type { TreeNode } from '@/utils/tree';
+
+interface OperatorItem extends TreeNode {
+  id: string;
+  name: string;
+  key: string;
+  description: string;
+  code: string;
+  children: OperatorItem[];
+}

+ 2 - 2
src/components/FRuleEditor/index.less

@@ -11,7 +11,7 @@
     .left {
       display: flex;
       align-items: center;
-      width: 200px;
+      width: 60%;
       margin: 0 5px;
 
       span {
@@ -26,7 +26,7 @@
     .right {
       display: flex;
       align-items: center;
-      width: 100px;
+      width: 10%;
       margin: 0 5px;
 
       span {

+ 41 - 133
src/components/FRuleEditor/index.tsx

@@ -1,137 +1,45 @@
-import MonacoEditor from 'react-monaco-editor';
-import styles from './index.less';
-import { Dropdown, Menu, message, Tooltip } from 'antd';
-import { BugOutlined, FullscreenOutlined, MoreOutlined, PlusOutlined } from '@ant-design/icons';
+import Advance from '@/components/FRuleEditor/Advance';
+import Editor from '@/components/FRuleEditor/Editor';
+import { model } from '@formily/reactive';
+import { observer } from '@formily/react';
+import { useEffect } from 'react';
+import { Store } from 'jetlinks-store';
 
-const symbolList = [
-  {
-    key: 'add',
-    value: '+',
-  },
-  {
-    key: 'subtract',
-    value: '-',
-  },
-  {
-    key: 'multiply',
-    value: '*',
-  },
-  {
-    key: 'divide',
-    value: '/',
-  },
-  {
-    key: 'parentheses',
-    value: '()',
-  },
-  {
-    key: 'cubic',
-    value: '^',
-  },
-  {
-    key: 'dayu',
-    value: '>',
-  },
-  {
-    key: 'dayudengyu',
-    value: '>=',
-  },
-  {
-    key: 'dengyudengyu',
-    value: '==',
-  },
-  {
-    key: 'xiaoyudengyu',
-    value: '<=',
-  },
-  {
-    key: 'xiaoyu',
-    value: '<',
-  },
-  {
-    key: 'jiankuohao',
-    value: '<>',
-  },
-  {
-    key: 'andand',
-    value: '&&',
-  },
-  {
-    key: 'huohuo',
-    value: '||',
-  },
-  {
-    key: 'fei',
-    value: '!',
-  },
-  {
-    key: 'and',
-    value: '&',
-  },
-  {
-    key: 'huo',
-    value: '|',
-  },
-  {
-    key: 'bolang',
-    value: '~',
-  },
-];
-const FRuleEditor = () => {
+export const State = model<{
+  model: 'simple' | 'advance';
+  code: string;
+}>({
+  model: 'simple',
+  code: '',
+});
+
+interface Props {
+  value: string;
+  onChange: (value: string) => void;
+}
+
+const FRuleEditor = observer((props: Props) => {
+  const { value, onChange } = props;
+
+  useEffect(() => {
+    const subscription = Store.subscribe('rule-editor-value', onChange);
+    State.code = value;
+    return () => subscription.unsubscribe();
+  });
   return (
-    <div className={styles.box}>
-      <div className={styles.top}>
-        <div className={styles.left}>
-          {symbolList
-            .filter((t, i) => i <= 3)
-            .map((item) => (
-              <span key={item.key} onClick={() => message.success(`插入数据${item.value}`)}>
-                {item.value}
-              </span>
-            ))}
-          <span>
-            <Dropdown
-              overlay={
-                <Menu>
-                  {symbolList
-                    .filter((t, i) => i > 6)
-                    .map((item) => (
-                      <Menu.Item
-                        key={item.key}
-                        onClick={async () => {
-                          message.success(`选中了这个${item.value}`);
-                        }}
-                      >
-                        {item.value}
-                      </Menu.Item>
-                    ))}
-                </Menu>
-              }
-            >
-              <a className="ant-dropdown-link" onClick={(e) => e.preventDefault()}>
-                <MoreOutlined />
-              </a>
-            </Dropdown>
-          </span>
-        </div>
-        <div className={styles.right}>
-          <span>
-            <Tooltip title="进行调试">
-              <BugOutlined />
-            </Tooltip>
-          </span>
-          <span>
-            <Tooltip title="快速添加">
-              <PlusOutlined />
-            </Tooltip>
-          </span>
-          <span>
-            <FullscreenOutlined />
-          </span>
-        </div>
-      </div>
-      <MonacoEditor language={'javascript'} height={300} />
-    </div>
+    <>
+      <Editor
+        onChange={(v) => {
+          State.model = v;
+        }}
+      />
+      <Advance
+        model={State.model}
+        onChange={(v) => {
+          State.model = v;
+        }}
+      />
+    </>
   );
-};
+});
 export default FRuleEditor;

+ 1 - 0
src/components/HeaderSearch/index.tsx

@@ -31,6 +31,7 @@ const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
     ...restProps
   } = props;
 
+  // @ts-ignore
   const inputRef = useRef<Input | null>(null);
 
   const [value, setValue] = useMergedState<string | undefined>(defaultValue, {

+ 1 - 0
src/locales/en-US/pages.ts

@@ -210,6 +210,7 @@ export default {
   'pages.device.category.key': 'Key',
   'pages.device.category.name': 'Classification Name',
   'pages.device.category.addClass': 'Add Subclasses',
+  'pages.device.category.sortIndex': 'Sort',
   // 设备管理-设备
   'pages.device.instance': 'Instance Manage',
   'pages.device.instance.registrationTime': 'Registration Time',

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

@@ -225,6 +225,7 @@ export default {
   'pages.device.category.key': '标识',
   'pages.device.category.name': '分类名称',
   'pages.device.category.addClass': '添加子分类',
+  'pages.device.category.sortIndex': '分类排序',
   // 设备管理-设备
   'pages.device.instance': '设备',
   'pages.device.instance.registrationTime': '注册时间',

+ 26 - 24
src/pages/device/Category/Save/index.tsx

@@ -61,7 +61,10 @@ const Save = (props: Props) => {
   });
 
   const save = async () => {
-    const value = await form.submit();
+    const value: CategoryItem = await form.submit();
+    if (!!state.parentId) {
+      value.parentId = state.parentId;
+    }
     const resp = props.data.id
       ? await service.update(value as CategoryItem)
       : ((await service.save(value as any)) as Response<CategoryItem>);
@@ -76,23 +79,23 @@ const Save = (props: Props) => {
   const schema: ISchema = {
     type: 'object',
     properties: {
-      parentId: {
-        title: '上级分类',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        name: 'parentId',
-        'x-disabled': true,
-        'x-visible': !!state.parentId,
-        'x-value': state.parentId,
-      },
-      id: {
-        title: 'ID',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        required: true,
-        name: 'id',
-        'x-disabled': !!props.data.id,
-      },
+      // parentId: {
+      //   title: '上级分类',
+      //   'x-decorator': 'FormItem',
+      //   'x-component': 'Input',
+      //   name: 'parentId',
+      //   'x-disabled': true,
+      //   'x-visible': !!state.parentId,
+      //   'x-value': state.parentId,
+      // },
+      // id: {
+      //   title: 'ID',
+      //   'x-decorator': 'FormItem',
+      //   'x-component': 'Input',
+      //   required: true,
+      //   name: 'id',
+      //   'x-disabled': !!props.data.id,
+      // },
       name: {
         title: intl.formatMessage({
           id: 'pages.table.name',
@@ -103,15 +106,14 @@ const Save = (props: Props) => {
         required: true,
         name: 'name',
       },
-      key: {
+      sortIndex: {
         title: intl.formatMessage({
-          id: 'pages.device.category.key',
-          defaultMessage: '标识',
+          id: 'pages.device.category.sortIndex',
+          defaultMessage: '排序',
         }),
         'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        required: true,
-        name: 'name',
+        'x-component': 'NumberPicker',
+        name: 'sortIndex',
       },
       description: {
         type: 'string',

+ 26 - 22
src/pages/device/Category/index.tsx

@@ -1,9 +1,9 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import Service from '@/pages/device/Category/service';
 import type { ProColumns } from '@jetlinks/pro-table';
-import { EditOutlined, MinusOutlined, PlusOutlined } from '@ant-design/icons';
+import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
 import { Button, message, Popconfirm, Tooltip } from 'antd';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import type { ActionType } from '@jetlinks/pro-table';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import ProTable from '@jetlinks/pro-table';
@@ -11,6 +11,7 @@ import Save from '@/pages/device/Category/Save';
 import { model } from '@formily/reactive';
 import { observer } from '@formily/react';
 import type { Response } from '@/utils/typings';
+import SearchComponent from '@/components/SearchComponent';
 
 export const service = new Service('device/category');
 
@@ -25,35 +26,27 @@ export const state = model<{
 });
 const Category = observer(() => {
   const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
 
   const intl = useIntl();
 
   const columns: ProColumns<CategoryItem>[] = [
     {
       title: intl.formatMessage({
-        id: 'pages.device.category.id',
-        defaultMessage: '分类ID',
-      }),
-      align: 'left',
-      width: 400,
-      dataIndex: 'id',
-      sorter: true,
-    },
-    {
-      title: intl.formatMessage({
-        id: 'pages.device.category.key',
-        defaultMessage: '标识',
-      }),
-      align: 'left',
-      dataIndex: 'key',
-    },
-    {
-      title: intl.formatMessage({
         id: 'pages.device.category.name',
         defaultMessage: '分类名称',
       }),
       dataIndex: 'name',
+    },
+    {
+      title: '分类排序',
+      dataIndex: 'sortIndex',
       align: 'center',
+      // render: (text) => (
+      //   <Space>{text}<EditOutlined onClick={() => {
+
+      //   }} /></Space>
+      // )
     },
     {
       title: intl.formatMessage({
@@ -74,6 +67,7 @@ const Category = observer(() => {
       align: 'center',
       render: (text, record) => [
         <a
+          key={'edit'}
           onClick={() => {
             state.visible = true;
             state.current = record;
@@ -89,6 +83,7 @@ const Category = observer(() => {
           </Tooltip>
         </a>,
         <a
+          key={'add-next'}
           onClick={() => {
             state.visible = true;
             state.parentId = record.id;
@@ -104,6 +99,7 @@ const Category = observer(() => {
           </Tooltip>
         </a>,
         <Popconfirm
+          key={'delete'}
           onConfirm={async () => {
             const resp = (await service.remove(record.id)) as Response<any>;
             if (resp.status === 200) {
@@ -122,7 +118,7 @@ const Category = observer(() => {
                 defaultMessage: '删除',
               })}
             >
-              <MinusOutlined />
+              <DeleteOutlined />
             </Tooltip>
           </a>
         </Popconfirm>,
@@ -132,9 +128,17 @@ const Category = observer(() => {
 
   return (
     <PageContainer>
+      <SearchComponent
+        field={columns}
+        onSearch={(data) => {
+          setParam(data);
+        }}
+        target="category"
+      />
       <ProTable
+        params={param}
+        search={false}
         request={async (params) => {
-          delete params.pageIndex;
           const response = await service.queryTree({ paging: false, ...params });
           return {
             code: response.message,

+ 1 - 1
src/pages/device/Category/service.ts

@@ -3,7 +3,7 @@ import { request } from '@@/plugin-request/request';
 
 class Service extends BaseService<CategoryItem> {
   queryTree = (params?: Record<string, any>) =>
-    request(`${this.uri}/_tree`, { params, method: 'GET' });
+    request(`${this.uri}/_tree`, { data: params, method: 'POST' });
 }
 
 export default Service;

+ 134 - 27
src/pages/device/Instance/Detail/MetadataLog/Property/index.tsx

@@ -1,10 +1,10 @@
-import ProTable from '@jetlinks/pro-table';
 import { service } from '@/pages/device/Instance';
 import { useParams } from 'umi';
-import { Drawer } from 'antd';
-import encodeQuery from '@/utils/encodeQuery';
+import { DatePicker, Modal, Radio, Space, Table } from 'antd';
 import type { PropertyMetadata } from '@/pages/device/Product/typings';
-import columns from '@/pages/device/Instance/Detail/MetadataLog/columns';
+import encodeQuery from '@/utils/encodeQuery';
+import { useEffect, useState } from 'react';
+import moment from 'moment';
 
 interface Props {
   visible: boolean;
@@ -15,35 +15,142 @@ interface Props {
 const PropertyLog = (props: Props) => {
   const params = useParams<{ id: string }>();
   const { visible, close, data } = props;
+  const [dataSource, setDataSource] = useState<any>({});
+  const [start, setStart] = useState<number>(moment().startOf('day').valueOf());
+  const [end, setEnd] = useState<number>(new Date().getTime());
+  const [radioValue, setRadioValue] = useState<undefined | 'today' | 'week' | 'month'>('today');
+  const [dateValue, setDateValue] = useState<any>(undefined);
+
+  const columns = [
+    {
+      title: '时间',
+      dataIndex: 'timestamp',
+      key: 'timestamp',
+      render: (text: any) => <span>{text ? moment(text).format('YYYY-MM-DD HH:mm:ss') : ''}</span>,
+    },
+    {
+      title: '自定义属性',
+      dataIndex: 'formatValue',
+      key: 'formatValue',
+    },
+  ];
+
+  const handleSearch = (param: any, startTime?: number, endTime?: number) => {
+    service
+      .getPropertyData(
+        params.id,
+        encodeQuery({
+          ...param,
+          terms: {
+            property: data.id,
+            timestamp$BTW: startTime && endTime ? [startTime, endTime] : [],
+          },
+          sorts: { timestamp: 'desc' },
+        }),
+      )
+      .then((resp) => {
+        if (resp.status === 200) {
+          setDataSource(resp.result);
+        }
+      });
+  };
+
+  useEffect(() => {
+    if (visible) {
+      handleSearch(
+        {
+          pageSize: 10,
+          pageIndex: 0,
+        },
+        start,
+        end,
+      );
+    }
+  }, [visible]);
 
   return (
-    <Drawer title={data.name} visible={visible} onClose={() => close()} width="45vw">
-      <ProTable
+    <Modal title="详情" visible={visible} onCancel={() => close()} width="45vw">
+      <div style={{ marginBottom: '20px' }}>
+        <Space>
+          <Radio.Group
+            value={radioValue}
+            buttonStyle="solid"
+            onChange={(e) => {
+              const value = e.target.value;
+              setRadioValue(value);
+              let st: number = 0;
+              const et = new Date().getTime();
+              if (value === 'today') {
+                st = moment().startOf('day').valueOf();
+              } else if (value === 'week') {
+                st = moment().subtract(6, 'days').valueOf();
+              } else if (value === 'month') {
+                st = moment().subtract(29, 'days').valueOf();
+              }
+              setDateValue(undefined);
+              setStart(st);
+              setEnd(et);
+              handleSearch(
+                {
+                  pageSize: 10,
+                  pageIndex: 0,
+                },
+                st,
+                et,
+              );
+            }}
+          >
+            <Radio.Button value="today">今日</Radio.Button>
+            <Radio.Button value="week">近一周</Radio.Button>
+            <Radio.Button value="month">近一月</Radio.Button>
+          </Radio.Group>
+          <DatePicker.RangePicker
+            value={dateValue}
+            showTime
+            onChange={(dates: any) => {
+              if (dates) {
+                setRadioValue(undefined);
+                setDateValue(dates);
+                const st = dates[0]?.valueOf();
+                const et = dates[1]?.valueOf();
+                setStart(st);
+                setEnd(et);
+                handleSearch(
+                  {
+                    pageSize: 10,
+                    pageIndex: 0,
+                  },
+                  st,
+                  et,
+                );
+              }
+            }}
+          />
+        </Space>
+      </div>
+
+      <Table
         size="small"
-        toolBarRender={false}
-        request={async (param) =>
-          service.getPropertyData(
-            params.id,
-            encodeQuery({
-              ...param,
-              terms: { property: data.id },
-              sorts: { timestamp: 'desc' },
-            }),
-          )
-        }
+        rowKey={'id'}
+        onChange={(page) => {
+          handleSearch(
+            {
+              pageSize: page.pageSize,
+              pageIndex: Number(page.current) - 1 || 0,
+            },
+            start,
+            end,
+          );
+        }}
+        dataSource={dataSource?.data || []}
+        columns={columns}
         pagination={{
-          pageSize: 15,
+          pageSize: dataSource?.pageSize || 10,
+          showSizeChanger: true,
+          total: dataSource?.total || 0,
         }}
-        columns={[
-          ...columns,
-          {
-            dataIndex: 'formatValue',
-            title: '数据',
-            // copyable: true,
-          },
-        ]}
       />
-    </Drawer>
+    </Modal>
   );
 };
 export default PropertyLog;

+ 70 - 0
src/pages/device/Instance/Detail/Running/Property/EditProperty.tsx

@@ -0,0 +1,70 @@
+import { Alert, message, Modal } from 'antd';
+import { Input, FormItem } from '@formily/antd';
+import { createForm } from '@formily/core';
+import { FormProvider, createSchemaField } from '@formily/react';
+import { service } from '@/pages/device/Instance';
+import { useParams } from 'umi';
+import type { PropertyMetadata } from '@/pages/device/Product/typings';
+
+interface Props {
+  visible: boolean;
+  data: Partial<PropertyMetadata>;
+  onCancel: () => void;
+}
+const EditProperty = (props: Props) => {
+  const { visible, data } = props;
+  const params = useParams<{ id: string }>();
+
+  const SchemaField = createSchemaField({
+    components: {
+      Input,
+      FormItem,
+    },
+  });
+
+  const form = createForm();
+  const schema = {
+    type: 'object',
+    properties: {
+      propertyValue: {
+        type: 'string',
+        title: '自定义属性',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+      },
+    },
+  };
+
+  const handleSetPropertyValue = async (propertyValue: string) => {
+    const resp = await service.setProperty(params.id, { [`${data.id}`]: propertyValue });
+    if (resp.status === 200) {
+      message.success('操作成功');
+    }
+    props.onCancel();
+  };
+  return (
+    <Modal
+      title="编辑"
+      visible={visible}
+      onOk={async () => {
+        const values: any = await form.submit();
+        if (!!values) {
+          handleSetPropertyValue(values?.propertyValue);
+        }
+      }}
+      onCancel={() => {
+        props.onCancel();
+      }}
+    >
+      <Alert message="当数据来源为设备时,填写的值将下发到设备" type="warning" showIcon />
+      <div style={{ marginTop: '30px' }}>
+        <FormProvider form={form}>
+          <SchemaField schema={schema} />
+        </FormProvider>
+      </div>
+    </Modal>
+  );
+};
+
+export default EditProperty;

src/pages/device/Instance/Detail/Running/Property.tsx → src/pages/device/Instance/Detail/Running/Property/PropertyCard copy.tsx


+ 85 - 0
src/pages/device/Instance/Detail/Running/Property/PropertyCard.tsx

@@ -0,0 +1,85 @@
+import { EditOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
+import { Divider, message, Spin, Tooltip } from 'antd';
+import ProCard from '@ant-design/pro-card';
+import type { PropertyMetadata } from '@/pages/device/Product/typings';
+import { useState } from 'react';
+import { service } from '@/pages/device/Instance';
+import { useParams } from 'umi';
+import PropertyLog from '@/pages/device/Instance/Detail/MetadataLog/Property';
+import EditProperty from '@/pages/device/Instance/Detail/Running/Property/EditProperty';
+
+interface Props {
+  data: Partial<PropertyMetadata>;
+  value: any;
+}
+
+const Property = (props: Props) => {
+  const { data, value } = props;
+
+  const params = useParams<{ id: string }>();
+
+  const [loading, setLoading] = useState<boolean>(false);
+  const refreshProperty = async () => {
+    setLoading(true);
+    if (!data.id) return;
+    const resp = await service.getProperty(params.id, data.id);
+    setLoading(false);
+    if (resp.status === 200) {
+      message.success('操作成功');
+    }
+  };
+
+  const [visible, setVisible] = useState<boolean>(false);
+  const [editVisible, setEditVisible] = useState<boolean>(false);
+
+  return (
+    <ProCard
+      title={`${data?.name}`}
+      extra={
+        <>
+          {(data.expands?.readOnly === false || data.expands?.readOnly === 'false') && (
+            <>
+              <Tooltip placement="top" title="设置属性至设备">
+                <EditOutlined
+                  onClick={() => {
+                    setEditVisible(true);
+                  }}
+                />
+              </Tooltip>
+              <Divider type="vertical" />
+            </>
+          )}
+          <Tooltip placement="top" title="获取最新属性值">
+            <SyncOutlined onClick={refreshProperty} />
+          </Tooltip>
+          <Divider type="vertical" />
+          <Tooltip placement="top" title="详情">
+            <UnorderedListOutlined
+              onClick={() => {
+                setVisible(true);
+              }}
+            />
+          </Tooltip>
+        </>
+      }
+      bordered
+      hoverable
+      colSpan={{ xs: 12, sm: 8, md: 6, lg: 6, xl: 6 }}
+    >
+      <Spin spinning={loading}>
+        <div style={{ height: 60, fontWeight: 600, fontSize: '30px' }}>
+          {value?.formatValue || ''}
+        </div>
+      </Spin>
+      <EditProperty
+        visible={editVisible}
+        onCancel={() => {
+          setEditVisible(false);
+        }}
+        data={data}
+      />
+      <PropertyLog data={data} visible={visible} close={() => setVisible(false)} />
+    </ProCard>
+  );
+};
+export default Property;

+ 7 - 0
src/pages/device/Instance/Detail/Running/Property/index.less

@@ -0,0 +1,7 @@
+.page {
+  :global {
+    .ant-pagination-item {
+      display: none;
+    }
+  }
+}

+ 244 - 0
src/pages/device/Instance/Detail/Running/Property/index.tsx

@@ -0,0 +1,244 @@
+import { Col, Input, message, Pagination, Row, Space, Table } from 'antd';
+import CheckButton from '@/components/CheckButton';
+import { useEffect, useState } from 'react';
+import type { PropertyMetadata } from '@/pages/device/Product/typings';
+import PropertyCard from './PropertyCard';
+import { EditOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
+import { InstanceModel, service } from '@/pages/device/Instance';
+import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+import { map } from 'rxjs/operators';
+import EditProperty from './EditProperty';
+import { useParams } from 'umi';
+import PropertyLog from '../../MetadataLog/Property';
+import moment from 'moment';
+import styles from './index.less';
+
+interface Props {
+  data: Partial<PropertyMetadata>[];
+}
+
+const ColResponsiveProps = {
+  xs: 24,
+  sm: 12,
+  md: 12,
+  lg: 12,
+  xl: 6,
+  style: { marginBottom: 24 },
+};
+
+const Property = (props: Props) => {
+  const { data } = props;
+  const device = InstanceModel.detail;
+  const params = useParams<{ id: string }>();
+  const [subscribeTopic] = useSendWebsocketMessage();
+  const [visible, setVisible] = useState<boolean>(false);
+  const [infoVisible, setInfoVisible] = useState<boolean>(false);
+  const [currentInfo, setCurrentInfo] = useState<any>({});
+  const [propertyValue, setPropertyValue] = useState<any>({});
+  const [propertyList, setPropertyList] = useState<any[]>(data || []);
+  const [dataSource, setDataSource] = useState<any>({
+    total: data.length,
+    data: (data || []).slice(0, 8),
+    pageSize: 8,
+    currentPage: 0,
+  });
+
+  const [check, setCheck] = useState<boolean>(true);
+
+  const refreshProperty = async (id: string) => {
+    if (!id) return;
+    const resp = await service.getProperty(params.id, id);
+    if (resp.status === 200) {
+      message.success('操作成功');
+    }
+  };
+
+  const columns = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '值',
+      dataIndex: 'value',
+      key: 'value',
+      render: (text: any, record: any) => (
+        <span>{propertyValue[record.id]?.formatValue || '--'}</span>
+      ),
+    },
+    {
+      title: '更新时间',
+      dataIndex: 'time',
+      key: 'time',
+      render: (text: any, record: any) => (
+        <span>{moment(propertyValue[record.id]?.timestamp).format('YYYY-MM-DD HH:mm:ss')}</span>
+      ),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (text: any, record: any) => (
+        <Space size="middle" style={{ color: '#1d39c4' }}>
+          {(record.expands?.readOnly === false || record.expands?.readOnly === 'false') && (
+            <EditOutlined
+              onClick={() => {
+                setVisible(true);
+              }}
+            />
+          )}
+          <SyncOutlined
+            onClick={() => {
+              refreshProperty(record?.id);
+            }}
+          />
+          <UnorderedListOutlined
+            onClick={() => {
+              setCurrentInfo(record);
+              setInfoVisible(true);
+            }}
+          />
+        </Space>
+      ),
+    },
+  ];
+
+  /**
+   * 订阅属性数据
+   */
+  const subscribeProperty = () => {
+    const id = `instance-info-property-${device.id}-${device.productId}-${dataSource.data
+      .map((i: PropertyMetadata) => i.id)
+      .join('-')}`;
+    const topic = `/dashboard/device/${device.productId}/properties/realTime`;
+    subscribeTopic!(id, topic, {
+      deviceId: device.id,
+      properties: dataSource.data.map((i: PropertyMetadata) => i.id),
+      history: 1,
+    })
+      ?.pipe(map((res) => res.payload))
+      .subscribe((payload: any) => {
+        const { value } = payload;
+        propertyValue[value.property] = value;
+        setPropertyValue({ ...propertyValue });
+      });
+  };
+
+  const getDashboard = () => {
+    const param = [
+      {
+        dashboard: 'device',
+        object: device.productId,
+        measurement: 'properties',
+        dimension: 'history',
+        params: {
+          deviceId: device.id,
+          history: 1,
+          properties: dataSource.data.map((i: PropertyMetadata) => i.id),
+        },
+      },
+    ];
+
+    service.propertyRealTime(param).subscribe({
+      next: (resp) => {
+        propertyValue[resp.property] = resp.list[0];
+        setPropertyValue({ ...propertyValue });
+      },
+    });
+  };
+
+  useEffect(() => {
+    if (dataSource.data.length > 0) {
+      getDashboard();
+      subscribeProperty();
+    }
+  }, [dataSource]);
+
+  return (
+    <div>
+      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+        <Space>
+          <Input.Search
+            allowClear
+            placeholder="请输入名称"
+            onSearch={(value: string) => {
+              if (!!value) {
+                const list = data.filter((item) => {
+                  return (
+                    item.name && item.name.toLowerCase().indexOf(value.toLocaleLowerCase()) !== -1
+                  );
+                });
+                setPropertyList(list);
+                setDataSource({
+                  total: list.length,
+                  data: (list || []).slice(0, 8),
+                  pageSize: 8,
+                  currentPage: 0,
+                });
+              } else {
+                setPropertyList(data);
+                setDataSource({
+                  total: data.length,
+                  data: (data || []).slice(0, 8),
+                  pageSize: 8,
+                  currentPage: 0,
+                });
+              }
+            }}
+            style={{ width: 300 }}
+          />
+          {/* <Checkbox onChange={() => {
+
+                    }}>仅显示当前有数据的属性</Checkbox> */}
+        </Space>
+        <CheckButton
+          value={check}
+          change={(value: boolean) => {
+            setCheck(value);
+          }}
+        />
+      </div>
+      <div style={{ marginTop: '20px' }}>
+        {check ? (
+          <Row gutter={[16, 16]}>
+            {dataSource.data.map((item: any) => (
+              <Col {...ColResponsiveProps} key={item.id}>
+                <PropertyCard data={item} value={item?.id ? propertyValue[item?.id] : '--'} />
+              </Col>
+            ))}
+          </Row>
+        ) : (
+          <Table pagination={false} columns={columns} dataSource={dataSource.data} rowKey="id" />
+        )}
+        <div
+          style={{ marginTop: '20px', width: '100%', display: 'flex', justifyContent: 'flex-end' }}
+        >
+          <Pagination
+            className={styles.page}
+            defaultCurrent={1}
+            total={dataSource.total}
+            showSizeChanger
+            pageSize={dataSource.pageSize}
+            onChange={(page: number, size: number) => {
+              setDataSource({
+                total: propertyList.length,
+                data: (propertyList || []).slice((page - 1) * size, page * size),
+                pageSize: size,
+                currentPage: page - 1,
+              });
+            }}
+          />
+        </div>
+      </div>
+      <EditProperty
+        data={currentInfo}
+        visible={visible}
+        onCancel={() => {
+          setVisible(false);
+        }}
+      />
+      <PropertyLog data={currentInfo} visible={infoVisible} close={() => setInfoVisible(false)} />
+    </div>
+  );
+};
+export default Property;

+ 164 - 159
src/pages/device/Instance/Detail/Running/index.tsx

@@ -1,182 +1,187 @@
-import { InstanceModel, service } from '@/pages/device/Instance';
-import { Card, Col, Row } from 'antd';
-import type {
-  DeviceMetadata,
-  EventMetadata,
-  ObserverMetadata,
-  PropertyMetadata,
-} from '@/pages/device/Product/typings';
-import { useIntl } from '@@/plugin-locale/localeExports';
-import { useCallback, useEffect, useState } from 'react';
+import { InstanceModel } from '@/pages/device/Instance';
+import { Card, Tabs } from 'antd';
+import type { DeviceMetadata } from '@/pages/device/Product/typings';
+// import { useIntl } from '@@/plugin-locale/localeExports';
+import { useEffect } from 'react';
 import Property from '@/pages/device/Instance/Detail/Running/Property';
-import Event from '@/pages/device/Instance/Detail/Running/Event';
-import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
-import { map } from 'rxjs/operators';
-import moment from 'moment';
-import { deviceStatus } from '@/pages/device/Instance/Detail';
+// import Event from '@/pages/device/Instance/Detail/Running/Event';
+// import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+// import { map } from 'rxjs/operators';
+// import moment from 'moment';
+// import { deviceStatus } from '@/pages/device/Instance/Detail';
 
-const ColResponsiveProps = {
-  xs: 24,
-  sm: 12,
-  md: 12,
-  lg: 12,
-  xl: 6,
-  style: { marginBottom: 24 },
-};
+// const ColResponsiveProps = {
+//   xs: 24,
+//   sm: 12,
+//   md: 12,
+//   lg: 12,
+//   xl: 6,
+//   style: { marginBottom: 24 },
+// };
 
 const Running = () => {
-  const intl = useIntl();
+  // const intl = useIntl();
   const metadata = JSON.parse(InstanceModel.detail.metadata as string) as DeviceMetadata;
 
-  const device = InstanceModel.detail;
-  const [subscribeTopic] = useSendWebsocketMessage();
+  // const device = InstanceModel.detail;
+  // const [subscribeTopic] = useSendWebsocketMessage();
 
-  const addObserver = (item: Record<string, any>) => {
-    item.listener = [];
-    item.subscribe = (callback: () => void) => {
-      item.listener.push(callback);
-    };
-    item.next = (data: any) => {
-      item.listener.forEach((element: any) => {
-        element(data);
-      });
-    };
-    return item;
-  };
-  metadata.events = metadata.events.map(addObserver);
-  metadata.properties = metadata.properties.map(addObserver);
-  const [propertiesList, setPropertiesList] = useState<string[]>(
-    metadata.properties.map((item: any) => item.id),
-  );
+  // const addObserver = (item: Record<string, any>) => {
+  //   item.listener = [];
+  //   item.subscribe = (callback: () => void) => {
+  //     item.listener.push(callback);
+  //   };
+  //   item.next = (data: any) => {
+  //     item.listener.forEach((element: any) => {
+  //       element(data);
+  //     });
+  //   };
+  //   return item;
+  // };
+  // metadata.events = metadata.events.map(addObserver);
+  // metadata.properties = metadata.properties.map(addObserver);
+  // const [propertiesList, setPropertiesList] = useState<string[]>(
+  //   metadata.properties.map((item: any) => item.id),
+  // );
 
   /**
    * 订阅属性数据
    */
-  const subscribeProperty = () => {
-    const id = `instance-info-property-${device.id}-${device.productId}-${propertiesList.join(
-      '-',
-    )}`;
-    const topic = `/dashboard/device/${device.productId}/properties/realTime`;
-    subscribeTopic!(id, topic, {
-      deviceId: device.id,
-      properties: propertiesList,
-      history: 0,
-    })
-      ?.pipe(map((res) => res.payload))
-      .subscribe((payload: any) => {
-        const property = metadata.properties.find(
-          (i) => i.id === payload.value.property,
-        ) as PropertyMetadata & ObserverMetadata;
-        if (property) {
-          property.next(payload);
-        }
-      });
-  };
+  // const subscribeProperty = () => {
+  //   const id = `instance-info-property-${device.id}-${device.productId}-${propertiesList.join(
+  //     '-',
+  //   )}`;
+  //   const topic = `/dashboard/device/${device.productId}/properties/realTime`;
+  //   subscribeTopic!(id, topic, {
+  //     deviceId: device.id,
+  //     properties: propertiesList,
+  //     history: 0,
+  //   })
+  //     ?.pipe(map((res) => res.payload))
+  //     .subscribe((payload: any) => {
+  //       const property = metadata.properties.find(
+  //         (i) => i.id === payload.value.property,
+  //       ) as PropertyMetadata & ObserverMetadata;
+  //       if (property) {
+  //         property.next(payload);
+  //       }
+  //     });
+  // };
 
-  const getDashboard = () => {
-    const params = [
-      {
-        dashboard: 'device',
-        object: device.productId,
-        measurement: 'properties',
-        dimension: 'history',
-        params: {
-          deviceId: device.id,
-          history: 15,
-          properties: propertiesList,
-        },
-      },
-    ];
+  // const getDashboard = () => {
+  //   const params = [
+  //     {
+  //       dashboard: 'device',
+  //       object: device.productId,
+  //       measurement: 'properties',
+  //       dimension: 'history',
+  //       params: {
+  //         deviceId: device.id,
+  //         history: 15,
+  //         properties: propertiesList,
+  //       },
+  //     },
+  //   ];
 
-    service.propertyRealTime(params).subscribe({
-      next: (data) => {
-        const index = metadata.properties.findIndex((i) => i.id === data.property);
-        if (index > -1) {
-          const property = metadata.properties[index] as PropertyMetadata & ObserverMetadata;
-          property.list = data.list as Record<string, unknown>[];
-          property.next(data.list);
-        }
-      },
-    });
-  };
+  //   service.propertyRealTime(params).subscribe({
+  //     next: (data) => {
+  //       const index = metadata.properties.findIndex((i) => i.id === data.property);
+  //       if (index > -1) {
+  //         const property = metadata.properties[index] as PropertyMetadata & ObserverMetadata;
+  //         property.list = data.list as Record<string, unknown>[];
+  //         property.next(data.list);
+  //       }
+  //     },
+  //   });
+  // };
 
-  /**
-   * 订阅事件数据
-   */
-  const subscribeEvent = () => {
-    const id = `instance-info-event-${device.id}-${device.productId}`;
-    const topic = `/dashboard/device/${device.productId}/events/realTime`;
-    subscribeTopic!(id, topic, { deviceId: device.id })
-      ?.pipe(map((res) => res.payload))
-      .subscribe((payload: any) => {
-        const event = metadata.events.find((i) => i.id === payload.value.event) as EventMetadata &
-          ObserverMetadata;
-        if (event) {
-          event.next(payload);
-        }
-      });
-  };
+  // /**
+  //  * 订阅事件数据
+  //  */
+  // const subscribeEvent = () => {
+  //   const id = `instance-info-event-${device.id}-${device.productId}`;
+  //   const topic = `/dashboard/device/${device.productId}/events/realTime`;
+  //   subscribeTopic!(id, topic, { deviceId: device.id })
+  //     ?.pipe(map((res) => res.payload))
+  //     .subscribe((payload: any) => {
+  //       const event = metadata.events.find((i) => i.id === payload.value.event) as EventMetadata &
+  //         ObserverMetadata;
+  //       if (event) {
+  //         event.next(payload);
+  //       }
+  //     });
+  // };
   useEffect(() => {
-    subscribeProperty();
-    subscribeEvent();
-    getDashboard();
+    // subscribeProperty();
+    // subscribeEvent();
+    // getDashboard();
   }, []);
 
-  const [renderCount, setRenderCount] = useState<number>(15);
-  window.onscroll = () => {
-    const a = document.documentElement.scrollTop;
-    const c = document.documentElement.scrollHeight;
-    const b = document.body.clientHeight;
-    if (a + b >= c - 50) {
-      const list: any = [];
-      metadata.properties.slice(renderCount, renderCount + 15).map((item) => {
-        list.push(item.id);
-      });
-      setPropertiesList([...list]);
-      setRenderCount(renderCount + 15);
-    }
-  };
+  // const [renderCount, setRenderCount] = useState<number>(15);
+  // window.onscroll = () => {
+  //   const a = document.documentElement.scrollTop;
+  //   const c = document.documentElement.scrollHeight;
+  //   const b = document.body.clientHeight;
+  //   if (a + b >= c - 50) {
+  //     const list: any = [];
+  //     metadata.properties.slice(renderCount, renderCount + 15).map((item) => {
+  //       list.push(item.id);
+  //     });
+  //     setPropertiesList([...list]);
+  //     setRenderCount(renderCount + 15);
+  //   }
+  // };
 
-  const renderCard = useCallback(() => {
-    return [
-      ...metadata.properties.map((item) => (
-        <Col {...ColResponsiveProps} key={item.id}>
-          <Property data={item as Partial<PropertyMetadata> & ObserverMetadata} />
-        </Col>
-      )),
-      ...metadata.events.map((item) => (
-        <Col {...ColResponsiveProps} key={item.id}>
-          <Event data={item as Partial<EventMetadata> & ObserverMetadata} />
-        </Col>
-      )),
-    ].splice(0, renderCount);
-  }, [device, renderCount]);
+  // const renderCard = useCallback(() => {
+  //   return [
+  //     ...metadata.properties.map((item) => (
+  //       <Col {...ColResponsiveProps} key={item.id}>
+  //         <Property data={item as Partial<PropertyMetadata> & ObserverMetadata} />
+  //       </Col>
+  //     )),
+  //     ...metadata.events.map((item) => (
+  //       <Col {...ColResponsiveProps} key={item.id}>
+  //         <Event data={item as Partial<EventMetadata> & ObserverMetadata} />
+  //       </Col>
+  //     )),
+  //   ].splice(0, renderCount);
+  // }, [device, renderCount]);
 
   return (
-    <Row gutter={24}>
-      <Col {...ColResponsiveProps}>
-        <Card
-          title={intl.formatMessage({
-            id: 'pages.device.instanceDetail.running.status',
-            defaultMessage: '设备状态',
-          })}
-        >
-          <div style={{ height: 60 }}>
-            <Row gutter={[16, 16]}>
-              <Col span={24}>{deviceStatus.get(InstanceModel.detail.state?.value)}</Col>
-              <Col span={24}>
-                {device.state?.value === 'online' ? (
-                  <span>上线时间:{moment(device?.onlineTime).format('YYYY-MM-DD HH:mm:ss')}</span>
-                ) : (
-                  <span>离线时间:{moment(device?.offlineTime).format('YYYY-MM-DD HH:mm:ss')}</span>
-                )}
-              </Col>
-            </Row>
-          </div>
-        </Card>
-      </Col>
-      {renderCard()}
-    </Row>
+    // <Row gutter={24}>
+    //   <Col {...ColResponsiveProps}>
+    //     <Card
+    //       title={intl.formatMessage({
+    //         id: 'pages.device.instanceDetail.running.status',
+    //         defaultMessage: '设备状态',
+    //       })}
+    //     >
+    //       <div style={{ height: 60 }}>
+    //         <Row gutter={[16, 16]}>
+    //           <Col span={24}>{deviceStatus.get(InstanceModel.detail.state?.value)}</Col>
+    //           <Col span={24}>
+    //             {device.state?.value === 'online' ? (
+    //               <span>上线时间:{moment(device?.onlineTime).format('YYYY-MM-DD HH:mm:ss')}</span>
+    //             ) : (
+    //               <span>离线时间:{moment(device?.offlineTime).format('YYYY-MM-DD HH:mm:ss')}</span>
+    //             )}
+    //           </Col>
+    //         </Row>
+    //       </div>
+    //     </Card>
+    //   </Col>
+    //   {renderCard()}
+    // </Row>
+    <Card>
+      <Tabs defaultActiveKey="1" tabPosition="left">
+        <Tabs.TabPane tab="属性" key="1">
+          <Property data={metadata?.properties || {}} />
+        </Tabs.TabPane>
+        <Tabs.TabPane tab="事件1" key="2">
+          Content of Tab Pane 2
+        </Tabs.TabPane>
+      </Tabs>
+    </Card>
   );
 };
 export default Running;

+ 44 - 8
src/pages/device/Product/Detail/index.tsx

@@ -1,6 +1,6 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { history, useParams } from 'umi';
-import { Button, Card, Descriptions, Space, Tabs, Badge, message, Spin } from 'antd';
+import { Button, Card, Descriptions, Space, Tabs, Badge, message, Spin, Tooltip } from 'antd';
 import BaseInfo from '@/pages/device/Product/Detail/BaseInfo';
 import { observer } from '@formily/react';
 import { productModel, service } from '@/pages/device/Product';
@@ -12,6 +12,7 @@ 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';
 
 const ProductDetail = observer(() => {
   const intl = useIntl();
@@ -52,9 +53,11 @@ const ProductDetail = observer(() => {
     if (!productModel.current) {
       history.goBack();
     } else {
-      service.getProductDetail(param.id).subscribe((data) => {
-        const metadata: DeviceMetadata = JSON.parse(data.metadata);
-        MetadataAction.insert(metadata);
+      service.getProductDetail(param?.id).subscribe((data) => {
+        if (data.metadata) {
+          const metadata: DeviceMetadata = JSON.parse(data.metadata);
+          MetadataAction.insert(metadata);
+        }
       });
     }
 
@@ -193,10 +196,43 @@ const ProductDetail = observer(() => {
             <BaseInfo />
           </Tabs.TabPane>
           <Tabs.TabPane
-            tab={intl.formatMessage({
-              id: 'pages.device.productDetail.metadata',
-              defaultMessage: '物模型',
-            })}
+            tab={
+              <Tooltip
+                placement="right"
+                title={
+                  <div>
+                    <p>
+                      属性:
+                      <br />
+                      用于描述设备运行时具体信息和状态。
+                      例如,环境监测设备所读取的当前环境温度、智能灯开关状态、电风扇风力等级等。
+                    </p>
+                    功能:
+                    <br />
+                    <p>
+                      指设备可供外部调用的指令或方法。功能调用中可设置输入和输出参数。输入参数是服务执行时的参数,输出参数是服务执行后的结果。
+                      相比于属性,功能可通过一条指令实现更复杂的业务逻辑,例如执行某项特定的任务。
+                      功能分为异步和同步两种调用方式。
+                    </p>
+                    <p> 事件:</p>
+                    <p>
+                      设备运行时,主动上报给云端的信息,一般包含需要被外部感知和处理的信息、告警和故障。事件中可包含多个输出参数。
+                      例如,某项任务完成后的通知信息;设备发生故障时的温度、时间信息;设备告警时的运行状态等。
+                    </p>
+                    <p> 标签:</p>
+                    <p>
+                      统一为设备添加拓展字段,添加后将在设备信息页显示。可用于对设备基本信息描述的补充。
+                    </p>
+                  </div>
+                }
+              >
+                {intl.formatMessage({
+                  id: 'pages.device.productDetail.metadata',
+                  defaultMessage: '物模型',
+                })}
+                <QuestionCircleOutlined />
+              </Tooltip>
+            }
             key="metadata"
           >
             <Metadata type="product" />

+ 0 - 6
src/pages/device/Product/index.tsx

@@ -21,9 +21,6 @@ import ProTable from '@jetlinks/pro-table';
 import { lastValueFrom } from 'rxjs';
 import encodeQuery from '@/utils/encodeQuery';
 import Save from '@/pages/device/Product/Save';
-import Edit from '@/pages/device/components/Metadata/Base/Edit';
-// import Operator from '@/pages/device/components/Operator';
-// import Debug from '@/pages/device/components/Debug';
 
 export const service = new Service('device-product');
 export const statusMap = {
@@ -209,9 +206,6 @@ const Product = observer(() => {
         }}
         visible={visible}
       />
-      <Edit type={'product'} />
-      {/*<Operator />*/}
-      {/*<Debug />*/}
     </PageContainer>
   );
 });

+ 64 - 52
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -42,7 +42,7 @@ import DB from '@/db';
 import _ from 'lodash';
 import { useParams } from 'umi';
 import { InstanceModel } from '@/pages/device/Instance';
-// import FRuleEditor from '@/components/FRuleEditor';
+import FRuleEditor from '@/components/FRuleEditor';
 
 interface Props {
   type: 'product' | 'device';
@@ -97,7 +97,7 @@ const Edit = (props: Props) => {
       EnumParam,
       BooleanEnum,
       ConfigParam,
-      // FRuleEditor,
+      FRuleEditor,
     },
     scope: {
       async asyncOtherConfig(field: Field) {
@@ -361,29 +361,36 @@ const Edit = (props: Props) => {
           rule: {
             type: 'string',
             'x-component': 'FRuleEditor',
+            'x-visible': false,
+            'x-reactions': {
+              dependencies: ['.source'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="rule"}}',
+                },
+              },
+            },
           },
-          readOnly: {
-            title: intl.formatMessage({
-              id: 'pages.device.productDetail.metadata.whetherReadOnly',
-              defaultMessage: '是否只读',
-            }),
+          type: {
+            title: '属性类型',
             required: true,
             'x-decorator': 'FormItem',
-            'x-component': 'Radio.Group',
+            'x-component': 'Select',
+            'x-component-props': {
+              mode: 'tags',
+            },
             enum: [
               {
-                label: intl.formatMessage({
-                  id: 'pages.device.productDetail.metadata.true',
-                  defaultMessage: '是',
-                }),
-                value: true,
+                label: '读',
+                value: 'read',
               },
               {
-                label: intl.formatMessage({
-                  id: 'pages.device.productDetail.metadata.false',
-                  defaultMessage: '否',
-                }),
-                value: false,
+                label: '写',
+                value: 'write',
+              },
+              {
+                label: '上报',
+                value: 'report',
               },
             ],
           },
@@ -529,7 +536,10 @@ const Edit = (props: Props) => {
     const params = (await form.submit()) as MetadataItem;
 
     if (!typeMap.get(props.type)) return;
+
+    console.log(props.type, typeMap.get(props.type).metadata, 'JSON-PARSE');
     const metadata = JSON.parse(typeMap.get(props.type).metadata) as DeviceMetadata;
+    console.log(metadata, 'metadata');
     const config = metadata[type] as MetadataItem[];
     const index = config.findIndex((item) => item.id === params.id);
     if (index > -1) {
@@ -570,40 +580,42 @@ const Edit = (props: Props) => {
     </Menu>
   );
   return (
-    <Drawer
-      width="25vw"
-      visible
-      title={`${intl.formatMessage({
-        id: `pages.data.option.${MetadataModel.action}`,
-        defaultMessage: '新增',
-      })}-${intl.formatMessage({
-        id: `pages.device.metadata.${MetadataModel.type}`,
-        defaultMessage: metadataTypeMapping[MetadataModel.type].name,
-      })}`}
-      onClose={() => {
-        MetadataModel.edit = false;
-        MetadataModel.item = {};
-      }}
-      destroyOnClose
-      zIndex={1000}
-      placement={'right'}
-      extra={
-        props.type === 'product' ? (
-          <Dropdown.Button type="primary" onClick={() => saveMetadata()} overlay={menu}>
-            保存数据
-          </Dropdown.Button>
-        ) : (
-          <Button type="primary" onClick={() => saveMetadata()}>
-            保存数据
-          </Button>
-        )
-      }
-    >
-      <Form form={form} layout="vertical" size="small">
-        <SchemaField schema={metadataTypeMapping.properties.schema} />
-        {/*<SchemaField schema={metadataTypeMapping[MetadataModel.type].schema} />*/}
-      </Form>
-    </Drawer>
+    <>
+      <Drawer
+        width="25vw"
+        visible
+        title={`${intl.formatMessage({
+          id: `pages.data.option.${MetadataModel.action}`,
+          defaultMessage: '新增',
+        })}-${intl.formatMessage({
+          id: `pages.device.metadata.${MetadataModel.type}`,
+          defaultMessage: metadataTypeMapping[MetadataModel.type].name,
+        })}`}
+        onClose={() => {
+          MetadataModel.edit = false;
+          MetadataModel.item = {};
+        }}
+        destroyOnClose
+        zIndex={1000}
+        placement={'right'}
+        extra={
+          props.type === 'product' ? (
+            <Dropdown.Button type="primary" onClick={() => saveMetadata()} overlay={menu}>
+              保存数据
+            </Dropdown.Button>
+          ) : (
+            <Button type="primary" onClick={() => saveMetadata()}>
+              保存数据
+            </Button>
+          )
+        }
+      >
+        <Form form={form} layout="vertical" size="small">
+          <SchemaField schema={metadataTypeMapping.properties.schema} />
+          {/*<SchemaField schema={metadataTypeMapping[MetadataModel.type].schema} />*/}
+        </Form>
+      </Drawer>
+    </>
   );
 };
 export default Edit;

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

@@ -20,7 +20,7 @@ interface Props {
 }
 
 const BaseMetadata = observer((props: Props) => {
-  const { type, target } = props;
+  const { type, target = 'product' } = props;
   const intl = useIntl();
   const param = useParams<{ id: string }>();
 

File diff ditekan karena terlalu besar
+ 19081 - 0
yarn.lock