Browse Source

feature: svg拖拽,任务组件

xiongxt 1 year ago
parent
commit
3a99c64897
4 changed files with 258 additions and 25 deletions
  1. 132 4
      src/views/svg/components/svgRect.vue
  2. 99 12
      src/views/svg/index.vue
  3. 18 0
      src/views/svg/utils/index.js
  4. 9 9
      vue.config.js

+ 132 - 4
src/views/svg/components/svgRect.vue

@@ -1,8 +1,136 @@
 <template>
-  <g>
-    <rect x="0" y="100" width="100" height="100" fill="#59fa81" />
-    <rect x="100" y="0" width="100" height="100" fill="#59fa81" />
+  <g @mousedown="handleMousedown" :id="id">
+    <rect v-bind="outlineRectAttr" />
+    <rect v-bind="attr" />
+    <circle
+      v-for="i in pointsAttr"
+      v-bind="i"
+      :key="i.id"
+      @mousedown="($event) => startLine($event, i)"
+    ></circle>
   </g>
 </template>
 
-<script setup></script>
+<script setup>
+import {
+  defineProps,
+  ref,
+  watch,
+  defineExpose,
+  defineEmits,
+  computed,
+} from "vue";
+import { sticky } from "../utils/index";
+const props = defineProps(["pos", "size", "mousePos", "id", "draggingId"]);
+const boxPos = ref(props.pos);
+const boxSize = ref(props.size);
+const innerPos = ref({ x: 0, y: 0 });
+const isDraging = computed(() => props.id === props.draggingId);
+const emits = defineEmits(["startDrag", "startLine", "updatePos"]);
+
+const attr = computed(() => {
+  return {
+    x: boxPos.value.x,
+    y: boxPos.value.y,
+    width: boxSize.value.width,
+    height: boxSize.value.height,
+    fill: "rgba(255,255,255,1)",
+    stroke: "#666",
+    rx: "15",
+  };
+});
+
+const outlineRectAttr = computed(() => {
+  const space = 8;
+  return {
+    x: boxPos.value.x - space,
+    y: boxPos.value.y - space,
+    width: boxSize.value.width + space * 2,
+    height: boxSize.value.height + space * 2,
+    fill: "rgba(255,255,255,0)",
+    "stroke-dasharray": "3 4",
+    stroke: "#000",
+    rx: "18",
+  };
+});
+
+const pointsAttr = computed(() => [
+  {
+    id: `${props.id}-top`,
+    cx: boxPos.value.x + boxSize.value.width / 2,
+    cy: boxPos.value.y,
+    r: 4,
+    stroke: "#000",
+    fill: "rgba(255,255,255,1)",
+  },
+  {
+    id: `${props.id}-right`,
+    cx: boxPos.value.x + boxSize.value.width,
+    cy: boxPos.value.y + boxSize.value.height / 2,
+    r: 4,
+    stroke: "#000",
+    fill: "rgba(255,255,255,1)",
+  },
+  {
+    id: `${props.id}-bottom`,
+    cx: boxPos.value.x + boxSize.value.width / 2,
+    cy: boxPos.value.y + boxSize.value.height,
+    r: 4,
+    stroke: "#000",
+    fill: "rgba(255,255,255,1)",
+  },
+  {
+    id: `${props.id}-left`,
+    cx: boxPos.value.x,
+    cy: boxPos.value.y + boxSize.value.height / 2,
+    r: 4,
+    stroke: "#000",
+    fill: "rgba(255,255,255,1)",
+  },
+]);
+
+function handleMousedown(e) {
+  emits("startDrag", {
+    id: props.id,
+    event: e,
+  });
+  innerPos.value = {
+    x: e.offsetX - boxPos.value.x,
+    y: e.offsetY - boxPos.value.y,
+  };
+}
+
+function startLine(e, point) {
+  e.stopPropagation();
+  emits("startLine", {
+    id: props.id,
+    point: point,
+  });
+}
+
+function setPos(pos) {
+  boxPos.value = {
+    x: sticky(pos.x - innerPos.value.x),
+    y: sticky(pos.y - innerPos.value.y),
+  };
+  emits("updatePos", {
+    id: props.id,
+    pos: boxPos.value,
+  });
+}
+
+watch(
+  () => props.mousePos,
+  (pos) => {
+    if (isDraging.value) {
+      setPos(pos);
+    }
+  }
+);
+</script>
+
+<style lang="less" scoped>
+circle {
+  cursor: pointer;
+}
+</style>

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

@@ -1,31 +1,114 @@
+<!-- eslint-disable vue/no-unused-vars -->
+<!-- eslint-disable vue/valid-v-for -->
 <template>
-  <div class="flow-wrap">
-    <!-- <svg
+  <div ref="wrapRef" class="svg-wrap" @mousemove="handleMouseMove">
+    <svg
       version="1.1"
       baseProfile="full"
       xmlns="http://www.w3.org/2000/svg"
       xmlns:xlink="http://www.w3.org/1999/xlink"
       xmlns:ev="http://www.w3.org/2001/xml-events"
+      :width="wrapSize.width"
+      :height="wrapSize.height"
     >
-      <rect x="0" y="0" width="10" height="10" fill="blue" />
-      <text x="5" y="30">A nice rectangle</text>
-      <circle cx="50" cy="50" r="50" fill="#529fca" />
-      <polygon
-        points="9.9, 2.2, 3.3, 21.78, 19.8, 8.58, 0, 8.58, 16.5, 21.78"
+      <SvgRect
+        v-for="i in children"
+        :key="i.id"
+        v-bind="i"
+        :draggingId="draggingId"
+        :mousePos="curPos"
+        @startLine="startLine"
+        @startDrag="startDrag"
+        @updatePos="updatePos"
       />
 
