萤石云对接、温湿度卡片合并、接口更新

This commit is contained in:
吉浩茹
2025-10-06 15:06:48 +08:00
parent 5f20cc7cd3
commit 4b65bea0bb
21 changed files with 5445 additions and 1027 deletions

View File

@ -3,7 +3,6 @@ import mqttDataManager from '@/utils/mqttDataManager.js'
export default {
onLaunch: function () {
console.log('App Launch')
// 应用启动时的初始化逻辑
this.initApp()
},
@ -26,17 +25,17 @@ export default {
}
// 显示平台信息
console.log('📱 当前平台:',
// #ifdef H5
'H5'
// #endif
// #ifdef APP-PLUS
'APP-PLUS'
// #endif
// #ifdef MP-WEIXIN
'MP-WEIXIN'
// #endif
)
let platform = 'Unknown'
// #ifdef H5
platform = 'H5'
// #endif
// #ifdef APP-PLUS
platform = 'APP-PLUS'
// #endif
// #ifdef MP-WEIXIN
platform = 'MP-WEIXIN'
// #endif
console.log('📱 当前平台:', platform)
// MQTT连接已在mqttDataManager中自动初始化
console.log('✅ 应用初始化完成')
@ -55,7 +54,7 @@ page {
sans-serif;
height: 100%;
width: 100%;
background-color: #f5f5f5;
background-color: #f5f6fa;
}
/* 确保根元素和页面容器都是100%高度 */
@ -74,15 +73,16 @@ page {
/* 固定头部样式 */
.fixed-header {
// position: fixed;
// top: 0;
// left: 0;
// right: 0;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 40px;
z-index: 1000;
background-color: #3f51b5;
padding: 20rpx 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
background-color: #ffffff;
padding: 15rpx 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
border-bottom: 2rpx solid #e1e5e9;
display: flex;
flex-direction: row;
justify-content: center;
@ -90,9 +90,9 @@ page {
}
.header-title {
color: white;
color: #2c3e50;
font-size: 32rpx;
font-weight: bold;
font-weight: 600;
text-align: center;
}
@ -102,7 +102,7 @@ page {
padding-top: 100rpx; /* 为固定头部留出空间 */
padding-bottom: 200rpx; /* 为tabbar留出空间 */
overflow-y: auto;
background-color: #f5f5f5;
background-color: #f5f6fa;
display: flex;
flex-direction: column;
}
@ -110,13 +110,14 @@ page {
/* tabbar页面内容区域 */
.tabbar-content {
flex: 1;
padding: 20rpx;
// padding: 0 20rpx; /* 增加底部padding为tabbar留出空间 */
margin-top: 100rpx; /* 为固定头部留出空间,增加距离 */
overflow-y: auto;
display: flex;
flex-direction: column;
// padding-bottom: 60px
// min-height: calc(100vh - 200rpx); /* 调整最小高度计算 */
// #ifdef H5
margin-bottom: 50px;
// margin-bottom: 50px;
// #endif
}
@ -148,29 +149,69 @@ button::after {
/* 卡片样式 */
.card {
background: white;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
background: #ffffff;
border-radius: 8rpx;
padding: 20rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
border: 1rpx solid #e1e5e9;
}
/* 按钮样式 */
.btn-primary {
background-color: #3f51b5;
background-color: #2980b9;
color: white;
padding: 20rpx 40rpx;
border-radius: 8rpx;
font-size: 28rpx;
padding: 16rpx 20rpx;
border-radius: 6rpx;
font-size: 26rpx;
border: none;
font-weight: 600;
transition: background-color 0.2s ease;
}
.btn-primary:active {
background-color: #21618c;
}
.btn-secondary {
background-color: #666;
background-color: #7f8c8d;
color: white;
padding: 20rpx 40rpx;
border-radius: 8rpx;
font-size: 28rpx;
padding: 16rpx 20rpx;
border-radius: 6rpx;
font-size: 26rpx;
border: none;
font-weight: 600;
transition: background-color 0.2s ease;
}
.btn-secondary:active {
background-color: #6c7b7d;
}
/* 响应式设计 */
@media screen and (max-width: 750rpx) {
.fixed-header {
padding: 16rpx 20rpx;
}
.tabbar-content {
padding: 16rpx 16rpx 100rpx; /* 调整小屏幕下的内边距 */
margin-top: 90rpx; /* 调整小屏幕下的顶部间距 */
}
}
@media screen and (max-width: 600rpx) {
.fixed-header {
padding: 12rpx 16rpx;
}
.tabbar-content {
padding: 16rpx 16rpx 90rpx; /* 更小屏幕下的内边距 */
margin-top: 80rpx; /* 更小屏幕下的顶部间距 */
}
.header-title {
font-size: 28rpx;
}
}
</style>

View File

@ -0,0 +1,351 @@
<template>
<view class="simple-video-player">
<view class="debug-info" v-if="showDebug">
<text>平台: {{ platform }}</text>
<text>状态: {{ status }}</text>
<text>播放状态: {{ isPlaying ? '播放中' : '已暂停' }}</text>
</view>
<!-- APP平台使用web-view -->
<!-- #ifdef APP-PLUS -->
<web-view
v-if="webviewUrl"
ref="videoWebview"
:src="webviewUrl"
class="video-webview"
@message="handleMessage"
></web-view>
<!-- #endif -->
<!-- H5平台提示 -->
<!-- #ifdef H5 -->
<view class="h5-tip">H5平台暂不支持</view>
<!-- #endif -->
<!-- 控制按钮 -->
<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 v-if="loading" class="loading">
<text>{{ loadingText }}</text>
</view>
<view v-if="error" class="error">
<text>{{ errorText }}</text>
<button @click="retry">重试</button>
</view>
</view>
</template>
<script>
export default {
name: 'EzvizVideoPlayerSimple',
props: {
showDebug: {
type: Boolean,
default: false
}
},
data() {
return {
platform: '',
status: '未初始化',
webviewUrl: '',
loading: false,
loadingText: '',
error: false,
errorText: '',
config: null,
isPlaying: true // 默认自动播放
}
},
mounted() {
this.detectPlatform()
},
methods: {
detectPlatform() {
// #ifdef H5
this.platform = 'H5'
// #endif
// #ifdef APP-PLUS
this.platform = 'APP'
// #endif
console.log('[简单播放器] 平台:', this.platform)
},
initEzuikit(config) {
console.log('[简单播放器] 初始化:', config)
if (!config || !config.accessToken || !config.play_url) {
this.error = true
this.errorText = '配置参数不完整'
return
}
this.config = config
this.loading = true
this.loadingText = '正在加载播放器...'
this.status = '加载中'
try {
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('[简单播放器] 使用iframe版本URL已设置')
setTimeout(() => {
if (this.loading) {
this.loading = false
this.status = '播放器已加载'
}
}, 3000)
} catch (err) {
console.error('[简单播放器] 错误:', err)
this.error = true
this.errorText = err.message
this.loading = false
}
},
handleMessage(event) {
console.log('[简单播放器] 收到消息:', event)
try {
const data = event.detail.data
const msg = Array.isArray(data) ? data[0] : data
if (msg && msg.type === 'success') {
this.loading = false
this.status = '播放成功'
} else if (msg && msg.type === 'error') {
this.loading = false
this.error = true
this.errorText = msg.message || '播放失败'
}
} catch (err) {
console.error('[简单播放器] 消息处理错误:', err)
}
},
retry() {
this.error = false
this.errorText = ''
if (this.config) {
this.initEzuikit(this.config)
}
},
// 切换播放/暂停
togglePlay() {
console.log('[简单播放器] 切换播放状态:', this.isPlaying ? '暂停' : '播放')
// 通过重新加载URL来实现播放/暂停
// 因为iframe播放器不支持直接控制所以采用重新加载的方式
if (this.isPlaying) {
// 暂停清空URL
this.webviewUrl = ''
this.isPlaying = false
this.status = '已暂停'
// 触发状态变化事件
this.$emit('playStateChange', false)
} else {
// 播放重新设置URL
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
this.status = '播放中'
// 触发状态变化事件
this.$emit('playStateChange', true)
}
}
// 返回新的播放状态
return this.isPlaying
},
// 获取当前播放状态
getPlayState() {
return this.isPlaying
},
// 刷新播放器
refresh() {
console.log('[简单播放器] 刷新播放器')
if (this.config) {
uni.showToast({
title: '正在刷新...',
icon: 'loading',
duration: 1000
})
// 先清空再重新加载
this.webviewUrl = ''
setTimeout(() => {
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
this.status = '播放中'
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
})
}, 500)
}
}
}
}
</script>
<style scoped>
.simple-video-player {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background: #000;
}
.debug-info {
position: absolute;
top: 10rpx;
left: 10rpx;
background: rgba(0,0,0,0.7);
color: white;
padding: 10rpx;
font-size: 24rpx;
z-index: 100;
}
.debug-info text {
display: block;
margin: 5rpx 0;
}
.video-webview {
width: 100%;
height: 50%;
}
.h5-tip {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: white;
font-size: 28rpx;
}
.loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.8);
color: white;
font-size: 28rpx;
z-index: 50;
}
.error {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255,0,0,0.8);
color: white;
font-size: 28rpx;
z-index: 50;
padding: 40rpx;
}
.error text {
margin-bottom: 30rpx;
text-align: center;
}
.error button {
background: white;
color: #333;
padding: 20rpx 40rpx;
border-radius: 10rpx;
}
/* 控制按钮 */
.control-buttons {
position: absolute;
bottom: 30rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20rpx;
z-index: 100;
}
.control-btn {
background: rgba(0, 0, 0, 0.75);
color: white;
border: 2rpx solid rgba(255, 255, 255, 0.6);
padding: 16rpx 32rpx;
border-radius: 40rpx;
font-size: 24rpx;
font-weight: 600;
min-width: 140rpx;
backdrop-filter: blur(10rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
}
.control-btn:active {
background: rgba(0, 0, 0, 0.9);
transform: scale(0.96);
}
.play-btn {
background: rgba(46, 125, 50, 0.85);
border-color: rgba(76, 175, 80, 0.9);
}
.play-btn:active {
background: rgba(46, 125, 50, 1);
}
.refresh-btn {
background: rgba(25, 118, 210, 0.85);
border-color: rgba(33, 150, 243, 0.9);
}
.refresh-btn:active {
background: rgba(25, 118, 210, 1);
}
</style>

View File

@ -1,28 +1,28 @@
import {
createSSRApp
} from "vue";
import App from "./App.vue";
import httpService from "./utils/http.js";
import api from "./utils/api.js";
export function createApp() {
const app = createSSRApp(App);
// 注册全局HTTP服务
app.config.globalProperties.$http = httpService;
app.config.globalProperties.$api = api;
return {
app,
};
}
// #ifndef MP
// 处理 wx.connectSocket promisify 兼容问题,强制返回 SocketTask
uni.connectSocket = (function(connectSocket) {
return function(options) {
options.success = options.success || function() {}
return connectSocket.call(this, options)
}
})(uni.connectSocket)
// #endif
import {
createSSRApp
} from "vue";
import App from "./App.vue";
import httpService from "./utils/http.js";
import api from "./utils/api.js";
export function createApp() {
const app = createSSRApp(App);
// 注册全局HTTP服务
app.config.globalProperties.$http = httpService;
app.config.globalProperties.$api = api;
return {
app,
};
}
// #ifndef MP
// 处理 wx.connectSocket promisify 兼容问题,强制返回 SocketTask
uni.connectSocket = (function(connectSocket) {
return function(options) {
options.success = options.success || function() {}
return connectSocket.call(this, options)
}
})(uni.connectSocket)
// #endif

View File

@ -11,6 +11,9 @@
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 2,
"orientation" : "portrait",
"compatible" : {
"largeHeap" : true
},
"icons" : {
"app" : {
"hdpi" : "static/app-icon.png",

View File

@ -24,7 +24,7 @@
"path": "pages/visual/index",
"style": {
"navigationBarTitleText": "移动式检修车间",
"navigationStyle": "custom"
"pageOrientation": "landscape"
}
},
{
@ -45,23 +45,23 @@
"path": "pages/system/index",
"style": {
"navigationBarTitleText": "移动式检修车间",
"navigationStyle": "custom",
"orientation": "landscape"
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTextStyle": "black",
"navigationBarTitleText": "移动式检修车间系统",
"navigationBarBackgroundColor": "#3f51b5",
"backgroundColor": "#F8F8F8"
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f5f6fa"
},
"tabBar": {
"color": "#666666",
"selectedColor": "#3f51b5",
"color": "#7f8c8d",
"selectedColor": "#2980b9",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"fontSize": "14px",
"borderStyle": "white",
"fontSize": "13px",
"height": "65px",
"list": [
{
"pagePath": "pages/environment/index",

File diff suppressed because it is too large Load Diff

View File

@ -7,17 +7,22 @@
<!-- 内容区域 -->
<view class="tabbar-content">
<!-- 日期选择 -->
<!-- 日期导航 -->
<view class="date-selector">
<picker mode="date" :value="selectedDate" @change="onDateChange">
<view class="date-picker">
<view class="date-navigation">
<button class="nav-button prev-button" @click="goToPreviousDay">
<!-- <text class="nav-icon"></text> -->
<text class="nav-text">上一天</text>
</button>
<view class="current-date">
<text class="date-text">{{ selectedDate }}</text>
<text class="picker-arrow"></text>
<!-- <text class="date-weekday">{{ getWeekday(selectedDate) }}</text> -->
</view>
</picker>
<!-- <view class="data-status" :class="dataStatus.dataSource">
<text class="status-text">{{ dataStatus.dataSource === 'api' ? '实时数据' : '示例数据' }}</text>
</view> -->
<button class="nav-button next-button" @click="goToNextDay">
<text class="nav-text">下一天</text>
<!-- <text class="nav-icon"></text> -->
</button>
</view>
</view>
<!-- 温度趋势图表 -->
@ -101,10 +106,12 @@ export default {
},
// 页面初始化状态
hasInitialized: false,
// 查询模式:'default' 表示过去24小时'date' 表示按日期查询
queryMode: 'default',
// ECharts配置选项
temperatureOption: {
title: {
text: '温度趋势',
text: '',
left: 'center',
textStyle: {
fontSize: 16,
@ -115,7 +122,7 @@ export default {
trigger: 'axis',
formatter: function(params) {
const data = params[0];
return `时间: ${data.axisValue}<br/>温度: ${data.value}°C`;
return `时间: ${data.axisValue} 温度: ${data.value}°C`;
}
},
grid: {
@ -126,7 +133,7 @@ export default {
},
xAxis: {
type: 'category',
data: Array.from({length: 24}, (_, i) => `${i.toString().padStart(2, '0')}:00`),
data: this.generateXAxisLabels(),
axisLabel: {
interval: 3, // 每4个小时显示一个标签
fontSize: 10
@ -172,7 +179,7 @@ export default {
},
humidityOption: {
title: {
text: '湿度趋势',
text: '',
left: 'center',
textStyle: {
fontSize: 16,
@ -183,7 +190,7 @@ export default {
trigger: 'axis',
formatter: function(params) {
const data = params[0];
return `时间: ${data.axisValue}<br/>湿度: ${data.value}%`;
return `时间: ${data.axisValue} 湿度: ${data.value}%`;
}
},
grid: {
@ -194,7 +201,7 @@ export default {
},
xAxis: {
type: 'category',
data: Array.from({length: 24}, (_, i) => `${i.toString().padStart(2, '0')}:00`),
data: this.generateXAxisLabels(),
axisLabel: {
interval: 3,
fontSize: 10
@ -240,7 +247,7 @@ export default {
},
pm25Option: {
title: {
text: 'PM2.5趋势',
text: '',
left: 'center',
textStyle: {
fontSize: 16,
@ -251,7 +258,7 @@ export default {
trigger: 'axis',
formatter: function(params) {
const data = params[0];
return `时间: ${data.axisValue}<br/>PM2.5: ${data.value}μg/m³`;
return `时间: ${data.axisValue} PM2.5: ${data.value}μg/m³`;
}
},
grid: {
@ -262,7 +269,7 @@ export default {
},
xAxis: {
type: 'category',
data: Array.from({length: 24}, (_, i) => `${i.toString().padStart(2, '0')}:00`),
data: this.generateXAxisLabels(),
axisLabel: {
interval: 3,
fontSize: 10
@ -334,19 +341,90 @@ export default {
}
},
methods: {
// 生成x轴标签
generateXAxisLabels() {
if (this.queryMode === 'date') {
// 按日期查询时显示0-23小时
return Array.from({length: 24}, (_, i) => `${i.toString().padStart(2, '0')}:00`)
} else {
// 默认查询时显示当前时间之前24小时整点时间
const now = new Date()
const currentHour = now.getHours()
const labels = []
for (let i = 23; i >= 0; i--) {
// 计算目标小时
let targetHour = currentHour - i
if (targetHour < 0) {
targetHour += 24 // 跨天处理
}
labels.push(`${String(targetHour).padStart(2, '0')}:00`)
}
return labels
}
},
// 获取今天的日期(本地时区)
getTodayDate() {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
console.log('📅 获取今天日期:', {
'原始Date对象': today,
'ISO字符串': today.toISOString(),
'本地字符串': today.toLocaleString(),
'本地日期字符串': today.toLocaleDateString(),
'本地时间字符串': today.toLocaleTimeString(),
'时区偏移': today.getTimezoneOffset(),
'格式化结果': `${year}-${month}-${day}`
})
return `${year}-${month}-${day}`
},
// 格式化日期为 YYYY-MM-DD 格式
formatDate(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
// 获取星期几
getWeekday(dateString) {
const date = new Date(dateString)
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
return weekdays[date.getDay()]
},
// 获取时间范围显示文本
getTimeRangeText() {
if (this.queryMode === 'date') {
return `查询日期: ${this.selectedDate}`
} else {
const now = new Date()
const past24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const startTime = this.formatTimeDisplay(past24Hours)
const endTime = this.formatTimeDisplay(now)
return `过去24小时: ${startTime} ~ ${endTime}`
}
},
// 格式化时间显示(用于界面显示)
formatTimeDisplay(date) {
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${month}-${day} ${hours}:${minutes}`
},
// 初始化MQTT监听
initMqttListener() {
// 监听数据更新
this.dataUpdateHandler = (data) => {
console.log('参数记录页面收到MQTT数据:', data)
// this.updateChartData(data)
}
mqttDataManager.addListener('dataUpdate', this.dataUpdateHandler)
@ -354,7 +432,6 @@ export default {
// 监听连接状态
this.statusUpdateHandler = (status) => {
this.connectionStatus = status
console.log('参数记录页面连接状态更新:', status)
}
mqttDataManager.addListener('connectionStatus', this.statusUpdateHandler)
@ -364,25 +441,33 @@ export default {
// 更新图表数据
updateChartData(data) {
console.log('📊 参数记录页面更新数据:', data)
// 只处理WSD设备的数据
if (data.deviceType === 'WSD') {
const now = new Date()
const currentHour = now.getHours()
const currentMinute = now.getMinutes()
const currentTimeInHours = currentHour + currentMinute / 60
// 更新对应小时的数据
// 计算数据在数组中的索引位置
let dataIndex
if (this.queryMode === 'date') {
// 按日期查询时,直接使用当前小时
dataIndex = currentHour
} else {
// 默认查询时当前时间对应索引0最新数据
dataIndex = 0
}
// 更新对应位置的数据
if (data.temperature !== undefined) {
Math.round(data.temperature) && (this.temperatureData[currentHour] = Math.round(data.temperature))
console.log(`✅ 温度数据已更新 - 小时${currentHour}:`, this.temperatureData[currentHour])
Math.round(data.temperature) && (this.temperatureData[dataIndex] = Math.round(data.temperature))
}
if (data.humidity !== undefined) {
Math.round(data.humidity) && (this.humidityData[currentHour] = Math.round(data.humidity))
console.log(`✅ 湿度数据已更新 - 小时${currentHour}:`, this.humidityData[currentHour])
Math.round(data.humidity) && (this.humidityData[dataIndex] = Math.round(data.humidity))
}
if (data.pm !== undefined) {
Math.round(data.pm) && (this.pm25Data[currentHour] = Math.round(data.pm))
console.log(`✅ PM2.5数据已更新 - 小时${currentHour}:`, this.pm25Data[currentHour])
Math.round(data.pm) && (this.pm25Data[dataIndex] = Math.round(data.pm))
}
// 重新绘制图表
@ -391,9 +476,9 @@ export default {
})
console.log('✅ 图表数据更新完成:', {
temperature: this.temperatureData[currentHour],
humidity: this.humidityData[currentHour],
hour: currentHour
temperature: this.temperatureData[dataIndex],
humidity: this.humidityData[dataIndex],
dataIndex: dataIndex
})
} else {
console.log('⚠️ 非WSD设备数据跳过图表更新:', data.deviceType)
@ -402,26 +487,29 @@ export default {
// 图表初始化方法
initTemperatureChart() {
console.log('初始化温度图表')
this.temperatureOption.xAxis.data = this.generateXAxisLabels()
this.temperatureOption.series[0].data = this.temperatureData
this.$refs.temperatureChartRef.init(this.temperatureOption)
},
initHumidityChart() {
console.log('初始化湿度图表')
this.humidityOption.xAxis.data = this.generateXAxisLabels()
this.humidityOption.series[0].data = this.humidityData
this.$refs.humidityChartRef.init(this.humidityOption)
},
initPM25Chart() {
console.log('初始化PM2.5图表')
this.pm25Option.xAxis.data = this.generateXAxisLabels()
this.pm25Option.series[0].data = this.pm25Data
this.$refs.pm25ChartRef.init(this.pm25Option)
},
onDateChange(e) {
this.selectedDate = e.detail.value
console.log('📅 日期已更改为:', this.selectedDate)
// 上一天
goToPreviousDay() {
const currentDate = new Date(this.selectedDate)
currentDate.setDate(currentDate.getDate() - 1)
this.selectedDate = this.formatDate(currentDate)
this.queryMode = 'date' // 切换到按日期查询模式
// 显示加载状态
uni.showLoading({
@ -429,29 +517,64 @@ export default {
})
// 重新获取历史数据
this.getHistoryData().finally(() => {
this.getHistoryDataByDate().finally(() => {
uni.hideLoading()
})
},
// 下一天
goToNextDay() {
const currentDate = new Date(this.selectedDate)
const today = new Date()
// 检查是否已经是今天,如果是则不允许继续往后
if (this.selectedDate >= this.formatDate(today)) {
uni.showToast({
title: '不能查看未来日期',
icon: 'none',
duration: 2000
})
return
}
currentDate.setDate(currentDate.getDate() + 1)
this.selectedDate = this.formatDate(currentDate)
this.queryMode = 'date' // 切换到按日期查询模式
// 显示加载状态
uni.showLoading({
title: '加载数据中...'
})
// 重新获取历史数据
this.getHistoryDataByDate().finally(() => {
uni.hideLoading()
})
},
// 更新图表数据
updateCharts() {
const xAxisLabels = this.generateXAxisLabels()
if (this.$refs.temperatureChartRef) {
this.temperatureOption.xAxis.data = xAxisLabels
this.temperatureOption.series[0].data = this.temperatureData
this.$refs.temperatureChartRef.setOption(this.temperatureOption)
}
if (this.$refs.humidityChartRef) {
this.humidityOption.xAxis.data = xAxisLabels
this.humidityOption.series[0].data = this.humidityData
this.$refs.humidityChartRef.setOption(this.humidityOption)
}
if (this.$refs.pm25ChartRef) {
this.pm25Option.xAxis.data = xAxisLabels
this.pm25Option.series[0].data = this.pm25Data
this.$refs.pm25ChartRef.setOption(this.pm25Option)
}
},
// 获取历史数据
async getHistoryData() {
// 根据选择的日期获取历史数据
async getHistoryDataByDate() {
try {
// 根据选择的日期构建时间范围
const startTime = `${this.selectedDate} 00:00:00`
@ -462,7 +585,83 @@ export default {
endTime: endTime
}
console.log('📊 请求历史数据:', params)
const response = await dataHistoryApi.getHistory(params)
// 处理历史数据
if (response && Array.isArray(response) && response.length > 0) {
this.historyData = response
this.processHistoryData(response)
// 更新数据状态
this.dataStatus = {
isRealData: true,
lastUpdateTime: new Date().toLocaleString(),
dataSource: 'api'
}
// 保存查询事件
await this.createQueryEvent('success', response.length)
} else {
console.log('📊 没有历史数据,显示空状态')
// 没有数据时显示空状态
this.showEmptyState()
// 更新数据状态
this.dataStatus = {
isRealData: false,
lastUpdateTime: new Date().toLocaleString(),
dataSource: 'empty'
}
// 保存查询事件(无数据)
await this.createQueryEvent('empty', 0)
}
return response
} catch (error) {
console.error('❌ 历史数据获取失败:', error)
// 出错时显示空状态
this.showEmptyState()
// 更新数据状态
this.dataStatus = {
isRealData: false,
lastUpdateTime: new Date().toLocaleString(),
dataSource: 'error'
}
// 保存查询事件(错误)
await this.createQueryEvent('error', 0)
// 显示错误提示
uni.showToast({
title: '数据加载失败',
icon: 'none',
duration: 2000
})
throw error
}
},
// 获取历史数据默认过去24小时
async getHistoryData() {
try {
// 构建时间范围从当前时间开始过去24小时
const now = new Date()
const past24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000) // 24小时前
// 格式化时间字符串
const startTime = this.formatDateTimeString(past24Hours)
const endTime = this.formatDateTimeString(now)
const params = {
startTime: startTime,
endTime: endTime
}
console.log('📊 请求历史数据过去24小时:', params)
const response = await dataHistoryApi.getHistory(params)
@ -586,11 +785,37 @@ export default {
const hour = time.getHours()
const minute = time.getMinutes()
// 计算在24小时数组中的索引位置
let dataIndex
if (this.queryMode === 'date') {
// 按日期查询时,直接使用小时作为索引
dataIndex = hour
} else {
// 默认查询时,需要计算相对于当前时间的位置(按小时计算)
const now = new Date()
const currentHour = now.getHours()
// 计算时间差(小时)
let timeDiff = currentHour - hour
if (timeDiff < 0) {
timeDiff += 24 // 跨天的情况
}
// 转换为数组索引23表示24小时前0表示当前时间
dataIndex = 23 - timeDiff
if (dataIndex < 0 || dataIndex >= 24) {
console.log(`⚠️ 数据超出范围,跳过 - 索引:${dataIndex}`)
return // 超出范围的数据跳过
}
}
// 处理温度数据 (wd是温度)
const temperature = item.wd || item.temperature || item.temp || item.T
if (temperature !== undefined && temperature !== null && temperature >= 0) {
this.chartData.temperature.push({
time: hour + minute / 60,
time: dataIndex,
value: Number(temperature),
timestamp: item.createTime || item.timestamp || item.time
})
@ -600,7 +825,7 @@ export default {
const humidity = item.sd || item.humidity || item.hum || item.H
if (humidity !== undefined && humidity !== null && humidity >= 0) {
this.chartData.humidity.push({
time: hour + minute / 60,
time: dataIndex,
value: Number(humidity),
timestamp: item.createTime || item.timestamp || item.time
})
@ -610,7 +835,7 @@ export default {
const pm = item.pm || item.pm25 || item.pm2_5 || item.PM
if (pm !== undefined && pm !== null && pm >= 0) {
this.chartData.pm.push({
time: hour + minute / 60,
time: dataIndex,
value: Number(pm),
timestamp: item.createTime || item.timestamp || item.time
})
@ -633,17 +858,20 @@ export default {
updateChartsWithHistoryData() {
console.log('🎨 使用历史数据更新图表')
const xAxisLabels = this.generateXAxisLabels()
// 处理温度数据
if (this.chartData.temperature.length > 0) {
const temperatureData = new Array(24).fill(0)
this.chartData.temperature.forEach(item => {
const hour = Math.floor(item.time)
if (hour >= 0 && hour < 24) {
temperatureData[hour] = item.value || 0
const dataIndex = Math.floor(item.time)
if (dataIndex >= 0 && dataIndex < 24) {
temperatureData[dataIndex] = item.value || 0
}
})
this.temperatureData = temperatureData
if (this.$refs.temperatureChartRef) {
this.temperatureOption.xAxis.data = xAxisLabels
this.temperatureOption.series[0].data = temperatureData
this.$refs.temperatureChartRef.setOption(this.temperatureOption)
}
@ -651,6 +879,7 @@ export default {
// 没有温度数据时使用0填充
this.temperatureData = new Array(24).fill(0)
if (this.$refs.temperatureChartRef) {
this.temperatureOption.xAxis.data = xAxisLabels
this.temperatureOption.series[0].data = this.temperatureData
this.$refs.temperatureChartRef.setOption(this.temperatureOption)
}
@ -660,13 +889,14 @@ export default {
if (this.chartData.humidity.length > 0) {
const humidityData = new Array(24).fill(0)
this.chartData.humidity.forEach(item => {
const hour = Math.floor(item.time)
if (hour >= 0 && hour < 24) {
humidityData[hour] = item.value || 0
const dataIndex = Math.floor(item.time)
if (dataIndex >= 0 && dataIndex < 24) {
humidityData[dataIndex] = item.value || 0
}
})
this.humidityData = humidityData
if (this.$refs.humidityChartRef) {
this.humidityOption.xAxis.data = xAxisLabels
this.humidityOption.series[0].data = humidityData
this.$refs.humidityChartRef.setOption(this.humidityOption)
}
@ -674,6 +904,7 @@ export default {
// 没有湿度数据时使用0填充
this.humidityData = new Array(24).fill(0)
if (this.$refs.humidityChartRef) {
this.humidityOption.xAxis.data = xAxisLabels
this.humidityOption.series[0].data = this.humidityData
this.$refs.humidityChartRef.setOption(this.humidityOption)
}
@ -683,13 +914,14 @@ export default {
if (this.chartData.pm.length > 0) {
const pmData = new Array(24).fill(0)
this.chartData.pm.forEach(item => {
const hour = Math.floor(item.time)
if (hour >= 0 && hour < 24) {
pmData[hour] = item.value || 0
const dataIndex = Math.floor(item.time)
if (dataIndex >= 0 && dataIndex < 24) {
pmData[dataIndex] = item.value || 0
}
})
this.pm25Data = pmData
if (this.$refs.pm25ChartRef) {
this.pm25Option.xAxis.data = xAxisLabels
this.pm25Option.series[0].data = pmData
this.$refs.pm25ChartRef.setOption(this.pm25Option)
}
@ -697,6 +929,7 @@ export default {
// 没有PM数据时使用0填充
this.pm25Data = new Array(24).fill(0)
if (this.$refs.pm25ChartRef) {
this.pm25Option.xAxis.data = xAxisLabels
this.pm25Option.series[0].data = this.pm25Data
this.$refs.pm25ChartRef.setOption(this.pm25Option)
}
@ -752,6 +985,19 @@ export default {
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
const result = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
return result
},
// 格式化时间字符串用于API请求
formatDateTimeString(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
}
@ -768,12 +1014,20 @@ export default {
.date-selector {
background: white;
border-radius: 8rpx;
padding: 15rpx;
margin-bottom: 15rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.time-range-info {
flex: 1;
}
.time-range-text {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.connection-status {
@ -809,23 +1063,70 @@ export default {
font-size: 24rpx;
}
.date-picker {
.date-navigation {
display: flex;
align-items: center;
gap: 8rpx;
padding: 15rpx 20rpx;
background-color: #f8f8f8;
border-radius: 6rpx;
gap: 15rpx;
justify-content: space-between;
}
.nav-button {
// padding: 12rpx 20rpx;
// background: #007aff;
// color: white;
margin: 0;
border: none;
border-radius: 8rpx;
font-size: 24rpx;
// min-width: 120rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 6rpx;
transition: all 0.2s ease;
}
.nav-button:active {
// background: #0056b3;
transform: scale(0.95);
}
.nav-button::after {
border: none;
}
.nav-icon {
font-size: 24rpx;
color: white;
}
.nav-text {
// color: white;
font-size: 24rpx;
font-weight: 500;
}
.current-date {
padding: 16rpx 24rpx;
background: #f8f9fa;
border-radius: 8rpx;
min-width: 180rpx;
text-align: center;
border: 1rpx solid #e9ecef;
}
.date-text {
font-size: 28rpx;
color: #333;
font-weight: 600;
display: block;
margin-bottom: 4rpx;
}
.picker-arrow {
color: #999;
font-size: 20rpx;
.date-weekday {
font-size: 22rpx;
color: #666;
font-weight: 400;
}
.chart-card {
@ -839,7 +1140,7 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
// margin-bottom: 30rpx;
}
.chart-title {

View File

@ -1,22 +1,86 @@
<template>
<view class="visual-monitoring-page">
<!-- 固定头部 -->
<!-- 固定头部 - 有视频时隐藏 -->
<view class="fixed-header">
<text class="header-title">移动式检修车间</text>
</view>
<!-- 内容区域 -->
<view class="tabbar-content">
<!-- <demo /> -->
<view class="no-data-container" v-if="!ezstate">
<view class="no-data-icon">📹</view>
<text class="no-data-text">暂无监控数据</text>
</view>
<!-- 视频播放区域 - 保持16:9比例 -->
<view v-else-if="ezstate" :key="videoData" class="video-wrapper">
<view class="video-content">
<!-- 使用简化版播放器 -->
<EzvizVideoPlayer
ref="playerVideoRef"
:show-debug="debugMode"
@playStateChange="handlePlayStateChange"
></EzvizVideoPlayer>
</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">
<button @click="checkDevice" class="test-btn">检查设备状态</button>
<button @click="toggleDebug" class="test-btn">
{{ debugMode ? '关闭调试' : '开启调试' }}
</button>
<button @click="getVideoData" class="test-btn">启动视频播放</button>
</view>
</view>
</view>
</template>
<script>
// 改用简化版播放器
import EzvizVideoPlayer from '@/components/EzvizVideoPlayerSimple.vue'
import tokenManager from '@/utils/ezvizTokenManager.js'
import deviceChecker from '@/utils/ezvizDeviceChecker.js'
export default {
components: {
EzvizVideoPlayer
},
data() {
return {
ezstate:false,
debugMode: true, // 默认开启调试模式
videoLoaded: false,
isRecording: false,
isPlaying: true, // 播放状态
cameraStatus: {
text: '离线',
class: 'offline'
@ -51,105 +115,187 @@ export default {
},
onLoad() {
console.log('视觉监控页面加载')
this.getVideoData()
},
onShow() {
console.log('📱 视觉监控页面显示,触发页面更新')
// 可以在这里添加重新连接摄像头等逻辑
this.getVideoData()
},
methods: {
connectCamera() {
// 切换调试模式
toggleDebug() {
this.debugMode = !this.debugMode
console.log('调试模式:', this.debugMode ? '开启' : '关闭')
uni.showToast({
title: this.debugMode ? '调试模式已开启' : '调试模式已关闭',
icon: 'success',
duration: 1500
})
},
// 初始化播放器
async handleInitPlayer() {
console.log('🔄 重新初始化播放器...')
uni.showLoading({
title: '连接中...'
title: '正在初始化...'
})
setTimeout(() => {
try {
// 重新获取视频数据并初始化
await this.getVideoData()
uni.hideLoading()
this.videoLoaded = true
this.cameraStatus = {
text: '在线',
class: 'online'
}
uni.showToast({
title: '摄像头连接成功',
icon: 'success'
title: '初始化成功',
icon: 'success',
duration: 2000
})
// 重置播放状态
this.isPlaying = true
} catch (error) {
uni.hideLoading()
console.error('初始化失败:', error)
uni.showToast({
title: '初始化失败',
icon: 'error',
duration: 2000
})
}, 2000)
},
toggleRecording() {
this.isRecording = !this.isRecording
this.recordingStatus = {
text: this.isRecording ? '录制中' : '未录制',
class: this.isRecording ? 'recording' : 'inactive'
}
},
// 切换播放/暂停
handleTogglePlay() {
console.log('🎬 切换播放状态:', this.isPlaying ? '暂停' : '播放')
uni.showToast({
title: this.isRecording ? '开始录制' : '停止录制',
icon: 'success'
if (this.$refs.playerVideoRef) {
// 调用播放器组件的切换播放方法(状态会通过事件同步)
this.$refs.playerVideoRef.togglePlay()
} else {
console.error('❌ 播放器组件未找到')
uni.showToast({
title: '播放器未就绪',
icon: 'error',
duration: 2000
})
}
},
// 处理播放状态变化(由播放器组件触发)
handlePlayStateChange(isPlaying) {
console.log('📡 播放状态变化:', isPlaying ? '播放中' : '已暂停')
this.isPlaying = isPlaying
},
// 检查设备状态
async checkDevice() {
console.log('🔍 开始检查设备状态...')
const playUrl = "ezopen://open.ys7.com/FT1718031/1.hd.live"
uni.showLoading({
title: '检查设备中...'
})
},
takeSnapshot() {
uni.showToast({
title: '拍照成功',
icon: 'success'
})
},
toggleFullscreen() {
uni.showToast({
title: '全屏功能开发中',
icon: 'none'
})
},
onQualityChange(e) {
this.qualityIndex = e.detail.value
},
onDurationChange(e) {
this.durationIndex = e.detail.value
},
onAutoSaveChange(e) {
this.autoSave = e.detail.value
},
playVideo(item) {
uni.showToast({
title: `播放 ${item.time}`,
icon: 'none'
})
},
downloadVideo(item) {
uni.showToast({
title: `下载 ${item.time}`,
icon: 'success'
})
},
deleteVideo(item) {
uni.showModal({
title: '确认删除',
content: `确定要删除 ${item.time} 的录制文件吗?`,
success: (res) => {
if (res.confirm) {
const index = this.historyList.indexOf(item)
this.historyList.splice(index, 1)
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
try {
const result = await deviceChecker.comprehensiveCheck(playUrl)
uni.hideLoading()
if (result.success) {
const status = result.isOnline ? '在线' : '离线'
const message = `设备 ${result.deviceSerial}: ${status}\n设备名: ${result.device.deviceName || '未知'}`
uni.showModal({
title: '设备检查结果',
content: message,
showCancel: false
})
console.log('✅ 设备检查结果:', result)
} else {
uni.showModal({
title: '设备检查失败',
content: result.error,
showCancel: false
})
console.error('❌ 设备检查失败:', result.error)
}
})
} catch (error) {
uni.hideLoading()
uni.showToast({
title: '检查异常',
icon: 'error',
duration: 3000
})
console.error('设备检查异常:', error)
}
},
clearHistory() {
uni.showModal({
title: '确认清空',
content: '确定要清空所有录制历史吗?',
success: (res) => {
if (res.confirm) {
this.historyList = []
uni.showToast({
title: '清空成功',
icon: 'success'
})
async getVideoData() {
console.log('getVideoData')
try {
let ezuikitInfo = {}
// 使用TokenManager自动获取AccessToken
try {
console.log('🔑 开始获取AccessToken...')
const accessToken = await tokenManager.getValidAccessToken()
ezuikitInfo = {
accessToken: accessToken,
play_url: "ezopen://open.ys7.com/FT1718031/1.hd.live"
}
console.log('✅ 使用自动获取的AccessToken:', accessToken.substring(0, 20) + '...')
} catch (error) {
console.error('❌ 自动获取AccessToken失败:', error)
// 如果自动获取失败使用备用token需要手动更新
console.log('🔄 使用备用AccessToken')
ezuikitInfo = {
accessToken: "at.4q22023n62a4knwpcx1yxavda1sfqfo5-3ns0ca16sb-1wgwwc3-aj2mctqys",
play_url: "ezopen://open.ys7.com/FT1718031/1.hd.live"
}
uni.showToast({
title: 'AccessToken自动获取失败使用备用token',
icon: 'none',
duration: 3000
})
}
})
// 先启用视频状态,让组件渲染
this.ezstate = true
// 等待组件渲染完成后初始化播放器
await this.$nextTick()
// 确保ref存在后再调用
if (this.$refs.playerVideoRef) {
this.$refs.playerVideoRef.initEzuikit(ezuikitInfo)
} else {
console.error('❌ 播放器组件未找到')
uni.showToast({
title: '播放器组件加载失败',
icon: 'error',
duration: 2000
})
}
} catch (error) {
console.error('初始化视频失败:', error)
uni.showToast({
title: '视频初始化失败',
icon: 'none',
duration: 3000
})
}
}
}
}
@ -157,10 +303,180 @@ export default {
<style lang="scss" scoped>
.visual-monitoring-page {
height: 100vh;
width: 100%;
height: 100%;
background: #f5f6fa;
}
/* 内容区域 */
.tabbar-content {
width: 100%;
height: calc(100vh - 100rpx); /* 减去底部tabbar */
padding: 30rpx;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 30rpx;
box-sizing: border-box;
}
/* 视频外层容器 */
.video-wrapper {
width: 100%;
height: 200px;
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;
background-color: #0056b3;
/* 使用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;
}
}
/* 视频控制按钮区域 */
.control-section {
display: flex;
gap: 20rpx;
padding: 0 10rpx;
}
.control-btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: bold;
color: white;
border: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
}
.init-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.init-btn:active {
background: linear-gradient(135deg, #5568d3 0%, #6a4193 100%);
transform: scale(0.98);
}
.pause-btn {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.pause-btn:active {
background: linear-gradient(135deg, #e082ea 0%, #e4465b 100%);
transform: scale(0.98);
}
.test-section {
padding: 40rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
align-items: center;
}
.test-btn {
background: #007aff;
color: white;
border: none;
border-radius: 8rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
width: 300rpx;
}
.test-btn:active {
background: #0056b3;
}
.camera-status {

View File

@ -0,0 +1,130 @@
<!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: 50%;
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>

View File

@ -88,7 +88,7 @@
// 统一的页面内容区域样式
.page-content {
flex: 1;
padding: 24rpx 0;
// padding: 24rpx 0;
overflow: hidden;
display: flex;
flex-direction: column;
@ -329,7 +329,7 @@
}
.page-content {
padding: 28rpx 0;
// padding: 28rpx 0;
}
.header-cell {
@ -360,7 +360,7 @@
}
.page-content {
padding: 20rpx 0;
// padding: 20rpx 0;
}
.header-cell {
@ -401,7 +401,7 @@
}
.page-content {
padding: 32rpx 0;
// padding: 32rpx 0;
}
.header-cell {

View File

@ -13,101 +13,16 @@ export const dataHistoryApi = {
}
}
// 设备相关接口
export const deviceApi = {
// 获取设备详情
getDetail(deviceId) {
return httpService.get(`/api/devices/${deviceId}`)
},
// 获取设备列表
getList(params = {}) {
return httpService.get('/api/devices', params)
},
// 更新设备状态
updateStatus(deviceId, status) {
return httpService.put(`/api/devices/${deviceId}/status`, { status })
}
}
// 环境参数接口
export const environmentApi = {
// 获取环境参数
getParams(params = {}) {
return httpService.get('/api/environment/params', params)
},
// 获取环境参数历史
getHistory(params) {
return httpService.post('/api/environment/history', params)
}
}
// 报警相关接口
export const alarmApi = {
// 获取报警记录
getRecords(params = {}) {
return httpService.get('/api/alarms', params)
},
// 获取报警统计
getStatistics(params = {}) {
return httpService.get('/api/alarms/statistics', params)
},
// 处理报警
handleAlarm(alarmId, action) {
return httpService.put(`/api/alarms/${alarmId}/handle`, { action })
}
}
// 系统日志接口
export const logApi = {
// 获取系统日志
getLogs(params = {}) {
return httpService.get('/api/logs', params)
},
// 获取日志统计
getStatistics(params = {}) {
return httpService.get('/api/logs/statistics', params)
}
}
// 用户相关接口
export const userApi = {
// 用户登录
login(credentials) {
return httpService.post('/api/auth/login', credentials)
},
// 用户登出
logout() {
return httpService.post('/api/auth/logout')
},
// 获取用户信息
getUserInfo() {
return httpService.get('/api/user/info')
},
// 更新用户信息
updateUserInfo(userInfo) {
return httpService.put('/api/user/info', userInfo)
}
}
// 温湿度数据接口
// 空调目标温湿度数据接口
export const thDataApi = {
// 获取最新空调温度
getLatest() {
return httpService.get('/api/th/data/latest')
return httpService.get('/api/ac/data/latest')
},
// 提交温湿度数据
submit(data) {
return httpService.post('/api/th/data', data)
return httpService.post('/api/ac/data', data)
}
}
@ -137,15 +52,21 @@ export const eventApi = {
}
}
// 导出所有API
export default {
dataHistory: dataHistoryApi,
device: deviceApi,
environment: environmentApi,
alarm: alarmApi,
log: logApi,
user: userApi,
thData: thDataApi,
alert: alertApi,
event: eventApi
}
// 温湿度区间设置接口
export const wsdApi = {
// 更新温湿度区间设置
update(data) {
return httpService.post('/api/wsd', data)
},
// 获取温湿度区间设置
getById(id) {
return httpService.get(`/api/wsd/${id}`)
},
getLatest() {
return httpService.get('/api/data/latest')
}
}
export default {}

View File

@ -1,213 +0,0 @@
/**
* 历史数据接口使用示例
* 演示如何使用封装的历史数据API
*/
import { dataHistoryApi } from './api.js'
// 使用示例类
class DataHistoryExample {
// 示例1: 获取指定时间范围的历史数据
async getHistoryData() {
try {
const params = {
startTime: "2025-09-30 06:51:40",
endTime: "2025-09-30 23:51:40"
}
console.log('📊 请求历史数据:', params)
const response = await dataHistoryApi.getHistory(params)
console.log('✅ 历史数据获取成功:', response)
return response
} catch (error) {
console.error('❌ 历史数据获取失败:', error)
throw error
}
}
// 示例2: 获取今天的历史数据
async getTodayHistory() {
try {
const today = new Date()
const startTime = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0)
const endTime = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59)
const params = {
startTime: this.formatDateTime(startTime),
endTime: this.formatDateTime(endTime)
}
console.log('📊 请求今天历史数据:', params)
const response = await dataHistoryApi.getHistory(params)
console.log('✅ 今天历史数据获取成功:', response)
return response
} catch (error) {
console.error('❌ 今天历史数据获取失败:', error)
throw error
}
}
// 示例3: 获取最近7天的历史数据
async getLastWeekHistory() {
try {
const endTime = new Date()
const startTime = new Date(endTime.getTime() - 7 * 24 * 60 * 60 * 1000)
const params = {
startTime: this.formatDateTime(startTime),
endTime: this.formatDateTime(endTime)
}
console.log('📊 请求最近7天历史数据:', params)
const response = await dataHistoryApi.getHistory(params)
console.log('✅ 最近7天历史数据获取成功:', response)
return response
} catch (error) {
console.error('❌ 最近7天历史数据获取失败:', error)
throw error
}
}
// 示例4: 获取指定小时的历史数据
async getHourlyHistory(date, hour) {
try {
const startTime = new Date(date)
startTime.setHours(hour, 0, 0, 0)
const endTime = new Date(startTime)
endTime.setHours(hour + 1, 0, 0, 0)
const params = {
startTime: this.formatDateTime(startTime),
endTime: this.formatDateTime(endTime)
}
console.log('📊 请求指定小时历史数据:', params)
const response = await dataHistoryApi.getHistory(params)
console.log('✅ 指定小时历史数据获取成功:', response)
return response
} catch (error) {
console.error('❌ 指定小时历史数据获取失败:', error)
throw error
}
}
// 格式化日期时间
formatDateTime(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 示例5: 在Vue组件中使用
async useInVueComponent() {
// 在Vue组件的methods中使用
const methods = {
async loadHistoryData() {
try {
uni.showLoading({
title: '加载历史数据中...'
})
const params = {
startTime: "2025-09-30 06:51:40",
endTime: "2025-09-30 23:51:40"
}
// 使用全局注册的API
const response = await this.$api.dataHistory.getHistory(params)
console.log('历史数据:', response)
// 处理响应数据
if (response.code === 200) {
this.historyData = response.data
uni.showToast({
title: '数据加载成功',
icon: 'success'
})
} else {
throw new Error(response.message || '数据加载失败')
}
} catch (error) {
console.error('加载历史数据失败:', error)
uni.showToast({
title: error.message || '数据加载失败',
icon: 'error'
})
} finally {
uni.hideLoading()
}
}
}
return methods
}
}
// 创建实例
const dataHistoryExample = new DataHistoryExample()
export default dataHistoryExample
// 使用示例
/*
// 1. 直接使用API
import { dataHistoryApi } from '@/utils/api.js'
const getData = async () => {
try {
const response = await dataHistoryApi.getHistory({
startTime: "2025-09-30 06:51:40",
endTime: "2025-09-30 23:51:40"
})
console.log('历史数据:', response)
} catch (error) {
console.error('获取失败:', error)
}
}
// 2. 在Vue组件中使用
export default {
methods: {
async loadData() {
try {
const response = await this.$api.dataHistory.getHistory({
startTime: "2025-09-30 06:51:40",
endTime: "2025-09-30 23:51:40"
})
this.data = response.data
} catch (error) {
this.$toast(error.message)
}
}
}
}
// 3. 使用示例类
import dataHistoryExample from '@/utils/dataHistoryExample.js'
const example = new dataHistoryExample()
example.getHistoryData()
example.getTodayHistory()
example.getLastWeekHistory()
*/

View File

@ -0,0 +1,209 @@
// 萤石云设备检查工具
import tokenManager from './ezvizTokenManager.js'
class EzvizDeviceChecker {
constructor() {
this.baseUrl = 'https://open.ys7.com/api/lapp'
}
// 检查设备是否存在
async checkDevice(deviceSerial) {
try {
console.log('🔍 检查设备:', deviceSerial)
const accessToken = await tokenManager.getValidAccessToken()
const response = await uni.request({
url: `${this.baseUrl}/device/info`,
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: {
accessToken: accessToken,
deviceSerial: deviceSerial
}
})
console.log('📡 设备信息响应:', response)
if (response.data && response.data.code === '200') {
const deviceInfo = response.data.data
console.log('✅ 设备信息:', deviceInfo)
return {
success: true,
device: deviceInfo,
isOnline: deviceInfo.status === 1,
deviceName: deviceInfo.deviceName,
deviceType: deviceInfo.deviceType
}
} else {
console.log('❌ 设备查询失败:', response.data?.msg)
return {
success: false,
error: response.data?.msg || '设备查询失败'
}
}
} catch (error) {
console.error('❌ 设备检查异常:', error)
return {
success: false,
error: error.message
}
}
}
// 获取设备列表
async getDeviceList() {
try {
console.log('📋 获取设备列表')
const accessToken = await tokenManager.getValidAccessToken()
const response = await uni.request({
url: `${this.baseUrl}/device/list`,
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: {
accessToken: accessToken,
pageStart: 0,
pageSize: 50
}
})
console.log('📡 设备列表响应:', response)
if (response.data && response.data.code === '200') {
const devices = response.data.data
console.log('✅ 设备列表:', devices)
return {
success: true,
devices: devices,
total: devices.length
}
} else {
console.log('❌ 设备列表获取失败:', response.data?.msg)
return {
success: false,
error: response.data?.msg || '设备列表获取失败'
}
}
} catch (error) {
console.error('❌ 设备列表获取异常:', error)
return {
success: false,
error: error.message
}
}
}
// 检查直播地址是否有效
async checkLiveUrl(deviceSerial, channelNo = 1) {
try {
console.log('🎥 检查直播地址:', deviceSerial, channelNo)
const accessToken = await tokenManager.getValidAccessToken()
const response = await uni.request({
url: `${this.baseUrl}/device/live`,
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: {
accessToken: accessToken,
deviceSerial: deviceSerial,
channelNo: channelNo,
protocol: 1 // 1-rtmp2-hls3-flv
}
})
console.log('📡 直播地址响应:', response)
if (response.data && response.data.code === '200') {
const liveInfo = response.data.data
console.log('✅ 直播地址信息:', liveInfo)
return {
success: true,
liveUrl: liveInfo.url,
hd: liveInfo.hd,
sd: liveInfo.sd
}
} else {
console.log('❌ 直播地址获取失败:', response.data?.msg)
return {
success: false,
error: response.data?.msg || '直播地址获取失败'
}
}
} catch (error) {
console.error('❌ 直播地址检查异常:', error)
return {
success: false,
error: error.message
}
}
}
// 从URL中提取设备序列号
extractDeviceSerial(playUrl) {
try {
// ezopen://open.ys7.com/K74237657/1.hd.live
const match = playUrl.match(/ezopen:\/\/open\.ys7\.com\/([^\/]+)\//)
return match ? match[1] : null
} catch (error) {
console.error('提取设备序列号失败:', error)
return null
}
}
// 综合检查
async comprehensiveCheck(playUrl) {
console.log('🔍 开始综合检查播放地址:', playUrl)
const deviceSerial = this.extractDeviceSerial(playUrl)
if (!deviceSerial) {
return {
success: false,
error: '无法从播放地址中提取设备序列号'
}
}
console.log('📱 提取到设备序列号:', deviceSerial)
// 检查设备信息
const deviceCheck = await this.checkDevice(deviceSerial)
if (!deviceCheck.success) {
return {
success: false,
error: `设备检查失败: ${deviceCheck.error}`,
deviceSerial: deviceSerial
}
}
// 检查直播地址
const liveCheck = await this.checkLiveUrl(deviceSerial)
return {
success: true,
deviceSerial: deviceSerial,
device: deviceCheck.device,
isOnline: deviceCheck.isOnline,
liveUrl: liveCheck.success ? liveCheck.liveUrl : null,
liveError: liveCheck.success ? null : liveCheck.error
}
}
}
// 导出单例
const deviceChecker = new EzvizDeviceChecker()
export default deviceChecker

View File

@ -0,0 +1,164 @@
// 萤石云AccessToken管理工具
// 使用方法:
// 1. 在萤石云开放平台获取AppKey和AppSecret
// 2. 调用getAccessToken()获取新的token
class EzvizTokenManager {
constructor(appKey, appSecret) {
this.appKey = appKey
this.appSecret = appSecret
this.baseUrl = 'https://open.ys7.com/api/lapp'
}
// 获取AccessToken
async getAccessToken() {
try {
console.log('🔑 开始获取AccessToken...')
const response = await uni.request({
url: `${this.baseUrl}/token/get`,
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: {
appKey: this.appKey,
appSecret: this.appSecret
}
})
console.log('📡 API响应:', response)
if (response.data && response.data.code === '200') {
const tokenData = response.data.data
console.log('✅ AccessToken获取成功:', tokenData)
// 保存到本地存储
uni.setStorageSync('ezviz_access_token', tokenData.accessToken)
uni.setStorageSync('ezviz_token_expire', tokenData.expireTime)
return {
success: true,
accessToken: tokenData.accessToken,
expireTime: tokenData.expireTime
}
} else {
throw new Error(response.data?.msg || '获取AccessToken失败')
}
} catch (error) {
console.error('❌ 获取AccessToken失败:', error)
return {
success: false,
error: error.message
}
}
}
// 检查token是否过期
isTokenExpired() {
const expireTime = uni.getStorageSync('ezviz_token_expire')
if (!expireTime) return true
const now = Date.now()
return now >= expireTime * 1000 // expireTime是秒需要转换为毫秒
}
// 获取有效的AccessToken
async getValidAccessToken() {
// let token = uni.getStorageSync('ezviz_access_token')
// if (!token || this.isTokenExpired()) {
// console.log('🔄 Token不存在或已过期重新获取...')
// const result = await this.getAccessToken()
// if (result.success) {
// return result.accessToken
// } else {
// throw new Error(result.error)
// }
// }
const result = await this.getAccessToken()
if (result.success) {
return result.accessToken
} else {
throw new Error(result.error)
}
console.log('✅ 使用缓存的AccessToken')
return token
}
// 获取设备信息
async getDeviceList() {
try {
const accessToken = await this.getValidAccessToken()
const response = await uni.request({
url: `${this.baseUrl}/device/list`,
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: {
accessToken: accessToken,
pageStart: 0,
pageSize: 50
}
})
if (response.data && response.data.code === '200') {
return {
success: true,
devices: response.data.data
}
} else {
throw new Error(response.data?.msg || '获取设备列表失败')
}
} catch (error) {
console.error('❌ 获取设备列表失败:', error)
return {
success: false,
error: error.message
}
}
}
}
// 导出单例
const tokenManager = new EzvizTokenManager(
// '19c3a50bc19a4b27832408e003797644', // 你的AppKey
// 'cf43a4f58bc64d7e37bba9947daf70b3' // 你的AppSecret
'19c3a50bc19a4b27832408e003797644',
'cf43a4f58bc64d7e37bba9947daf70b3',
)
export default tokenManager
// 使用示例:
/*
import tokenManager from '@/utils/ezvizTokenManager.js'
// 获取新的AccessToken
const result = await tokenManager.getAccessToken()
if (result.success) {
console.log('新的AccessToken:', result.accessToken)
} else {
console.error('获取失败:', result.error)
}
// 获取有效的AccessToken自动处理过期
try {
const token = await tokenManager.getValidAccessToken()
console.log('有效的AccessToken:', token)
} catch (error) {
console.error('获取AccessToken失败:', error)
}
// 获取设备列表
const devices = await tokenManager.getDeviceList()
if (devices.success) {
console.log('设备列表:', devices.devices)
}
*/

View File

@ -106,11 +106,11 @@ const initEventHandleMqtt = (topicUrl) => {
console.log("✅ MQTT连接成功");
// 显示连接成功提示
uni.showToast({
title: 'MQTT连接成功',
icon: 'success',
duration: 2000
});
// uni.showToast({
// title: 'MQTT连接成功',
// icon: 'success',
// duration: 2000
// });
//订阅主题
client.subscribe(topicUrl, function(err) {