Forráskód Böngészése

feat: 新增雷达检测展示的组件;

liujia 1 hónapja
szülő
commit
a0af0e2
4 módosított fájl, 367 hozzáadás és 157 törlés
  1. 1 2
      components.d.ts
  2. 309 0
      src/components/RadarView/index.vue
  3. 28 0
      src/types/radar.ts
  4. 29 155
      src/views/device/detail/index.vue

+ 1 - 2
components.d.ts

@@ -46,7 +46,6 @@ declare module 'vue' {
     ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
     ASelect: typeof import('ant-design-vue/es')['Select']
     ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
-    ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
     ASpace: typeof import('ant-design-vue/es')['Space']
     ASpin: typeof import('ant-design-vue/es')['Spin']
     ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
@@ -55,7 +54,6 @@ declare module 'vue' {
     ATag: typeof import('ant-design-vue/es')['Tag']
     ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
     ATooltip: typeof import('ant-design-vue/es')['Tooltip']
-    ATree: typeof import('ant-design-vue/es')['Tree']
     AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
     BaseAreaViewer: typeof import('./src/components/baseAreaViewer/index.vue')['default']
     BaseCard: typeof import('./src/components/baseCard/index.vue')['default']
@@ -68,6 +66,7 @@ declare module 'vue' {
     FurnitureItem: typeof import('./src/components/furnitureItem/index.vue')['default']
     FurnitureList: typeof import('./src/components/furnitureList/index.vue')['default']
     RadarPointCloud: typeof import('./src/components/radarPointCloud/index.vue')['default']
+    RadarView: typeof import('./src/components/RadarView/index.vue')['default']
     RangePicker: typeof import('./src/components/rangePicker/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 309 - 0
src/components/RadarView/index.vue

@@ -0,0 +1,309 @@
+<template>
+  <div class="radar-view" :style="areaStyle">
+    <div class="furnitures">
+      <furniture-icon
+        icon="radar"
+        :width="radar.width"
+        :height="radar.length"
+        :style="{
+          left: `${radar.left}px`,
+          top: `${radar.top}px`,
+          position: 'absolute',
+          transform: `translate(-50%, -50%) rotate(${radar.rotate}deg)`,
+          cursor: 'default',
+        }"
+        :draggable="false"
+      />
+
+      <furniture-icon
+        v-for="item in filteredFurniture"
+        :key="item.nanoid"
+        :icon="item.type"
+        :width="item.width"
+        :height="item.length"
+        :style="{
+          left: `${item.left}px`,
+          top: `${item.top}px`,
+          position: 'absolute',
+          rotate: `${item.rotate}deg`,
+          cursor: 'default',
+        }"
+        :draggable="false"
+      />
+    </div>
+
+    <div class="targets">
+      <template v-if="targets && Object.keys(targets).length > 0">
+        <template v-for="t in targets" :key="t.id">
+          <div
+            class="target-dot"
+            :style="{
+              position: 'absolute',
+              width: '18px',
+              height: '18px',
+              background: t.id === 0 ? 'red' : t.id === 1 ? 'blue' : 'green',
+              borderRadius: '50%',
+              transform: `translate3d(${t.displayX}px, ${-t.displayY!}px, 0) translate(-50%, -50%)`,
+              zIndex: 10,
+              transition: 'transform 1s linear',
+              willChange: 'transform',
+            }"
+          >
+            <span
+              style="
+                color: #fff;
+                font-size: 12px;
+                font-weight: 600;
+                position: absolute;
+                left: 50%;
+                top: 50%;
+                transform: translate(-50%, -50%);
+                pointer-events: none;
+              "
+            >
+              {{ t.id + 1 }}
+            </span>
+          </div>
+        </template>
+      </template>
+    </div>
+
+    <div class="content"><slot /></div>
+
+    <div v-if="showInfo" class="info-box">
+      检测区域:{{ areaWidth }} × {{ areaHeight }} cm<br />
+      坐标范围:[{{ xxStart }}, {{ xxEnd }}, {{ yyStart }}, {{ yyEnd }}]<br />
+      正北夹角:{{ angle }}° {{ northArrow }}<br />
+      坐标参考:X轴 {{ xArrow }},Y轴 {{ yArrow }}
+    </div>
+
+    <div v-if="areaWidth > 50 && areaHeight > 50" class="info-toggle" @click="showInfo = !showInfo">
+      <QuestionCircleOutlined />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue'
+import type { FurnitureItem, TargetPoint } from '@/types/radar'
+import { QuestionCircleOutlined } from '@ant-design/icons-vue'
+
+defineOptions({ name: 'RadarView' })
+
+interface Props {
+  coordinates: [number, number, number, number] // 坐标边界:[xStart, xEnd, yStart, yEnd]
+  angle: number // 雷达旋转角度(单位:度)
+  furnitureItems?: FurnitureItem[] // 家具列表(包含雷达和其他家具)
+  targets?: TargetPoint[] // 目标点列表(已处理好的点位数据)
+}
+const props = defineProps<Props>()
+
+// 坐标边界拆解为响应式变量
+const xxStart = computed(() => props.coordinates[0])
+const xxEnd = computed(() => props.coordinates[1])
+const yyStart = computed(() => props.coordinates[2])
+const yyEnd = computed(() => props.coordinates[3])
+
+// 区域宽高计算(单位:cm)
+const areaWidth = computed(() => xxEnd.value - xxStart.value)
+const areaHeight = computed(() => yyEnd.value - yyStart.value)
+// 雷达角度(默认值为 0°,表示正北朝上)
+const angle = computed(() => props.angle ?? 0)
+
+/**
+ * 坐标转换函数:将雷达坐标系中的点 (x, y) 转换为 CSS 坐标系中的位置 (left, top)
+ * - 雷达坐标系以左下角为原点,单位为 cm
+ * - CSS 坐标系以左上角为原点,单位为 px
+ * - 支持角度旋转(0°, 90°, 180°, 270°)
+ * 使用场景:用于将家具或目标点定位到页面上
+ */
+function convertRadarToCss(x: number, y: number): { left: number; top: number } {
+  let rx = x,
+    ry = y
+  switch (angle.value) {
+    case 90:
+      ;[rx, ry] = [y, -x]
+      break
+    case 180:
+      ;[rx, ry] = [-x, -y]
+      break
+    case 270:
+      ;[rx, ry] = [-y, x]
+      break
+  }
+  return {
+    left: rx - xxStart.value,
+    top: yyEnd.value - ry,
+  }
+}
+
+// 雷达图标位置计算:固定在坐标原点 (0, 0),并转换为 CSS 坐标
+const radar = computed(() => {
+  const { left, top } = convertRadarToCss(0, 0)
+  return {
+    name: '雷达',
+    type: 'radar',
+    width: 20,
+    length: 20,
+    rotate: 0,
+    left,
+    top,
+    x: 0,
+    y: 0,
+  }
+})
+
+// 过滤家具列表,排除雷达图标,仅保留其他家具
+const filteredFurniture = computed(
+  () => props.furnitureItems?.filter((item) => item.type !== 'radar') ?? []
+)
+
+/**
+ * 雷达区域样式计算:
+ * - 设置区域尺寸
+ * - 添加网格背景(20px 间距)
+ * - 设置边框和阴影
+ * 使用场景:用于渲染雷达区域容器
+ */
+const areaStyle = computed(() => ({
+  width: `${areaWidth.value}px`,
+  height: `${areaHeight.value}px`,
+  position: 'relative' as const,
+  backgroundImage: `
+    linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
+    linear-gradient(to right, rgba(0, 0, 0, 0.1) 1px, transparent 1px)
+  `,
+  backgroundSize: '20px 20px',
+  border: '1px solid rgba(0, 0, 0, 0.8)',
+  boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
+  backgroundColor: '#fff',
+}))
+
+// 区域信息展示开关(点击问号按钮切换)
+const showInfo = ref(false)
+
+const northArrow = computed(() => {
+  switch (angle.value) {
+    case 0:
+      return '⬆'
+    case 90:
+      return '➡'
+    case 180:
+      return '⬇'
+    case 270:
+      return '⬅'
+    default:
+      return ''
+  }
+})
+
+const xArrow = computed(() => {
+  switch (angle.value) {
+    case 0:
+      return '➡'
+    case 90:
+      return '⬆'
+    case 180:
+      return '⬅'
+    case 270:
+      return '⬇'
+    default:
+      return ''
+  }
+})
+
+const yArrow = computed(() => {
+  switch (angle.value) {
+    case 0:
+      return '⬆'
+    case 90:
+      return '⬅'
+    case 180:
+      return '⬇'
+    case 270:
+      return '➡'
+    default:
+      return ''
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.radar-view {
+  flex-shrink: 0;
+
+  .furnitures {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    position: absolute;
+    top: 0;
+    z-index: 1;
+  }
+
+  .targets {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    z-index: 2;
+  }
+
+  .content {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    z-index: 3;
+  }
+
+  .info-toggle {
+    position: absolute;
+    right: 4px;
+    bottom: 4px;
+    z-index: 100;
+    border: 1px solid rgba(0, 0, 0, 0.2);
+    padding: 4px 6px;
+    font-size: 12px;
+    border-radius: 4px;
+    cursor: pointer;
+    color: #333;
+    transition: background 0.2s;
+    font-size: 14px;
+
+    &:hover {
+      background: rgba(0, 0, 0, 0.1);
+      border: 1px solid rgba(0, 0, 0, 0.2);
+    }
+  }
+
+  .info-box {
+    position: absolute;
+    right: 4px;
+    bottom: 36px;
+    font-size: 12px;
+    color: #333;
+    background: rgba(255, 255, 255, 0.85);
+    padding: 6px 10px;
+    border-radius: 6px;
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+    z-index: 99;
+    line-height: 1.5;
+    pointer-events: none;
+    min-width: 200px;
+  }
+}
+
+.target-dot {
+  span {
+    color: #fff;
+    font-size: 12px;
+    font-weight: 600;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    pointer-events: none;
+  }
+}
+</style>

+ 28 - 0
src/types/radar.ts

@@ -0,0 +1,28 @@
+/**
+ * 家具元素,用于在雷达区域中展示和编辑
+ */
+export interface FurnitureItem {
+  name: string // 家具名称(如:床、桌子)
+  type: string // 家具类型标识(如:'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)
+}
+
+/**
+ * 雷达点位图元素,用于展示检测目标位置
+ */
+export interface TargetPoint {
+  id: number // 点位唯一标识(通常为目标编号)
+  x: number // 雷达坐标系下的 X 坐标(以原点为参考)
+  y: number // 雷达坐标系下的 Y 坐标(以原点为参考)
+  displayX?: number // 可选:CSS 坐标系下的 X 坐标(如果已转换)
+  displayY?: number // 可选:CSS 坐标系下的 Y 坐标(如果已转换)
+  color?: string // 可选:点位颜色(如 'red'、'blue'、'green')
+  label?: string // 可选:点位标签(如 '1号目标')
+}

+ 29 - 155
src/views/device/detail/index.vue

@@ -1,12 +1,7 @@
 <template>
   <a-spin :spinning="spinning">
     <div class="deviceDetail">
-      <info-card
-        title="实时点位"
-        :class="[
-          furnitureItems && furnitureItems.some((item) => item.type === 'bed') ? 'pointCard' : '',
-        ]"
-      >
+      <info-card title="实时点位">
         <template #extra>
           <a-space>
             <a-button type="primary" size="small" @click="roomConfigHandler('area')">
@@ -22,70 +17,23 @@
           style="margin-bottom: 10px"
         />
         <div class="pointMap">
-          <div
-            class="radarArea"
-            :style="{
-              width: `${detailState?.length || 400}px`,
-              height: `${detailState?.width || 400}px`,
-            }"
-          >
-            <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',
-                rotate: `${item.rotate}deg`,
-                cursor: 'default',
-                pointerEvents: 'none',
-              }"
-              :draggable="false"
-            />
-          </div>
-
-          <template v-if="targets && Object.keys(targets).length > 0">
-            <template v-for="t in targets" :key="t.id">
-              <div
-                class="target-dot"
-                :style="{
-                  position: 'absolute',
-                  width: '18px',
-                  height: '18px',
-                  background: t.id === 0 ? 'red' : t.id === 1 ? 'blue' : 'green',
-                  borderRadius: '50%',
-                  transform: `translate3d(${t.displayX}px, ${-t.displayY}px, 0) translate(-50%, -50%)`,
-                  zIndex: 10,
-                  transition: 'transform 1s linear',
-                  willChange: 'transform',
-                }"
-              >
-                <span
-                  style="
-                    color: #fff;
-                    font-size: 12px;
-                    font-weight: 600;
-                    position: absolute;
-                    left: 50%;
-                    top: 50%;
-                    transform: translate(-50%, -50%);
-                    pointer-events: none;
-                  "
-                >
-                  {{ t.id + 1 }}
-                </span>
-              </div>
-            </template>
-          </template>
-
-          <div
-            v-if="furnitureItems && furnitureItems.some((item) => item.type === 'bed')"
-            class="breathLine"
-          >
-            <BreathLineChart :data="breathRpmList"></BreathLineChart>
+          <RadarView
+            :angle="detailState.northAngle"
+            :coordinates="[
+              detailState.xxStart,
+              detailState.xxEnd,
+              detailState.yyStart,
+              detailState.yyEnd,
+            ]"
+            :furnitureItems="furnitureItems"
+            :targets="Object.values(targets)"
+          ></RadarView>
+
+          <div class="breathLine">
+            <BreathLineChart
+              v-if="furnitureItems && furnitureItems.some((item) => item.type === 'bed')"
+              :data="breathRpmList"
+            ></BreathLineChart>
           </div>
         </div>
       </info-card>
