Browse Source

拖拽v1.0.0

CzRger 3 months ago
parent
commit
f3bb2f165a

+ 0 - 2
package.json

@@ -11,7 +11,6 @@
   },
   "dependencies": {
     "@types/node": "^20.17.11",
-    "@vueuse/core": "^13.1.0",
     "axios": "^1.7.9",
     "default-passive-events": "^2.0.0",
     "echarts": "^5.6.0",
@@ -32,7 +31,6 @@
     "vite-plugin-top-level-await": "^1.4.4",
     "vue": "^3.5.13",
     "vue-router": "^4.5.0",
-    "vuedraggable": "^4.1.0"
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "^5.2.1",

+ 182 - 0
src/views/draw/FlowChart.vue

@@ -0,0 +1,182 @@
+<template>
+  <div class="flowchart-container" ref="container">
+    <div class="flowchart" :style="flowchartStyle">
+      <FlowNode
+        v-for="node in nodes"
+        :key="node.id"
+        :node="node"
+        @add-child="handleAddChild"
+        @node-drag="handleNodeDrag"
+      />
+      <svg class="connectors">
+        <path
+          v-for="connector in connectors"
+          :key="connector.id"
+          :d="connector.path"
+          stroke="#999"
+          stroke-width="2"
+          fill="none"
+          marker-end="url(#arrowhead)"
+        />
+      </svg>
+    </div>
+    <defs>
+      <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
+        <polygon points="0 0, 10 3.5, 0 7" fill="#999" />
+      </marker>
+    </defs>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, watch } from 'vue'
+import { useDraggable } from '@vueuse/core'
+import FlowNode from './FlowNode.vue'
+
+// 节点类型定义
+const nodeTypes = {
+  start: { name: '开始', color: '#4CAF50' },
+  process: { name: '处理', color: '#2196F3' },
+  decision: { name: '判断', color: '#FFC107' },
+  end: { name: '结束', color: '#F44336' }
+}
+
+// 初始节点数据
+const nodes = ref([
+  {
+    id: '1',
+    type: 'start',
+    x: 300,
+    y: 50,
+    children: ['2']
+  },
+  {
+    id: '2',
+    type: 'process',
+    x: 300,
+    y: 150,
+    parent: '1'
+  }
+])
+
+// 容器拖拽
+const container = ref(null)
+// 修改容器拖拽初始化
+const { x: containerX, y: containerY } = useDraggable(container, {
+  onStart: (e) => {
+    // 只有当点击的不是节点时才允许拖拽容器
+    return !e.target.closest('.flow-node')
+  }
+})
+
+// 修改节点拖拽处理
+const handleNodeDrag = (nodeId, newX, newY) => {
+  const nodeIndex = nodes.value.findIndex(n => n.id === nodeId)
+  if (nodeIndex !== -1) {
+    // 创建新数组确保响应性
+    const newNodes = [...nodes.value]
+    newNodes[nodeIndex] = {
+      ...newNodes[nodeIndex],
+      x: newX,
+      y: newY
+    }
+    nodes.value = newNodes
+  }
+}
+const flowchartStyle = computed(() => ({
+  transform: `translate(${containerX.value}px, ${containerY.value}px)`
+}))
+
+// 计算连接线
+const connectors = computed(() => {
+  const result = []
+  nodes.value.forEach(node => {
+    if (node.children) {
+      node.children.forEach(childId => {
+        const childNode = nodes.value.find(n => n.id === childId)
+        if (childNode) {
+          result.push({
+            id: `${node.id}-${childId}`,
+            path: calculatePath(node, childNode)
+          })
+        }
+      })
+    }
+  })
+  return result
+})
+
+function calculatePath(startNode, endNode) {
+  const startX = startNode.x + 100
+  const startY = startNode.y + 40
+  const endX = endNode.x + 100
+  const endY = endNode.y
+
+  // 简单的贝塞尔曲线路径
+  const controlY = (startY + endY) / 2
+  return `M${startX},${startY} C${startX},${controlY} ${endX},${controlY} ${endX},${endY}`
+}
+
+// 添加子节点
+function handleAddChild(parentId, nodeType) {
+  const parentNode = nodes.value.find(n => n.id === parentId)
+  if (!parentNode) return
+
+  const newNodeId = Date.now().toString()
+  const newNode = {
+    id: newNodeId,
+    type: nodeType,
+    x: parentNode.x,
+    y: parentNode.y + 120,
+    parent: parentId
+  }
+
+  if (!parentNode.children) {
+    parentNode.children = []
+  }
+  parentNode.children.push(newNodeId)
+
+  nodes.value.push(newNode)
+}
+// 自动排列
+function autoLayout() {
+  // 这里可以实现自动排列算法,如树状布局等
+  // 简化版:简单垂直排列
+  nodes.value.forEach((node, index) => {
+    if (index > 0) {
+      node.x = 300
+      node.y = 50 + index * 120
+    }
+  })
+}
+
+// 初始化时自动排列
+onMounted(() => {
+  autoLayout()
+})
+</script>
+
+<style>
+.flowchart-container {
+  width: 100%;
+  height: 100vh;
+  overflow: hidden;
+  cursor: grab;
+}
+
+.flowchart {
+  position: relative;
+  width: max-content;
+  height: max-content;
+}
+
+.connectors {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+  z-index: 0;
+}
+</style>

