Lind 3 лет назад
Родитель
Сommit
b695ffb834

+ 74 - 74
config/routes.ts

@@ -105,80 +105,80 @@
       //
     ],
   },
-  // {
-  //   path: '/device',
-  //   name: 'device',
-  //   icon: 'crown',
-  //   routes: [
-  //     {
-  //       path: '/device',
-  //       redirect: '/device/product',
-  //     },
-  //     {
-  //       path: '/device/product',
-  //       name: 'product',
-  //       icon: 'smile',
-  //       component: './device/Product',
-  //     },
-  //     {
-  //       path: '/device/category',
-  //       name: 'category',
-  //       icon: 'smile',
-  //       component: './device/Category',
-  //     },
-  //     {
-  //       hideInMenu: true,
-  //       path: '/device/product/detail/:id',
-  //       name: 'product-detail',
-  //       icon: 'smile',
-  //       component: './device/Product/Detail',
-  //     },
-  //     {
-  //       path: '/device/instance',
-  //       name: 'instance',
-  //       icon: 'smile',
-  //       component: './device/Instance',
-  //     },
-  //     {
-  //       hideInMenu: true,
-  //       path: '/device/instance/detail/:id',
-  //       name: 'instance-detail',
-  //       icon: 'smile',
-  //       component: './device/Instance/Detail',
-  //     },
-  //     {
-  //       path: '/device/command',
-  //       name: 'command',
-  //       icon: 'smile',
-  //       component: './device/Command',
-  //     },
-  //     {
-  //       path: '/device/firmware',
-  //       name: 'firmware',
-  //       icon: 'smile',
-  //       component: './device/Firmware',
-  //     },
-  //     {
-  //       hideInMenu: true,
-  //       path: '/device/firmware/detail/:id',
-  //       name: 'firmware-detail',
-  //       icon: 'smile',
-  //       component: './device/Firmware/Detail',
-  //     },
-  //     {
-  //       path: '/device/alarm',
-  //       name: 'alarm',
-  //       icon: 'smile',
-  //       component: './device/Alarm',
-  //     },
-  //     {
-  //       path: '/device/location',
-  //       name: 'location',
-  //       icon: 'smile',
-  //       component: './device/Location',
-  //     },
-  //   ],
-  // },
+  {
+    path: '/device',
+    name: 'device',
+    icon: 'crown',
+    routes: [
+      {
+        path: '/device',
+        redirect: '/device/product',
+      },
+      {
+        path: '/device/product',
+        name: 'product',
+        icon: 'smile',
+        component: './device/Product',
+      },
+      {
+        path: '/device/category',
+        name: 'category',
+        icon: 'smile',
+        component: './device/Category',
+      },
+      {
+        hideInMenu: true,
+        path: '/device/product/detail/:id',
+        name: 'product-detail',
+        icon: 'smile',
+        component: './device/Product/Detail',
+      },
+      {
+        path: '/device/instance',
+        name: 'instance',
+        icon: 'smile',
+        component: './device/Instance',
+      },
+      {
+        hideInMenu: true,
+        path: '/device/instance/detail/:id',
+        name: 'instance-detail',
+        icon: 'smile',
+        component: './device/Instance/Detail',
+      },
+      {
+        path: '/device/command',
+        name: 'command',
+        icon: 'smile',
+        component: './device/Command',
+      },
+      {
+        path: '/device/firmware',
+        name: 'firmware',
+        icon: 'smile',
+        component: './device/Firmware',
+      },
+      {
+        hideInMenu: true,
+        path: '/device/firmware/detail/:id',
+        name: 'firmware-detail',
+        icon: 'smile',
+        component: './device/Firmware/Detail',
+      },
+      {
+        path: '/device/alarm',
+        name: 'alarm',
+        icon: 'smile',
+        component: './device/Alarm',
+      },
+      {
+        path: '/device/location',
+        name: 'location',
+        icon: 'smile',
+        component: './device/Location',
+      },
+    ],
+  },
   // {
   //   path: '/link',
   //   name: 'link',

+ 4 - 0
package.json

@@ -71,6 +71,7 @@
     "@formily/shared": "2.0.0-rc.17",
     "@jetlinks/pro-list": "^1.10.8",
     "@jetlinks/pro-table": "^2.63.10",
