Explorar o código

[Feat 0000]预置位操作 摄像头绘制

bobo04052021@163.com hai 8 horas
pai
achega
2adabb137e

+ 499 - 0
src/components/vent/camera/RegionDrawOverlay.vue

@@ -0,0 +1,499 @@
+<template>
+  <div ref="layerEl" class="region-draw-layer" :class="{ 'is-drawing': isDrawingMode }" @mousedown="onMouseDown" @dblclick="onDblclick">
+    <!-- SVG:绘制线与多边形 -->
+    <svg class="region-draw-svg" viewBox="0 0 100 100" preserveAspectRatio="none">
+      <line
+        v-for="line in lineList"
+        :key="`${line.region}-${line.no}`"
+        :x1="line.points[0].x * 100"
+        :y1="line.points[0].y * 100"
+        :x2="line.points[1].x * 100"
+        :y2="line.points[1].y * 100"
+        :stroke="drawColor(line.region)"
+        stroke-width="2"
+        vector-effect="non-scaling-stroke"
+      />
+      <polygon
+        v-for="poly in polyList"
+        :key="`${poly.region}-${poly.no}`"
+        :points="svgPoints(poly.points)"
+        :stroke="drawColor(poly.region)"
+        :fill="drawColor(poly.region)"
+        fill-opacity="0.12"
+        stroke-width="2"
+        vector-effect="non-scaling-stroke"
+      />
+      <!-- 线预览 -->
+      <line
+        v-if="previewLine"
+        :x1="previewLine[0].x * 100"
+        :y1="previewLine[0].y * 100"
+        :x2="previewLine[1].x * 100"
+        :y2="previewLine[1].y * 100"
+        stroke="#01fefc"
+        stroke-width="2"
+        stroke-dasharray="4 3"
+        vector-effect="non-scaling-stroke"
+      />
+      <!-- 多边形预览 -->
+      <polyline
+        v-if="previewPolyPoints"
+        :points="previewPolyPoints"
+        fill="none"
+        stroke="#01fefc"
+        stroke-width="2"
+        stroke-dasharray="4 3"
+        vector-effect="non-scaling-stroke"
+      />
+    </svg>
+
+    <!-- 矩形(带坐标与温度标注) -->
+    <div
+      v-for="rect in rectList"
+      :key="`${rect.region}-${rect.no}`"
+      class="region-rect"
+      :class="{ 'is-active': rect.region === activeRegion && rect.no === regionState[rect.region].no }"
+      :style="rectStyle(rect)"
+    >
+      <span class="region-rect-coord region-rect-coord-tl">({{ rect.px }}, {{ rect.py }})</span>
+      <span class="region-rect-coord region-rect-coord-br">({{ rect.px + rect.pw }}, {{ rect.py + rect.ph }})</span>
+      <span v-if="isThermalItem(rect.region)" class="region-rect-temp">temp:{{ regionState[rect.region].temp }}</span>
+    </div>
+
+    <!-- 线端点坐标标注 -->
+    <template v-for="line in lineList" :key="`labels-${line.region}-${line.no}`">
+      <span class="region-point-label" :style="pointLabelStyle(line.points[0])">({{ line.points[0].px }}, {{ line.points[0].py }})</span>
+      <span class="region-point-label" :style="pointLabelStyle(line.points[1])">({{ line.points[1].px }}, {{ line.points[1].py }})</span>
+    </template>
+
+    <!-- 矩形预览 -->
+    <div v-if="drawingRect" class="region-rect region-rect-preview" :style="previewRectStyle"></div>
+
+    <!-- 绘制提示 -->
+    <div v-if="isDrawingMode" class="region-draw-tip">{{ shapeTip(activeShape) }}</div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, ref, toRefs, onBeforeUnmount } from 'vue';
+import { drawableShapes, shapeTip } from './regionDraw';
+import type { RegionSection, RegionState, DrawItem, RectData, LineData, PolyData, DrawPoint, PxPoint } from './regionDraw';
+
+const props = defineProps<{
+  /** 当前激活的区域 key */
+  activeRegion: string;
+  /** 当前激活的形状 */
+  activeShape: string;
+  /** 区域配置 */
+  sections: RegionSection[];
+  /** 各区域当前编辑值 */
+  regionState: Record<string, RegionState>;
+}>();
+
+const emit = defineEmits<{
+  (e: 'save', region: string, drawings: DrawItem[], latest?: DrawItem): void;
+}>();
+
+const { activeRegion, activeShape, sections, regionState } = toRefs(props);
+
+/** 绘制层根元素(铺满视频画面,作为坐标参考) */
+const layerEl = ref<HTMLElement | null>(null);
+
+/** 各区域已绘制的图形(`区域:形状` → 编号 → 图形) */
+const regionDrawings = reactive<Record<string, Record<number, DrawItem>>>({});
+
+/** 是否处于绘制模式 */
+const isDrawingMode = computed(() => drawableShapes.includes(activeShape.value) && !!activeRegion.value);
+
+/** 绘制模式 */
+const drawMode = ref<'' | 'rect' | 'line' | 'poly'>('');
+/** 绘制起点(像素) */
+const drawStartPx = ref<PxPoint | null>(null);
+/** 当前鼠标点(像素) */
+const currentPointPx = ref<PxPoint | null>(null);
+/** 多边形已添加的顶点(像素) */
+const polyPointsPx = ref<PxPoint[]>([]);
+/** 矩形拖拽中的临时矩形(像素) */
+const drawingRect = ref<{ x: number; y: number; w: number; h: number } | null>(null);
+
+/** 矩形图形列表 */
+const rectList = computed(() =>
+  Object.values(regionDrawings)
+    .flatMap((map) => Object.values(map))
+    .filter((it): it is RectData => it.type === 'rect')
+    .sort((a, b) => a.no - b.no)
+);
+
+/** 线图形列表 */
+const lineList = computed(() =>
+  Object.values(regionDrawings)
+    .flatMap((map) => Object.values(map))
+    .filter((it): it is LineData => it.type === 'line')
+    .sort((a, b) => a.no - b.no)
+);
+
+/** 多边形图形列表 */
+const polyList = computed(() =>
+  Object.values(regionDrawings)
+    .flatMap((map) => Object.values(map))
+    .filter((it): it is PolyData => it.type === 'poly' || it.type === 'polyEdge')
+    .sort((a, b) => a.no - b.no)
+);
+
+/** 鼠标事件 → 画面内像素坐标 */
+function layerPoint(e: MouseEvent): PxPoint {
+  const el = layerEl.value;
+  if (!el) return { x: 0, y: 0 };
+  const r = el.getBoundingClientRect();
+  return { x: e.clientX - r.left, y: e.clientY - r.top };
+}
+
+/** 像素点 → 绘制点(归一化 + 像素) */
+function toDrawPoint(p: PxPoint): DrawPoint {
+  const el = layerEl.value;
+  const box = el?.getBoundingClientRect();
+  const w = box?.width || 1;
+  const h = box?.height || 1;
+  return { x: p.x / w, y: p.y / h, px: Math.round(p.x), py: Math.round(p.y) };
+}
+
+type DrawItemInput = Omit<RectData, 'no' | 'region'> | Omit<LineData, 'no' | 'region'> | Omit<PolyData, 'no' | 'region'>;
+
+/** 区域 + 形状 的组合 key */
+function drawKey(region: string, shape: string) {
+  return `${region}:${shape}`;
+}
+
+/** 保存图形到当前激活区域(编号取面板当前值) */
+function saveDrawItem(item: DrawItemInput) {
+  const region = activeRegion.value;
+  if (!region) return;
+  const no = regionState.value[region]?.no ?? 1;
+  const saved = { ...item, no, region } as DrawItem;
+  const key = drawKey(region, activeShape.value);
+  if (!regionDrawings[key]) regionDrawings[key] = {};
+  regionDrawings[key][no] = saved;
+  emitSave(region, saved);
+}
+
+/** 通知父组件当前区域图形已变更(latest 为刚绘制的图形) */
+function emitSave(region: string, latest?: DrawItem) {
+  const prefix = `${region}:`;
+  const drawings = Object.keys(regionDrawings)
+    .filter((k) => k.startsWith(prefix))
+    .flatMap((k) => Object.values(regionDrawings[k]))
+    .sort((a, b) => a.no - b.no);
+  emit('save', region, drawings, latest);
+}
+
+/** 删除指定区域已绘制的所有图形 */
+function clearRegion(region: string) {
+  const prefix = `${region}:`;
+  Object.keys(regionDrawings).forEach((k) => {
+    if (k.startsWith(prefix)) delete regionDrawings[k];
+  });
+  emitSave(region);
+}
+
+/** 设置指定区域指定形状指定编号的图形(外部查询回显),item 为 null 时删除 */
+function setRegionItem(region: string, shape: string, no: number, item: DrawItem | null) {
+  const key = drawKey(region, shape);
+  if (!regionDrawings[key]) regionDrawings[key] = {};
+  if (item == null) {
+    delete regionDrawings[key][no];
+  } else {
+    regionDrawings[key][no] = { ...item, no, region };
+  }
+}
+
+defineExpose({ clearRegion, setRegionItem });
+
+function onMouseDown(e: MouseEvent) {
+  if (!isDrawingMode.value || e.button !== 0) return;
+  const p = layerPoint(e);
+  const shape = activeShape.value;
+
+  if (shape === 'rect') {
+    drawMode.value = 'rect';
+    drawStartPx.value = p;
+    currentPointPx.value = p;
+    drawingRect.value = { x: p.x, y: p.y, w: 0, h: 0 };
+  } else if (shape === 'line') {
+    drawMode.value = 'line';
+    drawStartPx.value = p;
+    currentPointPx.value = p;
+  } else if (shape === 'poly' || shape === 'polyEdge') {
+    drawMode.value = 'poly';
+    polyPointsPx.value.push(p);
+    currentPointPx.value = p;
+  }
+  window.addEventListener('mousemove', onMouseMove);
+  window.addEventListener('mouseup', onMouseUp);
+  e.preventDefault();
+}
+
+function onMouseMove(e: MouseEvent) {
+  const p = layerPoint(e);
+  currentPointPx.value = p;
+  if (drawMode.value === 'rect' && drawStartPx.value) {
+    const s = drawStartPx.value;
+    drawingRect.value = {
+      x: Math.min(s.x, p.x),
+      y: Math.min(s.y, p.y),
+      w: Math.abs(p.x - s.x),
+      h: Math.abs(p.y - s.y),
+    };
+  }
+}
+
+function onMouseUp(_e: MouseEvent) {
+  if (drawMode.value === 'rect') finishRect();
+  else if (drawMode.value === 'line') finishLine();
+  // 多边形不在此结束(双击结束)
+}
+
+function onDblclick(e: MouseEvent) {
+  if (drawMode.value !== 'poly') return;
+  e.preventDefault();
+  finishPoly();
+}
+
+function finishRect() {
+  const rect = drawingRect.value;
+  resetDrawState();
+  if (!rect || rect.w < 4 || rect.h < 4) return;
+  const tl = toDrawPoint({ x: rect.x, y: rect.y });
+  const br = toDrawPoint({ x: rect.x + rect.w, y: rect.y + rect.h });
+  saveDrawItem({
+    type: 'rect',
+    x: tl.x,
+    y: tl.y,
+    w: br.x - tl.x,
+    h: br.y - tl.y,
+    px: tl.px,
+    py: tl.py,
+    pw: br.px - tl.px,
+    ph: br.py - tl.py,
+  });
+}
+
+function finishLine() {
+  const s = drawStartPx.value;
+  const c = currentPointPx.value;
+  resetDrawState();
+  if (!s || !c) return;
+  if (Math.hypot(c.x - s.x, c.y - s.y) < 4) return;
+  saveDrawItem({
+    type: 'line',
+    points: [toDrawPoint(s), toDrawPoint(c)] as [DrawPoint, DrawPoint],
+  });
+}
+
+function finishPoly() {
+  let pts = polyPointsPx.value;
+  // 双击结束:移除最后一次 mousedown 重复添加的顶点
+  if (pts.length >= 2) {
+    const last = pts[pts.length - 1];
+    const prev = pts[pts.length - 2];
+    if (Math.hypot(last.x - prev.x, last.y - prev.y) < 3) {
+      pts = pts.slice(0, -1);
+    }
+  }
+  resetDrawState();
+  if (pts.length < 2) return;
+  saveDrawItem({
+    type: activeShape.value === 'polyEdge' ? 'polyEdge' : 'poly',
+    points: pts.map((p) => toDrawPoint(p)),
+  });
+}
+
+function resetDrawState() {
+  drawMode.value = '';
+  drawStartPx.value = null;
+  currentPointPx.value = null;
+  polyPointsPx.value = [];
+  drawingRect.value = null;
+  window.removeEventListener('mousemove', onMouseMove);
+  window.removeEventListener('mouseup', onMouseUp);
+}
+
+/** 图形所属区域颜色 */
+function drawColor(region: string) {
+  return regionState.value[region]?.color || '#3ed43e';
+}
+
+/** 是否为测温区域图形 */
+function isThermalItem(region: string) {
+  return sections.value.find((s) => s.key === region)?.thermal === true;
+}
+
+/** 矩形展示样式(归一化坐标 → 百分比) */
+function rectStyle(rect: RectData) {
+  return {
+    left: `${rect.x * 100}%`,
+    top: `${rect.y * 100}%`,
+    width: `${rect.w * 100}%`,
+    height: `${rect.h * 100}%`,
+    borderColor: drawColor(rect.region),
+  };
+}
+
+/** 拖拽中临时矩形的展示样式 */
+const previewRectStyle = computed(() => {
+  const r = drawingRect.value;
+  const el = layerEl.value;
+  if (!r || !el) return {};
+  const box = el.getBoundingClientRect();
+  return {
+    left: `${(r.x / box.width) * 100}%`,
+    top: `${(r.y / box.height) * 100}%`,
+    width: `${(r.w / box.width) * 100}%`,
+    height: `${(r.h / box.height) * 100}%`,
+    borderColor: drawColor(activeRegion.value),
+  };
+});
+
+/** 线预览(两端归一化点) */
+const previewLine = computed(() => {
+  if (drawMode.value !== 'line' || !drawStartPx.value || !currentPointPx.value) return null;
+  return [toDrawPoint(drawStartPx.value), toDrawPoint(currentPointPx.value)];
+});
+
+/** 多边形预览点串(SVG points,归一化×100) */
+const previewPolyPoints = computed(() => {
+  if (drawMode.value !== 'poly' || !polyPointsPx.value.length) return '';
+  const pts = [...polyPointsPx.value];
+  if (currentPointPx.value) pts.push(currentPointPx.value);
+  return pts
+    .map((p) => {
+      const d = toDrawPoint(p);
+      return `${d.x * 100},${d.y * 100}`;
+    })
+    .join(' ');
+});
+
+/** SVG points 工具:归一化点 → "x,y" 串(×100 映射到 viewBox 0 0 100 100) */
+function svgPoints(points: DrawPoint[]) {
+  return points.map((p) => `${p.x * 100},${p.y * 100}`).join(' ');
+}
+
+/** 线端点坐标标注样式 */
+function pointLabelStyle(p: DrawPoint) {
+  return { left: `${p.x * 100}%`, top: `${p.y * 100}%` };
+}
+
+onBeforeUnmount(() => {
+  resetDrawState();
+});
+</script>
+
+<style lang="less" scoped>
+.region-draw-layer {
+  position: absolute;
+  inset: 0;
+  z-index: 5;
+  pointer-events: none;
+
+  &.is-drawing {
+    pointer-events: auto;
+    cursor: crosshair;
+  }
+}
+
+.region-draw-svg {
+  position: absolute;
+  inset: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+}
+
+.region-point-label {
+  position: absolute;
+  z-index: 1;
+  padding: 1px 4px;
+  color: #0a1a2e;
+  font-size: 11px;
+  line-height: 14px;
+  white-space: nowrap;
+  transform: translate(-50%, -50%);
+  background: rgba(1, 254, 252, 0.85);
+  border-radius: 2px;
+  pointer-events: none;
+}
+
+.region-rect {
+  position: absolute;
+  box-sizing: border-box;
+  border: 2px dashed #3ed43e;
+  background: rgba(62, 212, 62, 0.12);
+  pointer-events: none;
+
+  &.is-active {
+    border-color: #ff6a1a;
+    background: rgba(255, 106, 26, 0.14);
+  }
+}
+
+.region-rect-coord {
+  position: absolute;
+  z-index: 1;
+  padding: 1px 4px;
+  color: #0a1a2e;
+  font-size: 11px;
+  line-height: 14px;
+  white-space: nowrap;
+  background: rgba(1, 254, 252, 0.85);
+  border-radius: 2px;
+  pointer-events: none;
+}
+
+.region-rect-coord-tl {
+  top: 2px;
+  left: 2px;
+}
+
+.region-rect-coord-br {
+  right: 2px;
+  bottom: 2px;
+}
+
+.region-rect-temp {
+  position: absolute;
+  top: 50%;
+  right: 2px;
+  z-index: 1;
+  padding: 1px 6px;
+  color: #ff7a3d;
+  font-size: 12px;
+  line-height: 16px;
+  font-weight: 600;
+  white-space: nowrap;
+  transform: translateY(-50%);
+  background: rgba(4, 18, 34, 0.82);
+  border: 1px solid rgba(255, 122, 61, 0.6);
+  border-radius: 2px;
+  pointer-events: none;
+}
+
+.region-rect-preview {
+  border-style: solid;
+  border-color: #01fefc;
+  background: rgba(1, 254, 252, 0.18);
+}
+
+.region-draw-tip {
+  position: absolute;
+  top: 12px;
+  left: 50%;
+  transform: translateX(-50%);
+  padding: 3px 12px;
+  color: #01fefc;
+  font-size: 12px;
+  white-space: nowrap;
+  background: rgba(4, 18, 34, 0.82);
+  border: 1px solid rgba(1, 254, 252, 0.5);
+  border-radius: 2px;
+  pointer-events: none;
+}
+</style>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1065 - 134
src/components/vent/camera/createPlayer.vue


