Преглед на файлове

feat: 1、新增roomStore管理房间数据;2、将展示与编辑房间组件的数据源换成roomStore数据;3、调整设备区域配置组件,删除冗余代码;

liujia преди 1 седмица
родител
ревизия
606aec2

+ 40 - 1
src/api/room/types.ts

@@ -55,9 +55,48 @@ export interface SubRegions {
   isFalling?: boolean
 }
 
+export interface FurnitureItem {
+  name: string // 家具名称
+  type: string // 家具类型
+  width: number // 家具宽度
+  length: number // 家具长度
+  rotate: number // 家具旋转角度
+  x: number // 家具距离雷达的x坐标
+  y: number // 家具距离雷达的y坐标
+}
+
+export interface LocalFurnitureItem extends FurnitureItem {
+  nanoid: string // 家具唯一标识
+}
+
+export interface SubRegionItem {
+  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-是
+}
+
+export interface LocalSubRegionItem extends SubRegionItem {
+  nanoid: string // 子区域唯一标识
+  isDraging: boolean // 正在拖动
+  isResizing: boolean // 正在调整大小
+  isActice: boolean // 是否选中
+  isTracking: boolean // 是否开启区域跟踪  0-否,1-是 对应 trackPresence 字段
+  isFalling: boolean // 是否屏蔽区域跌倒检测  0-否,1-是 对应 excludeFalling 字段
+  isBed: boolean // 是否是床 本地判断使用
+}
+
 export interface RoomData {
   roomId?: string // 房间ID
   devId?: number | string // 设备ID
   subRegions?: SubRegions[] | null // 屏蔽子区域信息
-  furnitures?: Furniture[] // 家具信息
+  furnitures?: Furniture[] | null // 家具信息
 }

+ 3 - 2
src/components/DetectionAreaView/index.vue

@@ -40,7 +40,7 @@
 
 <script setup lang="ts">
 import { ref, onMounted, watch, computed, type CSSProperties } from 'vue'
-import type { FurnitureItem, TargetPoint } from '@/types/radar'
+import type { TargetPoint } from '@/types/radar'
 import {
   convert_region_r2c,
   convert_region_c2r,
@@ -52,6 +52,7 @@ import {
   type RadarFurniture,
 } from '@/utils/coordTransform'
 import radarUrl from '@/assets/furnitures/radar.png'
+import type { FurnitureItem, LocalFurnitureItem } from '@/api/room/types'
 
 defineOptions({ name: 'DetectionAreaView' })
 
@@ -66,7 +67,7 @@ defineOptions({ name: 'DetectionAreaView' })
 interface Props {
   coordinates: [number, number, number, number]
   direction?: number
-  furnitureItems?: FurnitureItem[]
+  furnitureItems?: LocalFurnitureItem[]
   targets?: TargetPoint[]
   mode?: 'view' | 'edit'
 }

+ 9 - 4
src/components/RadarEditor/index.vue

@@ -372,7 +372,7 @@
 
 <script setup lang="ts">
 import { ref, watch, computed, onUnmounted, reactive } from 'vue'
-import type { FurnitureItem } from '@/types/radar'
+// import type { FurnitureItem } from '@/types/radar'
 import DetectionAreaView from '../DetectionAreaView/index.vue'
 import EditableFurniture from '../EditableFurniture/index.vue'
 import EditableSubregion from '../EditableSubregion/index.vue'
@@ -380,7 +380,12 @@ import type { FurnitureIconType } from '@/types/furniture'
 import { message } from 'ant-design-vue'
 import { nanoid } from 'nanoid'
 import { furnitureIconNameMap, furnitureIconSizeMap } from '@/const/furniture'
-import type { SubRegions } from '@/api/room/types'
+import type {
+  FurnitureItem,
+  LocalFurnitureItem,
+  LocalSubRegionItem,
+  SubRegions,
+} from '@/api/room/types'
 import {
   DeleteOutlined,
   ArrowLeftOutlined,
@@ -394,8 +399,8 @@ defineOptions({ name: 'RadarEditor' })
 interface Props {
   coordinates: [number, number, number, number]
   angle: number
-  furnitureItems?: FurnitureItem[]
-  subRegions?: SubRegions[]
+  furnitureItems?: LocalFurnitureItem[]
+  subRegions?: LocalSubRegionItem[]
   disabled?: boolean
 }
 const props = defineProps<Props>()

+ 78 - 0
src/stores/room.ts

@@ -0,0 +1,78 @@
+import { ref } from 'vue'
+import { defineStore } from 'pinia'
+import { nanoid } from 'nanoid'
+import type {
+  FurnitureItem,
+  LocalFurnitureItem,
+  SubRegionItem,
+  LocalSubRegionItem,
+} from '@/api/room/types'
+
+export const useRoomStore = defineStore(
+  'room',
+  () => {
+    /* ========== 接口元数据 ========== */
+    const furnitureItems = ref<FurnitureItem[]>([]) // 家具列表
+    const subRegionItems = ref<SubRegionItem[]>([]) // 子区域列表
+
+    /* ========== 本地元数据 ========== */
+    const localFurnitureItems = ref<LocalFurnitureItem[]>([]) // 家具列表
+    const localSubRegions = ref<LocalSubRegionItem[]>([]) // 子区域列表
+
+    /* ========== 缓存元数据 ========== */
+    /**
+     * 缓存接口中的家具列表
+     * @param items - 家具列表
+     */
+    function cacheFurniture(items: FurnitureItem[]) {
+      // 过滤掉雷达家具
+      const furniture = items.filter((item) => item.type !== 'radar')
+      furnitureItems.value = furniture
+      localFurnitureItems.value = furniture.map((item) => ({
+        ...item,
+        nanoid: nanoid(),
+      }))
+    }
+
+    /**
+     * 缓存接口中的子区域列表
+     * @param items - 子区域列表
+     */
+    function cacheSubRegion(items: SubRegionItem[]) {
+      subRegionItems.value = items
+      localSubRegions.value = items.map((item) => ({
+        ...item,
+        nanoid: nanoid(),
+        isDraging: false,
+        isResizing: false,
+        isActice: false,
+        isTracking: item.trackPresence === 1,
+        isFalling: item.excludeFalling === 1,
+        isBed: item.isLowSnr === 1,
+      }))
+    }
+
+    /* ========== 坐标转换 ========== */
+
+    // 家具列表坐标转换    雷达坐标系 --> 画布坐标系
+    // 家具列表坐标转换    画布坐标系 --> 雷达坐标系
+
+    // 子区域列表坐标转换    雷达坐标系 --> 画布坐标系
+    // 子区域列表坐标转换    画布坐标系 --> 雷达坐标系
+
+    return {
+      furnitureItems,
+      subRegionItems,
+      localFurnitureItems,
+      localSubRegions,
+      cacheFurniture,
+      cacheSubRegion,
+    }
+  },
+  {
+    persist: {
+      key: `${import.meta.env.VITE_STORE_NAME}_ROOM_INFO`,
+      storage: localStorage,
+    },
+  }
+)

+ 0 - 18
src/types/radar.ts

@@ -1,21 +1,3 @@
-import type { FurnitureType } from '@/api/room/types'
-// import type { FurnitureIconType } from '@/types/furniture'
-/**
- * 家具元素,用于在雷达区域中展示和编辑
- */
-export interface FurnitureItem {
-  name: string // 家具名称(如:床、桌子)
-  type: FurnitureType // 家具类型标识(如:'bed'、'table')
-  width: number // 家具宽度(单位:px)
-  length: number // 家具长度(单位:px)
-  left: number // CSS 坐标系下的 left 值(用于定位)
-  top: number // CSS 坐标系下的 top 值(用于定位)
-  rotate: number // 家具旋转角度(单位:deg)
-  x?: number // 雷达坐标系下的 X 坐标(用于接口报错)
-  y?: number // 雷达坐标系下的 Y 坐标(用于接口报错)
-  nanoid?: string // 可选:用于标识家具的唯一 ID(如 nanoid)
-}
-
 /**
  * 雷达点位图元素,用于展示检测目标位置
  */

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

@@ -5,21 +5,9 @@
       message="检测区域范围未配置或数值较小,请在设备配置调整参数!"
       banner
     />
-    <!-- <furnitureCard
-      v-if="isEditDraggable"
-      v-model:is-edit="isEditDraggable"
-      :style="{ marginTop: '30px' }"
-      @add="addHandler"
-    ></furnitureCard> -->
-
     <div class="viewer">
       <div class="viewer-header">
-        <div>
-          <div class="viewer-header-title">家具配置</div>
-          <div class="viewer-header-subtitle">
-            <span>检测范围 {{ areaWidth }} x {{ areaHeight }} cm {{ props.ranges }}</span>
-          </div>
-        </div>
+        <div class="viewer-header-title">家具与子区域配置</div>
         <div class="viewer-header-extra">
           <a-space>
             <span v-if="props.online === 0" style="color: red">⚠️设备离线,不允许编辑保存</span>
@@ -46,213 +34,20 @@
         ref="radarEditorRef"
         :coordinates="props.ranges"
         :angle="props.angle"
-        :furnitureItems="furnitureItems"
-        :subRegions="blocks"
+        :furnitureItems="roomStore.localFurnitureItems"
+        :subRegions="roomStore.localSubRegions"
         :disabled="!isEditDraggable"
-        @update:furnitureItems="mapCanvasList = $event"
       />
     </div>
-
-    <div v-if="false" class="viewer">
-      <div class="viewer-header">
-        <div>
-          <div class="viewer-header-title">屏蔽子区域配置</div>
-          <div class="viewer-header-subtitle"
-            >检测范围 {{ areaWidth }} x {{ areaHeight }} cm {{ props.ranges }}</div
-          >
-        </div>
-        <div class="viewer-header-extra">
-          <a-space>
-            <a-button size="small" :disabled="!isEditDraggable" @click="createNewBlock">{{
-              isCreating ? '创建中...' : '新建区域'
-            }}</a-button>
-          </a-space>
-        </div>
-      </div>
-
-      <div v-if="!areaAvailable" class="viewer-content">
-        <div
-          class="mapBox blockArea"
-          :style="{
-            width: `${areaWidth}px`,
-            height: `${areaHeight}px`,
-            cursor: !isEditDraggable ? 'no-drop' : isCreating ? 'crosshair' : 'default',
-          }"
-          @mousedown="handleMouseDown"
-        >
-          <furniture-icon
-            v-for="(item, index) in furnitureItems"
-            :key="index"
-            :icon="item.type"
-            :width="item.width"
-            :height="item.length"
-            :style="{
-              left: `${item.left}px`,
-              top: `${item.top}px`,
-              position: 'absolute',
-              transform: `rotate(${item.rotate}deg)`,
-              cursor: 'default',
-            }"
-            :draggable="false"
-          />
-
-          <!-- 绘制临时选区 -->
-          <div
-            v-if="currentBlock"
-            class="temp-block"
-            :style="{
-              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>
-
-          <!-- 已创建区块 -->
-          <div
-            v-for="(block, blockIndex) in blocks"
-            :key="block.id"
-            class="block-item"
-            :style="{
-              left: `${block.x}px`,
-              top: `${block.y}px`,
-              width: `${block.width}px`,
-              height: `${block.height}px`,
-              border: `2px solid ${block?.isBed ? '#1abc1a' : block.isActice ? 'yellow' : '#1890ff'}`,
-              position: 'absolute',
-              cursor: !isEditDraggable ? 'no-drop' : 'move',
-              backgroundColor: block.isBed ? 'rgba(26, 188, 26, 0.1)' : 'rgba(24, 144, 255, 0.1)',
-            }"
-            @mousedown="startDrag(block, $event)"
-            @click="selectBlock(block)"
-          >
-            <div
-              class="resize-handle"
-              :style="{
-                backgroundColor: block.isBed ? '#1abc1a' : '#1890ff',
-              }"
-              @mousedown.stop="startResize(block, $event)"
-            >
-              {{ blockIndex + 1 }}
-            </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>
-        </div>
-      </div>
-    </div>
   </a-spin>
 </template>
 
 <script setup lang="ts">
 import { ref, computed } from 'vue'
 import * as roomApi from '@/api/room'
-import type { FurnitureType, Furniture } from '@/api/room/types'
 import { message } from 'ant-design-vue'
-import { nanoid } from 'nanoid'
-import { useDropZone } from '@vueuse/core'
-// import { furnitureIconNameMap, furnitureIconSizeMap } from '@/const/furniture'
-// import type { FurnitureIconType } from '@/types/furniture'
-// import furnitureCard from '../furnitureCard/index.vue'
 import RadarEditor from '@/components/RadarEditor/index.vue'
-import { DeleteOutlined, CloseOutlined } from '@ant-design/icons-vue'
-import { getOriginPosition } from '@/utils'
-// import type { FurnitureItem } from '@/types/radar'
+import { useRoomStore } from '@/stores/room'
 
 defineOptions({
   name: 'deviceAreaConfig',
@@ -279,7 +74,7 @@ const props = withDefaults(defineProps<Props>(), {
 })
 
 const deviceRoomId = ref('')
-const furnitureItems = ref<Furniture[]>([])
+const roomStore = useRoomStore()
 
 // 检测区域宽度 length
 const areaWidth = computed(() => {
@@ -315,557 +110,60 @@ const fetchRoomLayout = async () => {
     }
     const { furnitures, roomId, subRegions } = res.data
     deviceRoomId.value = roomId || ''
-    if (furnitures) {
-      furnitureItems.value = furnitures!.map((item) => {
-        // 将接口的家具,添加在家具画布上
-        mapCanvasList.value.push({
-          name: item.name,
-          type: item.type,
-          width: item.width,
-          height: item.length,
-          top: item.top,
-          left: item.left,
-          x: item.x,
-          y: item.y,
-          rotate: item.rotate,
-          nanoid: nanoid(),
-        })
-        // 将接口的家具,添加在屏蔽子区域画布上
-        return {
-          ...item,
-          width: item.width || 45,
-          length: item.length || 45,
-          top: item.top || 0,
-          left: item.left || 0,
-          rotate: item.rotate || 0,
-          x: item.x || 0,
-          y: item.y || 0,
-          nanoid: nanoid(),
-        }
-      })
-    }
-
-    if (subRegions) {
-      // 将接口的子区域,添加在子区域画布上
-      subRegions.forEach((item, index) => {
-        blocks.value.push({
-          // 本地需要使用的数据
-          id: nanoid(),
-          x: item.startXx + originX,
-          y: originY - item.startYy,
-          ox: item.startXx + originX - originX,
-          oy: originY - item.startYy - originY,
-          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 && mapCanvasList.value.some((item) => item.type === 'bed'),
-          // 来自接口回显的数据
-          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,
-        })
-      })
-
-      console.log('🚀', blocks.value)
-    }
+    roomStore.cacheFurniture(furnitures ?? [])
+    roomStore.cacheSubRegion(subRegions ?? [])
     spinning.value = false
   } catch (error) {
     console.error('❌获取房间布局信息失败', error)
     spinning.value = false
   }
 }
-fetchRoomLayout().finally(() => {
-  // 获取房间信息后,初始化雷达图标
-  // initRadarIcon()
-})
-
-interface CanvaseItem {
-  name: string // 名称
-  type: string // 类型,图标icon
-  width: number // 家具宽度
-  height?: number // 家具长度
-  length?: number // 家具长度
-  top: number // 距离检测范围上方位置 cm
-  left: number // 	距离检测范围左边位置 cm
-  rotate: number // 旋转角度: 0°,90°,180°,270°
-  x?: number // 距离雷达的X距离
-  y?: number // 距离雷达的Y距离
-  nanoid?: string // 本地使用
-  isActice?: boolean // 是否选中 本地使用
-  isDragging?: boolean // 是否拖拽 本地使用
-}
+fetchRoomLayout()
 
 const radarEditorRef = ref<InstanceType<typeof RadarEditor>>()
 
 // 画布上的家具列表
-const mapCanvasList = ref<CanvaseItem[]>([])
 const isEditDraggable = ref(false)
-// 家具列表添加
-// const addHandler = (icon: FurnitureIconType) => {
-//   console.log('addHandler', icon)
-
-//   if (icon === 'bed') {
-//     const isExist = mapCanvasList.value.some((item) => item.type === icon)
-//     if (isExist) {
-//       message.error('床已经添加过了,不可重复添加')
-//       return
-//     }
-//   }
-
-//   const originWidth = furnitureIconSizeMap[icon].width || 30
-//   const originHeight = furnitureIconSizeMap[icon].height || 30
-
-//   const newItem: FurnitureItem = {
-//     name: furnitureIconNameMap[icon],
-//     type: icon as FurnitureType,
-//     width: originWidth,
-//     length: originHeight,
-//     top: 0,
-//     left: 0,
-//     rotate: 0,
-//     x: 0,
-//     y: 0,
-//     nanoid: nanoid(),
-//   }
-
-//   radarEditorRef.value?.addFurniture(newItem)
-//   message.success('已添加家具')
-
-//   if (icon === 'bed') {
-//     blocks.value.unshift({
-//       id: nanoid(),
-//       x: 20,
-//       y: 15,
-//       ox: -150,
-//       oy: 180,
-//       width: originWidth,
-//       height: originHeight,
-//       isDragging: false,
-//       isResizing: false,
-//       isActice: false,
-//       isTracking: false,
-//       isFalling: false,
-//       isBed: true,
-//       startXx: -150,
-//       stopXx: -100,
-//       startYy: 180,
-//       stopYy: 120,
-//       startZz: 0,
-//       stopZz: 0,
-//       isLowSnr: 1,
-//       isDoor: 0,
-//       presenceEnterDuration: 3,
-//       presenceExitDuration: 3,
-//       trackPresence: 0,
-//       excludeFalling: 0,
-//     })
-//     message.success('已添加子区域')
-//   }
-// }
-
-const contentEl = ref<HTMLElement>()
-const currentDragItem = ref<CanvaseItem | null>(null)
-// 内容区域放置处理
-useDropZone(contentEl, {
-  onDrop(files: File[] | null, event: DragEvent) {
-    if (contentEl.value && currentDragItem.value) {
-      const rect = contentEl.value.getBoundingClientRect()
-      // 计算基于画布容器的精确坐标
-      const x = event.clientX - rect.left
-      const y = event.clientY - rect.top
-      // 中心点对齐计算
-      currentDragItem.value.top = y - currentDragItem.value.height! / 2
-      currentDragItem.value.left = x - currentDragItem.value.width / 2
-      // 添加边界检查
-      // const maxX = contentEl.value.offsetWidth - currentDragItem.value.width
-      // const maxY = contentEl.value.offsetHeight - currentDragItem.value.height
-      // currentDragItem.value.left = Math.max(0, Math.min(x - currentDragItem.value.width / 2, maxX))
-      // currentDragItem.value.top = Math.max(0, Math.min(y - currentDragItem.value.height / 2, maxY))
-    }
-  },
-})
-
-const clickedDragItem = ref<CanvaseItem | null>()
-
-mapCanvasList.value.forEach((item) => {
-  item.isActice = false
-})
-clickedDragItem.value = null
-
-// 关闭子区域属性
-const closeSubregionAttr = () => {
-  blocks.value.forEach((item) => {
-    item.isActice = false
-  })
-  selectedBlock.value = null
-}
-
-// 新增区块类型
-interface BlockItem {
-  // 本地用
-  id: string // 唯一标识
-  x: number // 区块基于父元素的X偏移量,区块的左上角x坐标
-  y: number // 区块基于父元素的Y偏移量,区块的左上角y坐标
-  ox: number // 区块基于原点的X偏移量,区块的左上角x坐标
-  oy: number // 区块基于原点的Y偏移量,区块的左上角y坐标
-  width: number // 区块宽度
-  height: number // 区块高度
-  isDragging: boolean // 是否正在拖动
-  isResizing: boolean // 是否正在调整大小
-  isActice: boolean // 是否选中
-  isTracking: boolean // 是否开启区域跟踪  0-否,1-是 对应 trackPresence 字段
-  isFalling: boolean // 是否屏蔽区域跌倒检测  0-否,1-是 对应 excludeFalling 字段
-  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-是
-}
-
-const blocks = ref<BlockItem[]>([])
-const isCreating = ref(false)
-const currentBlock = ref<{
-  startX: number
-  startY: number
-  currentX: number
-  currentY: number
-} | null>(null)
-const selectedBlock = ref<BlockItem | null>(null)
-
-// 新建区块处理
-const createNewBlock = () => {
-  if (blocks.value && blocks.value.length > 5) {
-    message.warn('最多只能创建6个区块')
-    return
-  }
-  isCreating.value = true
-}
-
-// 获取容器边界
-const getContainerRect = () => {
-  const container = document.querySelector('.blockArea') as HTMLElement
-  return container?.getBoundingClientRect() || { left: 0, top: 0 }
-}
-
-// 鼠标事件处理
-const handleMouseDown = (e: MouseEvent) => {
-  if (!isEditDraggable.value) return
-  console.log('handleMouseDown', e)
-  // blocks.value.forEach((item) => {
-  //   item.isActice = false
-  // })
-  // selectedBlock.value = null
-  if (!isCreating.value) return
-
-  const rect = getContainerRect()
-  const startX = e.clientX - rect.left
-  const startY = e.clientY - rect.top
-
-  currentBlock.value = {
-    startX,
-    startY,
-    currentX: startX,
-    currentY: startY,
-  }
-
-  document.addEventListener('mousemove', handleMouseMove)
-  document.addEventListener('mouseup', handleMouseUp)
-}
-
-// 鼠标移动处理
-const handleMouseMove = (e: MouseEvent) => {
-  if (!currentBlock.value) return
-
-  const rect = getContainerRect()
-  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
-  const width = Math.abs(currentX - startX)
-  const height = Math.abs(currentY - startY)
-
-  if (width > 10 && height > 10) {
-    blocks.value.push({
-      // 本地用
-      id: nanoid(),
-      x: Math.round(Math.min(startX, currentX)),
-      y: Math.round(Math.min(startY, currentY)),
-      ox: Math.round(Math.min(startX, currentX)) - originX,
-      oy: Math.round(Math.min(startY, currentY)) - originY,
-      width,
-      height,
-      isDragging: false,
-      isResizing: false,
-      isActice: false,
-      isTracking: false,
-      isFalling: false,
-      // 接口用
-      startXx: Math.round(Math.min(startX, currentX)) - originX,
-      stopXx: Math.round(Math.min(startX, currentX)) - originX + width,
-      startYy: Math.round(Math.min(startY, currentY)) - originY,
-      stopYy: Math.round(Math.min(startY, currentY)) - originY + height,
-      startZz: 0,
-      stopZz: 0,
-      isLowSnr: 0,
-      isDoor: 0,
-      presenceEnterDuration: 3,
-      presenceExitDuration: 3,
-      trackPresence: 0,
-      excludeFalling: 0,
-    })
-  }
-
-  currentBlock.value = null
-  isCreating.value = false
-  document.removeEventListener('mousemove', handleMouseMove)
-  document.removeEventListener('mouseup', handleMouseUp)
-}
-
-// 区块拖动
-const startDrag = (block: BlockItem, e: MouseEvent) => {
-  if (!isEditDraggable.value) return
-  console.log('startDrag', block)
-  e.stopPropagation()
-  block.isDragging = true
-  block.isActice = true
-  const container = document.querySelector('.blockArea') as HTMLElement
-  const rect = container.getBoundingClientRect()
-  const offsetX = e.clientX - rect.left - block.x
-  const offsetY = e.clientY - rect.top - block.y
-
-  const moveHandler = (e: MouseEvent) => {
-    const newX = e.clientX - rect.left - offsetX
-    const newY = e.clientY - rect.top - offsetY
-    const containerWidth = container.offsetWidth
-    const containerHeight = container.offsetHeight
-
-    block.x = Math.max(0, Math.min(newX, containerWidth - block.width))
-    block.y = Math.max(0, Math.min(newY, containerHeight - block.height))
-    block.ox = block.x - originX
-    block.oy = originY - block.y
-    block.startXx = block.ox
-    block.stopXx = block.ox + block.width
-    block.startYy = block.oy
-    block.stopYy = block.oy - block.height
-  }
-
-  const upHandler = () => {
-    block.isDragging = false
-    block.isActice = false
-    document.removeEventListener('mousemove', moveHandler)
-    document.removeEventListener('mouseup', upHandler)
-  }
-
-  document.addEventListener('mousemove', moveHandler)
-  document.addEventListener('mouseup', upHandler)
-}
-
-const selectBlock = (block: BlockItem) => {
-  if (!isEditDraggable.value) return
-  console.log('selectBlock', block)
-  selectedBlock.value = block
-  blocks.value.forEach((item) => {
-    item.isActice = item === block
-  })
-}
-
-const { originX, originY } = getOriginPosition(props.ranges, [
-  currentDragItem.value?.left as number,
-  currentDragItem.value?.top as number,
-])
-
-// 初始化添加雷达图标
-// const initRadarIcon = () => {
-//   console.log('initRadarIcon', mapCanvasList.value, furnitureItems.value)
-// }
 
 // 保存所有配置
 const saveAllConfig = () => {
-  console.log('保存所有配置')
-  // const blockData = blocks.value.map((item) => {
-  //   return {
-  //     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),
-  //   }
-  // })
+  console.log('保存所有配置', {
+    furnitureItems: roomStore.localFurnitureItems,
+    subRegions: roomStore.localSubRegions,
+  })
 
-  const blockData =
-    Array.isArray(JSON.parse(localStorage.getItem('subRegions') ?? '[]')) &&
-    JSON.parse(localStorage.getItem('subRegions') ?? '[]').map((item) => {
-      return {
-        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),
-      }
-    })
-  console.log('当前所有区块配置:', blockData)
   try {
     const res = roomApi.saveRoomInfo({
       roomId: deviceRoomId.value,
       devId: props.devId,
-      furnitures: mapCanvasList.value
-        .filter((item) => item.type !== 'radar')
-        .map((item) => {
-          return {
-            name: item.name,
-            type: item.type as FurnitureType,
-            width: item.width,
-            length: item.height || (item.length as number),
-            top: item.top,
-            left: item.left,
-            rotate: item.rotate as 0 | 90 | 180 | 270,
-            x: item?.x || 0,
-            y: item?.y || 0,
-          }
-        }),
-      subRegions: blockData,
+      // furnitures: mapCanvasList.value
+      //   .filter((item) => item.type !== 'radar')
+      //   .map((item) => {
+      //     return {
+      //       name: item.name,
+      //       type: item.type as FurnitureType,
+      //       width: item.width,
+      //       length: item.height || (item.length as number),
+      //       top: item.top,
+      //       left: item.left,
+      //       rotate: item.rotate as 0 | 90 | 180 | 270,
+      //       x: item?.x || 0,
+      //       y: item?.y || 0,
+      //     }
+      //   }),
+      // subRegions: blockData,
     })
-    console.log('保存所有配置 成功', res)
+    console.log('保存所有配置 ✅', res)
     message.success('保存成功')
     emit('success')
   } catch (error) {
-    console.error('保存所有配置 失败', error)
-  }
-}
-
-const startResize = (block: BlockItem, e: MouseEvent) => {
-  block.isResizing = true
-  selectedBlock.value = block
-  const startX = e.clientX
-  const startY = e.clientY
-  const initialWidth = block.width
-  const initialHeight = block.height
-
-  const moveHandler = (e: MouseEvent) => {
-    const rect = getContainerRect()
-    const deltaX = e.clientX - startX
-    const deltaY = e.clientY - startY
-    // 限制最小尺寸和容器边界
-    block.width = Math.max(50, Math.min(initialWidth + deltaX, rect.width - block.x))
-    block.height = Math.max(50, Math.min(initialHeight + deltaY, rect.height - block.y))
-
-    // 改变了区块的长款,元素的结束位置也相应变化
-    block.stopXx = block.ox + block.width
-    block.stopYy = block.oy + block.height
-  }
-
-  const upHandler = () => {
-    block.isResizing = false
-    selectedBlock.value = null
-    document.removeEventListener('mousemove', moveHandler)
-    document.removeEventListener('mouseup', upHandler)
-  }
-
-  document.addEventListener('mousemove', moveHandler)
-  document.addEventListener('mouseup', upHandler)
-}
-
-const blockInputPressEnter = (e: Event, el: BlockItem, attr: string) => {
-  console.log('blockInputPressEnter', e, el, attr)
-  if (!el) return
-  if (attr === 'startXx') {
-    el.startXx = Number(el[attr as keyof BlockItem])
-    el.x = el.startXx + originX
-  }
-  if (attr === 'stopXx') {
-    el.stopXx = Number(el[attr as keyof BlockItem])
-    // el.width = el.stopXx + originX - el.width
-    el.width = el.stopXx - el.startXx
-  }
-
-  if (attr === 'startYy') {
-    el.startYy = Number(el[attr as keyof BlockItem])
-    el.x = el.startYy + originY
-  }
-  if (attr === 'stopYy') {
-    el.stopYy = Number(el[attr as keyof BlockItem])
-    // el.height = el.stopYy + originY - el.height
-    el.height = el.stopYy - el.startYy
-  }
-}
-
-const blockInputBlur = (e: Event, el: BlockItem, attr: string) => {
-  console.log('blockInputBlur', e, el, attr)
-  if (!el) return
-  if (attr === 'startXx') {
-    el.startXx = Number(el[attr as keyof BlockItem])
-    el.x = el.startXx + originX
-  }
-  if (attr === 'stopXx') {
-    el.stopXx = Number(el[attr as keyof BlockItem])
-    // el.width = el.stopXx + originX - el.width
-    el.width = el.stopXx - el.startXx
-  }
-
-  if (attr === 'startYy') {
-    el.startYy = Number(el[attr as keyof BlockItem])
-    el.x = el.startYy + originY
-  }
-  if (attr === 'stopYy') {
-    el.stopYy = Number(el[attr as keyof BlockItem])
-    // el.height = el.stopYy + originY - el.height
-    el.height = el.stopYy - el.startYy
-  }
-}
-
-const deleteBlockArea = (id: string) => {
-  if (id) {
-    blocks.value = blocks.value.filter((item) => item.id !== id)
-    selectedBlock.value = null
+    console.error('保存所有配置 ❌', error)
   }
 }
 </script>
 
 <style scoped lang="less">
 .viewer {
-  padding: 10px;
-  min-width: 500px;
   flex-shrink: 0;
 
   &-header {
@@ -878,10 +176,6 @@ const deleteBlockArea = (id: string) => {
       font-weight: 600;
       line-height: 24px;
     }
-    &-subtitle {
-      font-size: 14px;
-      color: #666;
-    }
   }
 
   &-content {
@@ -889,94 +183,4 @@ const deleteBlockArea = (id: string) => {
     gap: 20px;
   }
 }
-
-.mapBox {
-  background-color: #e0e0e0;
-  background-image:
-    linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
-    linear-gradient(to right, rgba(0, 0, 0, 0.1) 1px, transparent 1px);
-  background-size: 20px 20px;
-  position: relative;
-  flex-shrink: 0;
-
-  // 添加黑边框
-  &::before {
-    content: '';
-    position: absolute;
-    top: -5px;
-    left: -5px;
-    width: calc(100% + 10px);
-    height: calc(100% + 10px);
-    border: 5px solid rgba(0, 0, 0, 0.8);
-    box-sizing: border-box;
-    pointer-events: none;
-  }
-}
-
-.mapConfig {
-  background-color: #f5f5f5;
-  border-radius: 10px;
-  padding: 12px;
-  &-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;
-    }
-  }
-}
-
-.temp-block {
-  position: absolute;
-  background: rgba(24, 144, 255, 0.2);
-  border: 2px dashed #1890ff;
-}
-
-.block-item {
-  background: rgba(24, 144, 255, 0.1);
-
-  .resize-handle {
-    position: absolute;
-    right: -4px;
-    bottom: -4px;
-    width: 15px;
-    height: 15px;
-    background: #1890ff;
-    cursor: nwse-resize;
-    font-size: 12px;
-    color: #fff;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-}
-
-.dragging-item {
-  opacity: 0.5;
-  transition: opacity 0.2s ease;
-}
 </style>

+ 25 - 106
src/views/device/detail/index.vue

@@ -7,7 +7,6 @@
             <a-button type="primary" size="small" @click="roomConfigHandler('area')">
               区域配置
             </a-button>
-            <div class="extraIcon"> <FullscreenOutlined @click="openFullView = true" /> </div>
           </a-space>
         </template>
         <a-alert
@@ -36,46 +35,23 @@
               detailState.yyStart,
               detailState.yyEnd,
             ]"
-            :direction="270"
-            :furnitureItems="furnitureItems"
+            :direction="detailState.northAngle || 0"
+            :furnitureItems="roomStore.furnitureItems"
             :targets="Object.values(targets)"
           ></DetectionAreaView>
 
           <div class="breathLine">
             <BreathLineChart
-              v-if="furnitureItems && furnitureItems.some((item) => item.type === 'bed')"
+              v-if="
+                roomStore.furnitureItems &&
+                roomStore.furnitureItems.some((item) => item.type === 'bed')
+              "
               :data="breathRpmList"
             ></BreathLineChart>
           </div>
         </div>
       </info-card>
 
-      <FullViewModal v-model:open="openFullView" :title="detailState.devName">
-        <div class="fullView">
-          <div class="pointTitle">实时点位图</div>
-          <div class="pointMap">
-            <RadarView
-              :angle="detailState.northAngle"
-              :coordinates="[
-                detailState.xxStart,
-                detailState.xxEnd,
-                detailState.yyStart,
-                detailState.yyEnd,
-              ]"
-              :furnitureItems="furnitureItems"
-              :targets="Object.values(targets)"
-            ></RadarView>
-          </div>
-
-          <div
-            v-if="furnitureItems && furnitureItems.some((item) => item.type === 'bed')"
-            class="breathLine"
-          >
-            <BreathLineChart :data="breathRpmList"></BreathLineChart>
-          </div>
-        </div>
-      </FullViewModal>
-
       <info-card>
         <info-item-group title="基本信息">
           <template #extra>
@@ -251,8 +227,6 @@
         }"
         :mode="configDrawerMode"
         :room-id="deviceRoomId"
-        :furniture-items="furnitureItems"
-        :sub-region-items="subRegionItems"
         :angle="detailState.northAngle ?? 0"
         :online="detailState.online"
         @success="saveConfigSuccess"
@@ -297,7 +271,6 @@ import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
 import { useRoute } from 'vue-router'
 import { message } from 'ant-design-vue'
 import * as roomApi from '@/api/room'
-import type { Furniture } from '@/api/room/types'
 import mqtt, { type MqttClient } from 'mqtt'
 import * as deviceApi from '@/api/device'
 import type { DeviceDetailData } from '@/api/device/types'
@@ -306,16 +279,14 @@ import deviceConfigDrawer from './components/deviceConfig/index.vue'
 import deviceStatsDrawer from './components/deviceStatsDrawer/index.vue'
 import BreathLineChart from './components/breathLineChart/index.vue'
 import { formatDateTime } from '@/utils'
-import { FullscreenOutlined } from '@ant-design/icons-vue'
-import FullViewModal from './components/fullViewModal/index.vue'
 import alarmPlanModal from './components/alarmPlanModal/index.vue'
 import { EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
 import * as alarmApi from '@/api/alarm'
 import { Empty } from 'ant-design-vue'
 const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
-// import { getOriginPosition } from '@/utils'
 import { useDict } from '@/hooks/useDict'
 import DeviceUpgrade from './components/DeviceUpgrade/index.vue'
+import { useRoomStore } from '@/stores/room'
 
 defineOptions({
   name: 'DeviceDetail',
@@ -323,30 +294,12 @@ defineOptions({
 
 const route = useRoute()
 // const router = useRouter()
-const devId = ref<string>((route.query.devId as string) || '') // 设备id
-const clientId = ref<string>((route.query.clientId as string) || '') // 设备id
-
-interface BlockItem {
-  startXx: number // 屏蔽子区域X开始
-  stopXx: number // 屏蔽子区域X结束
-  startYy: number // 屏蔽子区域Y开始
-  stopYy: number // 屏蔽子区域Y结束
-  startZz: number // 屏蔽子区域Z开始
-  stopZz: number // 屏蔽子区域Z结束
-  isLowSnr: number // 默认0
-  isDoor: number // 是否是门 0-否,1-是 默认0
-  presenceEnterDuration: number // 	人员进入时间 默认3
-  presenceExitDuration: number // 人员离开时间 默认3
-  trackPresence: number // 是否开启区域跟踪存在 0-否,1-是
-  excludeFalling: number // 是否屏蔽区域跌倒检测 0-否,1-是
-}
+const devId = ref<string>((route.query.devId as string) || '')
+const clientId = ref<string>((route.query.clientId as string) || '')
+const roomStore = useRoomStore()
 
 const deviceRoomId = ref<string>('')
-const furnitureItems = ref<Furniture[]>([])
-const subRegionItems = ref<BlockItem[]>([])
-/**
- * 获取房间布局
- */
+// 获取房间布局
 const fetchRoomLayout = async () => {
   console.log('fetchRoomLayout', devId.value)
   if (!devId.value) {
@@ -360,21 +313,9 @@ const fetchRoomLayout = async () => {
     console.log('✅获取到房间布局信息', res)
     if (!res) return
     const { furnitures, roomId, subRegions } = res.data
-    if (furnitures) {
-      // 添加接口返回的家具数据
-      furnitureItems.value = furnitures!.map((item) => ({
-        ...item,
-        width: item.width || 45,
-        length: item.length || 45,
-        top: item.top || 0,
-        left: item.left || 0,
-        rotate: item.rotate || 0,
-        x: item.x || 0,
-        y: item.y || 0,
-      }))
-    }
-    deviceRoomId.value = roomId || ''
-    subRegionItems.value = subRegions || []
+    roomStore.cacheFurniture(furnitures ?? [])
+    roomStore.cacheSubRegion(subRegions ?? [])
+    deviceRoomId.value = roomId ?? ''
   } catch (error) {
     console.error('❌获取房间布局信息失败', error)
   }
@@ -437,32 +378,6 @@ const fetchDeviceDetail = async () => {
     console.log('✅获取到设备详情', res)
     detailState.value = res.data
     spinning.value = false
-
-    // 获取雷达图标尺寸
-    // const { radarX, radarY, radarWidth, radarHeight, originOffsetX, originOffsetY } =
-    //   getOriginPosition(
-    //     [
-    //       res.data.xxStart as number,
-    //       res.data.xxEnd as number,
-    //       res.data.yyStart as number,
-    //       res.data.yyEnd as number,
-    //     ],
-    //     [0, 0]
-    //   )
-
-    furnitureItems.value = furnitureItems.value.filter((item) => item.type !== 'radar')
-    // 添加雷达图标
-    // furnitureItems.value.unshift({
-    //   name: '雷达',
-    //   type: 'radar',
-    //   width: radarWidth,
-    //   length: radarHeight,
-    //   top: radarY,
-    //   left: radarX,
-    //   x: originOffsetX,
-    //   y: originOffsetY,
-    //   rotate: 0,
-    // })
   } catch (error) {
     console.error('❌获取设备详情失败', error)
     spinning.value = false
@@ -527,7 +442,7 @@ const resetMqttTimeout = () => {
 // 呼吸率
 const breathRpmList = ref<number[]>([])
 
-onMounted(() => {
+const subscribePoint = () => {
   console.log('onMounted', mqttClient)
   const mqttConfig = {
     host: import.meta.env.VITE_MQTT_HOST_POINT,
@@ -640,11 +555,17 @@ onMounted(() => {
       console.error('MQTT消息解析失败', e)
     }
   })
-})
+}
 
-// setInterval(() => {
-//   breathRpmList.value.push(Math.floor(Math.random() * 30))
-// }, 100)
+// const mockBreathRpm = () => {
+//   setInterval(() => {
+//     breathRpmList.value.push(Math.floor(Math.random() * 30))
+//   }, 100)
+// }
+
+onMounted(() => {
+  subscribePoint()
+})
 
 const areaAvailable = computed(() => {
   const { length, width } = detailState.value
@@ -659,8 +580,6 @@ onUnmounted(() => {
   if (mqttTimeout) clearTimeout(mqttTimeout)
 })
 
-const openFullView = ref(false) // 全屏展示点位图
-
 const alarmPlanVisible = ref(false) // 告警计划弹窗
 const alarmPlanTitle = ref('新增告警计划') // 告警计划弹窗标题
 const alarmPlanId = ref<number | null>(null) // 当前编辑的告警计划id