Selaa lähdekoodia

feat: 调整雷达展示为新组件(画布+检测区域);

liujia 1 viikko sitten
vanhempi
commit
2d4b36c63e

+ 1 - 0
components.d.ts

@@ -68,6 +68,7 @@ declare module 'vue' {
     BasePagination: typeof import('./src/components/basePagination/index.vue')['default']
     BaseWeather: typeof import('./src/components/baseWeather/index.vue')['default']
     Copyright: typeof import('./src/components/Copyright/index.vue')['default']
+    DetectionAreaView: typeof import('./src/components/DetectionAreaView/index.vue')['default']
     ECharts: typeof import('./src/components/baseCard/components/e-charts/index.vue')['default']
     EditableFurniture: typeof import('./src/components/EditableFurniture/index.vue')['default']
     EditableSubregion: typeof import('./src/components/EditableSubregion/index.vue')['default']

+ 641 - 0
src/components/DetectionAreaView/index.vue

@@ -0,0 +1,641 @@
+<template>
+  <div class="detection-area-view">
+    <canvas ref="canvasRef" width="400" height="400"></canvas>
+
+    <div class="overlay-layer">
+      <template v-if="mode === 'view'">
+        <div class="furniture-container" :class="{ 'clip-overflow': mode === 'view' }">
+          <div
+            v-for="item in furnitureItems"
+            :key="item.nanoid"
+            class="furniture-item"
+            :style="getFurnitureStyle(item)"
+          >
+            <furniture-icon
+              :icon="item.type"
+              :width="item.width"
+              :height="item.length"
+              :draggable="false"
+            />
+          </div>
+        </div>
+
+        <div
+          v-for="target in targets"
+          :key="target.id"
+          class="target-point"
+          :style="getTargetStyle(target)"
+        >
+          <span class="target-id">{{ target.id }}</span>
+        </div>
+      </template>
+
+      <template v-else>
+        <slot name="furnitures"> </slot>
+
+        <div class="subregion-container">
+          <slot name="subregion"></slot>
+        </div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch, computed, type CSSProperties } from 'vue'
+import type { FurnitureItem, TargetPoint } from '@/types/radar'
+import {
+  convert_region_r2c,
+  convert_region_c2r,
+  convert_furniture_r2c,
+  convert_furniture_c2r,
+  type RadarRect,
+  type CanvasRect,
+  type RadarPosition,
+  type RadarFurniture,
+} from '@/utils/coordTransform'
+import radarUrl from '@/assets/furnitures/radar.png'
+
+defineOptions({ name: 'DetectionAreaView' })
+
+/**
+ * 组件属性接口定义
+ * @property coordinates - 检测区域坐标 [x起始, x结束, y起始, y结束],单位:厘米
+ * @property direction - 设备方向角度,0-360度,默认90度(东向)
+ * @property furnitureItems - 家具配置项列表
+ * @property targets - 目标点位数据
+ * @property mode - 组件工作模式:view(展示) 或 edit(编辑)
+ */
+interface Props {
+  coordinates: [number, number, number, number]
+  direction?: number
+  furnitureItems?: FurnitureItem[]
+  targets?: TargetPoint[]
+  mode?: 'view' | 'edit'
+}
+
+// 属性默认值配置
+const props = withDefaults(defineProps<Props>(), {
+  direction: 90,
+  furnitureItems: () => [],
+  targets: () => [],
+  mode: 'view',
+})
+
+// 画布配置常量
+const CANVAS_SIZE = 400 // 画布尺寸(像素)
+const RADAR_RANGE = 400 // 雷达检测范围(厘米)
+
+// Canvas 相关引用
+const canvasRef = ref<HTMLCanvasElement | null>(null)
+let ctx: CanvasRenderingContext2D | null = null
+
+// 雷达图片加载处理
+const radarImage = new Image()
+radarImage.src = radarUrl
+let isRadarImageLoaded = false
+
+radarImage.onload = () => {
+  isRadarImageLoaded = true
+  drawDetectionArea()
+}
+
+// 雷达在画布中的中心点位置
+const radarPosition: RadarPosition = {
+  x_radar: CANVAS_SIZE / 2,
+  y_radar: CANVAS_SIZE / 2,
+}
+
+// 设备显示配置
+const device = {
+  x: CANVAS_SIZE / 2, // 设备在画布中的x坐标
+  y: CANVAS_SIZE / 2, // 设备在画布中的y坐标
+  size: 20, // 设备图标尺寸
+}
+
+/**
+ * 地理坐标转换为像素坐标
+ * @param geoX - 地理坐标系x坐标(厘米)
+ * @param geoY - 地理坐标系y坐标(厘米)
+ * @returns 像素坐标系中的坐标点
+ */
+const geoToPixel = (geoX: number, geoY: number): { x: number; y: number } => {
+  const radarFurniture: RadarFurniture = {
+    x: geoX,
+    y: geoY,
+    width: 0,
+    height: 0,
+  }
+
+  const canvasRect = convert_furniture_r2c(radarFurniture, radarPosition)
+  return {
+    x: canvasRect.left,
+    y: canvasRect.top,
+  }
+}
+
+/**
+ * 像素坐标转换为地理坐标
+ * @param pixelX - 像素坐标系x坐标
+ * @param pixelY - 像素坐标系y坐标
+ * @returns 地理坐标系中的坐标点
+ */
+const pixelToGeo = (pixelX: number, pixelY: number): { x: number; y: number } => {
+  const canvasRect: CanvasRect = {
+    left: pixelX,
+    top: pixelY,
+    width: 0,
+    height: 0,
+  }
+
+  const radarFurniture = convert_furniture_c2r(canvasRect, radarPosition)
+  return {
+    x: radarFurniture.x,
+    y: radarFurniture.y,
+  }
+}
+
+/**
+ * 根据设备方向转换检测区域坐标
+ * 将原始检测区域坐标转换为设备当前朝向下的实际坐标
+ * @param area - 原始检测区域坐标 [xStart, xEnd, yStart, yEnd]
+ * @param direction - 设备方向角度(0-360度)
+ * @returns 转换后的检测区域坐标
+ */
+const convertAreaByDirection = (
+  area: [number, number, number, number],
+  direction: number
+): [number, number, number, number] => {
+  const [xStart, xEnd, yStart, yEnd] = area
+  const normalizedDirection = ((direction % 360) + 360) % 360
+
+  switch (normalizedDirection) {
+    case 0: // 设备朝北:x轴反向,y轴保持不变
+      return [-xEnd, -xStart, yStart, yEnd]
+    case 90: // 设备朝东:保持原始坐标系
+      return [xStart, xEnd, yStart, yEnd]
+    case 180: // 设备朝南:x轴保持不变,y轴反向
+      return [xStart, xEnd, -yEnd, -yStart]
+    case 270: // 设备朝西:x轴反向,y轴反向
+      return [-xEnd, -xStart, -yEnd, -yStart]
+    default: // 其他角度使用任意角度转换
+      return convertAreaForArbitraryAngle(area, direction)
+  }
+}
+
+/**
+ * 任意角度的检测区域坐标转换
+ * 通过旋转矩阵计算区域四个角点在新方向下的坐标范围
+ * @param area - 原始检测区域坐标
+ * @param direction - 设备方向角度
+ * @returns 旋转后的检测区域边界坐标
+ */
+const convertAreaForArbitraryAngle = (
+  area: [number, number, number, number],
+  direction: number
+): [number, number, number, number] => {
+  const [xStart, xEnd, yStart, yEnd] = area
+
+  // 定义检测区域的四个角点
+  const corners = [
+    { x: xStart, y: yStart }, // 左下角
+    { x: xEnd, y: yStart }, // 右下角
+    { x: xStart, y: yEnd }, // 左上角
+    { x: xEnd, y: yEnd }, // 右上角
+  ]
+
+  // 计算旋转角度(逆时针旋转)
+  const rotationAngle = (-direction * Math.PI) / 180
+  const cosA = Math.cos(rotationAngle)
+  const sinA = Math.sin(rotationAngle)
+
+  // 应用旋转矩阵变换到所有角点
+  const rotatedCorners = corners.map((corner) => {
+    return {
+      x: corner.x * cosA - corner.y * sinA,
+      y: corner.x * sinA + corner.y * cosA,
+    }
+  })
+
+  // 计算旋转后的边界范围
+  const xValues = rotatedCorners.map((c) => c.x)
+  const yValues = rotatedCorners.map((c) => c.y)
+
+  return [Math.min(...xValues), Math.max(...xValues), Math.min(...yValues), Math.max(...yValues)]
+}
+
+/**
+ * 计算转换后的检测区域坐标(响应式)
+ * 根据当前设备方向实时计算检测区域的实际坐标
+ */
+const convertedCoordinates = computed(() => {
+  return convertAreaByDirection(props.coordinates, props.direction)
+})
+
+/**
+ * 计算家具元素的样式
+ * 将地理坐标系中的家具位置转换为画布中的像素位置和样式
+ * @param item - 家具配置项
+ * @returns 家具的CSS样式对象
+ */
+const getFurnitureStyle = (item: FurnitureItem) => {
+  const radarFurniture: RadarFurniture = {
+    x: item?.x || 0,
+    y: item?.y || 0,
+    width: item.width,
+    height: item.length,
+  }
+
+  const canvasRect = convert_furniture_r2c(radarFurniture, radarPosition)
+
+  return {
+    position: 'absolute',
+    left: `${canvasRect.left - item.width / 2}px`, // 居中定位
+    top: `${canvasRect.top - item.length / 2}px`, // 居中定位
+    width: `${item.width}px`,
+    height: `${item.length}px`,
+    transform: `rotate(${item.rotate}deg)`, // 家具自身旋转
+    transformOrigin: 'center center',
+    pointerEvents: props.mode === 'edit' ? 'auto' : 'none', // 编辑模式下可交互
+  } as CSSProperties
+}
+
+/**
+ * 计算目标点位的样式
+ * 将地理坐标系中的目标位置转换为画布中的像素位置和样式
+ * @param target - 目标点位数据
+ * @returns 目标点位的CSS样式对象
+ */
+const getTargetStyle = (target: TargetPoint) => {
+  const radarFurniture: RadarFurniture = {
+    x: target.x,
+    y: target.y,
+    width: 0,
+    height: 0,
+  }
+
+  const canvasRect = convert_furniture_r2c(radarFurniture, radarPosition)
+
+  // 定义红橙黄绿青蓝紫颜色序列
+  const colorPalette = [
+    'red', // 0: 红
+    'orange', // 1: 橙
+    'yellow', // 2: 黄
+    'green', // 3: 绿
+    'cyan', // 4: 青
+    'blue', // 5: 蓝
+    'purple', // 6: 紫
+  ]
+
+  // 如果 target.id 超出颜色数组范围,使用最后一个颜色(紫色)
+  const colorIndex = target.id < colorPalette.length ? target.id : colorPalette.length - 1
+  const color = colorPalette[colorIndex]
+
+  return {
+    position: 'absolute',
+    width: '18px',
+    height: '18px',
+    background: color,
+    borderRadius: '50%',
+    transform: `translate3d(${canvasRect.left}px, ${canvasRect.top}px, 0) translate(-50%, -50%)`,
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center',
+    color: 'white',
+    fontSize: '12px',
+    fontWeight: '600',
+    pointerEvents: 'none',
+    zIndex: 10,
+    transition: 'transform 1s linear',
+    willChange: 'transform',
+    boxShadow: '0 0 8px rgba(0, 0, 0, 0.5)',
+    userSelect: 'none',
+  } as CSSProperties
+}
+
+/**
+ * 初始化Canvas上下文
+ * 获取Canvas的2D渲染上下文并开始绘制
+ */
+const initCanvas = () => {
+  if (!canvasRef.value) return
+
+  ctx = canvasRef.value.getContext('2d')
+  if (!ctx) return
+
+  drawDetectionArea()
+}
+
+/**
+ * 绘制完整的检测区域视图
+ * 包含背景、网格、边界、坐标轴、设备和信息文本
+ */
+const drawDetectionArea = () => {
+  if (!ctx || !canvasRef.value) return
+
+  // 清除画布
+  ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE)
+
+  // 设置浅灰色背景
+  ctx.fillStyle = '#f2f2f0'
+  ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE)
+
+  // 依次绘制各个图层
+  drawGrid() // 背景网格
+  drawRadarBoundary() // 雷达边界
+  drawAreaBorder() // 检测区域边框
+  drawCoordinateAxes() // 坐标轴
+  drawRadarDevice() // 雷达设备
+  drawInfoText() // 信息文本
+}
+
+/**
+ * 绘制雷达设备图标
+ * 根据设备方向旋转显示雷达图标
+ */
+const drawRadarDevice = () => {
+  if (!ctx) return
+
+  // 图片未加载时使用备用绘制方案
+  if (!isRadarImageLoaded) {
+    drawRadarDeviceFallback()
+    return
+  }
+
+  // 保存当前画布状态
+  ctx.save()
+
+  // 移动到设备中心并应用旋转
+  ctx.translate(device.x, device.y)
+  const rotation = props.direction
+  ctx.rotate((rotation * Math.PI) / 180)
+
+  // 绘制居中的雷达图标
+  const imageSize = device.size
+  ctx.drawImage(radarImage, -imageSize / 2, -imageSize / 2, imageSize, imageSize)
+
+  // 恢复画布状态
+  ctx.restore()
+}
+
+/**
+ * 雷达设备备用绘制方案
+ * 在雷达图片加载失败时使用矢量图形绘制设备
+ */
+const drawRadarDeviceFallback = () => {
+  if (!ctx) return
+
+  // 绘制设备主体(红色圆形)
+  ctx.fillStyle = '#e74c3c'
+  ctx.beginPath()
+  ctx.arc(device.x, device.y, device.size / 2, 0, Math.PI * 2)
+  ctx.fill()
+
+  // 保存画布状态以应用旋转
+  ctx.save()
+  ctx.translate(device.x, device.y)
+  const rotation = props.direction
+  ctx.rotate((rotation * Math.PI) / 180)
+
+  // 绘制方向指示器(白色三角形)
+  ctx.fillStyle = '#fff'
+  ctx.strokeStyle = '#e74c3c'
+  ctx.lineWidth = 1
+
+  const indicatorLength = device.size * 0.6
+  const indicatorWidth = device.size * 0.3
+
+  ctx.beginPath()
+  ctx.moveTo(0, -device.size / 2 - indicatorLength) // 尖角顶点
+  ctx.lineTo(-indicatorWidth / 2, -device.size / 2) // 左下角
+  ctx.lineTo(indicatorWidth / 2, -device.size / 2) // 右下角
+  ctx.closePath()
+  ctx.fill()
+  ctx.stroke()
+
+  ctx.restore()
+}
+
+/**
+ * 绘制信息文本
+ * 在画布左下角显示检测区域坐标信息
+ */
+const drawInfoText = () => {
+  if (!ctx) return
+
+  const [xStart, xEnd, yStart, yEnd] = props.coordinates
+  const [convertedXStart, convertedXEnd, convertedYStart, convertedYEnd] =
+    convertedCoordinates.value
+
+  ctx.fillStyle = '#2c3e50'
+  ctx.font = '12px Arial'
+  ctx.textAlign = 'left'
+
+  // 文本显示位置(左下角)
+  const baseX = 5
+  const baseY = CANVAS_SIZE - 10
+
+  if (props.direction === 90) {
+    // 默认方向只显示原始坐标
+    ctx.fillText(`检测区域: [${xStart}, ${xEnd}, ${yStart}, ${yEnd}] cm`, baseX, baseY)
+  } else {
+    // 其他方向显示原始和转换后的坐标
+    ctx.fillText(`原始区域: [${xStart}, ${xEnd}, ${yStart}, ${yEnd}] cm`, baseX, baseY - 15)
+    ctx.fillText(
+      `转换区域: [${Math.round(convertedXStart)}, ${Math.round(convertedXEnd)}, ${Math.round(convertedYStart)}, ${Math.round(convertedYEnd)}] cm`,
+      baseX,
+      baseY
+    )
+  }
+}
+
+/**
+ * 绘制背景网格
+ * 50px间距的灰色网格线,辅助定位
+ */
+const drawGrid = () => {
+  if (!ctx) return
+
+  ctx.strokeStyle = '#d9d9d7'
+  ctx.lineWidth = 0.5
+
+  // 绘制水平网格线
+  for (let i = 50; i < CANVAS_SIZE; i += 50) {
+    ctx.beginPath()
+    ctx.moveTo(0, i)
+    ctx.lineTo(CANVAS_SIZE, i)
+    ctx.stroke()
+  }
+
+  // 绘制垂直网格线
+  for (let i = 50; i < CANVAS_SIZE; i += 50) {
+    ctx.beginPath()
+    ctx.moveTo(i, 0)
+    ctx.lineTo(i, CANVAS_SIZE)
+    ctx.stroke()
+  }
+}
+
+/**
+ * 绘制雷达检测区域边界
+ * 整个画布范围的边界框,表示雷达的最大检测范围
+ */
+const drawRadarBoundary = () => {
+  if (!ctx) return
+
+  ctx.strokeStyle = '#95a5a6'
+  ctx.lineWidth = 1
+  ctx.strokeRect(0, 0, CANVAS_SIZE, CANVAS_SIZE)
+}
+
+/**
+ * 绘制检测区域边框
+ * 实际的检测子区域,使用蓝色边框和半透明填充
+ */
+const drawAreaBorder = () => {
+  if (!ctx) return
+
+  const [xStart, xEnd, yStart, yEnd] = convertedCoordinates.value
+
+  // 确保检测区域不超出雷达范围
+  const clampedXStart = Math.max(xStart, -RADAR_RANGE / 2)
+  const clampedXEnd = Math.min(xEnd, RADAR_RANGE / 2)
+  const clampedYStart = Math.max(yStart, -RADAR_RANGE / 2)
+  const clampedYEnd = Math.min(yEnd, RADAR_RANGE / 2)
+
+  // 转换到画布坐标系
+  const radarRect: RadarRect = {
+    x_cm_start: clampedXStart,
+    x_cm_stop: clampedXEnd,
+    y_cm_start: clampedYStart,
+    y_cm_stop: clampedYEnd,
+  }
+
+  const canvasRect = convert_region_r2c(radarRect, radarPosition)
+
+  // 绘制蓝色边框
+  ctx.strokeStyle = '#1677ff'
+  ctx.lineWidth = 3
+  ctx.strokeRect(canvasRect.left, canvasRect.top, canvasRect.width, canvasRect.height)
+
+  // 添加半透明蓝色填充
+  ctx.fillStyle = 'rgba(52, 152, 219, 0.3)'
+  ctx.fillRect(canvasRect.left, canvasRect.top, canvasRect.width, canvasRect.height)
+}
+
+/**
+ * 绘制坐标轴
+ * 虚线样式的X轴和Y轴,标识坐标系中心
+ */
+const drawCoordinateAxes = () => {
+  if (!ctx) return
+
+  const center = CANVAS_SIZE / 2
+
+  ctx.strokeStyle = '#2c3e50'
+  ctx.lineWidth = 1
+  ctx.setLineDash([5, 5]) // 虚线样式
+
+  // 绘制X轴(水平线)
+  ctx.beginPath()
+  ctx.moveTo(0, center)
+  ctx.lineTo(CANVAS_SIZE, center)
+  ctx.stroke()
+
+  // 绘制Y轴(垂直线)
+  ctx.beginPath()
+  ctx.moveTo(center, 0)
+  ctx.lineTo(center, CANVAS_SIZE)
+  ctx.stroke()
+
+  ctx.setLineDash([]) // 恢复实线样式
+}
+
+/**
+ * 监听属性变化
+ * 当坐标、方向或模式变化时重新绘制检测区域
+ */
+watch(
+  () => [props.coordinates, props.direction, props.mode],
+  () => {
+    drawDetectionArea()
+  },
+  { deep: true }
+)
+
+// 组件挂载时初始化Canvas
+onMounted(() => {
+  initCanvas()
+})
+
+/**
+ * 暴露给父组件的方法和属性
+ * 提供坐标转换功能和雷达位置信息
+ */
+defineExpose({
+  geoToPixel,
+  pixelToGeo,
+  convertAreaByDirection,
+  convert_region_r2c: (rect: RadarRect) => convert_region_r2c(rect, radarPosition),
+  convert_region_c2r: (rect: CanvasRect) => convert_region_c2r(rect, radarPosition),
+  convert_furniture_r2c: (furniture: RadarFurniture) =>
+    convert_furniture_r2c(furniture, radarPosition),
+  convert_furniture_c2r: (furniture: CanvasRect) => convert_furniture_c2r(furniture, radarPosition),
+  radarPosition,
+})
+</script>
+
+<style scoped lang="less">
+.detection-area-view {
+  position: relative;
+  display: inline-block;
+  width: 400px;
+  height: 400px;
+
+  canvas {
+    border: 1px solid #ddd;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+    pointer-events: none;
+  }
+
+  .overlay-layer {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 400px;
+    height: 400px;
+    pointer-events: auto;
+
+    .furniture-container {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+
+      &.clip-overflow {
+        overflow: hidden;
+        pointer-events: none;
+      }
+
+      .furniture-item {
+        position: absolute;
+      }
+    }
+
+    .target-point {
+      position: absolute;
+      pointer-events: none;
+    }
+
+    .subregion-container {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      pointer-events: auto;
+    }
+  }
+}
+</style>

