|
@@ -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>
|