瀏覽代碼

bug-370 辨识仪配置流程添加说明和快捷连接节点

cc12458 6 月之前
父節點
當前提交
fe289333d7

+ 37 - 2
src/pages/aio/flow-config/index.vue

@@ -55,7 +55,9 @@ let isUnmounted = false;
 const hasInstance = () => !isUnmounted && !!instance && !!instance.lf;
 const hasInstance = () => !isUnmounted && !!instance && !!instance.lf;
 // end
 // end
 let oldValue: FlowRequestData;
 let oldValue: FlowRequestData;
+const maxPanelHeight = ref(0);
 const init = (lf: LogicFlowInstance): void => {
 const init = (lf: LogicFlowInstance): void => {
+  maxPanelHeight.value = lf.container.getBoundingClientRect().height - 80;
   instance = VLogicFlowInit(lf, {
   instance = VLogicFlowInit(lf, {
     register: [{ category: 'node', type: 'FlowNode', view: FlowNodeView, model: FlowNodeViewModel }],
     register: [{ category: 'node', type: 'FlowNode', view: FlowNodeView, model: FlowNodeViewModel }],
   });
   });
@@ -141,6 +143,27 @@ const init = (lf: LogicFlowInstance): void => {
       getContainer: () => (el.value as HTMLElement) ?? document.body,
       getContainer: () => (el.value as HTMLElement) ?? document.body,
     });
     });
   });
   });