+ 271 - 267
src/components/EditableFurniture/index.vue

@@ -1,28 +1,18 @@
 <template>
   <div
     class="editable-furniture"
-    :style="{
-      position: 'absolute',
-      left: `${centerCss.left}px`,
-      top: `${centerCss.top}px`,
-      pointerEvents: 'auto',
-    }"
+    :style="containerStyle"
+    @mousedown.stop="startDrag"
+    @dblclick.stop="showPanel = !showPanel"
   >
-    <div
-      class="furniture-wrapper"
-      :style="wrapperStyle"
-      @mousedown.stop="startDrag"
-      @dblclick.stop="showPanel = !showPanel"
-    >
-      <furnitureIcon
-        :icon="localItem.type as FurnitureIconType"
-        :width="localItem.width"
-        :height="localItem.length"
-      />
+    <div class="furniture-rotated-container" :style="rotatedContainerStyle">
+      <div class="furniture-wrapper">
+        <furnitureIcon :icon="localItem.type" :width="localItem.width" :height="localItem.length" />
+      </div>
     </div>
 
     <div
-      v-if="showPanel"
+      v-if="false"
       class="property-panel"
       :style="{ left: `${panelPosition.left}px`, top: `${panelPosition.top}px` }"
       @mousedown.stop
@@ -52,8 +42,8 @@
       </div>
 
       <div class="panel-item">
-        <label>旋转角度:</label>
-        <a-select v-model:value="localItem.rotate" size="small">
+        <label>家具旋转:</label>
+        <a-select v-model:value="localItem.rotate" size="small" @change="onRotateChange">
           <a-select-option :value="0">0°</a-select-option>
           <a-select-option :value="90">90°</a-select-option>
           <a-select-option :value="180">180°</a-select-option>
@@ -73,10 +63,28 @@
       </div>
 
       <div class="panel-item">
-        <label>平面坐标:</label>
+        <label>left/top:</label>
+        <a-space>
+          <a-input-number
+            v-model:value="localItem.left"
+            @change="updateByGeoCoords"
+            disabled
+            size="small"
+          />
+          <a-input-number
+            v-model:value="localItem.top"
+            @change="updateByGeoCoords"
+            disabled
+            size="small"
+          />
+        </a-space>
+      </div>
+
+      <div class="panel-item">
+        <label>x/y:</label>
         <a-space>
-          x:<a-input-number v-model:value="localItem.x" @change="updateByFlatCoords" size="small" />
-          y:<a-input-number v-model:value="localItem.y" @change="updateByFlatCoords" size="small" />
+          <a-input-number v-model:value="localItem.x" @change="updateByGeoCoords" size="small" />
+          <a-input-number v-model:value="localItem.y" @change="updateByGeoCoords" size="small" />
         </a-space>
       </div>
 
@@ -89,7 +97,7 @@
         </a-space>
       </div>
 
-      <!-- <pre class="debug">{{ localItem }}</pre> -->
+      <pre>{{ localItem }}</pre>
     </div>
   </div>
 </template>
@@ -98,14 +106,14 @@
 import { reactive, watch, ref, computed, onMounted, nextTick, type CSSProperties } from 'vue'
 import type { FurnitureItem } from '@/types/radar'
 import furnitureIcon from '../furnitureIcon/index.vue'
-import type { FurnitureIconType } from '@/types/furniture'
+// import type { FurnitureIconType } from '@/types/furniture'
 import {
   ArrowLeftOutlined,
   ArrowUpOutlined,
   ArrowDownOutlined,
   ArrowRightOutlined,
 } from '@ant-design/icons-vue'
-import { useRadarCoordinateTransform } from '@/hooks/useRadarCoordinateTransform'
+import type { FurnitureType } from '@/api/room/types'
 
 defineOptions({ name: 'EditableFurniture' })
 
@@ -113,301 +121,283 @@ interface Props {
   item: FurnitureItem
   angle: number
   coordinates: [number, number, number, number]
+  canvasSize?: number
   initialAtOrigin?: boolean
+  disabled?: boolean
 }
-const props = defineProps<Props>()
+
+const props = withDefaults(defineProps<Props>(), {
+  canvasSize: 400,
+  initialAtOrigin: false,
+  disabled: false,
+})
+
 const emit = defineEmits<{
-  (e: 'update', item: FurnitureItem & { left?: number; top?: number }): void
+  (e: 'update', item: FurnitureItem): void
   (e: 'delete', nanoid: string): void
 }>()
 
 const showPanel = ref(false)
 const nudgeStep = ref(5)
+// const showDebug = ref(true) // 调试模式
+
+// 坐标转换函数
+const geoToPixel = (geoX: number, geoY: number): { x: number; y: number } => {
+  return {
+    x: props.canvasSize / 2 + geoX,
+    y: props.canvasSize / 2 - geoY,
+  }
+}
 
