| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- <template>
- <div class="radar-view" :style="areaStyle">
- <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(${adjustedAngle + 90}deg)`,
- cursor: 'default',
- zIndex: 5,
- }"
- :draggable="false"
- />
- <div class="content">
- <div class="furnitures" :style="{ overflow: !!slots.furnitures ? 'visible' : 'hidden' }">
- <slot name="furnitures">
- <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"
- />
- </slot>
- </div>
- <div class="subregion"><slot name="subregion"></slot></div>
- </div>
- <div v-if="targets && Object.keys(targets).length > 0" class="targets">
- <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 }}
- </span>
- </div>
- </template>
- </div>
- <!-- <div class="content"><slot /></div> -->
- <div v-if="showInfo" class="info-box">
- 检测区域:{{ areaWidth }} × {{ areaHeight }} cm<br />
- 坐标范围:[{{ xxStart }}, {{ xxEnd }}, {{ yyStart }}, {{ yyEnd }}]<br />
- 电源灯朝向: {{ northArrow }} ({{ angle }}°)<br />
- 坐标参考:<span style="color: red">X轴 {{ xArrow }}</span
- >,<span style="color: blue">Y轴 {{ yArrow }}</span>
- </div>
- <div v-if="areaWidth > 50 && areaHeight > 50" class="info-toggle">
- <a-switch
- v-model:checked="showAxis"
- size="small"
- checked-children="展示坐标"
- un-checked-children="隐藏坐标"
- />
- <QuestionCircleOutlined @click="showInfo = !showInfo" />
- </div>
- <div class="full-coordinate-axes" v-if="showAxis">
- <div class="axis-line x-axis" :style="getFullAxisStyle('x')"> </div>
- <div class="axis-line y-axis" :style="getFullAxisStyle('y')"> </div>
- </div>
- <div class="axis-markers">
- <div class="axis-dot x-dot" :style="getAxisDotStyle('x')"></div>
- <div class="axis-dot y-dot" :style="getAxisDotStyle('y')"></div>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { computed, ref, type CSSProperties, useSlots } from 'vue'
- import type { FurnitureItem, TargetPoint } from '@/types/radar'
- import { QuestionCircleOutlined } from '@ant-design/icons-vue'
- defineOptions({ name: 'RadarView' })
- const slots = useSlots()
- 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(() => Math.abs(xxEnd.value - xxStart.value))
- const areaHeight = computed(() => Math.abs(yyEnd.value - yyStart.value))
- // 雷达角度(默认值为 0°,表示正北朝上)
- const angle = computed(() => props.angle ?? 0)
- const adjustedAngle = computed(() => props.angle - 90)
- /**
- * 坐标转换函数:将雷达坐标系中的点 (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 (adjustedAngle.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 showAxis = ref(false)
- const northArrow = computed(() => {
- switch (angle.value) {
- case 0:
- return '北边'
- case 90:
- return '东边'
- case 180:
- return '南边'
- case 270:
- return '西边'
- default:
- return ''
- }
- })
- function getDirectionLabelWithIcon(axis: 'x' | 'y', angleDeg: number): string {
- const rad = (angleDeg * Math.PI) / 180
- const dx = axis === 'x' ? Math.cos(rad) : Math.sin(rad)
- const dy = axis === 'x' ? -Math.sin(rad) : Math.cos(rad)
- const deg = Math.atan2(dx, dy) * (180 / Math.PI)
- const normalized = (deg + 360) % 360
- if (normalized >= 45 && normalized < 135) return '东 ➡'
- if (normalized >= 135 && normalized < 225) return '南 ⬇'
- if (normalized >= 225 && normalized < 315) return '西 ⬅'
- return '北 ⬆'
- }
- const xArrow = computed(() => getDirectionLabelWithIcon('x', adjustedAngle.value))
- const yArrow = computed(() => getDirectionLabelWithIcon('y', adjustedAngle.value))
- function getFullAxisStyle(axis: 'x' | 'y') {
- const originX = radar.value.left
- const originY = radar.value.top
- const boxWidth = areaWidth.value
- const boxHeight = areaHeight.value
- const angleDeg = axis === 'x' ? angle.value : (angle.value + 90) % 360
- const { dx, dy } = getDirectionVector(angleDeg)
- // 最大延伸长度:对角线长度,确保覆盖整个盒子
- const maxLength = Math.sqrt(boxWidth ** 2 + boxHeight ** 2)
- const rotateDeg = Math.atan2(dy, dx) * (180 / Math.PI)
- return {
- position: 'absolute',
- left: `${originX}px`,
- top: `${originY}px`,
- width: `${maxLength}px`,
- height: '1px',
- backgroundColor: axis === 'x' ? 'red' : 'blue',
- transform: `translate(-50%, -50%) rotate(${rotateDeg}deg)`,
- transformOrigin: 'center center',
- zIndex: 5,
- pointerEvents: 'none',
- } as CSSProperties
- }
- function getDirectionVector(angleDeg: number): { dx: number; dy: number } {
- const rad = (angleDeg * Math.PI) / 180
- return {
- dx: Math.sin(rad), // 东向为正
- dy: -Math.cos(rad), // 北向为正
- }
- }
- function getAxisDotStyle(axis: 'x' | 'y') {
- const originX = radar.value.left
- const originY = radar.value.top
- const boxWidth = areaWidth.value
- const boxHeight = areaHeight.value
- const angleDeg = axis === 'x' ? angle.value : (angle.value - 90) % 360
- const { dx, dy } = getDirectionVector(angleDeg)
- // 计算终点比例,确保不超出盒子边界
- const scaleX = dx > 0 ? (boxWidth - originX) / dx : dx < 0 ? -originX / dx : Infinity
- const scaleY = dy > 0 ? (boxHeight - originY) / dy : dy < 0 ? -originY / dy : Infinity
- const scale = Math.min(dx !== 0 ? scaleX : Infinity, dy !== 0 ? scaleY : Infinity)
- const x = originX + dx * scale
- const y = originY + dy * scale
- return {
- position: 'absolute',
- left: `${x}px`,
- top: `${y}px`,
- width: '8px',
- height: '8px',
- borderRadius: '50%',
- backgroundColor: axis === 'x' ? 'red' : 'blue',
- transform: 'translate(-50%, -50%)',
- zIndex: 10,
- pointerEvents: 'none',
- } as CSSProperties
- }
- </script>
- <style lang="less" scoped>
- .radar-view {
- flex-shrink: 0;
- z-index: 0;
- // 家具层
- .furnitures {
- width: 100%;
- height: 100%;
- overflow: hidden;
- position: absolute;
- top: 0;
- }
- // 子区域层
- .subregion {
- width: 0%;
- height: 0%;
- position: absolute;
- top: 0;
- }
- // 点位层
- .targets {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- z-index: 2;
- }
- // 信息展示按钮
- .info-toggle {
- position: absolute;
- right: 4px;
- bottom: 4px;
- z-index: 3;
- border: 1px solid rgba(0, 0, 0, 0.2);
- padding: 4px 6px;
- font-size: 12px;
- border-radius: 4px;
- cursor: pointer;
- color: rgba(51, 51, 51, 0.5);
- transition: background 0.2s;
- font-size: 14px;
- opacity: 0.5;
- display: flex;
- align-items: center;
- gap: 4px;
- background: rgba(255, 255, 255, 0.8);
- &:hover {
- background: rgba(0, 0, 0, 0.1);
- color: #222222;
- }
- }
- // 信息展示框
- .info-box {
- position: absolute;
- right: 4px;
- bottom: 36px;
- right: -210px;
- bottom: -50px;
- 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;
- user-select: none;
- }
- // 全屏坐标轴线和点
- .full-coordinate-axes {
- position: absolute;
- width: 100%;
- height: 100%;
- z-index: 3;
- pointer-events: none;
- overflow: hidden;
- .axis-line {
- position: absolute;
- height: 1px;
- background-color: #000;
- }
- .x-axis {
- background-color: red;
- opacity: 0.6;
- }
- .y-axis {
- background-color: blue;
- opacity: 0.6;
- }
- .axis-arrow {
- position: absolute;
- font-weight: bold;
- font-size: 12px;
- line-height: 1;
- }
- }
- // 坐标轴标记点
- .axis-markers {
- position: absolute;
- width: 100%;
- height: 100%;
- z-index: 3;
- pointer-events: none;
- .axis-dot {
- position: absolute;
- }
- .x-dot {
- background-color: red;
- opacity: 0.6;
- }
- .y-dot {
- background-color: blue;
- opacity: 0.6;
- }
- }
- }
- .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>
|