+  // @ts-ignore
+  instance.listener('connection:quick', (event: any) => {
+    const add = (id: string) => {
+      for (const group of nodeGroup.value) {
+        const node = group.find((node) => node.id === id);
+        if (node) return instance.lf.addNode(node);
+      }
+      return void 0;
+    };
+    const { sourceNodeId, targetNodeId } = event;
+    const source = instance.lf.getNodeModelById(sourceNodeId) ?? add(sourceNodeId);
+    const target = instance.lf.getNodeModelById(targetNodeId) ?? add(targetNodeId);
+    if (source && target) {
+      const sourceEdge = instance.lf.getNodeOutgoingEdge(sourceNodeId);
+      const targetEdge = instance.lf.getNodeIncomingEdge(targetNodeId);
+      for (const edge of sourceEdge) instance.lf.deleteEdge(edge.id);
+      for (const edge of targetEdge) instance.lf.deleteEdge(edge.id);
+      instance.lf.addEdge({ sourceNodeId, targetNodeId });
+      updateLayout();
+    }
+  });
 };
 };
 
 
 const updateLayout = (dir?: 'LR' | 'TB' | 'center') => {
 const updateLayout = (dir?: 'LR' | 'TB' | 'center') => {
@@ -243,7 +266,7 @@ const update = (data?: FlowRequestData) => {
 
 
   instance.lf.renderRawData(graph);
   instance.lf.renderRawData(graph);
   updateLayout('TB');
   updateLayout('TB');
-  if (graph.nodes && graph.nodes.length > 2) setTimeout(() => updateLayout('center'), 100)
+  if (graph.nodes && graph.nodes.length > 2) setTimeout(() => updateLayout('center'), 100);
 };
 };
 const validate = (tips = true) => {
 const validate = (tips = true) => {
   if (!hasInstance()) return Promise.reject(new Error('LogicFlow 已销毁'));
   if (!hasInstance()) return Promise.reject(new Error('LogicFlow 已销毁'));
@@ -353,7 +376,7 @@ defineExpose({
           </template>
           </template>
         </a-button>
         </a-button>
         <template #overlay>
         <template #overlay>
-          <a-card size="small" style="width: 370px">
+          <a-card size="small" style="width: 370px; overflow-y: auto;" :style="{ maxHeight: maxPanelHeight + 'px' }">
             <div class="flex justify-between m-y-2" v-for="(group, g) in nodeGroup" :key="g">
             <div class="flex justify-between m-y-2" v-for="(group, g) in nodeGroup" :key="g">
               <FlowNodeComponent
               <FlowNodeComponent
                 :class="{ selected: dragPanelNodeId === node.id, disabled: getPanelNodeDisabled(node) }"
                 :class="{ selected: dragPanelNodeId === node.id, disabled: getPanelNodeDisabled(node) }"
@@ -365,6 +388,12 @@ defineExpose({
                 @mousedown="startDragPanelNode(node, $event)"
                 @mousedown="startDragPanelNode(node, $event)"
               />
               />
             </div>
             </div>
+            <a-space direction="vertical" class="tips-wrapper">
+              <div>添加流程:从左侧拖入</div>
+              <div>编辑流程:单击</div>
+              <div>删除流程和连线:双击</div>
+              <div>连接流程:从上流节点中拖拽锚点到下流节点以连接</div>
+            </a-space>
           </a-card>
           </a-card>
         </template>
         </template>
       </a-dropdown>
       </a-dropdown>
@@ -406,6 +435,12 @@ defineExpose({
   left: 24px;
   left: 24px;
   z-index: 1;
   z-index: 1;
 }
 }
+.tips-wrapper {
+  width: 100%;
+  padding: 4px;
+  color: rgba(0, 0, 0, 0.8);
+  border: 1px #bbbbbb dashed;
+}
 .ant-float-btn-group {
 .ant-float-btn-group {
   position: absolute !important;
   position: absolute !important;
   bottom: 24px;
   bottom: 24px;

+ 104 - 1
src/pages/aio/flow-config/nodes/FlowNode.vue

@@ -5,6 +5,7 @@ import { VueNodeModel } from '@logicflow/vue-node-registry';
 
 
 import { useEventListener } from '@/libs/logic-flow/use';
 import { useEventListener } from '@/libs/logic-flow/use';
 
 
+import { ID_End, ID_Start } from './config';
 import type { FlowNodeProperties } from './index';
 import type { FlowNodeProperties } from './index';
 import FlowNodeInlay from './FlowNodeInlay.vue';
 import FlowNodeInlay from './FlowNodeInlay.vue';
 
 
@@ -21,6 +22,8 @@ const getGraph = inject<() => GraphModelInstance>('getGraph');
 const text = ref('');
 const text = ref('');
 const id = ref('');
 const id = ref('');
 
 
+const popover = ref(false);
+
 const properties = ref<FlowNodeProperties>({
 const properties = ref<FlowNodeProperties>({
   width: 0,
   width: 0,
   height: 0,
   height: 0,
@@ -29,6 +32,10 @@ const properties = ref<FlowNodeProperties>({
   iconColor: '#000',
   iconColor: '#000',
 });
 });
 
 
+const appendNodes = shallowRef<{ id: string; text: string }[]>([]);
+const linkNodes = shallowRef<{ id: string; text: string; disabled?: boolean }[]>([]);
+
+let mouseenterTimer: ReturnType<typeof setTimeout>;
 tryOnMounted(() => {
 tryOnMounted(() => {
   const model = getNode?.()!;
   const model = getNode?.()!;
   const graph = getGraph?.()!;
   const graph = getGraph?.()!;
@@ -43,15 +50,102 @@ tryOnMounted(() => {
     (event) => updateProperties(event.properties),
     (event) => updateProperties(event.properties),
     (event) => event.id === id.value
     (event) => event.id === id.value
   );
   );
+  useEventListener(
+    graph.eventCenter,
+    'node:dnd-drag',
+    () => {
+      popover.value = false;
+      clearTimeout(mouseenterTimer);
+    },
+    (event) => event.data?.id === id.value
+  );
+  useEventListener(
+    graph.eventCenter,
+    'node:drag',
+    () => {
+      popover.value = false;
+      clearTimeout(mouseenterTimer);
+    },
+    (event) => event.data?.id === id.value
+  );
+  useEventListener(
+    graph.eventCenter,
+    'node:mouseenter',
+    (event) => {
+      mouseenterTimer = setTimeout(() => {
+        popover.value = true;
+        const quickNodes = event.data.properties?.quickNodes ?? [];
+        appendNodes.value = [...quickNodes];
+        linkNodes.value = [];
+        const outgoing = graph.getNodeOutgoingNode(event.data.id).map((node) => node.id);
+        for (const { id } of graph.nodes) {
+          const index = appendNodes.value.findIndex((node) => node.id === id);
+          if (index !== -1) {
+            const node = appendNodes.value.splice(index, 1).at(0);
+            linkNodes.value.push({ ...node, disabled: outgoing.includes(node.id) });
+          }
+        }
+        triggerRef(appendNodes);
+        triggerRef(linkNodes);
+      }, 100);
+    },
+    (event) => event.data?.id === id.value
+  );
 });
 });
 
 
 function updateProperties(props: FlowNodeProperties) {
 function updateProperties(props: FlowNodeProperties) {
   properties.value = Object.assign(properties.value, props);
   properties.value = Object.assign(properties.value, props);
 }
 }
+
+function configure(event: MouseEvent) {
+  const data = getNode?.().getData();
+  // @ts-ignore
+  if (data) getGraph?.()?.eventCenter?.emit('node:click', { data, e: event, position: void 0 });
+  popover.value = false;
+}
+
+function remove() {
+  const data = getNode?.().getData();
+  if (data) getGraph?.()?.deleteNode(data.id);
+  popover.value = false;
+}
+
+function link(targetNodeId: string) {
+  const data = getNode?.().getData();
+  if (data)
+    getGraph?.()?.eventCenter?.emit('connection:quick', {
+      sourceNodeId: data.id,
+      targetNodeId,
+    });
+  popover.value = false;
+}
 </script>
 </script>
 
 
 <template>
 <template>
-  <FlowNodeInlay class="node" :id :text v-bind="properties"></FlowNodeInlay>
+  <FlowNodeInlay v-if="id === ID_End" class="node" :id :text v-bind="properties"></FlowNodeInlay>
+  <a-popover v-else :title="text" placement="right" trigger="hover" v-model:open="popover" :mouseEnterDelay="1">
+    <template #content>
+      <div class="popover-content-wrapper">
+        <a-space wrap>
+          <a-button type="primary" v-if="properties?.configurable" @click="configure">配置数据</a-button>
+          <a-button danger :disabled="id === ID_End || id === ID_Start" @click.prevent="remove">移除节点</a-button>
+        </a-space>
+        <div style="margin-top: 14px" v-if="appendNodes.length">
+          <div class="title">添加下流节点</div>
+          <a-space wrap>
+            <a-button v-for="node in appendNodes" :key="node.id" type="dashed" @click.prevent="link(node.id)">{{ node.text }}</a-button>
+          </a-space>
+        </div>
+        <div style="margin-top: 14px" v-if="linkNodes.length">
+          <div class="title">连接下流节点</div>
+          <a-space wrap>
+            <a-button v-for="node in linkNodes" :key="node.id" :disabled="node.disabled" type="dashed" @click.prevent="link(node.id)">{{ node.text }}</a-button>
+          </a-space>
+        </div>
+      </div>
+    </template>
+    <FlowNodeInlay class="node" :id :text v-bind="properties"></FlowNodeInlay>
+  </a-popover>
 </template>
 </template>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
@@ -62,4 +156,13 @@ function updateProperties(props: FlowNodeProperties) {
   --icon-background: v-bind(properties.iconBackground);
   --icon-background: v-bind(properties.iconBackground);
   --icon-color: v-bind(properties.iconColor);
   --icon-color: v-bind(properties.iconColor);
 }
 }
+
+.popover-content-wrapper {
+  min-width: 300px;
+  .title {
+    margin-bottom: 8px;
+    color: rgba(0, 0, 0, 0.88);
+    font-weight: 600;
+  }
+}
 </style>
 </style>

+ 6 - 0
src/pages/aio/flow-config/nodes/config.ts

@@ -76,6 +76,7 @@ export function start(options?: CreateNodeOptions) {
     iconBackground: '#cf1322',
     iconBackground: '#cf1322',
     iconColor: '#fff',
     iconColor: '#fff',
     configurable: true,
     configurable: true,
+    quickNodes: [ID_Register, ID_Analysis_Pulse, ID_Analysis_TongueAndFace].map((id) => ({ id, text: formatText(id) })),
     ...options,
     ...options,
   });
   });
 }
 }
@@ -104,6 +105,7 @@ export function register(options?: CreateNodeOptions) {
     iconColor: '#fff',
     iconColor: '#fff',
     onlySource: [START_ID],
     onlySource: [START_ID],
     configurable: true,
     configurable: true,
+    quickNodes: [ID_Analysis_Pulse, ID_Analysis_TongueAndFace].map((id) => ({ id, text: formatText(id) })),
     ...options,
     ...options,
   });
   });
 }
 }
