bill 1 rok temu
rodzic
commit
b62ede3c71

+ 1 - 0
src/components/tex-tooltip.vue

@@ -43,6 +43,7 @@ window.addEventListener("resize", initSample);
 }
 .sample {
   display: -webkit-box;
+  cursor: pointer;
   max-height: 46px;
   overflow: hidden;
   text-overflow: ellipsis;

+ 6 - 0
src/router.ts

@@ -16,6 +16,12 @@ const routes: RouteRecordRaw[] = [
     component: () => import("@/view/login.vue"),
   },
   {
+    path: "/tree",
+    name: "query-tree",
+    meta: { title: "登录" },
+    component: () => import("@/view/step-tree/example/example.vue"),
+  },
+  {
     path: "/",
     name: "main-layout",
     component: () => import("@/view/layout/nav.vue"),

+ 4 - 2
src/util/pc4xlsl.ts

@@ -34,7 +34,9 @@ export const downloadPointsXLSL1 = async (
   const temp = await fetch("/templaten.xls").then((r) => r.arrayBuffer());
   const tabs = points.map((point, i) => {
     const des = desc[i] || { title: "无", desc: "无" };
-    return [i, des.title, toDegrees(point[1], 4), toDegrees(point[0], 4)];
+    return [i, des.title, toDegrees(point[1], 4), toDegrees(point[0], 4)].map(
+      (i) => i.toString()
+    );
   });
 
   await genXLSLByTemp(temp, tabs, name);
@@ -54,7 +56,7 @@ export const downloadPointsXLSL2 = async (
       round(point[2], 4),
       des.title,
       des.desc,
-    ];
+    ].map((i) => i.toString());
   });
 
   await genXLSLByTemp(temp, tabs, name);

+ 231 - 0
src/view/step-tree/example/data.ts

