liujia 2 дней назад
Родитель
Сommit
a5b9888
2 измененных файлов с 394 добавлено и 0 удалено
  1. 1 0
      components.d.ts
  2. 393 0
      src/components/baseRadarView/index.vue

+ 1 - 0
components.d.ts

@@ -67,6 +67,7 @@ declare module 'vue' {
     BaseChart: typeof import('./src/components/baseChart/index.vue')['default']
     BaseModal: typeof import('./src/components/baseModal/index.vue')['default']
     BasePagination: typeof import('./src/components/basePagination/index.vue')['default']
+    BaseRadarView: typeof import('./src/components/baseRadarView/index.vue')['default']
     BaseWeather: typeof import('./src/components/baseWeather/index.vue')['default']
     Copyright: typeof import('./src/components/Copyright/index.vue')['default']
     DetectionAreaView: typeof import('./src/components/DetectionAreaView/index.vue')['default']

+ 393 - 0
src/components/baseRadarView/index.vue

@@ -0,0 +1,393 @@
+<template>
+  <div
+    class="base-radar-view"
+    :style="{ width: canvasSize.width + 'px', height: canvasSize.height + 'px' }"
+  >
+    <canvas ref="canvasRef" :width="canvasSize.width" :height="canvasSize.height"></canvas>
+
+    <div class="overlay-layer">
+      <div
+        v-for="target in targets"
+        :key="target.id"
+        class="target-point"
+        :style="getTargetStyle(target)"
+      >
+        <span class="target-id">{{ target.id }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<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 type { TargetPoint } from '@/types/radar'
+
+defineOptions({ name: 'BaseRadarView' })
+
+interface CanvasSize {
+  width: number
+  height: number
+}
+
+interface Props {
+  coordinates: number[] // 检测区域坐标
+  angle?: number // 旋转角度,默认 0 度
+  targets?: TargetPoint[] // 目标点数组
+  canvasSize?: CanvasSize // 画布尺寸,默认 500x500
+  mode?: 'view' | 'edit'
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  canvasSize: () => ({
+    width: 500,
+    height: 500,
+  }),
+  angle: 0,
+  mode: 'view',
+})
+
+const canvasRef = ref<HTMLCanvasElement | null>(null)
+let ctx: CanvasRenderingContext2D | null = null
+
+/**
+ * 网格选项
+ * 用于绘制背景网格的选项
+ * @param width 画布宽度
+ * @param height 画布高度
+ * @param spacing 网格间距,默认 50
+ * @param color 网格线颜色,默认 '#d9d9d7'
+ * @param lineWidth 网格线宽度,默认 0.5
+ */
+interface GridOptions {
+  width: number
+  height: number
+  spacing?: number
+  color?: string
+  lineWidth?: number
+}
+/**
+ * 绘制背景网格
+ * 50px间距的灰色网格线,辅助定位
+ * @param ctx 画布上下文
+ * @param options 网格选项
+ */
+const drawGrid = (ctx: CanvasRenderingContext2D, options: GridOptions) => {
+  const { width, height, spacing = 50, color = '#d9d9d7', lineWidth = 0.5 } = options
+
+  if (!ctx) return
+
+  ctx.strokeStyle = color
+  ctx.lineWidth = lineWidth
+
+  // 水平网格线
+  for (let y = spacing; y < height; y += spacing) {
+    ctx.beginPath()
+    ctx.moveTo(0, y)
+    ctx.lineTo(width, y)
+    ctx.stroke()
+  }
+
+  // 垂直网格线
+  for (let x = spacing; x < width; x += spacing) {
+    ctx.beginPath()
+    ctx.moveTo(x, 0)
+    ctx.lineTo(x, height)
+    ctx.stroke()
+  }
+}
+
+/**
+ * 坐标轴选项
+ * 用于绘制坐标轴的选项
+ * @param width 画布宽度
+ * @param height 画布高度
+ * @param angle 旋转角度(单位:度),默认 0
+ * @param xColor X轴颜色,默认 'red'
+ * @param yColor Y轴颜色,默认 'green'
+ */
+interface CoordinateOptions {
+  width: number
+  height: number
+  angle?: number
+  xColor?: string
+  yColor?: string
+}
+/**
+ * 绘制坐标轴
+ * 虚线样式的X轴和Y轴,标识坐标系中心
+ * @param ctx 画布上下文
+ * @param options 坐标轴选项
+ */
+const drawCoordinateAxes = (ctx: CanvasRenderingContext2D, options: CoordinateOptions) => {
+  const { width, height, angle = 0, xColor = '#2c3e50', yColor = '#2c3e50' } = options
+
+  if (!ctx) return
+
+  const centerX = width / 2
+  const centerY = height / 2
+  // const arrowSize = 10
+
+  // 只接受 0, 90, 180, 270,其他归为 0
+  const validAngles: number[] = [0, 90, 180, 270]
+  const rotationDeg: number = validAngles.includes(angle) ? angle : 0
+  const rotationRad = (rotationDeg * Math.PI) / 180
+
+  ctx.save()
+  ctx.translate(centerX, centerY)
+  ctx.rotate(rotationRad)
+  ctx.translate(-centerX, -centerY)
+
+  ctx.lineWidth = 1
+  ctx.setLineDash([5, 5])
+
+  // 绘制X轴(水平线)
+  ctx.strokeStyle = xColor
+  ctx.beginPath()
+  ctx.moveTo(0, centerY)
+  ctx.lineTo(width, centerY)
+  ctx.stroke()
+
+  // X轴箭头(右端)
+  // ctx.beginPath()
+  // ctx.moveTo(width, centerY)
+  // ctx.lineTo(width - arrowSize, centerY - arrowSize / 2)
+  // ctx.lineTo(width - arrowSize, centerY + arrowSize / 2)
+  // ctx.closePath()
+  // ctx.fillStyle = xColor
+  // ctx.fill()
+
+  // 绘制Y轴(垂直线)
+  ctx.strokeStyle = yColor
+  ctx.beginPath()
+  ctx.moveTo(centerX, 0)
+  ctx.lineTo(centerX, height)
+  ctx.stroke()
+
+  // Y轴箭头(顶部)
+  // ctx.beginPath()
+  // ctx.moveTo(centerX, 0)
+  // ctx.lineTo(centerX - arrowSize / 2, arrowSize)
+  // ctx.lineTo(centerX + arrowSize / 2, arrowSize)
+  // ctx.closePath()
+  // ctx.fillStyle = yColor
+  // ctx.fill()
+
+  ctx.setLineDash([])
+  ctx.restore()
+}
+
+/**
+ * 雷达图图片选项
+ * 用于绘制雷达图图片的选项
+ * @param width 画布宽度
+ * @param height 画布高度
+ * @param radarImage 雷达图图片
+ * @param angle 旋转角度,默认 0
+ * @param size 图片尺寸(宽高),默认 20
+ * @param offset 图片偏移角度,默认 90 度 指向X轴
+ */
+interface RadarImageOptions {
+  width: number
+  height: number
+  radarImage: HTMLImageElement
+  angle?: number
+  size?: number
+  offset?: number
+}
+/**
+ * 绘制雷达图
+ * 图片以指定角度旋转,默认指向X轴
+ * @param ctx 画布上下文
+ * @param options 雷达图图片选项
+ */
+const drawRadarImage = (ctx: CanvasRenderingContext2D, options: RadarImageOptions) => {
+  const { width, height, angle = 0, size = 20, radarImage, offset = 90 } = options
+
+  const centerX = width / 2
+  const centerY = height / 2
+  const halfSize = size / 2
+  const angleRad = ((angle + offset) * Math.PI) / 180
+
+  ctx.save()
+  ctx.translate(centerX, centerY)
+  ctx.rotate(angleRad)
+  ctx.drawImage(radarImage, -halfSize, -halfSize, size, size)
+  ctx.restore()
+}
+
+/**
+ * 检测区域选项
+ * 用于绘制检测区域的选项
+ * @param area 检测区域坐标 [xStart, xEnd, yStart, yEnd]
+ * @param radarPosition 雷达图位置 { x_radar, y_radar }
+ * @param angle 旋转角度(单位:度),默认 0
+ */
+interface DetectionAreaOptions {
+  area: number[] // 检测区域坐标 [xStart, xEnd, yStart, yEnd]
+  radarPosition: {
+    x_radar: number
+    y_radar: number
+  }
+  angle?: number
+}
+/**
+ * 绘制检测区域
+ * @param ctx 画布上下文
+ * @param options 检测区域选项
+ */
+const drawDetectionArea = (ctx: CanvasRenderingContext2D, options: DetectionAreaOptions) => {
+  const { area, radarPosition, angle = 0 } = options
+  const [xStart, xEnd, yStart, yEnd] = area
+  const canvasRect = convert_region_r2c(
+    {
+      x_cm_start: xStart,
+      x_cm_stop: xEnd,
+      y_cm_start: yStart,
+      y_cm_stop: yEnd,
+    },
+    radarPosition
+  )
+
+  const rotatedRect = rotateRect(
+    {
+      left: canvasRect.left,
+      top: canvasRect.top,
+      width: canvasRect.width,
+      height: canvasRect.height,
+    },
+    { x: radarPosition.x_radar, y: radarPosition.y_radar },
+    angle
+  )
+
+  if (!ctx) return
+
+  // 绘制蓝色边框
+  ctx.strokeStyle = '#1677ff'
+  ctx.lineWidth = 1
+  ctx.strokeRect(rotatedRect.left, rotatedRect.top, rotatedRect.width, rotatedRect.height)
+
+  // 添加半透明蓝色填充
+  ctx.fillStyle = 'rgba(52, 152, 219, 0.3)'
+  ctx.fillRect(rotatedRect.left, rotatedRect.top, rotatedRect.width, rotatedRect.height)
+}
+
+/**
+ * 绘制画布
+ */
+const drawCanvans = () => {
+  /* 初始化画布 */
+  if (!canvasRef.value) return
+  ctx = canvasRef.value.getContext('2d')
+  if (!ctx) return
+
+  const CANVAS_W = props.canvasSize.width
+  const CANVAS_H = props.canvasSize.height
+
+  const CANVAS_SIZE = {
+    width: CANVAS_W,
+    height: CANVAS_H,
+  }
+
+  const radarPosition = {
+    x_radar: CANVAS_W / 2,
+    y_radar: CANVAS_H / 2,
+  }
+
+  /* 清除画布并设置背景色 */
+  ctx.clearRect(0, 0, CANVAS_W, CANVAS_H)
+  ctx.fillStyle = '#f2f2f0'
+  ctx.fillRect(0, 0, CANVAS_W, CANVAS_H)
+
+  /* 绘制网格和坐标轴 */
+  drawGrid(ctx, { width: CANVAS_W, height: CANVAS_H })
+  drawCoordinateAxes(ctx, { ...CANVAS_SIZE, angle: props.angle })
+
+  /* 绘制雷达图 */
+  const radarImage = new Image()
+  radarImage.src = radarUrl
+  radarImage.onload = () => {
+    if (!ctx) return
+    drawRadarImage(ctx, { ...CANVAS_SIZE, radarImage, angle: props.angle })
+  }
+
+  /* 绘制检测区域 */
+  drawDetectionArea(ctx, { area: props.coordinates, radarPosition, angle: props.angle })
+}
+
+// 监听角度变化,重新绘制画布
+watch(
+  () => [props.coordinates, props.angle],
+  () => {
+    drawCanvans()
+  },
+  {
+    immediate: true,
+  }
+)
+
+onMounted(() => {
+  drawCanvans()
+})
+
+/**
+ * 获取目标点样式
+ * @param target 目标点
+ * @returns 目标点样式
+ */
+const getTargetStyle = (target: TargetPoint) => {
+  const colorPalette = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple']
+
+  const colorIndex = target.id < colorPalette.length ? target.id : colorPalette.length - 1
+  const color = colorPalette[colorIndex]
+
+  return {
+    position: 'absolute',
+    width: '18px',
+    height: '18px',
+    background: color,
+    borderRadius: '50%',
+    transform: `translate3d(${target.displayX}px, ${target.displayY}px, 0) translate(-50%, -50%)`,
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center',
+    color: 'white',
+    fontSize: '12px',
+    fontWeight: '600',
+    pointerEvents: 'none',
+    zIndex: 10,
+    transition: 'transform 1s linear',
+    willChange: 'transform',
+    boxShadow: '0 0 8px rgba(0, 0, 0, 0.5)',
+    userSelect: 'none',
+  } as CSSProperties
+}
+</script>
+
+<style scoped lang="less">
+.base-radar-view {
+  position: relative;
+  display: flex;
+
+  canvas {
+    border: 2px solid #ddd;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+    pointer-events: none;
+  }
+
+  .overlay-layer {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    pointer-events: auto;
+
+    .target-point {
+      position: absolute;
+      pointer-events: none;
+    }
+  }
+}
+</style>