+ 103 - 0
src/components/vent/camera/regionDraw.ts

@@ -0,0 +1,103 @@
+/** 区域形状选项 */
+export interface RegionShape {
+  key: string;
+  label: string;
+}
+
+/** 左侧操作面板区域配置 */
+export interface RegionSection {
+  key: string;
+  title: string;
+  /** 区域默认颜色 */
+  color: string;
+  /** 是否显示「修改」按钮 */
+  showModify?: boolean;
+  /** 是否显示 I/O 范围(闯入区域) */
+  showIoRange?: boolean;
+  /** 是否为测温区域(纠正系数 / 温度) */
+  thermal?: boolean;
+  shapes: RegionShape[];
+}
+
+/** 区域可编辑状态 */
+export interface RegionState {
+  /** 编号(对应预置位编号) */
+  no: number;
+  /** 当前选择的区域形状,空串表示未选择 */
+  shape: string;
+  /** 区域颜色 */
+  color: string;
+  /** I/O 起始值 */
+  ioStart: string;
+  /** I/O 结束值 */
+  ioEnd: string;
+  /** 测温纠正系数 */
+  k: number;
+  /** 测温温度值 */
+  temp: string;
+}
+
+/** 像素坐标点 */
+export interface PxPoint {
+  x: number;
+  y: number;
+}
+
+/** 绘制点(归一化 0-1 + 像素坐标) */
+export interface DrawPoint {
+  x: number;
+  y: number;
+  px: number;
+  py: number;
+}
+
+/** 矩形图形 */
+export interface RectData {
+  no: number;
+  region: string;
+  type: 'rect';
+  x: number;
+  y: number;
+  w: number;
+  h: number;
+  px: number;
+  py: number;
+  pw: number;
+  ph: number;
+}
+
+/** 线图形 */
+export interface LineData {
+  no: number;
+  region: string;
+  type: 'line';
+  points: [DrawPoint, DrawPoint];
+}
+
+/** 多边形图形 */
+export interface PolyData {
+  no: number;
+  region: string;
+  type: 'poly' | 'polyEdge';
+  points: DrawPoint[];
+}
+
+/** 绘制图形 */
+export type DrawItem = RectData | LineData | PolyData;
+
+/** 可在画面上直接绘制的形状 */
+export const drawableShapes = ['rect', 'line', 'poly', 'polyEdge'];
+
+/** 是否为可绘制形状 */
+export function isDrawableShape(shape: string) {
+  return drawableShapes.includes(shape);
+}
+
+/** 绘制提示文案 */
+export function shapeTip(shape: string) {
+  if (shape === 'rect') return '矩形:在右侧画面拖拽绘制,松手自动保存坐标';
+  if (shape === 'line') return '线:在右侧画面点击起点,拖到终点松手保存';
+  if (shape === 'poly') return '多边形:在右侧画面单击添加顶点,双击结束保存';
+  if (shape === 'polyEdge') return '多边形边界:在右侧画面单击添加顶点,双击结束保存';
+  return '';
+}

+ 26 - 25
src/hooks/system/useCameraPianation.ts

@@ -11,7 +11,7 @@ import { createPlayerVNode } from '../component/createPlayer';
 I18N.use(ZH);
 
 export function useCamera() {
-  const cameraList = (params) => defHttp.get({ url: '/safety/ventanalyCamera/list', params}, { joinParamsToUrl: true });
+  const cameraList = (params) => defHttp.get({ url: '/safety/ventanalyCamera/list', params }, { joinParamsToUrl: true });
   const cameraAddrList = (params) => defHttp.post({ url: '/monitor/camera/info', params });
   const cameraAddr = (params) => defHttp.get({ url: '/monitor/camera/queryByCameraCode', params });
 
@@ -20,13 +20,16 @@ export function useCamera() {
   const playerDoms = <(HTMLVideoElement | undefined | null)[]>[];
   const videoParentDomList: (HTMLElement | [string, { name: string; addr: string; cameraRate: number; devicekind: string }])[] = [];
   let Total = ref(0);
+  /** createPlayer 组件 VNode 与实例(用于外部触发「放大 + 区域选择」) */
+  let playerVNode: VNode | null = null;
+  let playerComponent: any = null;
 
   /** name 排序权重:可见光在前,热成像在后,其余居中 */
   function getCameraNameOrder(name?: string) {
-    const n = name || ''
-    if (n.includes('可见光')) return 0
-    if (n.includes('热成像')) return 2
-    return 1
+    const n = name || '';
+    if (n.includes('可见光')) return 0;
+    if (n.includes('热成像')) return 2;
+    return 1;
   }
 
   /** 按 name 过滤排序,保持同组内原有相对顺序 */
@@ -34,17 +37,17 @@ export function useCamera() {
     return list
       .map((item, index) => ({ item, index }))
       .sort((a, b) => {
-        const orderDiff = getCameraNameOrder(a.item?.name) - getCameraNameOrder(b.item?.name)
-        return orderDiff !== 0 ? orderDiff : a.index - b.index
+        const orderDiff = getCameraNameOrder(a.item?.name) - getCameraNameOrder(b.item?.name);
+        return orderDiff !== 0 ? orderDiff : a.index - b.index;
       })
-      .map(({ item }) => item)
+      .map(({ item }) => item);
   }
 
   async function getCamera(deviceid, parentPlayerDom, renderPlayer, pagination, cameraData?, devKind?, isCustom = false) {
     await removeCameraRef(parentPlayerDom, renderPlayer);
     let res;
     if (!devKind && !cameraData) {
-      res = await cameraList({ devIdList:deviceid, pageNo: pagination.current, pageSize: pagination.pageSize });
+      res = await cameraList({ devIdList: deviceid, pageNo: pagination.current, pageSize: pagination.pageSize });
     } else if (devKind && !cameraData) {
       res = await cameraList({ sysId: deviceid, devKind, pageNo: pagination.current, pageSize: pagination.pageSize });
     }
@@ -107,13 +110,7 @@ export function useCamera() {
     if (isCustom) {
       return sortedCameraAddrs;
     } else {
-      await deviceCameraInit1(
-        sortedCameraAddrs,
-        parentPlayerDom,
-        pagination?.autoPlayCount,
-        pagination?.autoPlayStart,
-        pagination?.onCameraSelect
-      );
+      await deviceCameraInit1(sortedCameraAddrs, parentPlayerDom, pagination?.autoPlayCount, pagination?.autoPlayStart, pagination?.onCameraSelect);
     }
   }
 
@@ -400,15 +397,17 @@ export function useCamera() {
     });
   }
 
-  function deviceCameraInit1(
-    cameraAddrs,
-    player,
-    autoPlayCount?: number,
-    autoPlayStart?: number,
-    onSelect?: (camera: any, index: number) => void
-  ) {
-    const vnode = createPlayerVNode(cameraAddrs, autoPlayCount, autoPlayStart, onSelect);
-    render(vnode, player.value);
+  function deviceCameraInit1(cameraAddrs, player, autoPlayCount?: number, autoPlayStart?: number, onSelect?: (camera: any, index: number) => void) {
+    playerVNode = createPlayerVNode(cameraAddrs, autoPlayCount, autoPlayStart, onSelect);
+    render(playerVNode, player.value);
+    nextTick(() => {
+      playerComponent = (playerVNode as any)?.component || null;
+    });
+  }
+
+  /** 外部按钮触发:放大当前选中摄像头并显示左侧区域选择 */
+  function openRegionPreview() {
+    playerComponent?.exposed?.openSelectedPreview?.(true);
   }
 
   function getPlayer(fileExtension, playerDomId, camerakind, cameraUrl, cameraRate, option = { width: '100%', height: '100%' }) {
@@ -538,12 +537,14 @@ export function useCamera() {
 
   return {
     getCamera,
+    cameraList,
     webRtcServer,
     playerDoms,
     deviceCameraInit,
     removeCamera,
     getPlayer,
     removeCameraRef,
+    openRegionPreview,
     Total,
   };
 }

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 325 - 325
src/views/vent/monitorManager/hsqHome/components/DeviceTree.vue


+ 256 - 269
src/views/vent/monitorManager/hsqHome/components/DeviceView.vue

@@ -1,47 +1,42 @@
 <template>
   <!-- 设备画面组件:右侧面板上方,包含操作按钮和 2×4 监控画面网格 -->
   <div class="device-view">
-   <div class="device-view-content">
-     <!-- 顶部操作按钮:最新热源 / 最新闯入 / 最新烟雾 / 关闭声光报警 -->
-     <div class="view-actions">
-      <button v-for="btn in actions" :key="btn.key" type="button"
-        :class="['action-btn', { active: activeAction === btn.key }]" @click="onAction(btn.key)">
-        {{ btn.label }}
-      </button>
-    </div>
+    <div class="device-view-content">
+      <!-- 顶部操作按钮:最新热源 / 最新闯入 / 最新烟雾 / 关闭声光报警 -->
+      <div class="view-actions">
+        <button
+          v-for="btn in actions"
+          :key="btn.key"
+          type="button"
+          :class="['action-btn', { active: activeAction === btn.key }]"
+          @click="onAction(btn.key)"
+        >
+          {{ btn.label }}
+        </button>
+      </div>
 
-    <!-- 2×4 监控画面网格 -->
-    <div class="view-grid">
-      <div class="view-cell">
-        <div class="media-wrap">
-          <div
-            v-if="renderPlayer"
-            ref="playerRef"
-            style="
-              display: flex;
-              width: 100%;
-              height: 100%;
-              overflow: hidden;
-              pointer-events: none;
-            "
-          ></div>
+      <!-- 2×4 监控画面网格 -->
+      <div class="view-grid">
+        <div class="view-cell">
+          <div class="media-wrap">
+            <div v-if="renderPlayer" ref="playerRef" style="display: flex; width: 100%; height: 100%; overflow: hidden; pointer-events: none"></div>
+          </div>
         </div>
       </div>
-    </div>
 
-    <div class="view-pagination">
-      <a-pagination
-        size="small"
-        :current="pagination.current"
-        :page-size="pagination.pageSize"
-        :total="total"
-        :show-size-changer="false"
-        :show-less-items="true"
-        :hide-on-single-page="false"
-        @change="onPageChange"
-      />
+      <div class="view-pagination">
+        <a-pagination
+          size="small"
+          :current="pagination.current"
+          :page-size="pagination.pageSize"
+          :total="total"
+          :show-size-changer="false"
+          :show-less-items="true"
+          :hide-on-single-page="false"
+          @change="onPageChange"
+        />
+      </div>
     </div>
-   </div>
 
     <!-- 最新热源 / 最新闯入 报警图片弹窗 -->
     <Teleport to="body">
@@ -53,26 +48,33 @@
               <button type="button" class="latest-alarm-icon-btn" title="放大" @click="toggleLatestAlarmExpand">
                 <svg viewBox="0 0 16 16" width="14" height="14">
                   <rect x="3" y="3" width="7" height="7" fill="none" stroke="currentColor" stroke-width="1.3" />
-                  <path d="M9 3 H13 V7 M13 3 L9.5 6.5 M7 9 L3 13 M3 9 V13 H7" fill="none" stroke="currentColor"
-                    stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" />
+                  <path
+                    d="M9 3 H13 V7 M13 3 L9.5 6.5 M7 9 L3 13 M3 9 V13 H7"
+                    fill="none"
+                    stroke="currentColor"
+                    stroke-width="1.3"
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                  />
                 </svg>
               </button>
               <button type="button" class="latest-alarm-icon-btn" title="关闭" @click="closeLatestAlarmPopup">
                 <svg viewBox="0 0 16 16" width="14" height="14">
                   <rect x="2.5" y="2.5" width="8" height="8" fill="none" stroke="currentColor" stroke-width="1.3" />
-                  <path d="M8 8 L13.5 13.5 M10.5 13.5 H13.5 V10.5" fill="none" stroke="currentColor" stroke-width="1.3"
-                    stroke-linecap="round" stroke-linejoin="round" />
+                  <path
+                    d="M8 8 L13.5 13.5 M10.5 13.5 H13.5 V10.5"
+                    fill="none"
+                    stroke="currentColor"
+                    stroke-width="1.3"
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                  />
                 </svg>
               </button>
             </div>
 
             <div class="latest-alarm-image-wrap">
-              <img
-                v-if="latestAlarmData.imageUrl"
-                class="latest-alarm-image"
-                :src="latestAlarmData.imageUrl"
-                :alt="latestAlarmTitle"
-              />
+              <img v-if="latestAlarmData.imageUrl" class="latest-alarm-image" :src="latestAlarmData.imageUrl" :alt="latestAlarmTitle" />
               <div v-else class="latest-alarm-image-empty">暂无告警图像</div>
               <div class="latest-alarm-overlay bottom-right">{{ latestAlarmData.deviceName || '--' }}</div>
             </div>
@@ -92,211 +94,197 @@
 </template>
 
 <script setup lang="ts">
-import { nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
-import dayjs from 'dayjs'
-import { useCamera } from '/@/hooks/system/useCameraPianation'
-import { useMessage } from '/@/hooks/web/useMessage'
-import { getFileAccessHttpUrl } from '/@/utils/common/compUtils'
-import { dscAlarmLogList } from '../hsqHome.api'
+import { nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
+import dayjs from 'dayjs';
+import { useCamera } from '/@/hooks/system/useCameraPianation';
+import { useMessage } from '/@/hooks/web/useMessage';
+import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
+import { dscAlarmLogList } from '../hsqHome.api';
 
 /** 操作按钮数据结构 */
 interface ActionItem {
-  key: string
-  label: string
+  key: string;
+  label: string;
 }
 
 /** 最新报警弹窗展示数据 */
 interface LatestAlarmPopupData {
-  imageUrl: string
-  alarmTime: string
-  deviceName: string
-  alarmType: string
+  imageUrl: string;
+  alarmTime: string;
+  deviceName: string;
+  alarmType: string;
 }
 
 /** 事件类型编码:热源 / 闯入 */
 const EVENT_TYPE_MAP: Record<string, { code: string; title: string; typeLabel: string }> = {
   heat: { code: '10001', title: '最新热源告警图', typeLabel: '热源报警' },
   intrusion: { code: '10002', title: '最新闯入告警图', typeLabel: '闯入报警' },
-}
+};
 
 let props = defineProps({
   deviceId: {
     type: String,
-    default: ''
+    default: '',
   },
 });
 
 /** 组件自定义事件 */
 const emit = defineEmits<{
-  (e: 'action', key: string): void
-  (e: 'select', channel: any): void
-}>()
+  (e: 'action', key: string): void;
+  (e: 'select', channel: any): void;
+}>();
 
-const { getCamera, removeCamera, Total: total } = useCamera()
-const playerRef = ref()
-const renderPlayer = ref(true)
+const { getCamera, removeCamera, cameraList, openRegionPreview, Total: total } = useCamera();
+const playerRef = ref();
+const renderPlayer = ref(true);
 /** 网格分页:2×4,每页 8 路画面;autoPlayStart/Count 控制自动播放区间;onCameraSelect 选中回调 */
 const pagination = reactive<{
-  current: number
-  pageSize: number
-  autoPlayStart?: number
-  autoPlayCount?: number
-  onCameraSelect?: (camera: any, index: number) => void
+  current: number;
+  pageSize: number;
+  autoPlayStart?: number;
+  autoPlayCount?: number;
+  onCameraSelect?: (camera: any, index: number) => void;
 }>({
   current: 1,
   pageSize: 8,
   onCameraSelect: handleCameraSelect,
-})
+});
 /** 顶部操作按钮列表 */
 const actions: ActionItem[] = [
   { key: 'heat', label: '最新热源' },
   { key: 'intrusion', label: '最新闯入' },
   { key: 'smoke', label: '最新烟雾' },
   { key: 'alarmOff', label: '关闭声光报警' },
-]
+];
 
 /** 当前激活的操作按钮 */