@@ -0,0 +1,231 @@
+export default [
+  {
+    yamlversion: "v1.0",
+    subStepsParallel: "False", // false 串行,True并行,表示子步骤step1、step2串行执行
+    steps: [
+      {
+        name: "step1",
+        displayName: "Stop All Services",
+        subStepsParallel: "True",
+        status: "success",
+        steps: [
+          {
+            name: "step1_1",
+            displayName: "Stop app1 Services",
+            type: "execution",
+            serviceType: "app1",
+            status: "stop",
+            serviceTypeParallel: "True",
+
+            steps: [
+              {
+                name: "step1_1_1",
+                displayName: "Stop app1 Services",
+                type: "execution",
+                serviceType: "app1",
+                status: "waiting",
+                serviceTypeParallel: "False",
+                steps: [
+                  {
+                    name: "step1_1_1_1",
+                    displayName: "Stop app1 Services",
+                    type: "execution",
+                    serviceType: "app1",
+                    status: "waiting",
+                    serviceTypeParallel: "False",
+                    hosts: [
+                      { host: "qladpax1", status: "success" },
+                      { host: "qladpax2", status: "lose" },
+                      { host: "qladpax3", status: "wating" },
+                    ],
+                  },
+                  {
+                    name: "step1_1_1_2",
+                    displayName: "Stop app1 Services",
+                    type: "execution",
+                    serviceType: "app1",
+                    status: "waiting",
+                    serviceTypeParallel: "False",
+                    hosts: [
+                      { host: "qladpax1", status: "success" },
+                      { host: "qladpax2", status: "lose" },
+                      { host: "qladpax3", status: "wating" },
+                    ],
+                  },
+                ],
+              },
+              {
+                name: "step1_1_2",
+                displayName: "Stop app1 Services",
+                type: "execution",
+                serviceType: "app1",
+                status: "waiting",
+                serviceTypeParallel: "True",
+                hosts: [
+                  { host: "qladpax1", status: "success" },
+                  { host: "qladpax2", status: "lose" },
+                  { host: "qladpax3", status: "wating" },
+                ],
+              },
+              {
+                name: "step1_1_3",
+                displayName: "Stop app1 Services",
+                type: "execution",
+                serviceType: "app1",
+                status: "waiting",
+                serviceTypeParallel: "True",
+              },
+              {
+                name: "step1_1_4",
+                displayName: "Stop app1 Services",
+                type: "execution",
+                serviceType: "app1",
+                status: "waiting",
+                serviceTypeParallel: "True",
+              },
+            ],
+          },
+          {
+            name: "step1_2",
+            displayName: "Stop app2_part1 Services",
+            type: "execution",
+            serviceType: "app2_part1",
+            status: "waiting",
+            serviceTypeParallel: "True",
+            steps: [
+              {
+                name: "step1_2_1",
+                displayName: "Stop app1 Services",
+                type: "execution",
+                serviceType: "app1",
+                status: "waiting",
+                serviceTypeParallel: "True",
+              },
+              {
+                name: "step1_2_2",
+                displayName: "Stop app1 Services",
+                type: "execution",
+                serviceType: "app1",
+                status: "waiting",
+                serviceTypeParallel: "True",
+                steps: [
+                  {
+                    name: "step1_2aaaa",
+                    displayName: "Stop app2_part1 Services",
+                    type: "execution",
+                    serviceType: "app2_part1",
+                    status: "waiting",
+                    serviceTypeParallel: "True",
+                    steps: [
+                      {
+                        name: "step1_2_1a",
+                        displayName: "Stop app1 Services",
+                        type: "execution",
+                        serviceType: "app1",
+                        status: "waiting",
+                        serviceTypeParallel: "True",
+                      },
+                      {
+                        name: "step1ccc",
+                        displayName: "Stop app1 Services",
+                        type: "execution",
+                        serviceType: "app1",
+                        status: "waiting",
+                        serviceTypeParallel: "True",
+                      },
+                    ],
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+      },
+      {
+        // name: "step2step2step2step2step2step2step2step2step2stestep2step2step2step2step2step2step2step2step2stestep2step2step2step2step2step2step2step2step2stestep2step2step2step2step2step2step2step2step2ste",
+        name: "step2",
+        displayName: "Stop All Services",
+        subStepsParallel: "True",
+        status: "waiting",
+        steps: [
+          {
+            name: "step2_1",
+            displayName: "Waiting",
+            type: "execution",
+            status: "waiting",
+          },
+          {
+            name: "step2_2",
+            displayName: "Waiting",
+            type: "execution",
+            status: "waiting",
+          },
+        ],
+      },
+      {
+        name: "step3",
+        displayName: "deploy All Services",
+        subStepsParallel: "True",
+        status: "waiting",
+        steps: [
+          {
+            name: "step3_1ssstep3",
+            displayName: "deploy app1 Services",
+            type: "execution",
+            serviceType: "app1",
+            status: "waiting",
+            serviceTypeParallel: "True",
+          },
+          {
+            name: "step3_1stes",
+            displayName: "deploy app1 Services",
+            type: "execution",
+            serviceType: "app1",
+            status: "waiting",
+            serviceTypeParallel: "True",
+          },
+          {
+            name: "step3_1s",
+            displayName: "deploy app1 Services",
+            type: "execution",
+            serviceType: "app1",
+            status: "waiting",
+            serviceTypeParallel: "True",
+          },
+          {
+            name: "step3_2",
+            displayName: "deploy app2_part1 Services",
+            type: "execution",
+            serviceType: "app2_part1",
+            status: "waiting",
+            serviceTypeParallel: "True",
+          },
+        ],
+      },
+      {
+        name: "step4",
+        displayName: "start All Services",
+        subStepsParallel: "False",
+        status: "waiting",
+        steps: [
+          {
+            name: "step4_1",
+            displayName: "start app1 Services",
+            type: "execution",
+            serviceType: "app1",
+            status: "waiting",
+            serviceTypeParallel: "True",
+          },
+          {
+            name: "step4_2",
+            displayName: "start app2_part1 Services",
+            type: "execution",
+            serviceType: "app2_part1",
+            status: "waiting",
+            serviceTypeParallel: "True",
+          },
+        ],
+      },
+    ],
+  },
+];

+ 65 - 0
src/view/step-tree/example/example.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="test">
+    <StepTree
+      :data="treeData"
+      :margin="[15, 15]"
+      :padding="[10, 10]"
+      :font-size="16"
+      :hostFontSize="10"
+      :hostMargin="[3, 3]"
+      :hostPadding="[3, 3]"
+      font-family="微软雅黑"
+      :custom-step-style="customStepStyle"
+      @step-click="stepClickHandler"
+      @step-host-click="stepHostClickHandler"
+      :line-gap="20"
+    />
+  </div>
+</template>
+
+<script setup>
+import data from "./data";
+import StepTree from "../step-tree.vue";
+
+const treeData = [{ name: "开始" }, ...data[0].steps, { name: "结束" }];
+
+const customStepStyle = (step) => {
+  if (step.action === "stop") {
+    return {
+      lineColor: "red",
+      lineWidth: 3,
+      textColor: "red",
+      rectBorderColor: "red",
+      rectBorderColor: "red",
+      rectBgColor: "#ccc",
+      rectRadius: 2,
+      rectBorderWidth: 1,
+    };
+  } else {
+    return {
+      lineColor: "#000",
+      lineWidth: 1,
+      textColor: "#000",
+      rectBorderColor: "#000",
+      rectBgColor: "#ffff",
+      rectRadius: 2,
+      rectBorderWidth: 1,
+    };
+  }
+};
+const stepClickHandler = (step) => {
+  console.log(step);
+};
+const stepHostClickHandler = (host) => {
+  console.log(host);
+};
+</script>
+
+<style scoped>
+.test {
+  width: 100%;
+  height: 100%;
+
+  overflow: auto;
+}
+</style>

+ 276 - 0
src/view/step-tree/helper.ts

@@ -0,0 +1,276 @@
+export type NStep = {
+  id: number;
+  parentIds: number[];
+  name: string;
+  bound: { width: number; height: number; left: number; top: number };
+  prevId: number;
+  raw: any;
+  level: number;
+};
+
+const idFactory = () => {
+  let id = 0;
+  return () => id++;
+};
+const getId = idFactory();
+
+const _flatSteps = (
+  steps: any,
+  parentIds: number[] = [],
+  level = 0,
+  parallel = false,
+  nsteps: NStep[] = []
+): number[] => {
+  const lonelyStepIds: number[] = [];
+
+  let tempLevel = level;
+  let tempParentIds = parentIds;
+
+  for (const step of steps) {
+    const id = getId();
+    const stepParallel = parallel;
+
+    if (!stepParallel && lonelyStepIds.length) {
+      tempParentIds = [...lonelyStepIds];
+      tempLevel =
+        Math.max(
+          ...nsteps
+            .filter((nstep) => lonelyStepIds.includes(nstep.id))
+            .map((nstep) => nstep.level)
+        ) + 1;
+      lonelyStepIds.length = 0;
+    }
+
+    let tempPrevId = -1;
+    for (let i = nsteps.length - 1; i >= 0; i--) {
+      if (nsteps[i].level === tempLevel) {
+        tempPrevId = nsteps[i].id;
+        break;
+      }
+    }
+
+    const nstep = {
+      id,
+      name: step.name,
+      parentIds: tempParentIds,
+      prevId: tempPrevId,
+      raw: step,
+      level: tempLevel,
+    } as NStep;
+    nsteps.push(nstep);
+
+    if (step.steps && step.steps.length) {
+      lonelyStepIds.push(
+        ..._flatSteps(
+          step.steps as any,
+          [id],
+          tempLevel + 1,
+          step.subStepsParallel === "True" ||
+            step.serviceTypeParallel === "True",
+          nsteps
+        )
+      );
+    } else {
+      lonelyStepIds.push(nstep.id);
+    }
+  }
+
+  return lonelyStepIds;
+};
+
+type Size = { width: number; height: number };
+export const attachBoundAttrib = (
+  steps: NStep[],
+  getStepSize: (step: any) => { width: number; height: number }
+) => {
+  steps.sort((a, b) => a.level - b.level);
+  const stepSizeMap = new Map<NStep, Size>();
+  for (const step of steps) {
+    const size = getStepSize(step.raw);
+    stepSizeMap.set(step, size);
+  }
+
+  const treeSizeMap = new Map<NStep, Size>();
+  const levelHeights: number[] = new Array(
+    steps[steps.length - 1].level + 1
+  ).fill(0);
+  for (let i = steps.length - 1; i >= 0; i--) {
+    const root = steps[i];
+    const child = steps.filter(
+      (oStep) =>
+        oStep.parentIds.length === 1 &&
+        oStep.parentIds.some((id) => root.id === id)
+    );
+
+    const rootBound = stepSizeMap.get(steps[i])!;
+    const topLevel = root.level;
+    let treeWidth = rootBound.width;
+    let treeHeight = rootBound.height;
+    levelHeights[topLevel] = Math.max(levelHeights[topLevel], treeHeight);
+
+    if (child.length) {
+      const bottomLevel = child[child.length - 1]?.level;
+      for (let i = topLevel; i <= bottomLevel; i++) {
+        let width = 0;
+        let height = 0;
+        for (let j = 0; j < child.length; j++) {
+          if (child[j].level === i) {
+            const childTreeSize = treeSizeMap.get(child[j])!;
+            width += childTreeSize.width;
+            height = Math.max(childTreeSize.height + rootBound.height, height);
+          }
+        }
+        treeWidth = Math.max(treeWidth, width);
+        treeHeight = Math.max(treeHeight, height);
+      }
+    }
+
+    treeSizeMap.set(root, { width: treeWidth, height: treeHeight });
+  }
+
+  const levelsSteps: NStep[][] = [];
+  let level = 0;
+  while (true) {
+    const levelSteps = steps.filter((step) => step.level === level);
+    if (levelSteps.length === 0) {
+      break;
+    } else {
+      levelsSteps[level] = levelSteps;
+    }
+    level++;
+  }
+
+  const getStepOffset = (step: NStep) => {
+    const stepBound = stepSizeMap.get(step)!;
+    const treeBound = treeSizeMap.get(step)!;
+
+    let offset = 0;
+    let prevId = step.prevId;
+    while (prevId !== -1) {
+      const prevStep = steps.find((tstep) => tstep.id === prevId)!;
+      const treeBound = treeSizeMap.get(prevStep)!;
+      offset += treeBound.width;
+      prevId = prevStep.prevId;
+    }
+    const prevStep = steps.find((tstep) => tstep.id === step.prevId)!;
+    if (prevStep) {
+      offset = Math.max(prevStep.bound.left + prevStep.bound.width, offset);
+    }
+
+    let left = offset + (treeBound.width - stepBound.width) / 2;
+
+    // 如果超出预设范围则修正
+    if (step.parentIds.length === 1 && step.parentIds[0] !== -1) {
+      if (step.parentIds[0] !== -1) {
+        const parent = steps.find((pstep) => pstep.id === step.parentIds[0])!;
+        const paretnBound = treeSizeMap.get(parent)!;
+        if (parent.bound.left - left > paretnBound.width / 2) {
+          // const width = stepBound.width;
+          const width = steps
+            .filter((step) => step.parentIds.includes(parent.id))
+            .reduce((t, c) => t + stepSizeMap.get(c)!.width, 0);
+
+          left = Math.max(
+            left,
+            parent.bound.left + (parent.bound.width - width) / 2
+          );
+        }
+      }
+    }
+
+    return left;
+  };
+
+  let top = 0;
+  let width = 0;
+  for (let i = 0; i < levelsSteps.length; i++) {
+    let levelHeight = levelHeights[i];
+    for (const step of levelsSteps[i]) {
+      const stepSize = stepSizeMap.get(step)!;
+      step.bound = {
+        left: getStepOffset(step),
+        top: top + (levelHeight - stepSize.height) / 2,
+        ...stepSize,
+      };
+
+      width = Math.max(step.bound.left + step.bound.width, width);
+    }
+    top += levelHeight;
+  }
+
+  const getParentBound = (step: NStep) => {
+    const parentSteps = step.parentIds.map(
+      (id) => steps.find((parentStep) => parentStep.id === id)!
+    );
+    let left = 0;
+    let top = 0;
+    let width = 0,
+      height = 0;
+    for (const step of parentSteps) {
+      left = Math.min(step.bound.left, left);
+      top = Math.min(step.bound.top, top);
+      width += step.bound.width;
+      height += step.bound.height;
+    }
+    return {
+      width,
+      height,
+      left,
+      top,
+    };
+  };
+  const getCompleteTree = (root: NStep) => {
+    const completeTree = [root];
+    const completeTreeIds = [root.id];
+    const levelsChilds = levelsSteps.slice(root.level);
+    for (const childs of levelsChilds) {
+      for (const step of childs) {
+        if (step.parentIds.some((id) => completeTreeIds.includes(id))) {
+          completeTree.push(step);
+          completeTreeIds.push(step.id);
+        }
+      }
+    }
+    return completeTree;
+  };
+  // 偏移所有多父级树
+  for (let i = 0; i < levelsSteps.length; i++) {
+    for (const step of levelsSteps[i]) {
+      if (step.parentIds.length <= 1) continue;
+      const parentBound = getParentBound(step);
+      const offset = (parentBound.width - treeSizeMap.get(step)!.width) / 2;
+      getCompleteTree(step).forEach((ctStep) => {
+        ctStep.bound.left += offset;
+      });
+    }
+  }
+
+  let left = 0,
+    right = 0;
+  for (let i = 0; i < steps.length; i++) {
+    left = Math.min(steps[i].bound.left, left);
+    right = Math.max(steps[i].bound.left + steps[i].bound.width, right);
+  }
+  return { left, right, top: 0, bottom: top, levelHeights };
+};
+
+export const flatSteps = (data: any) => {
+  const nsteps: NStep[] = [];
+  _flatSteps(data, [], 0, false, nsteps);
+  return nsteps;
+};
+
+const ctx = document.createElement("canvas").getContext("2d")!;
+export const getTextBound = (
+  text: string,
+  padding: number[],
+  margin: number[],
+  font: string
+) => {
+  ctx.font = font;
+  const textMetrics = ctx.measureText(text);
+  const width = textMetrics.width + (padding[1] + margin[1]) * 2;
+  const height = textMetrics.hangingBaseline + (padding[0] + margin[0]) * 2;
+
+  return { width, height };
+};

+ 161 - 0
src/view/step-tree/step-tree.vue

@@ -0,0 +1,161 @@
+<template>
+  <!-- :style="{ width: svgAttrib.viewBox[2] + 'px', height: svgAttrib.viewBox[3] + 'px' }" -->
+
+  <svg
+    :viewBox="svgAttrib.viewBox.join(' ')"
+    v-if="svgAttrib"
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <Step
+      v-for="step in steps"
+      :key="step.id"
+      v-bind="getStepAttrib(step)"
+      @click="emit('stepClick', step)"
+      @click-host="(data) => emit('stepHostClick', data)"
+    />
+  </svg>
+</template>
+
+<script lang="ts" setup>
+import { computed } from "vue";
+import { flatSteps, attachBoundAttrib, NStep, getTextBound } from "./helper";
+import Step from "./step.vue";
+
+const props = withDefaults(
+  defineProps<{
+    margin?: number[];
+    padding?: number[];
+    fontSize?: number;
+    data: any;
+    fontFamily?: string;
+    hostFontSize?: number;
+    hostMargin?: number[];
+    hostPadding?: number[];
+    customStepStyle?: (step: any) => {};
+    lineGap: number;
+  }>(),
+  {
+    margin: () => [10, 10],
+    padding: () => [10, 10],
+    hostMargin: () => [2, 2],
+    hostPadding: () => [2, 2],
+    hostFontSize: 10,
+    lineGap: 5,
+    fontSize: 14,
+    fontFamily: "sans-serif",
+  }
+);
+
+const emit = defineEmits<{
+  (e: "stepClick", data: any): void;
+  (e: "stepHostClick", host: any): void;
+}>();
+
+const getStepSize = (step: any) => {
+  const size = getTextBound(
+    step.name,
+    props.padding,
+    props.margin,
+    `${props.fontSize}px normal ${props.fontFamily}`
+  );
+
+  if (step.hosts?.length) {
+    const hostSize = step.hosts.reduce(
+      (t: any, host: any) => {
+        const size = getTextBound(
+          host.host,
+          props.hostPadding,
+          props.hostMargin,
+          `${props.hostFontSize}px normal ${props.fontFamily}`
+        );
+        t.width += size.width;
+        t.height = Math.max(t.height, size.height);
+        host.bound = size;
+        return t;
+      },
+      { width: 0, height: 0 }
+    );
+
+    step.hostSize = hostSize;
+    size.width = Math.max(
+      size.width,
+      hostSize.width + (props.padding[1] + props.margin[1]) * 2
+    );
+    console.log(hostSize, size);
+    size.height += hostSize.height;
+  }
+
+  return size;
+};
+
+const steps = computed(() => flatSteps(props.data));
+const bound = computed(() => attachBoundAttrib(steps.value, getStepSize));
+
+const svgAttrib = computed(() => {
+  if (!bound.value) return null;
+  const { left, right, top, bottom } = bound.value;
+  return {
+    viewBox: [left, top, right - left, bottom - top],
+  };
+});
+
+const lineGap = computed(() => Math.min(props.lineGap, props.margin[0]));
+
+const getStepLines = (step: NStep) => {
+  if (!step.parentIds.length) return [];
+  const start = [
+    step.bound.left + step.bound.width / 2,
+    step.bound.top + props.margin[0],
+  ];
+  const points = [];
+  for (let parentId of step.parentIds) {
+    const parent = steps.value.find((step) => step.id === parentId)!;
+    const end = [
+      parent.bound.left + parent.bound.width / 2,
+      parent.bound.top + parent.bound.height - props.margin[0],
+    ];
+    const startLevelHeight = bound.value.levelHeights[step.level];
+    // const parentLevelHeight = bound.value.levelHeights[parent.level];
+    const offset = lineGap.value + (startLevelHeight - step.bound.height) / 2;
+
+    points.push([
+      ...start,
+      start[0],
+      start[1] - offset,
+      end[0],
+      start[1] - offset,
+      ...end,
+    ]);
+  }
+  return points;
+};
+
+const defaultStyle = {
+  lineColor: "#000",
+  lineWidth: 1,
+  textColor: "#000",
+  rectBorderColor: "#000",
+  rectBgColor: "#ffff",
+  rectRadius: 2,
+  rectBorderWidth: 1,
+};
+
+const getStepAttrib = (step: NStep) => {
+  let style = defaultStyle;
+  if (props.customStepStyle) {
+    style = { ...defaultStyle, ...props.customStepStyle(step.raw) };
+  }
+  return {
+    style,
+    step,
+    margin: props.margin,
+    padding: props.padding,
+    fontSize: props.fontSize,
+    hostMargin: props.hostMargin,
+    hostPadding: props.hostPadding,
+    hostFontSize: props.hostFontSize,
+    fontFamily: props.fontFamily,
+    lines: getStepLines(step),
+  };
+};
+</script>

+ 132 - 0
src/view/step-tree/step.vue

@@ -0,0 +1,132 @@
+<template>
+  <rect
+    @click="emit('click')"
+    v-bind="rectBound"
+    :rx="style.rectRadius"
+    :ry="style.rectRadius"
+    :fill="style.rectBgColor"
+    :stroke="style.rectBorderColor"
+    :stroke-width="style.rectBorderWidth"
+  >
+  </rect>
+
+  <template v-if="step.raw.hosts">
+    <rect
+      v-for="(_, index) in step.raw.hosts"
+      :x="hostBounds[index].x"
+      :y="hostBounds[index].y"
+      :width="hostBounds[index].width"
+      :height="hostBounds[index].height"
+      :rx="style.rectRadius"
+      :ry="style.rectRadius"
+      :fill="style.rectBgColor"
+      :stroke="style.rectBorderColor"
+      :stroke-width="style.rectBorderWidth"
+      @click="emit('clickHost', step.raw.hosts[index])"
+    >
+    </rect>
+
+    <text
+      v-for="(hostTex, i) in hostTextAttribs"
+      @click="emit('clickHost', step.raw.hosts[i])"
+      :font-family="fontFamily"
+      :fill="style.textColor"
+      dominant-baseline="middle"
+      :font-size="hostFontSize"
+      text-anchor="middle"
+      v-bind="hostTex"
+    >
+      {{ step.raw.hosts[i].host }}
+    </text>
+  </template>
+  <text
+    @click="emit('click')"
+    :font-family="fontFamily"
+    :fill="style.textColor"
+    :font-size="fontSize"
+    dominant-baseline="middle"
+    text-anchor="middle"
+    v-bind="textAttrib"
+  >
+    {{ step.name }}
+  </text>
+
+  <template v-if="lines.length">
+    <polyline
+      v-for="line in lines"
+      :points="line.join(',')"
+      fill="none"
+      :stroke="style.lineColor"
+      :stroke-width="style.lineWidth"
+    />
+  </template>
+</template>
+
+<script setup lang="ts">
+import { computed } from "vue";
+import { NStep } from "./helper";
+
+const props = defineProps<{
+  step: NStep;
+  margin: number[];
+  padding: number[];
+  fontSize: number;
+  fontFamily: string;
+  lines: number[][];
+  hostMargin: number[];
+  hostPadding: number[];
+  hostFontSize: number;
+  style: {
+    lineColor: string;
+    lineWidth: number;
+    textColor: string;
+    rectBorderColor: string;
+    rectBorderWidth: number;
+    rectBgColor: string;
+    rectRadius: number;
+  };
+}>();
+
+const emit = defineEmits<{ (e: "click"): void; (e: "clickHost", host: any): void }>();
+
+const rectBound = computed(() => ({
+  x: props.step.bound.left + props.margin[1],
+  y: props.step.bound.top + props.margin[0],
+  width: props.step.bound.width - props.margin[1] * 2,
+  height: props.step.bound.height - props.margin[0] * 2,
+}));
+const textAttrib = computed(() => ({
+  x: rectBound.value.x + rectBound.value.width / 2,
+  y: rectBound.value.y + props.padding[0] + props.fontSize / 2,
+}));
+
+const hostBounds = computed(() => {
+  console.log(
+    rectBound.value.width - props.padding[1] * 2 - props.step.raw.hostSize.width
+  );
+  let left =
+    rectBound.value.x +
+    props.padding[1] +
+    (rectBound.value.width - props.padding[1] * 2 - props.step.raw.hostSize.width) / 2;
+  let bottom = rectBound.value.y + rectBound.value.height;
+
+  const hosts = props.step.raw.hosts;
+  return hosts.map((host: any) => {
+    const x = left + props.hostMargin[1];
+    const y = bottom - host.bound.height + props.hostMargin[0];
+    left += host.bound.width;
+    return {
+      x,
+      y,
+      width: host.bound.width - props.hostMargin[1] * 2,
+      height: host.bound.height - props.hostMargin[0] * 2,
+    };
+  });
+});
+const hostTextAttribs = computed(() =>
+  hostBounds.value.map((hostBound: any) => ({
+    x: hostBound.x + hostBound.width / 2,
+    y: hostBound.y + props.hostPadding[0] + props.hostFontSize / 2,
+  }))
+);
+</script>