|
|
@@ -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);
|
|
|
}
|
|
|
|
|
|
@@ -710,20 +709,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 {
|