Przeglądaj źródła

feat(ruleEditor): metadata ruleEditor

Lind 3 lat temu
rodzic
commit
da0f329757

+ 1 - 1
src/app.tsx

@@ -210,7 +210,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
 
 export function patchRoutes(routes: any) {
   if (extraRoutes && extraRoutes.length) {
-    routes.routes[1].routes = [...routes.routes[1].routes, ...getRoutes(extraRoutes)];
+    routes.routes[1].routes = [...routes?.routes[1]?.routes, ...getRoutes(extraRoutes)];
   }
 }
 

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

@@ -74,5 +74,11 @@
         }
       }
     }
+
+    .log {
+      height: 290px;
+      padding: 5px;
+      overflow: auto;
+    }
   }
 }

+ 72 - 9
src/components/FRuleEditor/Debug/index.tsx

@@ -1,12 +1,21 @@
 import styles from './index.less';
-import { createSchemaField } from '@formily/react';
+import { createSchemaField, observer } from '@formily/react';
 import { ArrayTable, Form, FormItem, Input, Select } from '@formily/antd';
 import { useMemo } from 'react';
+import type { Field } from '@formily/core';
 import { createForm } from '@formily/core';
 import type { ISchema } from '@formily/json-schema';
 import FAutoComplete from '@/components/FAutoComplete';
+import { Descriptions, Tooltip } from 'antd';
+import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+import type { WebsocketPayload } from '@/hooks/websocket/typings';
+import { State } from '@/components/FRuleEditor';
+import moment from 'moment';
+import DB from '@/db';
+import type { PropertyMetadata } from '@/pages/device/Product/typings';
+import { action } from '@formily/reactive';
 
