摄像头模块开发
This commit is contained in:
@@ -23,8 +23,10 @@
|
|||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"echarts": "5.6.0",
|
"echarts": "5.6.0",
|
||||||
"element-plus": "2.10.7",
|
"element-plus": "2.10.7",
|
||||||
|
"ezuikit-js": "^9.0.4",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"fuse.js": "6.6.2",
|
"fuse.js": "6.6.2",
|
||||||
|
"hls.js": "^1.6.13",
|
||||||
"js-beautify": "1.14.11",
|
"js-beautify": "1.14.11",
|
||||||
"js-cookie": "3.0.5",
|
"js-cookie": "3.0.5",
|
||||||
"jsencrypt": "3.3.2",
|
"jsencrypt": "3.3.2",
|
||||||
|
|||||||
62
src/api/worn/camera.js
Normal file
62
src/api/worn/camera.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询摄像头设备列表
|
||||||
|
export function listCamera(query) {
|
||||||
|
return request({
|
||||||
|
url: '/worn/camera/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询摄像头设备详情
|
||||||
|
export function getCamera(id) {
|
||||||
|
return request({
|
||||||
|
url: '/worn/camera/' + id,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增摄像头设备
|
||||||
|
export function addCamera(data) {
|
||||||
|
return request({
|
||||||
|
url: '/worn/camera',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改摄像头设备
|
||||||
|
export function updateCamera(data) {
|
||||||
|
return request({
|
||||||
|
url: '/worn/camera',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除摄像头设备
|
||||||
|
export function delCamera(id) {
|
||||||
|
return request({
|
||||||
|
url: '/worn/camera/' + id,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取摄像头播放地址
|
||||||
|
export function getCameraPlayUrl(deviceSerial) {
|
||||||
|
return request({
|
||||||
|
url: '/worn/camera/play',
|
||||||
|
method: 'get',
|
||||||
|
params: { deviceSerial }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取摄像头播放 token
|
||||||
|
export function getCameraPlayToken(deviceSerial) {
|
||||||
|
return request({
|
||||||
|
url: '/worn/camera/play/token',
|
||||||
|
method: 'get',
|
||||||
|
params: { deviceSerial }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -272,16 +272,18 @@ function handleWsMessage(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadHomeData() {
|
function loadHomeData() {
|
||||||
loadHomeStat()
|
return Promise.all([
|
||||||
loadDeviceStat()
|
loadHomeStat(),
|
||||||
loadAlarmStat()
|
loadDeviceStat(),
|
||||||
loadAlarmTrend()
|
loadAlarmStat(),
|
||||||
loadAlarmType()
|
loadAlarmTrend(),
|
||||||
loadProjectPage()
|
loadAlarmType(),
|
||||||
|
loadProjectPage()
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadHomeStat() {
|
function loadHomeStat() {
|
||||||
getHomeStat().then((res) => {
|
return getHomeStat().then((res) => {
|
||||||
const data = res.data || {}
|
const data = res.data || {}
|
||||||
overview.projectTotal = toNumber(data.projectCount, overview.projectTotal)
|
overview.projectTotal = toNumber(data.projectCount, overview.projectTotal)
|
||||||
overview.warehouseTotal = toNumber(data.warehouseCount, overview.warehouseTotal)
|
overview.warehouseTotal = toNumber(data.warehouseCount, overview.warehouseTotal)
|
||||||
@@ -289,7 +291,7 @@ function loadHomeStat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadDeviceStat() {
|
function loadDeviceStat() {
|
||||||
getHomeDeviceStat().then((res) => {
|
return getHomeDeviceStat().then((res) => {
|
||||||
const data = res.data || {}
|
const data = res.data || {}
|
||||||
overview.deviceTotal = toNumber(data.total, overview.deviceTotal)
|
overview.deviceTotal = toNumber(data.total, overview.deviceTotal)
|
||||||
overview.onlineCount = toNumber(data.online, overview.onlineCount)
|
overview.onlineCount = toNumber(data.online, overview.onlineCount)
|
||||||
@@ -299,7 +301,7 @@ function loadDeviceStat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadAlarmStat() {
|
function loadAlarmStat() {
|
||||||
getHomeAlarmStat().then((res) => {
|
return getHomeAlarmStat().then((res) => {
|
||||||
const data = res.data || {}
|
const data = res.data || {}
|
||||||
overview.alarmToday = toNumber(data.today, overview.alarmToday)
|
overview.alarmToday = toNumber(data.today, overview.alarmToday)
|
||||||
overview.alarmUnhandled = toNumber(data.unhandled, overview.alarmUnhandled)
|
overview.alarmUnhandled = toNumber(data.unhandled, overview.alarmUnhandled)
|
||||||
@@ -307,7 +309,7 @@ function loadAlarmStat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadAlarmTrend() {
|
function loadAlarmTrend() {
|
||||||
getHomeAlarmTrend().then((res) => {
|
return getHomeAlarmTrend().then((res) => {
|
||||||
const list = Array.isArray(res.data) ? res.data : []
|
const list = Array.isArray(res.data) ? res.data : []
|
||||||
const maxCount = Math.max(...list.map((item) => toNumber(item.count, 0)), 1)
|
const maxCount = Math.max(...list.map((item) => toNumber(item.count, 0)), 1)
|
||||||
|
|
||||||
@@ -319,14 +321,14 @@ function loadAlarmTrend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadAlarmType() {
|
function loadAlarmType() {
|
||||||
getHomeAlarmType().then((res) => {
|
return getHomeAlarmType().then((res) => {
|
||||||
const list = Array.isArray(res.data) ? res.data : []
|
const list = Array.isArray(res.data) ? res.data : []
|
||||||
exceptionStats.total = list.reduce((sum, item) => sum + toNumber(item.value, 0), 0)
|
exceptionStats.total = list.reduce((sum, item) => sum + toNumber(item.value, 0), 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadProjectPage() {
|
function loadProjectPage() {
|
||||||
getHomeProjectList().then((res) => {
|
return getHomeProjectList().then((res) => {
|
||||||
const list = Array.isArray(res.data) ? res.data : []
|
const list = Array.isArray(res.data) ? res.data : []
|
||||||
projectList.value = list.map(normalizeProject).sort(sortProjectWarehouse)
|
projectList.value = list.map(normalizeProject).sort(sortProjectWarehouse)
|
||||||
if (projectPage.value > projectPageTotal.value) {
|
if (projectPage.value > projectPageTotal.value) {
|
||||||
@@ -354,7 +356,8 @@ function openWarehouseDetail(item) {
|
|||||||
query: {
|
query: {
|
||||||
deptId: item.deptId,
|
deptId: item.deptId,
|
||||||
warehouseName: item.warehouseName,
|
warehouseName: item.warehouseName,
|
||||||
projectName: item.name
|
projectName: item.name,
|
||||||
|
deviceSerial: item.deviceSerial || ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -369,6 +372,7 @@ function normalizeProject(item, index) {
|
|||||||
deptId: item.deptId || item.warehouseId || item.id,
|
deptId: item.deptId || item.warehouseId || item.id,
|
||||||
name,
|
name,
|
||||||
warehouseName,
|
warehouseName,
|
||||||
|
deviceSerial: item.deviceSerial || '',
|
||||||
onlineRate: `${onlineRate}%`,
|
onlineRate: `${onlineRate}%`,
|
||||||
status: onlineRate >= 95 ? 'normal' : 'warning'
|
status: onlineRate >= 95 ? 'normal' : 'warning'
|
||||||
}
|
}
|
||||||
@@ -751,9 +755,7 @@ function pushRealtimeAlarm(event) {
|
|||||||
realtimeAlarms.value = realtimeAlarms.value.slice(0, 5)
|
realtimeAlarms.value = realtimeAlarms.value.slice(0, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
loadHomeData()
|
|
||||||
|
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
|
|
||||||
if (!token || token === 'undefined' || token === 'null' || token.length < 30) {
|
if (!token || token === 'undefined' || token === 'null' || token.length < 30) {
|
||||||
@@ -761,7 +763,12 @@ onMounted(() => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
connectWs(handleWsMessage)
|
try {
|
||||||
|
await loadHomeData()
|
||||||
|
connectWs(handleWsMessage)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Home] 棣栭〉鍒濆鍖栧け璐ワ紝璺宠繃 WebSocket 杩炴帴', error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|||||||
328
src/views/worn/camera/index.vue
Normal file
328
src/views/worn/camera/index.vue
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
|
||||||
|
<el-form-item label="所属部门ID" prop="deptId">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.deptId"
|
||||||
|
placeholder="请输入所属部门ID"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备序列号" prop="deviceSerial">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.deviceSerial"
|
||||||
|
placeholder="请输入设备序列号"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备名称" prop="deviceName">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.deviceName"
|
||||||
|
placeholder="请输入设备名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="布防状态" prop="defence">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.defence"
|
||||||
|
placeholder="请输入布防状态"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备IP" prop="netAddress">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.netAddress"
|
||||||
|
placeholder="请输入设备IP"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否删除" prop="isDelete">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.isDelete"
|
||||||
|
placeholder="请输入是否删除"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</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-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="Plus"
|
||||||
|
@click="handleAdd"
|
||||||
|
v-hasPermi="['worn:camera:add']"
|
||||||
|
>新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
icon="Edit"
|
||||||
|
:disabled="single"
|
||||||
|
@click="handleUpdate"
|
||||||
|
v-hasPermi="['worn:camera:edit']"
|
||||||
|
>修改</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
icon="Delete"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleDelete"
|
||||||
|
v-hasPermi="['worn:camera:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="Download"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['worn:camera:export']"
|
||||||
|
>导出</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
plain
|
||||||
|
icon="Refresh"
|
||||||
|
@click="handleSyncEzviz"
|
||||||
|
>同步萤石设备</el-button>
|
||||||
|
</el-col>
|
||||||
|
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="cameraList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column label="主键" align="center" prop="id" />
|
||||||
|
<el-table-column label="所属部门ID" align="center" prop="deptId" />
|
||||||
|
<el-table-column label="设备序列号" align="center" prop="deviceSerial" />
|
||||||
|
<el-table-column label="设备名称" align="center" prop="deviceName" />
|
||||||
|
<el-table-column label="设备型号" align="center" prop="deviceType" />
|
||||||
|
<el-table-column label="设备状态" align="center" prop="status" />
|
||||||
|
<el-table-column label="布防状态" align="center" prop="defence" />
|
||||||
|
<el-table-column label="设备IP" align="center" prop="netAddress" />
|
||||||
|
<el-table-column label="备注" align="center" prop="remark" />
|
||||||
|
<el-table-column label="是否删除" align="center" prop="isDelete" />
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['worn:camera:edit']">修改</el-button>
|
||||||
|
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['worn:camera:remove']">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="total>0"
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.pageNum"
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 添加或修改摄像头设备对话框 -->
|
||||||
|
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
|
||||||
|
<el-form ref="cameraRef" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item label="所属部门ID" prop="deptId">
|
||||||
|
<el-input v-model="form.deptId" placeholder="请输入所属部门ID" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备序列号" prop="deviceSerial">
|
||||||
|
<el-input v-model="form.deviceSerial" placeholder="请输入设备序列号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备名称" prop="deviceName">
|
||||||
|
<el-input v-model="form.deviceName" placeholder="请输入设备名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="布防状态" prop="defence">
|
||||||
|
<el-input v-model="form.defence" placeholder="请输入布防状态" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备IP" prop="netAddress">
|
||||||
|
<el-input v-model="form.netAddress" placeholder="请输入设备IP" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否删除" prop="isDelete">
|
||||||
|
<el-input v-model="form.isDelete" placeholder="请输入是否删除" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="Camera">
|
||||||
|
import { listCamera, getCamera, delCamera, addCamera, updateCamera } from "@/api/worn/camera"
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
|
const cameraList = ref([])
|
||||||
|
const open = ref(false)
|
||||||
|
const loading = ref(true)
|
||||||
|
const showSearch = ref(true)
|
||||||
|
const ids = ref([])
|
||||||
|
const single = ref(true)
|
||||||
|
const multiple = ref(true)
|
||||||
|
const total = ref(0)
|
||||||
|
const title = ref("")
|
||||||
|
|
||||||
|
const data = reactive({
|
||||||
|
form: {},
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
deptId: null,
|
||||||
|
deviceSerial: null,
|
||||||
|
deviceName: null,
|
||||||
|
deviceType: null,
|
||||||
|
status: null,
|
||||||
|
defence: null,
|
||||||
|
netAddress: null,
|
||||||
|
isDelete: null
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
deviceSerial: [
|
||||||
|
{ required: true, message: "设备序列号不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { queryParams, form, rules } = toRefs(data)
|
||||||
|
|
||||||
|
/** 查询摄像头设备列表 */
|
||||||
|
function getList() {
|
||||||
|
loading.value = true
|
||||||
|
listCamera(queryParams.value).then(response => {
|
||||||
|
cameraList.value = response.rows
|
||||||
|
total.value = response.total
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消按钮
|
||||||
|
function cancel() {
|
||||||
|
open.value = false
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单重置
|
||||||
|
function reset() {
|
||||||
|
form.value = {
|
||||||
|
id: null,
|
||||||
|
deptId: null,
|
||||||
|
deviceSerial: null,
|
||||||
|
deviceName: null,
|
||||||
|
deviceType: null,
|
||||||
|
status: null,
|
||||||
|
defence: null,
|
||||||
|
netAddress: null,
|
||||||
|
createBy: null,
|
||||||
|
createTime: null,
|
||||||
|
updateBy: null,
|
||||||
|
updateTime: null,
|
||||||
|
remark: null,
|
||||||
|
isDelete: null
|
||||||
|
}
|
||||||
|
proxy.resetForm("cameraRef")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
function handleQuery() {
|
||||||
|
queryParams.value.pageNum = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
function resetQuery() {
|
||||||
|
proxy.resetForm("queryRef")
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多选框选中数据
|
||||||
|
function handleSelectionChange(selection) {
|
||||||
|
ids.value = selection.map(item => item.id)
|
||||||
|
single.value = selection.length != 1
|
||||||
|
multiple.value = !selection.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增按钮操作 */
|
||||||
|
function handleAdd() {
|
||||||
|
reset()
|
||||||
|
open.value = true
|
||||||
|
title.value = "添加摄像头设备"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改按钮操作 */
|
||||||
|
function handleUpdate(row) {
|
||||||
|
reset()
|
||||||
|
const _id = row.id || ids.value
|
||||||
|
getCamera(_id).then(response => {
|
||||||
|
form.value = response.data
|
||||||
|
open.value = true
|
||||||
|
title.value = "修改摄像头设备"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交按钮 */
|
||||||
|
function submitForm() {
|
||||||
|
proxy.$refs["cameraRef"].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
if (form.value.id != null) {
|
||||||
|
updateCamera(form.value).then(response => {
|
||||||
|
proxy.$modal.msgSuccess("修改成功")
|
||||||
|
open.value = false
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
addCamera(form.value).then(response => {
|
||||||
|
proxy.$modal.msgSuccess("新增成功")
|
||||||
|
open.value = false
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除按钮操作 */
|
||||||
|
function handleDelete(row) {
|
||||||
|
const _ids = row.id || ids.value
|
||||||
|
proxy.$modal.confirm('是否确认删除摄像头设备编号为"' + _ids + '"的数据项?').then(function() {
|
||||||
|
return delCamera(_ids)
|
||||||
|
}).then(() => {
|
||||||
|
getList()
|
||||||
|
proxy.$modal.msgSuccess("删除成功")
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出按钮操作 */
|
||||||
|
function handleExport() {
|
||||||
|
proxy.download('worn/camera/export', {
|
||||||
|
...queryParams.value
|
||||||
|
}, `camera_${new Date().getTime()}.xlsx`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步萤石设备 */
|
||||||
|
function handleSyncEzviz() {
|
||||||
|
proxy.$modal.msgSuccess("已触发同步萤石设备")
|
||||||
|
}
|
||||||
|
|
||||||
|
getList()
|
||||||
|
</script>
|
||||||
177
src/views/worn/warehouseDashboard/components/CameraPlayer.vue
Normal file
177
src/views/worn/warehouseDashboard/components/CameraPlayer.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<div class="camera-player">
|
||||||
|
<div
|
||||||
|
:id="playerId"
|
||||||
|
ref="playerShellRef"
|
||||||
|
class="camera-player-shell"
|
||||||
|
@click="handleShellClick"
|
||||||
|
>
|
||||||
|
<div v-if="!ready" class="camera-player-placeholder">
|
||||||
|
<strong>{{ loading ? '正在连接摄像头...' : '点击开始播放摄像头画面' }}</strong>
|
||||||
|
<span>{{ error || statusText }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onBeforeUnmount, nextTick, watch } from 'vue'
|
||||||
|
import EZUIKit from 'ezuikit-js'
|
||||||
|
import { getCameraPlayToken } from '@/api/worn/camera'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
deviceSerial: String
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['state-change'])
|
||||||
|
|
||||||
|
const playerShellRef = ref(null)
|
||||||
|
const playerId = `player-${Date.now()}`
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const ready = ref(false)
|
||||||
|
const playing = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const statusText = ref('等待播放')
|
||||||
|
|
||||||
|
let playerInstance = null
|
||||||
|
|
||||||
|
async function getToken() {
|
||||||
|
const res = await getCameraPlayToken(props.deviceSerial)
|
||||||
|
const data = res.data || {}
|
||||||
|
|
||||||
|
if (!data.accessToken) {
|
||||||
|
throw new Error('未获取到accessToken')
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.accessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
async function play() {
|
||||||
|
if (!props.deviceSerial || loading.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
statusText.value = '获取播放授权...'
|
||||||
|
emitState()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await getToken()
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
destroy()
|
||||||
|
|
||||||
|
statusText.value = '连接摄像头...'
|
||||||
|
emitState()
|
||||||
|
|
||||||
|
const shell = playerShellRef.value
|
||||||
|
const rect = shell.getBoundingClientRect()
|
||||||
|
|
||||||
|
playerInstance = new EZUIKit.EZUIKitPlayer({
|
||||||
|
id: playerId,
|
||||||
|
width: rect.width || 600,
|
||||||
|
height: rect.height || 400,
|
||||||
|
template: 'pcLive',
|
||||||
|
url: `ezopen://open.ys7.com/${props.deviceSerial}/1.live`,
|
||||||
|
accessToken: token,
|
||||||
|
autoplay: true,
|
||||||
|
|
||||||
|
handleSuccess: () => {
|
||||||
|
loading.value = false
|
||||||
|
ready.value = true
|
||||||
|
playing.value = true
|
||||||
|
statusText.value = '播放中'
|
||||||
|
emitState()
|
||||||
|
},
|
||||||
|
|
||||||
|
handleError: (err) => {
|
||||||
|
console.error('播放失败:', err)
|
||||||
|
loading.value = false
|
||||||
|
ready.value = false
|
||||||
|
playing.value = false
|
||||||
|
error.value = '摄像头播放失败'
|
||||||
|
statusText.value = '播放失败'
|
||||||
|
emitState()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('异常:', e)
|
||||||
|
loading.value = false
|
||||||
|
ready.value = false
|
||||||
|
playing.value = false
|
||||||
|
error.value = e.message
|
||||||
|
statusText.value = '播放异常'
|
||||||
|
emitState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitState() {
|
||||||
|
emit('state-change', {
|
||||||
|
loading: loading.value,
|
||||||
|
ready: ready.value,
|
||||||
|
playing: playing.value,
|
||||||
|
error: error.value,
|
||||||
|
statusText: statusText.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy() {
|
||||||
|
if (playerInstance) {
|
||||||
|
try { playerInstance.stop?.() } catch {}
|
||||||
|
try { playerInstance.destroy?.() } catch {}
|
||||||
|
playerInstance = null
|
||||||
|
}
|
||||||
|
ready.value = false
|
||||||
|
playing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleShellClick() {
|
||||||
|
if (!ready.value && !loading.value) {
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.deviceSerial, () => {
|
||||||
|
destroy()
|
||||||
|
error.value = ''
|
||||||
|
statusText.value = '点击开始播放摄像头'
|
||||||
|
emitState()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
play,
|
||||||
|
destroy
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.camera-player {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-player-shell {
|
||||||
|
height: 260px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #000;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-player-placeholder {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-loading="loading" class="sensor-grid">
|
<section v-loading="loading" class="sensor-grid">
|
||||||
<div v-if="!loading && sensorCards.length === 0" class="empty-state">
|
<div v-if="!loading && sensorCards.length === 0 && !hasCameraCard" class="empty-state">
|
||||||
当前仓库暂无传感器设备
|
当前仓库暂无传感器设备
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -123,12 +123,60 @@
|
|||||||
<span>{{ item.reportTime || '暂无上报' }}</span>
|
<span>{{ item.reportTime || '暂无上报' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article v-if="hasCameraCard" :class="['sensor-card', 'camera-card', cameraReady ? 'camera-active' : '']">
|
||||||
|
<div class="sensor-content">
|
||||||
|
<div class="sensor-main">
|
||||||
|
<div class="sensor-top">
|
||||||
|
<div class="sensor-icon camera">
|
||||||
|
<span>监</span>
|
||||||
|
</div>
|
||||||
|
<div class="sensor-state">
|
||||||
|
<i></i>
|
||||||
|
{{ cameraReady ? '播放中' : (cameraLoading ? '加载中' : '已绑定') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sensor-body">
|
||||||
|
<p>{{ warehouseName || '仓库摄像头' }}</p>
|
||||||
|
<h2>仓库摄像头</h2>
|
||||||
|
<strong>{{ cameraSummaryText }}</strong>
|
||||||
|
<div class="socket-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="control-btn control-on"
|
||||||
|
:disabled="cameraLoading"
|
||||||
|
@click="handleCameraPlay"
|
||||||
|
>
|
||||||
|
{{ cameraLoading ? '加载中...' : (cameraPlaying ? '重新播放' : '开始播放') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metric-panel camera-panel">
|
||||||
|
<p>实时画面</p>
|
||||||
|
<CameraPlayer
|
||||||
|
ref="cameraPlayerRef"
|
||||||
|
:device-serial="cameraDeviceSerial"
|
||||||
|
@state-change="handleCameraStateChange"
|
||||||
|
/>
|
||||||
|
<div v-if="cameraError" class="camera-error">{{ cameraError }}</div>
|
||||||
|
<div v-else-if="cameraExpireTime" class="camera-expire">地址有效期至 {{ cameraExpireTime }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sensor-footer">
|
||||||
|
<span>DeviceSerial:{{ cameraDeviceSerial }}</span>
|
||||||
|
<span>{{ cameraReady ? '视频已开始播放' : (cameraLoading ? '正在建立视频连接' : cameraStatusText) }}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup name="WarehouseDashboard">
|
<script setup name="WarehouseDashboard">
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { listMqttDevice } from '@/api/worn/mqttDevice'
|
import { listMqttDevice } from '@/api/worn/mqttDevice'
|
||||||
@@ -136,6 +184,7 @@ import { listMqttData } from '@/api/worn/mqttData'
|
|||||||
import { listMqttEvent } from '@/api/worn/mqttEvent'
|
import { listMqttEvent } from '@/api/worn/mqttEvent'
|
||||||
import { controlLightSwitch, controlSocket } from '@/api/worn/socket'
|
import { controlLightSwitch, controlSocket } from '@/api/worn/socket'
|
||||||
import { closeWs, connectWs } from '@/utils/ws'
|
import { closeWs, connectWs } from '@/utils/ws'
|
||||||
|
import CameraPlayer from './components/CameraPlayer.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -145,10 +194,31 @@ const devices = ref([])
|
|||||||
const latestDataMap = reactive({})
|
const latestDataMap = reactive({})
|
||||||
const controlLoadingMap = reactive({})
|
const controlLoadingMap = reactive({})
|
||||||
const ENV_CARD_TYPES = ['tempHum', 'toxicGas', 'hydrogenSulfide']
|
const ENV_CARD_TYPES = ['tempHum', 'toxicGas', 'hydrogenSulfide']
|
||||||
|
const cameraPlayerRef = ref(null)
|
||||||
|
const cameraLoading = ref(false)
|
||||||
|
const cameraPlaying = ref(false)
|
||||||
|
const cameraExpireTime = ref('')
|
||||||
|
const cameraError = ref('')
|
||||||
|
const cameraReady = ref(false)
|
||||||
|
const cameraStatusText = ref('点击开始播放摄像头')
|
||||||
|
|
||||||
const deptId = computed(() => route.query.deptId)
|
const deptId = computed(() => route.query.deptId)
|
||||||
const projectName = computed(() => route.query.projectName || '')
|
const projectName = computed(() => route.query.projectName || '')
|
||||||
const warehouseName = computed(() => route.query.warehouseName || '')
|
const warehouseName = computed(() => route.query.warehouseName || '')
|
||||||
|
const cameraDeviceSerial = computed(() => String(route.query.deviceSerial || '').trim())
|
||||||
|
const hasCameraCard = computed(() => !!cameraDeviceSerial.value)
|
||||||
|
const cameraSummaryText = computed(() => {
|
||||||
|
if (cameraError.value) {
|
||||||
|
return '播放失败,请重新尝试'
|
||||||
|
}
|
||||||
|
if (cameraLoading.value) {
|
||||||
|
return '正在获取摄像头视频流'
|
||||||
|
}
|
||||||
|
if (cameraReady.value || cameraPlaying.value) {
|
||||||
|
return '摄像头画面播放中'
|
||||||
|
}
|
||||||
|
return '点击卡片开始播放摄像头画面'
|
||||||
|
})
|
||||||
|
|
||||||
const sensorCards = computed(() => devices.value.flatMap((device) => expandDeviceCards(device)))
|
const sensorCards = computed(() => devices.value.flatMap((device) => expandDeviceCards(device)))
|
||||||
const onlineCount = computed(() => sensorCards.value.filter((item) => item.isOnline).length)
|
const onlineCount = computed(() => sensorCards.value.filter((item) => item.isOnline).length)
|
||||||
@@ -157,11 +227,43 @@ const latestReportTime = computed(() => {
|
|||||||
const times = sensorCards.value.map((item) => item.reportTime).filter(Boolean)
|
const times = sensorCards.value.map((item) => item.reportTime).filter(Boolean)
|
||||||
return times[0] || ''
|
return times[0] || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
router.push('/index')
|
router.push('/index')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCameraPlay() {
|
||||||
|
if (!cameraDeviceSerial.value || cameraLoading.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraPlayerRef.value?.play?.().catch((error) => {
|
||||||
|
const message = error?.message || '摄像头播放失败'
|
||||||
|
cameraError.value = message
|
||||||
|
cameraStatusText.value = message
|
||||||
|
ElMessage.error(message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCameraFullscreen() {
|
||||||
|
cameraPlayerRef.value?.fullscreen?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCameraStateChange(state = {}) {
|
||||||
|
cameraLoading.value = !!state.loading
|
||||||
|
cameraReady.value = !!state.ready
|
||||||
|
cameraPlaying.value = !!state.playing
|
||||||
|
cameraError.value = state.error || ''
|
||||||
|
cameraExpireTime.value = state.expireTime || ''
|
||||||
|
cameraStatusText.value = state.statusText || '点击开始播放摄像头'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCameraState() {
|
||||||
|
cameraLoading.value = false
|
||||||
|
cameraReady.value = false
|
||||||
|
cameraPlaying.value = false
|
||||||
|
cameraStatusText.value = '点击开始播放摄像头'
|
||||||
|
}
|
||||||
|
|
||||||
function loadDevices() {
|
function loadDevices() {
|
||||||
if (!deptId.value) {
|
if (!deptId.value) {
|
||||||
devices.value = []
|
devices.value = []
|
||||||
@@ -893,8 +995,15 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
cameraPlayerRef.value?.destroy?.()
|
||||||
closeWs(handleWsMessage)
|
closeWs(handleWsMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(cameraDeviceSerial, () => {
|
||||||
|
resetCameraState()
|
||||||
|
cameraPlayerRef.value?.destroy?.()
|
||||||
|
cameraStatusText.value = '点击开始播放摄像头'
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -1139,6 +1248,7 @@ onBeforeUnmount(() => {
|
|||||||
&.tempHum { color: #0d9488; background: linear-gradient(135deg, #ccfbf1, #f0fdfa); border-color: #99f6e4; }
|
&.tempHum { color: #0d9488; background: linear-gradient(135deg, #ccfbf1, #f0fdfa); border-color: #99f6e4; }
|
||||||
&.toxicGas { color: #ca8a04; background: linear-gradient(135deg, #fef9c3, #fffbeb); border-color: #fde68a; }
|
&.toxicGas { color: #ca8a04; background: linear-gradient(135deg, #fef9c3, #fffbeb); border-color: #fde68a; }
|
||||||
&.hydrogenSulfide { color: #7c3aed; background: linear-gradient(135deg, #ede9fe, #faf5ff); border-color: #ddd6fe; }
|
&.hydrogenSulfide { color: #7c3aed; background: linear-gradient(135deg, #ede9fe, #faf5ff); border-color: #ddd6fe; }
|
||||||
|
&.camera { color: #2563eb; background: linear-gradient(135deg, #dbeafe, #eff6ff); border-color: #bfdbfe; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.sensor-state {
|
.sensor-state {
|
||||||
@@ -1322,6 +1432,84 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.camera-card {
|
||||||
|
.sensor-state {
|
||||||
|
color: #2563eb;
|
||||||
|
background: rgba(219, 234, 254, 0.86);
|
||||||
|
|
||||||
|
i {
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.camera-active .sensor-state {
|
||||||
|
color: #059669;
|
||||||
|
background: rgba(220, 252, 231, 0.86);
|
||||||
|
|
||||||
|
i {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-panel {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-stage {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 238px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
background:
|
||||||
|
linear-gradient(145deg, rgba(15, 23, 42, 0.92), rgba(30, 41, 59, 0.95)),
|
||||||
|
radial-gradient(circle at 15% 20%, rgba(59, 130, 246, 0.28), transparent 28%);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-placeholder {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 18px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(226, 232, 240, 0.78);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 238px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #020617;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-expire,
|
||||||
|
.camera-error {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-expire {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-error {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
.metric-list {
|
.metric-list {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user