-const activeAction = ref('')
+const activeAction = ref('');
 /** 当前选中的通道 ID / 名称 */
-const selectedId = ref('')
+const selectedId = ref('');
 /** 当前选中的画面下标 */
-const selectedIndex = ref(-1)
+const selectedIndex = ref(-1);
 /** 取流播放模式:visible 前4播后4停;infrared 前4停后4播 */
-const streamPlayMode = ref<'visible' | 'infrared' | null>(null)
+const streamPlayMode = ref<'visible' | 'infrared' | null>(null);
 /** 各画面本地焦距缩放比例(不调接口) */
-const viewZoomMap = reactive<Record<number, number>>({})
+const viewZoomMap = reactive<Record<number, number>>({});
 /** 各画面本地焦点模糊程度(px,不调接口) */
-const viewFocusBlurMap = reactive<Record<number, number>>({})
+const viewFocusBlurMap = reactive<Record<number, number>>({});
 /** 各画面本地光圈亮度系数(1 为正常,不调接口) */
-const viewIrisBrightMap = reactive<Record<number, number>>({})
-const ZOOM_STEP = 0.15
-const ZOOM_MIN = 1
-const ZOOM_MAX = 3
-const FOCUS_BLUR_STEP = 0.4
-const FOCUS_BLUR_MIN = 0
-const FOCUS_BLUR_MAX = 4
-const IRIS_BRIGHT_STEP = 0.12
-const IRIS_BRIGHT_MIN = 0.4
-const IRIS_BRIGHT_MAX = 2
-
-const { createMessage } = useMessage()
+const viewIrisBrightMap = reactive<Record<number, number>>({});
+const ZOOM_STEP = 0.15;
+const ZOOM_MIN = 1;
+const ZOOM_MAX = 3;
+const FOCUS_BLUR_STEP = 0.4;
+const FOCUS_BLUR_MIN = 0;
+const FOCUS_BLUR_MAX = 4;
+const IRIS_BRIGHT_STEP = 0.12;
+const IRIS_BRIGHT_MIN = 0.4;
+const IRIS_BRIGHT_MAX = 2;
+
+const { createMessage } = useMessage();
 
 /** 最新报警弹窗 */
