Browse Source

标尺功能开发

lichunyang 2 weeks ago
parent
commit
ff65ad0fcf
3 changed files with 210 additions and 2 deletions
  1. 203 0
      src/components/Ruler.vue
  2. 1 1
      src/components/layout/home.vue
  3. 6 1
      src/views/model/index.vue

+ 203 - 0
src/components/Ruler.vue

@@ -0,0 +1,203 @@
+<template>
+  <div v-if="visible" class="ruler-container" :style="{ pointerEvents: 'none' }">
+    <!-- 垂直标尺(左侧) -->
+    <svg class="ruler-svg vertical" :width="rulerWidth" :height="containerHeight" :style="verticalStyle">
+      <defs>
+        <pattern id="verticalTicks" width="1" height="10" patternUnits="userSpaceOnUse">
+          <line x1="0" y1="0" x2="0" y2="10" stroke="#ddd" stroke-width="1" />
+        </pattern>
+      </defs>
+      <rect width="100%" height="100%" fill="url(#verticalTicks)" />
+      <!-- 大刻度线和标签 -->
+      <g v-for="(tick, index) in verticalTicks" :key="index">
+        <line :x1="0" :y1="tick.pos" :x2="rulerWidth" :y2="tick.pos" :stroke="tick.isMajor ? '#333' : '#666'" :stroke-width="tick.isMajor ? 2 : 1" />
+        <text v-if="tick.isMajor" :x="rulerWidth - 5" :y="tick.pos + 4" font-size="10" fill="#333" text-anchor="end">{{ tick.label }}</text>
+      </g>
+    </svg>
+    <!-- 水平标尺(顶部) -->
+    <svg class="ruler-svg horizontal" :width="containerWidth" :height="rulerHeight" :style="horizontalStyle">
+      <defs>
+        <pattern id="horizontalTicks" width="10" height="1" patternUnits="userSpaceOnUse">
+          <line x1="0" y1="0" x2="10" y2="0" stroke="#ddd" stroke-width="1" />
+        </pattern>
+      </defs>
+      <rect width="100%" height="100%" fill="url(#horizontalTicks)" />
+      <!-- 大刻度线和标签 -->
+      <g v-for="(tick, index) in horizontalTicks" :key="index">
+        <line :x1="tick.pos" :y1="0" :x2="tick.pos" :y2="rulerHeight" :stroke="tick.isMajor ? '#333' : '#666'" :stroke-width="tick.isMajor ? 2 : 1" />
+        <text v-if="tick.isMajor" :x="tick.pos - 5" :y="rulerHeight - 2" font-size="10" fill="#333" text-anchor="middle">{{ tick.label }}</text>
+      </g>
+    </svg>
+    <!-- 角落重叠遮罩(可选,避免重叠视觉问题) -->
+    <div class="ruler-corner" :style="{ width: rulerWidth + 'px', height: rulerHeight + 'px', background: 'white' }"></div>
+  </div>
+</template>
+
+<script setup>
+import { computed, onMounted, onUnmounted, ref, nextTick, watch } from 'vue'
+import { useVueFlow } from '@vue-flow/core'
+
+const props = defineProps({
+  visible: { type: Boolean, default: false },
+  containerRef: { type: Object, required: true } // 传入 Vue Flow 容器的 ref,用于获取尺寸
+})
+
+const { getViewport } = useVueFlow()
+
+// 标尺尺寸(固定宽度/高度)
+const rulerWidth = 20 // 垂直标尺宽度
+const rulerHeight = 20 // 水平标尺高度
+
+// 获取容器尺寸
+const containerWidth = ref(0)
+const containerHeight = ref(0)
+
+const updateDimensions = () => {
+  if (props.containerRef) {
+    const rect = props.containerRef.getBoundingClientRect()
+    containerWidth.value = rect.width
+    containerHeight.value = rect.height
+    console.log('Container dimensions updated:', containerWidth.value, containerHeight.value) // 调试日志
+  }
+}
+
+let resizeObserver = null
+onMounted(async () => {
+  await nextTick()
+  updateDimensions() // 初始获取尺寸
+
+  resizeObserver = new ResizeObserver(() => {
+    updateDimensions()
+  })
+  if (props.containerRef) {
+    resizeObserver.observe(props.containerRef)
+  }
+})
+
+onUnmounted(() => {
+  if (resizeObserver) {
+    resizeObserver.disconnect()
+  }
+})
+
+// 计算当前 transform(响应式)
+const currentViewport = computed(() => getViewport())
+const scale = computed(() => currentViewport.value.zoom || 1) // 当前缩放,默认1
+const translateX = computed(() => currentViewport.value.x || 0) // 平移 X,默认0
+const translateY = computed(() => currentViewport.value.y || 0) // 平移 Y,默认0
+
+// 动态刻度间隔(基于缩放调整,避免太密/太疏)——固定世界单位间隔,动态生成
+const worldTickInterval = computed(() => {
+  // 根据缩放选择合适的间隔:小缩放时大间隔,避免太密
+  if (scale.value >= 2) return 10; // 放大时小间隔
+  if (scale.value >= 1) return 50;
+  if (scale.value >= 0.5) return 100;
+  return 200; // 缩小时大间隔
+})
+
+// 生成垂直刻度:基于世界坐标范围,确保覆盖整个视口
+const verticalTicks = computed(() => {
+  const ticks = []
+  // 计算视口的世界坐标范围
+  const minWorldY = -translateY.value / scale.value
+  const maxWorldY = (containerHeight.value - translateY.value) / scale.value
+  // 从范围开始生成 ticks,确保有至少 5-10 个
+  let startWorld = Math.floor(minWorldY / worldTickInterval.value) * worldTickInterval.value
+  if (startWorld > minWorldY) startWorld -= worldTickInterval.value // 确保覆盖起始
+
+  for (let worldY = startWorld; worldY <= maxWorldY; worldY += worldTickInterval.value) {
+    // 转换回屏幕坐标
+    const screenY = (worldY * scale.value + translateY.value)
+    if (screenY >= 0 && screenY <= containerHeight.value) { // 只渲染可见的
+      const isMajor = (worldY % (worldTickInterval.value * 5)) === 0 // 每5小刻一大刻
+      ticks.push({ 
+        pos: screenY, 
+        label: isMajor ? Math.round(worldY) : '', 
+        isMajor 
+      })
+    }
+  }
+  console.log('Vertical ticks generated:', ticks.length, ticks) // 调试日志:检查生成数量和位置
+  return ticks
+})
+
+// 生成水平刻度:类似
+const horizontalTicks = computed(() => {
+  const ticks = []
+  const minWorldX = -translateX.value / scale.value
+  const maxWorldX = (containerWidth.value - translateX.value) / scale.value
+  let startWorld = Math.floor(minWorldX / worldTickInterval.value) * worldTickInterval.value
+  if (startWorld > minWorldX) startWorld -= worldTickInterval.value
+
+  for (let worldX = startWorld; worldX <= maxWorldX; worldX += worldTickInterval.value) {
+    const screenX = (worldX * scale.value + translateX.value)
+    if (screenX >= 0 && screenX <= containerWidth.value) {
+      const isMajor = (worldX % (worldTickInterval.value * 5)) === 0
+      ticks.push({ 
+        pos: screenX, 
+        label: isMajor ? Math.round(worldX) : '', 
+        isMajor 
+      })
+    }
+  }
+  console.log('Horizontal ticks generated:', ticks.length) // 调试日志
+  return ticks
+})
+
+// 垂直标尺样式(固定位置,不跟随平移)
+const verticalStyle = computed(() => ({
+  position: 'absolute',
+  left: '0px',
+  top: '0px'
+}))
+
+// 水平标尺样式(避开垂直标尺)
+const horizontalStyle = computed(() => ({
+  position: 'absolute',
+  left: `${rulerWidth}px`,
+  top: '0px'
+}))
+
+// 监听 visible 变化,强制更新尺寸(可选,防初始空)
+watch(() => props.visible, (newVal) => {
+  if (newVal) {
+    nextTick(() => updateDimensions())
+  }
+})
+</script>
+
+<style scoped>
+.ruler-container {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 100;
+  background: transparent;
+}
+
+.ruler-svg {
+  position: absolute;
+  background: #fdfdfd;
+  border: 1px solid #ddd;
+  overflow: visible;
+}
+
+.vertical {
+  z-index: 102;
+}
+
+.horizontal {
+  z-index: 101;
+}
+
+.ruler-corner {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 103; /* 覆盖重叠部分 */
+  background: white;
+  border: 1px solid #ddd;
+}
+</style>