+ 158 - 0
src/views/draw/FlowNode.vue

@@ -0,0 +1,158 @@
+<template>
+  <div
+    class="flow-node"
+    :style="nodeStyle"
+    ref="nodeElement"
+    @mousedown="startDrag"
+  >
+    <div class="node-header" :style="headerStyle">
+      {{ nodeData.name }}
+    </div>
+    <div class="node-content">
+      <slot></slot>
+    </div>
+    <div class="node-footer">
+      <button
+        v-for="type in availableChildTypes"
+        :key="type"
+        @click.stop="addChild(type)"
+        class="add-child-btn"
+      >
+        添加{{ nodeTypes[type].name }}节点
+      </button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue'
+
+const props = defineProps({
+  node: {
+    type: Object,
+    required: true
+  }
+})
+
+const emit = defineEmits(['add-child', 'node-drag'])
+
+const nodeTypes = {
+  start: { name: '开始', color: '#4CAF50' },
+  process: { name: '处理', color: '#2196F3' },
+  decision: { name: '判断', color: '#FFC107' },
+  end: { name: '结束', color: '#F44336' }
+}
+
+const nodeElement = ref(null)
+const isDragging = ref(false)
+const dragStartPos = ref({ x: 0, y: 0 })
+const nodeStartPos = ref({ x: 0, y: 0 })
+
+const nodeData = computed(() => ({
+  ...props.node,
+  ...nodeTypes[props.node.type]
+}))
+
+const nodeStyle = computed(() => ({
+  left: `${props.node.x}px`,
+  top: `${props.node.y}px`,
+  borderColor: nodeData.value.color
+}))
+
+const headerStyle = computed(() => ({
+  backgroundColor: nodeData.value.color
+}))
+
+const availableChildTypes = computed(() => {
+  if (props.node.type === 'start') return ['process', 'decision']
+  if (props.node.type === 'process') return ['process', 'decision', 'end']
+  if (props.node.type === 'decision') return ['process', 'end']
+  return []
+})
+
+function addChild(type) {
+  emit('add-child', props.node.id, type)
+}
+
+function startDrag(e) {
+  if (e.target.closest('.add-child-btn')) return
+
+  isDragging.value = true
+  dragStartPos.value = {
+    x: e.clientX,
+    y: e.clientY
+  }
+  nodeStartPos.value = {
+    x: props.node.x,
+    y: props.node.y
+  }
+
+  document.addEventListener('mousemove', handleDrag)
+  document.addEventListener('mouseup', stopDrag)
+  e.preventDefault()
+}
+
+function handleDrag(e) {
+  if (!isDragging.value) return
+
+  const dx = e.clientX - dragStartPos.value.x
+  const dy = e.clientY - dragStartPos.value.y
+
+  const newX = nodeStartPos.value.x + dx
+  const newY = nodeStartPos.value.y + dy
+
+  emit('node-drag', props.node.id, newX, newY)
+}
+
+function stopDrag() {
+  isDragging.value = false
+  document.removeEventListener('mousemove', handleDrag)
+  document.removeEventListener('mouseup', stopDrag)
+}
+</script>
+
+<style>
+.flow-node {
+  position: absolute;
+  width: 200px;
+  background: white;
+  border: 2px solid;
+  border-radius: 4px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+  z-index: 1;
+  cursor: move;
+  user-select: none;
+}
+
+.node-header {
+  padding: 8px 12px;
+  color: white;
+  font-weight: bold;
+  border-top-left-radius: 2px;
+  border-top-right-radius: 2px;
+}
+
+.node-content {
+  padding: 12px;
+}
+
+.node-footer {
+  padding: 8px;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.add-child-btn {
+  padding: 4px 8px;
+  background: #f5f5f5;
+  border: 1px solid #ddd;
+  border-radius: 3px;
+  cursor: pointer;
+  font-size: 12px;
+}
+
+.add-child-btn:hover {
+  background: #e0e0e0;
+}
+</style>

+ 189 - 0
src/views/draw/chart/index.vue

@@ -0,0 +1,189 @@
+<template>
+  <div class="chart"
+    @mousedown="moveStart"
+    @mousemove="move"
+    @mouseup="moveEnd"
+    @mouseleave="moveEnd"
+  >
+    <div class="chart-block" ref="ref_chart">
+      <template v-for="n in state.nodes">
+        <node
+          :type="n.type"
+          v-model:x="n.x"
+          v-model:y="n.y"
+          v-model:w="n.w"
+          v-model:h="n.h"
+        />
+      </template>
+      <svg class="lines" :style="{width: offsetStyleCpt.width + 'px', height: offsetStyleCpt.height + 'px', left: offsetStyleCpt.left + 'px', top: offsetStyleCpt.top + 'px'}">
+        <defs>
+          <marker
+            id="arrow"
+            :viewBox="`0 0 ${markerStyle.width} ${markerStyle.width}`"
+            :refX="0"
+            :refY="markerStyle.height"
+            :markerWidth="markerStyle.width"
+            :markerHeight="markerStyle.height"
+            orient="auto-start-reverse"
+          >
+            <path :d="`M 0 0 L ${markerStyle.width} ${markerStyle.height} L 0 ${markerStyle.width} z`" fill="#d0d5dc" />
+          </marker>
+        </defs>
+        <!-- 绘制曲线连线 -->
+        <path
+          v-for="(line, index) in state.lines"
+          :key="index"
+          :d="getPath(line)"
+          stroke="#d0d5dc"
+          stroke-width="2"
+          fill="none"
+          marker-end="url(#arrow)"
+        />
+      </svg>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {computed, getCurrentInstance, onMounted, reactive, ref} from "vue";
+import node from '../node/index.vue'
+
+type Node = {
+  id: string
+  type: string
+  x: number
+  y: number
+  w: number
+  h: number
+}
+type Line = {
+  source: string
+  target: string
+}
+
+const {proxy} = getCurrentInstance()
+const markerStyle = {
+  width: 10,
+  height: 5,
+}
+const state: any = reactive({
+  moving: false,
+  nodes: [
+    {
+      id: '8facce0d-c5ff-42ba-b6a0-6a920510c794',
+      type: 'root',
+      x: 0,
+      y: 0,
+      w: 240,
+      h: 50,
+    },
+    {
+      id: 'f001c786-c4a0-402f-a79d-19456f9346a6',
+      type: 'type-1',
+      x: 300,
+      y: 0,
+      w: 240,
+      h: 100,
+    },
+    {
+      id: 'e7dacb7d-8505-4bf7-abae-91e81d27106e',
+      type: 'type-1',
+      x: 300,
+      y: 200,
+      w: 240,
+      h: 200,
+    }
+  ] as Node[],
+  lines: [
+    {source: '8facce0d-c5ff-42ba-b6a0-6a920510c794', target: 'f001c786-c4a0-402f-a79d-19456f9346a6'},
+    {source: '8facce0d-c5ff-42ba-b6a0-6a920510c794', target: 'e7dacb7d-8505-4bf7-abae-91e81d27106e'},
+  ] as Line[]
+})
+const ref_chart = ref()
+const offsetStyleCpt = computed(() => {
+  let maxW = 0
+  let minW = 0
+  let maxH = 0
+  let minH = 0
+  state.nodes.forEach((v: Node) => {
+    if (v.x < minW) {
+      minW = v.x
+    }
+    if (v.x + v.w > maxW) {
+      maxW = v.x + v.w
+    }
+    if (v.y < minH) {
+      minH = v.y
+    }
+    if (v.y + v.h > maxH) {
+      maxH = v.y + v.h
+    }
+  })
+  return {
+    width: maxW - minW - 40,
+    height: maxH - minH,
+    left: minW - 40,
+    top: minH
+  }
+})
+const moveStart = (e) => {
+  state.moving = true
+}
+const move = (e) => {
+  if (state.moving) {
+    const dom = ref_chart.value
+    const x = dom.style.left ? Number(dom.style.left.replace('px', '')) : 0
+    const y = dom.style.top ? Number(dom.style.top.replace('px', '')) : 0
+    dom.style.left = `${x + e.movementX}px`
+    dom.style.top = `${y + e.movementY}px`
+  }
+}
+const moveEnd = (e) => {
+  state.moving = false
+}
+// 计算连线路径
+const getPath = (line) => {
+  const sourceNode = state.nodes.find(node => node.id === line.source);
+  const targetNode = state.nodes.find(node => node.id === line.target);
+  const startX = sourceNode.x + sourceNode.w - offsetStyleCpt.value.left
+  const startY = sourceNode.y + sourceNode.h / 2 - offsetStyleCpt.value.top;
+  const endX = targetNode.x - offsetStyleCpt.value.left - markerStyle.width;
+  const endY = targetNode.y + targetNode.h / 2 - offsetStyleCpt.value.top;
+  // 调整控制点位置,让连线起点和终点保持距离再弯曲
+  const offset = 50;
+  const controlX1 = startX + offset;
+  const controlY1 = startY;
+  const controlX2 = endX - offset;
+  const controlY2 = endY;
+
+  return `M ${startX} ${startY} C ${controlX1} ${controlY1} ${controlX2} ${controlY2} ${endX} ${endY}`;
+};
+
+onMounted(() => {
+})
+</script>
+
+<style lang="scss" scoped>
+.chart {
+  position: relative;
+  width: 800px;
+  height: 800px;
+  overflow: hidden;
+  background-color: #f2f4f7;
+  .chart-block {
+    position: absolute;
+  }
+}
+.lines {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+  z-index: 0;
+  marker {
+
+  }
+}
+</style>

+ 23 - 0
src/views/draw/draw.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="draw">
+    <chart/>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {getCurrentInstance, reactive} from "vue";
+import chart from './chart/index.vue'
+
+const {proxy} = getCurrentInstance()
+const state: any = reactive({})
+</script>
+
+<style lang="scss" scoped>
+.draw {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+</style>

+ 4 - 10
src/views/draw/index.vue

@@ -1,20 +1,14 @@
 <template>
   <div class="main">
-    绘制
+    <draw/>
   </div>
 </template>
 
 <script setup lang="ts">
-import {computed, getCurrentInstance, onMounted, reactive, ref, watch} from "vue";
-import {ElMessage, ElMessageBox} from "element-plus";
-
+import {getCurrentInstance, reactive} from "vue";
+import draw from './draw.vue'
 const {proxy} = getCurrentInstance()
-const state: any = reactive({
-})
-const ref_form = ref()
-
-onMounted(() => {
-})
+const state: any = reactive({})
 </script>
 
 <style lang="scss" scoped>

+ 54 - 0
src/views/draw/node/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="node" ref="ref_node" :style="{
+    width: w + 'px',
+    height: h + 'px',
+    left: x + 'px',
+    top: y + 'px',
+  }"
+    @mousedown.stop="moveStart"
+    @mousemove.stop="move"
+    @mouseup.stop="moveEnd"
+    @mouseleave.stop="moveEnd"
+  >
+    {{ type }}
+  </div>
+</template>
+
+<script setup lang="ts">
+import {getCurrentInstance, reactive, ref} from "vue";
+
+const emits = defineEmits(['update:x', 'update:y', 'update:w', 'update:h'])
+const props = defineProps({
+  type: {default: 'root'},
+  x: {default: 0},
+  y: {default: 0},
+  w: {default: 100},
+  h: {default: 100},
+})
+const {proxy} = getCurrentInstance()
+const state: any = reactive({
+  moving: false,
+})
+const ref_node = ref()
+const moveStart = (e) => {
+  state.moving = true
+}
+const move = (e) => {
+  if (state.moving) {
+    emits('update:x', proxy.x + e.movementX)
+    emits('update:y', proxy.y + e.movementY)
+  }
+}
+const moveEnd = (e) => {
+  state.moving = false
+}
+</script>
+
+<style lang="scss" scoped>
+.node {
+  border: 1px solid gray;
+  border-radius: 10px;
+  background-color: #409eff;
+  position: absolute;
+}
+</style>

+ 1 - 1
vite.config.ts

@@ -48,7 +48,7 @@ export default defineConfig(({mode, command}) => {
       },
     },
     server: {
-      port: 9616,
+      port: 9757,
       host: '0.0.0.0',
       open: true,
       strictPort: false,