From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue |  265 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 265 insertions(+), 0 deletions(-)

diff --git a/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue b/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue
new file mode 100644
index 0000000..a8a0ac6
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue
@@ -0,0 +1,265 @@
+<template>
+  <div class="simple-process-model-container position-relative">
+    <div class="position-absolute top-0px right-0px bg-#fff z-index-button-group">
+      <el-row type="flex" justify="end">
+        <el-button-group key="scale-control" size="default">
+          <el-button v-if="!readonly" size="default" @click="exportJson">
+            <Icon icon="ep:download" /> 瀵煎嚭
+          </el-button>
+          <el-button v-if="!readonly" size="default" @click="importJson">
+            <Icon icon="ep:upload" />瀵煎叆
+          </el-button>
+          <!-- 鐢ㄤ簬鎵撳紑鏈湴鏂囦欢-->
+          <input
+            v-if="!readonly"
+            type="file"
+            id="files"
+            ref="refFile"
+            style="display: none"
+            accept=".json"
+            @change="importLocalFile"
+          />
+          <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
+          <el-button size="default" :plain="true" :icon="ZoomOut" @click="zoomOut()" />
+          <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
+          <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" />
+          <el-button size="default" @click="resetPosition">閲嶇疆</el-button>
+        </el-button-group>
+      </el-row>
+    </div>
+    <div
+      class="simple-process-model"
+      :style="`transform: translate(${currentX}px, ${currentY}px) scale(${scaleValue / 100});`"
+      @mousedown="startDrag"
+      @mousemove="onDrag"
+      @mouseup="stopDrag"
+      @mouseleave="stopDrag"
+      @mouseenter="setGrabCursor"
+    >
+      <ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
+    </div>
+  </div>
+  <Dialog v-model="errorDialogVisible" title="淇濆瓨澶辫触" width="400" :fullscreen="false">
+    <div class="mb-2">浠ヤ笅鑺傜偣鍐呭涓嶅畬鍠勶紝璇蜂慨鏀瑰悗淇濆瓨</div>
+    <div
+      class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal"
+      v-for="(item, index) in errorNodes"
+      :key="index"
+    >
+      {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
+    </div>
+    <template #footer>
+      <el-button type="primary" @click="errorDialogVisible = false">鐭ラ亾浜�</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import ProcessNodeTree from './ProcessNodeTree.vue'
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
+import { useWatchNode } from './node'
+import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+import { isString } from '@/utils/is'
+import download from '@/utils/download'
+
+defineOptions({
+  name: 'SimpleProcessModel'
+})
+
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  },
+  readonly: {
+    type: Boolean,
+    required: false,
+    default: true
+  }
+})
+
+const emits = defineEmits<{
+  save: [node: SimpleFlowNode | undefined]
+}>()
+
+const processNodeTree = useWatchNode(props)
+
+provide('readonly', props.readonly)
+
+// TODO 鍙紭鍖栵細鎷栨嫿鏈夌偣鍗¢】
+/** 鎷栨嫿銆佹斁澶х缉灏忕瓑鎿嶄綔 */
+let scaleValue = ref(100)
+const MAX_SCALE_VALUE = 200
+const MIN_SCALE_VALUE = 50
+const isDragging = ref(false)
+const startX = ref(0)
+const startY = ref(0)
+const currentX = ref(0)
+const currentY = ref(0)
+const initialX = ref(0)
+const initialY = ref(0)
+
+const setGrabCursor = () => {
+  document.body.style.cursor = 'grab'
+}
+
+const resetCursor = () => {
+  document.body.style.cursor = 'default'
+}
+
+const startDrag = (e: MouseEvent) => {
+  isDragging.value = true
+  startX.value = e.clientX - currentX.value
+  startY.value = e.clientY - currentY.value
+  setGrabCursor() // 璁剧疆灏忔墜鍏夋爣
+}
+
+const onDrag = (e: MouseEvent) => {
+  if (!isDragging.value) return
+  e.preventDefault() // 绂佺敤鏂囨湰閫夋嫨
+
+  // 浣跨敤 requestAnimationFrame 浼樺寲鎬ц兘
+  requestAnimationFrame(() => {
+    currentX.value = e.clientX - startX.value
+    currentY.value = e.clientY - startY.value
+  })
+}
+
+const stopDrag = () => {
+  isDragging.value = false
+  resetCursor() // 閲嶇疆鍏夋爣
+}
+
+const zoomIn = () => {
+  if (scaleValue.value == MAX_SCALE_VALUE) {
+    return
+  }
+  scaleValue.value += 10
+}
+
+const zoomOut = () => {
+  if (scaleValue.value == MIN_SCALE_VALUE) {
+    return
+  }
+  scaleValue.value -= 10
+}
+
+const processReZoom = () => {
+  scaleValue.value = 100
+}
+
+const resetPosition = () => {
+  currentX.value = initialX.value
+  currentY.value = initialY.value
+}
+
+/** 鏍¢獙鑺傜偣璁剧疆 */
+const errorDialogVisible = ref(false)
+let errorNodes: SimpleFlowNode[] = []
+
+const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
+  if (node) {
+    const { type, showText, conditionNodes } = node
+    if (type == NodeType.END_EVENT_NODE) {
+      return
+    }
+    if (type == NodeType.START_USER_NODE) {
+      // 鍙戣捣浜鸿妭鐐规殏鏃朵笉鐢ㄦ牎楠岋紝鐩存帴鏍¢獙瀛╁瓙鑺傜偣
+      validateNode(node.childNode, errorNodes)
+    }
+
+    if (
+      type === NodeType.USER_TASK_NODE ||
+      type === NodeType.COPY_TASK_NODE ||
+      type === NodeType.CONDITION_NODE
+    ) {
+      if (!showText) {
+        errorNodes.push(node)
+      }
+      validateNode(node.childNode, errorNodes)
+    }
+
+    if (
+      type == NodeType.CONDITION_BRANCH_NODE ||
+      type == NodeType.PARALLEL_BRANCH_NODE ||
+      type == NodeType.INCLUSIVE_BRANCH_NODE
+    ) {
+      // 鍒嗘敮鑺傜偣
+      // 1. 鍏堟牎楠屽悇涓垎鏀�
+      conditionNodes?.forEach((item) => {
+        validateNode(item, errorNodes)
+      })
+      // 2. 鏍¢獙瀛╁瓙鑺傜偣
+      validateNode(node.childNode, errorNodes)
+    }
+  }
+}
+
+/** 鑾峰彇褰撳墠娴佺▼鏁版嵁 */
+const getCurrentFlowData = async () => {
+  try {
+    errorNodes = []
+    validateNode(processNodeTree.value, errorNodes)
+    if (errorNodes.length > 0) {
+      errorDialogVisible.value = true
+      return undefined
+    }
+    return processNodeTree.value
+  } catch (error) {
+    console.error('鑾峰彇娴佺▼鏁版嵁澶辫触:', error)
+    return undefined
+  }
+}
+
+defineExpose({
+  getCurrentFlowData
+})
+
+/** 瀵煎嚭 JSON */
+const exportJson = () => {
+  download.json(new Blob([JSON.stringify(processNodeTree.value)]), 'model.json')
+}
+
+/** 瀵煎叆 JSON */
+const refFile = ref()
+const importJson = () => {
+  refFile.value.click()
+}
+const importLocalFile = () => {
+  const file = refFile.value.files[0]
+  const reader = new FileReader()
+  reader.readAsText(file)
+  reader.onload = function () {
+    if (isString(this.result)) {
+      processNodeTree.value = JSON.parse(this.result)
+      emits('save', processNodeTree.value)
+    }
+  }
+}
+
+// 鍦ㄧ粍浠跺垵濮嬪寲鏃惰褰曞垵濮嬩綅缃�
+onMounted(() => {
+  initialX.value = currentX.value
+  initialY.value = currentY.value
+})
+</script>
+
+<style lang="scss" scoped>
+.simple-process-model-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  overflow: hidden;
+  user-select: none; // 绂佺敤鏂囨湰閫夋嫨
+}
+
+.simple-process-model {
+  position: relative; // 纭繚鐩稿瀹氫綅
+  min-width: 100%; // 纭繚瀹藉害涓�100%
+  min-height: 100%; // 纭繚楂樺害涓�100%
+}
+
+.z-index-button-group {
+  z-index: 10;
+}
+</style>

--
Gitblit v1.8.0