feat:设备通道详情
This commit is contained in:
1 parent
0297f0e0ce
commit
135823c995
9 files changed
+428
-17
No files matched your search
+2
-2
@@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>军航科技管理系统</title>
|
||||
<!-- 监控播放器样式 -->
|
||||
<link rel="stylesheet" href="/lib/WSPlayer/player.css">
|
||||
<link rel="stylesheet" href="/lib/WSPlayer/window.division.css">
|
||||
<!-- <link rel="stylesheet" href="/lib/WSPlayer/player.css">
|
||||
<link rel="stylesheet" href="/lib/WSPlayer/window.division.css"> -->
|
||||
</head>
|
||||
<style>
|
||||
html, body {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import request from "/@/utils/request"
|
||||
|
||||
// 云台方向控制
|
||||
export const ytOperateDirect = (data: any) => {
|
||||
return request({
|
||||
url: '/admin/third/camera/yt/operateDirect',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 云台镜头控制
|
||||
export const ytOperateCamera = (data: any) => {
|
||||
return request({
|
||||
url: '/admin/third/camera/yt/operateCamera',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 云台三维定位接口
|
||||
export const ytSitPosition = (data: any) => {
|
||||
return request({
|
||||
url: '/admin/third/camera/yt/sitPosition',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ let lastRealSignature = ""
|
||||
let lastRealStartAt = 0
|
||||
let lastPlaybackSignature = ""
|
||||
let lastPlaybackStartAt = 0
|
||||
let currentSnum = null
|
||||
let currentSnum = 0
|
||||
|
||||
const ytRef = ref()
|
||||
|
||||
@@ -739,7 +739,7 @@ function emitSetting() {
|
||||
}
|
||||
|
||||
function clickWindowCallback() {
|
||||
if (props.visibleYt && currentWindowType == 0) {
|
||||
if (props.visibleYt && currentWindowType.value == 0) {
|
||||
updateYtChannel()
|
||||
}
|
||||
}
|
||||
@@ -747,20 +747,42 @@ function clickWindowCallback() {
|
||||
/* ************************************* start 云台相关 start ************************************* */
|
||||
|
||||
function visibleYt() {
|
||||
console.log("visibleYt,currentSnum:", currentSnum);
|
||||
console.log("VIDEO_PLAYER:", VIDEO_PLAYER.setting);
|
||||
console.log("currentSnum:", currentSnum);
|
||||
if (currentSnum !== null && currentSnum !== undefined) {
|
||||
VIDEO_PLAYER.setting
|
||||
const channelList = VIDEO_PLAYER.setting.channelList
|
||||
const target = channelList.find((item) => item.snum === currentSnum)
|
||||
if (!target || !target?.cameraType || !target?.capability) {
|
||||
ytRef.value.open()
|
||||
return
|
||||
}
|
||||
ytState.title = target?.channelName ?? ""
|
||||
ytRef.value.showYt({
|
||||
id: target?.channelId,
|
||||
cameraType: target?.cameraType,
|
||||
capability: target?.capability,
|
||||
})
|
||||
} else {
|
||||
ytRef.value.open()
|
||||
}
|
||||
ytRef.value.open()
|
||||
}
|
||||
|
||||
const updateYtChannel = () => {
|
||||
if (!ytRef.value?.isVisible?.()) {
|
||||
if (!ytRef.value?.isVisible) {
|
||||
return
|
||||
}
|
||||
if (currentSnum !== null && currentSnum !== undefined) {
|
||||
ytState.title = ""
|
||||
ytRef.value.setChannel({})
|
||||
const channelList = VIDEO_PLAYER.setting.channelList
|
||||
const target = channelList.find((item) => item.snum === currentSnum)
|
||||
if (!target || !target?.cameraType || !target?.capability) {
|
||||
return
|
||||
}
|
||||
ytState.title = target?.channelName ?? ""
|
||||
ytRef.value.setChannel({
|
||||
id: target?.channelId,
|
||||
cameraType: target?.cameraType,
|
||||
capability: target?.capability,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<script setup>
|
||||
import { ref, defineExpose, onUnmounted, onMounted, onBeforeUnmount, defineProps, nextTick } from "vue";
|
||||
|
||||
import { ytApi } from "@/api/v2"
|
||||
import * as ytApi from "/@/api/device/yt"
|
||||
|
||||
import "./lib/panTilt.css"
|
||||
import PanTilt from './lib/panTilt';
|
||||
@@ -36,7 +36,7 @@ const props = defineProps({
|
||||
let YT = null
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const minSize = { width: 350, height: 290 }
|
||||
const minSize = { width: 350, height: 400 }
|
||||
const sizeRatio = minSize.width / minSize.height
|
||||
const size = ref({ width: minSize.width, height: minSize.height })
|
||||
const position = ref({ x: 100, y: 100 })
|
||||
@@ -273,10 +273,12 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.yt-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -357,7 +359,6 @@ onBeforeUnmount(() => {
|
||||
height: 14px;
|
||||
border-right: 2px solid rgba(255, 255, 255, 0.6);
|
||||
border-bottom: 2px solid rgba(255, 255, 255, 0.6);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.resize_se {
|
||||
|
||||
@@ -35,3 +35,79 @@ export function timeDiff(enterTime: string, exitTime: string) {
|
||||
if (diffMinutes === 0) return `${diffHours}小时`;
|
||||
return `${diffHours}小时${diffMinutes}分钟`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备通道能力位图(32 位,从右到左为第 1 位至第 32 位)
|
||||
* - 输入:支持以下任意格式
|
||||
* 1) 纯二进制字符串,例如 "1010..."; 最右侧为第 1 位
|
||||
* 2) 逗号分隔的 0/1,例如 "1,0,0,..."; 第 1 项为第 1 位
|
||||
* 3) 十进制字符串,例如 "12345";将自动转换为二进制位图
|
||||
* - 输出:返回启用能力对应的中文名称数组(仅包含位为 1 的能力)
|
||||
*/
|
||||
export function parseDeviceCapability(capability: string): string[] {
|
||||
if (!capability) return [];
|
||||
const labels: string[] = [
|
||||
"智能报警",
|
||||
"鱼眼",
|
||||
"电动聚焦",
|
||||
"红外测温",
|
||||
"热度图统计",
|
||||
"过线统计",
|
||||
"区域人数统计",
|
||||
"人脸检测",
|
||||
"人员识别",
|
||||
"目标抓拍",
|
||||
"主从跟踪",
|
||||
"出入口抓拍",
|
||||
"AR",
|
||||
"多区域人数统计",
|
||||
"雷球联动",
|
||||
"行为分析",
|
||||
"云台控制",
|
||||
"保留位",
|
||||
"扬声器",
|
||||
"智能算法升级",
|
||||
"MAC采集",
|
||||
"自定义定时抓图",
|
||||
"人脸开门",
|
||||
"工装检测",
|
||||
"人脸质量检测",
|
||||
"人脸相似度比对",
|
||||
"客流定时订阅",
|
||||
"人脸分析",
|
||||
"AR后端打标",
|
||||
"AR广角180°",
|
||||
"AR广角360°",
|
||||
"废弃位",
|
||||
];
|
||||
const trimmed = capability.trim();
|
||||
let getBitFromRight: (posFromRight: number) => number;
|
||||
if (trimmed.includes(",")) {
|
||||
const arr = trimmed.split(",").map((s) => s.trim());
|
||||
getBitFromRight = (posFromRight: number) => {
|
||||
const idx = posFromRight - 1;
|
||||
const v = arr[idx];
|
||||
return v === "1" ? 1 : 0;
|
||||
};
|
||||
} else if (/^[01]+$/.test(trimmed)) {
|
||||
getBitFromRight = (posFromRight: number) => {
|
||||
const idx = trimmed.length - posFromRight;
|
||||
if (idx < 0) return 0;
|
||||
return trimmed[idx] === "1" ? 1 : 0;
|
||||
};
|
||||
} else if (/^\d+$/.test(trimmed)) {
|
||||
const bin = BigInt(trimmed).toString(2);
|
||||
getBitFromRight = (posFromRight: number) => {
|
||||
const idx = bin.length - posFromRight;
|
||||
if (idx < 0) return 0;
|
||||
return bin[idx] === "1" ? 1 : 0;
|
||||
};
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
const result: string[] = [];
|
||||
for (let i = 1; i <= labels.length; i++) {
|
||||
if (getBitFromRight(i) === 1) result.push(labels[i - 1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -6,6 +6,9 @@
|
||||
<el-form-item style="width: 200px;" label="报警编号" prop="alarmCode">
|
||||
<el-input v-model="state.queryForm.alarmCode" placeholder="请输入报警编号" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px;" label="报警设备" prop="deviceName">
|
||||
<el-input v-model="state.queryForm.deviceName" placeholder="请输入报警设备名" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 350px;" label="报警类型" prop="alarmTypeList">
|
||||
<dict-select v-model="state.queryForm.alarmTypeList" :options="alarm_type" multiple filterable
|
||||
placeholder="请选择报警类型" />
|
||||
@@ -36,6 +39,7 @@
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column prop="alarmCode" width="180" label="报警编号" show-overflow-tooltip />
|
||||
<el-table-column prop="deviceName" width="180" label="报警设备" />
|
||||
<el-table-column prop="alarmDate" width="180" label="报警时间" />
|
||||
<el-table-column prop="alarmPosition" width="200" label="报警地点" show-overflow-tooltip />
|
||||
<el-table-column prop="alarmType" width="180" label="报警类型">
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="containner">
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle">
|
||||
<el-table-column type="index" label="#" width="80" />
|
||||
<el-table-column prop="channelCode" align="left" width="150" label="通道编号" />
|
||||
<el-table-column prop="channelName" align="left" width="250" label="通道名称" />
|
||||
<el-table-column prop="cameraType" align="left" width="100" label="摄像头类型">
|
||||
<template #default="{ row }">
|
||||
<dict-tag v-if="row.cameraType" :options="camera_type" :value="row.cameraType" />
|
||||
<el-tag type="danger" v-else>未知</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="channelName" align="left" label="通道能力">
|
||||
<template #default="{ row }">
|
||||
<div class="tag-container">
|
||||
<el-tag v-for="(v, i) in parseDeviceCapability(row.capability)" :key="i">
|
||||
{{ v }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="installLocation" align="left" width="300" label="当前所在位置">
|
||||
<template #default="{ row }">
|
||||
<div class="color-success" v-if="row.installLocation">{{ row.installLocation }}</div>
|
||||
<div class="color-danger" v-else>未安装</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
<div style="height: 15px;"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="deviceChannelList">
|
||||
import { ref, onMounted, reactive } from "vue"
|
||||
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table";
|
||||
|
||||
import * as DeviceChannelApi from "/@/api/device/deviceChannel";
|
||||
|
||||
import { useDict } from '/@/hooks/dict';
|
||||
import { parseDeviceCapability } from "/@/utils/dispose"
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
deviceCode: string
|
||||
}>(), {
|
||||
deviceCode: ""
|
||||
})
|
||||
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
createdIsNeed: false,
|
||||
queryForm: {
|
||||
installLocation: true
|
||||
},
|
||||
pageList: DeviceChannelApi.fetchList
|
||||
})
|
||||
|
||||
const { camera_type } = useDict("camera_type")
|
||||
|
||||
// table hook
|
||||
const {
|
||||
getDataList,
|
||||
currentChangeHandle,
|
||||
sizeChangeHandle,
|
||||
sortChangeHandle,
|
||||
downBlobFile,
|
||||
tableStyle
|
||||
} = useTable(state)
|
||||
|
||||
watch(
|
||||
() => props.deviceCode,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
state.queryForm.deviceCode = newVal
|
||||
getDataList()
|
||||
}
|
||||
}, { immediate: true }
|
||||
)
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tag-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.containner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="header">
|
||||
<div class="title">基本信息</div>
|
||||
<div class="info">
|
||||
<div class="info-item">
|
||||
<span>厂商:</span>
|
||||
<span>
|
||||
<dict-tag :options="dh_device_manufacturer" :value="state.deviceInfo.deviceManufacturer" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>设备类别:</span>
|
||||
<dict-tag :options="device_category" :value="state.deviceInfo.deviceCategory" />
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>是否在线:</span>
|
||||
<tag-online :isOnline="state.deviceInfo.isOnline == 1" />
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>设备编码:</span>
|
||||
<span>{{ state.deviceInfo.deviceCode }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>设备名称:</span>
|
||||
<span>{{ state.deviceInfo.deviceName }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>设备ip:</span>
|
||||
<span>{{ state.deviceInfo.deviceIp }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>设备端口:</span>
|
||||
<span>{{ state.deviceInfo.devicePort }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="main-tab">
|
||||
<el-tabs v-model="activeTab" class="demo-tabs">
|
||||
<el-tab-pane label="视频通道" name="CHANNELS"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="main-content">
|
||||
<device-channel-list :deviceCode="state?.deviceInfo?.deviceCode" v-if="activeTab == 'CHANNELS'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="deviceDetail">
|
||||
import { ref, onMounted, reactive } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
import { useDict } from '/@/hooks/dict';
|
||||
import * as DeviceManageApi from "/@/api/device/deviceManage";
|
||||
|
||||
const DeviceChannelList = defineAsyncComponent(() => import("./components/device-channer.vue"));
|
||||
|
||||
const { dh_device_manufacturer, device_category } = useDict("dh_device_manufacturer", "device_category")
|
||||
|
||||
const activeTab = ref("CHANNELS")
|
||||
|
||||
const state = reactive<any>({
|
||||
deviceId: null,
|
||||
deviceInfo: {}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const route = useRoute();
|
||||
state.deviceId = route.query.id;
|
||||
getDeviceDetail()
|
||||
})
|
||||
|
||||
// 获取设备详情
|
||||
const getDeviceDetail = async () => {
|
||||
if (!state.deviceId) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { data, code } = await DeviceManageApi.getObj(state.deviceId)
|
||||
if (code != 0) {
|
||||
throw new Error()
|
||||
}
|
||||
state.deviceInfo = data
|
||||
} catch (error) {
|
||||
console.log("获取设备详情失败", error);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.info {
|
||||
padding: 10px 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
>span:first-child {
|
||||
width: 40%;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
>span:last-child {
|
||||
flex: 1;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 4px;
|
||||
height: 15px;
|
||||
margin-right: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 20%;
|
||||
background: var(--el-color-white);
|
||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
padding: 0 20px;
|
||||
margin-top: 10px;
|
||||
background: var(--el-color-white);
|
||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
||||
border-radius: 4px;
|
||||
|
||||
&-tab {
|
||||
height: 10%;
|
||||
}
|
||||
|
||||
&-content {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -74,11 +74,16 @@
|
||||
{{ row.offlineReason }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="150">
|
||||
<el-table-column label="操作" fixed="right" width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 justify-items-center">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 justify-items-center">
|
||||
<el-button icon="InfoFilled" text type="primary" @click="toDetail(row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button icon="VideoPlay" text type="primary"
|
||||
@click="previewVideoRef.openDialog(row.id)">预览</el-button>
|
||||
@click="previewVideoRef.openDialog(row.id)">
|
||||
预览
|
||||
</el-button>
|
||||
<el-button icon="VideoCamera" text type="primary"
|
||||
@click="replayVideoDialogRef.openDialog('device', row.deviceCode)">
|
||||
回放
|
||||
@@ -109,6 +114,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemDeviceManage">
|
||||
import { useRouter } from "vue-router"
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table";
|
||||
import * as DeviceManageApi from "/@/api/device/deviceManage";
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message";
|
||||
@@ -118,6 +124,9 @@ import { useDict } from '/@/hooks/dict';
|
||||
const FormDialog = defineAsyncComponent(() => import("../components/deviceForm.vue"));
|
||||
const PreviewVideoDialog = defineAsyncComponent(() => import("../components/preview-video/preview-video-dialog.vue"));
|
||||
const ReplayVideoDialog = defineAsyncComponent(() => import("../components/replay-video/replay-video-dialog.vue"));
|
||||
|
||||
const { push } = useRouter()
|
||||
|
||||
// 定义查询字典
|
||||
const { device_category, is_sync, is_nline, dh_device_manufacturer } = useDict("device_category", "is_sync", "is_nline", "dh_device_manufacturer")
|
||||
// 定义变量内容
|
||||
@@ -168,6 +177,15 @@ const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
multiple.value = !objs.length;
|
||||
};
|
||||
|
||||
const toDetail = (row: any) => {
|
||||
push({
|
||||
path: "/device/detail/index",
|
||||
query: {
|
||||
id: row.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 树节点选择时
|
||||
const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepTree: boolean) => {
|
||||
state.queryForm.ownerCode = curClickedNode.id
|
||||
|
||||
Reference in new issue
Block a user