+    "@types/react-syntax-highlighter": "^13.5.2",
     "@umijs/route-utils": "^1.0.36",
     "ahooks": "^2.10.9",
     "antd": "^4.18.8",
@@ -88,8 +89,11 @@
     "react-dev-inspector": "^1.1.1",
     "react-dom": "^17.0.0",
     "react-helmet-async": "^1.0.4",
+    "react-markdown": "^8.0.0",
     "react-monaco-editor": "^0.46.0",
+    "react-syntax-highlighter": "^15.4.5",
     "reconnecting-websocket": "^4.4.0",
+    "remark-gfm": "^3.0.1",
     "rxjs": "^7.2.0",
     "rxjs-websockets": "8",
     "umi": "^3.5.0",

+ 37 - 0
src/components/FRuleEditor/index.less

@@ -0,0 +1,37 @@
+.box {
+  border: 1px solid lightgray;
+
+  .top {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    width: 100%;
+    border-bottom: 1px solid lightgray;
+
+    .left {
+      display: flex;
+      align-items: center;
+      width: 200px;
+      margin: 0 5px;
+
+      span {
+        display: inline-block;
+        height: 40px;
+        margin: 0 10px;
+        line-height: 40px;
+        cursor: pointer;
+      }
+    }
+
+    .right {
+      display: flex;
+      align-items: center;
+      width: 100px;
+      margin: 0 5px;
+
+      span {
+        margin: 0 5px;
+      }
+    }
+  }
+}

+ 137 - 0
src/components/FRuleEditor/index.tsx

@@ -0,0 +1,137 @@
+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';
+
+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 = () => {
+  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>
+  );
+};
+export default FRuleEditor;

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

@@ -21,6 +21,9 @@ 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 = {
@@ -206,6 +209,9 @@ const Product = observer(() => {
         }}
         visible={visible}
       />
+      <Edit type={'product'} />
+      {/*<Operator />*/}
+      {/*<Debug />*/}
     </PageContainer>
   );
 });

+ 75 - 0
src/pages/device/components/Debug/index.less

@@ -0,0 +1,75 @@
+.container {
+  display: flex;
+  width: 100%;
+  height: 340px;
+
+  .left {
+    flex: 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;
+        }
+      }
+    }
+  }
+}

+ 122 - 0
src/pages/device/components/Debug/index.tsx

@@ -0,0 +1,122 @@
+import { Modal } from 'antd';
+import styles from './index.less';
+import { createSchemaField, FormProvider } from '@formily/react';
+import { ArrayTable, FormItem, Input, Select } from '@formily/antd';
+import { useMemo } from 'react';
+import { createForm } from '@formily/core';
+import type { ISchema } from '@formily/json-schema';
+
+const Debug = () => {
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      ArrayTable,
+    },
+  });
+  const form = useMemo(() => createForm(), []);
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      array: {
+        type: 'array',
+        'x-decorator': 'FormItem',
+        'x-component': 'ArrayTable',
+        'x-component-props': {
+          pagination: false,
+        },
+        items: {
+          type: 'object',
+          properties: {
+            column1: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '字段1',
+              },
+              properties: {
+                t1: {
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                },
+              },
+            },
+            column2: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '字段2',
+              },
+              properties: {
+                t2: {
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                },
+              },
+            },
+            column3: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '字段3',
+              },
+              properties: {
+                t2: {
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                },
+              },
+            },
+          },
+        },
+        properties: {
+          add: {
+            type: 'void',
+            'x-component': 'ArrayTable.Addition',
+            title: '添加条目',
+          },
+        },
+      },
+    },
+  };
+  return (
+    <Modal visible width="40vw" title="debug">
+      <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>
+          <FormProvider form={form}>
+            <SchemaField schema={schema} />
+          </FormProvider>
+        </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>
+    </Modal>
+  );
+};
+
+export default Debug;

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

@@ -600,7 +600,8 @@ const Edit = (props: Props) => {
       }
     >
       <Form form={form} layout="vertical" size="small">