@@ -94,64 +42,17 @@
         <div class="fullView">
           <div class="pointTitle">实时点位图</div>
           <div class="pointMap">
-            <div
-              class="radarArea"
-              :style="{
-                width: `${detailState?.length || 400}px`,
-                height: `${detailState?.width || 400}px`,
-              }"
-            >
-              <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',
-                  rotate: `${item.rotate}deg`,
-                  cursor: 'default',
-                  pointerEvents: 'none',
-                }"
-                :draggable="false"
-              />
-            </div>
-
-            <template v-if="targets && Object.keys(targets).length > 0">
-              <template v-for="t in targets" :key="t.id">
-                <div
-                  class="target-dot"
-                  :style="{
-                    position: 'absolute',
-                    width: '18px',
-                    height: '18px',
-                    background: t.id === 0 ? 'red' : t.id === 1 ? 'blue' : 'green',
-                    borderRadius: '50%',
-                    transform: `translate3d(${t.displayX}px, ${-t.displayY}px, 0) translate(-50%, -50%)`,
-                    zIndex: 10,
-                    transition: 'transform 1s linear',
-                    willChange: 'transform',
-                  }"
-                >
-                  <span
-                    style="
-                      color: #fff;
-                      font-size: 12px;
-                      font-weight: 600;
-                      position: absolute;
-                      left: 50%;
-                      top: 50%;
-                      transform: translate(-50%, -50%);
-                      pointer-events: none;
-                    "
-                  >
-                    {{ t.id + 1 }}
-                  </span>
-                </div>
-              </template>
-            </template>
+            <RadarView
+              :angle="detailState.northAngle"
+              :coordinates="[
+                detailState.xxStart,
+                detailState.xxEnd,
+                detailState.yyStart,
+                detailState.yyEnd,
+              ]"
+              :furnitureItems="furnitureItems"
+              :targets="Object.values(targets)"
+            ></RadarView>
           </div>
 
           <div
@@ -849,10 +750,6 @@ const swtichAlarmItem = async (id: number, swtich: boolean, item: AlarmPlanItem)
   gap: 16px;
 }
 
-.info.pointCard {
-  min-width: 800px;
-}
-
 .pointCloudMap {
   width: 770px;
   height: 100%;
@@ -868,26 +765,6 @@ const swtichAlarmItem = async (id: number, swtich: boolean, item: AlarmPlanItem)
   flex-direction: row;
 }
 
-.radarArea {
-  position: relative;
-  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;
-  border: 1px solid rgba(0, 0, 0, 0.8);
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-  overflow: hidden;
-  flex-shrink: 0;
-
-  .furniture-item {
-    position: absolute;
-    user-select: none;
-    cursor: move;
-    width: 30px;
-    height: 30px;
-  }
-}
-
 .extraIcon {
   font-size: 16px;
   font-weight: 600;
@@ -905,9 +782,6 @@ const swtichAlarmItem = async (id: number, swtich: boolean, item: AlarmPlanItem)
     min-height: auto;
     margin: auto;
   }
-  .breathLine {
-    margin-left: 0;
-  }
 
   .pointTitle {
     font-size: 16px;