-const latestAlarmVisible = ref(false)
-const latestAlarmExpanded = ref(false)
-const latestAlarmTitle = ref('最新热源告警图')
-const latestAlarmLoading = ref(false)
+const latestAlarmVisible = ref(false);
+const latestAlarmExpanded = ref(false);
+const latestAlarmTitle = ref('最新热源告警图');
+const latestAlarmLoading = ref(false);
 const latestAlarmData = reactive<LatestAlarmPopupData>({
   imageUrl: '',
   alarmTime: '',
   deviceName: '',
   alarmType: '',
-})
+});
 
 /** 解析报警记录中的图片地址 */
 function resolveAlarmImageUrl(record: Record<string, any>) {
-  const raw =
-    record.picName ||
-    record.picName2 ||
-    ''
-  if (!raw) return ''
-  const text = String(raw)
-  if (/^https?:\/\//i.test(text) || text.startsWith('data:')) return text
+  const raw = record.picName || record.picName2 || '';
+  if (!raw) return '';
+  const text = String(raw);
+  if (/^https?:\/\//i.test(text) || text.startsWith('data:')) return text;
   // 兼容路径中带盘符前缀的情况(与报警详情一致)
-  const path = text.includes('h') ? text.substring(text.indexOf('h')) : text
-  return getFileAccessHttpUrl(path.replace(/\\/g, '/'))
+  const path = text.includes('h') ? text.substring(text.indexOf('h')) : text;
+  return getFileAccessHttpUrl(path.replace(/\\/g, '/'));
 }
 
 /** 从列表中过滤出指定类型的最新一条记录 */
 function pickLatestAlarmRecord(records: any[], eventCode: string) {
-  const matched = (records || []).filter((item) => String(item?.eventType) === eventCode)
-  if (!matched.length) return null
-  return matched
-    .slice()
-    .sort(
-      (a, b) =>
-        dayjs(b.createTime || b.alarmTime || 0).valueOf() -
-        dayjs(a.createTime || a.alarmTime || 0).valueOf(),
-    )[0]
+  const matched = (records || []).filter((item) => String(item?.eventType) === eventCode);
+  if (!matched.length) return null;
+  return matched.slice().sort((a, b) => dayjs(b.createTime || b.alarmTime || 0).valueOf() - dayjs(a.createTime || a.alarmTime || 0).valueOf())[0];
 }
 
 /** 点击最新热源 / 最新闯入:请求列表后展示对应最新报警图 */
 async function openLatestAlarmByType(key: 'heat' | 'intrusion') {
-  if (latestAlarmLoading.value) return
-  const meta = EVENT_TYPE_MAP[key]
-  if (!meta) return
+  if (latestAlarmLoading.value) return;
+  const meta = EVENT_TYPE_MAP[key];
+  if (!meta) return;
 
-  latestAlarmLoading.value = true
+  latestAlarmLoading.value = true;
   try {
-    const res = await dscAlarmLogList({ pageNo: 1, pageSize: 1000 })
-    const records = res?.records || []
-    console.log('报警图片列表', records)
-    const latest = pickLatestAlarmRecord(records, meta.code)
+    const res = await dscAlarmLogList({ pageNo: 1, pageSize: 1000 });
+    const records = res?.records || [];
+    console.log('报警图片列表', records);
+    const latest = pickLatestAlarmRecord(records, meta.code);
     if (!latest) {
-      createMessage.warning(`暂无${meta.typeLabel}数据`)
-      return
+      createMessage.warning(`暂无${meta.typeLabel}数据`);
+      return;
     }
 
-    latestAlarmTitle.value = meta.title
-    latestAlarmData.imageUrl = resolveAlarmImageUrl(latest)
-    latestAlarmData.alarmTime = String(latest.createTime || latest.alarmTime || '')
-    latestAlarmData.deviceName = String(latest.deviceName || latest.devId || latest.strname || '')
-    latestAlarmData.alarmType = meta.typeLabel
-    latestAlarmExpanded.value = false
-    latestAlarmVisible.value = true
+    latestAlarmTitle.value = meta.title;
+    latestAlarmData.imageUrl = resolveAlarmImageUrl(latest);
+    latestAlarmData.alarmTime = String(latest.createTime || latest.alarmTime || '');
+    latestAlarmData.deviceName = String(latest.deviceName || latest.devId || latest.strname || '');
+    latestAlarmData.alarmType = meta.typeLabel;
+    latestAlarmExpanded.value = false;
+    latestAlarmVisible.value = true;
   } catch (e) {
-    console.error('获取最新报警记录失败', e)
-    createMessage.error('获取最新报警记录失败')
+    console.error('获取最新报警记录失败', e);
+    createMessage.error('获取最新报警记录失败');
   } finally {
-    latestAlarmLoading.value = false
+    latestAlarmLoading.value = false;
   }
 }
 
 function toggleLatestAlarmExpand() {
-  latestAlarmExpanded.value = !latestAlarmExpanded.value
+  latestAlarmExpanded.value = !latestAlarmExpanded.value;
 }
 
 function closeLatestAlarmPopup() {
-  latestAlarmVisible.value = false
-  latestAlarmExpanded.value = false
+  latestAlarmVisible.value = false;
+  latestAlarmExpanded.value = false;
 }
 
 /** 点击操作按钮,触发 action 事件 */
 function onAction(key: string) {
-  activeAction.value = key
+  activeAction.value = key;
   if (key === 'heat' || key === 'intrusion') {
-    openLatestAlarmByType(key)
+    openLatestAlarmByType(key);
   }
-  emit('action', key)
+  emit('action', key);
 }
 
 /** 选中监控通道,触发 select 事件 */
 function handleCameraSelect(camera: any, index: number) {
-  selectedIndex.value = index
-  selectedId.value = camera?.id || camera?.name || String(index)
-  const deviceid =
-    camera?.deviceid ||
-    camera?.deviceId ||
-    camera?.deviceID ||
-    camera?.devId ||
-    ''
+  selectedIndex.value = index;
+  selectedId.value = camera?.id || camera?.name || String(index);
+  const deviceid = camera?.deviceid || camera?.deviceId || camera?.deviceID || camera?.devId || '';
   emit('select', {
     ...camera,
     index,
     deviceid: deviceid != null && deviceid !== '' ? String(deviceid) : '',
     ip: camera?.ip || camera?.strip || '',
-  })
-  applyViewLensStyles()
+  });
+  applyViewLensStyles();
 }
 
 /** 对当前选中画面做本地焦距缩放(不调接口) */
 function zoomSelectedView(direction: 1 | -1) {
-  if (selectedIndex.value < 0) return
-  const index = selectedIndex.value
-  const current = viewZoomMap[index] ?? 1
-  const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Number((current + direction * ZOOM_STEP).toFixed(2))))
-  viewZoomMap[index] = next
-  applyViewLensStyles()
+  if (selectedIndex.value < 0) return;
+  const index = selectedIndex.value;
+  const current = viewZoomMap[index] ?? 1;
+  const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Number((current + direction * ZOOM_STEP).toFixed(2))));
+  viewZoomMap[index] = next;
+  applyViewLensStyles();
 }
 
 /**
@@ -304,15 +292,12 @@ function zoomSelectedView(direction: 1 | -1) {
  * direction > 0 焦点后调(更模糊);direction < 0 焦点前调(更清晰)
  */
 function focusSelectedView(direction: 1 | -1) {
-  if (selectedIndex.value < 0) return
-  const index = selectedIndex.value
-  const current = viewFocusBlurMap[index] ?? 0
-  const next = Math.min(
-    FOCUS_BLUR_MAX,
-    Math.max(FOCUS_BLUR_MIN, Number((current + direction * FOCUS_BLUR_STEP).toFixed(2))),
-  )
-  viewFocusBlurMap[index] = next
-  applyViewLensStyles()
+  if (selectedIndex.value < 0) return;
+  const index = selectedIndex.value;
+  const current = viewFocusBlurMap[index] ?? 0;
+  const next = Math.min(FOCUS_BLUR_MAX, Math.max(FOCUS_BLUR_MIN, Number((current + direction * FOCUS_BLUR_STEP).toFixed(2))));
+  viewFocusBlurMap[index] = next;
+  applyViewLensStyles();
 }
 
 /**
@@ -320,143 +305,159 @@ function focusSelectedView(direction: 1 | -1) {
  * direction > 0 光圈扩大(更亮);direction < 0 光圈缩小(更暗)
  */
 function irisSelectedView(direction: 1 | -1) {
-  if (selectedIndex.value < 0) return
-  const index = selectedIndex.value
-  const current = viewIrisBrightMap[index] ?? 1
-  const next = Math.min(
-    IRIS_BRIGHT_MAX,
-    Math.max(IRIS_BRIGHT_MIN, Number((current + direction * IRIS_BRIGHT_STEP).toFixed(2))),
-  )
-  viewIrisBrightMap[index] = next
-  applyViewLensStyles()
+  if (selectedIndex.value < 0) return;
+  const index = selectedIndex.value;
+  const current = viewIrisBrightMap[index] ?? 1;
+  const next = Math.min(IRIS_BRIGHT_MAX, Math.max(IRIS_BRIGHT_MIN, Number((current + direction * IRIS_BRIGHT_STEP).toFixed(2))));
+  viewIrisBrightMap[index] = next;
+  applyViewLensStyles();
 }
 
 /** 获取画面实际绘制节点(video/canvas),避免操作外层容器 */
 function getPaintNode(item: HTMLElement) {
-  const media = item.querySelector('video, canvas') as HTMLElement | null
-    || (item.querySelector('.liveVideo') as HTMLElement | null)
-  if (!media || media === item) return null
-  return (media.matches('video, canvas')
-    ? media
-    : (media.querySelector('video, canvas') as HTMLElement | null) || media)
+  const media = (item.querySelector('video, canvas') as HTMLElement | null) || (item.querySelector('.liveVideo') as HTMLElement | null);
+  if (!media || media === item) return null;
+  return media.matches('video, canvas') ? media : (media.querySelector('video, canvas') as HTMLElement | null) || media;
 }
 
 /** 把焦距缩放 + 焦点模糊 + 光圈亮度应用到对应画面内容(不改变外层容器尺寸) */
 function applyViewLensStyles() {
-  const root = playerRef.value as HTMLElement | undefined
-  if (!root) return
-  const items = Array.from(root.querySelectorAll('.live-video-item')) as HTMLElement[]
+  const root = playerRef.value as HTMLElement | undefined;
+  if (!root) return;
+  const items = Array.from(root.querySelectorAll('.live-video-item')) as HTMLElement[];
   items.forEach((item, index) => {
-    item.style.overflow = 'hidden'
-    item.style.transform = ''
+    item.style.overflow = 'hidden';
+    item.style.transform = '';
 
-    const paintNode = getPaintNode(item)
-    if (!paintNode) return
+    const paintNode = getPaintNode(item);
+    if (!paintNode) return;
 
-    const scale = viewZoomMap[index] ?? 1
-    const blur = viewFocusBlurMap[index] ?? 0
-    const bright = viewIrisBrightMap[index] ?? 1
-    const filters: string[] = []
-    if (bright !== 1) filters.push(`brightness(${bright})`)
-    if (blur > 0) filters.push(`blur(${blur}px)`)
+    const scale = viewZoomMap[index] ?? 1;
+    const blur = viewFocusBlurMap[index] ?? 0;
+    const bright = viewIrisBrightMap[index] ?? 1;
+    const filters: string[] = [];
+    if (bright !== 1) filters.push(`brightness(${bright})`);
+    if (blur > 0) filters.push(`blur(${blur}px)`);
 
-    paintNode.style.transformOrigin = 'center center'
-    paintNode.style.transition = 'transform 0.15s ease, filter 0.15s ease'
-    paintNode.style.transform = scale === 1 ? 'none' : `scale(${scale})`
-    paintNode.style.filter = filters.length ? filters.join(' ') : 'none'
-  })
+    paintNode.style.transformOrigin = 'center center';
+    paintNode.style.transition = 'transform 0.15s ease, filter 0.15s ease';
+    paintNode.style.transform = scale === 1 ? 'none' : `scale(${scale})`;
+    paintNode.style.filter = filters.length ? filters.join(' ') : 'none';
+  });
 }
 
 /** 同步分页上的自动播放区间 */
 function syncPaginationPlayRange() {
   if (streamPlayMode.value === 'visible') {
-    pagination.autoPlayStart = 0
-    pagination.autoPlayCount = 4
+    pagination.autoPlayStart = 0;
+    pagination.autoPlayCount = 4;
   } else if (streamPlayMode.value === 'infrared') {
-    pagination.autoPlayStart = 4
-    pagination.autoPlayCount = 4
+    pagination.autoPlayStart = 4;
+    pagination.autoPlayCount = 4;
   } else {
-    pagination.autoPlayStart = undefined
-    pagination.autoPlayCount = undefined
+    pagination.autoPlayStart = undefined;
+    pagination.autoPlayCount = undefined;
   }
 }
 
 /** 按当前取流模式校正播放/停止状态 */
 function applyStreamPlayState() {
-  if (!streamPlayMode.value) return
-  const root = playerRef.value as HTMLElement | undefined
-  if (!root) return
-  const videos = Array.from(root.querySelectorAll('video')) as HTMLVideoElement[]
+  if (!streamPlayMode.value) return;
+  const root = playerRef.value as HTMLElement | undefined;
+  if (!root) return;
+  const videos = Array.from(root.querySelectorAll('video')) as HTMLVideoElement[];
   videos.forEach((video, index) => {
-    const shouldPlay = streamPlayMode.value === 'visible' ? index < 4 : index >= 4
+    const shouldPlay = streamPlayMode.value === 'visible' ? index < 4 : index >= 4;
     if (shouldPlay) {
-      video.play?.().catch(() => {})
+      video.play?.().catch(() => {});
     } else {
-      video.pause?.()
+      video.pause?.();
     }
-  })
+  });
 }
 
 /** 手动勾选可见光取流:前4播放,后4停止 */
 async function enableVisibleLightPlayMode() {
-  streamPlayMode.value = 'visible'
-  syncPaginationPlayRange()
-  await nextTick()
-  applyStreamPlayState()
+  streamPlayMode.value = 'visible';
+  syncPaginationPlayRange();
+  await nextTick();
+  applyStreamPlayState();
 }
 
 /** 手动勾选红外取流:前4停止,后4播放 */
 async function enableInfraredPlayMode() {
-  streamPlayMode.value = 'infrared'
-  syncPaginationPlayRange()
-  await nextTick()
-  applyStreamPlayState()
+  streamPlayMode.value = 'infrared';
+  syncPaginationPlayRange();
+  await nextTick();
+  applyStreamPlayState();
+}
+
+/**
+ * 勾选设备 ID 预检:能查到摄像头才用它过滤
+ * 后端 devIdList 过滤查不到数据时退回不带过滤,保证画面能展示
+ */
+async function resolveUsableDevIdList(raw: string) {
+  const ids = String(raw ?? '')
+    .split(',')
+    .map((id) => id.trim())
+    .filter((id) => id !== '' && id !== 'undefined' && id !== 'null')
+    .join(',');
+  if (!ids) return '';
+  try {
+    const res = await cameraList({ devIdList: ids, pageNo: 1, pageSize: pagination.pageSize });
+    if (res?.records?.length) return ids;
+    console.warn('[DeviceView] 按勾选设备ID查询无结果,退回全部摄像头。devIdList =', ids);
+  } catch (e) {
+    console.warn('[DeviceView] 按勾选设备ID查询异常,退回全部摄像头', e);
+  }
+  return '';
 }
 
 /** 加载当前页监控画面 */
 async function loadCameraList() {
-  syncPaginationPlayRange()
-  await getCamera(props.deviceId, playerRef, renderPlayer, pagination)
+  syncPaginationPlayRange();
+  const devIdList = await resolveUsableDevIdList(props.deviceId);
+  await getCamera(devIdList, playerRef, renderPlayer, pagination);
   if (streamPlayMode.value) {
-    await nextTick()
+    await nextTick();
     // 等待播放器挂载完成后再校正播放状态
     setTimeout(() => {
-      applyStreamPlayState()
-      applyViewLensStyles()
-    }, 300)
+      applyStreamPlayState();
+      applyViewLensStyles();
+    }, 300);
   } else {
-    await nextTick()
-    setTimeout(() => applyViewLensStyles(), 300)
+    await nextTick();
+    setTimeout(() => applyViewLensStyles(), 300);
   }
 }
 
 /** 分页切换 */
 async function onPageChange(page: number) {
-  pagination.current = page
-  selectedIndex.value = -1
-  selectedId.value = ''
-  Object.keys(viewZoomMap).forEach((key) => delete viewZoomMap[Number(key)])
-  Object.keys(viewFocusBlurMap).forEach((key) => delete viewFocusBlurMap[Number(key)])
-  Object.keys(viewIrisBrightMap).forEach((key) => delete viewIrisBrightMap[Number(key)])
-  await loadCameraList()
+  pagination.current = page;
+  selectedIndex.value = -1;
+  selectedId.value = '';
+  Object.keys(viewZoomMap).forEach((key) => delete viewZoomMap[Number(key)]);
+  Object.keys(viewFocusBlurMap).forEach((key) => delete viewFocusBlurMap[Number(key)]);
+  Object.keys(viewIrisBrightMap).forEach((key) => delete viewIrisBrightMap[Number(key)]);
+  await loadCameraList();
 }
 
 onMounted(async () => {
-  await loadCameraList()
-})
+  await loadCameraList();
+});
 
 watch(
   () => props.deviceId,
   async () => {
-    pagination.current = 1
-    selectedIndex.value = -1
-    selectedId.value = ''
-    Object.keys(viewZoomMap).forEach((key) => delete viewZoomMap[Number(key)])
-    Object.keys(viewFocusBlurMap).forEach((key) => delete viewFocusBlurMap[Number(key)])
-    Object.keys(viewIrisBrightMap).forEach((key) => delete viewIrisBrightMap[Number(key)])
-    await loadCameraList()
+    pagination.current = 1;
+    selectedIndex.value = -1;
+    selectedId.value = '';
+    Object.keys(viewZoomMap).forEach((key) => delete viewZoomMap[Number(key)]);
+    Object.keys(viewFocusBlurMap).forEach((key) => delete viewFocusBlurMap[Number(key)]);
+    Object.keys(viewIrisBrightMap).forEach((key) => delete viewIrisBrightMap[Number(key)]);
+    await loadCameraList();
   }
-)
+);
 
 onBeforeUnmount(() => {
   removeCamera(playerRef);
@@ -469,7 +470,8 @@ defineExpose({
   zoomSelectedView,
   focusSelectedView,
   irisSelectedView,
-})
+  openRegionPreview,
+});
 </script>
 
 <style lang="less" scoped>
