Переглянути джерело

feature: circle和rhombic

后羿 1 рік тому
батько
коміт
320ad99ba2

+ 29 - 3
src/views/svg/components/bar.vue

@@ -1,15 +1,34 @@
 <template>
   <div class="svg-bar">
     <div class="item">
-      <div class="rect"></div>
+      <div
+        class="rect"
+        @mousedown="($event) => createNode($event, { type: 'rect' })"
+      ></div>
     </div>
     <div class="item">
-      <div class="circle"></div>
+      <div
+        class="circle"
+        @mousedown="($event) => createNode($event, { type: 'circle' })"
+      ></div>
+    </div>
+    <div class="item">
+      <div
+        class="rhombic"
+        @mousedown="($event) => createNode($event, { type: 'rhombic' })"
+      ></div>
     </div>
   </div>
 </template>
 
-<script setup></script>
+<script setup>
+import { defineEmits } from "vue";
+const emits = defineEmits(["createRect", "createNode"]);
+
+function createNode(e, config) {
+  emits("createNode", e, config);
+}
+</script>
 
 <style lang="less" scoped>
 .svg-bar {
@@ -45,6 +64,13 @@
       border-radius: 22px;
       cursor: pointer;
     }
+    .rhombic {
+      width: 40px;
+      height: 40px;
+      border: 2px solid #666;
+      cursor: pointer;
+      transform: rotate(45deg) scale(0.8);
+    }
   }
 }
 </style>

+ 150 - 0
src/views/svg/components/item.mixin.js

@@ -0,0 +1,150 @@
+import {
+  defineProps,
+  ref,
+  watch,
+  defineExpose,
+  defineEmits,
+  computed,
+} from "vue";
+import { sticky, countPointByPos } from "../utils/index";
+
+export function useFlowItem(props, emits) {
+  const nodePos = ref(props.pos);
+  const nodeSize = ref(props.size);
+  const isFocus = computed(() => props.id === props.curNodeId);
+
+  const rectAttr = computed(() => {
+    return {
+      x: props.pos.x,
+      y: props.pos.y,
+      width: props.size.width,
+      height: props.size.height,
+    };
+  });
+
+  const outlineRectAttr = computed(() => {
+    const space = 8;
+    return {
+      x: props.pos.x - space,
+      y: props.pos.y - space,
+      width: props.size.width + space * 2,
+      height: props.size.height + space * 2,
+    };
+  });
+
+  const circleAttr = computed(() => {
+    const r = props.size.width / 2;
+    return {
+      cx: props.pos.x + r,
+      cy: props.pos.y + r,
+      r,
+    };
+  });
+
+  const outlineCircleAttr = computed(() => {
+    const space = 8;
+    const r = props.size.width / 2;
+    return {
+      cx: props.pos.x + r,
+      cy: props.pos.y + r,
+      r: r + space,
+    };
+  });
+
+  const rhombicAttr = computed(() => {
+    const { x, y } = props.pos;
+    const { width: w, height: h } = props.size;
+    return {
+      points: [
+        [x, y + h / 2].join(","),
+        [x + w / 2, y].join(","),
+        [x + w, y + h / 2].join(","),
+        [x + w / 2, y + h].join(","),
+      ].join(" "),
+    };
+  });
+
+  const outlineRhombicAttr = computed(() => {
+    const space = 8;
+    const { x, y } = props.pos;
+    const { width: w, height: h } = props.size;
+    return {
+      points: [
+        [x - space, y + h / 2].join(","),
+        [x + w / 2, y - space].join(","),
+        [x + w + space, y + h / 2].join(","),
+        [x + w / 2, y + h + space].join(","),
+      ].join(" "),
+    };
+  });
+
+  const pointsAttr = computed(() => {
+    return [
+      {
+        pos: "top",
+        ...countPointByPos("top", props.pos, props.size),
+      },
+      {
+        pos: "right",
+        ...countPointByPos("right", props.pos, props.size),
+      },
+      {
+        pos: "bottom",
+        ...countPointByPos("bottom", props.pos, props.size),
+      },
+      {
+        pos: "left",
+        ...countPointByPos("left", props.pos, props.size),
+      },
+    ];
+  });
+
+  function handleMousedown(e) {
+    emits("startDrag", {
+      id: props.id,
+      event: e,
+      innerPos: {
+        x: e.offsetX - props.pos.x,
+        y: e.offsetY - props.pos.y,
+      },
+    });
+  }
+
+  function startLine(e, point) {
+    e.stopPropagation();
+    emits("startLine", {
+      id: props.id,
+      point: point,
+    });
+  }
+
+  function pointMouseEnter(p) {
+    if (props.startPoint?.targetId !== props.id) {
+      emits("endLine", {
+        point: p,
+        id: props.id,
+      });
+    }
+  }
+
+  function pointMouseLeave() {
+    emits("cancelEndLine");
+  }
+
+  return {
+    nodePos,
+    nodeSize,
+    isFocus,
+    rectAttr,
+    outlineRectAttr,
+    circleAttr,
+    outlineCircleAttr,
+    pointsAttr,
+    rhombicAttr,
+    outlineRhombicAttr,
+    handleMousedown,
+    startLine,
+    pointMouseEnter,
+    pointMouseLeave,
+  };
+}

