Files
movecheck/src/pages/environment/index.vue
2025-10-01 04:07:37 +08:00

1358 lines
36 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="environment-page">
<!-- 固定头部 -->
<view class="fixed-header">
<text class="header-title">移动式检修车间</text>
</view>
<!-- 内容区域 -->
<view class="tabbar-content">
<!-- 顶部参数卡片 -->
<view class="parameter-cards">
<!-- 温度卡片 -->
<view class="card-item temperature-card">
<view class="card-icon temperature-icon">🌡</view>
<view class="card-content">
<text class="card-label">温度</text>
<text class="card-value">{{ temperature }}°C</text>
</view>
<view class="card-status" :class="temperature > 0 ? 'active' : 'inactive'"></view>
</view>
<!-- 湿度卡片 -->
<view class="card-item humidity-card">
<view class="card-icon humidity-icon">💧</view>
<view class="card-content">
<text class="card-label">湿度</text>
<text class="card-value">{{ humidity }}%</text>
</view>
<view class="card-status" :class="humidity > 0 ? 'active' : 'inactive'"></view>
</view>
<!-- 洁净度卡片 -->
<view class="card-item cleanliness-card">
<view class="card-icon cleanliness-icon"></view>
<view class="card-content">
<text class="card-label">洁净度</text>
<text class="card-value">{{ cleanliness > 0 ? cleanliness + '%' : '-%' }}</text>
</view>
<view class="card-status" :class="cleanliness > 0 ? 'active' : 'inactive'"></view>
</view>
</view>
<!-- 环境参数详情 -->
<view class="parameter-details">
<!-- 温度卡片 -->
<view class="detail-card temperature-detail-card">
<view class="detail-header">
<text class="detail-label">温度</text>
<text class="detail-value">{{ temperature }}°C</text>
</view>
<view class="detail-progress-bar">
<view class="detail-progress-fill temperature-progress" :style="{ width: temperatureProgress + '%' }"></view>
</view>
<view class="detail-range">
<!-- <text class="detail-range-text">{{ temperatureRange.min }}°C - {{ temperatureRange.max }}°C</text> -->
<text class="detail-range-text">{{ 0 }}°C - {{ 100 }}°C</text>
</view>
</view>
<!-- 湿度卡片 -->
<view class="detail-card humidity-detail-card">
<view class="detail-header">
<text class="detail-label">湿度</text>
<text class="detail-value">{{ humidity }}%</text>
</view>
<view class="detail-progress-bar">
<view class="detail-progress-fill humidity-progress" :style="{ width: humidityProgress + '%' }"></view>
</view>
<view class="detail-range">
<!-- <text class="detail-range-text">{{ humidityRange.min }}% - {{ humidityRange.max }}%</text> -->
<text class="detail-range-text">{{ 0 }}% - {{ 100 }}%</text>
</view>
</view>
<!-- 洁净度卡片 -->
<view class="detail-card cleanliness-detail-card">
<view class="detail-header">
<text class="detail-label">洁净度</text>
<text class="detail-value">{{ cleanliness > 0 ? cleanliness + '%' : '-%' }}</text>
</view>
<view class="detail-progress-bar">
<view class="detail-progress-fill cleanliness-progress" :style="{ width: cleanlinessProgress + '%' }"></view>
</view>
<view class="detail-range">
<!-- <text class="detail-range-text">暂无数据</text> -->
<text class="detail-range-text">{{ 0 }}% - {{ 100 }}%</text>
</view>
</view>
</view>
<!-- 空调目标参数设置 -->
<view class="air-conditioner-settings">
<view class="ac-header">
<text class="ac-title">空调目标参数设置</text>
</view>
<view class="ac-control">
<view class="ac-label">目标温度</view>
<view class="ac-temp-control">
<button class="ac-btn decrease" @click="decreaseTemperature"></button>
<view class="ac-temp-display">
<text class="ac-temp-value">{{ targetTemperature }}</text>
<text class="ac-temp-unit">°C</text>
</view>
<button class="ac-btn increase" @click="increaseTemperature">+</button>
</view>
</view>
</view>
<!-- 环境控制区域 -->
<view class="environment-control">
<view class="env-header">
<text class="env-title">环境控制</text>
<!-- <view class="env-status">
<view class="status-dot"></view>
<text class="status-text">异常</text>
</view> -->
</view>
<view class="env-info">
<!-- <view class="info-item">
<text class="info-label">最后更新</text>
<text class="info-value">{{ lastUpdate }}</text>
</view> -->
<view class="control-ranges">
<view class="range-item">
<view class="range-header">
<text class="range-label">温度控制</text>
<view class="range-status" :class="getTemperatureStatus().class">
<text class="status-text">{{ getTemperatureStatus().text }}</text>
</view>
</view>
<view class="range-content">
<text class="current-value">当前: {{ temperature }}°C</text>
<text class="range-value">范围: {{ temperatureRange.min }}°C - {{ temperatureRange.max }}°C</text>
</view>
</view>
<view class="range-item">
<view class="range-header">
<text class="range-label">湿度控制</text>
<view class="range-status" :class="getHumidityStatus().class">
<text class="status-text">{{ getHumidityStatus().text }}</text>
</view>
</view>
<view class="range-content">
<text class="current-value">当前: {{ humidity }}%</text>
<text class="range-value">范围: {{ humidityRange.min }}% - {{ humidityRange.max }}%</text>
</view>
</view>
</view>
</view>
<button class="settings-button" @click="openSettingsModal">
<text class="settings-icon"></text>
<text class="settings-text">参数设定</text>
</button>
</view>
<!-- 参数设定弹窗 -->
<uni-popup ref="settingsPopup" type="center" :mask-click="false">
<view class="settings-modal">
<view class="modal-header">
<text class="modal-title">参数设定</text>
<text class="close-btn" @click="closeSettingsModal"></text>
</view>
<view class="modal-content">
<!-- 温度设定 -->
<view class="setting-item">
<text class="setting-label">温度控制范围</text>
<view class="range-inputs">
<input
class="range-input"
type="number"
v-model="tempSettings.min"
placeholder="最小值"
/>
<text class="range-separator">-</text>
<input
class="range-input"
type="number"
v-model="tempSettings.max"
placeholder="最大值"
/>
<text class="unit">°C</text>
</view>
</view>
<!-- 湿度设定 -->
<view class="setting-item">
<text class="setting-label">湿度控制范围</text>
<view class="range-inputs">
<input
class="range-input"
type="number"
v-model="humiditySettings.min"
placeholder="最小值"
/>
<text class="range-separator">-</text>
<input
class="range-input"
type="number"
v-model="humiditySettings.max"
placeholder="最大值"
/>
<text class="unit">%</text>
</view>
</view>
</view>
<view class="modal-actions">
<button class="cancel-btn" @click="closeSettingsModal">取消</button>
<button class="confirm-btn" @click="saveSettings">确定</button>
</view>
</view>
</uni-popup>
</view>
</view>
</template>
<script>
import mqttDataManager from '@/utils/mqttDataManager.js'
import { manualReconnect, sendMqttData } from '@/utils/sendMqtt.js'
import { thDataApi, alertApi, eventApi } from '@/utils/api.js'
export default {
data() {
return {
temperature: 0,
humidity: 0,
cleanliness: 0,
temperatureProgress: 0,
humidityProgress: 0,
cleanlinessProgress: 0,
lastUpdate: '暂无数据',
temperatureRange: {
min: 25,
max: 35
},
humidityRange: {
min: 40,
max: 70
},
tempSettings: {
min: 25,
max: 35
},
humiditySettings: {
min: 40,
max: 70
},
connectionStatus: {
isConnected: false,
lastUpdate: null
},
targetTemperature: 30,
targetHumidity: 50, // 空调设定湿度
// 报警相关数据
acFaultStatus: 0, // 空调故障状态1表示故障
alertHistory: [], // 报警历史记录
// 系统启动事件相关
hasCreatedStartupEvent: false, // 是否已创建启动事件
}
},
onLoad() {
console.log('环境参数页面加载')
// 从本地存储读取是否已创建启动事件的状态
const hasCreatedStartupEvent = uni.getStorageSync('hasCreatedStartupEvent')
if (hasCreatedStartupEvent) {
this.hasCreatedStartupEvent = true
console.log('📱 从本地存储读取到启动事件状态: 已创建')
}
this.initMqttListener()
// 获取最新空调温度
this.getLatestAirConditionerTemperature()
// 首次进入系统时创建启动事件
this.createStartupEventIfNeeded()
},
onShow() {
console.log('📱 环境参数页面显示,触发页面更新')
// 只有在非首次显示时才重新获取最新空调温度
if (this.hasCreatedStartupEvent) {
this.getLatestAirConditionerTemperature()
}
},
onUnload() {
// console.log('🔌 环境参数页面卸载,清理资源...')
// 页面卸载时移除监听器
if (this.dataUpdateHandler) {
mqttDataManager.removeListener('dataUpdate', this.dataUpdateHandler)
// console.log('✅ 数据更新监听器已移除')
}
if (this.statusUpdateHandler) {
mqttDataManager.removeListener('connectionStatus', this.statusUpdateHandler)
// console.log('✅ 状态更新监听器已移除')
}
// 清理调试定时器
if (this.debugInterval) {
clearInterval(this.debugInterval)
// console.log('✅ 调试定时器已清理')
}
// console.log('✅ 环境参数页面资源清理完成')
},
methods: {
// 首次进入系统时创建启动事件
async createStartupEventIfNeeded() {
// 检查是否已经创建过启动事件
if (this.hasCreatedStartupEvent) {
console.log('✅ 启动事件已存在,跳过创建')
return
}
try {
console.log('🚀 首次进入系统,创建启动事件...')
const currentTime = this.formatDateTime(new Date())
const startupEvent = {
eventType: "设备重启",
eventTime: currentTime,
status: "已完成",
description: "设备正常重启维护",
deviceId: "AC_001"
}
console.log('📤 提交启动事件:', startupEvent)
const response = await eventApi.create(startupEvent)
console.log('✅ 启动事件创建成功:', response)
// 标记已创建启动事件
this.hasCreatedStartupEvent = true
// 可以保存到本地存储,避免刷新页面后重复创建
uni.setStorageSync('hasCreatedStartupEvent', true)
} catch (error) {
console.error('❌ 启动事件创建失败:', error)
}
},
// 获取温度控制状态
getTemperatureStatus() {
if (this.temperature === 0) {
return { text: '无数据', class: 'no-data' }
} else if (this.temperature >= this.temperatureRange.min && this.temperature <= this.temperatureRange.max) {
return { text: '正常', class: 'normal' }
} else if (this.temperature < this.temperatureRange.min) {
return { text: '偏低', class: 'low' }
} else {
return { text: '偏高', class: 'high' }
}
},
// 获取湿度控制状态
getHumidityStatus() {
if (this.humidity === 0) {
return { text: '无数据', class: 'no-data' }
} else if (this.humidity >= this.humidityRange.min && this.humidity <= this.humidityRange.max) {
return { text: '正常', class: 'normal' }
} else if (this.humidity < this.humidityRange.min) {
return { text: '偏低', class: 'low' }
} else {
return { text: '偏高', class: 'high' }
}
},
// 获取最新空调温度
async getLatestAirConditionerTemperature() {
try {
console.log('🌡️ 开始获取最新空调温度...')
const res = await thDataApi.getLatest();
if (res.status === 'success') {
this.targetTemperature = res.temperature;
// this.humidity = res.humidity;
// this.updateEnvironmentData({
// temperature: res.temperature,
// humidity: res.humidity,
// deviceType: 'WSD'
// })
}
// {
// "status": "success",
// "message": "获取成功",
// "temperature": 25.5,
// "humidity": 60.2,
// "deviceId": "TH_SENSOR_001",
// "createTime": "2025-09-30T15:55:13"
// }
console.log('✅ 最新空调温度数据:', res)
} catch (error) {
console.error('❌ 获取最新空调温度失败:', error)
}
},
// 初始化MQTT监听
initMqttListener() {
// console.log('🔧 环境参数页面开始初始化MQTT监听...')
// 监听数据更新
this.dataUpdateHandler = (data) => {
// console.log('📨 环境参数页面收到MQTT数据:', data)
this.updateEnvironmentData(data)
}
mqttDataManager.addListener('dataUpdate', this.dataUpdateHandler)
// 监听连接状态
this.statusUpdateHandler = (status) => {
// console.log('🔄 环境参数页面连接状态更新:', status)
const wasConnected = this.connectionStatus.isConnected
this.connectionStatus = status
// 只在状态发生变化时显示提示(避免重复提示)
if (wasConnected !== status.isConnected) {
// if (status.isConnected) {
// console.log('✅ MQTT连接状态: 已连接')
// // 不显示Toast因为sendMqtt.js中已经显示了
// } else {
// console.log('❌ MQTT连接状态: 未连接')
// // 不显示Toast因为sendMqtt.js中已经显示了
// }
}
}
mqttDataManager.addListener('connectionStatus', this.statusUpdateHandler)
// 获取初始数据
const lastData = mqttDataManager.getLastData()
// console.log('📊 获取初始数据:', lastData)
if (lastData.timestamp) {
this.updateEnvironmentData(lastData)
}
// 获取初始连接状态
this.connectionStatus = mqttDataManager.getConnectionStatus()
// console.log('🔍 初始连接状态:', this.connectionStatus)
// 定期检查连接状态(用于调试)
this.debugInterval = setInterval(() => {
const currentStatus = mqttDataManager.getConnectionStatus()
// console.log('🔍 定期检查连接状态:', currentStatus)
}, 10000) // 每10秒检查一次
// console.log('✅ 环境参数页面MQTT监听初始化完成')
},
// 更新环境数据
updateEnvironmentData(data) {
// console.log('🌡️ 环境参数页面更新数据:', data)
// 处理空调故障状态
if (data.deviceType === 'AC' && data.faultStatus !== undefined) {
this.acFaultStatus = data.faultStatus
}
// 只处理WSD设备的数据
if (data.deviceType === 'WSD') {
if (data.temperature !== undefined) {
this.temperature = parseFloat(data.temperature.toFixed(1))
this.temperatureProgress = Math.min(Math.max(this.temperature, 0), 100)
// console.log('✅ 温度已更新:', this.temperature)
}
if (data.humidity !== undefined) {
this.humidity = parseFloat(data.humidity.toFixed(1))
this.humidityProgress = Math.min(Math.max(this.humidity, 0), 100)
// console.log('✅ 湿度已更新:', this.humidity)
}
this.lastUpdate = data.time || new Date().toLocaleString('zh-CN')
// 检查报警条件传入MQTT原始数据
this.checkAlerts(data)
// console.log('✅ 环境数据更新完成:', {
// temperature: this.temperature,
// humidity: this.humidity,
// lastUpdate: this.lastUpdate
// })
} else {
console.log('⚠️ 非WSD设备数据跳过更新:', data.deviceType)
}
},
// 格式化日期时间为指定格式
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}`
},
// 检查报警条件
checkAlerts(mqttData) {
const currentTime = this.formatDateTime(new Date())
// 只处理WSD设备的数据
if (mqttData.deviceType !== 'WSD') {
return
}
// 获取MQTT原始数据
const mqttTemperature = mqttData.temperature
const mqttHumidity = mqttData.humidity
// 1. 温度报警使用环境控制设置的区间使用MQTT原始数据
if (mqttTemperature !== undefined && (mqttTemperature < this.temperatureRange.min || mqttTemperature > this.temperatureRange.max)) {
const alert = {
// content: `温度传感器异常,读数${mqttTemperature}°C超出控制范围${this.temperatureRange.min}°C-${this.temperatureRange.max}°C`,
content: `温度超出控制范围`,
category: "传感器故障",
alertTime: currentTime,
level: "高危",
action: "检查温度传感器连接",
actionTime: currentTime,
deviceId: "WSD_001"
}
this.logAlert(alert)
}
// 2. 湿度报警使用环境控制设置的区间使用MQTT原始数据
if (mqttHumidity !== undefined && (mqttHumidity < this.humidityRange.min || mqttHumidity > this.humidityRange.max)) {
const alert = {
// content: `湿度传感器异常,读数${mqttHumidity}%超出控制范围${this.humidityRange.min}%-${this.humidityRange.max}%`,
content: `温度超出控制范围`,
category: "传感器故障",
alertTime: currentTime,
level: "高危",
action: "检查湿度传感器连接",
actionTime: currentTime,
deviceId: "WSD_001"
}
this.logAlert(alert)
}
// 3. 空调故障报警acFaultStatus为1
if (this.acFaultStatus === 1) {
const alert = {
// content: "空调故障,需要手动设置温度",
content: "空调报警",
category: "设备故障",
alertTime: currentTime,
level: "高危",
action: "手动调节空调温度",
actionTime: currentTime,
deviceId: "AC_001"
}
this.logAlert(alert)
}
// 4. 温度偏差报警:|温湿度计温度 - 空调设定温度| / 空调设定温度 > 30%使用MQTT原始数据
if (mqttTemperature !== undefined && this.targetTemperature > 0) {
const temperatureDiff = Math.abs(mqttTemperature - this.targetTemperature)
const deviationPercent = (temperatureDiff / this.targetTemperature) * 100
if (deviationPercent > 30) {
const alert = {
// content: `温度偏差过大,实际${mqttTemperature}°C与设定${this.targetTemperature}°C偏差${deviationPercent.toFixed(1)}%`,
content: `温度偏差过大`,
category: "温度控制异常",
alertTime: currentTime,
level: "中危",
action: "调整空调设定温度",
actionTime: currentTime,
deviceId: "AC_001"
}
this.logAlert(alert)
}
}
// 5. 湿度偏差报警:|温湿度计湿度 - 空调设定湿度| / 空调设定湿度 > 30%使用MQTT原始数据
if (mqttHumidity !== undefined && this.targetHumidity > 0) {
const humidityDiff = Math.abs(mqttHumidity - this.targetHumidity)
const deviationPercent = (humidityDiff / this.targetHumidity) * 100
if (deviationPercent > 30) {
const alert = {
// content: `湿度偏差过大,实际${mqttHumidity}%与设定${this.targetHumidity}%偏差${deviationPercent.toFixed(1)}%`,
content: `湿度偏差过大`,
category: "湿度控制异常",
alertTime: currentTime,
level: "中危",
action: "调整空调设定湿度",
actionTime: currentTime,
deviceId: "AC_001"
}
this.logAlert(alert)
}
}
},
// 记录报警到控制台并调用创建告警接口
async logAlert(alert) {
console.log('🚨 报警触发:', JSON.stringify(alert, null, 2))
this.alertHistory.push(alert)
// 调用创建告警接口
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)
}
},
// 降低目标温度
decreaseTemperature() {
if (this.targetTemperature > 16) {
this.targetTemperature--
console.log('目标温度降低至:', this.targetTemperature + '°C')
this.showTemperatureChangeToast()
} else {
uni.showToast({
title: '温度不能低于16°C',
icon: 'none'
})
}
},
// 提高目标温度
increaseTemperature() {
if (this.targetTemperature < 50) {
this.targetTemperature++
console.log('目标温度提高至:', this.targetTemperature + '°C')
this.showTemperatureChangeToast()
} else {
uni.showToast({
title: '温度不能高于50°C',
icon: 'none'
})
}
},
// 显示温度变化提示
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
}
this.checkAlerts(mockMqttData)
},
// 发送空调参数
async sendAirConditionerParams() {
const airConditionerData = {
"TagValue": this.targetTemperature,
"TagName": "JS_COD",
"method": "setValue"
}
console.log('🌡️ 发送空调参数:', airConditionerData)
// 调用发送MQTT数据的方法
const success = sendMqttData(airConditionerData)
if (success) {
console.log('✅ 空调参数发送请求已提交')
// 发送成功后调用提交温湿度数据API
await this.submitTemperatureData()
} else {
console.log('❌ 空调参数发送失败')
}
},
// 提交温湿度数据
async submitTemperatureData() {
try {
const temperatureData = {
temperature: this.targetTemperature,
deviceId: "TH_SENSOR_001",
humidity: 0,
}
await thDataApi.submit(temperatureData)
} catch (error) {
console.error('❌ 温湿度数据提交失败:', error)
}
},
// 手动重连MQTT
manualReconnect() {
console.log('🔄 用户手动触发MQTT重连')
uni.showToast({
title: '正在重连...',
icon: 'loading',
duration: 2000
})
// 调用sendMqtt.js中的手动重连函数
manualReconnect()
},
openSettingsModal() {
// 打开弹窗前,将当前设置复制到临时变量
this.tempSettings = { ...this.temperatureRange }
this.humiditySettings = { ...this.humidityRange }
this.$refs.settingsPopup.open()
},
closeSettingsModal() {
this.$refs.settingsPopup.close()
},
saveSettings() {
// 保存设置
this.temperatureRange = { ...this.tempSettings }
this.humidityRange = { ...this.humiditySettings }
// 更新最后更新时间
this.lastUpdate = new Date().toLocaleString()
// 关闭弹窗
this.closeSettingsModal()
// 显示保存成功提示
uni.showToast({
title: '参数设置已保存',
icon: 'success'
})
console.log('保存的温度范围:', this.temperatureRange)
console.log('保存的湿度范围:', this.humidityRange)
}
}
}
</script>
<style lang="scss" scoped>
.environment-page {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 顶部参数卡片样式 */
.parameter-cards {
display: flex;
gap: 20rpx;
margin-bottom: 30rpx;
}
.card-item {
flex: 1;
background: white;
border-radius: 12rpx;
padding: 25rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
min-height: 120rpx;
border: 2rpx solid transparent;
transition: all 0.3s ease;
}
.card-item:hover {
transform: translateY(-2rpx);
box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.15);
}
/* 温度卡片 */
.temperature-card {
// border-left: 4rpx solid #ff4444;
}
.temperature-card .card-icon {
color: #ff4444;
}
/* 湿度卡片 */
.humidity-card {
// border-left: 4rpx solid #4488ff;
}
.humidity-card .card-icon {
color: #4488ff;
}
/* 洁净度卡片 */
.cleanliness-card {
// border-left: 4rpx solid #ffaa00;
}
.cleanliness-card .card-icon {
color: #ffaa00;
}
.card-icon {
font-size: 36rpx;
margin-bottom: 12rpx;
}
.card-content {
text-align: center;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.card-label {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
font-weight: 500;
}
.card-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.card-status {
position: absolute;
top: 15rpx;
right: 15rpx;
width: 14rpx;
height: 14rpx;
border-radius: 50%;
}
.card-status.active {
background-color: #4caf50;
box-shadow: 0 0 8rpx rgba(76, 175, 80, 0.5);
}
.card-status.inactive {
background-color: #ccc;
}
.parameter-details {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 20rpx;
}
.detail-card {
background: white;
border-radius: 12rpx;
padding: 25rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.08);
border-left: 4rpx solid transparent;
transition: all 0.3s ease;
}
.detail-card:hover {
transform: translateY(-1rpx);
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.12);
}
/* 温度详情卡片 */
.temperature-detail-card {
// border-left-color: #ff4444;
}
/* 湿度详情卡片 */
.humidity-detail-card {
// border-left-color: #4488ff;
}
/* 洁净度详情卡片 */
.cleanliness-detail-card {
// border-left-color: #ffaa00;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.detail-label {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.detail-value {
font-size: 28rpx;
color: #4488ff;
font-weight: bold;
}
.detail-progress-bar {
height: 16rpx;
background-color: #f0f0f0;
border-radius: 8rpx;
overflow: hidden;
margin-bottom: 10rpx;
}
.detail-progress-fill {
height: 100%;
border-radius: 8rpx;
transition: width 0.3s ease;
}
.temperature-progress {
background: linear-gradient(90deg, #4488ff, #44ff88);
}
.humidity-progress {
background: linear-gradient(90deg, #4488ff, #44ff88);
}
.cleanliness-progress {
background: linear-gradient(90deg, #4488ff, #44ff88);
}
.detail-range {
display: flex;
justify-content: flex-end;
}
.detail-range-text {
font-size: 24rpx;
color: #666;
}
/* 空调目标参数设置样式 */
.air-conditioner-settings {
background: white;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.08);
border: 1rpx solid #f0f0f0;
}
.ac-header {
margin-bottom: 25rpx;
text-align: center;
}
.ac-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
.ac-control {
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
}
.ac-label {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
.ac-temp-control {
display: flex;
align-items: center;
gap: 30rpx;
background: #f8f9fa;
border-radius: 50rpx;
padding: 15rpx 25rpx;
border: 2rpx solid #e9ecef;
}
.ac-btn {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
font-weight: bold;
border: none;
color: white;
transition: all 0.3s ease;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
}
.ac-btn.decrease {
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
}
.ac-btn.increase {
background: linear-gradient(135deg, #51cf66, #40c057);
}
.ac-btn:active {
transform: scale(0.95);
box-shadow: 0 1rpx 4rpx rgba(0,0,0,0.2);
}
.ac-temp-display {
display: flex;
align-items: baseline;
gap: 8rpx;
min-width: 120rpx;
justify-content: center;
}
.ac-temp-value {
font-size: 48rpx;
font-weight: 700;
color: #333;
line-height: 1;
}
.ac-temp-unit {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
.environment-control {
background: white;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.08);
border: 1rpx solid #f0f0f0;
}
.env-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.env-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
.env-status {
display: flex;
align-items: center;
gap: 10rpx;
// background: #f0f8ff;
padding: 8rpx 16rpx;
border-radius: 20rpx;
// border: 1rpx solid #e6f3ff;
}
.status-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #4a90e2;
}
.status-text {
font-size: 24rpx;
color: #4a90e2;
font-weight: 500;
}
.env-info {
margin-bottom: 25rpx;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding: 15rpx 20rpx;
background: #f8fbff;
border-radius: 12rpx;
border-left: 4rpx solid #4a90e2;
}
.info-label {
font-size: 26rpx;
color: #666;
font-weight: 500;
}
.info-value {
font-size: 24rpx;
color: #4a90e2;
font-weight: 500;
}
.control-ranges {
display: flex;
flex-direction: column;
gap: 15rpx;
}
.range-item {
padding: 20rpx;
background: #f8fbff;
border-radius: 12rpx;
border-left: 4rpx solid #4a90e2;
}
.range-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.range-label {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
.range-status {
padding: 6rpx 12rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 500;
}
.range-status.normal {
background-color: #e8f5e8;
color: #4caf50;
}
.range-status.low {
background-color: #fff3e0;
color: #ff9800;
}
.range-status.high {
background-color: #ffebee;
color: #f44336;
}
.range-status.no-data {
background-color: #f5f5f5;
color: #999;
}
.range-content {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.current-value {
font-size: 26rpx;
color: #4a90e2;
font-weight: 500;
}
.range-value {
font-size: 24rpx;
color: #666;
font-weight: 400;
}
.settings-button {
background-color: #3f51b5;
color: white;
padding: 18rpx 30rpx;
border-radius: 12rpx;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
width: 100%;
border: none;
box-shadow: 0 4rpx 12rpx rgba(74, 144, 226, 0.3);
transition: all 0.3s ease;
}
.settings-button:active {
transform: translateY(1rpx);
box-shadow: 0 2rpx 8rpx rgba(74, 144, 226, 0.4);
}
.settings-icon {
font-size: 24rpx;
}
.settings-text {
font-size: 28rpx;
font-weight: 500;
}
/* 弹窗样式 */
.settings-modal {
background: white;
border-radius: 16rpx;
width: 600rpx;
max-width: 90vw;
overflow: hidden;
border: 1rpx solid #f0f0f0;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
// background-color: #f8fbff;
border-bottom: 1rpx solid #e6f3ff;
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
.close-btn {
font-size: 28rpx;
color: #999;
padding: 8rpx;
border-radius: 50%;
background-color: #f5f5f5;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.close-btn:active {
background-color: #e0e0e0;
transform: scale(0.95);
}
.modal-content {
padding: 30rpx;
}
.setting-item {
margin-bottom: 30rpx;
&:last-child {
margin-bottom: 0;
}
}
.setting-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
font-weight: 500;
}
.range-inputs {
display: flex;
align-items: center;
gap: 20rpx;
}
.range-input {
flex: 1;
padding: 20rpx;
border: 2rpx solid #e6f3ff;
border-radius: 12rpx;
font-size: 28rpx;
text-align: center;
background-color: #f8fbff;
transition: all 0.3s ease;
}
.range-input:focus {
border-color: #4a90e2;
background-color: white;
box-shadow: 0 0 0 4rpx rgba(74, 144, 226, 0.1);
}
.range-separator {
font-size: 28rpx;
color: #4a90e2;
font-weight: 600;
min-width: 40rpx;
text-align: center;
}
.unit {
font-size: 24rpx;
color: #4a90e2;
min-width: 60rpx;
font-weight: 500;
}
.modal-actions {
display: flex;
gap: 20rpx;
padding: 30rpx;
background-color: #f8fbff;
}
.cancel-btn {
flex: 1;
background-color: #f5f5f5;
color: #666;
padding: 20rpx;
border-radius: 12rpx;
font-size: 28rpx;
border: 2rpx solid #e0e0e0;
transition: all 0.3s ease;
}
.cancel-btn:active {
background-color: #e0e0e0;
transform: translateY(1rpx);
}
.confirm-btn {
flex: 1;
background-color: #3f51b5;
color: white;
padding: 20rpx;
border-radius: 12rpx;
font-size: 28rpx;
border: none;
box-shadow: 0 4rpx 12rpx rgba(63, 81, 181, 0.3);
transition: all 0.3s ease;
}
.confirm-btn:active {
transform: translateY(1rpx);
box-shadow: 0 2rpx 8rpx rgba(63, 81, 181, 0.4);
}
</style>