@@ -488,9 +490,9 @@ defineExpose({
   padding: 15px;
   box-sizing: border-box;
 }
-.device-view-content{
-  width:100%;
-  height: 100%; 
+.device-view-content {
+  width: 100%;
+  height: 100%;
   display: flex;
   flex-direction: column;
   overflow: hidden;
@@ -601,7 +603,6 @@ defineExpose({
   }
 }
 
-
 // 无画面占位提示
 .media-empty {
   height: 100%;
@@ -641,9 +642,7 @@ defineExpose({
     /* 选中态:青色描边高亮,与监控界面风格一致 */
     &.is-selected {
       border-color: #01fefc;
-      box-shadow:
-        0 0 10px rgba(1, 254, 252, 0.45),
-        inset 0 0 12px rgba(0, 183, 255, 0.12);
+      box-shadow: 0 0 10px rgba(1, 254, 252, 0.45), inset 0 0 12px rgba(0, 183, 255, 0.12);
       background-color: rgba(0, 90, 140, 0.18);
     }
 
@@ -680,8 +679,8 @@ defineExpose({
   }
 }
 ::-webkit-scrollbar {
-    display: none;
-  }
+  display: none;
+}
 
 /* 最新热源 / 最新闯入 弹窗(风格对齐报警自动弹窗) */
 .latest-alarm-mask {
@@ -708,20 +707,8 @@ defineExpose({
   padding: 18px 12px 0;
   background: linear-gradient(180deg, rgba(6, 28, 48, 0.98) 0%, rgba(4, 16, 32, 0.98) 100%);
   border: 1px solid rgba(0, 210, 255, 0.85);
-  box-shadow:
-    0 0 0 2px rgba(8, 40, 70, 0.95),
-    0 0 22px rgba(0, 200, 255, 0.35),
-    inset 0 0 0 1px rgba(0, 180, 255, 0.35);
-  clip-path: polygon(
-    10px 0,
-    calc(100% - 10px) 0,
-    100% 10px,
-    100% calc(100% - 10px),
-    calc(100% - 10px) 100%,
-    10px 100%,
-    0 calc(100% - 10px),
-    0 10px
-  );
+  box-shadow: 0 0 0 2px rgba(8, 40, 70, 0.95), 0 0 22px rgba(0, 200, 255, 0.35), inset 0 0 0 1px rgba(0, 180, 255, 0.35);
+  clip-path: polygon(10px 0, calc(100% - 10px) 0, 100% 10px, 100% calc(100% - 10px), calc(100% - 10px) 100%, 10px 100%, 0 calc(100% - 10px), 0 10px);
 }
 
 .latest-alarm-title-tab {

+ 66 - 63
src/views/vent/monitorManager/hsqHome/components/type.ts

@@ -1,138 +1,141 @@
 /** 设备节点 */
 export interface DeviceNode {
-  id: string
-  name: string
+  id: string;
+  name: string;
   /** 勾选:可见光取流 / 红外取流 */
-  checks: [boolean, boolean]
+  checks: [boolean, boolean];
   /** 在线状态:1/true 在线,0/false 离线 */
-  netStatus?: number | boolean
+  netStatus?: number | boolean;
   /** 设备 IP(SDK 抓拍等接口用) */
-  ip?: string
+  ip?: string;
   /** 经度 / 纬度(可选) */
-  longitude?: string | number
-  latitude?: string | number
+  longitude?: string | number;
+  latitude?: string | number;
 }
 
 /** 告警上报表单字段 */
 export interface AlarmFormState {
-  deviceId: string
-  deviceName: string
-  longitude: string
-  latitude: string
-  operator: string
-  remark: string
+  deviceId: string;
+  deviceName: string;
+  longitude: string;
+  latitude: string;
+  operator: string;
+  remark: string;
 }
 
 /** 告警上报字段配置 */
 export interface AlarmReportFieldItem {
-  key: keyof AlarmFormState
-  label: string
-  placeholder?: string
+  key: keyof AlarmFormState;
+  label: string;
+  placeholder?: string;
 }
 
 /** 设备分组 */
 export interface DeviceGroup {
-  id: string
-  title: string
-  children?: DeviceNode[]
+  id: string;
+  title: string;
+  children?: DeviceNode[];
 }
 
 /** 扫描角度配置 */
 export interface AngleState {
-  hMin: number
-  hMax: number
-  vMin: number
-  vMax: number
+  hMin: number;
+  hMax: number;
+  vMin: number;
+  vMax: number;
 }
 
 /** 角度字段配置 */
 export interface AngleField {
-  label: string
-  minKey: keyof AngleState
-  maxKey: keyof AngleState
+  label: string;
+  minKey: keyof AngleState;
+  maxKey: keyof AngleState;
 }
 
 /** 在线/离线统计 */
 export interface StatsCount {
-  online: number
-  offline: number
+  online: number;
+  offline: number;
 }
 
 /** 预置位工具栏项 */
 export interface PresetToolbarItem {
-  key: string
+  key: string;
   /** SvgIcon name,对应 assets/icons 下文件名 */
-  icon: string
-  title: string
+  icon: string;
+  title: string;
 }
 
 /** 预置位列表项 */
 export interface PresetItem {
-  id: string
-  name: string
+  /** 展示用编号(编号列) */
+  id: string;
+  /** 接口 preset(预置位编号):新增/删除接口以 i1 传递 */
+  preset?: number;
+  name: string;
 }
 
 /** 红外控制单项 */
 export interface IrControlItem {
-  key: string
-  icon: string
+  key: string;
+  icon: string;
   /** + 按钮提示 */
-  plusTip?: string
+  plusTip?: string;
   /** − 按钮提示 */
-  minusTip?: string
+  minusTip?: string;
 }
 
 /** 激光操作按钮 */
 export interface LaserActionItem {
-  key: string
-  label: string
+  key: string;
+  label: string;
 }
 
 /** 录像文件(来自 recordMP4File 返回) */
 export interface RecordFileItem {
-  fileName: string
+  fileName: string;
   /** 存储路径(不含文件名) */
-  filePath: string
+  filePath: string;
   /** 完整路径 = filePath + fileName */
-  fullPath: string
+  fullPath: string;
   /** 可播放地址(HTTP 映射后) */
-  playUrl: string
-  recordTime: string
+  playUrl: string;
+  recordTime: string;
 }
 
 /** 报警自动弹窗展示数据 */
 export interface AutoAlarmPopupData {
-  imageUrl: string
-  maxTemp: string
-  overlayTime: string
-  deviceLabel: string
-  alarmTime: string
-  alarmDevice: string
-  alarmType: string
-  hotspotValue: string
+  imageUrl: string;
+  maxTemp: string;
+  overlayTime: string;
+  deviceLabel: string;
+  alarmTime: string;
+  alarmDevice: string;
+  alarmType: string;
+  hotspotValue: string;
 }
 
 /** 历史筛选树子节点 */
 export interface HistoryFilterDeviceNode {
-  id: string
-  name: string
-  checked: boolean
+  id: string;
+  name: string;
+  checked: boolean;
 }
 
 /** 历史筛选树分组节点 */
 export interface HistoryFilterGroupNode {
-  id: string
-  title: string
-  checked: boolean
-  expanded: boolean
-  children: HistoryFilterDeviceNode[]
+  id: string;
+  title: string;
+  checked: boolean;
+  expanded: boolean;
+  children: HistoryFilterDeviceNode[];
 }
 
 /** 历史数据筛选条件 */
 export interface HistoryFilterPayload {
   /** 开始时间 YYYY-MM-DD HH:mm:ss */
-  startTime: string
+  startTime: string;
   /** 结束时间 YYYY-MM-DD HH:mm:ss */
-  endTime: string
-  deviceIds: string[]
+  endTime: string;
+  deviceIds: string[];
 }

+ 108 - 93
src/views/vent/monitorManager/hsqHome/hsqHome.api.ts

@@ -1,11 +1,11 @@
 import { defHttp } from '/@/utils/http/axios';
 
 enum Api {
-  userList = '/sys/user/list',//用户列表接口
-  LongList = '/sys/log/list',//日志列表接口
-  getUserLoginStats = '/sys/log/getUserLoginStats',//今日登录统计
-  deviceList = '/safety/ventanalyDeviceInfo/list',//设备列表
-  autoLogList = '/safety/managesysAutoLog/list',//报警监控接口
+  userList = '/sys/user/list', //用户列表接口
+  LongList = '/sys/log/list', //日志列表接口
+  getUserLoginStats = '/sys/log/getUserLoginStats', //今日登录统计
+  deviceList = '/safety/ventanalyDeviceInfo/list', //设备列表
+  autoLogList = '/safety/managesysAutoLog/list', //报警监控接口
   monitorDevice = '/ventanaly-device/monitor/device', //设备监控
   deviceSetLog = '/safety/ventanalyDevicesetLog/list', //设备操作记录
   getManagesysDeviceNum = '/ventanaly-device/safety/ventanalyManageSystem/getManagesysDeviceNum', //设备数量统计
@@ -13,82 +13,81 @@ enum Api {
   monitorSystem = '/monitor/system', //场景设备列表
   queryRoleAndUserNum = '/sys/role/queryRoleAndUserNum', //查询角色和用户数量
   getWarningDeviceList = '/safety/managesysAlarm/list', //获取配置预警设备数据
-  getAlarmLogList = '/monitor/groupCompany/getAlarmLogList',  // 设备预警历史查询
-  warningList = '/safety/managesysAlarmInfo/list',//预警条目list
-  dscAlarmLogList = '/safety/dscAlarmLog/list',//设备预警历史查询
-  listdays = '/safety/ventanalyMonitorData/listdays',//详情历史曲线
-  updateProcessState = '/safety/dscAlarmLog/updateProcessState',//更新处理状态
-  captureToFile = '/api/zc/device/captureToFile',//一键抓拍图片接口
-  recordMP4File='/api/zc/device/recordMP4File',//录制MP4文件接口
-  stopRecordMP4File='/api/zc/device/stopRecordMP4File',//停止录制MP4文件接口
-  getSysCfg = '/api/zc/device/getSysCfg',//获取系统配置参数
-  setSysCfg = '/api/zc/device/setSysCfg',//设置系统配置参数
-  alarmAutoCapture = '/api/zc/device/alarmAutoCapture',//报警自动抓拍接口
-  hsqComControl = '/safety/hsqSdk/hsqComControl',//双光谱摄像机控制接口
-  setRunModeCfg = '/api/zc/device/setRunModeCfg',//设置云台运行模式配置
-  setPesudoColor = '/api/zc/device/setPesudoColor',//设置伪彩色
-  getAlarmStat = '/safety/dscAlarmLog/getAlarmStat',//获取报警统计信息-数据曲线
+  getAlarmLogList = '/monitor/groupCompany/getAlarmLogList', // 设备预警历史查询
+  warningList = '/safety/managesysAlarmInfo/list', //预警条目list
+  dscAlarmLogList = '/safety/dscAlarmLog/list', //设备预警历史查询
+  listdays = '/safety/ventanalyMonitorData/listdays', //详情历史曲线
+  updateProcessState = '/safety/dscAlarmLog/updateProcessState', //更新处理状态
+  captureToFile = '/api/zc/device/captureToFile', //一键抓拍图片接口
+  recordMP4File = '/api/zc/device/recordMP4File', //录制MP4文件接口
+  stopRecordMP4File = '/api/zc/device/stopRecordMP4File', //停止录制MP4文件接口
+  setSysCfg = '/api/zc/device/setSysCfg', //设置系统配置参数
+  alarmAutoCapture = '/api/zc/device/alarmAutoCapture', //报警自动抓拍接口
+  hsqComControl = '/safety/hsqSdk/hsqComControl', //双光谱摄像机控制接口
+  setRunModeCfg = '/api/zc/device/setRunModeCfg', //设置云台运行模式配置
+  setPesudoColor = '/api/zc/device/setPesudoColor', //设置伪彩色
+  getAlarmStat = '/safety/dscAlarmLog/getAlarmStat', //获取报警统计信息-数据曲线
 }
 /**
-* 日志列表接口
-* @param params
-*/
+ * 日志列表接口
+ * @param params
+ */
 export const LongList = (params) => defHttp.get({ url: Api.LongList, params });
 /**
-* 用户列表接口
-* @param params
-*/
+ * 用户列表接口
+ * @param params
+ */
 export const userList = (params) => defHttp.get({ url: Api.userList, params });
 
 //设备列表
 export const deviceList = (params) => defHttp.get({ url: Api.deviceList, params });
 
 /**
-* 今日登录统计
-* @param params
-*/
+ * 今日登录统计
+ * @param params
+ */
 export const getUserLoginStats = (params) => defHttp.post({ url: Api.getUserLoginStats, params });
 
 /**
-* 报警监控接口
-* @param params
-*/
+ * 报警监控接口
+ * @param params
+ */
 export const autoLogList = (params) => defHttp.get({ url: Api.autoLogList, params });
 
 /**
-* 设备监控接口
-* @param params
-*/
+ * 设备监控接口
+ * @param params
+ */
 export const monitorDevice = (params) => defHttp.post({ url: Api.monitorDevice, params });
 
 /**
-* 设备操作记录接口
-* @param params
-*/
+ * 设备操作记录接口
+ * @param params
+ */
 export const deviceSetLog = (params) => defHttp.get({ url: Api.deviceSetLog, params });
 
 /**
-* 设备数量统计接口
-* @param params
-*/
+ * 设备数量统计接口
+ * @param params
+ */
 export const getManagesysDeviceNum = (params) => defHttp.get({ url: Api.getManagesysDeviceNum, params });
 
 /**
-* 获取场景信息接口
-* @param params
-*/
+ * 获取场景信息接口
+ * @param params
+ */
 export const managesysList = (params) => defHttp.get({ url: Api.managesysList, params });
 
 /**
-* 场景设备列表接口
-* @param params
-*/
+ * 场景设备列表接口
+ * @param params
+ */
 export const monitorSystem = (params) => defHttp.post({ url: Api.monitorSystem, params });
 
 /**
-* 查询角色和用户数量接口
-* @param params
-*/
+ * 查询角色和用户数量接口
+ * @param params
+ */
 export const queryRoleAndUserNum = (params) => defHttp.get({ url: Api.queryRoleAndUserNum, params });
 
 export const getWarningDeviceList = (params) => defHttp.get({ url: Api.getWarningDeviceList, params });
@@ -96,81 +95,97 @@ export const getAlarmLogList = (params) => defHttp.post({ url: Api.getAlarmLogLi
 export const warningList = (params) => defHttp.get({ url: Api.warningList, params });
 
 /**
-* 双光谱摄像机报警记录-分页列表查询
-* @param params
-*/
+ * 双光谱摄像机报警记录-分页列表查询
+ * @param params
+ */
 export const dscAlarmLogList = (params) => defHttp.get({ url: Api.dscAlarmLogList, params });
 
 /**
-* 详情历史曲线
-* @param params
-*/
+ * 详情历史曲线
+ * @param params
+ */
 export const listdays = (params) => defHttp.get({ url: Api.listdays, params }, { joinParamsToUrl: true });
 
 /**
-* 更新处理状态接口
-* @param params
-*/
+ * 更新处理状态接口
+ * @param params
+ */
 export const updateProcessState = (params) => defHttp.post({ url: Api.updateProcessState, params });
 
 /**
-* 一键抓拍图片接口
-* @param params
-*/
+ * 一键抓拍图片接口
+ * @param params
+ */
 export const captureToFile = (params) => defHttp.post({ url: Api.captureToFile, params });
 
 /**
-* 录制MP4文件接口
-* @param params
-*/
+ * 录制MP4文件接口
+ * @param params
+ */
 export const recordMP4File = (params) => defHttp.post({ url: Api.recordMP4File, params });
 
 /**
-* 停止录制MP4文件接口
-* @param params
-*/
+ * 停止录制MP4文件接口
+ * @param params
+ */
 export const stopRecordMP4File = (params) => defHttp.post({ url: Api.stopRecordMP4File, params });
 
 /**
-* 获取系统配置参数接口
-* @param params
-*/
-export const getSysCfg = (params) => defHttp.post({ url: Api.getSysCfg, params });
+ * 获取系统配置参数:paramcode = getSysCfg
+ * @param params { deviceid, paramcode: 'getSysCfg', noCheckPassword }
+ */
+export const getSysCfg = (params) => defHttp.post({ url: Api.hsqComControl, params });
 
 /**
-* 设置系统配置参数接口
-* @param params
-*/
+ * 设置系统配置参数接口
+ * @param params
+ */
 export const setSysCfg = (params) => defHttp.post({ url: Api.setSysCfg, params });
 
-
 /**
-* 报警自动抓拍接口
-* @param params
-*/
+ * 报警自动抓拍接口
+ * @param params
+ */
 export const alarmAutoCapture = (params) => defHttp.post({ url: Api.alarmAutoCapture, params });
 
-
 /**
-* 双光谱摄像机控制接口
-* @param params
-*/
+ * 双光谱摄像机控制接口
+ * @param params
+ */
 export const hsqComControl = (params) => defHttp.post({ url: Api.hsqComControl, params });
 
 /**
-* 设置云台运行模式配置接口
-* @param params
-*/
+ * 设置云台运行模式配置接口
+ * @param params
+ */
 export const setRunModeCfg = (params) => defHttp.post({ url: Api.setRunModeCfg, params });
 
 /**
-* 设置伪彩色接口
-* @param params
-*/
+ * 设置伪彩色接口
+ * @param params
+ */
 export const setPesudoColor = (params) => defHttp.post({ url: Api.setPesudoColor, params });
 
 /**
-* 获取报警统计信息-数据曲线接口
-* @param params
-*/
-export const getAlarmStat = (params) => defHttp.post({ url: Api.getAlarmStat, params });
+ * 获取报警统计信息-数据曲线接口
+ * @param params
+ */
+export const getAlarmStat = (params) => defHttp.post({ url: Api.getAlarmStat, params });
+
+/**
+ * 获取预置位列表:paramcode = getAllPreset
+ * @param params { deviceid, paramcode: 'getAllPreset', noCheckPassword }
+ */
+export const presetList = (params) => defHttp.post({ url: Api.hsqComControl, params });
+
+/**
+ * 新增预置位:paramcode = setPtzPreset
+ * @param params { deviceid, paramcode: 'setPtzPreset', noCheckPassword, i1: 预置位编号 }
+ */
+export const presetSet = (params) => defHttp.post({ url: Api.hsqComControl, params });
+
+/**
+ * 删除预置位:paramcode = delPtzPreset
+ * @param params { deviceid, paramcode: 'delPtzPreset', noCheckPassword }
+ */
+export const presetDelete = (params) => defHttp.post({ url: Api.hsqComControl, params });

+ 364 - 358
src/views/vent/monitorManager/hsqHome/hsqHome.data.ts

@@ -1,127 +1,133 @@
 import { getAssetURL } from '/@/utils/ui';
-import type {LaserActionItem, DeviceNode, DeviceGroup, AlarmFormState, AlarmReportFieldItem, AngleState, StatsCount,
-AngleField, PresetToolbarItem } from
-'./components/type'
+import type {
+  LaserActionItem,
+  DeviceNode,
+  DeviceGroup,
+  AlarmFormState,
+  AlarmReportFieldItem,
+  AngleState,
+  StatsCount,
+  AngleField,
+  PresetToolbarItem,
+} from './components/type';
 /** Tab 标签项数据结构 */
 interface TabItem {
-key: string
-label: string
+  key: string;
+  label: string;
 }
 /** 镜头控制项(变焦/聚焦/光圈)数据结构 */
 interface LensControl {
-key: string
-icon: string
-/** + 按钮提示 */
-plusTip: string
-/** − 按钮提示 */
-minusTip: string
+  key: string;
+  icon: string;
+  /** + 按钮提示 */
+  plusTip: string;
+  /** − 按钮提示 */
+  minusTip: string;
 }
 
-
 //设备状态总览
 export let option = [
-{
-label: '在线设备',
-value: 'onlineNum',
-},
-{
-label: '离线设备',
-value: 'offlineNum',
-},
-{
-label: '故障设备',
-value: 'faultNum',
-},
-{
-label: '报警设备',
-value: 'alarmNum',
-},
-]
+  {
+    label: '在线设备',
+    value: 'onlineNum',
+  },
+  {
+    label: '离线设备',
+    value: 'offlineNum',
+  },
+  {
+    label: '故障设备',
+    value: 'faultNum',
+  },
+  {
+    label: '报警设备',
+    value: 'alarmNum',
+  },
+];
 
 //实时温度曲线
 export const chartConfig: any = {
-type: 'line_area',
-grid: {
-top: 25,
-left: 25,
-bottom: 10,
-right: 25,
-},
-legend: { show: true },
-xAxis: [{ show: true }],
-yAxis: [{ show: true, name: '(℃)', position: 'left', nameTextStyle: { color: '#fff', fontSize: 12, lineHeight: 2, } }],
-series: [
-{
-label: '平均温度',
-readFrom: '',
-xprop: 'pos',
-yprop: 'value',
-color: ['rgba(52, 193, 248,.8)', 'rgba(52, 193, 248,.2)'],
-},
-],
+  type: 'line_area',
+  grid: {
+    top: 25,
+    left: 25,
+    bottom: 10,
+    right: 25,
+  },
+  legend: { show: true },
+  xAxis: [{ show: true }],
+  yAxis: [{ show: true, name: '(℃)', position: 'left', nameTextStyle: { color: '#fff', fontSize: 12, lineHeight: 2 } }],
+  series: [
+    {
+      label: '平均温度',
+      readFrom: '',
+      xprop: 'pos',
+      yprop: 'value',
+      color: ['rgba(52, 193, 248,.8)', 'rgba(52, 193, 248,.2)'],
+    },
+  ],
 };
 
 //设备状态表格
 export let deviceColumns: any[] = [
-{
-name: '设备名称',
-prop: 'strname',
-},
-{
-name: '在线状态',
-prop: 'netStatus',
-},
-{
-name: '温度(‌℃‌)',
-//prop: 'syswarnLevel',
-prop: 'avg'
-},
-]
+  {
+    name: '设备名称',
+    prop: 'strname',
+  },
+  {
+    name: '在线状态',
+    prop: 'netStatus',
+  },
+  {
+    name: '温度(‌℃‌)',
+    //prop: 'syswarnLevel',
+    prop: 'avg',
+  },
+];
 
 //快捷操作
 export let quickOption = [
-{ label: '系统运行时间', value: 'time', iconName: 'time-run' },
-{ label: '数据刷新', value: 'sjsx', iconName: 'data-reset' },
-{ label: '网络延迟', value: 'wlyc', iconName: 'internet-on' },
-]
+  { label: '系统运行时间', value: 'time', iconName: 'time-run' },
+  { label: '数据刷新', value: 'sjsx', iconName: 'data-reset' },
+  { label: '网络延迟', value: 'wlyc', iconName: 'internet-on' },
+];
 export let btnOption = [
-{ firstLabel: '报警确认', secondLabel: '视屏调取', thirdLabel: '联动控制' },
-{ firstLabel: '数据导出', secondLabel: '生成报表', thirdLabel: '定位追踪' },
-{ firstLabel: '声光关闭', secondLabel: '应急广播', thirdLabel: '系统设置' },
-]
+  { firstLabel: '报警确认', secondLabel: '视屏调取', thirdLabel: '联动控制' },
+  { firstLabel: '数据导出', secondLabel: '生成报表', thirdLabel: '定位追踪' },
+  { firstLabel: '声光关闭', secondLabel: '应急广播', thirdLabel: '系统设置' },
+];
 
 //报警监控
 export let alarmOption = [
-{
-label: '今日报警',
-value: 'dayWarn',
-},
-{
-label: '已处理',
-value: 'warnOk',
-},
-{
-label: '未处理',
-value: 'warnUnOk',
-},
-]
+  {
+    label: '今日报警',
+    value: 'dayWarn',
+  },
+  {
+    label: '已处理',
+    value: 'warnOk',
+  },
+  {
+    label: '未处理',
+    value: 'warnUnOk',
+  },
+];
 
 //区域温度统计
 export let chartTempConfig: any = {
-type: 'bar_cylinder_wide',
-readFrom: '',
-grid: {
-top: 40,
-left: 50,
-bottom: 14,
-right: 50,
-},
-legend: { show: false },
-xAxis: [{ show: true }],
-yAxis: [{ show: true, name: '', position: 'left' }],
-series: [{ readFrom: '', xprop: 'x', yprop: 'y', label: '温度' }],
-}
-
+  type: 'bar_cylinder_wide',
+  readFrom: '',
+  grid: {
+    top: 40,
+    left: 50,
+    bottom: 14,
+    right: 50,
+  },
+  legend: { show: false },
+  xAxis: [{ show: true }],
+  yAxis: [{ show: true, name: '', position: 'left' }],
+  series: [{ readFrom: '', xprop: 'x', yprop: 'y', label: '温度' }],
+};
 
 //export let envOption = [
 // { label: '环境温度', value: 'tempHj', unit: '℃' },
@@ -138,321 +144,321 @@ series: [{ readFrom: '', xprop: 'x', yprop: 'y', label: '温度' }],
 
 //用户列表
 export let optionUser = [
-{ label: '用户账号', value: 'username' },
-{ label: '用户姓名', value: 'realname' },
-{ label: '所属角色', value: 'userRolesStr' },
-{ label: '部门', value: 'orgCodeTxt' },
-{ label: '手机号', value: 'phone' },
-{ label: '状态', value: 'status_dictText' },
-]
+  { label: '用户账号', value: 'username' },
+  { label: '用户姓名', value: 'realname' },
+  { label: '所属角色', value: 'userRolesStr' },
+  { label: '部门', value: 'orgCodeTxt' },
+  { label: '手机号', value: 'phone' },
+  { label: '状态', value: 'status_dictText' },
+];
 
 //日志审计
 export let logbtnOption = [
-{ label: '登录日志', value: 'login' },
-{ label: '操作日志', value: 'operate' },
-{ label: '浏览日志', value: 'look' },
-]
+  { label: '登录日志', value: 'login' },
+  { label: '操作日志', value: 'operate' },
+  { label: '浏览日志', value: 'look' },
+];
 export let titleOption = [
-{ label: '操作人ID', value: 'userid' },
-{ label: '操作人', value: 'username' },
-{ label: '日志内容', value: 'logContent' },
-{ label: 'IP', value: 'ip' },
-{ label: '创建时间', value: 'createTime' },
-{ label: '日志类型', value: 'logType_dictText' },
-]
+  { label: '操作人ID', value: 'userid' },
+  { label: '操作人', value: 'username' },
+  { label: '日志内容', value: 'logContent' },
+  { label: 'IP', value: 'ip' },
+  { label: '创建时间', value: 'createTime' },
+  { label: '日志类型', value: 'logType_dictText' },
+];
 export let titleLookOption = [
-{ label: '操作人ID', value: 'userid' },
-{ label: '操作人', value: 'username' },
-{ label: '日志内容', value: 'logContent' },
-{ label: 'IP', value: 'ip' },
-{ label: '创建时间', value: 'createTime' },
-]
+  { label: '操作人ID', value: 'userid' },
+  { label: '操作人', value: 'username' },
+  { label: '日志内容', value: 'logContent' },
+  { label: 'IP', value: 'ip' },
+  { label: '创建时间', value: 'createTime' },
+];
 
 //今日登录统计
 export let dayLoginConfig: any = {
-type: 'bar_cylinder_wide',
-readFrom: '',
-grid: {
-top: 55,
-left: 20,
-bottom: 25,
-right: 20,
-},
-legend: { show: false },
-xAxis: [{ show: true }],
-yAxis: [{ show: true, name: '', position: 'left' }],
-series: [{ readFrom: '', xprop: 'x', yprop: 'y', label: '次数' }],
-}
+  type: 'bar_cylinder_wide',
+  readFrom: '',
+  grid: {
+    top: 55,
+    left: 20,
+    bottom: 25,
+    right: 20,
+  },
+  legend: { show: false },
+  xAxis: [{ show: true }],
+  yAxis: [{ show: true, name: '', position: 'left' }],
+  series: [{ readFrom: '', xprop: 'x', yprop: 'y', label: '次数' }],
+};
 
 //报警列表表头
 export let warnTitle = [
-{ label: '设备名称', value: 'deviceName' },
-{ label: '报警类型', value: 'eventTypeC' },
-{ label: '模式', value: 'mode_dictText' },
-{ label: '时间', value: 'createTime' },
-{ label: '操作', value: 'operation' },
-]
+  { label: '设备名称', value: 'deviceName' },
+  { label: '报警类型', value: 'eventTypeC' },
+  { label: '模式', value: 'mode_dictText' },
+  { label: '时间', value: 'createTime' },
+  { label: '操作', value: 'operation' },
+];
 
 export let alarmColumns: any[] = [
-{
-name: '设备名称',
-prop: 'deviceName',
-},
-{
-name: '报警类型',
-prop: 'eventTypeC',
-},
-{
-name: '模式',
-prop: 'mode_dictText',
-},
-{
-name: '时间',
-prop: 'createTime',
-},
-]
+  {
+    name: '设备名称',
+    prop: 'deviceName',
+  },
+  {
+    name: '报警类型',
+    prop: 'eventTypeC',
+  },
+  {
+    name: '模式',
+    prop: 'mode_dictText',
+  },
+  {
+    name: '时间',
+    prop: 'createTime',
+  },
+];
 export let dscAlarmLogColumns: any[] = [
-{
-name: '设备名称',
-prop: 'deviceName',
-},
-{
-name: '安装位置',
-prop: 'devicePos',
-},
-{
-name: '报警类型',
-prop: 'eventTypeC',
-},
-{
-name: '红外类型',
-prop: 'irModel',
-},
-{
-name: '模式',
-prop: 'mode',
-},
-{
-name: '红外水平像素',
-prop: 'numOfIrHorPixs',
-},
-{
-name: '红外垂直像素',
-prop: 'numOfIrVerPixs',
-},
-{
-name: '云台水平角',
-prop: 'ptzHorAngle',
-},
-{
-name: '云台垂直角',
-prop: 'ptzVerAngle',
-},
-]
+  {
+    name: '设备名称',
+    prop: 'deviceName',
+  },
+  {
+    name: '安装位置',
+    prop: 'devicePos',
+  },
+  {
+    name: '报警类型',
+    prop: 'eventTypeC',
+  },
+  {
+    name: '红外类型',
+    prop: 'irModel',
+  },
+  {
+    name: '模式',
+    prop: 'mode',
+  },
+  {
+    name: '红外水平像素',
+    prop: 'numOfIrHorPixs',
+  },
+  {
+    name: '红外垂直像素',
+    prop: 'numOfIrVerPixs',
+  },
+  {
+    name: '云台水平角',
+    prop: 'ptzHorAngle',
+  },
+  {
+    name: '云台垂直角',
+    prop: 'ptzVerAngle',
+  },
+];
 
 //联动规则配置
 export let warnTableConfig: any = {
-readFrom: '',
-type: 'C',
-tableReadFrom: 'warnInfo',
-columns: [
-{
-name: '报警条目',
-prop: 'alarmName',
-},
-{
-name: '预警等级',
-prop: 'alarmLevel',
-},
-{
-name: '所属系统',
-prop: 'systemType_dictText',
-},
-{
-name: '预警类型',
-prop: 'alarmType_dictText',
-},
-{
-name: '创建人',
-prop: 'createBy',
-},
-{
-name: '操作',
-prop: 'operation',
-},
-],
-}
+  readFrom: '',
+  type: 'C',
+  tableReadFrom: 'warnInfo',
+  columns: [
+    {
+      name: '报警条目',
+      prop: 'alarmName',
+    },
+    {
+      name: '预警等级',
+      prop: 'alarmLevel',
+    },
+    {
+      name: '所属系统',
+      prop: 'systemType_dictText',
+    },
+    {
+      name: '预警类型',
+      prop: 'alarmType_dictText',
+    },
+    {
+      name: '创建人',
+      prop: 'createBy',
+    },
+    {
+      name: '操作',
+      prop: 'operation',
+    },
+  ],
+};
 export let warnTableConfigD: any = {
-readFrom: '',
-type: 'D',
-}
+  readFrom: '',
+  type: 'D',
+};
 
 //视频预览-报警列表表头
 export let videoColumns: any[] = [
-{
-name: '设备号',
-prop: 'preset',
-},
-{
-name: '设备名称',
-prop: 'deviceName',
-},
-{
-name: '设备安装位置',
-prop: 'devicePos',
-},
-{
-name: '报警类型',
-prop: 'eventTypeC',
-},
-//{
-//name: '红外类型',
-//prop: 'irModel',
-//},
-{
-name: '模式',
-prop: 'modeC',
-},
-{
-name: '红外水平像素数量',
-prop: 'numOfIrHorPixs',
-},
-{
-name: '红外垂直像素数量',
-prop: 'numOfIrVerPixs',
-},
-{
-name: '云台水平角',
-prop: 'ptzHorAngle',
-},
-{
-name: '云台垂直角',
-prop: 'ptzVerAngle',
-},
-{
-name: '测温标记',
-prop: 'tempFlag',
-},
-]
+  {
+    name: '设备号',
+    prop: 'preset',
+  },
+  {
+    name: '设备名称',
+    prop: 'deviceName',
+  },
+  {
+    name: '设备安装位置',
+    prop: 'devicePos',
+  },
+  {
+    name: '报警类型',
+    prop: 'eventTypeC',
+  },
+  //{
+  //name: '红外类型',
+  //prop: 'irModel',
+  //},
+  {
+    name: '模式',
+    prop: 'modeC',
+  },
+  {
+    name: '红外水平像素数量',
+    prop: 'numOfIrHorPixs',
+  },
+  {
+    name: '红外垂直像素数量',
+    prop: 'numOfIrVerPixs',
+  },
+  {
+    name: '云台水平角',
+    prop: 'ptzHorAngle',
+  },
+  {
+    name: '云台垂直角',
+    prop: 'ptzVerAngle',
+  },
+  {
+    name: '测温标记',
+    prop: 'tempFlag',
+  },
+];
 
 /**视频预览-Tab 标签列表 */
 export const tabs: TabItem[] = [
-{ key: 'device', label: '设备' },
-{ key: 'stats', label: '统计' },
-{ key: 'advanced', label: '高级' },
-{ key: 'angle', label: '角度' },
-{ key: 'preset', label: '预置位' },
-]
+  { key: 'device', label: '设备' },
+  { key: 'stats', label: '统计' },
+  { key: 'advanced', label: '高级' },
+  { key: 'angle', label: '角度' },
+  { key: 'preset', label: '预置位' },
+];
 
 /**云台控制-功能 Tab 列表:云台控制 / 红外控制 / 激光 / 透雾 */
 export const tabsPtz: TabItem[] = [
-{ key: 'ptz', label: '云台控制' },
-{ key: 'ir', label: '红外控制' },
-{ key: 'laser', label: '激光' },
-{ key: 'defog', label: '透雾' },
-]
+  { key: 'ptz', label: '云台控制' },
+  { key: 'ir', label: '红外控制' },
+  { key: 'laser', label: '激光' },
+  { key: 'defog', label: '透雾' },
+];
 /**云台控制 - 镜头控制项:变焦、聚焦、光圈 */
 export const lensControls: LensControl[] = [
-{
-key: 'zoom',
-plusTip: '焦距变大',
-minusTip: '焦距变小',
-icon: `<svg viewBox="0 0 24 24" width="18"
+  {
+    key: 'zoom',
+    plusTip: '焦距变大',
+    minusTip: '焦距变小',
+    icon: `<svg viewBox="0 0 24 24" width="18"
   height="18"><circle cx="10" cy="10" r="6" fill="none" stroke="#fff" stroke-width="1.6"/><path d="M14.5 14.5 L20 20" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><path d="M10 7.5 V12.5 M7.5 10 H12.5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>`,
-},
-{
-key: 'focus',
-plusTip: '焦点前调',
-minusTip: '焦点后调',
-icon: `<svg viewBox="0 0 24 24" width="18"
+  },
+  {
+    key: 'focus',
+    plusTip: '焦点前调',
+    minusTip: '焦点后调',
+    icon: `<svg viewBox="0 0 24 24" width="18"
   height="18"><rect x="3" y="7" width="12" height="10" rx="1.5" fill="none" stroke="#fff" stroke-width="1.5"/><circle cx="9" cy="12" r="2.5" fill="none" stroke="#fff" stroke-width="1.4"/><path d="M15 9.5 L21 7 V17 L15 14.5 Z" fill="none" stroke="#fff" stroke-width="1.4" stroke-linejoin="round"/></svg>`,
-},
-{
-key: 'iris',
-plusTip: '光圈扩大',
-minusTip: '光圈缩小',
-icon: `<svg viewBox="0 0 24 24" width="18"
+  },
+  {
+    key: 'iris',
+    plusTip: '光圈扩大',
+    minusTip: '光圈缩小',
+    icon: `<svg viewBox="0 0 24 24" width="18"
   height="18"><circle cx="12" cy="12" r="8" fill="none" stroke="#fff" stroke-width="1.4"/><path d="M12 4 L14.5 10.5 L21 12 L14.5 13.5 L12 20 L9.5 13.5 L3 12 L9.5 10.5 Z" fill="none" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/></svg>`,
-},
-]
+  },
+];
 
 export let picList = [
-{ src: getAssetURL('home-container/configurable/hsq/4-1.png'), label: '1.报警触发' },
-{ src: getAssetURL('home-container/configurable/hsq/6-1.png'), label: '2.系统确认' },
-{ src: getAssetURL('home-container/configurable/hsq/6-2.png'), label: '3.通知值班' },
-{ src: getAssetURL('home-container/configurable/hsq/4-4.png'), label: '4.现场处置' },
-{ src: getAssetURL('home-container/configurable/hsq/4-5.png'), label: '5.确认解除' },
-]
+  { src: getAssetURL('home-container/configurable/hsq/4-1.png'), label: '1.报警触发' },
+  { src: getAssetURL('home-container/configurable/hsq/6-1.png'), label: '2.系统确认' },
+  { src: getAssetURL('home-container/configurable/hsq/6-2.png'), label: '3.通知值班' },
+  { src: getAssetURL('home-container/configurable/hsq/4-4.png'), label: '4.现场处置' },
+  { src: getAssetURL('home-container/configurable/hsq/4-5.png'), label: '5.确认解除' },
+];
 /** 处置流程按钮 */
 export const actionBtns = [
-{ key: 2, label: '系统确认' },
-{ key: 3, label: '通知值班' },
-{ key: 4, label: '现场处置' },
-{ key: 5, label: '确认解除' },
-]
+  { key: 2, label: '系统确认' },
+  { key: 3, label: '通知值班' },
+  { key: 4, label: '现场处置' },
+  { key: 5, label: '确认解除' },
+];
 