+ 97 - 0
src/views/svg/components/svgCircle.vue

@@ -0,0 +1,97 @@
+<template>
+  <g :id="id" class="node-wrap" @mousedown="handleMousedown">
+    <circle
+      class="node-outline"
+      v-bind="outlineCircleAttr"
+      :class="{ focus: isFocus }"
+    />
+    <circle class="node" v-bind="circleAttr" />
+    <circle
+      v-for="i in pointsAttr"
+      v-bind="i"
+      :key="i.pos"
+      class="point"
+      :class="{ focus: isFocus }"
+      @mouseenter="pointMouseEnter(i)"
+      @mouseleave="pointMouseLeave(i)"
+      @mousedown="($event) => startLine($event, i)"
+    />
+  </g>
+</template>
+
+<script setup>
+import { defineProps } from "vue";
+import { useFlowItem } from "./item.mixin";
+const props = defineProps([
+  "pos",
+  "size",
+  "startPoint",
+  "id",
+  "curNodeId",
+  "isDragging",
+]);
+const emits = defineEmits([
+  "startDrag",
+  "startLine",
+  "endLine",
+  "cancelEndLine",
+]);
+
+const {
+  nodePos,
+  nodeSize,
+  innerPos,
+  isFocus,
+  circleAttr,
+  outlineCircleAttr,
+  pointsAttr,
+  handleMousedown,
+  startLine,
+  pointMouseEnter,
+  pointMouseLeave,
+} = useFlowItem(props, emits);
+</script>
+
+<style lang="less" scoped>
+.node-wrap {
+  &:hover {
+    .node-outline {
+      display: block;
+    }
+    .point {
+      stroke: rgba(0, 0, 0, 1);
+      fill: rgba(255, 255, 255, 1);
+    }
+  }
+
+  .node-outline {
+    fill: rgba(255, 255, 255, 0);
+    stroke-dasharray: 3 4;
+    stroke: rgba(0, 0, 0, 1);
+    display: none;
+    &.focus {
+      display: block;
+    }
+  }
+
+  .node {
+    fill: rgba(255, 255, 255, 0.6);
+    stroke: #666;
+    rx: 15;
+  }
+  .point {
+    r: 6;
+    stroke: rgba(255, 255, 255, 0);
+    fill: rgba(255, 255, 255, 0);
+    transform: all 0.3s;
+    &.focus {
+      stroke: rgba(0, 0, 0, 1);
+      fill: rgba(255, 255, 255, 1);
+    }
+    &:hover {
+      stroke: #5ba8ff;
+      stroke-width: 4;
+    }
+  }
+}
+</style>

+ 60 - 0
src/views/svg/components/svgLines.vue

