Files
Visualize-electronic-fences/src/views/equipmentList/list.vue
Ironsp 12a8ee4991 feat equipmentList
Co-authored-by: Copilot <copilot@github.com>
2026-05-21 17:53:46 +08:00

555 lines
19 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-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="status" width="100">
<template #default="scope">
<span v-if="scope.row.status === 1 || scope.row.status === '1'">在线</span>
<span v-else-if="scope.row.status === 2 || scope.row.status === '2'">离线</span>
<span v-else>{{ scope.row.status ?? '-' }}</span>
</template>
</el-table-column>
<el-table-column label="设备型号" align="center" prop="model" />
<el-table-column label="固件版本" align="center" prop="id" />
<el-table-column label="是否绘制区域" align="center" prop="isDrawArea">
<template #default="scope">
<span v-if="scope.row.isDrawArea === 1 || scope.row.isDrawArea === '1'"></span>
<span v-else-if="scope.row.isDrawArea === 0 || scope.row.isDrawArea === '0'"></span>
<span v-else>{{ scope.row.isDrawArea ?? '-' }}</span>
</template>
</el-table-column>
<el-table-column label="绑定时间" align="center" prop="isDelete" />
<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 type="primary" @click="handleSee(scope.row)">查看监控</el-button>
<el-button :disabled="scope.row.status === 1 || scope.row.status === '1'" type="primary" @click="handleActivate(scope.row)"
>唤醒设备</el-button
>
<el-button 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">实时监控-北门出入门监控</span>
<div>
<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 } from '@/api/equipmentList/types';
import { listDemo, getDemo, delDemo, addDemo, updateDemo } from '@/api/demo/demo';
import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types';
import { log } from 'node:console';
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 queryFormRef = ref<ElFormInstance>();
const demoFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const dialog1 = reactive<DialogOption>({
visible: false,
title: ''
});
const monitorVideoUrl = 'http://192.168.0.214:8088/5a0db047943df198aff98b650ac9e001.mp4';
const monitorVideoRef = ref<HTMLVideoElement>();
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 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;
};
const handleSee = async (row?: any) => {
reset();
resetMonitorDraw();
seeSee.value = row?.isDrawArea === 1 ? 1 : 0;
const _id = row?.id || ids.value[0];
const res = await getDeviceInfo(_id);
Object.assign(form.value, res.data);
dialog1.visible = true;
dialog1.title = '修改设备';
};
const handleMonitorDialogClose = (done: () => void) => {
monitorVideoRef.value?.pause();
monitorVideoRef.value && (monitorVideoRef.value.currentTime = 0);
resetMonitorDraw();
done();
};
/** 显示坐标(相对 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();
const drawImage = () => {
resetMonitorDraw();
monitorDrawCollecting.value = true;
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 = [];
const minVx = Math.min(v1.vx, v2.vx);
const maxVx = Math.max(v1.vx, v2.vx);
const minVy = Math.min(v1.vy, v2.vy);
const maxVy = Math.max(v1.vy, v2.vy);
const rw = Math.max(maxVx - minVx, 1);
const rh = Math.max(maxVy - minVy, 1);
const firstTopPctOfHeight = (v1.vy / VH) * 100;
const firstRightPctOfWidth = ((VW - v1.vx) / VW) * 100;
const rectWidthPctOfWidth = (rw / VW) * 100;
const rectHeightPctOfHeight = (rh / VH) * 100;
payload.value = {
// videoIntrinsicSize: { width: VW, height: VH },
// firstPointTopOffset_pctOfVideoHeight: Number(firstTopPctOfHeight.toFixed(4)),
// firstPointDistanceToRight_pctOfVideoWidth: Number(firstRightPctOfWidth.toFixed(4)),
// rectWidth_pctOfVideoWidth: Number(rectWidthPctOfWidth.toFixed(4)),
// rectHeight_pctOfVideoHeight: Number(rectHeightPctOfHeight.toFixed(4))
x: Number(firstRightPctOfWidth.toFixed(4)),
y: Number(firstTopPctOfHeight.toFixed(4)),
w: Number(rectWidthPctOfWidth.toFixed(4)),
h: Number(rectHeightPctOfHeight.toFixed(4))
};
console.log('[监控区域] 四点百分比', payload.value);
//proxy?.$modal.msgSuccess(`已采集(相对原视频)x右距${payload.value.x}% y顶距${payload.value.y}% w框宽${payload.value.w}% h框高${payload.value.h}%`);
seeSee.value = 1;
};
const clearImage = () => {
payload.value = {};
console.log(payload.value);
drawStart.value = false;
seeSee.value = 0;
};
const saveImage = async () => {
await drawInfo(payload.value);
drawStart.value = false;
seeSee.value = 0;
};
/** 删除按钮操作 */
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();
});
</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: translate(-50%, -100%);
margin-top: -6px;
color: #2563eb;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
}
</style>