-export let navList =[
-{ label: '光谱摄像机', value: 'duaSpeCamera' },
-{ label: '火灾探测器', value: 'imgFireDet' }
-]
-export let titleList =[
-{ label: '用户', value: 'username' },
-{ label: '操作设备', value: 'devicename' },
-{ label: '操作记录', value: 'strremark' },
-{ label: '登录IP', value: 'ip' },
-{ label: '时间', value: 'updateTime' },
-]
+export let navList = [
+  { label: '光谱摄像机', value: 'duaSpeCamera' },
+  { label: '火灾探测器', value: 'imgFireDet' },
+];
+export let titleList = [
+  { label: '用户', value: 'username' },
+  { label: '操作设备', value: 'devicename' },
+  { label: '操作记录', value: 'strremark' },
+  { label: '登录IP', value: 'ip' },
+  { label: '时间', value: 'updateTime' },
+];
 
 /** 角度默认值(刷新时恢复) */
 export const ANGLE_DEFAULT: AngleState = {
-hMin: 0,
-hMax: 360,
-vMin: 90,
-vMax: 180,
-}
+  hMin: 0,
+  hMax: 360,
+  vMin: 90,
+  vMax: 180,
+};
 
 export const angleFields: AngleField[] = [
-{ label: '水平扫描角度:', minKey: 'hMin', maxKey: 'hMax' },
-{ label: '垂直扫描角度:', minKey: 'vMin', maxKey: 'vMax' },
-]
+  { label: '水平扫描角度:', minKey: 'hMin', maxKey: 'hMax' },
+  { label: '垂直扫描角度:', minKey: 'vMin', maxKey: 'vMax' },
+];
 
 /** 多选框提示:第一个可见光取流,第二个红外取流 */
