Sfoglia il codice sorgente

Merge branch 'feature/bug' into develop

cc12458 6 mesi fa
parent
commit
dab3c663f5

+ 18 - 0
@types/global.d.ts

@@ -0,0 +1,18 @@
+export {};
+
+declare global {
+  /**
+   * Promise 扩展
+   */
+  interface PromiseConstructor {
+    withResolvers<T>(): {
+      promise: Promise<T>;
+      resolve: (value: T | PromiseLike<T>) => void;
+      reject: (reason?: any) => void;
+    };
+  }
+
+  interface Array<T> {
+    at(index: number): T;
+  }
+}

+ 3 - 0
src/libs/logic-flow/constant.ts

@@ -1,2 +1,5 @@
 export const START_ID = `start`;
 export const END_ID = `end`;
+
+export const START_NODE = `StartNode`
+export const END_NODE = `EndNode`

+ 1 - 1
src/main.ts

@@ -1,6 +1,6 @@
 import 'virtual:uno.css';
 import '@/themes/index.scss';
-
+import './polyfill'
 import vxe    from '@/libs/vxe';
 import router from '@/router';
 import pinia  from '@/stores';

File diff suppressed because it is too large
+ 0 - 0
src/pages/aio/flow-config/assets/finish.svg


+ 44 - 6
src/pages/aio/flow-config/index.vue

@@ -55,7 +55,9 @@ let isUnmounted = false;
 const hasInstance = () => !isUnmounted && !!instance && !!instance.lf;
 // end
 let oldValue: FlowRequestData;
+const maxPanelHeight = ref(0);
 const init = (lf: LogicFlowInstance): void => {
+  maxPanelHeight.value = lf.container.getBoundingClientRect().height - 80;
   instance = VLogicFlowInit(lf, {
     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,
     });
   });