-// 扩展类型:在组件内部我们也维护页面像素 left/top(可选)
-type UIItem = FurnitureItem & { left?: number; top?: number }
+const pixelToGeo = (pixelX: number, pixelY: number): { x: number; y: number } => {
+  return {
+    x: pixelX - props.canvasSize / 2,
+    y: props.canvasSize / 2 - pixelY,
+  }
+}
+
+// 计算旋转后的边界框(基于左上角基准)
+const calculateBoundingBox = () => {
+  const { width, length, rotate } = localItem
+  const rad = (rotate * Math.PI) / 180
+
+  if (rotate === 0) {
+    // 未旋转时,边界框就是家具本身
+    return {
+      left: 0,
+      right: width,
+      top: 0,
+      bottom: length,
+      width: width,
+      height: length,
+    }
+  }
+
+  // 计算旋转后的四个角点(相对于左上角)
+  const corners = [
+    { x: 0, y: 0 }, // 左上
+    { x: width, y: 0 }, // 右上
+    { x: width, y: length }, // 右下
+    { x: 0, y: length }, // 左下
+  ]
+
+  // 旋转角点(围绕左上角旋转)
+  const rotatedCorners = corners.map((corner) => ({
+    x: corner.x * Math.cos(rad) - corner.y * Math.sin(rad),
+    y: corner.x * Math.sin(rad) + corner.y * Math.cos(rad),
+  }))
+
+  // 计算边界框
+  const xs = rotatedCorners.map((c) => c.x)
+  const ys = rotatedCorners.map((c) => c.y)
+
+  const minX = Math.min(...xs)
+  const maxX = Math.max(...xs)
+  const minY = Math.min(...ys)
+  const maxY = Math.max(...ys)
+
+  return {
+    left: minX,
+    right: maxX,
+    top: minY,
+    bottom: maxY,
+    width: maxX - minX,
+    height: maxY - minY,
+  }
+}
 