@@ -0,0 +1,60 @@
+<template>
+  <g class="lines-wrap">
+    <marker
+      id="markerArrow"
+      markerUnits="strokeWidth"
+      markerWidth="5"
+      markerHeight="4"
+      refX="6"
+      refY="2"
+      orient="auto"
+    >
+      <path d="M 0 0 L 5 2 L 0 4 z" class="trangle" />
+    </marker>
+    <line
+      v-for="i in linesAttr"
+      :key="i.id"
+      v-bind="i"
+      class="line"
+      marker-end="url(#markerArrow)"
+    />
+  </g>
+</template>
+
+<script setup>
+import { defineProps, computed } from "vue";
+import { countPointByPos } from "../utils/index";
+const props = defineProps(["nodes", "lines"]);
+
+const linesAttr = computed(() =>
+  props.lines.map((line) => {
+    const { targetId: startTargetId, pos: startPos } = line.start;
+    const { targetId: endTargetId, pos: endPos } = line.end;
+    const startNode = props.nodes.find((i) => i.id === startTargetId);
+    const endNode = props.nodes.find((i) => i.id === endTargetId);
+    const endPoint = countPointByPos(endPos, endNode.pos, endNode.size);
+    const startPoint = countPointByPos(startPos, startNode.pos, startNode.size);
+    return {
+      id: `${startTargetId}-${startPos}-${endTargetId}-${endPos}`,
+      x1: startPoint.cx,
+      y1: startPoint.cy,
+      x2: endPoint.cx,
+      y2: endPoint.cy,
+    };
+  })
+);
+</script>
+
+<style lang="less" scoped>
+.lines-wrap {
+  .line {
+    stroke: #5ba8ff;
+    stroke-width: 2;
+  }
+
+  .trangle {
+    stroke: #5ba8ff;
+    fill: #5ba8ff;
+  }
+}
+</style>

+ 16 - 89
src/views/svg/components/svgRect.vue

@@ -5,7 +5,7 @@
       v-bind="outlineRectAttr"
       :class="{ focus: isFocus }"
     />
-    <rect class="node" v-bind="attr" />
+    <rect class="node" v-bind="rectAttr" />
     <circle
       v-for="i in pointsAttr"
       v-bind="i"
@@ -20,15 +20,8 @@
 </template>
 
 <script setup>