-      <SvgRect />
-    </svg> -->
+      <line v-if="lineStartPoint" v-bind="dashLineAttr"></line>
+    </svg>
   </div>
 </template>
 
 <script setup>
+import { reactive, ref, onBeforeUnmount, onMounted, computed } from "vue";
 import SvgRect from "./components/svgRect.vue";
+import { findFromArray, getPosFromEvent } from "./utils/index";
+
+const wrapRef = ref(null);
+const childrenRefs = ref(null);
+let curPos = ref({ x: 0, y: 0 });
+const wrapSize = reactive({ width: 0, height: 0 });
+const draggingId = ref(0);
+const lineStartPoint = ref(null);
+const children = ref([
+  { id: 1, pos: { x: 10, y: 10 }, size: { width: 100, height: 100 } },
+  { id: 2, pos: { x: 20, y: 20 }, size: { width: 100, height: 100 } },
+  { id: 3, pos: { x: 30, y: 30 }, size: { width: 100, height: 100 } },
+]);
+
+const dashLineAttr = computed(() => {
+  if (lineStartPoint.value) {
+    return {
+      x1: lineStartPoint.value?.cx,
+      y1: lineStartPoint.value?.cy,
+      x2: curPos.value.x,
+      y2: curPos.value.y,
+      stroke: "black",
+      "stroke-dasharray": "3 4",
+      "stroke-width": "2",
+    };
+  }
+  return {};
+});
+
+function startDrag({ id, event }) {
+  draggingId.value = id;
+  lineStartPoint.value = null;
+  curPos.value = getPosFromEvent(event);
+  const { index, result } = findFromArray(children.value, (i) => i.id === id);
+  children.value.splice(index, 1);
+  children.value.push(result);
+}
+
+function startLine({ id, point }) {
+  draggingId.value = 0;
+  lineStartPoint.value = point;
+}
+
+function updatePos({ id, pos }) {
+  const { result } = findFromArray(children.value, (i) => i.id === id);
+  result.pos = pos;
+}
+
+function handleMouseMove(e) {
+  curPos.value = getPosFromEvent(e);
+}
+
+function handleMouseUp() {
+  draggingId.value = 0;
+  lineStartPoint.value = null;
+}
+
+function initSvgSize() {
+  // 避免因为转rem时有小数误差导致得滚动条
+  wrapSize.width = wrapRef.value.offsetWidth - 1;
+  wrapSize.height = wrapRef.value.offsetHeight - 1;
+}
+
+window.addEventListener("mouseup", handleMouseUp);
+window.addEventListener("resize", initSvgSize);
+
+onMounted(() => {
+  initSvgSize();
+});
+
+onBeforeUnmount(() => {
+  window.removeEventListener("mouseup", handleMouseUp);
+  window.removeEventListener("resize", initSvgSize);
+});
 </script>
 
 <style lang="less" scoped>
-.flow-wrap {
-  height: 45px;
+.svg-wrap {
+  min-height: 400px;
   background-color: blue;
   background-image: linear-gradient(
       transparent 0px,
@@ -33,7 +116,11 @@ import SvgRect from "./components/svgRect.vue";
       #fff 2px,
       #fff 15px
     ),
-    linear-gradient(90deg, #999 0px, #999 2px, #fff 2px, #fff 15px);
+    linear-gradient(90deg, #ccc 0px, #ccc 2px, #fff 2px, #fff 15px);
   background-size: 15px 15px, 15px 15px;
+  position: relative;
+  svg {
+    display: block;
+  }
 }
 </style>

+ 18 - 0
src/views/svg/utils/index.js

@@ -0,0 +1,18 @@
+export function sticky(number, space = 5) {
+  return Math.round(number / space) * space;
+}
+
+export function getPosFromEvent(e) {
+  return {
+    x: e.offsetX,
+    y: e.offsetY,
+  };
+}
+
+export function findFromArray(array, filter) {
+  const index = array.findIndex(filter);
+  return {
+    index,
+    result: array[index],
+  };
+}

+ 9 - 9
vue.config.js

@@ -28,15 +28,15 @@ module.exports = defineConfig({
               ],
               grid: true,
             }),
-            // require("postcss-pxtorem")({
-            //   rootValue: 140, //设计稿宽度%10 比如 1920
-            //   exclude: /(node_module)/, //默认false,可以(reg)利用正则表达式排除某些文件夹的方法,例如/(node_module|src)/
-            //   propList: ["*"], //是一个存储哪些将被转换的属性列表,这里设置为["*"]全部,假设需要仅对边框进行设置,可以写]['*','!border*']
-            //   //selectorBlackList :['.box'],//,那例如fs-xl类名,里面有关px的样式将不被转换,这里也支持正则写法。
-            //   replace: true, //替换包含rems的规则。
-            //   mediaQuery: false, //(布尔值)允许在媒体查询中转换px。
-            //   minPixelValue: 0, //设置要替换的最小像素值(3px会被转rem)。 默认 0
-            // }),
+            require("postcss-pxtorem")({
+              rootValue: 140, //设计稿宽度%10 比如 1920
+              exclude: /(node_module)/, //默认false,可以(reg)利用正则表达式排除某些文件夹的方法,例如/(node_module|src)/
+              propList: ["*"], //是一个存储哪些将被转换的属性列表,这里设置为["*"]全部,假设需要仅对边框进行设置,可以写]['*','!border*']
+              selectorBlackList: [/svg/], //,那例如fs-xl类名,里面有关px的样式将不被转换,这里也支持正则写法。
+              replace: true, //替换包含rems的规则。
+              mediaQuery: false, //(布尔值)允许在媒体查询中转换px。
+              minPixelValue: 0, //设置要替换的最小像素值(3px会被转rem)。 默认 0
+            }),
           ],
         },
       },