@@ -114,6 +116,7 @@ export function pulseAnalysis(options?: CreateNodeOptions) {
     iconBackground: '#d46b08',
     iconBackground: '#d46b08',
     iconColor: '#fff',
     iconColor: '#fff',
     forbidSource: [ID_Report_Pulse],
     forbidSource: [ID_Report_Pulse],
+    quickNodes: [ID_Analysis_TongueAndFace].map((id) => ({ id, text: formatText(id) })),
     ...options,
     ...options,
   });
   });
 }
 }
@@ -123,6 +126,7 @@ export function tongueAndFaceAnalysis(options?: CreateNodeOptions) {
     iconBackground: '#d46b08',
     iconBackground: '#d46b08',
     iconColor: '#fff',
     iconColor: '#fff',
     forbidSource: [ID_Report_TongueAndFace, ID_Analysis_Health, ID_Report_Alcohol, ID_Report_Health, ID_Scheme_Health],
     forbidSource: [ID_Report_TongueAndFace, ID_Analysis_Health, ID_Report_Alcohol, ID_Report_Health, ID_Scheme_Health],
+    quickNodes: [ID_Analysis_Health, ID_Report_TongueAndFace].map((id) => ({ id, text: formatText(id) })),
     ...options,
     ...options,
   });
   });
 }
 }
