Files
Visualize-electronic-fences/src/views/equipmentList/list.vue
2026-07-02 14:34:23 +08:00

955 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="设备编号" prop="code">
<el-input v-model="queryParams.code" placeholder="请输入设备编号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="设备名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入设备名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="监控区域" prop="monitoringArea">
<el-input v-model="queryParams.monitoringArea" placeholder="请输入监控区域" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="设备状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择设备状态" clearable style="width: 160px">
<el-option label="在线" :value="1" />
<el-option label="离线" :value="2" />
<el-option label="低帧" :value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="hover">
<template #header>
<el-row :gutter="10" class="flex justify-end mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd()">添加设备</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-hasPermi="['demo:demo:edit']" type="success" plain icon="Edit" :disabled="multiple" @click="handleActivate()"
>批量唤醒</el-button
>
</el-col>
<el-col :span="1.5">
<el-button v-hasPermi="['demo:demo:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
>批量删除</el-button
>
</el-col>
<!-- <el-col :span="1.5">
<el-button v-hasPermi="['demo:demo:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
</el-col> -->
<!-- <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar> -->
</el-row>
</template>
<el-table v-loading="loading" border :data="demoList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column v-if="true" label="设备编号" align="center" prop="code" />
<el-table-column label="设备名称" align="center" prop="name" />
<el-table-column label="监控区域" align="center" prop="monitoringArea" />
<el-table-column label="设备状态" align="center" prop="statusName" width="100">
<!-- <template #default="scope">
<el-tag v-if="scope.row.status === 1 || scope.row.status === '1'">在线</el-tag>
<el-tag v-else-if="scope.row.status === 2 || scope.row.status === '2'" type="warning">离线</el-tag>
<el-tag v-else-if="scope.row.status === 3 || scope.row.status === '3'" type="info">低帧</el-tag>
<el-tag v-else>{{ scope.row.status ?? '-' }}</el-tag>
</template> -->
</el-table-column>
<!-- <el-table-column label="电池电量" align="center" prop="batteryLevel1" width="150">
<template #default="scope">
<el-tag v-if="scope.row.batteryLevel1">{{ scope.row.batteryLevel1 + '%' }}</el-tag>
<el-tag v-if="scope.row.batteryLevel2" :style="scope.row.batteryLevel1 && scope.row.batteryLevel2 ? 'margin-left: 10px' : ''">
{{ scope.row.batteryLevel2 + '%' }}
</el-tag>
<el-tag v-if="!scope.row.batteryLevel1 && !scope.row.batteryLevel2"> 未获取 </el-tag>
</template>
</el-table-column> -->
<el-table-column label="电池电量" align="center" width="160">
<template #default="scope">
<div v-if="scope.row.batteryLevel1 != null || scope.row.batteryLevel2 != null" class="battery-group">
<!-- 左电池 -->
<div v-if="scope.row.batteryLevel1 != null" class="battery-item">
<svg viewBox="0 0 56 20" width="56" height="20">
<rect
x="0"
y="1"
width="44"
height="18"
rx="4"
ry="4"
fill="none"
:stroke="getBatteryColor(scope.row.batteryLevel1)"
stroke-width="1.5"
/>
<rect x="44" y="6" width="4" height="8" rx="2" ry="2" :fill="getBatteryColor(scope.row.batteryLevel1)" />
<rect
x="2"
y="3"
:width="Math.max((40 * scope.row.batteryLevel1) / 100, 0)"
height="14"
rx="2"
ry="2"
:fill="getBatteryColor(scope.row.batteryLevel1)"
opacity="0.25"
/>
<text
x="24"
y="11.5"
text-anchor="middle"
dominant-baseline="middle"
font-size="2"
:fill="getBatteryColor(scope.row.batteryLevel1)"
>
{{ scope.row.batteryLevel1 }}%
</text>
</svg>
</div>
<!-- 右电池 -->
<div v-if="scope.row.batteryLevel2 != null" class="battery-item">
<svg viewBox="0 0 56 20" width="56" height="20">
<rect
x="0"
y="1"
width="44"
height="18"
rx="4"
ry="4"
fill="none"
:stroke="getBatteryColor(scope.row.batteryLevel2)"
stroke-width="1.5"
/>
<rect x="44" y="6" width="4" height="8" rx="2" ry="2" :fill="getBatteryColor(scope.row.batteryLevel2)" />
<rect
x="2"
y="3"
:width="Math.max((40 * scope.row.batteryLevel2) / 100, 0)"
height="14"
rx="2"
ry="2"
:fill="getBatteryColor(scope.row.batteryLevel2)"
opacity="0.25"
/>
<text
x="24"
y="11.5"
text-anchor="middle"
dominant-baseline="middle"
font-size="2"
:fill="getBatteryColor(scope.row.batteryLevel2)"
>
{{ scope.row.batteryLevel2 }}%
</text>
</svg>
</div>
</div>
<span v-else>未获取</span>
</template>
</el-table-column>
<!-- <el-table-column align="center" label="电量" prop="powerLevel" width="160">
<template #default="scope">
<div v-if="scope.row.powerLevel !== undefined && scope.row.powerLevel !== null && scope.row.powerLevel !== ''" class="power-level">
<svg class="power-level__icon" :class="getPowerLevelClass(scope.row.powerLevel)" viewBox="0 0 48 24" width="48" height="24">
<rect x="0" y="2" width="42" height="20" rx="4" ry="4" fill="none" stroke="currentColor" stroke-width="2" />
<rect x="42" y="8" width="4" height="8" rx="2" ry="2" fill="currentColor" />
<rect
x="3"
y="5"
:width="Math.max((36 * formatPowerLevel(scope.row.powerLevel)) / 100, 0)"
height="14"
rx="2"
ry="2"
fill="currentColor"
/>
</svg>
<span class="power-level__text">{{ formatPowerLevel(scope.row.powerLevel) }}%</span>
</div>
<span v-else>-</span>
</template>
</el-table-column> -->
<el-table-column label="设备型号" align="center" prop="model" />
<el-table-column label="固件版本" align="center" prop="version" />
<el-table-column label="是否绘制区域" align="center" prop="isDrawArea">
<template #default="scope">
<el-tag v-if="scope.row.isDrawArea === 1 || scope.row.isDrawArea === '1'"></el-tag>
<el-tag v-else-if="scope.row.isDrawArea === 0 || scope.row.isDrawArea === '0'" type="info"></el-tag>
<el-tag v-else>{{ scope.row.isDrawArea ?? '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="绑定时间" align="center" prop="activateTime" />
<el-table-column label="设备异常说明" align="center" prop="remark" />
<el-table-column width="400" label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
<el-button :disabled="scope.row.status == 2 && scope.row.status == '2'" type="primary" @click="handleSee(scope.row)">查看监控</el-button>
<el-button
:disabled="scope.row.status === 1 || scope.row.status === 2 || scope.row.status === '1' || scope.row.status === '2'"
type="primary"
@click="handleActivate(scope.row)"
>唤醒设备</el-button
>
<el-button :disabled="scope.row.status !== 2 && scope.row.status !== '2'" type="primary" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
</el-card>
<!-- 添加或修改测试单对话框 -->
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
<el-form ref="demoFormRef" :model="form" :rules="rules">
<el-form-item label="设备编号:" prop="code">
<el-input v-model="form.code" placeholder="请输入设备编号" :disabled="haveCode" />
</el-form-item>
<el-form-item label="设备名称:" prop="name">
<el-input v-model="form.name" placeholder="请输入设备名称" />
</el-form-item>
<el-form-item label="监控区域:" prop="monitoringArea">
<el-input v-model="form.monitoringArea" placeholder="请输入监控区域" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<!-- 查看监控对话框 -->
<el-dialog v-model="dialog1.visible" class="monitor-dialog" :before-close="handleMonitorDialogClose" fullscreen destroy-on-close>
<template #header>
<div class="flex w-full items-center justify-between gap-2 min-w-0 pr-8">
<span class="truncate">{{ videoName }}-{{ monitoringArea }}监控</span>
<div>
<el-button type="primary" class="shrink-0" @click="handleRefresh">刷新</el-button>
<el-button v-if="!seeSee" type="primary" class="shrink-0" @click="drawImage()">绘制区域</el-button>
<el-button v-if="seeSee" type="primary" class="shrink-0" @click="clearImage()">清空区域</el-button>
<el-button v-if="seeSee" type="primary" class="shrink-0" @click="saveImage()">保存区域</el-button>
</div>
</div>
</template>
<div class="monitor-video-wrap">
<div class="monitor-video-stack">
<video ref="monitorVideoRef" class="monitor-video" controls preload="metadata" :src="monitorVideoUrl">您的浏览器不支持视频播放</video>
<div v-show="monitorDrawCollecting" class="monitor-draw-overlay" @click.stop="onMonitorDrawClick" />
<div v-if="monitorRectPx" class="monitor-draw-rect" :style="drawRectStyle" />
<div v-if="monitorRectPx" class="monitor-draw-label" :style="drawLabelStyle">当前报警区域</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup name="Demo" lang="ts">
import { addEquipment, getDevicetList, getDeviceInfo, delDevice, actDevice, updateDeviceInfo, drawInfo } from '@/api/equipmentList/index';
import { EquipmentList, DeviceInfoForm, AlarmAreaData } from '@/api/equipmentList/types';
import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types';
import { log } from 'node:console';
import { initHls, destroyHls } from '@/utils/video';
import { nextTick } from 'vue';
import { useVideosStore } from '@/store/modules/video';
import { addDemo } from '@/api/video/index';
const videosStore = useVideosStore();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const demoList = ref([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const multiple = ref(true);
const total = ref(0);
const seeSee = ref(0);
const haveCode = ref(false);
const videoName = ref<string>('实时监控');
const queryFormRef = ref<ElFormInstance>();
const demoFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const dialog1 = reactive<DialogOption>({
visible: false,
title: ''
});
const monitorVideoUrl = ref('');
const monitorVideoRef = ref<HTMLVideoElement>();
let visibilityHandler: (() => void) | null = null;
const monitoringArea = ref<string>('');
type MonitorPoint = { x: number; y: number };
const monitorDrawCollecting = ref(false);
const monitorDrawPoints = ref<MonitorPoint[]>([]);
const monitorRectPx = ref<{ left: number; top: number; width: number; height: number } | null>(null);
const currentDrawArea = ref<AlarmAreaData | null>(null);
const videoResizeObserver = ref<ResizeObserver | null>(null);
const drawRectStyle = computed<Record<string, string>>(() => {
const r = monitorRectPx.value;
if (!r) return {};
return {
left: `${r.left}px`,
top: `${r.top}px`,
width: `${Math.max(r.width, 2)}px`,
height: `${Math.max(r.height, 2)}px`
};
});
const drawLabelStyle = computed<Record<string, string>>(() => {
const r = monitorRectPx.value;
if (!r) return {};
return {
left: `${r.left + r.width / 2}px`,
top: `${r.top}px`
};
});
const initFormData = {
id: undefined,
code: undefined,
name: undefined,
monitoringArea: undefined
};
const data = reactive({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
code: undefined,
name: undefined,
monitoringArea: undefined,
status: undefined
},
rules: {
code: [{ required: true, message: '请输入设备编号', trigger: 'blur' }],
name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
monitoringArea: [{ required: true, message: '请输入监控区域', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询测试单列表 */
const getList = async () => {
loading.value = true;
const res = await getDevicetList(queryParams.value);
demoList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
demoFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: any) => {
ids.value = selection.map((item) => item.id);
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
haveCode.value = false;
reset();
dialog.visible = true;
dialog.title = '添加设备';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: any) => {
haveCode.value = true;
reset();
const _id = row?.id || ids.value[0];
const res = await getDeviceInfo(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改设备';
};
/** 提交按钮 */
const submitForm = () => {
demoFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateDeviceInfo(form.value).finally(() => (buttonLoading.value = false));
} else {
await addEquipment(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('修改成功');
dialog.visible = false;
await getList();
}
});
};
/** 唤醒设备(单行传 row批量不传则使用表格勾选 ids调用 actDevice */
const handleActivate = async (row?: any) => {
const targetIds = row?.id != null ? row.id : ids.value;
const isBatch = row == null;
if (isBatch && (!Array.isArray(targetIds) || !targetIds.length)) {
proxy?.$modal.msgWarning('请先勾选要唤醒的设备');
return;
}
const msg = isBatch ? `是否确认批量唤醒已选中的 ${targetIds.length} 台设备?` : `是否确认唤醒设备「${row!.name ?? row!.code ?? row!.id}」?`;
await proxy?.$modal.confirm(msg);
loading.value = true;
try {
await actDevice(targetIds);
proxy?.$modal.msgSuccess('唤醒成功');
await getList();
} finally {
loading.value = false;
}
};
/* 查看监控 */
const resetMonitorDraw = () => {
monitorDrawCollecting.value = false;
monitorDrawPoints.value = [];
monitorRectPx.value = null;
currentDrawArea.value = null;
};
const handleSee = async (row?: any) => {
console.log('1111111111111111', row, row.drawArea);
reset();
resetMonitorDraw();
seeSee.value = row?.isDrawArea === 1 ? 1 : 0;
monitoringArea.value = row?.monitoringArea;
const _id = row?.id || ids.value[0];
const res = await getDeviceInfo(_id);
Object.assign(form.value, res.data);
dialog1.visible = true;
dialog1.title = '修改设备';
destroyHls(monitorVideoRef);
monitorVideoUrl.value = '';
await videosStore.getVideos(row.id);
monitorVideoUrl.value = videosStore.hisUrl || '';
if (!monitorVideoUrl.value) {
//proxy?.$modal.msgWarning('该设备未登录或无法获取直播地址,已停止播放旧画面');
initVideoResizeObserver();
return;
}
// 等待 DOM 更新后初始化 Hls
await nextTick();
initHls(monitorVideoUrl, monitorVideoRef);
// 等待视频加载完成后初始化 ResizeObserver以便能正确获取视频尺寸
initVideoResizeObserver();
// 如果有 drawArea直接使用它显示矩形区域不允许点击绘制
if (row?.drawArea) {
try {
// 将 drawArea 从字符串转为对象
const drawArea = typeof row.drawArea === 'string' ? JSON.parse(row.drawArea) : row.drawArea;
console.log('[查看监控] 解析后的 drawArea:', drawArea);
if (drawArea && drawArea.x1 !== undefined && drawArea.y1 !== undefined && drawArea.x2 !== undefined && drawArea.y2 !== undefined) {
currentDrawArea.value = drawArea;
// 等待视频加载完成
await nextTick();
const video = monitorVideoRef.value;
// 如果视频已加载,立即显示矩形
if (video && video.videoWidth > 0 && video.videoHeight > 0) {
displayDrawArea(drawArea, video);
} else if (video) {
// 如果视频还未加载,等待 loadedmetadata 事件
video.addEventListener(
'loadedmetadata',
() => {
displayDrawArea(drawArea, video);
},
{ once: true }
);
}
}
} catch (error) {
console.error('[查看监控] 解析 drawArea 失败:', error);
}
}
// 后台运行的时候恢复播放
cleanupVisibilityChange();
const handleVisibilityChange = () => {
if (!document.hidden) {
const video = monitorVideoRef.value;
if (video && video.paused) {
video.play().catch((e) => console.warn('恢复播放失败:', e));
}
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
visibilityHandler = handleVisibilityChange;
};
// 提取显示报警区域的逻辑为独立函数
const displayDrawArea = (drawArea: any, video: HTMLVideoElement) => {
const VW = video.videoWidth;
const VH = video.videoHeight;
if (!VW || !VH) return;
// 计算视频原始像素坐标x 是距离右侧的千分比y 是距离顶部的千分比)
const x1Video = VW - (drawArea.x1 / 1000) * VW;
const y1Video = (drawArea.y1 / 1000) * VH;
const x2Video = VW - (drawArea.x2 / 1000) * VW;
const y2Video = (drawArea.y2 / 1000) * VH;
const rect = video.getBoundingClientRect();
const dw = rect.width;
const dh = rect.height;
if (dw <= 0 || dh <= 0) return;
const scale = Math.min(dw / VW, dh / VH);
const contentW = VW * scale;
const contentH = VH * scale;
const offsetX = (dw - contentW) / 2;
const offsetY = (dh - contentH) / 2;
const x1Px = offsetX + x1Video * scale;
const y1Px = offsetY + y1Video * scale;
const x2Px = offsetX + x2Video * scale;
const y2Px = offsetY + y2Video * scale;
const left = Math.min(x1Px, x2Px);
const top = Math.min(y1Px, y2Px);
const width = Math.max(Math.abs(x2Px - x1Px), 2);
const height = Math.max(Math.abs(y2Px - y1Px), 2);
monitorRectPx.value = { left, top, width, height };
currentDrawArea.value = drawArea;
monitorDrawCollecting.value = false; // 禁止点击绘制
// 同时添加到 payload 用于保存
payload.value = [
{
deviceId: form.value.id,
x1: drawArea.x1,
y1: drawArea.y1,
x2: drawArea.x2,
y2: drawArea.y2
}
];
console.log('[查看监控] 已加载已有报警区域', payload.value);
};
// 在窗口调整大小时更新绘制区域位置和大小
const updateDrawAreaOnResize = () => {
const drawArea = currentDrawArea.value;
const video = monitorVideoRef.value;
if (!drawArea || !video || video.videoWidth <= 0 || video.videoHeight <= 0) return;
displayDrawArea(drawArea, video);
};
// 初始化 ResizeObserver 监听视频元素尺寸变化,以便在窗口调整大小时更新绘制区域位置和大小
const initVideoResizeObserver = () => {
if (!window.ResizeObserver || videoResizeObserver.value) return;
const video = monitorVideoRef.value;
if (!video) return;
videoResizeObserver.value = new ResizeObserver(() => {
updateDrawAreaOnResize();
});
videoResizeObserver.value.observe(video);
};
// 销毁 ResizeObserver
const destroyVideoResizeObserver = () => {
if (videoResizeObserver.value) {
videoResizeObserver.value.disconnect();
videoResizeObserver.value = null;
}
};
// 清理 visibilitychange 事件监听
const cleanupVisibilityChange = () => {
if (visibilityHandler) {
document.removeEventListener('visibilitychange', visibilityHandler);
visibilityHandler = null;
}
};
const handleMonitorDialogClose = (done: () => void) => {
console.log('idddddddddddd', form.value.id);
try {
addDemo(form.value.id).then(() => {
console.log('关闭视频111111');
});
} catch (error) {
console.log('关闭视频22222222', error);
}
// 卸载 Hls 与清理 video
destroyHls(monitorVideoRef);
monitorVideoUrl.value = '';
destroyVideoResizeObserver();
resetMonitorDraw();
cleanupVisibilityChange();
done();
//刷新列表
getList();
};
const handleMonitorDialogOk = async () => {
if (monitorDrawCollecting.value) {
proxy?.$modal.msgWarning('请先完成报警区域绘制');
return;
}
if (!currentDrawArea.value) {
proxy?.$modal.msgWarning('请先绘制报警区域');
return;
}
};
/** 显示坐标(相对 video 元素) → 视频原始像素坐标,含 object-fit: contain 留白换算 */
const displayToVideoPixel = (px: number, py: number, video: HTMLVideoElement): { vx: number; vy: number } | null => {
const dw = video.clientWidth;
const dh = video.clientHeight;
const VW = video.videoWidth;
const VH = video.videoHeight;
if (dw <= 0 || dh <= 0 || VW <= 0 || VH <= 0) return null;
const scale = Math.min(dw / VW, dh / VH);
const contentW = VW * scale;
const contentH = VH * scale;
const offsetX = (dw - contentW) / 2;
const offsetY = (dh - contentH) / 2;
const vx = Math.min(VW, Math.max(0, (px - offsetX) / scale));
const vy = Math.min(VH, Math.max(0, (py - offsetY) / scale));
return { vx, vy };
};
/** 开始采集:视频上点击两个对角点 */
const drawStart = ref(false);
const payload = ref<AlarmAreaData[]>([]);
const drawImage = () => {
resetMonitorDraw();
monitorDrawCollecting.value = true;
videoName.value = '绘制区域';
proxy?.$modal.msgSuccess('请在视频画面上连续点击两个点,生成报警区域');
};
const onMonitorDrawClick = (e: MouseEvent) => {
if (!monitorDrawCollecting.value) return;
const video = monitorVideoRef.value;
if (!video) {
proxy?.$modal.msgWarning('视频未就绪');
return;
}
const rect = video.getBoundingClientRect();
const dw = rect.width;
const dh = rect.height;
if (dw <= 0 || dh <= 0) {
proxy?.$modal.msgWarning('无法获取视频区域尺寸');
return;
}
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const p: MonitorPoint = { x, y };
monitorDrawPoints.value = [...monitorDrawPoints.value, p];
if (monitorDrawPoints.value.length < 2) return;
const [p1, p2] = monitorDrawPoints.value;
const VW = video.videoWidth;
const VH = video.videoHeight;
const v1 = displayToVideoPixel(p1.x, p1.y, video);
const v2 = displayToVideoPixel(p2.x, p2.y, video);
if (!v1 || !v2 || !VW || !VH) {
monitorDrawPoints.value = [];
proxy?.$modal.msgWarning('请等待视频加载完成后再绘制(需能读取原始宽高)');
return;
}
const left = Math.min(p1.x, p2.x);
const top = Math.min(p1.y, p2.y);
const width = Math.max(Math.abs(p2.x - p1.x), 1);
const height = Math.max(Math.abs(p2.y - p1.y), 1);
monitorRectPx.value = { left, top, width, height };
monitorDrawCollecting.value = false;
monitorDrawPoints.value = [];
// 计算两个点相对于video的千分比坐标
const point1RightPctOfWidth = ((VW - v1.vx) / VW) * 1000;
const point1TopPctOfHeight = (v1.vy / VH) * 1000;
const point2RightPctOfWidth = ((VW - v2.vx) / VW) * 1000;
const point2TopPctOfHeight = (v2.vy / VH) * 1000;
const clamp = (value: number) => Math.max(0, Math.min(1000, Math.round(value)));
const newArea: AlarmAreaData = {
deviceId: form.value.id,
x1: clamp(point1RightPctOfWidth),
y1: clamp(point1TopPctOfHeight),
x2: clamp(point2RightPctOfWidth),
y2: clamp(point2TopPctOfHeight)
};
payload.value.push(newArea);
currentDrawArea.value = newArea;
console.log('[监控区域] 两点千分比坐标', newArea);
console.log('[监控区域] 所有报警区域', payload.value);
seeSee.value = 1;
};
const clearImage = () => {
payload.value = [];
console.log('[清空区域] 当前报警区域列表', payload.value);
drawStart.value = false;
seeSee.value = 0;
// 清除绘制的矩形图案
monitorRectPx.value = null;
monitorDrawPoints.value = [];
monitorDrawCollecting.value = false; // 重置为不允许点击绘制状态
};
const saveImage = async () => {
console.log('[保存区域] 准备保存的报警区域列表', payload.value);
try {
const response = await drawInfo(payload.value);
proxy?.$modal.msgSuccess('保存成功');
console.log('[保存区域] 保存结果', response);
seeSee.value = 0;
dialog1.visible = false;
//重新获取数据
await getList();
} catch (error) {
proxy?.$modal.msgSuccess('保存成功, 请联系管理员');
console.error('[保存区域] 保存失败', error);
}
};
const handleMonitorDialogOpen = () => {
// 初始化 Hls 与 video
initHls(monitorVideoUrl, monitorVideoRef);
initVideoResizeObserver();
monitorDrawCollecting.value = false;
monitorDrawPoints.value = [];
monitorRectPx.value = null;
};
/** 删除按钮操作 */
const handleDelete = async (row?: DemoVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除测试单编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delDevice(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'demo/demo/export',
{
...queryParams.value
},
`demo_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
watch(
() => seeSee.value,
(newValue) => {
videoName.value = newValue === 1 ? '绘制区域' : '实时监控';
}
);
/** 刷新按钮操作 */
const handleRefresh = async () => {
if (!form.value.id) return;
destroyHls(monitorVideoRef);
monitorVideoUrl.value = '';
await videosStore.getVideos(form.value.id);
monitorVideoUrl.value = videosStore.hisUrl || '';
if (monitorVideoUrl.value) {
await nextTick();
initHls(monitorVideoUrl, monitorVideoRef);
}
};
/** 电池颜色:>50% 绿30-50% 黄,<30% 红 */
const getBatteryColor = (val: number) => {
if (val > 50) return '#52c41a';
if (val >= 30) return '#faad14';
return '#ff4d4f';
};
const formatPowerLevel = (val: any) => Number(val) || 0;
const getPowerLevelClass = (val: any) => {
const v = Number(val);
if (v > 50) return 'power--high';
if (v >= 30) return 'power--mid';
return 'power--low';
};
</script>
<style scoped lang="scss">
/* 全屏监控弹窗:占满视口、禁止外层滚动,视频在可视区内尽可能放大 */
:deep(.monitor-dialog.el-dialog) {
display: flex;
flex-direction: column;
height: 100vh;
max-height: 100vh;
margin: 0;
overflow: hidden;
}
:deep(.monitor-dialog .el-dialog__header) {
flex-shrink: 0;
margin-right: 0;
}
:deep(.monitor-dialog .el-dialog__body) {
flex: 1;
min-height: 0;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
.monitor-video-wrap {
flex: 1;
min-height: 0;
width: 100%;
display: flex;
align-items: stretch;
justify-content: center;
background: #000;
}
.monitor-video-stack {
position: relative;
flex: 1;
min-height: 0;
width: 100%;
}
.monitor-video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
object-fit: contain; //cover裁剪
background: #000;
}
.monitor-draw-overlay {
position: absolute;
inset: 0;
z-index: 2;
cursor: crosshair;
background: transparent;
}
.monitor-draw-rect {
position: absolute;
z-index: 3;
pointer-events: none;
box-sizing: border-box;
border: 2px dashed #2563eb;
background: rgba(37, 99, 235, 0.1);
}
.monitor-draw-label {
position: absolute;
z-index: 4;
pointer-events: none;
transform: translateX(-50%);
white-space: nowrap;
font-size: 12px;
color: #fff;
background: rgba(37, 99, 235, 0.85);
padding: 2px 6px;
border-radius: 2px;
}
/* 隐藏视频进度条,保留其他控制按钮 */
.monitor-video::-webkit-media-controls-timeline {
display: none !important;
}
.monitor-video::-webkit-media-controls-current-time-display,
.monitor-video::-webkit-media-controls-time-remaining-display {
display: none !important;
}
.monitor-video::-moz-range-track {
display: none !important;
}
.monitor-video::-moz-progress-bar {
display: none !important;
}
.battery-group {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.battery-item {
display: flex;
align-items: center;
gap: 2px;
}
.battery-label {
font-size: 12px;
color: #909399;
}
/* 电量电池颜色 */
.power--high {
color: #52c41a;
}
.power--mid {
color: #faad14;
}
.power--low {
color: #ff4d4f;
}
</style>