-export const checkTips = ['可见光取流', '红外取流'] as const
+export const checkTips = ['可见光取流', '红外取流'] as const;
 
 /** 告警上报字段(全部可手动输入修改) */
 export const alarmReportFieldList: AlarmReportFieldItem[] = [
-{ key: 'deviceId', label: '设备号', placeholder: '请输入设备号' },
-{ key: 'deviceName', label: '设备名', placeholder: '请输入设备名' },
-{ key: 'longitude', label: '设备经度', placeholder: '请输入设备经度' },
-{ key: 'latitude', label: '设备纬度', placeholder: '请输入设备纬度' },
-{ key: 'operator', label: '操作员', placeholder: '请输入操作员' },
-{ key: 'remark', label: '备注', placeholder: '请输入备注' },
-]
+  { key: 'deviceId', label: '设备号', placeholder: '请输入设备号' },
+  { key: 'deviceName', label: '设备名', placeholder: '请输入设备名' },
+  { key: 'longitude', label: '设备经度', placeholder: '请输入设备经度' },
+  { key: 'latitude', label: '设备纬度', placeholder: '请输入设备纬度' },
+  { key: 'operator', label: '操作员', placeholder: '请输入操作员' },
+  { key: 'remark', label: '备注', placeholder: '请输入备注' },
+];
 
