2026-04-05 23:25:01 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="monitor-container">
|
|
|
|
|
|
<text class="header">实时监控</text>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 这里使用我们封装好的原生插件组件 ry-ipc-video -->
|
|
|
|
|
|
<view class="video-wrapper">
|
|
|
|
|
|
<!-- #ifdef APP-PLUS -->
|
|
|
|
|
|
<ry-ipc-video
|
|
|
|
|
|
ref="ipcVideo"
|
|
|
|
|
|
class="ipc-video"
|
|
|
|
|
|
:uid="deviceUid"
|
2026-04-06 15:14:46 +08:00
|
|
|
|
:channel="currentChannel"
|
|
|
|
|
|
:streamType="currentStreamType"
|
|
|
|
|
|
:isNvr="isNvr"
|
2026-04-05 23:25:01 +08:00
|
|
|
|
@onStatus="onVideoStatusChange">
|
|
|
|
|
|
</ry-ipc-video>
|
|
|
|
|
|
<!-- #endif -->
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 非App端提示 -->
|
|
|
|
|
|
<!-- #ifndef APP-PLUS -->
|
|
|
|
|
|
<view class="not-support-tips">
|
|
|
|
|
|
<text class="not-support-tips-text">当前环境不支持查看监控,请使用App</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<!-- #endif -->
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="status-text-wrap" v-if="statusMsg">
|
|
|
|
|
|
<text class="status-text">状态:{{ statusMsg }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-04-06 15:14:46 +08:00
|
|
|
|
<!-- 设置区域 -->
|
|
|
|
|
|
<view class="settings-panel">
|
|
|
|
|
|
<view class="setting-row">
|
|
|
|
|
|
<text class="setting-label">设备类型:</text>
|
|
|
|
|
|
<view class="options-group">
|
|
|
|
|
|
<text class="option-btn" :class="!isNvr ? 'active-btn' : ''" @click="changeNvr(false)">单体(IPC)</text>
|
|
|
|
|
|
<text class="option-btn" :class="isNvr ? 'active-btn' : ''" @click="changeNvr(true)">主机(NVR)</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="setting-row" v-if="isNvr">
|
|
|
|
|
|
<text class="setting-label">通道:</text>
|
|
|
|
|
|
<view class="options-group">
|
|
|
|
|
|
<text class="option-btn" :class="currentChannel === 0 ? 'active-btn' : ''" @click="changeChannel(0)">CH1</text>
|
|
|
|
|
|
<text class="option-btn" :class="currentChannel === 1 ? 'active-btn' : ''" @click="changeChannel(1)">CH2</text>
|
|
|
|
|
|
<text class="option-btn" :class="currentChannel === 2 ? 'active-btn' : ''" @click="changeChannel(2)">CH3</text>
|
|
|
|
|
|
<text class="option-btn" :class="currentChannel === 3 ? 'active-btn' : ''" @click="changeChannel(3)">CH4</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="setting-row">
|
|
|
|
|
|
<text class="setting-label">清晰度:</text>
|
|
|
|
|
|
<view class="options-group">
|
|
|
|
|
|
<text class="option-btn" :class="currentStreamType === 1 ? 'active-btn' : ''" @click="changeStream(1)">高清</text>
|
|
|
|
|
|
<text class="option-btn" :class="currentStreamType === 2 ? 'active-btn' : ''" @click="changeStream(2)">标清</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-04-05 23:25:01 +08:00
|
|
|
|
<view class="controls">
|
|
|
|
|
|
<button class="btn btn-primary" @click="startPlay"><text class="btn-text">播放</text></button>
|
|
|
|
|
|
<button class="btn btn-warn" @click="stopPlay"><text class="btn-text">停止</text></button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
export default {
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
2026-04-06 16:07:18 +08:00
|
|
|
|
deviceUid: 'HLTY036190SYNSK', // 默认使用你 Demo 里的测试 UID
|
2026-04-05 23:25:01 +08:00
|
|
|
|
isPlaying: false,
|
2026-04-06 15:14:46 +08:00
|
|
|
|
statusMsg: '等待操作',
|
|
|
|
|
|
currentChannel: 0, // 0对应CH1, 1对应CH2...
|
|
|
|
|
|
currentStreamType: 1, // 1为高清(主码流),2为标清(子码流)
|
2026-04-06 16:07:18 +08:00
|
|
|
|
isNvr: true // 是否为 NVR 设备
|
2026-04-05 23:25:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onLoad(options) {
|
|
|
|
|
|
if (options && options.uid) {
|
|
|
|
|
|
this.deviceUid = options.uid;
|
|
|
|
|
|
}
|
2026-04-06 15:14:46 +08:00
|
|
|
|
// 如果上个页面传了 isNvr 参数,这里可以接收
|
|
|
|
|
|
if (options && typeof options.isNvr !== 'undefined') {
|
|
|
|
|
|
// options.isNvr 通常传过来是字符串 'true' 或 'false'
|
|
|
|
|
|
this.isNvr = options.isNvr === 'true';
|
|
|
|
|
|
}
|
2026-04-05 23:25:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
onUnload() {
|
|
|
|
|
|
// 页面卸载时停止播放,释放资源
|
|
|
|
|
|
this.stopPlay();
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2026-04-06 15:14:46 +08:00
|
|
|
|
changeNvr(isNvrFlag) {
|
|
|
|
|
|
if (this.isNvr === isNvrFlag) return;
|
|
|
|
|
|
this.isNvr = isNvrFlag;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果切换到了单体 IPC,通道强制归 0
|
|
|
|
|
|
if (!isNvrFlag) {
|
|
|
|
|
|
this.currentChannel = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果正在播放,切换设备类型后需要完全重新连接并播放
|
|
|
|
|
|
if (this.isPlaying) {
|
|
|
|
|
|
this.stopPlay();
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.startPlay();
|
|
|
|
|
|
}, 1000); // 切换设备类型可能需要稍长一点的时间断开重连
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
changeChannel(ch) {
|
|
|
|
|
|
if (this.currentChannel === ch) return;
|
|
|
|
|
|
this.currentChannel = ch;
|
|
|
|
|
|
// 如果正在播放,切换通道后重新播放
|
|
|
|
|
|
if (this.isPlaying) {
|
|
|
|
|
|
this.stopPlay();
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.startPlay();
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
changeStream(type) {
|
|
|
|
|
|
if (this.currentStreamType === type) return;
|
|
|
|
|
|
this.currentStreamType = type;
|
|
|
|
|
|
// 如果有单独切换清晰度的方法可以调用,如果没有就重启播放
|
|
|
|
|
|
if (this.isPlaying) {
|
|
|
|
|
|
if (this.$refs.ipcVideo && typeof this.$refs.ipcVideo.setStreamType === 'function') {
|
|
|
|
|
|
this.$refs.ipcVideo.setStreamType(type);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.stopPlay();
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.startPlay();
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-04-05 23:25:01 +08:00
|
|
|
|
startPlay() {
|
|
|
|
|
|
// #ifdef APP-PLUS
|
|
|
|
|
|
if (this.$refs.ipcVideo) {
|
|
|
|
|
|
this.statusMsg = '正在发起播放请求...';
|
|
|
|
|
|
console.log('组件对象:', this.$refs.ipcVideo);
|
|
|
|
|
|
|
|
|
|
|
|
// 在 nvue 中调用组件的方法,有时可能需要通过 evalJS,但绝大多数情况可以直接调
|
|
|
|
|
|
if (typeof this.$refs.ipcVideo.start === 'function') {
|
2026-04-06 19:19:05 +08:00
|
|
|
|
try{
|
|
|
|
|
|
this.$refs.ipcVideo.start();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch(err){
|
|
|
|
|
|
console.error("this.$refs.ipcVideo.start时发生前端异常:", err);
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '前端回调解析异常',
|
|
|
|
|
|
content: err.toString(),
|
|
|
|
|
|
showCancel: false
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-04-05 23:25:01 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
console.error('start 方法不存在,可能插件注册失败或版本未生效');
|
|
|
|
|
|
uni.showToast({ title: '插件方法未找到', icon: 'none' });
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uni.showToast({ title: '插件未加载成功', icon: 'none' });
|
|
|
|
|
|
}
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
// #ifndef APP-PLUS
|
|
|
|
|
|
uni.showToast({ title: '仅App端支持', icon: 'none' });
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
},
|
|
|
|
|
|
stopPlay() {
|
|
|
|
|
|
// #ifdef APP-PLUS
|
|
|
|
|
|
if (this.$refs.ipcVideo) {
|
|
|
|
|
|
if (typeof this.$refs.ipcVideo.stop === 'function') {
|
|
|
|
|
|
this.$refs.ipcVideo.stop();
|
|
|
|
|
|
}
|
|
|
|
|
|
this.statusMsg = '已发送停止指令';
|
|
|
|
|
|
}
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
},
|
|
|
|
|
|
onVideoStatusChange(e) {
|
2026-04-06 15:14:46 +08:00
|
|
|
|
try {
|
|
|
|
|
|
console.log("视频状态改变:", e);
|
|
|
|
|
|
// 根据我们在 Java 封装里写的 fireEventStatus 传回的 msg 显示
|
|
|
|
|
|
if (e && e.detail) {
|
|
|
|
|
|
// 提取参数
|
|
|
|
|
|
const status = e.detail.status;
|
|
|
|
|
|
const msg = e.detail.msg;
|
|
|
|
|
|
const msgParam = e.detail.msgParam;
|
2026-04-05 23:25:01 +08:00
|
|
|
|
|
2026-04-06 15:14:46 +08:00
|
|
|
|
// 更新页面显示的文字状态
|
|
|
|
|
|
if (msg) {
|
|
|
|
|
|
this.statusMsg = msg;
|
|
|
|
|
|
}
|
2026-04-05 23:25:01 +08:00
|
|
|
|
|
2026-04-06 15:14:46 +08:00
|
|
|
|
// 捕获来自 Java 层主动抛出的原生崩溃错误
|
|
|
|
|
|
if (status === 'error' && msg && msg.indexOf('原生启动崩溃') !== -1) {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '原生插件崩溃',
|
|
|
|
|
|
content: msg,
|
|
|
|
|
|
showCancel: false
|
|
|
|
|
|
});
|
|
|
|
|
|
this.isPlaying = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理特定的连接错误状态
|
|
|
|
|
|
if (status === 'error' || msgParam === 7 || msgParam === 14) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: msg ? msg : '摄像机离线,请检查设备网络',
|
|
|
|
|
|
icon: 'none',
|
|
|
|
|
|
duration: 3000
|
|
|
|
|
|
});
|
|
|
|
|
|
// 可以在这里做一些重置操作
|
|
|
|
|
|
this.isPlaying = false;
|
|
|
|
|
|
} else if (status === 'playing') {
|
|
|
|
|
|
this.isPlaying = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果 e.detail 是空对象或者 undefined,提示异常
|
|
|
|
|
|
console.error("收到空的状态事件:", e);
|
2026-04-05 23:25:01 +08:00
|
|
|
|
uni.showToast({
|
2026-04-06 15:14:46 +08:00
|
|
|
|
title: '收到空状态,原生可能发生异常',
|
|
|
|
|
|
icon: 'none'
|
2026-04-05 23:25:01 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-04-06 15:14:46 +08:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("解析状态回调时发生前端异常:", err);
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '前端回调解析异常',
|
|
|
|
|
|
content: err.toString(),
|
|
|
|
|
|
showCancel: false
|
|
|
|
|
|
});
|
2026-04-05 23:25:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
/* nvue 样式必须是纯 CSS/flex,不支持 scss 嵌套和某些普通 css 属性 */
|
|
|
|
|
|
.monitor-container {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
.header {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
background-color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.video-wrapper {
|
|
|
|
|
|
width: 750rpx;
|
|
|
|
|
|
height: 450rpx;
|
|
|
|
|
|
background-color: #000000;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.ipc-video {
|
|
|
|
|
|
width: 750rpx;
|
|
|
|
|
|
height: 450rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.not-support-tips {
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.not-support-tips-text {
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.status-text-wrap {
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-top: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.status-text {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #666666;
|
|
|
|
|
|
}
|
|
|
|
|
|
.controls {
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
justify-content: space-around;
|
|
|
|
|
|
padding-top: 40rpx;
|
|
|
|
|
|
padding-bottom: 40rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn {
|
|
|
|
|
|
width: 300rpx;
|
|
|
|
|
|
height: 80rpx;
|
|
|
|
|
|
border-radius: 10rpx;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-primary {
|
|
|
|
|
|
background-color: #007aff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-warn {
|
|
|
|
|
|
background-color: #e64340;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-text {
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
|
}
|
2026-04-06 15:14:46 +08:00
|
|
|
|
.settings-panel {
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
margin-top: 20rpx;
|
|
|
|
|
|
background-color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
.setting-row {
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.setting-label {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #333333;
|
|
|
|
|
|
width: 120rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.options-group {
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.option-btn {
|
|
|
|
|
|
padding: 10rpx 20rpx;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #666666;
|
|
|
|
|
|
background-color: #f0f0f0;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
margin-right: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.active-btn {
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
background-color: #007aff;
|
|
|
|
|
|
}
|
2026-04-05 23:25:01 +08:00
|
|
|
|
</style>
|