ry_app/package_a/monitor/monitor.nvue

427 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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"
:channel="currentChannel"
:streamType="currentStreamType"
:isNvr="isNvr"
@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>
<!-- 自定义前端录像提示,替代原生 toast 避免崩溃 -->
<view class="recording-indicator" v-if="isRecording">
<text class="recording-text">正在录像...</text>
</view>
<!-- 设置区域 -->
<view class="settings-panel">
<view class="setting-row">
<text class="setting-label">设备类型:</text>
<view class="options-group">
<text class="option-btn" v-if="!isNvr" :class="!isNvr ? 'active-btn' : ''" @click="changeNvr(false)">单体(IPC)</text>
<text class="option-btn" v-else :class="isNvr ? 'active-btn' : ''" @click="changeNvr(true)">主机(NVR)</text>
</view>
</view>
<view class="setting-row" v-if="isNvr && totalChannel > 0">
<text class="setting-label">通道:</text>
<!-- nvue 的 flex 默认不支持换行flex-wrap通道多的话外面套一个 scroll-view 可以左右滑动 -->
<scroll-view class="channel-scroll" scroll-x="true" show-scrollbar="false">
<view class="options-group channel-group">
<text
class="option-btn"
v-for="(item, index) in totalChannel"
:key="index"
:class="currentChannel === index ? 'active-btn' : ''"
@click="changeChannel(index)">
CH{{ index + 1 }}
</text>
</view>
</scroll-view>
</view>
<view class="setting-row">
<text class="setting-label">清晰度:</text>
<view class="options-group">
<!-- <text class="option-btn" :class="currentStreamType === 0 ? 'active-btn' : ''" @click="changeStream(0)">高清</text> -->
<text class="option-btn" :class="currentStreamType === 1 ? 'active-btn' : ''" @click="changeStream(1)">标清</text>
</view>
</view>
</view>
<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 class="controls">
<button class="btn btn-primary" @click="beginRecord"><text class="btn-text">开始录像</text></button>
<button class="btn btn-warn" @click="endRecord"><text class="btn-text">停止录像</text></button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
deviceUid: '', // 默认使用你 Demo 里的测试 UID
isPlaying: false,
isRecording: false, // 是否正在录像
statusMsg: '等待操作',
currentChannel: 0, // 0对应CH1, 1对应CH2...
totalChannel: 1, // 设备的总通道数默认为1由上个页面传过来
currentStreamType: 1, // 0为高清(主码流)1为标清(子码流)
isNvr: true // 是否为 NVR 设备
}
},
onLoad(options) {
if (options && options.uid) {
this.deviceUid = options.uid;
}
// 接收总通道数
if (options && options.channel) {
this.totalChannel = parseInt(options.channel);
// 如果通道数不合理比如0或者NaN给个默认值
if (!this.totalChannel || this.totalChannel <= 0) {
this.totalChannel = 1;
}
}
// 如果上个页面传了 isNvr 参数,这里可以接收
if (options && typeof options.isNvr !== 'undefined') {
// options.isNvr 通常传过来是字符串 'true' 或 'false'
this.isNvr = options.isNvr;
}
},
onUnload() {
// 如果正在录像,必须先停止录像
if (this.isRecording) {
this.endRecord();
}
// 页面卸载时停止播放,释放资源,防止占用通道
this.stopPlay();
},
onHide() {
// 页面切到后台或跳转到其他页面时,也停止录像和播放,释放通道
if (this.isRecording) {
this.endRecord();
}
this.stopPlay();
},
methods: {
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);
}
}
},
startPlay() {
// #ifdef APP-PLUS
if (this.$refs.ipcVideo) {
this.statusMsg = '正在发起播放请求...';
console.log('组件对象:', this.$refs.ipcVideo);
// 在 nvue 中调用组件的方法,有时可能需要通过 evalJS但绝大多数情况可以直接调
if (typeof this.$refs.ipcVideo.start === 'function') {
try{
this.$refs.ipcVideo.start();
}
catch(err){
console.error("this.$refs.ipcVideo.start时发生前端异常:", err);
uni.showModal({
title: '前端回调解析异常',
content: err.toString(),
showCancel: false
});
}
} 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
},
beginRecord() {
// #ifdef APP-PLUS
if (this.$refs.ipcVideo && typeof this.$refs.ipcVideo.startRecord === 'function') {
// 将 _doc/ 相对路径转换为手机上的绝对绝对路径
const savePath = plus.io.convertLocalFileSystemURL("_doc/camera_record_" + Date.now() + ".mp4");
this.$refs.ipcVideo.startRecord({
path: savePath,
width: 1920, // 可选,默认 1920
height: 1080, // 可选,默认 1080
fps: 15 // 可选,默认 10
});
this.statusMsg = '正在录像...';
this.isRecording = true;
console.log("正在录像,保存到:", savePath);
// uni.showToast({ title: '已开始录像', icon: 'success' }); // 暂时注释掉,使用 isRecording 替代
} else {
// uni.showToast({ title: '原生插件缺少录像方法', icon: 'none' }); // 暂时注释掉
}
// #endif
},
endRecord() {
// #ifdef APP-PLUS
if (this.$refs.ipcVideo && typeof this.$refs.ipcVideo.stopRecord === 'function') {
this.$refs.ipcVideo.stopRecord();
this.statusMsg = '录像已保存';
this.isRecording = false;
// uni.showToast({ title: '录像已保存到 _doc 目录', icon: 'success' }); // 暂时注释掉
}
// #endif
},
onVideoStatusChange(e) {
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;
// 更新页面显示的文字状态
if (msg) {
this.statusMsg = msg;
}
// 捕获来自 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);
/* uni.showToast({
title: '收到空状态,原生可能发生异常',
icon: 'none'
}); */ // 暂时注释掉
}
} catch (err) {
console.error("解析状态回调时发生前端异常:", err);
/* uni.showModal({
title: '前端回调解析异常',
content: err.toString(),
showCancel: false
}); */ // 暂时注释掉
}
}
}
}
</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;
}
.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;
}
.channel-scroll {
flex: 1;
flex-direction: row;
}
.channel-group {
flex-direction: row;
padding-bottom: 10rpx; /* 预留一点底部空间,防止被遮挡 */
}
.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;
}
.recording-indicator {
position: absolute;
top: 40rpx;
right: 40rpx;
background-color: rgba(255, 0, 0, 0.7);
padding: 10rpx 20rpx;
border-radius: 10rpx;
flex-direction: row;
align-items: center;
}
.recording-text {
color: #ffffff;
font-size: 24rpx;
font-weight: bold;
}
</style>