Browse Source

530model流程框架

tangjunhao 3 tháng trước cách đây
mục cha
commit
bbfd51782c

BIN
src/assets/img/exit.png


+ 15 - 0
src/style/index.css

@@ -48,6 +48,12 @@ input[type="text"], input[type="password"] {
 img{
     vertical-align: middle; 
 }
+
+/* 取消所有元素的focus聚焦,点击时黑色边框 */
+* {
+  outline: none !important;
+}
+
 /* 改 el-dialog的默认样式*/
 .el-header{
     padding: 0;
@@ -60,6 +66,10 @@ img{
   padding: 10px 20px 20px 20px;
 }
 
+.lastbtn .el-button {
+  width: 85px;
+}
+
 .lastbtn .el-button:last-of-type {
   background-color: #12739E !important;
   color: white !important;
@@ -217,6 +227,11 @@ img{
 
 .custom-aside .full-height-tabs {
   flex: 1;
+  overflow: auto;
+}
+
+.el-tabs__content {
+  overflow: auto;
 }
 
 .custom-tabcontent {

+ 71 - 2
src/views/file/index.vue

@@ -1,3 +1,72 @@
 <template>
-  flie
-</template>
+  <el-container>
+    <el-aside width="200px" class="custom-aside separate-twoside">
+      <el-menu >
+        <el-menu-item
+        v-for="(item,index) in filemenuitems"
+        :key="index"
+        :index="item.index"
+        >
+        <img :src="item.img" style="width: 24px; margin-right: 5px;" />
+        <span>{{ item.label }}</span>
+        </el-menu-item>
+      </el-menu>
+      <div style="padding:20px;">
+        <el-divider style="margin: 10px 0;"></el-divider>
+        <el-button class="exitbutton" @click="exittop">
+          <img :src="exit"  style="width: 24px;">
+          <span>Exit</span>
+        </el-button>
+      </div>
+    </el-aside>
+    <el-main>Main</el-main>
+  </el-container>
+</template>
+
+<script setup>
+import { RouterView, RouterLink,useRouter } from "vue-router"
+import { useI18n } from 'vue-i18n'
+
+
+
+import tempic from "@/assets/img/temp.png"
+import exit from "@/assets/img/exit.png"
+
+const { t, locale } = useI18n()
+const router = useRouter();
+
+
+const filemenuitems = ref([
+  { index: '1', img: tempic, label: 'Back' },
+  { index: '2', img: tempic, label: 'Save' },
+  { index: '3', img: tempic, label: 'Save As' },
+  { index: '4', img: tempic, label: 'Export' },
+  { index: '1', img: tempic, label: 'Open' },
+  { index: '2', img: tempic, label: 'Recent' },
+  { index: '3', img: tempic, label: 'Close' },
+]);
+
+
+const exittop = () => {
+  router.push({path:'/'})
+}
+
+</script>
+
+<style scoped>
+.separate-twoside {
+  display: flex;
+  justify-content: space-between;
+}
+
+.exitbutton {
+  width: 100%;
+  border: none;
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 10px;
+}
+
+
+</style>

+ 52 - 71
src/views/model/index.vue

@@ -1,76 +1,19 @@
 <template>
   <el-container>
     <el-header class="custom-header">
-      <el-space :size="spacesize">
-        <el-button>
-          <img :src="getImgPath('newproject.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('save.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('importpro.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('exportpro.png')" style="width: 24px;" />
-        </el-button>
-        <el-divider direction="vertical" ></el-divider>
-        <el-button>
-          <img :src="getImgPath('back.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('goon.png')" style="width: 24px;" />
-        </el-button>
-        <el-divider direction="vertical" ></el-divider>
-        <el-button>
-          <img :src="getImgPath('temp.png')" style="width: 24px;" />
-        </el-button>
-        <el-divider direction="vertical" ></el-divider>
-        <el-button>
-          <img :src="getImgPath('temp.png')" style="width: 24px;" />
-        </el-button>
-        <el-divider direction="vertical" ></el-divider>
-        <el-button>
-          <img :src="getImgPath('temp.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('temp.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('temp.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('temp.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('temp.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('temp.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('temp.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('temp.png')" style="width: 24px;" />
-        </el-button>
-        <el-divider direction="vertical" ></el-divider>
-        <el-button>
-          <img :src="getImgPath('text.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('text2.png')" style="width: 24px;" />
-        </el-button>
-        <el-button>
-          <img :src="getImgPath('text3.png')" style="width: 24px;" />
-        </el-button>
-        <el-divider direction="vertical" ></el-divider>
-        <el-button>
-          <img :src="getImgPath('temp.png')" style="width: 24px;" />
-        </el-button>
+      <el-space :size="spacesize" class="spaceclass">
+        <template v-for="(item, index) in headerbuttons" :key="index">
+          <el-button v-if="item.type === 'button'" class="custom-icon-button">
+            <img :src="getImgPath(item.img)" style="width: 24px;" />
+          </el-button>
+          <el-divider
+            v-else-if="item.type === 'divider'"
+            direction="vertical"
+          ></el-divider>
+        </template>
       </el-space>
     </el-header>
-    <splitpanes class="default-theme">
+    <splitpanes class="default-theme diysplitpanes">
       <pane min-size="10" size="20" max-size="50" class="custom-aside">
         <el-tabs type="border-card" class="full-height-tabs">
 
@@ -109,7 +52,7 @@
         <div class="main-header">
           <el-tabs type="border-card">
             <el-tab-pane label="Topology">
-              <el-space :size="spacesize">
+              <el-space :size="spacesize" class="spaceclass">
                 <el-button
                   v-for="(item, index) in mainbuttons"
                   :key="index"
@@ -122,7 +65,7 @@
           </el-tabs>
         </div>
         <div class="flow-content">
-          
+          <vueflow />
         </div>
       </pane>
     </splitpanes>
@@ -137,6 +80,7 @@ import { request } from "@/utils/request";
 import { ElMessage} from 'element-plus'
 import { useProjectStore } from '@/store/project'
 import { useI18n } from 'vue-i18n'
+import vueflow from "./vueflow/index.vue"
 
 const { t, locale } = useI18n()
 
@@ -152,6 +96,34 @@ let spacesize = ref(10);
 let colactiveNames = ref(['1'])
 let collapseData = ref();
 
+const headerbuttons = ref([
+  { type: 'button', img: 'newproject.png' },
+  { type: 'button', img: 'save.png' },
+  { type: 'button', img: 'importpro.png' },
+  { type: 'button', img: 'exportpro.png' },
+  { type: 'divider' },
+  { type: 'button', img: 'back.png' },
+  { type: 'button', img: 'goon.png' },
+  { type: 'divider' },
+  { type: 'button', img: 'temp.png' },
+  { type: 'divider' },
+  { type: 'button', img: 'temp.png' },
+  { type: 'divider' },
+  { type: 'button', img: 'temp.png' },
+  { type: 'button', img: 'temp.png' },
+  { type: 'button', img: 'temp.png' },
+  { type: 'button', img: 'temp.png' },
+  { type: 'button', img: 'temp.png' },
+  { type: 'button', img: 'temp.png' },
+  { type: 'button', img: 'temp.png' },
+  { type: 'divider' },
+  { type: 'button', img: 'text.png' },
+  { type: 'button', img: 'text2.png' },
+  { type: 'button', img: 'text3.png' },
+  { type: 'divider' },
+  { type: 'button', img: 'temp.png' }
+]);
+
 let mainbuttons = ref([
   {img: 'temp.png'},
   {img: 'temp.png'},
@@ -188,12 +160,15 @@ onMounted(() => {
 
 <style scoped>
 .main-header {
-  height: 32px;
   background: #EEEEEE;
   border-radius: 0px 0px 0px 0px;
   border: 4px solid #EEEEEE;
 }
 
+.diysplitpanes {
+  overflow: auto;
+}
+
 .main-header :deep(.el-tabs--border-card>.el-tabs__content) {
   padding: 5px 15px;
 }
@@ -204,6 +179,11 @@ onMounted(() => {
   border: 1px solid #D8D8D8;
 }
 
+.spaceclass {
+  display: flex;
+  justify-items: flex-start;
+}
+
 .coms-container {
   display: flex;
   flex-wrap: wrap;
@@ -227,5 +207,6 @@ onMounted(() => {
 .flow-content {
   width: 100%;
   flex: 1;
+  background: #FFFFFF;
 }
 </style>

+ 38 - 0
src/views/model/vueflow/changebak.vue

@@ -0,0 +1,38 @@
+<script setup>
+defineProps({
+  name: {
+    type: String,
+    required: true,
+  },
+})
+</script>
+
+<template>
+  <svg v-if="name === 'reset'" width="16" height="16" viewBox="0 0 32 32">
+    <path d="M18 28A12 12 0 1 0 6 16v6.2l-3.6-3.6L1 20l6 6l6-6l-1.4-1.4L8 22.2V16a10 10 0 1 1 10 10Z" />
+  </svg>
+
+  <svg v-if="name === 'update'" width="16" height="16" viewBox="0 0 24 24">
+    <path
+      d="M14 20v-2h2.6l-3.2-3.2l1.425-1.425L18 16.55V14h2v6Zm-8.6 0L4 18.6L16.6 6H14V4h6v6h-2V7.4Zm3.775-9.425L4 5.4L5.4 4l5.175 5.175Z"
+    />
+  </svg>
+
+  <svg v-if="name === 'sun'" width="16" height="16" viewBox="0 0 24 24">
+    <path
+      d="M12 17q-2.075 0-3.537-1.463Q7 14.075 7 12t1.463-3.538Q9.925 7 12 7t3.538 1.462Q17 9.925 17 12q0 2.075-1.462 3.537Q14.075 17 12 17ZM2 13q-.425 0-.712-.288Q1 12.425 1 12t.288-.713Q1.575 11 2 11h2q.425 0 .713.287Q5 11.575 5 12t-.287.712Q4.425 13 4 13Zm18 0q-.425 0-.712-.288Q19 12.425 19 12t.288-.713Q19.575 11 20 11h2q.425 0 .712.287q.288.288.288.713t-.288.712Q22.425 13 22 13Zm-8-8q-.425 0-.712-.288Q11 4.425 11 4V2q0-.425.288-.713Q11.575 1 12 1t.713.287Q13 1.575 13 2v2q0 .425-.287.712Q12.425 5 12 5Zm0 18q-.425 0-.712-.288Q11 22.425 11 22v-2q0-.425.288-.712Q11.575 19 12 19t.713.288Q13 19.575 13 20v2q0 .425-.287.712Q12.425 23 12 23ZM5.65 7.05L4.575 6q-.3-.275-.288-.7q.013-.425.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7q0 .4-.275.7q-.275.3-.687.287q-.413-.012-.713-.287ZM18 19.425l-1.05-1.075q-.275-.3-.275-.712q0-.413.275-.688q.275-.3.688-.287q.412.012.712.287L19.425 18q.3.275.288.7q-.013.425-.288.725q-.3.3-.725.3t-.7-.3ZM16.95 7.05q-.3-.275-.287-.688q.012-.412.287-.712L18 4.575q.275-.3.7-.288q.425.013.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275q-.4 0-.7-.275ZM4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.713-.275q.412 0 .687.275q.3.275.288.688q-.013.412-.288.712L6 19.425q-.275.3-.7.287q-.425-.012-.725-.287Z"
+    />
+  </svg>
+
+  <svg v-if="name === 'moon'" width="16" height="16" viewBox="0 0 24 24">
+    <path
+      d="M12 21q-3.75 0-6.375-2.625T3 12q0-3.75 2.625-6.375T12 3q.35 0 .688.025q.337.025.662.075q-1.025.725-1.637 1.887Q11.1 6.15 11.1 7.5q0 2.25 1.575 3.825Q14.25 12.9 16.5 12.9q1.375 0 2.525-.613q1.15-.612 1.875-1.637q.05.325.075.662Q21 11.65 21 12q0 3.75-2.625 6.375T12 21Z"
+    />
+  </svg>
+
+  <svg v-if="name === 'log'" width="20" height="20" viewBox="0 0 24 24">
+    <path
+      d="m12 17l4-4l-1.4-1.4l-1.6 1.55V9h-2v4.15L9.4 11.6L8 13zm-6 5q-.825 0-1.412-.587T4 20V8l6-6h8q.825 0 1.413.588T20 4v16q0 .825-.587 1.413T18 22zm0-2h12V4h-7.15L6 8.85zm0 0h12z"
+    />
+  </svg>
+</template>

+ 80 - 0
src/views/model/vueflow/defaultnode.vue

@@ -0,0 +1,80 @@
+<script  setup>
+import { ref, onMounted, reactive, } from "vue";
+import { VueFlow,Handle, useVueFlow,Position } from '@vue-flow/core'
+const props = defineProps({
+  node: {
+    type: Object,
+    required: true,
+  },
+  sourcePosition: {
+    type: String,
+  },
+  targetPosition: {
+    type: String,
+  },
+})
+onMounted(() => {
+});
+</script>
+<template>
+  <!-- <div v-if="props.node.data.label!='模块化'"> -->
+  <div v-if="props.node.data!=null">
+    <div class="custom-node icons  " :id="`node-${node.id}`"  >
+    <img :src="props.node.data.image"/>
+    <span>{{props.node.data.label }}</span>
+  </div>
+  <Handle type="source" id="target-a" :position="Position.Right"  />
+  <Handle id="target-c" type="source" :position="Position.Top"  /> 
+  <Handle id="target-b" type="source" :position="Position.Left" /> 
+  <Handle id="target-d" type="source" :position="Position.Bottom"  /> 
+  </div>
+<!-- </div> -->
+</template>
+<style scoped> 
+
+.icons img{
+    width: 26px;
+
+}
+.icons span{
+  display: block;
+  font-size: 8px;
+}
+
+
+</style>
+<style>
+.vue-flow__node-default, .vue-flow__node-input, .vue-flow__node-output{
+  width: 60px;
+  height: 58px;
+  pointer-events: auto;
+}
+
+.vue-flow__node-default:not(.selected) {
+  z-index: 1 !important; /* 强制设置未选中时的 z-index */
+}
+
+.vue-flow__node.draggable {
+    background-color: rgba(0,0,0,0);
+}
+
+.vue-flow__node-default.selected, .vue-flow__node-default.selected:hover, .vue-flow__node-input.selected, .vue-flow__node-input.selected:hover, .vue-flow__node-output.selected, .vue-flow__node-output.selected:hover{
+  box-shadow:none;
+}
+.vue-flow__node {
+          stroke: none; /* 移除节点边框 */
+        }
+        
+ 
+.vue-flow__edge-text {
+  display: block;
+}
+.vue-flow__node-custom {
+    background: purple;
+    color: white;
+    border: 1px solid purple;
+    border-radius: 4px;
+    box-shadow: 0 0 0 1px purple;
+    padding: 8px;
+}
+</style>

+ 221 - 0
src/views/model/vueflow/index.vue

@@ -0,0 +1,221 @@
+<template>
+  <VueFlow ref="vueFlowRef" v-model:nodes="nodes" v-model:edges="edges"  :class="{ dark }"
+    class="basic-flow" 
+    :default-viewport="{ zoom: 1.5 }" :min-zoom="0.2" :max-zoom="2.5" @drop="onDrop"
+    @node-contextmenu="onNodeContextMenu" 
+    @dragover="onDragOver" @dragleave="onDragLeave" @edge-click="onEdgeClick" @node-double-click="onNodeDoubleClick" 
+    @node-click="onNodeClick" @edge-double-click="onEdgeDoubleClick">
+    
+    <!-- 自定义节点类型为default的节点 -->
+    <template  #node-default="props">
+      <defaultnode :node="props" />
+    </template>
+
+    <Background pattern-color="#aaa" :gap="16" 
+    />
+
+    <Controls position="top-left">
+      <ControlButton title="重置" @click="resetTransform">
+        <changebak name="reset" />
+      </ControlButton>
+      <ControlButton title="背景切换" @click="toggleDarkMode">
+        <changebak v-if="dark" name="sun" />
+        <changebak v-else name="moon" />
+      </ControlButton>
+
+      <ControlButton title="保存" @click="saveproject">
+        <!-- <Icon name="log" /> -->
+        <el-icon :color="iconcolor"><UploadFilled /></el-icon>
+      </ControlButton>
+      <ControlButton title="删除节点" @click="removeNode()">
+        <el-icon  :color="iconcolor"><DocumentDelete /></el-icon>
+      </ControlButton>
+      <ControlButton title="删除线" @click="removeEdge()">
+        <el-icon  :color="iconcolor"><Crop /></el-icon>
+      </ControlButton>
+      <ControlButton title="清空全部" @click="confirmDelete()">
+        <el-icon :color="iconcolor"><DeleteFilled /></el-icon>
+      </ControlButton>
+      
+    
+    </Controls>
+    
+  </VueFlow>
+</template>
+
+<script setup>
+import { VueFlow,Panel, useVueFlow, MarkerType} from '@vue-flow/core'
+import { ElMessage, ElButton, ElDialog, ElSelect, ElMessageBox} from 'element-plus'
+import {
+  DocumentDelete,
+  Delete,
+  UploadFilled,
+  Histogram,
+  DeleteFilled,
+  Crop,
+} from '@element-plus/icons-vue'
+
+import { request, uploadFile } from "@/utils/request";
+import { Background } from '@vue-flow/background'
+import { ControlButton, Controls } from '@vue-flow/controls'
+import "./main.css";//重置样式
+import defaultnode from './defaultnode.vue'
+import useDragAndDrop from './useDnD';
+import changebak from './changebak.vue'
+import { useProjectStore } from '@/store/project'
+import emitter from "@/utils/emitter";
+
+const { onInit, onNodeDragStop, onConnect, addEdges, setViewport, toObject,addNodes,updateEdgeData,onConnectStart} = useVueFlow()
+
+const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop();
+
+const dark = ref(false)
+let vueFlowRef = ref();
+let iconcolor=ref('#000')
+
+const edges = ref([]);
+const nodes = ref([]);
+
+// 选中节点
+let noid = ref([]);
+let Edgeid = ref();
+let seledge=ref(null);
+// 连线颜色
+let linecolor=ref('#2267B1')
+
+
+const projectStore = useProjectStore()
+let pid = computed(() => projectStore.pid || '')
+
+
+
+
+const resetTransform = () => {
+  setViewport({ x: 0, y: 0, zoom: 1 })
+}
+
+const toggleDarkMode = () => {
+  dark.value = !dark.value;
+  if(dark.value){
+    iconcolor.value='#fff'
+  }else{
+    iconcolor.value='#000'
+  }
+}
+
+const saveproject = () => {
+
+}
+
+const removeNode = () => {
+
+}
+
+const removeEdge = () => {
+
+}
+
+const confirmDelete = () => {
+
+}
+
+const onNodeContextMenu = ( event,node) => {
+  console.log('右键点击', event, node);
+}
+
+const onEdgeClick = (event,edge) => {
+  console.log('边数据:', edge.data);
+}
+
+const onNodeClick = (event, node) => {
+  console.log('节点被单击:', node);
+  // node 是被双击的节点对象,包含 id、position、data 等信息
+};
+
+const onNodeDoubleClick = (event, node) => {
+  console.log('节点被双击:', node);
+  // node 是被双击的节点对象,包含 id、position、data 等信息
+};
+
+const onEdgeDoubleClick = (event, edge) => {
+  console.log("连线双击:",edge.data)
+}
+
+
+
+</script>
+
+<style scoped>
+/* .vue-flow__edge.selected .vue-flow__edge-path, .vue-flow__edge:focus .vue-flow__edge-path, .vue-flow__edge:focus-visible .vue-flow__edge-path {
+    stroke: #555 !important;
+} */
+.vue-flow__edge:focus .vue-flow__edge-path, .vue-flow__edge:focus-visible .vue-flow__edge-path {
+    stroke: #555 !important;
+} 
+.vue-flow__edge {
+  text-align: left;
+  /* 设置edges的左对齐 */
+}
+
+.vue-flow__edge-text {
+  transform:  translateY(-10px); /* 将 label 向上偏移 */
+  background: transparent !important;
+  font-size: 8px;
+  font-family: 'Microsoft YaHei';
+  color: #333333;
+}
+
+.vue-flow__edge-textbg {
+  fill: transparent !important; /* 将背景设置为透明 */
+}
+
+.vue-flow__node-default.selectable:hover,
+.vue-flow__node-input.selectable:hover,
+.vue-flow__node-output.selectable:hover {
+  box-shadow: none;
+}
+
+.remove {
+  background: #fff;
+  color: #666;
+  margin: 0 10px;
+  font-size: 12px;
+}
+.vue-flow__node-default, .vue-flow__node-input, .vue-flow__node-output{
+  /* width: auto !important; */
+  border: none;
+  background-color: rgba(0,0,0,0);
+}
+.node-content {
+  cursor: move;  /* 更改鼠标光标表示可拖动 */
+}
+.vue-flow__node {
+  cursor: move;
+}
+
+/* 禁用文本选中效果 */
+.left_main * {
+  -webkit-user-select: none; /* Safari */
+  -moz-user-select: none; /* Firefox */
+  -ms-user-select: none; /* IE10+/Edge */
+  user-select: none; /* Standard syntax */
+}
+.lableaniu{
+
+  font-size: 12px;
+    background-color: #ddd;
+    padding: 4px 16px;
+    /* margin-top: -17px; */
+    margin-left: 5px;
+    margin-top: 0px;
+    border-radius: 1px;
+}
+.vue-flow__controls-button svg{
+  max-width: 16px;
+  max-height: 16px;
+}
+.field{
+  display: flex;
+}
+
+</style>

+ 79 - 0
src/views/model/vueflow/main.css

@@ -0,0 +1,79 @@
+@import '@vue-flow/core/dist/style.css';
+@import '@vue-flow/core/dist/theme-default.css';
+@import '@vue-flow/controls/dist/style.css';
+@import '@vue-flow/minimap/dist/style.css';
+@import '@vue-flow/node-resizer/dist/style.css';
+
+html,
+body,
+#app {
+  margin: 0;
+  height: 100%;
+}
+
+#app {
+  /* text-transform: uppercase; */
+  font-family: 'JetBrains Mono', monospace;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-align: center;
+  color: #2c3e50;
+}
+
+.vue-flow__minimap {
+  transform: scale(75%);
+  transform-origin: bottom right;
+}
+
+.basic-flow.dark {
+    background:#2d3748;
+    color:#fffffb
+}
+
+.basic-flow.dark .vue-flow__node {
+    background:#4a5568;
+    color:#fffffb
+}
+
+.basic-flow.dark .vue-flow__node.selected {
+    background:#333;
+    box-shadow:0 0 0 2px #2563eb
+}
+
+.basic-flow .vue-flow__controls {
+    display:flex;
+    flex-wrap:wrap;
+    justify-content:center
+}
+
+.basic-flow.dark .vue-flow__controls {
+    border:1px solid #FFFFFB
+}
+
+.basic-flow .vue-flow__controls .vue-flow__controls-button {
+    border:none;
+    border-right:1px solid #eee
+}
+
+.basic-flow .vue-flow__controls .vue-flow__controls-button svg {
+    height:100%;
+    width:100%
+}
+
+.basic-flow.dark .vue-flow__controls .vue-flow__controls-button {
+    background:#333;
+    fill:#fffffb;
+    border:none
+}
+
+.basic-flow.dark .vue-flow__controls .vue-flow__controls-button:hover {
+    background:#4d4d4d
+}
+
+.basic-flow.dark .vue-flow__edge-textbg {
+    fill:#292524
+}
+
+.basic-flow.dark .vue-flow__edge-text {
+    fill:#fffffb
+}

+ 305 - 0
src/views/model/vueflow/useDnD.js

@@ -0,0 +1,305 @@
+// import { useVueFlow,Position,MarkerType } from '@vue-flow/core';
+import { useVueFlow,Position,MarkerType} from '@vue-flow/core';
+import { ref, watch } from 'vue'
+import { ElMessage, ElButton, ElDialog, ElSelect } from "element-plus"
+import { request, uploadFile } from "@/utils/request"
+import emitter from "@/utils/emitter"
+import gc1 from '@/assets/flowimg/gc1.png'
+import gc2 from '@/assets/flowimg/gc2.png'
+import gc3 from '@/assets/flowimg/gc3.png'
+import wen from '@/assets/flowimg/wen.png'
+import xuek1 from '@/assets/flowimg/xuek1.png'
+import xuek0 from '@/assets/flowimg/xuek0.png'
+import xuek2 from '@/assets/flowimg/xuek2.png'
+import xuek3 from '@/assets/flowimg/xuek3.png'
+import xuek4 from '@/assets/flowimg/xuek4.png'
+import xuek5 from '@/assets/flowimg/xuek5.png'
+import xuek6 from '@/assets/flowimg/xuek6.png'
+import xuek7 from '@/assets/flowimg/xuek7.png'
+import xuek8 from '@/assets/flowimg/xuek8.png'
+import xuek9 from '@/assets/flowimg/xuek9.png'
+import xuek10 from '@/assets/flowimg/xuek10.png'
+import xuek11 from '@/assets/flowimg/xuek11.png'
+import xuek12 from '@/assets/flowimg/xuek12.png'
+import r2 from '@/assets/flowimg/mob.png'
+import aero from '@/assets/flowimg/aero.png'
+import csh from '@/assets/flowimg/csh.png'
+import ffd from '@/assets/flowimg/ffd.png'
+import ADflow from '@/assets/flowimg/ADflow.png'
+import tacs from '@/assets/flowimg/TACS.png'
+import fsi from '@/assets/flowimg/fsi.png'
+import fsibackground from '@/assets/flowimg/fsibackg.png'
+import mathfunc from '@/assets/flowimg/MathFunc.png'
+import flight from '@/assets/flowimg/flight.png'
+let nid = 0;
+let id=0
+let treeobj=ref([]);
+let datas={}
+
+/**
+ * @returns {string} - A unique id.
+ */
+function getId() {
+  //return `node_${id++}`
+  return `${nid}${id++}`;
+}
+function imagefun(){
+  console.log(nid);
+  // if(nid=='1-1'){
+  //   return  datas = {label: '优化问题', image:gc1}
+  //   }else if(nid=='1-2'){
+  //     return datas = {label:'分析流程', image:gc2}
+  //   }else if(nid=='1-3'){
+  //     return datas = {label:'优化器', image:gc3}
+    // }else
+     if(nid=='2-1'){
+      return datas = {label:'优化问题', image:wen,name:'Project'}
+    }else if(nid=='3-1'){
+      return datas = {label:'CATIA', image:xuek1,name:'CATIA'}
+    }else if(nid=='3-0'){
+      return datas = {label:'CST', image:xuek0,name:'CST'}
+    }else if(nid=='3-10'){
+      return datas = {label:'FFD', image:ffd,name:'FFD'}
+    }else if(nid=='3-11'){
+      return datas = {label:'ADflow', image:ADflow,name:'ADflow'}
+    }else if(nid=='3-12'){
+      return datas = {label:'TACS', image:tacs,name:'TACS'}
+    }else if(nid=='3-13'){
+      return datas = {label:'FUN to FEM', image:fsi,backgroud:fsibackground , name:'FSI'}
+    }else if(nid=='3-14'){
+      return datas = {label:'MathFunc', image:mathfunc,name:'MathFunc'}
+    }else if(nid=='3-15'){
+      return datas = {label:'Flight', image:flight,name:'Flight'}
+    }
+     else if(nid=='3-2'){
+      return datas = {label:'Excel', image:xuek2,name:'Excel'}
+    }else if(nid=='3-3'){
+      return datas = {label:'Feko', image:xuek3,name:'Feko'}
+    }else if(nid=='3-4'){
+      return datas = {label:'Fluent', image:xuek4,name:'Fluent'}
+    }else if(nid=='3-5'){
+      return datas = {label:'HCFD', image:xuek5,name:'HCFD'}
+    }else if(nid=='3-6'){
+      return datas = {label:'Matlab', image:xuek6,name:'Matlab'}
+    }else if(nid=='3-7'){
+      return datas = {label:'Nastran', image:xuek7,name:'Nastran'}
+    }else if(nid=='3-8'){
+      return datas = {label:'Python', image:xuek8,name:'Python'}
+    }else if(nid=='3-9'){
+      return datas = {label:'Xfoil', image:xuek9,name:'Xfoil'}
+    }else if(nid=='4-1'){
+      return datas = {label:'进化优化器', image:xuek10,name:'optimizer1'}
+    }else if(nid=='4-2'){
+      return datas = {label:'代理优化器', image:xuek11,name:'optimizer3'}
+    }else if(nid=='4-3'){
+      return datas = {label:'梯度优化器', image:xuek12,name:'optimizer2'}
+    }else if(nid=='3-01'){
+      return datas = {label:'优化器', image:r2,name:'optimizer'}
+    }else if(nid=='3-02'){
+      return datas = {label:'参数化', image:csh,name:'参数化'}
+    }else if(nid=='3-03'){
+      return datas = {label:'气动分析', image:aero,name:'气动分析'}
+    }
+    else{
+      return null;
+    }
+
+  }
+
+/**
+ * In a real world scenario you'd want to avoid creating refs in a global scope like this as they might not be cleaned up properly.
+ * @type {{draggedType: Ref<string|null>, isDragOver: Ref<boolean>, isDragging: Ref<boolean>}}
+ */
+const state = {
+  /**
+   * The type of the node being dragged.
+   */
+  draggedType: ref(null),
+  isDragOver: ref(false),
+  isDragging: ref(false),
+}
+
+// 保存流
+const saveflow = async (pid, wid, uid, type, fromuid, touid) => {
+  const params = {
+    transCode: 'MDO0058',
+    pid: pid || '',
+    wid: wid || '', // 流ID
+    uid: uid || '',
+    type: type || '',
+    fromuid: fromuid || '',
+    touid: touid || '',
+  };
+
+  try {
+    const res = await request(params);
+    if (uid.includes('MathFunc')) {
+      console.log("pid:", pid);
+      console.log("wid:", res.wid);
+      emitter.emit("getMfcid", { id: pid, nowid: res.wid });
+    }
+    return res.wid;
+  } catch (err) {
+    ElMessage.error(err.returnMsg);
+  }
+};
+
+// 用来存储每种组件名称出现次数的对象
+const nameCount = {}
+
+export default function useDragAndDrop() {
+  const { draggedType, isDragOver, isDragging } = state
+
+  const { addNodes, addEdges,screenToFlowCoordinate, onNodesInitialized, updateNode } = useVueFlow()
+
+  watch(isDragging, (dragging) => {
+    document.body.style.userSelect = dragging ? 'none' : ''
+  })
+
+  function onDragStart(event, type,id) {
+  
+ 
+    if (event.dataTransfer) {
+     
+      nid=id;
+      event.dataTransfer.setData('application/vueflow', type)
+      event.dataTransfer.effectAllowed = 'move'    
+   
+    }
+
+    draggedType.value =type;
+    //draggedType.value = 'smoothstep'
+    isDragging.value = true
+
+    document.addEventListener('drop', onDragEnd)
+   
+  }
+
+  /**
+   * Handles the drag over event.
+   *
+   * @param {DragEvent} event
+   */
+  function onDragOver(event) {
+    event.preventDefault()
+
+    if (draggedType.value) {
+      isDragOver.value = true
+
+      if (event.dataTransfer) {
+        event.dataTransfer.dropEffect = 'move'
+      }
+    }
+  }
+  function handleNodeDrop(e){
+  
+  }
+  function onDragLeave(e) {
+  
+    isDragOver.value = false
+  }
+
+  function onDragEnd() {
+    isDragging.value = false
+    isDragOver.value = false
+    draggedType.value = null
+    nid=''
+    document.removeEventListener('drop', onDragEnd)
+  }
+
+  /**
+   * Handles the drop event.
+   *
+   * @param {DragEvent} event
+   */
+  async function onDrop(event) {
+    const position = screenToFlowCoordinate({
+      x: event.clientX,
+      y: event.clientY,
+    })
+
+    const savedObj = JSON.parse(sessionStorage.getItem("objlist"));
+    // console.log('objlist',savedObj);
+    const pid = savedObj.pid;
+    const newwid = '';
+
+    const nodeId = getId()
+    const image1=imagefun();
+    // 获取当前组件的名称
+    const componentName = image1.name;
+
+    // 如果该组件的名称已存在,给名称添加序号
+    if (!nameCount[componentName]) {
+      nameCount[componentName] = 0;
+    }
+  
+    nameCount[componentName]++; // 增加计数
+    const newName = `${componentName}${nameCount[componentName]}`; // 新的名称加上序号
+ 
+    let snodes=ref([]);
+    let sedges=ref([]);
+
+    if(nid=='4'){
+      // const nodes = ref([])
+      // const edges = ref([])
+
+    }else{
+      // 创建初始的节点数据,不包含 wid
+      // FSI为group类型
+      if(nid == '3-13'){
+        draggedType.value = 'group';
+      }
+      const newNode = {
+        id: nodeId,
+        type: draggedType.value,
+        position,
+        data: { ...image1, uid: newName },
+      };
+
+      snodes.value.push(newNode)
+
+      // 异步保存数据并更新 wid
+      saveflow(pid, newwid, newName, 'com', '', '').then((wid) => {
+        // 更新节点数据中的 wid
+        newNode.data.wid = wid;
+
+        // 使用 onNodesInitialized 来更新节点数据
+        onNodesInitialized(() => {
+          // 更新节点的其他数据
+          updateNode(nodeId, (node) => ({
+            ...node, 
+            data: {
+              ...node.data,
+              wid: newNode.data.wid,  // 更新节点中的 wid
+            },
+          }));
+        });
+      }).catch((err) => {
+        console.error('保存流程失败:', err.message);
+        ElMessage.error('保存流程失败');
+      });
+    }
+    const { off } = onNodesInitialized(() => {
+      updateNode(nodeId, (node) => ({
+        position: { x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2 },
+        dimensions:{ height:  58, width: 60}
+      }))
+
+      off()
+    })
+    addNodes(snodes.value)
+    treeobj.value=snodes.value
+
+  }
+  return {
+    treeobj,
+    draggedType,
+    isDragOver,
+    isDragging,
+    onDragStart,
+    onDragLeave,
+    onDragOver,
+    onDrop,
+    handleNodeDrop,
+  }
+}