feat:空调开关、状态

This commit is contained in:
吉浩茹
2025-10-09 17:22:26 +08:00
parent 4b65bea0bb
commit 279cc4a5ea
6 changed files with 518 additions and 877 deletions

View File

@ -248,3 +248,5 @@ console.log('组件ref:', this.$refs.playerVideoRef)
需要详细说明?查看 → [萤石云APP对接完整指南.md](./萤石云APP对接完整指南.md)

View File

@ -17,13 +17,33 @@
></web-view>
<!-- #endif -->
<!-- H5平台提示 -->
<!-- H5平台直接使用iframe -->
<!-- #ifdef H5 -->
<view class="h5-tip">H5平台暂不支持</view>
<view v-if="iframeUrl" class="h5-iframe-container">
<iframe
:src="iframeUrl"
class="h5-iframe"
allow="autoplay; fullscreen"
allowfullscreen
></iframe>
</view>
<!-- #endif -->
<!-- 控制按钮 -->
<view class="control-buttons" v-if="!loading && !error">
<!-- APP平台使用 cover-view -->
<!-- #ifdef APP-PLUS -->
<cover-view class="control-buttons-cover" v-if="!loading && webviewUrl">
<cover-view class="control-btn-cover play-btn-cover" @click="togglePlay">
<cover-view class="btn-text">{{ isPlaying ? '⏸ 暂停' : '▶ 播放' }}</cover-view>
</cover-view>
<cover-view class="control-btn-cover refresh-btn-cover" @click="refresh">
<cover-view class="btn-text">🔄 刷新</cover-view>
</cover-view>
</cover-view>
<!-- #endif -->
<!-- H5平台使用普通按钮 -->
<!-- #ifdef H5 -->
<view class="control-buttons" v-if="!loading && iframeUrl">
<button class="control-btn play-btn" @click="togglePlay">
{{ isPlaying ? ' 暂停' : ' 播放' }}
</button>
@ -31,6 +51,7 @@
🔄 刷新
</button>
</view>
<!-- #endif -->
<view v-if="loading" class="loading">
<text>{{ loadingText }}</text>
@ -39,6 +60,16 @@
<view v-if="error" class="error">
<text>{{ errorText }}</text>
<button @click="retry">重试</button>
<!-- 即使有错误也显示控制按钮 -->
<view class="error-controls">
<button class="control-btn play-btn" @click="togglePlay">
{{ isPlaying ? ' 暂停' : ' 播放' }}
</button>
<button class="control-btn refresh-btn" @click="refresh">
🔄 刷新
</button>
</view>
</view>
</view>
</template>
@ -56,7 +87,8 @@ export default {
return {
platform: '',
status: '未初始化',
webviewUrl: '',
webviewUrl: '', // APP平台使用
iframeUrl: '', // H5平台使用
loading: false,
loadingText: '',
error: false,
@ -96,12 +128,26 @@ export default {
this.status = '加载中'
try {
// #ifdef APP-PLUS
// APP平台使用本地HTML文件
const token = encodeURIComponent(config.accessToken)
const url = encodeURIComponent(config.play_url)
// 使用iframe版本内存占用更小
this.webviewUrl = `/static/html/ezviz-iframe.html?accessToken=${token}&playUrl=${url}`
console.log('[简单播放器] APP平台 - 使用本地HTML文件')
// #endif
console.log('[简单播放器] 使用iframe版本URL已设置')
// #ifdef H5
// H5平台直接构建萤石云iframe URL
this.iframeUrl = 'https://open.ys7.com/ezopen/h5/iframe?' +
'url=' + encodeURIComponent(config.play_url) +
'&accessToken=' + encodeURIComponent(config.accessToken) +
'&width=100%' +
'&height=100%' +
'&autoplay=1' +
'&audio=1' +
'&controls=1'
console.log('[简单播放器] H5平台 - 直接使用萤石云iframe')
// #endif
setTimeout(() => {
if (this.loading) {
@ -154,7 +200,13 @@ export default {
// 因为iframe播放器不支持直接控制所以采用重新加载的方式
if (this.isPlaying) {
// 暂停清空URL
// #ifdef APP-PLUS
this.webviewUrl = ''
// #endif
// #ifdef H5
this.iframeUrl = ''
// #endif
this.isPlaying = false
this.status = '已暂停'
@ -163,9 +215,23 @@ export default {
} else {
// 播放重新设置URL
if (this.config) {
// #ifdef APP-PLUS
const token = encodeURIComponent(this.config.accessToken)
const url = encodeURIComponent(this.config.play_url)
this.webviewUrl = `/static/html/ezviz-iframe.html?accessToken=${token}&playUrl=${url}`
// #endif
// #ifdef H5
this.iframeUrl = 'https://open.ys7.com/ezopen/h5/iframe?' +
'url=' + encodeURIComponent(this.config.play_url) +
'&accessToken=' + encodeURIComponent(this.config.accessToken) +
'&width=100%' +
'&height=100%' +
'&autoplay=1' +
'&audio=1' +
'&controls=1'
// #endif
this.isPlaying = true
this.status = '播放中'
@ -195,12 +261,31 @@ export default {
})
// 先清空再重新加载
// #ifdef APP-PLUS
this.webviewUrl = ''
// #endif
// #ifdef H5
this.iframeUrl = ''
// #endif
setTimeout(() => {
// #ifdef APP-PLUS
const token = encodeURIComponent(this.config.accessToken)
const url = encodeURIComponent(this.config.play_url)
this.webviewUrl = `/static/html/ezviz-iframe.html?accessToken=${token}&playUrl=${url}`
// #endif
// #ifdef H5
this.iframeUrl = 'https://open.ys7.com/ezopen/h5/iframe?' +
'url=' + encodeURIComponent(this.config.play_url) +
'&accessToken=' + encodeURIComponent(this.config.accessToken) +
'&width=100%' +
'&height=100%' +
'&autoplay=1' +
'&audio=1' +
'&controls=1'
// #endif
this.isPlaying = true
this.status = '播放中'
@ -244,16 +329,27 @@ export default {
.video-webview {
width: 100%;
height: 50%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.h5-tip {
display: flex;
align-items: center;
justify-content: center;
/* H5平台iframe容器 */
.h5-iframe-container {
width: 100%;
height: 100%;
color: white;
font-size: 28rpx;
position: absolute;
top: 0;
left: 0;
background: #000;
}
.h5-iframe {
width: 100%;
height: 100%;
border: none;
display: block;
}
.loading {
@ -300,6 +396,36 @@ export default {
border-radius: 10rpx;
}
/* 错误状态下的控制按钮 */
.error-controls {
display: flex;
gap: 20rpx;
margin-top: 30rpx;
justify-content: center;
}
.error-controls .control-btn {
flex: 1;
max-width: 200rpx;
height: 60rpx;
border-radius: 8rpx;
font-size: 24rpx;
font-weight: bold;
color: white;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.error-controls .play-btn {
background: linear-gradient(135deg, #46a049 0%, #4caf50 100%);
}
.error-controls .refresh-btn {
background: linear-gradient(135deg, #2196f3 0%, #21cbf3 100%);
}
/* 控制按钮 */
.control-buttons {
position: absolute;

View File

@ -24,9 +24,9 @@
<view class="detail-progress-bar">
<view class="detail-progress-fill temperature-progress" :style="{ width: temperatureProgress + '%' }"></view>
</view>
<view class="detail-range">
<!-- <view class="detail-range">
<text class="detail-range-text">{{ 0 }}°C - {{ 100 }}°C</text>
</view>
</view> -->
</view>
<!-- 湿度卡片 -->
@ -44,9 +44,9 @@
<view class="detail-progress-bar">
<view class="detail-progress-fill humidity-progress" :style="{ width: humidityProgress + '%' }"></view>
</view>
<view class="detail-range">
<!-- <view class="detail-range">
<text class="detail-range-text">{{ 0 }}% - {{ 100 }}%</text>
</view>
</view> -->
</view>
<!-- 洁净度卡片 -->
@ -64,22 +64,39 @@
<view class="detail-progress-bar">
<view class="detail-progress-fill cleanliness-progress" :style="{ width: cleanlinessProgress + '%' }"></view>
</view>
<view class="detail-range">
<!-- <view class="detail-range">
<text class="detail-range-text">{{ 0 }}% - {{ 100 }}%</text>
</view>
</view> -->
</view>
</view>
<!-- 空调目标参数设置 -->
<!-- 空调设置 -->
<view class="air-conditioner-settings">
<view class="ac-header">
<view class="ac-title-container">
<view class="ac-icon"></view>
<text class="ac-title">空调目标参数设置</text>
<text class="ac-title">空调设置</text>
</view>
<view class="ac-status-indicator">
<view class="status-dot active"></view>
<text class="status-label">运行中</text>
<text class="status-label">{{ acStatusList[acStatus] }}</text>
</view>
</view>
<!-- 空调开关控制卡片 -->
<view class="ac-control-card power-card">
<view class="control-header">
<view class="control-icon-container">
<text class="control-label">设备控制</text>
</view>
</view>
<view class="ac-power-controls">
<button class="ac-power-btn power-on" @click="turnOnAirConditioner" :disabled="acControlLoading">
开机
</button>
<button class="ac-power-btn power-off" @click="turnOffAirConditioner" :disabled="acControlLoading">
关机
</button>
</view>
</view>
@ -259,27 +276,27 @@ export default {
cleanlinessProgress: 0,
lastUpdate: '暂无数据',
temperatureRange: {
min: 25,
max: 35
min: 0,
max: 0
},
humidityRange: {
min: 40,
max: 70
min: 0,
max: 0
},
tempSettings: {
min: 25,
max: 35
min: 0,
max: 0
},
humiditySettings: {
min: 40,
max: 70
min: 0,
max: 0
},
connectionStatus: {
isConnected: false,
lastUpdate: null
},
targetTemperature: 30,
targetHumidity: 50, // 空调设定湿度
targetTemperature: 0,
targetHumidity: 0, // 空调设定湿度
// 温度输入相关
tempInputValue: '',
tempValidationMessage: '',
@ -295,36 +312,32 @@ export default {
alertHistory: [], // 报警历史记录
// 系统启动事件相关
hasCreatedStartupEvent: false, // 是否已创建启动事件
acStatus: 4,
// 0待机1启动中2运行中3关机中, 没有就默认连接中
acStatusList: ['待机', '启动中', '运行中', '关机中', '连接中'],
// 空调控制相关
acControlLoading: false, // 空调控制按钮加载状态
}
},
onLoad() {
console.log('环境参数页面加载')
// 从本地存储读取是否已创建启动事件的状态
const hasCreatedStartupEvent = uni.getStorageSync('hasCreatedStartupEvent')
if (hasCreatedStartupEvent) {
this.hasCreatedStartupEvent = true
console.log('📱 从本地存储读取到启动事件状态: 已创建')
}
// // 从本地存储读取是否已创建启动事件的状态
// const hasCreatedStartupEvent = uni.getStorageSync('hasCreatedStartupEvent')
// if (hasCreatedStartupEvent) {
// this.hasCreatedStartupEvent = true
// console.log('📱 从本地存储读取到启动事件状态: 已创建')
// }
this.initMqttListener();
this.init();
// // 获取最新空调温度
// this.getLatestAirConditionerTemperature()
// // 获取最新温湿度数据
// this.getLatestWsdData()
// // 首次进入系统时创建启动事件
// this.createStartupEventIfNeeded()
// // 获取温湿度区间设置
// this.loadWsdSettings()
},
onShow() {
console.log('📱 环境参数页面显示,触发页面更新')
// 只有在非首次显示时才重新获取最新空调温度
if (this.hasCreatedStartupEvent) {
this.init();
}
console.log('📱 环境参数页面显示,触发页面更新');
this.init();
// // 只有在非首次显示时才重新获取最新空调温度
// if (this.hasCreatedStartupEvent) {
// this.init();
// }
},
onUnload() {
// 页面卸载时移除监听器
@ -413,12 +426,12 @@ export default {
if (response && response.id) {
// 更新温度和湿度控制范围
this.temperatureRange = {
min: response.minTemperature || 25,
max: response.maxTemperature || 35
min: response.minTemperature || 0,
max: response.maxTemperature || 0
}
this.humidityRange = {
min: response.minHumidity || 40,
max: response.maxHumidity || 70
min: response.minHumidity || 0,
max: response.maxHumidity || 0
}
// 同时更新设置弹窗中的临时变量
@ -433,8 +446,8 @@ export default {
} catch (error) {
console.error('❌ 温湿度区间设置加载失败:', error)
// 使用默认值
this.temperatureRange = { min: 25, max: 35 }
this.humidityRange = { min: 40, max: 70 }
this.temperatureRange = { min: 0, max: 0 }
this.humidityRange = { min: 0, max: 0 }
this.tempSettings = { ...this.temperatureRange }
this.humiditySettings = { ...this.humidityRange }
console.log('🔄 使用默认温湿度区间设置')
@ -444,20 +457,18 @@ export default {
// 获取最新空调温湿度参数
async getLatestAirConditionerTemperature() {
try {
console.log('🌡️ 开始获取最新空调温湿度参数...')
const res = await thDataApi.getLatest();
if (res.status === 'success') {
// 从接口获取温度和湿度设定值
this.targetTemperature = res.temperature || 30;
this.targetHumidity = res.humidity || 50;
this.targetTemperature = res.temperature || 0;
this.targetHumidity = res.humidity || 0;
}
console.log('✅ 最新空调温湿度参数:', res)
} catch (error) {
console.error('❌ 获取最新空调温湿度参数失败:', error)
// 接口失败时使用默认值
this.targetTemperature = 30;
this.targetHumidity = 50;
this.targetTemperature = 0;
this.targetHumidity = 0;
}
},
// 获取最新温湿度数据
@ -465,13 +476,10 @@ export default {
try {
const res = await wsdApi.getLatest();
if (res.status === 'success') {
// this.temperature = res.wd || 30;
// this.humidity = res.sd || 50
// this.cleanliness = res.pm || 0;
this.updateEnvironmentData({
deviceType: 'WSD',
temperature: res.wd || 30,
humidity: res.sd || 50,
temperature: res.wd || 0,
humidity: res.sd || 0,
cleanliness: res.pm || 0,
})
}
@ -515,12 +523,8 @@ export default {
// 更新环境数据
updateEnvironmentData(data) {
// 处理空调故障状态
if (data.deviceType === 'AC' && data.faultStatus !== undefined) {
this.acFaultStatus = data.faultStatus
}
// 只处理WSD设备的数据
console.log('============data', data)
// 处理WSD设备的数据
if (data.deviceType === 'WSD') {
if (data.temperature !== undefined) {
this.temperature = parseFloat(data.temperature.toFixed(1))
@ -531,18 +535,31 @@ export default {
this.humidity = parseFloat(data.humidity.toFixed(1))
this.humidityProgress = Math.min(Math.max(this.humidity, 0), 100)
}
if (data.cleanliness !== undefined) {
this.cleanliness = parseFloat(data.cleanliness.toFixed(1))
this.cleanlinessProgress = Math.min(Math.max(this.cleanliness, 0), 100)
}
this.lastUpdate = data.time || new Date().toLocaleString('zh-CN')
// 检查报警条件传入MQTT原始数据
this.checkAlerts(data)
} else if (data.deviceType === 'AC') {
// 处理空调AC数据
if (data.faultStatus !== undefined) {
this.acFaultStatus = data.faultStatus // 空调故障
}
if (data.rawData.status !== undefined) {
this.acStatus = data.rawData.status
}
if (data.rawData.ctrlword !== undefined) {
this.acCtrlword = data.rawData.ctrlword
}
} else if (data.deviceType === 'PM25') {
// 处理PM25数据
if (data.rawData.PM25 !== undefined) {
this.cleanliness = data.rawData.PM25;
this.cleanlinessProgress = Math.min(Math.max(this.cleanliness, 0), 100)
}
} else {
console.log('⚠️ 非WSD设备数据跳过更新:', data.deviceType)
console.log('⚠️ 非WSD、AC、PM25设备数据,跳过更新:', data.deviceType)
// 处理其他设备数据
}
},
@ -566,13 +583,20 @@ export default {
if (mqttData.deviceType !== 'WSD') {
return
}
console.log('====mqttData', mqttData)
// {
// "deviceType": "WSD",
// "temperature": 0,
// "humidity": 0,
// "cleanliness": 0
// }
// 获取MQTT原始数据
const mqttTemperature = mqttData.temperature
const mqttHumidity = mqttData.humidity
// 1. 温度报警使用环境控制设置的区间使用MQTT原始数据
if (mqttTemperature !== undefined && (mqttTemperature < this.temperatureRange.min || mqttTemperature > this.temperatureRange.max)) {
if (mqttTemperature !== undefined && mqttTemperature !== 0 && (mqttTemperature < this.temperatureRange.min || mqttTemperature > this.temperatureRange.max)) {
const alert = {
// content: `温度传感器异常,读数${mqttTemperature}°C超出控制范围${this.temperatureRange.min}°C-${this.temperatureRange.max}°C`,
content: `温度超出控制范围`,
@ -660,21 +684,21 @@ export default {
// 记录报警到控制台并调用创建告警接口
async logAlert(alert) {
console.log('🚨 报警触发:', JSON.stringify(alert, null, 2))
this.alertHistory.push(alert)
// this.alertHistory.push(alert)
// 调用创建告警接口
try {
console.log('📤 正在创建告警记录...')
const response = await alertApi.create(alert)
console.log('✅ 告警记录创建成功:', response)
} catch (error) {
console.error('❌ 告警记录创建失败:', error)
}
// // 调用创建告警接口
// try {
// console.log('📤 正在创建告警记录...')
// const response = await alertApi.create(alert)
// console.log('✅ 告警记录创建成功:', response)
// } catch (error) {
// console.error('❌ 告警记录创建失败:', error)
// }
// 限制报警历史记录数量,避免内存溢出
if (this.alertHistory.length > 100) {
this.alertHistory = this.alertHistory.slice(-50)
}
// // 限制报警历史记录数量,避免内存溢出
// if (this.alertHistory.length > 100) {
// this.alertHistory = this.alertHistory.slice(-50)
// }
},
// 降低目标温度
@ -875,7 +899,7 @@ export default {
this.humidityValidationClass = ''
},
// 显示湿度变化提示
// 空调湿度更新
showHumidityChangeToast() {
// 发送空调参数到MQTT
this.sendAirConditionerParams()
@ -889,110 +913,156 @@ export default {
this.checkAlerts(mockMqttData)
},
// 显示温度变化提示
showTemperatureChangeToast() {
// uni.showToast({
// title: `目标温度: ${this.targetTemperature}°C`,
// icon: 'success',
// duration: 1500
// })
// 发送空调参数到MQTT
this.sendAirConditionerParams()
// 温度变化后检查报警使用当前页面数据模拟MQTT数据
const mockMqttData = {
deviceType: 'WSD',
temperature: this.temperature,
humidity: this.humidity
// 空调温度更新
showTemperatureChangeToast() {
// 发送空调参数到MQTT
this.sendAirConditionerParams()
// 温度变化后检查报警使用当前页面数据模拟MQTT数据
const mockMqttData = {
deviceType: 'WSD',
temperature: this.temperature,
humidity: this.humidity
}
this.checkAlerts(mockMqttData)
},
// 发送空调参数
async sendAirConditionerParams() {
try {
// 根据MQTT文档空调温度使用BSQWD但控制指令可能使用不同的TagName
// 发送温度参数 - 使用BSQWD作为TagName与接收数据保持一致
const temperatureData = {
"TagValue": this.targetTemperature,
"TagName": "JS_COD", // 使用与接收数据一致的TagName
"method": "setValue"
}
this.checkAlerts(mockMqttData)
},
// 发送空调参数
async sendAirConditionerParams() {
// 发送湿度参数 - 根据文档WSD设备湿度使用SD
const humidityData = {
"TagValue": this.targetHumidity,
"TagName": "JS_SD", // 使用与WSD设备湿度一致的TagName
"method": "setValue"
}
console.log('🌡️ 发送空调温度参数:', temperatureData)
console.log('💧 发送空调湿度参数:', humidityData)
// 调用发送MQTT数据的方法
const tempSuccess = sendMqttData(temperatureData)
const humiditySuccess = sendMqttData(humidityData)
if (tempSuccess && humiditySuccess) {
console.log('✅ 空调温湿度参数MQTT发送请求已提交')
// 发送成功后调用提交温湿度数据API
try {
// 根据MQTT文档空调温度使用BSQWD但控制指令可能使用不同的TagName
// 发送温度参数 - 使用BSQWD作为TagName与接收数据保持一致
const temperatureData = {
"TagValue": this.targetTemperature,
"TagName": "JS_COD", // 使用与接收数据一致的TagName
"method": "setValue"
}
// 发送湿度参数 - 根据文档WSD设备湿度使用SD
const humidityData = {
"TagValue": this.targetHumidity,
"TagName": "JS_SD", // 使用与WSD设备湿度一致的TagName
"method": "setValue"
}
console.log('🌡️ 发送空调温度参数:', temperatureData)
console.log('💧 发送空调湿度参数:', humidityData)
// 调用发送MQTT数据的方法
const tempSuccess = sendMqttData(temperatureData)
const humiditySuccess = sendMqttData(humidityData)
if (tempSuccess && humiditySuccess) {
console.log('✅ 空调温湿度参数MQTT发送请求已提交')
// 发送成功后调用提交温湿度数据API
try {
await this.submitTemperatureData()
// 显示成功提示
uni.showToast({
title: '参数设置成功',
icon: 'success',
duration: 1500
})
} catch (apiError) {
// MQTT发送成功但接口保存失败
console.warn('⚠️ MQTT发送成功但接口保存失败:', apiError)
uni.showToast({
title: 'MQTT已发送接口保存失败',
icon: 'none',
duration: 2500
})
}
} else {
console.error('❌ 空调参数MQTT发送失败')
uni.showToast({
title: 'MQTT发送失败',
icon: 'error',
duration: 2000
})
}
} catch (error) {
console.error('❌ 发送空调参数异常:', error)
await this.submitTemperatureData()
// 显示成功提示
uni.showToast({
title: '参数设置失败',
icon: 'error',
duration: 2000
title: '参数设置成功',
icon: 'success',
duration: 1500
})
} catch (apiError) {
// MQTT发送成功但接口保存失败
console.warn('⚠️ MQTT发送成功但接口保存失败:', apiError)
uni.showToast({
title: 'MQTT已发送接口保存失败',
icon: 'none',
duration: 2500
})
}
},
} else {
console.error('❌ 空调参数MQTT发送失败')
uni.showToast({
title: 'MQTT发送失败',
icon: 'error',
duration: 2000
})
}
} catch (error) {
console.error('❌ 发送空调参数异常:', error)
uni.showToast({
title: '参数设置失败',
icon: 'error',
duration: 2000
})
}
},
// 空调开机控制
async turnOnAirConditioner() {
await this.sendAirConditionerControl(1, '开机')
},
// 空调关机控制
async turnOffAirConditioner() {
await this.sendAirConditionerControl(2, '关机')
},
// 发送空调控制指令
async sendAirConditionerControl(ctrlValue, actionName) {
this.acControlLoading = true;
try {
// 构建控制指令数据
const controlData = {
"TagValue": ctrlValue,
"TagName": "ctrlword",
"method": "setValue"
}
// 提交温湿度数据
async submitTemperatureData() {
try {
const temperatureHumidityData = {
temperature: this.targetTemperature,
humidity: this.targetHumidity,
deviceId: "TH_SENSOR_001",
timestamp: new Date().toISOString(),
source: "manual_setting" // 标识为手动设置
}
console.log('📤 提交温湿度数据到接口:', temperatureHumidityData)
const response = await thDataApi.submit(temperatureHumidityData)
console.log('✅ 温湿度数据接口提交成功:', response)
} catch (error) {
console.error('❌ 温湿度数据接口提交失败:', error)
// 接口失败不影响MQTT发送只记录日志
throw error // 重新抛出错误,让调用方知道接口失败了
}
},
console.log(`🔧 发送空调${actionName}指令:`, controlData)
// 调用发送MQTT数据的方法
const success = sendMqttData(controlData)
this.acControlLoading = false;
if (success) {
// 显示成功提示
uni.showToast({
title: `${actionName}指令已发送`,
icon: 'success',
duration: 1500
})
} else {
uni.showToast({
title: `${actionName}失败`,
icon: 'error',
duration: 2000
})
}
} catch (error) {
uni.showToast({
title: `${actionName}异常`,
icon: 'error',
duration: 2000
})
} finally {
// 清除加载状态
this.acControlLoading = false
}
},
// 提交温湿度数据
async submitTemperatureData() {
try {
const temperatureHumidityData = {
temperature: this.targetTemperature,
humidity: this.targetHumidity,
deviceId: "TH_SENSOR_001",
timestamp: new Date().toISOString(),
source: "manual_setting" // 标识为手动设置
}
console.log('📤 提交温湿度数据到接口:', temperatureHumidityData)
const response = await thDataApi.submit(temperatureHumidityData)
console.log('✅ 温湿度数据接口提交成功:', response)
} catch (error) {
console.error('❌ 温湿度数据接口提交失败:', error)
// 接口失败不影响MQTT发送只记录日志
throw error // 重新抛出错误,让调用方知道接口失败了
}
},
// 手动重连MQTT
manualReconnect() {
@ -1449,6 +1519,58 @@ export default {
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
/* 空调开关控制按钮样式 */
.ac-power-controls {
display: flex;
justify-content: center;
gap: 20rpx;
background: #ffffff;
border-radius: 8rpx;
padding: 20rpx;
border: 1rpx solid #e1e5e9;
margin-bottom: 12rpx;
}
.ac-power-btn {
flex: 1;
max-width: 120rpx;
// height: 60rpx;
border-radius: 6rpx;
border: 1rpx solid;
font-size: 28rpx;
font-weight: 500;
transition: all 0.2s ease;
}
.power-on {
background: #27ae60;
color: white;
border-color: #27ae60;
}
.power-on:active {
background: #229954;
border-color: #229954;
}
.power-off {
background: #e74c3c;
color: white;
border-color: #e74c3c;
}
.power-off:active {
background: #c0392b;
border-color: #c0392b;
}
.ac-power-btn:disabled {
background: #bdc3c7;
color: #7f8c8d;
border-color: #bdc3c7;
opacity: 0.6;
}
.ac-temp-display {
display: flex;
align-items: baseline;

View File

@ -25,31 +25,6 @@
</view>
</view>
<!-- 视频信息区域 -->
<view v-if="ezstate" class="video-info">
<view class="info-item">
<text class="info-label">📡 设备状态</text>
<text class="info-value online">在线</text>
</view>
<view class="info-item">
<text class="info-label">🎥 分辨率</text>
<text class="info-value">高清</text>
</view>
<view class="info-item">
<text class="info-label">🔊 音频</text>
<text class="info-value">开启</text>
</view>
</view>
<!-- 视频控制按钮 -->
<view v-if="ezstate" class="control-section">
<button @click="handleInitPlayer" class="control-btn init-btn">
🔄 初始化
</button>
<button @click="handleTogglePlay" class="control-btn pause-btn">
{{ isPlaying ? '⏸ 暂停播放' : '▶ 开始播放' }}
</button>
</view>
<!-- API测试按钮 -->
<view class="test-section" v-if="!ezstate">
@ -136,55 +111,6 @@ export default {
})
},
// 初始化播放器
async handleInitPlayer() {
console.log('🔄 重新初始化播放器...')
uni.showLoading({
title: '正在初始化...'
})
try {
// 重新获取视频数据并初始化
await this.getVideoData()
uni.hideLoading()
uni.showToast({
title: '初始化成功',
icon: 'success',
duration: 2000
})
// 重置播放状态
this.isPlaying = true
} catch (error) {
uni.hideLoading()
console.error('初始化失败:', error)
uni.showToast({
title: '初始化失败',
icon: 'error',
duration: 2000
})
}
},
// 切换播放/暂停
handleTogglePlay() {
console.log('🎬 切换播放状态:', this.isPlaying ? '暂停' : '播放')
if (this.$refs.playerVideoRef) {
// 调用播放器组件的切换播放方法(状态会通过事件同步)
this.$refs.playerVideoRef.togglePlay()
} else {
console.error('❌ 播放器组件未找到')
uni.showToast({
title: '播放器未就绪',
icon: 'error',
duration: 2000
})
}
},
// 处理播放状态变化(由播放器组件触发)
handlePlayStateChange(isPlaying) {

View File

@ -9,7 +9,14 @@ class MqttDataManager {
temperature: null,
humidity: null,
pm25: null,
timestamp: null
timestamp: null,
// 扩展数据结构支持更多设备类型
deviceType: null,
rawData: null,
// 空调设备数据
acTemperature: null,
// 其他设备数据可在此扩展
otherDeviceData: {}
}
this.init()
}
@ -95,41 +102,65 @@ class MqttDataManager {
// console.log('设备数据:', deviceDataContent)
// console.log('时间戳:', timestamp)
// 根据设备类型处理数据
if (deviceType === 'WSD') {
// console.log('✅ 处理WSD设备数据 - 更新环境参数')
this.processWSDData(deviceDataContent, timestamp)
} else {
// console.log(`⚠️ 设备类型 ${deviceType} 暂不处理,仅打印到控制台`)
// console.log('设备详情:', {
// deviceType,
// data: deviceDataContent,
// timestamp: new Date(timestamp * 1000).toLocaleString('zh-CN')
// })
}
// 处理所有设备类型数据
console.log(`✅ 处理设备类型: ${deviceType}`)
this.processAllDeviceData(deviceType, deviceDataContent, timestamp)
} catch (error) {
console.error('❌ 处理设备数据失败:', error)
}
}
// 处理WSD设备数据
processWSDData(data, timestamp) {
// 处理所有设备类型的数据
processAllDeviceData(deviceType, data, timestamp) {
try {
// 解析WSD数据 - 根据您提供的数据结构WD是温度SD是湿度
const temperature = data.WD && parseFloat(data.WD);
const humidity = data.SD && parseFloat(data.SD);
console.log(`🌡️ ${deviceType}设备数据解析:`)
console.log('设备数据:', data)
// console.log('🌡️ WSD数据解析:')
// console.log('温度(WD):', temperature)
// console.log('湿度(SD):', humidity)
// 构建解析后的数据
// 构建基础解析数据
const parsedData = {
deviceType: 'WSD',
deviceType,
timestamp,
time: new Date(timestamp * 1000).toLocaleString('zh-CN'),
temperature,
humidity
rawData: data // 保存原始数据供页面特殊处理
}
// 根据设备类型解析特定数据
switch (deviceType) {
case 'WSD': // 温湿度传感器
if (data.WD !== undefined) {
parsedData.temperature = parseFloat(data.WD)
}
if (data.SD !== undefined) {
parsedData.humidity = parseFloat(data.SD)
}
break
case 'AC': // 空调设备
if (data.BSQWD !== undefined) {
parsedData.acTemperature = parseFloat(data.BSQWD)
}
if (data.SD !== undefined) {
parsedData.acHumidity = parseFloat(data.SD)
}
if (data.status !== undefined) {
parsedData.acStatus = data.status
}
// 可以添加其他空调参数
break
case 'PM': // PM2.5传感器
if (data.PM25 !== undefined) {
parsedData.pm25 = parseFloat(data.PM25)
}
break
default:
// 其他设备类型保存到otherDeviceData中
console.log(`📦 处理未知设备类型 ${deviceType},保存原始数据`)
// parsedData.otherDeviceData = data
break
}
// 更新最新数据
@ -138,9 +169,9 @@ class MqttDataManager {
// 通知所有监听器
this.notifyListeners('dataUpdate', parsedData)
// console.log('✅ WSD数据处理完成:', parsedData)
console.log(`${deviceType}数据处理完成:`, parsedData)
} catch (error) {
console.error('❌ 处理WSD数据失败:', error)
console.error(`❌ 处理${deviceType}数据失败:`, error)
}
}
@ -198,16 +229,18 @@ class MqttDataManager {
// 更新最新数据
updateLastData(parsedData) {
if (parsedData.temperature !== undefined) {
this.lastData.temperature = parsedData.temperature
}
if (parsedData.humidity !== undefined) {
this.lastData.humidity = parsedData.humidity
}
if (parsedData.pm25 !== undefined) {
this.lastData.pm25 = parsedData.pm25
}
this.lastData.timestamp = parsedData.timestamp
// 直接合并数据,只更新有值的字段
Object.keys(parsedData).forEach(key => {
if (parsedData[key] !== undefined) {
if (key === 'otherDeviceData' && this.lastData.otherDeviceData) {
// 对于 otherDeviceData进行深度合并
this.lastData.otherDeviceData = { ...this.lastData.otherDeviceData, ...parsedData.otherDeviceData }
} else {
// 其他字段直接赋值
this.lastData[key] = parsedData[key]
}
}
})
}
// 添加数据监听器

View File

@ -271,591 +271,21 @@ src/
### 1. 播放器组件 (`EzvizVideoPlayerSimple.vue`)
```vue
<template>
<view class="simple-video-player">
<!-- APP平台使用web-view -->
<web-view
v-if="webviewUrl"
:src="webviewUrl"
class="video-webview"
></web-view>
<!-- 控制按钮 -->
<view class="control-buttons" v-if="!loading && !error">
<button class="control-btn play-btn" @click="togglePlay">
{{ isPlaying ? '⏸ 暂停' : '▶ 播放' }}
</button>
<button class="control-btn refresh-btn" @click="refresh">
🔄 刷新
</button>
</view>
</view>
</template>
<script>
export default {
name: 'EzvizVideoPlayerSimple',
data() {
return {
webviewUrl: '',
isPlaying: true,
config: null
}
},
methods: {
initEzuikit(config) {
console.log('[播放器] 初始化')
if (!config || !config.accessToken || !config.play_url) {
console.error('配置参数不完整')
return
}
this.config = config
// 构建 URL通过 URL 参数传递配置
const token = encodeURIComponent(config.accessToken)
const url = encodeURIComponent(config.play_url)
this.webviewUrl = `/static/html/ezviz-iframe.html?accessToken=${token}&playUrl=${url}`
console.log('[播放器] URL已设置')
},
togglePlay() {
if (this.isPlaying) {
// 暂停:清空 URL
this.webviewUrl = ''
this.isPlaying = false
} else {
// 播放:重新加载
if (this.config) {
const token = encodeURIComponent(this.config.accessToken)
const url = encodeURIComponent(this.config.play_url)
this.webviewUrl = `/static/html/ezviz-iframe.html?accessToken=${token}&playUrl=${url}`
this.isPlaying = true
}
}
},
refresh() {
console.log('[播放器] 刷新')
this.webviewUrl = ''
setTimeout(() => {
if (this.config) {
const token = encodeURIComponent(this.config.accessToken)
const url = encodeURIComponent(this.config.play_url)
this.webviewUrl = `/static/html/ezviz-iframe.html?accessToken=${token}&playUrl=${url}`
}
}, 500)
}
}
}
</script>
```
---
### 2. iframe HTML (`ezviz-iframe.html`)
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>萤石云播放器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #1a1a1a;
position: relative;
}
#player-iframe {
width: 100%;
height: 100%;
border: none;
display: block;
object-fit: contain; /* 保持视频比例,不变形 */
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 14px;
text-align: center;
background: rgba(0, 0, 0, 0.7);
padding: 15px 30px;
border-radius: 8px;
z-index: 10;
}
.loading::after {
content: '';
display: block;
width: 20px;
height: 20px;
margin: 10px auto 0;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="loading" id="loading">正在加载播放器...</div>
<iframe id="player-iframe" allow="autoplay; fullscreen"></iframe>
<script>
function log(message) {
console.log('[iframe播放器] ' + message);
}
// 从URL参数获取配置
function getConfig() {
const params = new URLSearchParams(window.location.search);
return {
accessToken: params.get('accessToken'),
playUrl: params.get('playUrl')
};
}
// 初始化播放器
function init() {
log('初始化开始');
const config = getConfig();
if (!config.accessToken || !config.playUrl) {
log('配置参数不完整');
document.getElementById('loading').textContent = '配置参数错误';
return;
}
log('AccessToken: ' + config.accessToken.substring(0, 20) + '...');
log('PlayUrl: ' + config.playUrl);
try {
// 使用萤石云官方iframe播放器内存占用小
const iframeUrl = 'https://open.ys7.com/ezopen/h5/iframe?' +
'url=' + encodeURIComponent(config.playUrl) +
'&accessToken=' + encodeURIComponent(config.accessToken) +
'&width=100%' +
'&height=100%' +
'&autoplay=1' +
'&audio=1' +
'&controls=1';
log('iframe URL: ' + iframeUrl);
const iframe = document.getElementById('player-iframe');
iframe.src = iframeUrl;
// 隐藏loading
setTimeout(() => {
document.getElementById('loading').style.display = 'none';
log('播放器加载完成');
}, 2000);
} catch (error) {
log('初始化失败: ' + error.message);
document.getElementById('loading').textContent = '初始化失败';
}
}
// 页面加载完成后初始化
window.onload = function() {
log('页面加载完成');
init();
};
</script>
</body>
</html>
```
---
### 3. 监控页面 (`pages/visual/index.vue`)
```vue
<template>
<view class="visual-monitoring-page">
<!-- 固定头部 -->
<view class="fixed-header">
<text class="header-title">移动式检修车间</text>
</view>
<!-- 内容区域 -->
<view class="tabbar-content">
<!-- 视频播放区域 - 保持16:9比例 -->
<view v-if="ezstate" class="video-wrapper">
<view class="video-content">
<EzvizVideoPlayer ref="playerVideoRef"></EzvizVideoPlayer>
</view>
</view>
<!-- 暂无数据 -->
<view v-else class="no-data-container">
<view class="no-data-icon">📹</view>
<text class="no-data-text">暂无监控数据</text>
</view>
<!-- 视频信息 -->
<view v-if="ezstate" class="video-info">
<view class="info-item">
<text class="info-label">📡 设备状态</text>
<text class="info-value online">在线</text>
</view>
<view class="info-item">
<text class="info-label">🎥 分辨率</text>
<text class="info-value">高清</text>
</view>
<view class="info-item">
<text class="info-label">🔊 音频</text>
<text class="info-value">开启</text>
</view>
</view>
</view>
</view>
</template>
<script>
import EzvizVideoPlayer from '@/components/EzvizVideoPlayerSimple.vue'
import tokenManager from '@/utils/ezvizTokenManager.js'
export default {
components: {
EzvizVideoPlayer
},
data() {
return {
ezstate: false
}
},
onLoad() {
console.log('监控页面加载')
this.getVideoData()
},
methods: {
async getVideoData() {
try {
// 获取 AccessToken
let accessToken
try {
accessToken = await tokenManager.getValidAccessToken()
console.log('✅ AccessToken获取成功')
} catch (error) {
console.error('❌ AccessToken获取失败使用备用token')
accessToken = "your-backup-access-token"
}
// 配置参数
const ezuikitInfo = {
accessToken: accessToken,
play_url: "ezopen://open.ys7.com/K74237657/1.hd.live"
}
// 先启用视频状态,让组件渲染
this.ezstate = true
// 等待组件渲染完成后初始化播放器
await this.$nextTick()
// 确保ref存在后再调用
if (this.$refs.playerVideoRef) {
this.$refs.playerVideoRef.initEzuikit(ezuikitInfo)
} else {
console.error('❌ 播放器组件未找到')
}
} catch (error) {
console.error('初始化视频失败:', error)
}
}
}
}
</script>
<style lang="scss" scoped>
.visual-monitoring-page {
width: 100%;
height: 100vh;
background: #f5f6fa;
}
.fixed-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 100rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.header-title {
font-size: 32rpx;
color: white;
font-weight: bold;
}
.tabbar-content {
width: 100%;
height: calc(100vh - 100rpx - 100rpx);
margin-top: 100rpx;
padding: 30rpx;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 30rpx;
box-sizing: border-box;
}
.video-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
}
/* 视频内容区域 - 保持16:9宽高比不变形 */
.video-content {
width: 100%;
max-width: 100%;
position: relative;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
background: #000;
/* 使用padding-top技巧保持16:9宽高比 */
&::before {
content: '';
display: block;
padding-top: 56.25%; /* 16:9 = 9/16 = 0.5625 = 56.25% */
}
/* 播放器绝对定位填充容器 */
:deep(.simple-video-player) {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
.no-data-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400rpx;
background: linear-gradient(135deg, #f5f7fa 0%, #e3e7f0 100%);
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.no-data-icon {
font-size: 120rpx;
margin-bottom: 20rpx;
opacity: 0.6;
}
.no-data-text {
font-size: 32rpx;
color: #666;
}
.video-info {
display: flex;
gap: 20rpx;
padding: 20rpx 30rpx;
background: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
.info-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
padding: 15rpx 10rpx;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 10rpx;
}
.info-label {
font-size: 24rpx;
color: #666;
white-space: nowrap;
}
.info-value {
font-size: 26rpx;
font-weight: bold;
color: #333;
&.online {
color: #4caf50;
}
}
</style>
```
---
### 4. AccessToken 管理 (`utils/ezvizTokenManager.js`)
```javascript
// 萤石云 AccessToken 管理器
class EzvizTokenManager {
constructor() {
this.appKey = 'your-app-key'
this.appSecret = 'your-app-secret'
this.baseUrl = 'https://open.ys7.com/api/lapp'
}
// 获取有效的 AccessToken
async getValidAccessToken() {
// 1. 先从缓存读取
const cached = uni.getStorageSync('ezviz_access_token')
const expireTime = uni.getStorageSync('ezviz_token_expire')
// 2. 检查是否过期提前1小时刷新
const now = Date.now()
if (cached && expireTime && expireTime - now > 3600000) {
console.log('使用缓存的AccessToken')
return cached
}
// 3. 缓存失效,重新获取
console.log('重新获取AccessToken')
return await this.fetchAccessToken()
}
// 从萤石云服务器获取 AccessToken
async fetchAccessToken() {
return new Promise((resolve, reject) => {
uni.request({
url: `${this.baseUrl}/token/get`,
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: {
appKey: this.appKey,
appSecret: this.appSecret
},
success: (res) => {
if (res.data.code === '200') {
const accessToken = res.data.data.accessToken
const expireTime = Date.now() + (res.data.data.expireTime * 1000)
// 缓存 token
uni.setStorageSync('ezviz_access_token', accessToken)
uni.setStorageSync('ezviz_token_expire', expireTime)
console.log('✅ AccessToken获取成功')
resolve(accessToken)
} else {
reject(new Error(res.data.msg || '获取AccessToken失败'))
}
},
fail: (error) => {
reject(error)
}
})
})
}
}
export default new EzvizTokenManager()
```
---
## ⚙️ 配置说明
### 1. pages.json横屏配置
```json
{
"pages": [
{
"path": "pages/visual/index",
"style": {
"navigationBarTitleText": "移动式检修车间",
"navigationStyle": "custom",
"pageOrientation": "landscape" // ← 横屏展示
}
}
]
}
```
---
### 2. manifest.json内存配置
```json
{
"app-plus": {
"compatible": {
"largeHeap": true // ← 启用大内存堆512MB
}
}
}
```
---
### 3. 萤石云参数说明
#### AccessToken 获取
```javascript
// API: https://open.ys7.com/api/lapp/token/get
// 方法: POST
// 参数:
{
appKey: "your-app-key",
appSecret: "your-app-secret"
}
// 返回:
{
code: "200",
data: {
accessToken: "at.xxx...",
expireTime: 7200 // 秒默认2小时
}
}
```
#### ezopen 播放地址格式
```
ezopen://open.ys7.com/{设备序列号}/{通道号}.{清晰度}.live
@ -1223,3 +653,5 @@ onHide() {
🎉 **恭喜萤石云APP对接完成** 🎉