-import {
-  defineProps,
-  ref,
-  watch,
-  defineExpose,
-  defineEmits,
-  computed,
-} from "vue";
-import { sticky, countPointByPos } from "../utils/index";
+import { defineProps } from "vue";
+import { useFlowItem } from "./item.mixin";
 const props = defineProps([
   "pos",
   "size",
@@ -43,86 +36,20 @@ const emits = defineEmits([
   "endLine",
   "cancelEndLine",
 ]);
-const nodePos = ref(props.pos);
-const nodeSize = ref(props.size);
-const innerPos = ref({ x: 0, y: 0 });
-const isFocus = computed(() => props.id === props.curNodeId);
 
-const attr = computed(() => {
-  return {
-    x: props.pos.x,
-    y: props.pos.y,
-    width: props.size.width,
-    height: props.size.height,
-  };
-});
-
-const outlineRectAttr = computed(() => {
-  const space = 8;
-  return {
-    x: props.pos.x - space,
-    y: props.pos.y - space,
-    width: props.size.width + space * 2,
-    height: props.size.height + space * 2,
-  };
-});
-
-const pointsAttr = computed(() => {
-  return [
-    {
-      pos: "top",
-      ...countPointByPos("top", props.pos, props.size),
-    },
-    {
-      pos: "right",
-      ...countPointByPos("right", props.pos, props.size),
-    },
-    {
-      pos: "bottom",
-      ...countPointByPos("bottom", props.pos, props.size),
-    },
-    {
-      pos: "left",
-      ...countPointByPos("left", props.pos, props.size),
-    },
-  ];
-});
-
-function handleMousedown(e) {
-  emits("startDrag", {
-    id: props.id,
-    event: e,
-    innerPos: {
-      x: e.offsetX - props.pos.x,
-      y: e.offsetY - props.pos.y,
-    },
-  });
-  innerPos.value = {
-    x: e.offsetX - props.pos.x,
-    y: e.offsetY - props.pos.y,
-  };
-}
-
-function startLine(e, point) {
-  e.stopPropagation();
-  emits("startLine", {
-    id: props.id,
-    point: point,
-  });
-}
-
-function pointMouseEnter(p) {
-  if (props.startPoint?.targetId !== props.id) {
-    emits("endLine", {
-      point: p,
-      id: props.id,
-    });
-  }
-}
-
-function pointMouseLeave() {
-  emits("cancelEndLine");
-}
+const {
+  nodePos,
+  nodeSize,
+  innerPos,
+  isFocus,
+  rectAttr,
+  outlineRectAttr,
+  pointsAttr,
+  handleMousedown,
+  startLine,
+  pointMouseEnter,
+  pointMouseLeave,
+} = useFlowItem(props, emits);
 </script>
 
 <style lang="less" scoped>

+ 98 - 0
src/views/svg/components/svgRhombic.vue

@@ -0,0 +1,98 @@
+<template>
+  <g :id="id" class="node-wrap" @mousedown="handleMousedown">
+    <polygon
+      class="node-outline"
+      v-bind="outlineRhombicAttr"
+      :class="{ focus: isFocus }"
+    />
+    <polygon class="node" v-bind="rhombicAttr" />
+    <circle
+      v-for="i in pointsAttr"
+      v-bind="i"
+      :key="i.pos"
+      class="point"
+      :class="{ focus: isFocus }"
+      @mouseenter="pointMouseEnter(i)"
+      @mouseleave="pointMouseLeave(i)"
+      @mousedown="($event) => startLine($event, i)"
+    />
+  </g>
+</template>
+
+<script setup>
+import { defineProps } from "vue";
+import { useFlowItem } from "./item.mixin";
+const props = defineProps([
+  "pos",
+  "size",
+  "startPoint",
+  "id",
+  "curNodeId",
+  "isDragging",
+]);
+const emits = defineEmits([
+  "startDrag",
+  "startLine",
+  "endLine",
+  "cancelEndLine",
+]);
+
+const {
+  nodePos,
+  nodeSize,
+  innerPos,
+  isFocus,
+  rhombicAttr,
+  outlineRhombicAttr,
+  pointsAttr,
+  handleMousedown,
+  startLine,
+  pointMouseEnter,
+  pointMouseLeave,
+} = useFlowItem(props, emits);
+</script>
+
+<style lang="less" scoped>
+.node-wrap {
+  &:hover {
+    .node-outline {
+      display: block;
+    }
+    .point {
+      stroke: rgba(0, 0, 0, 1);
+      fill: rgba(255, 255, 255, 1);
+    }
+  }
+
+  .node-outline {
+    fill: rgba(255, 255, 255, 0);
+    stroke-dasharray: 3 4;
+    stroke: rgba(0, 0, 0, 1);
+    display: none;
+    rx: 18;
+    &.focus {
+      display: block;
+    }
+  }
+
+  .node {
+    fill: rgba(255, 255, 255, 0.6);
+    stroke: #666;
+    rx: 15;
+  }
+  .point {
+    r: 6;
+    stroke: rgba(255, 255, 255, 0);
+    fill: rgba(255, 255, 255, 0);
+    transform: all 0.3s;
+    &.focus {
+      stroke: rgba(0, 0, 0, 1);
+      fill: rgba(255, 255, 255, 1);
+    }
+    &:hover {
+      stroke: #5ba8ff;
+      stroke-width: 4;
+    }
+  }
+}
+</style>

+ 99 - 72
src/views/svg/index.vue

@@ -2,7 +2,7 @@
 <!-- eslint-disable vue/valid-v-for -->
 <template>
   <div ref="wrapRef" class="svg-wrap" @mousemove="handleMouseMove">
-    <Bar />
+    <Bar @create-node="createNode" />
     <svg
       version="1.1"
       baseProfile="full"
@@ -12,37 +12,21 @@
       :width="wrapSize.width"
       :height="wrapSize.height"
     >
-      <marker
-        id="markerArrow"
-        markerUnits="strokeWidth"
-        markerWidth="5"
-        markerHeight="4"
-        refX="6"
-        refY="2"
-        orient="auto"
-      >
-        <path d="M 0 0 L 5 2 L 0 4 z" class="trangle" />
-      </marker>
-      <line
-        v-for="i in linesAttr"
-        :key="i.id"
-        v-bind="i"
-        class="line"
-        marker-end="url(#markerArrow)"
-      />
-      <SvgRect
-        v-for="i in children"
-        :key="i.id"
-        v-bind="i"
-        :isDragging="isDragging"
-        :curNodeId="curNodeId"
-        :startPoint="lineStartPoint"
-        @click="nodeClick"
-        @startLine="startLine"
-        @endLine="endLine"
-        @cancelEndLine="cancelEndLine"
-        @startDrag="startDrag"
-      />
+      <SvgLines :lines="lines" :nodes="nodes" />
+      <template v-for="i in nodes" :key="i.id">
+        <component
+          :is="getComponentType(i.type)"
+          v-bind="i"
+          :isDragging="isDragging"
+          :curNodeId="curNodeId"
+          :startPoint="lineStartPoint"
+          @click="nodeClick"
+          @startLine="startLine"
+          @endLine="endLine"
+          @cancelEndLine="cancelEndLine"
+          @startDrag="startDrag"
+        ></component>
+      </template>
       <line v-if="lineStartPoint" v-bind="dashLineAttr" class="dash-line" />
     </svg>
   </div>
@@ -51,6 +35,9 @@
 <script setup>
 import { reactive, ref, onBeforeUnmount, onMounted, computed } from "vue";
 import SvgRect from "./components/svgRect.vue";
+import SvgCircle from "./components/svgCircle.vue";
+import SvgRhombic from "./components/svgRhombic.vue";
+import SvgLines from "./components/svgLines.vue";
 import Bar from "./components/bar.vue";
 import {
   findFromArray,
@@ -69,31 +56,43 @@ const curNodeConfig = ref({});
 const isDragging = ref(false);
 const lineStartPoint = ref(null);
 const lineEndPoint = ref(null);
-const lines = reactive([]);
-const children = ref([
+const lines = reactive([
+  { start: { targetId: 1, pos: "right" }, end: { targetId: 2, pos: "left" } },
+  { start: { targetId: 2, pos: "right" }, end: { targetId: 3, pos: "left" } },
+]);
+const nodes = ref([
   {
     id: 1,
-    pos: { x: 160, y: 110 },
+    pos: { x: 200, y: 200 },
+    type: "rect",
     size: { width: 100, height: 100 },
   },
   {
     id: 2,
-    pos: { x: 170, y: 120 },
-    size: { width: 100, height: 100 },
+    type: "rhombic",
+    pos: { x: 350, y: 210 },
+    size: { width: 120, height: 80 },
   },
   {
     id: 3,
-    pos: { x: 180, y: 130 },
-    size: { width: 100, height: 100 },
+    pos: { x: 550, y: 220 },
+    type: "circle",
+    size: { width: 60, height: 60 },
   },
 ]);
 
 const curNode = computed(() => {
   return curNodeId.value !== 0
-    ? children.value.find((i) => i.id === curNodeId.value)
+    ? nodes.value.find((i) => i.id === curNodeId.value)
     : null;
 });
 
+function getComponentType(type) {
+  return (
+    { circle: SvgCircle, rect: SvgRect, rhombic: SvgRhombic }[type] ?? SvgRect
+  );
+}
+
 const dashLineAttr = computed(() => {
   return lineStartPoint.value
     ? {
@@ -105,24 +104,6 @@ const dashLineAttr = computed(() => {
     : {};
 });
 
-const linesAttr = computed(() =>
-  lines.map((line) => {
-    const { targetId: startTargetId, pos: startPos } = line.start;
-    const { targetId: endTargetId, pos: endPos } = line.end;
-    const startNode = children.value.find((i) => i.id === startTargetId);
-    const endNode = children.value.find((i) => i.id === endTargetId);
-    const endPoint = countPointByPos(endPos, endNode.pos, endNode.size);
-    const startPoint = countPointByPos(startPos, startNode.pos, startNode.size);
-    return {
-      id: `${startTargetId}-${startPos}-${endTargetId}-${endPos}`,
-      x1: startPoint.cx,
-      y1: startPoint.cy,
-      x2: endPoint.cx,
-      y2: endPoint.cy,
-    };
-  })
-);
-
 function startDrag({ id, event, innerPos }) {
   isDragging.value = true;
   curNodeId.value = id;
@@ -133,9 +114,62 @@ function startDrag({ id, event, innerPos }) {
   lineEndPoint.value = null;
   // 移动当前的节点最后渲染
   curMousePos.value = getPosFromEvent(event, wrapRef.value);
-  const { index, result } = findFromArray(children.value, (i) => i.id === id);
-  children.value.splice(index, 1);
-  children.value.push(result);
+  const { index, result } = findFromArray(nodes.value, (i) => i.id === id);
+  nodes.value.splice(index, 1);
+  nodes.value.push(result);
+}
+
+function createNode(e, config) {
+  switch (config.type) {
+    case "rect":
+      config.width = 100;
+      config.height = 100;
+      break;
+    case "circle":
+      config.width = 60;
+      config.height = 60;
+      break;
+    case "rhombic":
+      config.width = 120;
+      config.height = 80;
+      break;
+  }
+  const newNode = {
+    id: nodes.value.length + 1,
+    type: config.type,
+    pos: {
+      x: curMousePos.value.x - config.width / 2,
+      y: curMousePos.value.y - config.height / 2,
+    },
+    size: { width: config.width, height: config.height },
+  };
+  nodes.value.push(newNode);
+  isDragging.value = true;
+  curNodeId.value = newNode.id;
+  curNodeConfig.value = {
+    innerPos: {
+      x: config.width / 2,
+      y: config.height / 2,
+    },
+  };
+}
+
+function createCircle() {
+  const newCircle = {
+    id: nodes.value.length + 1,
+    type: "circle",
+    pos: { x: curMousePos.value.x - 50, y: curMousePos.value.y - 50 },
+    size: { width: 50, height: 50 },
+  };
+  nodes.value.push(newCircle);
+  isDragging.value = true;
+  curNodeId.value = newCircle.id;
+  curNodeConfig.value = {
+    innerPos: {
+      x: 50,
+      y: 50,
+    },
+  };
 }
 
 function startLine({ id, point }) {
@@ -164,7 +198,6 @@ function cancelEndLine() {
 
 function handleMouseMove(e) {
   curMousePos.value = getPosFromEvent(e, wrapRef.value);
-  console.log(curMousePos.value);
   if (curNode.value && isDragging.value) {
     curNode.value.pos = {
       x: sticky(curMousePos.value.x - curNodeConfig.value.innerPos.x),
@@ -226,6 +259,7 @@ onBeforeUnmount(() => {
 
 <style lang="less" scoped>
 .svg-wrap {
+  height: 100%;
   min-height: 400px;
   background-color: blue;
   background-image: linear-gradient(
@@ -237,6 +271,9 @@ onBeforeUnmount(() => {
     linear-gradient(90deg, #ccc 0px, #ccc 2px, #fff 2px, #fff 15px);
   background-size: 15px 15px, 15px 15px;
   position: relative;
+  * {
+    user-select: none;
+  }
   svg {
     display: block;
   }
@@ -246,15 +283,5 @@ onBeforeUnmount(() => {
     stroke-dasharray: 3 4;
     stroke-width: 2;
   }
-
-  .line {
-    stroke: #5ba8ff;
-    stroke-width: 2;
-  }
-
-  .trangle {
-    stroke: #5ba8ff;
-    fill: #5ba8ff;
-  }
 }
 </style>

+ 8 - 8
src/views/svg/utils/index.js

@@ -3,16 +3,16 @@ export function sticky(number, space = 5) {
 }
 
 export function getPosFromEvent(e, offsetParent) {
+  const svgRoot = offsetParent.querySelector("svg");
+  if (svgRoot.isSameNode(e.target) || svgRoot.contains(e.target)) {
+    return {
+      x: e.offsetX,
+      y: e.offsetY,
+    };
+  }
   const parentRect = offsetParent.getBoundingClientRect();
   const curRect = e.target.getBoundingClientRect();
-  console.log(e.target);
-  // console.log(e.target.getBoundingClientRect());
-  // if (dom.classList.contains("svg-bar")) {
-  //   return {
-  //     x: e.offsetX + dom.offsetLeft - dom.offsetWidth / 2,
-  //     y: e.offsetY + dom.offsetTop,
-  //   };
-  // }
+
   return {
     x: curRect.left - parentRect.left + e.offsetX,
     y: curRect.top - parentRect.top + e.offsetY,