-        <SchemaField schema={metadataTypeMapping[MetadataModel.type].schema} />
+        <SchemaField schema={metadataTypeMapping.properties.schema} />
+        {/*<SchemaField schema={metadataTypeMapping[MetadataModel.type].schema} />*/}
       </Form>
     </Drawer>
   );

+ 38 - 0
src/pages/device/components/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/pages/device/components/Operator/index.tsx

@@ -0,0 +1,83 @@
+import { Input, Modal, Tree } from 'antd';
+import Service from '@/pages/device/components/Operator/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 '@/pages/device/components/Operator/typings';
+
+const service = new Service();
+
+interface Props {
+  onChange: (value: any) => void;
+  data: any;
+}
+
+const Operator = (props: Props) => {
+  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 (
+    <Modal visible={true} title="快速添加">
+      <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={() => props.onChange(item.code)}>添加</a>
+              </div>
+            </div>
+          )}
+          autoExpandParent={true}
+          treeData={data}
+        />
+        <div className={styles.explain}>
+          <ReactMarkdown>{item.description || ''}</ReactMarkdown>
+        </div>
+      </div>
+    </Modal>
+  );
+};
+export default Operator;

+ 11 - 0
src/pages/device/components/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/pages/device/components/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[];
+}

+ 117 - 0
src/utils/tree.ts

@@ -0,0 +1,117 @@
+/**
+ 场景
+ 树形数据过滤, 并保留原有树形结构不变, 即如果有子集被选中,父级同样保留。
+ 思路
+ 对数据进行处理,根据过滤标识对匹配的数据添加标识。如visible:true
+ 对有标识的子集的父级添加标识visible:true
+ 根据visible标识对数据进行递归过滤,得到最后的数据
+ */
+
+import _ from 'lodash';
+
+export type TreeNode = {
+  id: string;
+  name: string;
+  children: TreeNode[];
+  visible?: boolean;
+} & Record<string, any>;
+
+/*
+ *	对表格数据进行处理
+ *	data 树形数据数组
+ *	filter 过滤参数值
+ *	filterType 过滤参数名
+ */
+export function treeFilter(data: TreeNode[], filter: string, filterType: string): TreeNode[] {
+  const _data = _.cloneDeep(data);
+  const traverse = (item: TreeNode[]) => {
+    item.forEach((child) => {
+      child.visible = filterMethod(filter, child, filterType);
+      if (child.children) traverse(child.children);
+      if (!child.visible && child.children?.length) {
+        const visible = !child.children.some((c) => c.visible);
+        child.visible = !visible;
+      }
+    });
+  };
+  traverse(_data);
+  return filterDataByVisible(_data);
+}
+
+// 根据传入的值进行数据匹配, 并返回匹配结果
+function filterMethod(val: string, data: TreeNode, filterType: string | number) {
+  return data[filterType].includes(val);
+}
+
+// 递归过滤符合条件的数据
+function filterDataByVisible(data: TreeNode[]) {
+  return data.filter((item) => {
+    if (item.children) {
+      item.children = filterDataByVisible(item.children);
+    }
+    return item.visible;
+  });
+}
+
+const mockData = [
+  {
+    children: [
+      {
+        children: [],
+        name: '加',
+        id: 'operator-1',
+      },
+      {
+        children: [],
+        name: '减',
+        id: 'operator-2',
+      },
+      {
+        children: [],
+        name: '乘',
+        id: 'operator-3',
+      },
+      {
+        children: [],
+        name: '除',
+        id: 'operator-4',
+      },
+      {
+        children: [],
+        name: '括号',
+        id: 'operator-5',
+      },
+      {
+        children: [],
+        name: '按位异或',
+        id: 'operator-6',
+      },
+    ],
+    name: '操作符',
+    id: 'operator',
+  },
+  {
+    children: [
+      {
+        children: [],
+        name: 'if',
+        id: 'if',
+      },
+      {
+        children: [],
+        name: 'for',
+        id: 'for',
+      },
+      {
+        children: [],
+        name: 'while',
+        id: 'while',
+      },
+    ],
+    name: '控制语句',
+    id: 'control',
+  },
+];
+const myTree = treeFilter(mockData, '操作', 'name');
+
+console.log(JSON.stringify(myTree), 'mytree');