+ 1 - 1
src/components/layout/home.vue

@@ -269,7 +269,7 @@ const handleProjectTabRemove = (projectId) => {
 const handleFileSelected = (file) => {
   const formData = new FormData();
   formData.append('file', file);
-  formData.append('transCode', 'ES0003'); // 假设导入项目的 transCode 为 ES0003
+  formData.append('transCode', 'ES0029');
 
   request(formData, {
     headers: {

+ 6 - 1
src/views/model/index.vue

@@ -155,7 +155,7 @@
                 </el-tab-pane>
               </el-tabs> -->
             <!-- </div> -->
-            <div class="flow-content">
+            <div class="flow-content" ref="flowContentRef">
               <vueflow 
                 ref="vueflowRef" 
                 :jobId="jobId"
@@ -167,6 +167,7 @@
                 @cut="handleCut"
                 @paste="handlePaste"
               />
+              <Ruler :visible="buttonStates.showRuler" :container-ref="flowContentRef" />
             </div>
           </pane>
           <pane min-size="0" size="25" max-size="50">
@@ -219,6 +220,7 @@ import vueflow from "./vueflow/index.vue"
 import useDragAndDrop from "./vueflow/useDnD"
 import { ProjectTree } from "@/components/ProjectTree"
 import TopoButtonBar from "@/components/layout/TopoButtonBar.vue"
+import Ruler from '@/components/Ruler.vue'
 import {
   ZoomIn,
   ZoomOut,
@@ -257,6 +259,7 @@ const showSystemUnitDialog = ref(false)
 const showPersonUnitDialog = ref(false)
 // 存储 SystemUnitDialog 的 tableData
 const systemUnitData = ref([])
+const flowContentRef = ref(null)
 
 // 项目树数据
 const treeData = ref([
@@ -797,6 +800,8 @@ onUnmounted(() => {
   width: 100%;
   flex: 1;
   background: #ffffff;
+  position: relative;
+  overflow: hidden;
 }
 
 .btn-icon {