-const Debug = () => {
+const Debug = observer(() => {
   const SchemaField = createSchemaField({
     components: {
       FormItem,
@@ -17,11 +26,25 @@ const Debug = () => {
     },
   });
   const form = useMemo(() => createForm(), []);
+  const useAsyncDataSource = (services: (arg0: Field) => Promise<any>) => (field: Field) => {
+    field.loading = true;
+    services(field).then(
+      action.bound!((data: PropertyMetadata[]) => {
+        field.dataSource = data.map((item) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        field.loading = false;
+      }),
+    );
+  };
+
+  const getProperty = async () => DB.getDB().table('properties').toArray();
 
   const schema: ISchema = {
     type: 'object',
     properties: {
-      array: {
+      properties: {
         type: 'array',
         'x-decorator': 'FormItem',
         'x-component': 'ArrayTable',
@@ -48,7 +71,7 @@ const Debug = () => {
                 id: {
                   'x-decorator': 'FormItem',
                   'x-component': 'FAutoComplete',
-                  enum: [1, 2, 4],
+                  'x-reactions': '{{useAsyncDataSource(getProperty)}}',
                 },
               },
             },
@@ -112,6 +135,26 @@ const Debug = () => {
       },
     },
   };
+
+  const [subscribeTopic] = useSendWebsocketMessage();
+
+  const runScript = async () => {
+    subscribeTopic?.(
+      `virtual-property-debug-${State.property}-${new Date().getTime()}`,
+      '/virtual-property-debug',
+      {
+        virtualId: `${new Date().getTime()}-virtual-id`,
+        property: State.property,
+        virtualRule: {
+          type: 'script',
+          script: State.code,
+        },
+        properties: form.values.properties || [],
+      },
+    )?.subscribe((data: WebsocketPayload) => {
+      State.log.push({ time: new Date().getTime(), content: JSON.stringify(data.payload) });
+    });
+  };
   return (
     <div className={styles.container}>
       <div className={styles.left}>
@@ -124,7 +167,7 @@ const Debug = () => {
           </div>
         </div>
         <Form form={form}>
-          <SchemaField schema={schema} />
+          <SchemaField schema={schema} scope={{ useAsyncDataSource, getProperty }} />
         </Form>
       </div>
       <div className={styles.right}>
@@ -135,17 +178,37 @@ const Debug = () => {
 
           <div className={styles.action}>
             <div>
-              <a>开始运行</a>
-              {/*<a>停止运行</a>*/}
+              <a onClick={runScript}>开始运行</a>
             </div>
             <div>
-              <a>清空</a>
+              <a
+                onClick={() => {
+                  State.log = [];
+                }}
+              >
+                清空
+              </a>
             </div>
           </div>
         </div>
+        <div className={styles.log}>
+          <Descriptions>
+            {State.log.map((item) => (
+              <Descriptions.Item
+                label={moment(item.time).format('HH:mm:ss')}
+                key={item.time}
+                span={3}
+              >
+                <Tooltip placement="top" title={item.content}>
+                  {item.content}
+                </Tooltip>
+              </Descriptions.Item>
+            ))}
+          </Descriptions>
+        </div>
       </div>
     </div>
   );
-};
+});
 
 export default Debug;

+ 62 - 15
src/components/FRuleEditor/Operator/index.tsx

@@ -1,4 +1,4 @@
-import { Input, Tree } from 'antd';
+import { Button, Input, Popover, Space, Tooltip, Tree } from 'antd';
 import Service from './service';
 import { useEffect, useRef, useState } from 'react';
 import styles from './index.less';
@@ -6,6 +6,8 @@ import ReactMarkdown from 'react-markdown';
 import { treeFilter } from '@/utils/tree';
 import type { OperatorItem } from './typings';
 import { Store } from 'jetlinks-store';
+import DB from '@/db';
+import type { PropertyMetadata } from '@/pages/device/Product/typings';
 
 const service = new Service();
 
@@ -14,18 +16,21 @@ const Operator = () => {
   const [item, setItem] = useState<Partial<OperatorItem>>({});
   const dataRef = useRef<OperatorItem[]>([]);
   const getData = async () => {
-    // TODO 从物模型中获取属性数据
+    const _properties = (await DB.getDB().table('properties').toArray()) as PropertyMetadata[];
     const properties = {
       id: 'property',
       name: '属性',
       description: '',
       code: '',
-      children: [
-        {
-          id: 'test',
-          name: '测试数据',
-        },
-      ],
+      children: _properties.map((p) => ({
+        id: p.id,
+        name: p.name,
+        description: `### ${p.name}
+        \n 数据类型: ${p.valueType?.type}
+        \n 是否只读: ${p.expands?.readOnly || 'false'}
+        \n 可写数值范围: ---`,
+        type: 'property',
+      })),
     };
     const response = await service.getOperator();
     if (response.status === 200) {
@@ -45,6 +50,7 @@ const Operator = () => {
       setData(dataRef.current);
     }
   };
+  const [visible, setVisible] = useState<boolean>(false);
   return (
     <div className={styles.box}>
       <Input.Search onSearch={search} allowClear placeholder="搜索关键字" />
@@ -61,13 +67,54 @@ const Operator = () => {
           <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>
+              {node.type === 'property' ? (
+                <Popover
+                  visible={visible}
+                  placement="right"
+                  title="请选择使用值"
+                  content={
+                    <Space direction="vertical">
+                      <Tooltip
+                        placement="right"
+                        title="实时值为空时获取上一有效值补齐,实时值不为空则使用实时值"
+                      >
+                        <Button
+                          type="text"
+                          onClick={() => {
+                            Store.set('add-operator-value', `$recent("${node.id}")`);
+                            setVisible(!visible);
+                          }}
+                        >
+                          $recent实时值
+                        </Button>
+                      </Tooltip>
+                      <Tooltip placement="right" title="实时值的上一有效值">
+                        <Button
+                          onClick={() => {
+                            Store.set('add-operator-value', `$lastState("${node.id}")`);
+                            setVisible(!visible);
+                          }}
+                          type="text"
+                        >
+                          上一值
+                        </Button>
+                      </Tooltip>
+                    </Space>
+                  }
+                  trigger="click"
+                >
+                  <a onClick={() => setVisible(true)}>添加</a>
+                </Popover>
+              ) : (
+                <a
+                  onClick={() => {
+                    Store.set('add-operator-value', node.code);
+                    setVisible(true);
+                  }}
+                >
+                  添加
+                </a>
+              )}
             </div>
           </div>
         )}

+ 9 - 2
src/components/FRuleEditor/index.tsx

@@ -8,20 +8,27 @@ import { Store } from 'jetlinks-store';
 export const State = model<{
   model: 'simple' | 'advance';
   code: string;
+  property?: string;
+  log: {
+    content: string;
+    time: number;
+  }[];
 }>({
   model: 'simple',
   code: '',
+  log: [],
 });
 
 interface Props {
   value: string;
   onChange: (value: string) => void;
+  property?: string;
 }
 
 const FRuleEditor = observer((props: Props) => {
-  const { value, onChange } = props;
-
+  const { value, onChange, property } = props;
   useEffect(() => {
+    State.property = property;
     const subscription = Store.subscribe('rule-editor-value', onChange);
     State.code = value;
     return () => subscription.unsubscribe();

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

@@ -1,75 +0,0 @@
-.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;
-        }
-      }
-    }
-  }
-}

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

@@ -1,122 +0,0 @@
-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;

+ 19 - 10
src/pages/device/components/Metadata/Base/Edit/index.tsx

@@ -368,14 +368,22 @@ const Edit = observer((props: Props) => {
             type: 'string',
             'x-component': 'FRuleEditor',
             'x-visible': false,
-            'x-reactions': {
-              dependencies: ['.source'],
-              fulfill: {
-                state: {
-                  visible: '{{$deps[0]==="rule"}}',
+            'x-component-props': {
+              property: 'ggg',
+            },
+            'x-reactions': [
+              {
+                dependencies: ['.source', '..id'],
+                fulfill: {
+                  state: {
+                    visible: '{{$deps[0]==="rule"}}',
+                  },
+                  schema: {
+                    'x-component-props.property': '{{$deps[1]}}',
+                  },
                 },
               },
-            },
+            ],
           },
           type: {
             title: '属性类型',
@@ -537,10 +545,8 @@ const Edit = observer((props: Props) => {
 
     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 metadata = JSON.parse(typeMap.get(props.type).metadata || '{}') as DeviceMetadata;
+    const config = (metadata[type] || []) as MetadataItem[];
     const index = config.findIndex((item) => item.id === params.id);
     if (index > -1) {
       config[index] = params;
@@ -552,6 +558,9 @@ const Edit = observer((props: Props) => {
 
     if (props.type === 'product') {
       const product = typeMap.get('product');
+      // product.metadata = JSON.stringify(metadata);
+      // @ts-ignore
+      metadata[type] = config;
       product.metadata = JSON.stringify(metadata);
       saveMap.set('product', service.saveProductMetadata(product));
     } else {

+ 0 - 38
src/pages/device/components/Operator/index.less

@@ -1,38 +0,0 @@
-.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;
-      }
-    }
-  }
-}

+ 0 - 83
src/pages/device/components/Operator/index.tsx

@@ -1,83 +0,0 @@
-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;

+ 0 - 11
src/pages/device/components/Operator/service.ts

@@ -1,11 +0,0 @@
-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;

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

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