+  // @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') => {
@@ -243,7 +266,7 @@ const update = (data?: FlowRequestData) => {
 
   instance.lf.renderRawData(graph);
   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) => {
   if (!hasInstance()) return Promise.reject(new Error('LogicFlow 已销毁'));
@@ -282,10 +305,10 @@ const validate = (tips = true) => {
 
   const start = instance.lf.getNodeModelById(Node.ID_Start);
 
-  let gather;
+  let gather: Gather;
   const { promise, resolve, reject } = withResolvers<{ gather: Gather; data?: FlowRequestData; message?: string }>();
   try {
-    gather = map(start);
+    gather = map(start).sort((g1, g2) => g1.level - g2.level || +(g1.targetNodeId === Node.ID_Back) - +(g2.targetNodeId === Node.ID_Back));
 
     const data: Record<string, any> = {
       [Node.ID_Start]: instance.lf.getNodeModelById(Node.ID_Start)?.getProperties().requestData,
@@ -294,7 +317,7 @@ const validate = (tips = true) => {
     oldValue = toFlowRequestData(gather, data);
     requestData.value = oldValue;
     resolve({ gather, data: oldValue });
-    console.log('[AioFlowConfig] 更新 request-data 数据: ', );
+    console.log('[AioFlowConfig] 更新 request-data 数据: ', oldValue);
   } catch (error: any) {
     if (tips) {
       notification.error({
@@ -310,9 +333,12 @@ const validate = (tips = true) => {
   }
 
   if (Array.isArray(gather) && gather.length) {
+    const last = gather.at(-1);
+    const notOpenEdgeId = last.targetNodeId === Node.ID_Back ? last.edgeId : void 0;
     for (const { edgeId } of gather) {
       instance.lf.setProperties(edgeId, { isAnimation: true });
-      instance.lf.openEdgeAnimation(edgeId);
+      if (notOpenEdgeId === edgeId) instance.lf.closeEdgeAnimation(notOpenEdgeId);
+      else instance.lf.openEdgeAnimation(edgeId);
     }
   } else {
     for (const edge of instance.lf.getGraphRawData().edges) {
@@ -350,7 +376,7 @@ defineExpose({
           </template>
         </a-button>
         <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">
               <FlowNodeComponent
                 :class="{ selected: dragPanelNodeId === node.id, disabled: getPanelNodeDisabled(node) }"
@@ -362,6 +388,12 @@ defineExpose({
                 @mousedown="startDragPanelNode(node, $event)"
               />
             </div>
+            <a-space direction="vertical" class="tips-wrapper">
+              <div>添加流程:从左侧拖入</div>
+              <div>编辑流程:单击</div>
+              <div>删除流程和连线:双击</div>
+              <div>连接流程:从上流节点中拖拽锚点到下流节点以连接</div>
+            </a-space>
           </a-card>
         </template>
       </a-dropdown>
@@ -403,6 +435,12 @@ defineExpose({
   left: 24px;
   z-index: 1;
 }
+.tips-wrapper {
+  width: 100%;
+  padding: 4px;
+  color: rgba(0, 0, 0, 0.8);
+  border: 1px #bbbbbb dashed;
+}
 .ant-float-btn-group {
   position: absolute !important;
   bottom: 24px;

+ 26 - 14
src/pages/aio/flow-config/nodes/FlowNode.model.ts

@@ -1,5 +1,5 @@
 import { RectNodeModel } from '@logicflow/core';
-import { END_ID } from '@/libs/logic-flow/constant';
+import { END_ID, END_NODE, START_ID } from '@/libs/logic-flow/constant';
 import type { FlowNodeAnchor, FlowNodeConnectRuleResult, FlowNodeProperties } from './index';
 
 export default class FlowNodeModel extends RectNodeModel<FlowNodeProperties> {
@@ -20,6 +20,13 @@ export default class FlowNodeModel extends RectNodeModel<FlowNodeProperties> {
         isAllPass: false,
         msg: `当前 [${source.text.value}] 节点不能直接连接 [${forbidDirectTarget.map((id) => this.graphModel.getNodeModelById(id)?.text.value).join(' | ')}] 节点`,
       };
+    // 获取目标节点禁止直接连接
+    const forbidDirectSource = getProperties(target).forbidDirectSource ?? [];
+    if (forbidDirectSource.includes(source.id))
+      return {
+        isAllPass: false,
+        msg: `目标 [${target.text.value}] 节点不能直接连接 [${forbidDirectSource.map((id) => this.graphModel.getNodeModelById(id)?.text.value).join(' | ')}] 节点`,
+      };
 
     // 获取目标节点 forbidSource
     let forbidSource = getProperties(target).forbidSource ?? [];
@@ -63,6 +70,12 @@ export default class FlowNodeModel extends RectNodeModel<FlowNodeProperties> {
   }
 
   override isAllowConnectedAsTarget(...args: any[]): FlowNodeConnectRuleResult {
+    // 当前节点是否开始
+    if (this.id === START_ID)
+      return {
+        isAllPass: false,
+        msg: `当前 [${this.text.value}] 为开始节点不允许输入`,
+      };
     // 获取目标节点连接到的所有起始节点
     const directNodes = this.graphModel.getNodeIncomingNode(this.id);
     if (directNodes.length >= 1)
@@ -79,31 +92,30 @@ export default class FlowNodeModel extends RectNodeModel<FlowNodeProperties> {
         msg: `目标 [${this.text.value}] 节点仅允许 [${onlySource.map((id) => this.graphModel.getNodeModelById(id)?.text.value).join(' | ')}] 节点作为输入`,
       };
 
-    // 当前节点是否开始
-    if (getProperties(this).start)
-      return {
-        isAllPass: false,
-        msg: `当前 [${this.text.value}] 为开始节点不允许输入`,
-      };
-
-
     return this.isAllowConnected(args[0], this, args[1], args[2], args[3]);
   }
 
   override isAllowConnectedAsSource(...args: any[]): FlowNodeConnectRuleResult {
+    // 当前节点是否结束
+    if (this.id === END_ID)
+      return {
+        isAllPass: false,
+        msg: `当前 [${this.text.value}] 为结束节点不允许输出`,
+      };
     // 获取当前节点所有的下一级节点
-    const directNodes = this.graphModel.getNodeOutgoingNode(this.id).filter((node) => node.id !== END_ID);
-    if (directNodes.length >= 1 && args[0].id !== END_ID)
+    const directNodes = this.graphModel.getNodeOutgoingNode(this.id).filter((node) => (node.type as string) !== END_NODE);
+    if (directNodes.length >= 1 && args[0].type !== END_NODE)
       return {
         isAllPass: false,
         msg: `当前 [${this.text.value}] 节点已存在 ${directNodes.length}个输出 (${directNodes.map((node) => node.text.value)})`,
       };
 
-    // 当前节点是否结束
-    if (getProperties(this).finish && args[0].id !== END_ID)
+    // 获取目标节点 onlyTarget
+    const onlyTarget = (getProperties(this).onlyTarget as string[]) ?? [];
+    if (onlyTarget.length && !onlyTarget.includes(args[0].id))
       return {
         isAllPass: false,
-        msg: `当前 [${this.text.value}] 为结束节点不允许输出`,
+        msg: `当前 [${this.text.value}] 节点仅允许 [${onlyTarget.map((id) => this.graphModel.getNodeModelById(id)?.text.value).join(' | ')}] 节点作为输出`,
       };
 
     return this.isAllowConnected(this, args[0], args[1], args[2], args[3]);

+ 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 { ID_End, ID_Start } from './config';
 import type { FlowNodeProperties } from './index';
 import FlowNodeInlay from './FlowNodeInlay.vue';
 
@@ -21,6 +22,8 @@ const getGraph = inject<() => GraphModelInstance>('getGraph');
 const text = ref('');
 const id = ref('');
 
+const popover = ref(false);
+
 const properties = ref<FlowNodeProperties>({
   width: 0,
   height: 0,
@@ -29,6 +32,10 @@ const properties = ref<FlowNodeProperties>({
   iconColor: '#000',
 });
 
+const appendNodes = shallowRef<{ id: string; text: string }[]>([]);
+const linkNodes = shallowRef<{ id: string; text: string; disabled?: boolean }[]>([]);
+
+let mouseenterTimer: ReturnType<typeof setTimeout>;
 tryOnMounted(() => {
   const model = getNode?.()!;
   const graph = getGraph?.()!;
@@ -43,15 +50,102 @@ tryOnMounted(() => {
     (event) => updateProperties(event.properties),
     (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) {
   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>
 
 <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>
 
 <style scoped lang="scss">
@@ -62,4 +156,13 @@ function updateProperties(props: FlowNodeProperties) {
   --icon-background: v-bind(properties.iconBackground);
   --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>

+ 2 - 0
src/pages/aio/flow-config/nodes/FlowNodeInlay.vue

@@ -3,6 +3,7 @@ import { h } from 'vue';
 import type { FlowNodeProperties } from './index';
 
 import IconStart from '@/pages/aio/flow-config/assets/start.svg';
+import IconFinish from '@/pages/aio/flow-config/assets/finish.svg';
 import IconConfig from '@/pages/aio/flow-config/assets/config.svg';
 import IconReport from '@/pages/aio/flow-config/assets/report.svg';
 import IconPulse from '@/pages/aio/flow-config/assets/pulse.svg';
@@ -20,6 +21,7 @@ defineOptions({
 
 const Icon = {
   start: IconStart,
+  finish: IconFinish,
   config: IconConfig,
   report: IconReport,
   pulse: IconPulse,

+ 25 - 8
src/pages/aio/flow-config/nodes/config.ts

@@ -1,4 +1,4 @@
-import { END_ID, START_ID } from '@/libs/logic-flow/constant';
+import { END_ID, END_NODE, START_ID } from '@/libs/logic-flow/constant';
 import type { FlowNodeProperties } from './index';
 
 interface CreateNodeOptions extends FlowNodeProperties {
@@ -18,6 +18,7 @@ export const DEFAULT_NODE_HEIGHT = 40;
 
 export const ID_Start = START_ID;
 export const ID_End = END_ID;
+export const ID_Back = 'back';
 export const ID_Register = 'register';
 export const ID_Analysis_Pulse = 'pulseAnalysis';
 export const ID_Analysis_TongueAndFace = 'tongueAndFaceAnalysis';
@@ -48,7 +49,8 @@ const factory = (type: 'StartNode' | 'EndNode' | 'FlowNode', id: string, text: s
 
 const textRef = {
   [ID_Start]: '开始检测',
-  [ID_End]: '返回首页',
+  [ID_End]: '完成检测',
+  [ID_Back]: '返回首页',
   [ID_Register]: '建档',
   [ID_Analysis_Pulse]: '脉象分析',
   [ID_Analysis_TongueAndFace]: '舌面象分析',
@@ -73,14 +75,24 @@ export function start(options?: CreateNodeOptions) {
     icon: 'start',
     iconBackground: '#cf1322',
     iconColor: '#fff',
-    start: true,
     configurable: true,
+    quickNodes: [ID_Register, ID_Analysis_Pulse, ID_Analysis_TongueAndFace].map((id) => ({ id, text: formatText(id) })),
     ...options,
   });
 }
 
 export function end(options?: CreateNodeOptions) {
-  return factory('EndNode', ID_End, textRef[ID_End], {
+  return factory('FlowNode', ID_End, textRef[ID_End], {
+    icon: 'finish',
+    iconBackground: '#0958d9',
+    iconColor: '#fff',
+    forbidDirectSource: [ID_Start],
+    ...options,
+  });
+}
+
+export function back(options?: CreateNodeOptions) {
+  return factory(END_NODE, ID_Back, textRef[ID_Back], {
     radius: 20,
     ...options,
   });
@@ -93,6 +105,7 @@ export function register(options?: CreateNodeOptions) {
     iconColor: '#fff',
     onlySource: [START_ID],
     configurable: true,
+    quickNodes: [ID_Analysis_Pulse, ID_Analysis_TongueAndFace].map((id) => ({ id, text: formatText(id) })),
     ...options,
   });
 }
@@ -103,6 +116,7 @@ export function pulseAnalysis(options?: CreateNodeOptions) {
     iconBackground: '#d46b08',
     iconColor: '#fff',
     forbidSource: [ID_Report_Pulse],
+    quickNodes: [ID_Analysis_TongueAndFace].map((id) => ({ id, text: formatText(id) })),
     ...options,
   });
 }
@@ -112,6 +126,7 @@ export function tongueAndFaceAnalysis(options?: CreateNodeOptions) {
     iconBackground: '#d46b08',
     iconColor: '#fff',
     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,
   });
 }
@@ -122,6 +137,7 @@ export function healthAnalysis(options?: CreateNodeOptions) {
     iconBackground: '#d46b08',
     iconColor: '#fff',
     forbidSource: [ID_Report_Alcohol, ID_Report_Health, ID_Scheme_Health],
+    quickNodes: [ID_Report_Health, ID_Scheme_Health].map((id) => ({ id, text: formatText(id) })),
     ...options,
   });
 }
@@ -131,7 +147,7 @@ export function pulseAnalysisReport(options?: CreateNodeOptions) {
     icon: 'report',
     iconBackground: '#08979c',
     iconColor: '#fff',
-    forbidDirectTarget: [ID_End],
+    forbidDirectTarget: [ID_Back],
     ...options,
   });
 }
@@ -141,7 +157,8 @@ export function tongueAndFaceAnalysisReport(options?: CreateNodeOptions) {
     icon: 'report',
     iconBackground: '#08979c',
     iconColor: '#fff',
-    forbidDirectTarget: [ID_End],
+    configurable: true,
+    forbidDirectTarget: [ID_Back],
     ...options,
   });
 }
@@ -151,8 +168,8 @@ export function healthAnalysisReport(options?: CreateNodeOptions) {
     icon: 'report',
     iconBackground: '#08979c',
     iconColor: '#fff',
-    finish: true,
     configurable: true,
+    onlyTarget: [ID_End],
     ...options,
   });
 }
@@ -162,8 +179,8 @@ export function healthAnalysisScheme(options?: CreateNodeOptions) {
     icon: 'report',
     iconBackground: '#08979c',
     iconColor: '#fff',
-    finish: true,
     configurable: true,
+    onlyTarget: [ID_End],
     ...options,
   });
 }

+ 4 - 3
src/pages/aio/flow-config/nodes/index.ts

@@ -14,16 +14,17 @@ export {
 export * as Node from './config';
 
 export interface FlowNodeProperties extends IRectNodeProperties {
-  icon?: 'start' | 'config' | 'report' | 'pulse' | 'tongue' | 'questionnaire';
+  icon?: 'start' | 'finish' | 'config' | 'report' | 'pulse' | 'tongue' | 'questionnaire';
   iconBackground?: string;
   iconColor?: string;
 
+  forbidDirectSource?: string[];
   forbidDirectTarget?: string[];
   forbidSource?: string[];
   onlySource?: string[];
+  onlyTarget?: string[];
 
-  start?: boolean;
-  finish?: boolean;
+  quickNodes?: {id: string; text: string}[];
 
   configurable?: boolean;
   requestData?: Record<string, any>;

+ 8 - 3
src/pages/aio/flow-config/panel/StartPanel.vue

@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import { Preset_Image_1, Preset_Image_2 } from '../tool';
+
 interface Model {
   copyright?: string;
 
@@ -8,8 +10,10 @@ interface Model {
   homeValue?: string;
 }
 
-const Preset_Image_1 = `preset:1;el:scan|btn;`;
-const Preset_Image_2 = `preset:2;el:scan;`;
+const copyrightPlaceholder = `\
+某某机构
+杭州六智科技提供技术支持\
+`
 
 const props = defineProps<{ id: string }>();
 const requestData = defineModel<{ partner?: string; technicalSupporter?: string; tabletDrainageImage?: string }>('requestData', { default: {} });
@@ -114,7 +118,8 @@ const updateHomeType = () => {
       </div>
     </a-form-item>
     <a-form-item label="版权信息">
-      <a-textarea v-model:value="model.copyright" placeholder="最多显示两行文本" :auto-size="{ minRows: 2, maxRows: 2 }" />
+      <template #help>最多显示两行文本</template>
+      <a-textarea v-model:value="model.copyright" :placeholder="copyrightPlaceholder" :auto-size="{ minRows: 2, maxRows: 2 }" />
     </a-form-item>
     <div class="flex items-center" style="margin-top: 10px">
       <div class="flex-auto error">{{ error }}</div>

+ 14 - 7
src/pages/aio/flow-config/tool.ts

@@ -1,5 +1,6 @@
 import {
   alcoholAnalysisReport,
+  back,
   edge,
   end,
   healthAnalysis,
@@ -8,6 +9,7 @@ import {
   ID_Analysis_Health,
   ID_Analysis_Pulse,
   ID_Analysis_TongueAndFace,
+  ID_Back,
   ID_End,
   ID_Register,
   ID_Report_Alcohol,
@@ -33,24 +35,24 @@ const nodeRef = {
   [ID_Analysis_TongueAndFace]: /*拍照页*/ 'tongueface_upload',
   [ID_Analysis_Health]: /* 问诊页 */ 'tongueface_analysis',
   [ID_Report_Pulse]: /* 脉诊结果页 */ 'pulse_upload_result',
-  [ID_Report_TongueAndFace]:  /* 舌面分析报告页 */'tongueface_analysis_result',
+  [ID_Report_TongueAndFace]:  /* 舌面分析报告页 */'tongueface_analysis_result',
   [ID_Report_Health]: /* 健康报告页 */ 'health_analysis',
   [ID_Scheme_Health]: /* 调理方案页 */ 'health_analysis_scheme',
   [ID_Report_Alcohol]: /* 酒精结果页 */ 'alcohol_upload_result',
 } as const;
 
 export type NodeId = keyof typeof nodeRef;
-export type Gather = { level: number; sourceNodeId: NodeId; targetNodeId: NodeId; edgeId?: string }[];
+export type Gather = { level: number; sourceNodeId: NodeId; targetNodeId: NodeId | typeof ID_Back; edgeId: string }[];
 
 const refNode = ((ref) => Object.keys(ref).reduce((record, key) => ((record[ref[<NodeId>key]] = <NodeId>key), record), {} as Record<string, NodeId>))(nodeRef);
 
 export function toFlowRequestData(gather: Gather, data: Record<string, any>) {
   const value: string[] = [];
-  const _gather = [...gather].sort((g1, g2) => g1.level - g2.level || +(g1.targetNodeId === ID_End) - +(g2.targetNodeId === ID_End));
-  for (const { targetNodeId } of _gather) {
-    if (targetNodeId === ID_End) {
+  for (let i = 0; i < gather.length; i++) {
+    const { targetNodeId } = gather[i];
+    if (targetNodeId === ID_Back && i !== gather.length - 1) {
       value.push(value.pop()!.replace(/\?*$/, '?'));
-    } else value.push(nodeRef[targetNodeId]);
+    } else if (targetNodeId !== ID_Back && targetNodeId !== ID_End) value.push(nodeRef[targetNodeId]);
   }
   const getReportRequestData = (key: string) => data[key]?.elements ?? [];
   return {
@@ -74,6 +76,9 @@ export type FlowRequestData = Partial<ReturnType<typeof toFlowRequestData>>;
 
 type ReportPrefixKey = 'tongueface_upload_report_page' | 'health_analysis_report_page' | 'health_analysis_scheme_page';
 
+export const Preset_Image_1 = `preset:1;el:btn;`;
+export const Preset_Image_2 = `preset:2;el:scan;`;
+
 export function fromFlowRequestData(data?: FlowRequestData) {
   const getReportRequestData = (key: ReportPrefixKey) => ({
     key,
@@ -90,6 +95,7 @@ export function fromFlowRequestData(data?: FlowRequestData) {
       healthAnalysisReport({ requestData: getReportRequestData('health_analysis_report_page') }),
       healthAnalysisScheme({ requestData: getReportRequestData('health_analysis_scheme_page') }),
     ],
+    [back()],
   ];
 
   const nodes = new Set<string>([ID_Start, ID_End]);
@@ -98,6 +104,7 @@ export function fromFlowRequestData(data?: FlowRequestData) {
   if (flow.length && flow[0] === nodeRef[ID_Start]) flow.shift();
   if (flow.length && flow[flow.length - 1] === nodeRef[ID_End]) flow.pop();
   flow.unshift(ID_Start);
+  flow.push(ID_End);
   for (let i = 1; i < flow.length; i++) {
     const [source] = flow[i - 1].split(/[?:]/).filter(Boolean);
     const [target, title, countDown] = flow[i].split(/[?:]/).filter(Boolean);
@@ -120,7 +127,7 @@ export function fromFlowRequestData(data?: FlowRequestData) {
             requestData: {
               partner: data?.partner,
               technicalSupporter: data?.technicalSupporter,
-              tabletDrainageImage: data?.tabletDrainageImage,
+              tabletDrainageImage: data?.tabletDrainageImage || Preset_Image_1,
             },
           });
         if (id === ID_End) return end();

+ 21 - 0
src/polyfill.ts

@@ -0,0 +1,21 @@
+if (typeof Promise.withResolvers !== 'function') {
+  Promise.withResolvers = function <T>() {
+    let resolve!: (value: T | PromiseLike<T>) => void;
+    let reject!: (reason?: any) => void;
+
+    const promise = new Promise<T>((res, rej) => {
+      resolve = res;
+      reject = rej;
+    });
+
+    return { promise, resolve, reject };
+  };
+}
+
+if (typeof Array.prototype.at !== 'function') {
+  Array.prototype.at = function (index: number) {
+    return index < 0 ? this[this.length + index] : this[index];
+  };
+}
+
+export {};

+ 2 - 1
src/tools/promise.ts

@@ -1,3 +1,4 @@
+// @ts-nocheck
 export function withResolvers<T, R = any>(): {
   promise: Promise<T>;
   resolve: (value: T | PromiseLike<T>) => void;
@@ -16,6 +17,6 @@ export function withResolvers<T, R = any>(): {
       return { promise, resolve, reject };
     };
     withResolvers = fn;
+    return fn();
   }
-  return fn();
 }

Some files were not shown because too many files changed in this diff