@@ -133,6 +137,7 @@ export function healthAnalysis(options?: CreateNodeOptions) {
     iconBackground: '#d46b08',
     iconBackground: '#d46b08',
     iconColor: '#fff',
     iconColor: '#fff',
     forbidSource: [ID_Report_Alcohol, ID_Report_Health, ID_Scheme_Health],
     forbidSource: [ID_Report_Alcohol, ID_Report_Health, ID_Scheme_Health],
+    quickNodes: [ID_Report_Health, ID_Scheme_Health].map((id) => ({ id, text: formatText(id) })),
     ...options,
     ...options,
   });
   });
 }
 }
@@ -152,6 +157,7 @@ export function tongueAndFaceAnalysisReport(options?: CreateNodeOptions) {
     icon: 'report',
     icon: 'report',
     iconBackground: '#08979c',
     iconBackground: '#08979c',
     iconColor: '#fff',
     iconColor: '#fff',
+    configurable: true,
     forbidDirectTarget: [ID_Back],
     forbidDirectTarget: [ID_Back],
     ...options,
     ...options,
   });
   });

+ 2 - 0
src/pages/aio/flow-config/nodes/index.ts

@@ -24,6 +24,8 @@ export interface FlowNodeProperties extends IRectNodeProperties {
   onlySource?: string[];
   onlySource?: string[];
   onlyTarget?: string[];
   onlyTarget?: string[];
 
 
+  quickNodes?: {id: string; text: string}[];
+
   configurable?: boolean;
   configurable?: boolean;
   requestData?: Record<string, any>;
   requestData?: Record<string, any>;
 }
 }