-// localItem 保留业务语义 x/y 为业务左上角,同时维护 left/top 为页面像素(px)
-const localItem = reactive<UIItem>({
+// 本地家具数据
+const localItem = reactive<FurnitureItem>({
   ...props.item,
   x: props.item.x ?? 0,
   y: props.item.y ?? 0,
   width: props.item.width ?? 100,
   length: props.item.length ?? 100,
   rotate: props.item.rotate ?? 0,
-  left: (props.item as UIItem).left ?? 0,
-  top: (props.item as UIItem).top ?? 0,
+  type: props.item.type as FurnitureType,
 })
 
+// 像素位置和边界框
+const pixelPosition = reactive({ left: 0, top: 0 })
+const panelPosition = reactive({ left: 0, top: 0 })
+
+// 计算属性
+const boundingBox = computed(() => calculateBoundingBox())
+
+// 容器样式 - 基于边界框
+const containerStyle = computed(
+  () =>
+    ({
+      position: 'absolute',
+      left: `${pixelPosition.left}px`,
+      top: `${pixelPosition.top}px`,
+      width: `${boundingBox.value.width}px`,
+      height: `${boundingBox.value.height}px`,
+      pointerEvents: props.disabled ? 'none' : 'auto',
+      zIndex: 100,
+      cursor: 'move',
+    }) as CSSProperties
+)
+
+// 旋转容器样式 - 调整位置使左上角对齐
+const rotatedContainerStyle = computed(
+  () =>
+    ({
+      position: 'absolute',
+      left: `${-boundingBox.value.left}px`,
+      top: `${-boundingBox.value.top}px`,
+      width: `${localItem.width}px`,
+      height: `${localItem.length}px`,
+      transform: `rotate(${localItem.rotate}deg)`,
+      transformOrigin: '0 0', // 以左上角为旋转中心
+    }) as CSSProperties
+)
+
+// 监听props变化
 watch(
   () => props.item,
-  (next) => {
-    if (!next) return
-    const { x, y, width, length, rotate, name, type, nanoid, left, top } = next as Partial<UIItem>
-
-    if (x !== undefined) localItem.x = x
-    if (y !== undefined) localItem.y = y
-    if (width !== undefined) localItem.width = width
-    if (length !== undefined) localItem.length = length
-    if (rotate !== undefined) localItem.rotate = rotate
-    if (name !== undefined) localItem.name = name
-    if (type !== undefined) localItem.type = type
-    if (nanoid !== undefined) localItem.nanoid = nanoid
-    if (left !== undefined) localItem.left = left
-    if (top !== undefined) localItem.top = top
-
-    updateXYFromCss()
+  (newItem) => {
+    if (!newItem) return
+    Object.assign(localItem, {
+      ...newItem,
+      x: newItem.x ?? 0,
+      y: newItem.y ?? 0,
+      width: newItem.width ?? 100,
+      length: newItem.length ?? 100,
+      rotate: newItem.rotate ?? 0,
+    })
+    updatePixelPosition()
   },
   { deep: true }
 )
 
-// transforms(editor <-> radar <-> css)
-let transforms = useRadarCoordinateTransform(props.angle, props.coordinates)
+// 监听坐标和角度变化
 watch(
-  () => [props.angle, props.coordinates] as const,
+  () => [props.coordinates, props.angle],
   () => {
-    transforms = useRadarCoordinateTransform(props.angle, props.coordinates)
-    nextTick()
-      .then(() => new Promise<void>((r) => requestAnimationFrame(() => r())))
-      .then(() => new Promise((r) => setTimeout(r, 0)))
-      .then(() => {
-        updateCssFromXY()
-        setPanelInitialPosition()
-      })
+    nextTick(() => {
+      updatePixelPosition()
+      setPanelInitialPosition()
+    })
   },
   { deep: true }
 )
 
-// 渲染用中心点(DOM center,units: px)
-const centerCss = reactive({ left: 0, top: 0 })
+// 更新像素位置(从地理坐标转换)- 左上角基准
+const updatePixelPosition = () => {
+  const pixelPos = geoToPixel(localItem.x, localItem.y)
 
-// 面板位置(相对于 outer 根容器,即相对于 centerCss)
-const panelPosition = reactive({ left: 0, top: 0 })
+  // 直接使用左上角位置
+  pixelPosition.left = pixelPos.x
+  pixelPosition.top = pixelPos.y
 
-const DECIMALS = 4
-const EPSILON = 1e-10
-function norm(n: number): number {
-  const r = Math.round(n * Math.pow(10, DECIMALS)) / Math.pow(10, DECIMALS)
-  return Math.abs(r) < EPSILON ? 0 : r
-}
-function normXY(x: number, y: number): { x: number; y: number } {
-  return { x: norm(x), y: norm(y) }
+  console.log('Update pixel position (top-left):', {
+    geo: { x: localItem.x, y: localItem.y },
+    pixelPosition: { ...pixelPosition },
+    boundingBox: boundingBox.value,
+    canvasCenter: { x: 200, y: 200 },
+  })
 }
 
-// 旋转矩阵(编辑器坐标:Y 向上为正)
-function rot(x: number, y: number, deg: number) {
-  const rad = (deg * Math.PI) / 180
-  return {
-    x: x * Math.cos(rad) - y * Math.sin(rad),
-    y: x * Math.sin(rad) + y * Math.cos(rad),
-  }
+// 更新地理坐标(从像素位置转换)- 左上角基准
+const updateGeoPosition = () => {
+  // 直接转换左上角坐标
+  const geoPos = pixelToGeo(pixelPosition.left, pixelPosition.top)
+  localItem.x = geoPos.x
+  localItem.y = geoPos.y
+
+  const pixelPos = geoToPixel(geoPos.x, geoPos.y)
+  localItem.left = pixelPos.x
+  localItem.top = pixelPos.y
+
+  console.log('Update geo position (top-left):', {
+    pixel: { left: pixelPosition.left, top: pixelPosition.top },
+    geo: { x: localItem.x, y: localItem.y },
+  })
 }
 
-// 计算四角相对中心偏移(editor 坐标)
-function rotatedCornersOffsets(rotate: number, w: number, h: number) {
-  return {
-    topLeft: rot(-w / 2, +h / 2, rotate),
-    topRight: rot(+w / 2, +h / 2, rotate),
-    bottomRight: rot(+w / 2, -h / 2, rotate),
-    bottomLeft: rot(-w / 2, -h / 2, rotate),
-  }
-}
-
-// ------------- 业务坐标系 <-> 编辑器坐标系 映射 -------------
-function businessToEditor(x: number, y: number) {
-  return rot(x, y, 90)
-}
-function editorToBusiness(x: number, y: number) {
-  return rot(x, y, -90)
-}
-// -----------------------------------------------------------
-
-// helper: 把业务左上角 -> 页面左上角(cssTopLeft)
-function businessTopLeftToCssTopLeft(bizX: number, bizY: number) {
-  const editorPt = businessToEditor(bizX, bizY)
-  const radarPt = transforms.editorToRadar(editorPt.x, editorPt.y)
-  const cssTopLeft = transforms.radarToCss(radarPt.x, radarPt.y)
-  return cssTopLeft
-}
-
-// updateCssFromXY:计算 cssTopLeft, 更新 centerCss,并写回 localItem.left/top(方案 A)
-function updateCssFromXY(): void {
-  const w = localItem.width ?? 0
-  const h = localItem.length ?? 0
-
-  const bizLeft = localItem.x ?? 0
-  const bizTop = localItem.y ?? 0
-
-  const cssTopLeft = businessTopLeftToCssTopLeft(bizLeft, bizTop)
-
-  // 写回页面像素(left/top),与 RadarView 的字段语义一致
-  localItem.left = cssTopLeft.left
-  localItem.top = cssTopLeft.top
-
-  // centerCss = cssTopLeft + half size (wrapper uses translate(-50%,-50%))
-  centerCss.left = cssTopLeft.left + w / 2
-  centerCss.top = cssTopLeft.top + h / 2
-}
-
-// 拖拽回写:centerCss -> cssTopLeft -> biz -> 回写 localItem.x/y;同时更新 left/top(页面像素)
-function updateXYFromCss(): void {
-  const w = localItem.width ?? 0
-  const h = localItem.length ?? 0
-
-  const cssTopLeftX = centerCss.left - w / 2
-  const cssTopLeftY = centerCss.top - h / 2
-
-  const radarPt = transforms.cssToRadar(cssTopLeftX, cssTopLeftY)
-  const editorPt = transforms.radarToEditor(radarPt.x, radarPt.y)
-  const bizPt = editorToBusiness(editorPt.x, editorPt.y)
-
-  // 回写业务左上角
-  localItem.x = bizPt.x
-  localItem.y = bizPt.y
-  ;({ x: localItem.x, y: localItem.y } = normXY(localItem.x!, localItem.y!))
-
-  // 同步页面像素字段(保持和 RadarView 一致)
-  localItem.left = cssTopLeftX
-  localItem.top = cssTopLeftY
-}
-
-// 初始化面板位置(相对于 outer,即相对于 centerCss)
-function setPanelInitialPosition() {
-  const w = localItem.width ?? 0
-  const h = localItem.length ?? 0
-  panelPosition.left = Math.round(w / 2 + 50)
-  panelPosition.top = Math.round(-h / 2)
+// 初始化面板位置
+const setPanelInitialPosition = () => {
+  panelPosition.left = Math.round(boundingBox.value.width + 20)
+  panelPosition.top = Math.round(-10)
 }
 
+// 组件挂载
 onMounted(() => {
-  localItem.x = props.item.x ?? localItem.x
-  localItem.y = props.item.y ?? localItem.y
-  ;({ x: localItem.x, y: localItem.y } = normXY(localItem.x!, localItem.y!))
+  console.log('EditableFurniture mounted (top-left基准):', {
+    item: props.item,
+    coordinates: props.coordinates,
+    angle: props.angle,
+    canvasSize: props.canvasSize,
+  })
 
   if (props.initialAtOrigin) {
     localItem.x = 0
     localItem.y = 0
   }
 
-  const hasLeftTopFromProps =
-    (props.item as Partial<UIItem>).left !== undefined &&
-    (props.item as Partial<UIItem>).top !== undefined
-
-  nextTick()
-    .then(() => new Promise<void>((r) => requestAnimationFrame(() => r())))
-    .then(() => {
-      if (!hasLeftTopFromProps || props.initialAtOrigin) {
-        updateCssFromXY()
-      } else {
-        localItem.left = (props.item as Partial<UIItem>).left!
-        localItem.top = (props.item as Partial<UIItem>).top!
-        centerCss.left = localItem.left + (localItem.width ?? 0) / 2
-        centerCss.top = localItem.top + (localItem.length ?? 0) / 2
-      }
-
-      updateXYFromCss()
-    })
-    .then(() => {
-      setPanelInitialPosition()
-      emit('update', { ...localItem })
-    })
+  nextTick(() => {
+    updatePixelPosition()
+    setPanelInitialPosition()
+    emit('update', { ...localItem })
+  })
 })
 
-// wrapper style: 以 centerCss 为外层定位点,内部以中心旋转
-const wrapperStyle = computed(
-  () =>
-    ({
-      width: `${localItem.width}px`,
-      height: `${localItem.length}px`,
-      transform: `translate(-50%, -50%) rotate(${localItem.rotate}deg)`,
-      transformOrigin: '50% 50%',
-      cursor: 'move',
-      position: 'absolute',
-      left: '0px',
-      top: '0px',
-      display: 'flex',
-      alignItems: 'center',
-      justifyContent: 'center',
-    }) as CSSProperties
-)
-
-function updateByFlatCoords(): void {
-  ;({ x: localItem.x, y: localItem.y } = normXY(localItem.x!, localItem.y!))
-  updateCssFromXY()
+// 通过地理坐标更新
+const updateByGeoCoords = () => {
+  updatePixelPosition()
   emit('update', { ...localItem })
 }
 
-function onSizeChange() {
-  updateCssFromXY()
+// 尺寸变化处理
+const onSizeChange = () => {
+  updatePixelPosition()
   setPanelInitialPosition()
   emit('update', { ...localItem })
 }
 
-// 旋转监听:保持业务中心不动并更新左上角与渲染中心,写回 left/top
-watch(
-  () => localItem.rotate,
-  (angle, oldAngle) => {
-    const w = localItem.width ?? 0
-    const h = localItem.length ?? 0
-
-    const offsetsOld = rotatedCornersOffsets(oldAngle ?? angle, w, h)
-    const topLeftOldEditor = offsetsOld.topLeft
-    const topLeftOldBiz = editorToBusiness(topLeftOldEditor.x, topLeftOldEditor.y)
-    const centerBizX = localItem.x! - topLeftOldBiz.x
-    const centerBizY = localItem.y! - topLeftOldBiz.y
-
-    const offsetsNew = rotatedCornersOffsets(angle, w, h)
-    const topLeftNewEditor = offsetsNew.topLeft
-    const topLeftNewBiz = editorToBusiness(topLeftNewEditor.x, topLeftNewEditor.y)
-
-    localItem.x = centerBizX + topLeftNewBiz.x
-    localItem.y = centerBizY + topLeftNewBiz.y
-    ;({ x: localItem.x, y: localItem.y } = normXY(localItem.x!, localItem.y!))
-
-    const editorCenter = businessToEditor(centerBizX, centerBizY)
-    const radarCenter = transforms.editorToRadar(editorCenter.x, editorCenter.y)
-    const css = transforms.radarToCss(radarCenter.x, radarCenter.y)
-
-    // 更新 centerCss + left/top
-    centerCss.left = css.left
-    centerCss.top = css.top
-    localItem.left = css.left - w / 2
-    localItem.top = css.top - h / 2
-
-    emit('update', { ...localItem })
-  }
-)
+// 旋转变化处理
+const onRotateChange = () => {
+  updatePixelPosition()
+  setPanelInitialPosition()
+  emit('update', { ...localItem })
+}
 
-// 拖拽家具:直接修改 centerCss 并回写业务坐标与 left/top
-function startDrag(e: MouseEvent): void {
+// 拖拽家具
+const startDrag = (e: MouseEvent) => {
+  console.log('Start drag furniture (top-left):', localItem.nanoid)
   e.preventDefault()
   e.stopPropagation()
+
   const startX = e.clientX
   const startY = e.clientY
-  const initLeft = centerCss.left
-  const initTop = centerCss.top
+  const initLeft = pixelPosition.left
+  const initTop = pixelPosition.top
 
   const onMove = (ev: MouseEvent) => {
-    centerCss.left = initLeft + (ev.clientX - startX)
-    centerCss.top = initTop + (ev.clientY - startY)
-    updateXYFromCss()
-    // emit with updated business coords and page left/top
+    pixelPosition.left = initLeft + (ev.clientX - startX)
+    pixelPosition.top = initTop + (ev.clientY - startY)
+    updateGeoPosition()
     emit('update', { ...localItem })
   }
+
   const onUp = () => {
     window.removeEventListener('mousemove', onMove)
     window.removeEventListener('mouseup', onUp)
     document.body.style.userSelect = ''
+    console.log('End drag furniture')
   }
+
   window.addEventListener('mousemove', onMove)
   window.addEventListener('mouseup', onUp)
   document.body.style.userSelect = 'none'
 }
 
-// 面板拖拽:只移动 panelPosition(相对于 outer,不影响家具)
-function startPanelDrag(e: MouseEvent) {
+// 面板拖拽
+const startPanelDrag = (e: MouseEvent) => {
   e.preventDefault()
   e.stopPropagation()
+
   const startX = e.clientX
   const startY = e.clientY
   const initLeft = panelPosition.left
@@ -417,43 +407,64 @@ function startPanelDrag(e: MouseEvent) {
     panelPosition.left = initLeft + (ev.clientX - startX)
     panelPosition.top = initTop + (ev.clientY - startY)
   }
+
   const onUp = () => {
     window.removeEventListener('mousemove', onMove)
     window.removeEventListener('mouseup', onUp)
   }
+
   window.addEventListener('mousemove', onMove)
   window.addEventListener('mouseup', onUp)
 }
 
-// 微调
-function nudge(direction: 'left' | 'right' | 'up' | 'down'): void {
+// 微调功能
+const nudge = (direction: 'left' | 'right' | 'up' | 'down') => {
   const step = nudgeStep.value
   switch (direction) {
     case 'left':
-      centerCss.left -= step
+      pixelPosition.left -= step
       break
     case 'right':
-      centerCss.left += step
+      pixelPosition.left += step
       break
     case 'up':
-      centerCss.top -= step
+      pixelPosition.top -= step
       break
     case 'down':
-      centerCss.top += step
+      pixelPosition.top += step
       break
   }
-  updateXYFromCss()
+  updateGeoPosition()
   emit('update', { ...localItem })
 }
 </script>
 
 <style scoped lang="less">
 .editable-furniture {
-  width: 0;
-  height: 0;
-  position: relative;
-  .furniture-wrapper {
-    cursor: move;
+  position: absolute;
+  pointer-events: auto;
+  z-index: 100;
+  cursor: move;
+
+  &:hover {
+    outline: 2px dashed #1890ff;
+    outline-offset: 1px;
+  }
+
+  .furniture-rotated-container {
+    position: absolute;
+
+    .furniture-wrapper {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      :deep(*) {
+        pointer-events: none;
+      }
+    }
   }
 
   .property-panel {
@@ -505,6 +516,7 @@ function nudge(direction: 'left' | 'right' | 'up' | 'down'): void {
         color: #999;
         cursor: pointer;
         transition: color 0.2s;
+
         &:hover {
           color: #333;
         }
@@ -539,20 +551,12 @@ function nudge(direction: 'left' | 'right' | 'up' | 'down'): void {
         color: #333;
         cursor: pointer;
         transition: color 0.2s;
+
         &:hover {
           color: #1890ff;
         }
       }
     }
-
-    .panel-footer {
-      margin-top: 10px;
-      text-align: center;
-
-      :deep(.ant-btn) {
-        width: 100%;
-      }
-    }
   }
 }
 </style>

+ 293 - 384
src/components/EditableSubregion/index.vue

@@ -3,9 +3,12 @@
     <div
       class="mapBox blockArea"
       :style="{
-        width: `${areaWidth}px`,
-        height: `${areaHeight}px`,
+        width: `${canvasSize}px`,
+        height: `${canvasSize}px`,
         cursor: !editable ? 'no-drop' : isCreating ? 'crosshair' : 'default',
+        position: 'relative',
+        border: '1px solid #ccc',
+        background: 'rgba(242, 242, 240, 0.5)',
       }"
       @mousedown="handleMouseDown"
     >
@@ -14,10 +17,10 @@
         v-if="currentBlock"
         class="temp-block"
         :style="{
-          left: `${Math.min(currentBlock.startY, currentBlock.currentY)}px`,
-          top: `${Math.min(currentBlock.startX, currentBlock.currentX)}px`,
-          width: `${Math.abs(currentBlock.currentY - currentBlock.startY)}px`,
-          height: `${Math.abs(currentBlock.currentX - currentBlock.startX)}px`,
+          left: `${Math.min(currentBlock.startX, currentBlock.currentX)}px`,
+          top: `${Math.min(currentBlock.startY, currentBlock.currentY)}px`,
+          width: `${Math.abs(currentBlock.currentX - currentBlock.startX)}px`,
+          height: `${Math.abs(currentBlock.currentY - currentBlock.startY)}px`,
         }"
       ></div>
 
@@ -26,23 +29,14 @@
         v-for="(block, blockIndex) in blocks"
         :key="block.id"
         class="block-item"
-        :style="{
-          left: `${block.y}px`,
-          top: `${block.x}px`,
-          width: `${block.width}px`,
-          height: `${block.height}px`,
-          border: `2px solid ${block?.isBed ? '#1abc1a' : block.isActice ? 'yellow' : '#1890ff'}`,
-          position: 'absolute',
-          cursor: !editable ? 'no-drop' : 'move',
-          backgroundColor: block.isBed ? 'rgba(26, 188, 26, 0.1)' : 'rgba(24, 144, 255, 0.1)',
-        }"
+        :style="getBlockStyle(block)"
         @mousedown.self="startDrag(block, $event)"
         @click="selectBlock(block)"
       >
         <div
           class="resize-handle"
           :style="{
-            backgroundColor: block.isBed ? '#1abc1a' : '#1890ff',
+            backgroundColor: block?.isBed ? '#1abc1a' : '#1890ff',
           }"
           @mousedown.stop="startResize(block, $event)"
         >
@@ -50,162 +44,67 @@
         </div>
       </div>
     </div>
-
-    <!-- <div v-if="selectedBlock" class="mapConfig">
-      <div class="mapConfig-header">
-        <span class="title">子区域属性</span>
-        <span class="close" @click="closeSubregionAttr"><CloseOutlined /></span>
-      </div>
-      <div class="mapConfig-item">
-        <div class="mapConfig-item-label">X范围:</div>
-        <div class="mapConfig-item-content">
-          <a-space>
-            <a-input
-              v-model:value.trim="selectedBlock.startXx"
-              :style="{ width: '50px' }"
-              size="small"
-              @pressEnter="blockInputPressEnter($event, selectedBlock, 'startXx')"
-              @blur="blockInputBlur($event, selectedBlock, 'startXx')"
-            />
-
-            <a-input
-              v-model:value.trim="selectedBlock.stopXx"
-              :style="{ width: '50px' }"
-              size="small"
-              @pressEnter="blockInputPressEnter($event, selectedBlock, 'stopXx')"
-              @blur="blockInputBlur($event, selectedBlock, 'stopXx')"
-            />
-          </a-space>
-        </div>
-      </div>
-
-      <div class="mapConfig-item">
-        <div class="mapConfig-item-label">Y范围:</div>
-        <div class="mapConfig-item-content">
-          <a-space>
-            <a-input
-              v-model:value.trim="selectedBlock.startYy"
-              :style="{ width: '50px' }"
-              size="small"
-              @pressEnter="blockInputPressEnter($event, selectedBlock, 'startYy')"
-              @blur="blockInputBlur($event, selectedBlock, 'startYy')"
-            />
-
-            <a-input
-              v-model:value.trim="selectedBlock.stopYy"
-              :style="{ width: '50px' }"
-              size="small"
-              @pressEnter="blockInputPressEnter($event, selectedBlock, 'stopYy')"
-              @blur="blockInputBlur($event, selectedBlock, 'stopYy')"
-            />
-          </a-space>
-        </div>
-      </div>
-
-      <div class="mapConfig-item">
-        <div class="mapConfig-item-label">Z范围:</div>
-        <div class="mapConfig-item-content">
-          <a-space>
-            <a-input
-              v-model:value.trim="selectedBlock.startZz"
-              :style="{ width: '50px' }"
-              size="small"
-            />
-
-            <a-input
-              v-model:value.trim="selectedBlock.stopZz"
-              :style="{ width: '50px' }"
-              size="small"
-            />
-          </a-space>
-        </div>
-      </div>
-
-      <div class="mapConfig-item">
-        <div class="mapConfig-item-label">区域跟踪:</div>
-        <div class="mapConfig-item-content">
-          <a-switch v-model:checked="selectedBlock.isTracking" size="small" />
-        </div>
-      </div>
-
-      <div class="mapConfig-item">
-        <div class="mapConfig-item-label">区域跌倒:</div>
-        <div class="mapConfig-item-content">
-          <a-switch v-model:checked="selectedBlock.isFalling" size="small" />
-        </div>
-      </div>
-
-      <div v-if="selectedBlock.isBed" class="mapConfig-item">
-        <div class="mapConfig-item-label">呼吸检测:</div>
-        <div class="mapConfig-item-content"> 默认开启 </div>
-      </div>
-
-      <div class="mapConfig-item">
-        <div class="mapConfig-item-label">删除区域:</div>
-        <div class="mapConfig-item-content">
-          <DeleteOutlined @click="deleteBlockArea(selectedBlock.id || '')" />
-        </div>
-      </div>
-
-      <pre>{{ selectedBlock }}</pre>
-    </div> -->
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, computed, watch } from 'vue'
+import { ref, computed, watch, onMounted } from 'vue'
 import { message } from 'ant-design-vue'
 import { nanoid } from 'nanoid'
-// import { DeleteOutlined, CloseOutlined } from '@ant-design/icons-vue'
-import { getOriginPosition } from '@/utils'
 import type { SubRegions } from '@/api/room/types'
 
 defineOptions({ name: 'EditableSubregion' })
 
 interface Props {
-  ranges: [number, number, number, number] // 区域范围
-  width: number // 区域高度
-  length: number // 区域宽度
+  ranges: [number, number, number, number] // 区域范围 [xStart, xEnd, yStart, yEnd]
+  angle: number // 设备方向
+  canvasSize?: number // 画布尺寸
   subRegions?: SubRegions[]
   editable?: boolean
   hasBed?: boolean
 }
 
 interface BlockItem {
-  // 本地用
-  id: string // 唯一标识
-  x: number // 区块基于父元素的X偏移量(垂直方向,朝上为正)
-  y: number // 区块基于父元素的Y偏移量(水平方向,朝右为正)
-  ox: number // 区块基于原点的X偏移量
-  oy: number // 区块基于原点的Y偏移量
-  width: number // 区块宽度(水平方向)
-  height: number // 区块高度(垂直方向)
-  isDragging: boolean // 是否正在拖动
-  isResizing: boolean // 是否正在调整大小
-  isActice: boolean // 是否选中
-  isTracking: boolean // 是否开启区域跟踪
-  isFalling: boolean // 是否屏蔽区域跌倒检测
-  isBed?: boolean // 是否是床
-  // 接口用
-  startXx: number // 屏蔽子区域X开始(水平)
-  stopXx: number // 屏蔽子区域X结束(水平)
-  startYy: number // 屏蔽子区域Y开始(垂直)
-  stopYy: number // 屏蔽子区域Y结束(垂直)
-  startZz: number // 屏蔽子区域Z开始
-  stopZz: number // 屏蔽子区域Z结束
-  isLowSnr: number // 是否为床  0-不是,1-是
-  isDoor: number // 是否是门 0-否,1-是 默认0
-  presenceEnterDuration: number // 人员进入时间 默认3
-  presenceExitDuration: number // 人员离开时间 默认3
-  trackPresence: number // 是否开启区域跟踪存在 0-否,1-是
-  excludeFalling: number // 是否屏蔽区域跌倒检测 0-否,1-是
+  // 基础属性
+  id: string
+  isDragging: boolean
+  isResizing: boolean
+  isActive: boolean
+  isTracking: boolean
+  isFalling: boolean
+  isBed?: boolean
+
+  // 地理坐标(基于新坐标系)
+  geoX: number // 区块中心X坐标(向右为正)
+  geoY: number // 区块中心Y坐标(向上为正)
+  geoWidth: number // 区块宽度(地理坐标)
+  geoHeight: number // 区块高度(地理坐标)
+
+  // 像素坐标(用于显示)
+  pixelX: number
+  pixelY: number
+  pixelWidth: number
+  pixelHeight: number
+
+  // 接口数据
+  startXx: number
+  stopXx: number
+  startYy: number
+  stopYy: number
+  startZz: number
+  stopZz: number
+  isLowSnr: number
+  isDoor: number
+  presenceEnterDuration: number
+  presenceExitDuration: number
+  trackPresence: number
+  excludeFalling: number
 }
 
 const props = withDefaults(defineProps<Props>(), {
-  ranges: () => [Infinity, Infinity, Infinity, Infinity],
-  width: 0,
-  length: 0,
+  canvasSize: 500,
   editable: false,
+  hasBed: false,
 })
 
 const emit = defineEmits<{
@@ -214,62 +113,99 @@ const emit = defineEmits<{
   (e: 'update', regions: SubRegions[]): void
 }>()
 
-// 检测区域宽度 length
-const areaWidth = computed(() => {
-  return Math.abs(props.length)
-})
-// 检测区域高度 width
-const areaHeight = computed(() => {
-  return Math.abs(props.width)
+// 调试日志
+console.log('EditableSubregion mounted with props:', {
+  ranges: props.ranges,
+  canvasSize: props.canvasSize,
+  editable: props.editable,
+  subRegions: props.subRegions,
 })
 
+// 坐标转换函数(基于新组件逻辑)
+const geoToPixel = (geoX: number, geoY: number): { x: number; y: number } => {
+  const center = props.canvasSize / 2
+  return {
+    x: center + geoX,
+    y: center - geoY, // Canvas Y轴向下,所以用减法
+  }
+}
+
+const pixelToGeo = (pixelX: number, pixelY: number): { x: number; y: number } => {
+  const center = props.canvasSize / 2
+  return {
+    x: pixelX - center,
+    y: center - pixelY,
+  }
+}
+
 const editable = computed(() => props.editable)
 
 const blocks = ref<BlockItem[]>([])
 const isCreating = ref(false)
 const currentBlock = ref<{
-  startX: number // 垂直方向起点
-  startY: number // 水平方向起点
-  currentX: number // 垂直方向当前点
-  currentY: number // 水平方向当前点
+  startX: number // 像素坐标起点X
+  startY: number // 像素坐标起点Y
+  currentX: number // 像素坐标当前X
+  currentY: number // 像素坐标当前Y
 } | null>(null)
 const selectedBlock = ref<BlockItem | null>(null)
 
-const { originX, originY } = getOriginPosition(props.ranges, [0, 0])
-
 // 监听subRegions变化,更新blocks
 watch(
   () => props.subRegions,
   (newSubRegions) => {
+    console.log('subRegions changed:', newSubRegions)
     if (newSubRegions && newSubRegions.length > 0) {
-      blocks.value = newSubRegions.map((item, index) => ({
-        id: nanoid(),
-        x: originY - Number(item.startYy), // x对应垂直方向
-        y: Number(item.startXx) + originX, // y对应水平方向
-        ox: originY - item.startYy - originY,
-        oy: item.startXx + originX - originX,
-        width: Math.abs(item.stopXx - item.startXx),
-        height: Math.abs(item.stopYy - item.startYy),
-        isDragging: false,
-        isResizing: false,
-        isActice: false,
-        isTracking: Boolean(item.trackPresence),
-        isFalling: Boolean(item.excludeFalling),
-        isBed: index === 0 && props.hasBed,
-        // 来自接口回显的数据
-        startXx: item.startXx,
-        stopXx: item.stopXx,
-        startYy: item.startYy,
-        stopYy: item.stopYy,
-        startZz: item.startZz,
-        stopZz: item.stopZz,
-        isLowSnr: item.isLowSnr,
-        isDoor: item.isDoor,
-        presenceEnterDuration: item.presenceEnterDuration,
-        presenceExitDuration: item.presenceExitDuration,
-        trackPresence: item.trackPresence,
-        excludeFalling: item.excludeFalling,
-      }))
+      blocks.value = newSubRegions.map((item, index) => {
+        // 计算区块中心地理坐标
+        const centerX = (item.startXx + item.stopXx) / 2
+        const centerY = (item.startYy + item.stopYy) / 2
+        const geoWidth = Math.abs(item.stopXx - item.startXx)
+        const geoHeight = Math.abs(item.stopYy - item.startYy)
+
+        // 转换为像素坐标
+        const pixelPos = geoToPixel(centerX, centerY)
+        const pixelWidth = geoWidth // 直接使用地理宽度作为像素宽度
+        const pixelHeight = geoHeight // 直接使用地理高度作为像素高度
+
+        console.log(`Block ${index}:`, {
+          geo: { centerX, centerY, geoWidth, geoHeight },
+          pixel: { ...pixelPos, pixelWidth, pixelHeight },
+        })
+
+        return {
+          id: item.id || nanoid(),
+          isDragging: false,
+          isResizing: false,
+          isActive: false,
+          isTracking: Boolean(item.trackPresence),
+          isFalling: Boolean(item.excludeFalling),
+          isBed: index === 0 && props.hasBed,
+          // 地理坐标
+          geoX: centerX,
+          geoY: centerY,
+          geoWidth,
+          geoHeight,
+          // 像素坐标
+          pixelX: pixelPos.x - pixelWidth / 2,
+          pixelY: pixelPos.y - pixelHeight / 2,
+          pixelWidth,
+          pixelHeight,
+          // 接口数据
+          startXx: item.startXx,
+          stopXx: item.stopXx,
+          startYy: item.startYy,
+          stopYy: item.stopYy,
+          startZz: item.startZz || 0,
+          stopZz: item.stopZz || 0,
+          isLowSnr: item.isLowSnr || 0,
+          isDoor: item.isDoor || 0,
+          presenceEnterDuration: item.presenceEnterDuration || 3,
+          presenceExitDuration: item.presenceExitDuration || 3,
+          trackPresence: item.trackPresence || 0,
+          excludeFalling: item.excludeFalling || 0,
+        }
+      })
     } else {
       blocks.value = []
     }
@@ -277,35 +213,71 @@ watch(
   { immediate: true, deep: true }
 )
 
-// 手动触发子区域数据更新的函数
+// 区块样式计算
+const getBlockStyle = (block: BlockItem) => {
+  return {
+    left: `${block.pixelX}px`,
+    top: `${block.pixelY}px`,
+    width: `${block.pixelWidth}px`,
+    height: `${block.pixelHeight}px`,
+    border: `2px solid ${block?.isBed ? '#1abc1a' : block.isActive ? 'yellow' : '#1890ff'}`,
+    position: 'absolute',
+    cursor: !editable.value ? 'no-drop' : 'move',
+    backgroundColor: block.isBed ? 'rgba(26, 188, 26, 0.1)' : 'rgba(24, 144, 255, 0.1)',
+    zIndex: 1,
+  }
+}
+
+// 更新子区域数据
 const updateSubRegionsData = () => {
-  if (blocks.value) {
-    const subRegionsData = blocks.value.map((item) => ({
-      startXx: item.startXx,
-      stopXx: item.stopXx,
-      startYy: item.startYy,
-      stopYy: item.stopYy,
-      startZz: Number(item.startZz) || 0,
-      stopZz: Number(item.stopZz) || 0,
-      isLowSnr: item.isLowSnr,
-      isDoor: item.isDoor,
-      presenceEnterDuration: item.presenceEnterDuration,
-      presenceExitDuration: item.presenceExitDuration,
-      trackPresence: Number(item.isTracking),
-      excludeFalling: Number(item.isFalling),
-    }))
-    // emit('update:subRegions', subRegionsData)
+  console.log('Updating subregions data, blocks:', blocks.value)
+
+  if (blocks.value.length > 0) {
+    const subRegionsData = blocks.value.map((block) => {
+      // 从像素坐标转换回地理坐标
+      const centerGeo = pixelToGeo(
+        block.pixelX + block.pixelWidth / 2,
+        block.pixelY + block.pixelHeight / 2
+      )
+
+      const geoWidth = block.pixelWidth
+      const geoHeight = block.pixelHeight
+
+      const regionData = {
+        id: block.id,
+        startXx: centerGeo.x - geoWidth / 2,
+        stopXx: centerGeo.x + geoWidth / 2,
+        startYy: centerGeo.y - geoHeight / 2,
+        stopYy: centerGeo.y + geoHeight / 2,
+        startZz: Number(block.startZz) || 0,
+        stopZz: Number(block.stopZz) || 0,
+        isLowSnr: block.isLowSnr,
+        isDoor: block.isDoor,
+        presenceEnterDuration: block.presenceEnterDuration,
+        presenceExitDuration: block.presenceExitDuration,
+        trackPresence: Number(block.isTracking),
+        excludeFalling: Number(block.isFalling),
+      }
+
+      console.log('Region data:', regionData)
+      return regionData
+    })
+
     emit('update', subRegionsData)
+  } else {
+    emit('update', [])
   }
 }
 
 // 新建区块处理
 const createNewBlock = () => {
-  if (blocks.value && blocks.value.length > 5) {
+  console.log('createNewBlock called, current blocks:', blocks.value.length)
+  if (blocks.value && blocks.value.length >= 6) {
     message.warn('最多只能创建6个区块')
     return
   }
   isCreating.value = true
+  message.info('请在画布上拖拽创建子区域')
 }
 
 defineExpose({ createNewBlock })
@@ -313,7 +285,9 @@ defineExpose({ createNewBlock })
 // 获取容器边界
 const getContainerRect = () => {
   const container = document.querySelector('.blockArea') as HTMLElement
-  return container?.getBoundingClientRect() || { left: 0, top: 0, width: 0, height: 0 }
+  const rect = container?.getBoundingClientRect() || { left: 0, top: 0, width: 0, height: 0 }
+  console.log('Container rect:', rect)
+  return rect
 }
 
 // 鼠标移动处理
@@ -321,44 +295,51 @@ const handleMouseMove = (e: MouseEvent) => {
   if (!currentBlock.value) return
 
   const rect = getContainerRect()
-  // currentX 对应垂直方向(y轴)
-  currentBlock.value.currentX = Math.max(0, Math.min(e.clientY - rect.top, rect.height))
-  // currentY 对应水平方向(x轴)
-  currentBlock.value.currentY = Math.max(0, Math.min(e.clientX - rect.left, rect.width))
+  currentBlock.value.currentX = Math.max(0, Math.min(e.clientX - rect.left, rect.width))
+  currentBlock.value.currentY = Math.max(0, Math.min(e.clientY - rect.top, rect.height))
 }
 
-// 鼠标释放处理
+// 鼠标释放处理 - 创建新区块
 const handleMouseUp = () => {
   if (!currentBlock.value) return
 
   const { startX, startY, currentX, currentY } = currentBlock.value
-  // 宽度由水平方向(y)差值决定,高度由垂直方向(x)差值决定
-  const width = Math.abs(currentY - startY)
-  const height = Math.abs(currentX - startX)
+  const width = Math.abs(currentX - startX)
+  const height = Math.abs(currentY - startY)
+
+  console.log('Mouse up, creating block:', { startX, startY, currentX, currentY, width, height })
 
   if (width > 10 && height > 10) {
-    const minX = Math.round(Math.min(startX, currentX)) // 垂直方向最小坐标
-    const minY = Math.round(Math.min(startY, currentY)) // 水平方向最小坐标
+    const minX = Math.min(startX, currentX)
+    const minY = Math.min(startY, currentY)
 
-    blocks.value.push({
-      // 本地用
+    // 计算地理坐标
+    const centerGeo = pixelToGeo(minX + width / 2, minY + height / 2)
+    const geoWidth = width
+    const geoHeight = height
+
+    const newBlock: BlockItem = {
       id: nanoid(),
-      x: minX, // 垂直方向位置
-      y: minY, // 水平方向位置
-      ox: minX - originY,
-      oy: minY - originX,
-      width, // 水平宽度
-      height, // 垂直高度
       isDragging: false,
       isResizing: false,
-      isActice: false,
+      isActive: false,
       isTracking: false,
       isFalling: false,
-      // 接口用
-      startXx: minY - originX,
-      stopXx: minY - originX + width,
-      startYy: originY - minX,
-      stopYy: originY - minX - height,
+      // 地理坐标
+      geoX: centerGeo.x,
+      geoY: centerGeo.y,
+      geoWidth,
+      geoHeight,
+      // 像素坐标
+      pixelX: minX,
+      pixelY: minY,
+      pixelWidth: width,
+      pixelHeight: height,
+      // 接口数据
+      startXx: centerGeo.x - geoWidth / 2,
+      stopXx: centerGeo.x + geoWidth / 2,
+      startYy: centerGeo.y - geoHeight / 2,
+      stopYy: centerGeo.y + geoHeight / 2,
       startZz: 0,
       stopZz: 0,
       isLowSnr: 0,
@@ -367,9 +348,15 @@ const handleMouseUp = () => {
       presenceExitDuration: 3,
       trackPresence: 0,
       excludeFalling: 0,
-    })
+    }
+
+    console.log('New block created:', newBlock)
+    blocks.value.push(newBlock)
+
     emit('create')
     updateSubRegionsData()
+  } else {
+    message.warn('区域太小,请绘制更大的区域')
   }
 
   currentBlock.value = null
@@ -383,47 +370,41 @@ const startDrag = (block: BlockItem, e: MouseEvent) => {
   if (!editable.value) return
   e.stopPropagation()
 
+  console.log('Start dragging block:', block.id)
+
   // 取消选中其他区块
   blocks.value.forEach((b) => {
-    b.isActice = false
+    b.isActive = false
   })
 
   block.isDragging = true
-  block.isActice = true
+  block.isActive = true
+
   const container = document.querySelector('.blockArea') as HTMLElement
   const rect = container.getBoundingClientRect()
-  // 垂直方向偏移(x轴)基于clientY,水平方向偏移(y轴)基于clientX
-  const offsetX = e.clientY - rect.top - block.x
-  const offsetY = e.clientX - rect.left - block.y
+  const offsetX = e.clientX - rect.left - block.pixelX
+  const offsetY = e.clientY - rect.top - block.pixelY
 
-  const initialOx = block.ox
-  const initialOy = block.oy
+  const initialPixelX = block.pixelX
+  const initialPixelY = block.pixelY
 
   const moveHandler = (e: MouseEvent) => {
-    // 新垂直位置(x轴)= 鼠标垂直位置 - 容器顶部 - 偏移量
-    const newX = e.clientY - rect.top - offsetX
-    // 新水平位置(y轴)= 鼠标水平位置 - 容器左侧 - 偏移量
-    const newY = e.clientX - rect.left - offsetY
+    const newX = e.clientX - rect.left - offsetX
+    const newY = e.clientY - rect.top - offsetY
     const containerWidth = container.offsetWidth
     const containerHeight = container.offsetHeight
 
-    // 限制边界:x(垂直)不超过容器高度,y(水平)不超过容器宽度
-    block.x = Math.max(0, Math.min(newX, containerHeight - block.height))
-    block.y = Math.max(0, Math.min(newY, containerWidth - block.width))
-    block.ox = block.x - originY
-    block.oy = block.y - originX
+    // 限制边界
+    block.pixelX = Math.max(0, Math.min(newX, containerWidth - block.pixelWidth))
+    block.pixelY = Math.max(0, Math.min(newY, containerHeight - block.pixelHeight))
   }
 
   const upHandler = () => {
     block.isDragging = false
-    block.isActice = false
-
-    if (block.ox !== initialOx || block.oy !== initialOy) {
-      block.startXx = block.oy
-      block.stopXx = block.oy + block.width
-      block.startYy = originY - block.x
-      block.stopYy = originY - block.x - block.height
+    block.isActive = false
 
+    if (block.pixelX !== initialPixelX || block.pixelY !== initialPixelY) {
+      console.log('Block dragged, updating data')
       updateSubRegionsData()
     }
 
@@ -441,73 +422,60 @@ const startResize = (block: BlockItem, e: MouseEvent) => {
   e.stopPropagation()
   e.preventDefault()
 
+  console.log('Start resizing block:', block.id)
+
   block.isResizing = true
   selectedBlock.value = block
-  const startX = e.clientY // 垂直方向起点使用clientY
-  const startY = e.clientX // 水平方向起点使用clientX
-  const initialWidth = block.width
-  const initialHeight = block.height
+
+  const startX = e.clientX
+  const startY = e.clientY
+  const initialWidth = block.pixelWidth
+  const initialHeight = block.pixelHeight
+  const initialX = block.pixelX
+  const initialY = block.pixelY
 
   const moveHandler = (e: MouseEvent) => {
     const rect = getContainerRect()
-    const deltaX = e.clientY - startX // 垂直方向变化
-    const deltaY = e.clientX - startY // 水平方向变化
+    const deltaX = e.clientX - startX
+    const deltaY = e.clientY - startY
+
     // 限制最小尺寸和容器边界
-    block.width = Math.max(50, Math.min(initialWidth + deltaY, rect.width - block.y))
-    block.height = Math.max(50, Math.min(initialHeight + deltaX, rect.height - block.x))
+    const newWidth = Math.max(20, Math.min(initialWidth + deltaX, rect.width - block.pixelX))
+    const newHeight = Math.max(20, Math.min(initialHeight + deltaY, rect.height - block.pixelY))
+
+    block.pixelWidth = newWidth
+    block.pixelHeight = newHeight
   }
 
   const upHandler = () => {
     block.isResizing = false
     selectedBlock.value = null
-
-    block.stopXx = block.startXx + block.width
-    block.stopYy = block.startYy - block.height
-
+    console.log('Block resized, updating data')
+    updateSubRegionsData()
     document.removeEventListener('mousemove', moveHandler)
     document.removeEventListener('mouseup', upHandler)
-    updateSubRegionsData()
   }
 
   document.addEventListener('mousemove', moveHandler)
   document.addEventListener('mouseup', upHandler)
 }
 
-// 输入框回车事件
-// const blockInputPressEnter = (e: Event, el: BlockItem, attr: string) => {
-//   if (!el) return
-//   // X范围(水平方向)对应y轴逻辑
-//   if (attr === 'startXx') {
-//     el.startXx = Number(el[attr as keyof BlockItem])
-//     el.y = el.startXx + originX // y控制水平位置
-//   }
-//   if (attr === 'stopXx') {
-//     el.stopXx = Number(el[attr as keyof BlockItem])
-//     el.width = el.stopXx - el.startXx // 宽度由X范围决定
-//   }
-
-//   // Y范围(垂直方向,朝上为正)对应x轴逻辑
-//   if (attr === 'startYy') {
-//     el.startYy = Number(el[attr as keyof BlockItem])
-//     el.x = originY - el.startYy // x控制垂直位置
-//   }
-//   if (attr === 'stopYy') {
-//     el.stopYy = Number(el[attr as keyof BlockItem])
-//     el.height = el.startYy - el.stopYy // 高度由Y范围决定(朝上为正,差值为正)
-//   }
-
-//   updateSubRegionsData()
-// }
-
-// 鼠标按下事件
+// 鼠标按下事件 - 开始创建区块
 const handleMouseDown = (e: MouseEvent) => {
-  if (!editable.value) return
-  if (!isCreating.value) return
+  if (!editable.value) {
+    console.log('Not editable, ignoring mousedown')
+    return
+  }
+  if (!isCreating.value) {
+    console.log('Not in creating mode, ignoring mousedown')
+    return
+  }
+
+  console.log('Mouse down for creating block')
 
   const rect = getContainerRect()
-  // 交换x/y轴:startX 对应垂直方向(clientY),startY 对应水平方向(clientX)
-  const startX = e.clientY - rect.top // 垂直方向起点(x轴)
-  const startY = e.clientX - rect.left // 水平方向起点(y轴)
+  const startX = e.clientX - rect.left
+  const startY = e.clientY - rect.top
 
   currentBlock.value = {
     startX,
@@ -524,49 +492,15 @@ const selectBlock = (block: BlockItem) => {
   if (!editable.value) return
   selectedBlock.value = block
   blocks.value.forEach((item) => {
-    item.isActice = item === block
+    item.isActive = item === block
   })
+  console.log('Block selected:', block.id)
 }
 
-// 关闭子区域属性
-// const closeSubregionAttr = () => {
-//   blocks.value.forEach((item) => {
-//     item.isActice = false
-//   })
-//   selectedBlock.value = null
-// }
-
-// const blockInputBlur = (e: Event, el: BlockItem, attr: string) => {
-//   if (!el) return
-//   // X范围(水平方向)
-//   if (attr === 'startXx') {
-//     el.startXx = Number(el[attr as keyof BlockItem])
-//     el.y = el.startXx + originX
-//   }
-//   if (attr === 'stopXx') {
-//     el.stopXx = Number(el[attr as keyof BlockItem])
-//     el.width = el.stopXx - el.startXx
-//   }
-
-//   // Y范围(垂直方向)
-//   if (attr === 'startYy') {
-//     el.startYy = Number(el[attr as keyof BlockItem])
-//     el.x = originY - el.startYy
-//   }
-//   if (attr === 'stopYy') {
-//     el.stopYy = Number(el[attr as keyof BlockItem])
-//     el.height = el.startYy - el.stopYy
-//   }
-//   updateSubRegionsData()
-// }
-
-// const deleteBlockArea = (id: string) => {
-//   if (id) {
-//     blocks.value = blocks.value.filter((item) => item.id !== id)
-//     selectedBlock.value = null
-//     updateSubRegionsData()
-//   }
-// }
+// 添加调试信息
+onMounted(() => {
+  console.log('EditableSubregion component mounted')
+})
 </script>
 
 <style scoped lang="less">
@@ -578,54 +512,27 @@ const selectBlock = (block: BlockItem) => {
 .mapBox {
   position: relative;
   flex-shrink: 0;
-}
-
-.mapConfig {
-  background-color: #f5f5f5;
-  border-radius: 10px;
-  padding: 12px;
-  min-width: 200px;
-  &-header {
-    margin-bottom: 10px;
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    .title {
-      font-size: 14px;
-      font-weight: 600;
-      line-height: 24px;
-    }
-    .close {
-      font-size: 14px;
-      color: #666;
-      cursor: pointer;
-      position: relative;
-      top: -5px;
-    }
-  }
-
-  &-item {
-    display: flex;
-    line-height: 30px;
-    &-label {
-      color: #888;
-      min-width: 80px;
-    }
-    &-content {
-      color: #555;
-      min-width: 100px;
-    }
-  }
+  border: 1px solid #ddd;
+  background-color: rgba(242, 242, 240, 0.5);
+  pointer-events: auto;
 }
 
 .temp-block {
   position: absolute;
   background: rgba(24, 144, 255, 0.2);
   border: 2px dashed #1890ff;
+  pointer-events: none;
+  z-index: 2;
 }
 
 .block-item {
   background: rgba(24, 144, 255, 0.1);
+  pointer-events: all;
+  z-index: 1;
+
+  &:hover {
+    background: rgba(24, 144, 255, 0.2);
+  }
 
   .resize-handle {
     position: absolute;
@@ -635,11 +542,13 @@ const selectBlock = (block: BlockItem) => {
     height: 15px;
     background: #1890ff;
     cursor: nwse-resize;
-    font-size: 12px;
+    font-size: 10px;
     color: #fff;
     display: flex;
     align-items: center;
     justify-content: center;
+    border-radius: 2px;
+    z-index: 3;
   }
 }
 </style>

+ 371 - 158
src/components/RadarEditor/index.vue

@@ -1,6 +1,12 @@
 <template>
   <div class="radarEditor" :class="{ disabled: props.disabled }">
-    <RadarView :coordinates="coordinates" :angle="angle">
+    <DetectionAreaView
+      :coordinates="coordinates"
+      :direction="angle"
+      :furniture-items="localFurniture"
+      :canvas-size="500"
+      mode="edit"
+    >
       <template #furnitures>
         <EditableFurniture
           v-for="item in localFurniture"
@@ -8,6 +14,8 @@
           :item="item"
           :angle="angle"
           :coordinates="coordinates"
+          :canvas-size="400"
+          :disabled="disabled"
           @update="updateFurniture"
           @delete="deleteFurniture"
         />
@@ -15,12 +23,10 @@
 
       <template #subregion>
         <EditableSubregion
-          v-if="modeRadio === 2"
           ref="editableSubregionRef"
           :ranges="coordinates"
           :angle="angle"
-          :width="areaHeight"
-          :length="areaWidth"
+          :canvas-size="400"
           :subRegions="localSubRegions"
           :editable="!disabled"
           :has-bed="localFurniture.some((item) => item.type === 'bed')"
@@ -28,7 +34,7 @@
           @update="handleSubregionUpdate"
         />
       </template>
-    </RadarView>
+    </DetectionAreaView>
 
     <div v-if="!disabled && showPanel" class="options">
       <div class="close" @click="showPanel = false">×</div>
@@ -39,130 +45,289 @@
           size="small"
           @change="modeRadioChange"
         >
-          <a-radio-button :value="1">家具</a-radio-button>
-          <a-radio-button :value="2">子区域beta</a-radio-button>
-          <!-- <a-radio-button :value="3">信息面板</a-radio-button> -->
+          <a-radio-button :value="1">配置面板</a-radio-button>
+          <a-radio-button :value="2">信息面板</a-radio-button>
         </a-radio-group>
       </div>
 
-      <div v-if="modeRadio === 1" class="furniture">
-        <a-radio-group v-model:value="sideRadio" size="small">
-          <a-radio-button :value="1">客厅</a-radio-button>
-          <a-radio-button :value="2">餐厅</a-radio-button>
-          <a-radio-button :value="3">卧室</a-radio-button>
-          <a-radio-button :value="4">卫生间</a-radio-button>
-        </a-radio-group>
+      <div v-if="modeRadio === 1" class="config">
+        <div class="panel">
+          <div class="panel-hd">家具操作</div>
+          <div class="panel-ct">
+            <div class="furnitureTool">
+              <a-radio-group v-model:value="sideRadio" size="small">
+                <a-radio :style="radioStyle" :value="1">客厅</a-radio>
+                <a-radio :style="radioStyle" :value="2">餐厅</a-radio>
+                <a-radio :style="radioStyle" :value="3">卧室</a-radio>
+                <a-radio :style="radioStyle" :value="4">卫生间</a-radio>
+              </a-radio-group>
+
+              <furniture-list
+                v-if="sideRadio === 1"
+                :icons="livingroomIcons"
+                @add="add"
+              ></furniture-list>
+              <furniture-list
+                v-if="sideRadio === 2"
+                :icons="diningroomIcons"
+                @add="add"
+              ></furniture-list>
+              <furniture-list
+                v-if="sideRadio === 3"
+                :icons="bedroomIocns"
+                @add="add"
+              ></furniture-list>
+              <furniture-list
+                v-if="sideRadio === 4"
+                :icons="bathroomIocns"
+                @add="add"
+              ></furniture-list>
+            </div>
+          </div>
 
-        <furniture-list v-if="sideRadio === 1" :icons="livingroomIcons" @add="add"></furniture-list>
-        <furniture-list v-if="sideRadio === 2" :icons="diningroomIcons" @add="add"></furniture-list>
-        <furniture-list v-if="sideRadio === 3" :icons="bedroomIocns" @add="add"></furniture-list>
-        <furniture-list v-if="sideRadio === 4" :icons="bathroomIocns" @add="add"></furniture-list>
+          <div class="panel-hd">区域操作</div>
+          <div class="panel-ct">
+            <div class="subregionTool">
+              <div v-if="localSubRegions.length === 0">
+                暂无区域,立即<a-button type="link" size="small" @click="createSubregion"
+                  >新建</a-button
+                >
+              </div>
+              <div v-else>
+                <span>已创建 {{ localSubRegions.length }} 个区域</span>
+                <a-button
+                  v-if="localSubRegions.length < 6"
+                  type="link"
+                  size="small"
+                  @click="createSubregion"
+                  >继续新建</a-button
+                >
+              </div>
+            </div>
+          </div>
+        </div>
       </div>
 
-      <div v-if="modeRadio === 2" class="subregion">
-        <div class="subregion-header">
-          <div class="title">区域列表</div>
-          <a-button size="small" @click="createSubregion">新建区域</a-button>
-        </div>
+      <div v-if="modeRadio === 2" class="info">
+        <div class="panel">
+          <div class="panel-hd"
+            >家具列表 <span v-if="localFurniture.length">({{ localFurniture.length }})</span></div
+          >
+          <div class="panel-ct">
+            <div v-for="(furniture, index) in localFurniture" :key="index" class="list-item">
+              <a-collapse v-model:activeKey="furnitureActiveKey" ghost>
+                <a-collapse-panel :key="index + 2" :header="`${furniture.name} 属性`">
+                  <div class="mapConfig">
+                    <div class="mapConfig-item">
+                      <label class="mapConfig-item-label">家具尺寸:</label>
+                      <div class="mapConfig-item-content">
+                        <a-space>
+                          <a-input-number
+                            v-model:value="furniture.width"
+                            :min="10"
+                            size="small"
+                            :style="inputStyle"
+                          />
+                          <a-input-number
+                            v-model:value="furniture.length"
+                            :min="10"
+                            size="small"
+                            :style="inputStyle"
+                          />
+                        </a-space>
+                      </div>
+                    </div>
 
-        <div class="subregion-list">
-          <div v-for="(region, index) in localSubRegions" :key="index" class="list-item">
-            <a-collapse v-model:activeKey="activeKey" ghost>
-              <a-collapse-panel :key="index + 2" :header="`子区域 ${index + 1} 属性`">
-                <div class="mapConfig">
-                  <div class="mapConfig-item">
-                    <div class="mapConfig-item-label">X范围:</div>
-                    <div class="mapConfig-item-content">
-                      <a-space>
-                        <a-input
-                          v-model:value.trim="region.startXx"
-                          :style="{ width: '50px' }"
-                          size="small"
-                        />
+                    <div class="mapConfig-item">
+                      <label class="mapConfig-item-label">家具旋转:</label>
+                      <div class="mapConfig-item-content">
+                        <a-select v-model:value="furniture.rotate" size="small" :style="inputStyle">
+                          <a-select-option :value="0">0°</a-select-option>
+                          <a-select-option :value="90">90°</a-select-option>
+                          <a-select-option :value="180">180°</a-select-option>
+                          <a-select-option :value="270">270°</a-select-option>
+                        </a-select>
+                      </div>
+                    </div>
 
-                        <a-input
-                          v-model:value.trim="region.stopXx"
-                          :style="{ width: '50px' }"
+                    <div class="mapConfig-item">
+                      <label class="mapConfig-item-label">位置微调:</label>
+                      <div class="mapConfig-item-content"></div>
+                      <a-space>
+                        <ArrowLeftOutlined @click="nudge('left')" />
+                        <ArrowUpOutlined @click="nudge('up')" />
+                        <ArrowDownOutlined @click="nudge('down')" />
+                        <ArrowRightOutlined @click="nudge('right')" />
+                        <a-input-number
+                          v-model:value="nudgeStep"
+                          :min="1"
                           size="small"
+                          style="width: 60px"
                         />
                       </a-space>
                     </div>
-                  </div>
 
-                  <div class="mapConfig-item">
-                    <div class="mapConfig-item-label">Y范围:</div>
-                    <div class="mapConfig-item-content">
-                      <a-space>
-                        <a-input
-                          v-model:value.trim="region.startYy"
-                          :style="{ width: '50px' }"
-                          size="small"
-                        />
+                    <div class="mapConfig-item">
+                      <label class="mapConfig-item-label">left/top:</label>
+                      <div class="mapConfig-item-content">
+                        <a-space>
+                          <a-input-number
+                            v-model:value="furniture.left"
+                            disabled
+                            size="small"
+                            :style="inputStyle"
+                          />
+                          <a-input-number
+                            v-model:value="furniture.top"
+                            disabled
+                            size="small"
+                            :style="inputStyle"
+                          />
+                        </a-space>
+                      </div>
+                    </div>
 
-                        <a-input
-                          v-model:value.trim="region.stopYy"
-                          :style="{ width: '50px' }"
-                          size="small"
-                        />
-                      </a-space>
+                    <div class="mapConfig-item">
+                      <label class="mapConfig-item-label">x/y:</label>
+                      <div class="mapConfig-item-content">
+                        <a-space>
+                          <a-input-number
+                            v-model:value="furniture.x"
+                            size="small"
+                            :style="inputStyle"
+                          />
+                          <a-input-number
+                            v-model:value="furniture.y"
+                            size="small"
+                            :style="inputStyle"
+                          />
+                        </a-space>
+                      </div>
+                    </div>
+
+                    <div class="mapConfig-item">
+                      <label class="mapConfig-item-label">操作:</label>
+                      <div class="mapConfig-item-content">
+                        <a-space>
+                          <a-popconfirm
+                            title="确定删除家具吗?"
+                            @confirm="deleteFurniture(furniture.nanoid!)"
+                          >
+                            <DeleteOutlined />
+                          </a-popconfirm>
+                        </a-space>
+                      </div>
                     </div>
                   </div>
+                </a-collapse-panel>
+              </a-collapse>
+            </div>
+          </div>
+        </div>
 
-                  <div class="mapConfig-item">
-                    <div class="mapConfig-item-label">Z范围:</div>
-                    <div class="mapConfig-item-content">
-                      <a-space>
-                        <a-input
-                          v-model:value.trim="region.startZz"
-                          :style="{ width: '50px' }"
-                          size="small"
-                        />
+        <div class="panel">
+          <div class="panel-hd"
+            >区域列表 <span v-if="localSubRegions.length">({{ localSubRegions.length }})</span></div
+          >
+          <div class="panel-ct">
+            <div v-for="(region, index) in localSubRegions" :key="index" class="list-item">
+              <a-collapse v-model:activeKey="regionActiveKey" ghost>
+                <a-collapse-panel :key="index + 2" :header="`子区域 ${index + 1} 属性`">
+                  <div class="mapConfig">
+                    <div class="mapConfig-item">
+                      <div class="mapConfig-item-label">X范围:</div>
+                      <div class="mapConfig-item-content">
+                        <a-space>
+                          <a-input
+                            v-model:value.trim="region.startXx"
+                            :style="inputStyle"
+                            size="small"
+                          />
+
+                          <a-input
+                            v-model:value.trim="region.stopXx"
+                            :style="inputStyle"
+                            size="small"
+                          />
+                        </a-space>
+                      </div>
+                    </div>
 
-                        <a-input
-                          v-model:value.trim="region.stopZz"
-                          :style="{ width: '50px' }"
-                          size="small"
-                        />
-                      </a-space>
+                    <div class="mapConfig-item">
+                      <div class="mapConfig-item-label">Y范围:</div>
+                      <div class="mapConfig-item-content">
+                        <a-space>
+                          <a-input
+                            v-model:value.trim="region.startYy"
+                            :style="inputStyle"
+                            size="small"
+                          />
+
+                          <a-input
+                            v-model:value.trim="region.stopYy"
+                            :style="inputStyle"
+                            size="small"
+                          />
+                        </a-space>
+                      </div>
                     </div>
-                  </div>
 
-                  <div class="mapConfig-item">
-                    <div class="mapConfig-item-label">区域跟踪:</div>
-                    <div class="mapConfig-item-content">
-                      <a-switch v-model:checked="region.isTracking" size="small" />
+                    <div class="mapConfig-item">
+                      <div class="mapConfig-item-label">Z范围:</div>
+                      <div class="mapConfig-item-content">
+                        <a-space>
+                          <a-input
+                            v-model:value.trim="region.startZz"
+                            :style="inputStyle"
+                            size="small"
+                          />
+
+                          <a-input
+                            v-model:value.trim="region.stopZz"
+                            :style="inputStyle"
+                            size="small"
+                          />
+                        </a-space>
+                      </div>
                     </div>
-                  </div>
 
-                  <div class="mapConfig-item">
-                    <div class="mapConfig-item-label">区域跌倒:</div>
-                    <div class="mapConfig-item-content">
-                      <a-switch v-model:checked="region.isFalling" size="small" />
+                    <div class="mapConfig-item">
+                      <div class="mapConfig-item-label">区域跟踪:</div>
+                      <div class="mapConfig-item-content">
+                        <a-switch v-model:checked="region.isTracking" size="small" />
+                      </div>
                     </div>
-                  </div>
 
-                  <div v-if="region.isBed" class="mapConfig-item">
-                    <div class="mapConfig-item-label">呼吸检测:</div>
-                    <div class="mapConfig-item-content"> 默认开启 </div>
-                  </div>
+                    <div class="mapConfig-item">
+                      <div class="mapConfig-item-label">区域跌倒:</div>
+                      <div class="mapConfig-item-content">
+                        <a-switch v-model:checked="region.isFalling" size="small" />
+                      </div>
+                    </div>
+
+                    <div v-if="region.isBed" class="mapConfig-item">
+                      <div class="mapConfig-item-label">呼吸检测:</div>
+                      <div class="mapConfig-item-content"> 默认开启 </div>
+                    </div>
 
-                  <div class="mapConfig-item">
-                    <div class="mapConfig-item-label">删除区域:</div>
-                    <div class="mapConfig-item-content">
-                      <DeleteOutlined @click="deleteBlockArea(region.id || '')" />
+                    <div class="mapConfig-item">
+                      <div class="mapConfig-item-label">删除区域:</div>
+                      <div class="mapConfig-item-content">
+                        <a-popconfirm
+                          title="确定删除区域吗?"
+                          @confirm="deleteBlockArea(region.id || '')"
+                        >
+                          <DeleteOutlined />
+                        </a-popconfirm>
+                      </div>
                     </div>
                   </div>
-                </div>
-              </a-collapse-panel>
-            </a-collapse>
-          </div>
-          <div v-if="localSubRegions.length === 0" class="list-item empty">
-            暂无区域,请点击"新建区域"开始创建
+                </a-collapse-panel>
+              </a-collapse>
+            </div>
           </div>
         </div>
       </div>
-
-      <div v-if="modeRadio === 3" class="infoPanel"> 信息展示 </div>
     </div>
 
     <a-button v-else type="link" @click="showPanel = true">开始配置</a-button>
@@ -170,18 +335,23 @@
 </template>
 
 <script setup lang="ts">
-import { ref, watch, computed, onUnmounted } from 'vue'
+import { ref, watch, computed, onUnmounted, reactive } from 'vue'
 import type { FurnitureItem } from '@/types/radar'
-import RadarView from '../RadarView/index.vue'
+import DetectionAreaView from '../DetectionAreaView/index.vue'
 import EditableFurniture from '../EditableFurniture/index.vue'
 import EditableSubregion from '../EditableSubregion/index.vue'
 import type { FurnitureIconType } from '@/types/furniture'
 import { message } from 'ant-design-vue'
 import { nanoid } from 'nanoid'
-import type { FurnitureType } from '@/api/room/types'
 import { furnitureIconNameMap, furnitureIconSizeMap } from '@/const/furniture'
 import type { SubRegions } from '@/api/room/types'
-import { DeleteOutlined } from '@ant-design/icons-vue'
+import {
+  DeleteOutlined,
+  ArrowLeftOutlined,
+  ArrowUpOutlined,
+  ArrowDownOutlined,
+  ArrowRightOutlined,
+} from '@ant-design/icons-vue'
 
 defineOptions({ name: 'RadarEditor' })
 
@@ -201,16 +371,29 @@ const emit = defineEmits<{
 const localFurniture = ref<FurnitureItem[]>(props.furnitureItems ?? [])
 const localSubRegions = ref<SubRegions[]>(props.subRegions ?? [])
 
-// 计算区域宽度和高度
-const areaWidth = computed(() => {
-  const [minX, maxX, minY, maxY] = props.coordinates
-  return Math.abs(maxX - minX)
-})
-
-const areaHeight = computed(() => {
-  const [minX, maxX, minY, maxY] = props.coordinates
-  return Math.abs(maxY - minY)
-})
+const inputStyle = computed(() => ({ width: '70px' }))
+const pixelPosition = reactive({ left: 0, top: 0 })
+const nudgeStep = ref(5)
+// 微调功能
+const nudge = (direction: 'left' | 'right' | 'up' | 'down') => {
+  const step = nudgeStep.value
+  switch (direction) {
+    case 'left':
+      pixelPosition.left -= step
+      break
+    case 'right':
+      pixelPosition.left += step
+      break
+    case 'up':
+      pixelPosition.top -= step
+      break
+    case 'down':
+      pixelPosition.top += step
+      break
+  }
+  // updateGeoPosition()
+  // emit('update', { ...localItem })
+}
 
 watch(
   () => props.furnitureItems,
@@ -261,6 +444,12 @@ const modeRadio = ref<1 | 2 | 3>(1)
 const sideRadio = ref<1 | 2 | 3 | 4>(1)
 const editableSubregionRef = ref<InstanceType<typeof EditableSubregion>>()
 
+const radioStyle = reactive({
+  display: 'flex',
+  height: '30px',
+  lineHeight: '30px',
+})
+
 // 客厅图标
 const livingroomIcons = [
   'living_sofa',
@@ -298,7 +487,7 @@ const add = (icon: FurnitureIconType) => {
 
   const newItem: FurnitureItem = {
     name: furnitureIconNameMap[icon],
-    type: icon as FurnitureType,
+    type: icon,
     width: originWidth,
     length: originHeight,
     top: 0,
@@ -315,7 +504,7 @@ const add = (icon: FurnitureIconType) => {
 
 // 创建子区域
 const createSubregion = () => {
-  modeRadio.value = 2
+  // modeRadio.value = 2
   // 通过ref调用EditableSubregion组件的createNewBlock方法
   if (editableSubregionRef.value) {
     editableSubregionRef.value?.createNewBlock()
@@ -350,7 +539,8 @@ onUnmounted(() => {
   localStorage.removeItem('subRegions')
 })
 
-const activeKey = ref<number[]>([0])
+const regionActiveKey = ref<number[]>([0])
+const furnitureActiveKey = ref<number[]>([])
 
 const deleteBlockArea = (id: string) => {
   if (id) {
@@ -361,7 +551,7 @@ const deleteBlockArea = (id: string) => {
 const modeRadioChange = (e: Event) => {
   const value = (e.target as HTMLInputElement).value as unknown as 1 | 2 | 3
   console.log(11111111, value)
-  activeKey.value = [0]
+  regionActiveKey.value = [0]
 }
 </script>
 
@@ -385,7 +575,7 @@ const modeRadioChange = (e: Event) => {
   }
 
   .options {
-    width: 220px;
+    width: 280px;
     background-color: #fefefe;
     border-radius: 10px;
     padding: 12px;
@@ -410,57 +600,78 @@ const modeRadioChange = (e: Event) => {
       justify-content: space-between;
       margin-bottom: 12px;
     }
+  }
+}
 
-    .furniture {
-      display: flex;
-      flex-direction: column;
-      gap: 12px;
+.panel {
+  margin-bottom: 12px;
+  &-hd {
+    font-size: 12px;
+    font-weight: 600;
+    color: #666;
+    line-height: 1.5;
+    padding: 5px 8px;
+  }
+  &-ct {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    max-height: 300px;
+    overflow-y: auto;
+
+    // 滚动条不遮挡内容
+    ::-webkit-scrollbar {
+      width: 4px;
+      height: 4px;
+    }
+    ::-webkit-scrollbar-track {
+      background-color: transparent;
+    }
+    ::-webkit-scrollbar-thumb {
+      background-color: rgba(0, 0, 0, 0.2);
+      border-radius: 2px;
     }
 
-    .subregion {
+    .furnitureTool {
       display: flex;
-      flex-direction: column;
-      gap: 12px;
-
-      &-header {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-
-        .title {
-          font-weight: 600;
-          color: #666;
-        }
-      }
+      justify-content: space-between;
+      background-color: #f5f5f5;
+      padding: 8px;
+      border-radius: 8px;
 
-      &-list {
-        display: flex;
-        flex-direction: column;
-        gap: 8px;
-
-        .list-item {
-          display: flex;
-          align-items: center;
-          justify-content: space-between;
-          // padding: 8px 12px;
-          border-radius: 8px;
-          background-color: #f5f5f5;
-          cursor: pointer;
-        }
-        .list-item.empty {
-          color: #999;
-          text-align: center;
-        }
+      .furnitureList {
+        flex-grow: 1;
       }
     }
+
+    .subregionTool {
+      display: flex;
+      align-items: center;
+      background-color: #f5f5f5;
+      padding: 10px 12px;
+      border-radius: 8px;
+      font-size: 14px;
+      color: #333;
+    }
+  }
+  &:last-child {
+    margin-bottom: 0;
+  }
+
+  .list-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    border-radius: 8px;
+    background-color: #f5f5f5;
+    cursor: pointer;
   }
 }
 
 .mapConfig {
   background-color: #f5f5f5;
   border-radius: 10px;
-  padding: 12px;
-  // min-width: 200px;
+  padding: 0 12px;
   &-header {
     margin-bottom: 10px;
     display: flex;
@@ -485,14 +696,16 @@ const modeRadioChange = (e: Event) => {
     line-height: 30px;
     &-label {
       color: #888;
-      min-width: 55px;
+      min-width: 80px;
     }
     &-content {
       color: #555;
-      // min-width: 100px;
     }
   }
 }
+:deep(.ant-collapse) {
+  width: 100%;
+}
 :deep(.ant-collapse .ant-collapse-content > .ant-collapse-content-box) {
   padding: 0;
 }

+ 123 - 0
src/utils/coordTransform.ts

@@ -0,0 +1,123 @@
+// 定义雷达坐标系中的矩形接口
+export interface RadarRect {
+  x_cm_start: number
+  x_cm_stop: number
+  y_cm_start: number
+  y_cm_stop: number
+}
+
+// 定义画布坐标系中的矩形接口
+export interface CanvasRect {
+  left: number
+  top: number
+  width: number
+  height: number
+}
+
+// 定义雷达在画布中的位置接口
+export interface RadarPosition {
+  x_radar: number
+  y_radar: number
+}
+
+// 定义家具在雷达坐标系中的接口
+export interface RadarFurniture {
+  x: number
+  y: number
+  width: number
+  height: number
+}
+
+/**
+ * 检测区域、子区域 矩形转换 雷达坐标系 --> 画布坐标系
+ * @param src_rect - 雷达坐标系矩形
+ * @param p_radar - 画布中雷达的位置
+ * @returns CSS 可用矩形
+ */
+export function convert_region_r2c(src_rect: RadarRect, p_radar: RadarPosition): CanvasRect {
+  const x_radar = p_radar.x_radar
+  const y_radar = p_radar.y_radar
+
+  const left = x_radar + src_rect.x_cm_start
+  const top = y_radar - src_rect.y_cm_stop
+  const width = src_rect.x_cm_stop - src_rect.x_cm_start
+  const height = src_rect.y_cm_stop - src_rect.y_cm_start
+
+  return { left, top, width, height }
+}
+
+/**
+ * 检测区域、子区域 矩形转换 画布坐标系 --> 雷达坐标系
+ * @param dst_rect - 画布矩形
+ * @param p_radar - 雷达在画布坐标系中的位置
+ * @returns 雷达坐标矩形
+ */
+export function convert_region_c2r(dst_rect: CanvasRect, p_radar: RadarPosition): RadarRect {
+  const x_radar = p_radar.x_radar
+  const y_radar = p_radar.y_radar
+
+  const x_cm_start = dst_rect.left - x_radar
+  const x_cm_stop = x_cm_start + dst_rect.width
+
+  const y_cm_stop = y_radar - dst_rect.top
+  const y_cm_start = y_cm_stop - dst_rect.height
+
+  return {
+    x_cm_start,
+    x_cm_stop,
+    y_cm_start,
+    y_cm_stop,
+  }
+}
+
+/**
+ * 家具 矩形转换 雷达坐标系 --> 画布坐标系
+ * @param furniture - 雷达坐标家具对象
+ * @param p_radar - 雷达在画布上的坐标
+ * @returns CSS 可用家具矩形
+ */
+export function convert_furniture_r2c(
+  furniture: RadarFurniture,
+  p_radar: RadarPosition
+): CanvasRect {
+  const x_radar = p_radar.x_radar
+  const y_radar = p_radar.y_radar
+
+  const left = x_radar + furniture.x
+  const top = y_radar - furniture.y
+  const width = furniture.width
+  const height = furniture.height
+
+  return {
+    left,
+    top,
+    width,
+    height,
+  }
+}
+
+/**
+ * 家具 矩形转换 画布坐标系 --> 雷达坐标系
+ * @param furniture_item - 画布矩形(CSS)
+ * @param p_radar - 雷达在画布上的坐标
+ * @returns 雷达坐标家具矩形
+ */
+export function convert_furniture_c2r(
+  furniture_item: CanvasRect,
+  p_radar: RadarPosition
+): RadarFurniture {
+  const x_radar = p_radar.x_radar
+  const y_radar = p_radar.y_radar
+
+  const x = furniture_item.left - x_radar
+  const y = y_radar - furniture_item.top
+  const width = furniture_item.width
+  const height = furniture_item.height
+
+  return {
+    x,
+    y,
+    width,
+    height,
+  }
+}

+ 1 - 1
src/views/device/detail/components/deviceAreaConfig/index.vue

@@ -53,7 +53,7 @@
       />
     </div>
 
-    <div class="viewer">
+    <div v-if="false" class="viewer">
       <div class="viewer-header">
         <div>
           <div class="viewer-header-title">屏蔽子区域配置</div>

+ 14 - 2
src/views/device/detail/index.vue

@@ -17,7 +17,7 @@
           style="margin-bottom: 10px"
         />
         <div class="pointMap">
-          <RadarView
+          <!-- <RadarView
             :angle="detailState.northAngle || 0"
             :coordinates="[
               detailState.xxStart,
@@ -27,7 +27,19 @@
             ]"
             :furnitureItems="furnitureItems"
             :targets="Object.values(targets)"
-          ></RadarView>
+          ></RadarView> -->
+
+          <DetectionAreaView
+            :coordinates="[
+              detailState.xxStart,
+              detailState.xxEnd,
+              detailState.yyStart,
+              detailState.yyEnd,
+            ]"
+            :direction="detailState.northAngle || 0"
+            :furnitureItems="furnitureItems"
+            :targets="Object.values(targets)"
+          ></DetectionAreaView>
 
           <div class="breathLine">
             <BreathLineChart