-/** 预置位工具栏:图标对应 assets/icons 下 1.svg ~ 6.svg */
+/** 预置位工具栏:前 5 个图标对应 assets/icons 下 1.svg ~ 5.svg,添加为 add.svg */
 export const presetToolbarItems: PresetToolbarItem[] = [
-{ key: 'refresh', icon: '1', title: '刷新' },
-{ key: 'link', icon: '2', title: '关联' },
-{ key: 'config', icon: '3', title: '配置' },
-{ key: 'export', icon: '4', title: '导出' },
-{ key: 'clear', icon: '5', title: '清空' },
-{ key: 'delete', icon: '6', title: '删除' },
-]
+  { key: 'refresh', icon: '1', title: '刷新' },
+  { key: 'link', icon: '2', title: '关联' },
+  { key: 'config', icon: '3', title: '配置' },
+  { key: 'export', icon: '4', title: '导出' },
+  { key: 'clear', icon: '5', title: '清空' },
+  { key: 'add', icon: 'add', title: '添加' },
+];
 
 /** 预置位表格列 */
 export const presetColumns = [
-{ key: 'id', label: '编号' },
-{ key: 'name', label: '名称' },
-{ key: 'action', label: '操作' },
-] as const
+  { key: 'id', label: '编号' },
+  { key: 'name', label: '名称' },
+  { key: 'action', label: '操作' },
+] as const;
 
 /** 预置位空表占位行数(对齐设计稿斑马纹区域) */
-export const PRESET_EMPTY_ROW_COUNT = 8
+export const PRESET_EMPTY_ROW_COUNT = 8;
 
 /** 激光操作:开/关激光、补光、爆闪 */
 export const laserActions: LaserActionItem[] = [
-{ key: 'laserOn', label: '开激光' },
-{ key: 'laserOff', label: '关激光' },
-{ key: 'laserFillLight', label: '补光' },
-{ key: 'laserStrobe', label: '开爆闪' },
-]
+  { key: 'laserOn', label: '开激光' },
+  { key: 'laserOff', label: '关激光' },
+  { key: 'laserFillLight', label: '补光' },
+  { key: 'laserStrobe', label: '开爆闪' },
+];

+ 76 - 76
src/views/vent/monitorManager/hsqHome/videoPlayer.vue

@@ -10,15 +10,12 @@
           :selectedCamera="selectedCamera"
           @checkChange="onCheckChange"
           @checkedDeviceIdsChange="onCheckedDeviceIdsChange"
+          @regionPreview="onRegionPreview"
         />
       </div>
       <!-- 左下:设备控制组件 -->
       <div class="basic-bottom-left">
-        <DeviceControl
-          :deviceOptions="deviceOptions"
-          :selectedCamera="selectedCamera"
-          @action="onPtzAction"
-        />
+        <DeviceControl :deviceOptions="deviceOptions" :selectedCamera="selectedCamera" @action="onPtzAction" />
       </div>
     </div>
     <!-- 右侧面板:设备画面 + 告警列表 -->
@@ -36,47 +33,47 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, onUnmounted, ref } from 'vue'
+import { onMounted, onUnmounted, ref } from 'vue';
 
 // 子组件导入
-import DeviceTree from './components/DeviceTree.vue' // 设备树
-import DeviceControl from './components/DeviceControl.vue' // 设备控制
-import DeviceView from './components/DeviceView.vue' // 设备画面
-import DeviceWarnList from './components/DeviceWarnList.vue' // 设备告警列表
-import { managesysList, monitorSystem, dscAlarmLogList } from './hsqHome.api'
+import DeviceTree from './components/DeviceTree.vue'; // 设备树
+import DeviceControl from './components/DeviceControl.vue'; // 设备控制
+import DeviceView from './components/DeviceView.vue'; // 设备画面
+import DeviceWarnList from './components/DeviceWarnList.vue'; // 设备告警列表
+import { managesysList, monitorSystem, dscAlarmLogList } from './hsqHome.api';
 
 /** 设备分组节点数据结构 */
 interface GroupNode {
-  id: string
-  title: string
-  children: DeviceNode[]
+  id: string;
+  title: string;
+  children: DeviceNode[];
 }
 /** 设备节点数据结构 */
 interface DeviceNode {
-  id: string
-  name: string
+  id: string;
+  name: string;
   /** 勾选:可见光取流 / 红外取流 */
-  checks: [boolean, boolean]
+  checks: [boolean, boolean];
   /** 设备 IP */
-  ip?: string
+  ip?: string;
 }
 
 //系统id
-const systemId = ref('')
+const systemId = ref('');
 /** 设备树数据(包含分组和设备列表) */
-const treeData = ref<GroupNode[]>([])
+const treeData = ref<GroupNode[]>([]);
 // 设备选项
-const deviceOptions = ref<any[]>([])
+const deviceOptions = ref<any[]>([]);
 // 双光谱摄像机报警记录-分页列表查询
-const warnList = ref<any[]>([])
+const warnList = ref<any[]>([]);
 // // 当前选中的设备ID
 // const deviceId = ref('')
 /** 设备树勾选中的设备 ID 列表(随勾选动态更新) */
-const checkedDeviceIds = ref<string>('')
+const checkedDeviceIds = ref<string>('');
 /** 设备画面组件引用 */
-const deviceViewRef = ref<InstanceType<typeof DeviceView> | null>(null)
+const deviceViewRef = ref<InstanceType<typeof DeviceView> | null>(null);
 /** 当前画面区选中的摄像头 */
-const selectedCamera = ref<any>(null)
+const selectedCamera = ref<any>(null);
 /** 定时获取监测数据定时器 */
 let timer: null | NodeJS.Timeout = null;
 
@@ -86,60 +83,60 @@ let timer: null | NodeJS.Timeout = null;
  * - 红外取流(index === 1):当前页前 4 路停止、后 4 路播放
  */
 function onCheckChange(payload: { device: DeviceNode; index: number; checked: boolean }) {
-  console.log('设备树勾选变更', payload)
+  console.log('设备树勾选变更', payload);
   // deviceId.value = payload.device.id || ''
-  selectedCamera.value = null
-  if (!payload.checked) return
+  selectedCamera.value = null;
+  if (!payload.checked) return;
   if (payload.index === 0) {
-    deviceViewRef.value?.enableVisibleLightPlayMode?.()
+    deviceViewRef.value?.enableVisibleLightPlayMode?.();
   } else if (payload.index === 1) {
-    deviceViewRef.value?.enableInfraredPlayMode?.()
+    deviceViewRef.value?.enableInfraredPlayMode?.();
   }
 }
 
 /** 接收子组件勾选设备 ID 数组(默认全选后即为全部设备 ID) */
 function onCheckedDeviceIdsChange(ids: string[]) {
-  checkedDeviceIds.value = ids.join(',') || ''
-  console.log('勾选设备ID列表', checkedDeviceIds.value)
+  checkedDeviceIds.value = ids.join(',') || '';
+  console.log('勾选设备ID列表', checkedDeviceIds.value);
+}
+
+/** 预置位操作列点击放大:放大当前选中摄像头并显示左侧区域选择 */
+function onRegionPreview() {
+  deviceViewRef.value?.openRegionPreview?.();
 }
 
 /** 画面区摄像头选中(附带当前设备 IP / deviceid,供抓拍、录像等 SDK 接口使用) */
 function onCameraSelect(camera: any) {
   if (!camera) {
-    selectedCamera.value = null
-    return
+    selectedCamera.value = null;
+    return;
   }
-  const deviceid =
-    camera.deviceid ||
-    camera.deviceId ||
-    camera.deviceID ||
-    camera.devId ||
-    ''
+  const deviceid = camera.deviceid || camera.deviceId || camera.deviceID || camera.devId || '';
   selectedCamera.value = {
     ...camera,
     deviceid: deviceid != null && String(deviceid).trim() !== '' ? String(deviceid).trim() : '',
     ip: camera.ip || camera.strip || '',
-  }
+  };
 }
 
 /** 云台焦距 / 焦点 / 光圈:不调接口,直接作用于当前选中画面 */
 function onPtzAction(payload: { type: string; value: string }) {
-  if (!payload?.type || payload.value === 'stop') return
+  if (!payload?.type || payload.value === 'stop') return;
   if (payload.type === 'zoom') {
-    if (payload.value === 'plus') deviceViewRef.value?.zoomSelectedView?.(1)
-    else if (payload.value === 'minus') deviceViewRef.value?.zoomSelectedView?.(-1)
-    return
+    if (payload.value === 'plus') deviceViewRef.value?.zoomSelectedView?.(1);
+    else if (payload.value === 'minus') deviceViewRef.value?.zoomSelectedView?.(-1);
+    return;
   }
   if (payload.type === 'focus') {
     // plus=焦点前调(更清晰),minus=焦点后调(更模糊)
-    if (payload.value === 'plus') deviceViewRef.value?.focusSelectedView?.(-1)
-    else if (payload.value === 'minus') deviceViewRef.value?.focusSelectedView?.(1)
-    return
+    if (payload.value === 'plus') deviceViewRef.value?.focusSelectedView?.(-1);
+    else if (payload.value === 'minus') deviceViewRef.value?.focusSelectedView?.(1);
+    return;
   }
   if (payload.type === 'iris') {
     // plus=光圈扩大(更亮),minus=光圈缩小(更暗)
-    if (payload.value === 'plus') deviceViewRef.value?.irisSelectedView?.(1)
-    else if (payload.value === 'minus') deviceViewRef.value?.irisSelectedView?.(-1)
+    if (payload.value === 'plus') deviceViewRef.value?.irisSelectedView?.(1);
+    else if (payload.value === 'minus') deviceViewRef.value?.irisSelectedView?.(-1);
   }
 }
 
@@ -150,14 +147,17 @@ function onPtzAction(payload: { type: string; value: string }) {
  * @param flag - 是否立即执行(首次调用时传true跳过延时)
  */
 function getMonitor(flag?: boolean) {
-  timer = setTimeout(async () => {
-    await getTableData();
-    await getDscAlarmLogList();
-    if (timer) {
-      timer = null;
-    }
-    getMonitor();
-  }, flag ? 0 : 10000);
+  timer = setTimeout(
+    async () => {
+      await getTableData();
+      await getDscAlarmLogList();
+      if (timer) {
+        timer = null;
+      }
+      getMonitor();
+    },
+    flag ? 0 : 10000
+  );
 }
 
 /**
@@ -167,7 +167,7 @@ function getMonitor(flag?: boolean) {
 async function getManagesysList() {
   const res = await managesysList({ strtype: 'sys_openair_fire', pagetype: 'normal' });
   if (res && res.records) {
-    systemId.value = res.records[0].id
+    systemId.value = res.records[0].id;
   }
 }
 
@@ -252,34 +252,34 @@ async function getTableData() {
   //   }))
   // }
 
-  let data = res.deviceInfo.duaSpeCamera.datalist.map(el => Object.assign({}, el, el.readData)) || [];
+  let data = res.deviceInfo.duaSpeCamera.datalist.map((el) => Object.assign({}, el, el.readData)) || [];
   // 定时刷新时保留已有勾选状态,避免重置
-  const checkStateMap = new Map<string, [boolean, boolean]>()
+  const checkStateMap = new Map<string, [boolean, boolean]>();
   treeData.value.forEach((group) => {
     group.children?.forEach((device) => {
-      checkStateMap.set(device.id, [!!device.checks?.[0], !!device.checks?.[1]])
-    })
-  })
+      checkStateMap.set(device.id, [!!device.checks?.[0], !!device.checks?.[1]]);
+    });
+  });
   treeData.value = [
     {
       id: 'g1',
       title: '双光谱摄像机',
-      children: data.map(d => {
-        const prevChecks = checkStateMap.get(d.deviceID)
+      children: data.map((d) => {
+        const prevChecks = checkStateMap.get(d.deviceID);
         return {
           id: d.deviceID,
           name: d.strname,
           ip: d.ip || d.strip || '',
           checks: prevChecks ? [...prevChecks] : [true, true],
-        }
-      })
+        };
+      }),
     },
-  ]
-  deviceOptions.value = data.map(d => ({
+  ];
+  deviceOptions.value = data.map((d) => ({
     value: d.deviceID,
     label: d.strname,
-    deviceType: d.deviceType
-  }))
+    deviceType: d.deviceType,
+  }));
 }
 
 /**
@@ -290,24 +290,24 @@ async function getTableData() {
 async function getDscAlarmLogList() {
   const res = await dscAlarmLogList({ pageNo: 1, pageSize: 1000 });
   if (res && res.records) {
-    warnList.value = res.records.map(el => ({
+    warnList.value = res.records.map((el) => ({
       ...el,
       eventTypeC: el.eventType == '10001' ? '热源报警' : el.eventType == '10002' ? '闯入报警' : '-',
       modeC: el.mode == '1' || el.mode == 1 ? '预置位' : el.mode == '2' || el.mode == 2 ? '角度位下标号' : '-',
-    }))
+    }));
   }
 }
 
 onMounted(async () => {
   await getManagesysList();
   getMonitor(true);
-})
+});
 /**
  * 组件卸载前
  * 清理定时器,停止轮询获取监测数据
  */
 onUnmounted(() => {
-  timer = null
+  timer = null;
   clearTimeout(timer);
 });
 </script>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio