Explorar o código

Merge branch 'feature-1.1.0' of http://43.137.10.199:3000/HFLN/ln-web into feature-1.1.0

wangming hai 1 día
pai
achega
1abfdcf1b7

+ 18 - 0
CHANGELOG.md

@@ -1,4 +1,22 @@
 
+## v0.9.1 (2025-10-21)
+- feat: 暂时禁用家具自身旋转功能; (08a435e)
+- fix: 解决家具编辑回显的偏移问题 (cb0a5bd)
+
+## v0.9.0 (2025-10-21)
+- fix(EditableFurniture): 修复家具旋转后位置计算错误并添加序号显示 (5985c73)
+- feat: 调整转换函数变量名; (d708571)
+- feat: 添加检测区域提示的判断条件; (59b5d37)
+- fix: 告警计划模板交互调整,生效方式在四种频次统计的时候不需要进行选择;(如厕频次统计、夜间如厕频次统计、如厕频次异常、卫生间频次统计) (1a16648)
+- feat: 调整家具在90度和270度的时候位置的偏移;设置子区域随着角度回显逻辑; (2b648ce)
+- fix: 告警提示统计时间不完整; (caeae6c)
+- fix: 设备列表仅超管才展示抹掉数据按钮; (3987040)
+- feat: 新增baseRadarView (a5b9888)
+- fix: 修复家具坐标转换和初始化问题 (15e04a7)
+- feat: 调整点位坐标根据角度旋转位置; (b04901e)
+- feat(雷达编辑): 增加家具坐标转换和旋转功能 (8a4d934)
+- fix(区域配置): 修复区域配置保存失败时未清空缓存的问题 (958a6cc)
+
 ## v0.8.2 (2025-10-15)
 - feat: 家具编辑数据源使用roomStore数据; (75c8ce1)
 - feat: 1、新增roomStore管理房间数据;2、将展示与编辑房间组件的数据源换成roomStore数据;3、调整设备区域配置组件,删除冗余代码; (606aec2)

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "ln-web",
-  "version": "0.8.2",
+  "version": "0.9.1",
   "private": true,
   "type": "module",
   "scripts": {

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

@@ -45,7 +45,7 @@ import {
   type CanvasRect,
   type RadarPosition,
   type RadarFurniture,
-  rotateRect,
+  rotateRect_cw,
 } from '@/utils/coordTransform'
 import radarUrl from '@/assets/furnitures/radar.png'
 import type { LocalFurnitureItem } from '@/api/room/types'
@@ -118,7 +118,7 @@ const localFurnitureItems = computed(() => {
       },
       radarPosition
     )
-    const rotatedRect = rotateRect(
+    const rotatedRect = rotateRect_cw(
       {
         left: itemConvert.left,
         top: itemConvert.top,
@@ -450,8 +450,6 @@ 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'
@@ -460,9 +458,8 @@ const drawInfoText = () => {
   const baseX = 5
   const baseY = CANVAS_SIZE - 10
 
-  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`,
+    `检测区域: [${xStart}, ${xEnd}, ${yStart}, ${yEnd}] 角度:${props.direction}°`,
     baseX,
     baseY
   )

+ 62 - 36
src/components/EditableFurniture/index.vue

@@ -3,6 +3,7 @@
     <div class="furniture-rotated-container" :style="rotatedContainerStyle">
       <div class="furniture-wrapper">
         <furnitureIcon :icon="localItem.type" :width="localItem.width" :height="localItem.length" />
+        <span class="text"> {{ idx + 1 }}</span>
       </div>
     </div>
   </div>
@@ -11,12 +12,13 @@
 <script setup lang="ts">
 import { reactive, watch, computed, onMounted, nextTick, type CSSProperties } from 'vue'
 import type { FurnitureItem, FurnitureType, LocalFurnitureItem } from '@/api/room/types'
-import { convert_furniture_r2c, rotateRect } from '@/utils/coordTransform'
+import { convert_furniture_r2c, rotateRect_cw } from '@/utils/coordTransform'
 
 defineOptions({ name: 'EditableFurniture' })
 
 interface Props {
   item: FurnitureItem
+  idx: number
   angle: number
   coordinates: [number, number, number, number]
   canvasSize?: number
@@ -118,33 +120,34 @@ const pixelPosition = reactive({ left: props.item.left ?? 0, top: props.item.top
 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 containerStyle = computed(() => {
+  const cssObj: CSSProperties = {
+    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,
+  }
+  console.log('💆‍♀️💆‍♀️💆‍♀️💆‍♀️ AAAAA containerStyle', cssObj)
+  return cssObj
+})
 
 // 旋转容器样式 - 调整位置使左上角对齐
-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
-)
+const rotatedContainerStyle = computed(() => {
+  const cssObj: CSSProperties = {
+    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',
+  }
+  console.log('💆‍♀️💆‍♀️💆‍♀️💆‍♀️ BBBBB rotatedContainerStyle', cssObj)
+  return cssObj
+})
 
 // 监听props变化
 watch(
@@ -192,14 +195,16 @@ const updatePixelPosition = () => {
 }
 
 const initPixelPosition = () => {
-  const { x, y, width, length } = props.item
+  const { x, y, width: itemLength, length: itemWidth } = props.item
+  // ⚠️ width: itemLength, length: itemWidth 回显时需要转换一下,不然家具会有偏移
+
   // === 1️⃣ 将房间坐标转换为画布坐标 ===
   const itemConvert = convert_furniture_r2c(
     {
       x: x,
       y: y,
-      width: width,
-      height: length,
+      width: itemWidth,
+      height: itemLength,
     },
     {
       x_radar: 250,
@@ -208,12 +213,12 @@ const initPixelPosition = () => {
   )
 
   // === 2️⃣ 再根据雷达朝向旋转 ===
-  const rotatedRect = rotateRect(
+  const rotatedRect = rotateRect_cw(
     {
       left: itemConvert.left,
       top: itemConvert.top,
-      width: width,
-      height: length,
+      width: itemConvert.width,
+      height: itemConvert.height,
     },
     { x: 250, y: 250 },
     props.angle
@@ -224,8 +229,8 @@ const initPixelPosition = () => {
   pixelPosition.top = rotatedRect.top
 
   // 同步像素位置到 localItem(方便 emit)
-  localItem.left = rotatedRect.left
-  localItem.top = rotatedRect.top
+  localItem.left = pixelPosition.left
+  localItem.top = pixelPosition.top
 
   console.log('✅ Init pixel position (after convert & rotate):', {
     angle: props.angle,
@@ -281,10 +286,21 @@ const startDrag = (e: MouseEvent) => {
   const startY = e.clientY
   const initLeft = pixelPosition.left
   const initTop = pixelPosition.top
+  const canvasSize = props.canvasSize
 
   const onMove = (ev: MouseEvent) => {
-    pixelPosition.left = initLeft + (ev.clientX - startX)
-    pixelPosition.top = initTop + (ev.clientY - startY)
+    // 计算新的位置
+    let newLeft = initLeft + (ev.clientX - startX)
+    let newTop = initTop + (ev.clientY - startY)
+
+    // 边界检查 - 确保家具不会超出画布
+    newLeft = Math.max(0, Math.min(newLeft, canvasSize - boundingBox.value.width))
+    newTop = Math.max(0, Math.min(newTop, canvasSize - boundingBox.value.height))
+
+    // 更新位置
+    pixelPosition.left = newLeft
+    pixelPosition.top = newTop
+
     updateGeoPosition()
     emit('update', { ...localItem })
   }
@@ -323,10 +339,20 @@ const startDrag = (e: MouseEvent) => {
       display: flex;
       align-items: center;
       justify-content: center;
+      position: relative;
 
       :deep(*) {
         pointer-events: none;
       }
+
+      .text {
+        font-size: 14px;
+        color: #f41313;
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+      }
     }
   }
 

+ 37 - 2
src/components/EditableSubregion/index.vue

@@ -49,6 +49,7 @@ import { message } from 'ant-design-vue'
 import { nanoid } from 'nanoid'
 import { useRoomStore } from '@/stores/room'
 import type { LocalSubRegionItem } from '@/api/room/types'
+import { convert_region_r2c, rotateRect_cw } from '@/utils/coordTransform'
 
 defineOptions({ name: 'EditableSubregion' })
 const roomStore = useRoomStore()
@@ -292,6 +293,41 @@ const startResize = (block: LocalSubRegionItem, e: MouseEvent) => {
 // ✅ 回显数据(地理→像素)
 const echoSubRegions = () => {
   roomStore.localSubRegions = roomStore.localSubRegions.map((item, index) => {
+    const itemRegion = convert_region_r2c(
+      {
+        x_cm_start: Number(item.startXx),
+        x_cm_stop: Number(item.stopXx),
+        y_cm_start: Number(item.startYy),
+        y_cm_stop: Number(item.stopYy),
+      },
+      {
+        x_radar: 250,
+        y_radar: 250,
+      }
+    )
+
+    const routeRegion = rotateRect_cw(itemRegion, { x: 250, y: 250 }, props.angle)
+
+    return {
+      ...item,
+      nanoid: item.nanoid || nanoid(),
+      isDraging: false,
+      isResizing: false,
+      isActive: false,
+      isTracking: Boolean(item.trackPresence),
+      isFalling: Boolean(item.excludeFalling),
+      isBed: index === 0 && props.hasBed,
+      pixelX: routeRegion.left,
+      pixelY: routeRegion.top,
+      pixelWidth: routeRegion.width,
+      pixelHeight: routeRegion.height,
+    }
+  })
+}
+echoSubRegions()
+
+const renderSubRegions = () => {
+  roomStore.localSubRegions = roomStore.localSubRegions.map((item, index) => {
     const centerX = (Number(item.startXx) + Number(item.stopXx)) / 2
     const centerY = (Number(item.startYy) + Number(item.stopYy)) / 2
     const w = Math.abs(Number(item.stopXx) - Number(item.startXx))
@@ -314,7 +350,6 @@ const echoSubRegions = () => {
     }
   })
 }
-echoSubRegions()
 
 // ✅ 监听手动调整,重新回显像素位置
 const geoCoordinatesSignature = computed(() => {
@@ -325,7 +360,7 @@ const geoCoordinatesSignature = computed(() => {
 
 watch(geoCoordinatesSignature, () => {
   nextTick(() => {
-    echoSubRegions()
+    renderSubRegions()
   })
 })
 

+ 19 - 14
src/components/RadarEditor/index.vue

@@ -4,9 +4,10 @@
       <template #furnitures>
         <template v-if="showFurniture">
           <EditableFurniture
-            v-for="item in roomStore.localFurnitureItems"
+            v-for="(item, index) in roomStore.localFurnitureItems"
             :key="item.nanoid"
             :item="item"
+            :idx="index"
             :angle="angle"
             :coordinates="coordinates"
             :canvas-size="500"
@@ -113,7 +114,10 @@
                 class="list-item"
               >
                 <a-collapse v-model:activeKey="furnitureActiveKey" ghost>
-                  <a-collapse-panel :key="index + 1" :header="`${furniture.name} 属性`">
+                  <a-collapse-panel
+                    :key="index + 1"
+                    :header="`${index + 1}. ${furniture.name} 属性`"
+                  >
                     <div class="mapConfig">
                       <div class="mapConfig-item">
                         <label class="mapConfig-item-label">家具尺寸:</label>
@@ -142,6 +146,7 @@
                             v-model:value="furniture.rotate"
                             size="small"
                             :style="inputStyle"
+                            disabled
                           >
                             <a-select-option :value="0">0°</a-select-option>
                             <a-select-option :value="90">90°</a-select-option>
@@ -245,13 +250,13 @@
                 checked-children="显示"
                 un-checked-children="隐藏"
               />
-              <a-popconfirm
+              <!-- <a-popconfirm
                 v-if="roomStore.localSubRegions.length"
                 title="确定清空子区域吗?"
                 @confirm="clearSubregions"
               >
                 <a-button size="small" type="link">清空</a-button>
-              </a-popconfirm>
+              </a-popconfirm> -->
               <a-button size="small" type="link" @click="createSubregion">新建</a-button>
             </a-space>
           </div>
@@ -341,7 +346,7 @@
                         <div class="mapConfig-item-content"> 默认开启 </div>
                       </div>
 
-                      <div class="mapConfig-item">
+                      <!-- <div class="mapConfig-item">
                         <div class="mapConfig-item-label">删除区域:</div>
                         <div class="mapConfig-item-content">
                           <a-popconfirm
@@ -351,7 +356,7 @@
                             <DeleteOutlined />
                           </a-popconfirm>
                         </div>
-                      </div>
+                      </div> -->
                     </div>
                   </a-collapse-panel>
                 </a-collapse>
@@ -544,11 +549,11 @@ onUnmounted(() => {
 const regionActiveKey = ref<number[]>([])
 const furnitureActiveKey = ref<number[]>([])
 
-const deleteBlockArea = (id: string) => {
-  if (id) {
-    roomStore.localSubRegions = roomStore.localSubRegions.filter((item) => item.nanoid !== id)
-  }
-}
+// const deleteBlockArea = (id: string) => {
+//   if (id) {
+//     roomStore.localSubRegions = roomStore.localSubRegions.filter((item) => item.nanoid !== id)
+//   }
+// }
 
 const modeRadioChange = () => {
   regionActiveKey.value = []
@@ -559,9 +564,9 @@ const modeRadioChange = () => {
 //   console.log('同步坐标', localFurniture.value, localSubRegions.value)
 // }
 
-const clearSubregions = () => {
-  roomStore.localSubRegions = []
-}
+// const clearSubregions = () => {
+//   roomStore.localSubRegions = []
+// }
 
 const clearFurniture = () => {
   roomStore.localFurnitureItems = []

+ 2 - 2
src/components/baseRadarView/index.vue

@@ -21,7 +21,7 @@
 <script setup lang="ts">
 import { onMounted, ref, watch, type CSSProperties } from 'vue'
 import radarUrl from '@/assets/furnitures/radar.png'
-import { convert_region_r2c, rotateRect } from '@/utils/coordTransform'
+import { convert_region_r2c, rotateRect_cw } from '@/utils/coordTransform'
 import type { TargetPoint } from '@/types/radar'
 
 defineOptions({ name: 'BaseRadarView' })
@@ -250,7 +250,7 @@ const drawDetectionArea = (ctx: CanvasRenderingContext2D, options: DetectionArea
     radarPosition
   )
 
-  const rotatedRect = rotateRect(
+  const rotatedRect = rotateRect_cw(
     {
       left: canvasRect.left,
       top: canvasRect.top,

+ 58 - 1
src/utils/coordTransform.ts

@@ -142,7 +142,7 @@ interface Point {
  * @param angle - 顺时针旋转角度 0, 90, 180, 270
  * @returns 旋转后的矩形 { left, top, width, height }
  */
-export function rotateRect(src_rect: Rect, pRadar: Point, angle: number): Rect {
+export function rotateRect_cw(src_rect: Rect, pRadar: Point, angle: number): Rect {
   if (![0, 90, 180, 270].includes(angle)) angle = 0
 
   const { left, top, width, height } = src_rect
@@ -192,6 +192,63 @@ export function rotateRect(src_rect: Rect, pRadar: Point, angle: number): Rect {
   }
 }
 
+/**
+ * 逆时针旋转矩形(家具/检测区域)
+ * @param src_rect - 输入矩形 { left, top, width, height }
+ * @param pRadar - 雷达中心坐标 { x, y }
+ * @param angle - 逆时针旋转角度 0, 90, 180, 270
+ * @returns 旋转后的矩形 { left, top, width, height }
+ */
+export function rotateRect_ccw(src_rect: Rect, pRadar: Point, angle: number): Rect {
+  if (![0, 90, 180, 270].includes(angle)) angle = 0
+
+  const { left, top, width, height } = src_rect
+  const cx = left + width / 2
+  const cy = top + height / 2
+
+  const dx = cx - pRadar.x
+  const dy = cy - pRadar.y
+
+  let new_dx: number = 0
+  let new_dy: number = 0
+
+  switch (angle) {
+    case 0:
+      new_dx = dx
+      new_dy = dy
+      break
+    case 90:
+      new_dx = dy
+      new_dy = -dx
+      break
+    case 180:
+      new_dx = -dx
+      new_dy = -dy
+      break
+    case 270:
+      new_dx = -dy
+      new_dy = dx
+      break
+  }
+
+  const new_cx = pRadar.x + new_dx
+  const new_cy = pRadar.y + new_dy
+
+  let new_width = width
+  let new_height = height
+  if (angle === 90 || angle === 270) {
+    new_width = height
+    new_height = width
+  }
+
+  return {
+    left: new_cx - new_width / 2,
+    top: new_cy - new_height / 2,
+    width: new_width,
+    height: new_height,
+  }
+}
+
 interface Point {
   x: number
   y: number

+ 16 - 8
src/views/device/detail/components/alarmPlanModal/index.vue

@@ -205,7 +205,7 @@
           />
         </a-form-item>
 
-        <a-form-item label="生效时段">
+        <a-form-item v-if="![4, 5, 6, 8].includes(formState.eventType!)" label="生效时段">
           <div style="display: flex; align-items: center; gap: 8px">
             <a-time-range-picker
               v-model:value="formState.effectTimeFrame"
@@ -1049,19 +1049,27 @@ const submit = () => {
         },
       }
       console.log('🚀🚀🚀提交参数', params)
-      if (!formState.statisticsTime[0] || !formState.statisticsTime[1]) {
-        message.warn('统计时间不完整')
-        return
+      if ([4, 5, 6, 7, 8].includes(formState.eventType as number)) {
+        if (!formState.statisticsTime[0] || !formState.statisticsTime[1]) {
+          message.warn('统计时间不完整')
+          return
+        }
       }
       if (formState.effectTimeFrames.length === 0) {
         message.warn('请添加生效时段')
         return
       }
-      if (formState.effectTimeRanges.length === 0) {
-        message.warn('请选择生效方式')
-        return
+      if (![4, 5, 6, 8].includes(formState.eventType!)) {
+        if (formState.effectTimeRanges.length === 0) {
+          message.warn('请选择生效方式')
+          return
+        }
       }
-      if ([1, 2, 3, 9].includes(formState.eventType as number) && formState.region.length !== 4) {
+      if (
+        props.type === 'plan' &&
+        [1, 2, 3, 9].includes(formState.eventType as number) &&
+        formState.region.length !== 4
+      ) {
         message.warn('请选择检测区域')
         return
       }

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

@@ -46,7 +46,13 @@ import * as roomApi from '@/api/room'
 import { message } from 'ant-design-vue'
 import RadarEditor from '@/components/RadarEditor/index.vue'
 import { useRoomStore } from '@/stores/room'
-import { convert_furniture_r2c, rotateRect } from '@/utils/coordTransform'
+import {
+  convert_furniture_r2c,
+  convert_region_c2r,
+  convert_region_r2c,
+  rotateRect_ccw,
+  rotateRect_cw,
+} from '@/utils/coordTransform'
 
 defineOptions({
   name: 'deviceAreaConfig',
@@ -126,7 +132,7 @@ const fetchRoomLayout = async () => {
           y_radar: 250,
         }
       )
-      const rotatedRect = rotateRect(
+      const rotatedRect = rotateRect_cw(
         {
           left: itemConvert.left,
           top: itemConvert.top,
@@ -181,12 +187,38 @@ const saveAllConfig = () => {
     }
   })
 
+  console.log('保存的家具数据📚📚📚📚📚📚furnitureItems', furnitureItems)
+
   const subRegions = roomStore.localSubRegions.map((item) => {
+    const pRadar = {
+      x: 250,
+      y: 250,
+    }
+
+    const rectRotatedBefore = convert_region_r2c(
+      {
+        x_cm_start: Number(item.startXx),
+        x_cm_stop: Number(item.stopXx),
+        y_cm_start: Number(item.startYy),
+        y_cm_stop: Number(item.stopYy),
+      },
+      {
+        x_radar: 250,
+        y_radar: 250,
+      }
+    )
+
+    const rectRotatedBack = rotateRect_ccw(rectRotatedBefore, pRadar, props.angle)
+    const newRegion = convert_region_c2r(rectRotatedBack, {
+      x_radar: 250,
+      y_radar: 250,
+    })
+
     return {
-      startXx: item.startXx,
-      stopXx: item.stopXx,
-      startYy: item.startYy,
-      stopYy: item.stopYy,
+      startXx: newRegion.x_cm_start,
+      stopXx: newRegion.x_cm_stop,
+      startYy: newRegion.y_cm_start,
+      stopYy: newRegion.y_cm_stop,
       startZz: item.startZz,
       stopZz: item.stopZz,
       isLowSnr: Number(item.isBed),

+ 8 - 1
src/views/device/list/index.vue

@@ -102,7 +102,12 @@
             <a-button type="link" @click="detailHandler(record.devId, record.clientId)"
               >查看详情</a-button
             >
-            <a-button type="link" @click="unbindDeviceHandler(record)">抹掉配置</a-button>
+            <a-button
+              v-if="userStore.userInfo?.userType === 'admin'"
+              type="link"
+              @click="unbindDeviceHandler(record)"
+              >抹掉配置</a-button
+            >
           </template>
         </template>
       </a-table>
@@ -179,9 +184,11 @@ import * as tenantAPI from '@/api/tenant'
 import type { TenantItem } from '@/api/tenant/types'
 import * as adminAPI from '@/api/admin'
 import * as deviceApi from '@/api/device'
+import { useUserStore } from '@/stores/user'
 
 const router = useRouter()
 const route = useRoute()
+const userStore = useUserStore()
 
 interface SearchData {
   deviceId: string // 设备ID