951 lines
24 KiB
Plaintext
951 lines
24 KiB
Plaintext
<template> <scroll-view direction="vertical" class="sense-container">
|
||
<!-- 头部导航 -->
|
||
<view class="header">
|
||
<text class="title">传感器数据监控</text>
|
||
<view class="header-actions">
|
||
<button class="refresh-btn" @click="refreshData">刷新</button>
|
||
<button class="analyze-btn" @click="analyzeData">AI分析</button>
|
||
</view>
|
||
</view>
|
||
<!-- 页面导航菜单 -->
|
||
<view class="nav-menu">
|
||
<view class="nav-info" v-if="deviceInfo !== null">
|
||
<text class="nav-device-info">当前设备: {{ deviceName }}</text>
|
||
<text class="nav-device-status" :class="deviceStatusClass">{{ deviceStatus }}</text>
|
||
</view>
|
||
<view class="nav-tabs">
|
||
<button class="nav-tab active" @click="navigateToPage('index')">
|
||
<text class="nav-icon">📊</text>
|
||
<text class="nav-text">数据监控</text>
|
||
</button>
|
||
<button class="nav-tab" @click="navigateToPage('analysis')">
|
||
<text class="nav-icon">📈</text>
|
||
<text class="nav-text">数据分析</text>
|
||
</button>
|
||
<button class="nav-tab" @click="navigateToPage('devices')">
|
||
<text class="nav-icon">📱</text>
|
||
<text class="nav-text">设备管理</text>
|
||
</button>
|
||
<button class="nav-tab" @click="navigateToPage('simulator')">
|
||
<text class="nav-icon">🔧</text>
|
||
<text class="nav-text">数据模拟</text>
|
||
</button>
|
||
<button class="nav-tab" @click="navigateToPage('settings')">
|
||
<text class="nav-icon">⚙️</text>
|
||
<text class="nav-text">设置</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
<!-- 设备状态卡片 -->
|
||
<view class="device-card">
|
||
<text class="device-title">设备状态</text>
|
||
<view class="device-info" v-if="deviceInfo !== null">
|
||
<text class="device-name">{{ deviceName }}</text>
|
||
<text class="device-status" :class="deviceStatusClass">{{ deviceStatus }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 实时数据卡片 -->
|
||
<view class="realtime-card">
|
||
<text class="card-title">实时数据</text>
|
||
<view class="metrics-grid">
|
||
<view class="metric-item" v-for="(metric, index) in realtimeMetrics" :key="index">
|
||
<text class="metric-label">{{ metric.label }}</text>
|
||
<text class="metric-value">{{ metric.value }}</text>
|
||
<text class="metric-unit">{{ metric.unit }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 图表展示 -->
|
||
<view class="chart-card">
|
||
<text class="card-title">数据趋势</text>
|
||
<view class="chart-tabs">
|
||
<button class="tab-btn" :class="{ active: activeChartType == type }" v-for="(type, index) in chartTypes"
|
||
:key="index" @click="switchChart(type)">
|
||
{{ getChartTypeLabel(type) }}
|
||
</button>
|
||
</view>
|
||
<ak-charts :option="chartOption" :canvas-id="'sensor-chart'" class="chart-component" />
|
||
</view>
|
||
<!-- 历史记录列表 -->
|
||
<view class="history-card">
|
||
<view class="card-header">
|
||
<text class="card-title">最新记录</text>
|
||
<button class="load-more-btn" @click="loadMoreHistory">查看更多</button>
|
||
</view>
|
||
<scroll-view class="history-list" direction="vertical">
|
||
<view class="history-item" v-for="(item, index) in historyData" :key="index" @click="viewDetail(item)">
|
||
<view class="history-content">
|
||
<text class="history-type">{{ getTypeLabel(item.measurement_type ?? '') }}</text>
|
||
<text class="history-value">{{ formatValue(item) }}</text>
|
||
</view>
|
||
<text class="history-time">{{ formatTime(item.measured_at ?? '') }}</text>
|
||
</view>
|
||
<view v-if="historyData.length == 0" class="empty-history">
|
||
<text class="empty-text">暂无记录</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- AI分析结果弹窗 -->
|
||
<view class="analysis-modal" v-if="showAnalysis" @click="closeAnalysis">
|
||
<view class="modal-content" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">AI分析结果</text>
|
||
<button class="close-btn" @click="closeAnalysis">×</button>
|
||
</view>
|
||
<view class="modal-body">
|
||
<text class="analysis-summary">{{ analysisSummary }}</text>
|
||
<view class="recommendations" v-if="recommendations.length > 0">
|
||
<text class="rec-title">建议:</text>
|
||
<text class="rec-item" v-for="(rec, index) in recommendations" :key="index">• {{ rec }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import akCharts from '@/uni_modules/ak-charts/components/ak-charts.uvue'
|
||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||
import { SenseDataService, type SensorDataParams } from './senseDataService.uts'
|
||
import { state } from '@/utils/store.uts'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import type { SensorMeasurement, SensorAnalysisResult, DeviceInfo, ChartDataPoint } from './types.uts'
|
||
|
||
// 响应式数据
|
||
const deviceInfo = ref<DeviceInfo | null>(null)
|
||
const deviceStatus = ref<string>('离线')
|
||
const deviceStatusClass = ref<string>('status-offline')
|
||
const realtimeMetrics = ref<Array<UTSJSONObject>>([])
|
||
const historyData = ref<Array<SensorMeasurement>>([])
|
||
|
||
// 初始化 chartOption 为有效的图表配置
|
||
const initialChartOption = new UTSJSONObject()
|
||
initialChartOption.set('type', 'line')
|
||
initialChartOption.set('data', [] as number[])
|
||
initialChartOption.set('labels', [] as string[])
|
||
initialChartOption.set('color', '#1890ff')
|
||
const chartOption = ref<UTSJSONObject>(initialChartOption)
|
||
|
||
const activeChartType = ref<string>('heart_rate')
|
||
const showAnalysis = ref<boolean>(false)
|
||
const analysisSummary = ref<string>('')
|
||
const recommendations = ref<Array<string>>([])
|
||
const isLoading = ref<boolean>(false)
|
||
const error = ref<string>('')
|
||
|
||
// 实时订阅引用
|
||
let realtimeSubscription : any = null // 从 state 直接获取设备列表 - 使用computed确保响应性
|
||
const devices = computed<Array<DeviceInfo>>(() => state.deviceState.devices)
|
||
const currentDevice = computed<DeviceInfo | null>(() => state.deviceState.currentDevice) // 设备信息计算属性 - 避免smart cast问题
|
||
const deviceName = computed<string>(() => {
|
||
const device = deviceInfo.value
|
||
if (device != null) {
|
||
return device.device_name ?? '未知设备'
|
||
}
|
||
return '未知设备'
|
||
})// 常量
|
||
const chartTypes = ['heart_rate', 'steps', 'spo2', 'temp', 'bp']
|
||
let deviceId = '12345678-1234-5678-9abc-123456789012'
|
||
const userId = 'eed3824b-bba1-4309-8048-19d17367c084'
|
||
|
||
// 工具函数 - 定义在使用之前
|
||
function getTypeLabel(type : string) : string {
|
||
const labels = new Map<string, string>()
|
||
labels.set('heart_rate', '心率')
|
||
labels.set('steps', '步数')
|
||
labels.set('spo2', '血氧')
|
||
labels.set('temp', '体温')
|
||
labels.set('bp', '血压')
|
||
labels.set('stride', '步幅')
|
||
|
||
return labels.get(type) ?? type
|
||
}
|
||
|
||
function getChartTypeLabel(type : string) : string {
|
||
return getTypeLabel(type)
|
||
}
|
||
|
||
function getChartColor(type : string) : string {
|
||
const colors = new Map<string, string>()
|
||
colors.set('heart_rate', '#FF6B6B')
|
||
colors.set('steps', '#4ECDC4')
|
||
colors.set('spo2', '#45B7D1')
|
||
colors.set('temp', '#FFA726')
|
||
colors.set('bp', '#AB47BC')
|
||
|
||
return colors.get(type) ?? '#2196F3'
|
||
}
|
||
|
||
function formatValue(item : SensorMeasurement) : string {
|
||
const rawData = item.raw_data
|
||
console.log(rawData)
|
||
const type = item.measurement_type ?? ''
|
||
|
||
if (rawData == null) return '--'
|
||
|
||
if (type == 'heart_rate') {
|
||
const bpm = rawData.getNumber('bpm') ?? 0
|
||
return bpm.toString()
|
||
} else if (type == 'steps') {
|
||
const count = rawData.getNumber('count') ?? 0
|
||
return count.toString()
|
||
} else if (type == 'spo2') {
|
||
const spo2 = rawData.getNumber('spo2') ?? 0
|
||
return spo2.toString() + '%'
|
||
} else if (type == 'temp') {
|
||
const temp = rawData.getNumber('temp') ?? 0
|
||
return temp.toFixed(1) + '°C'
|
||
} else if (type == 'bp') {
|
||
const systolic = rawData.getNumber('systolic') ?? 0
|
||
const diastolic = rawData.getNumber('diastolic') ?? 0
|
||
return `${systolic}/${diastolic}`
|
||
}
|
||
console.log('should be not occur')
|
||
return '--'
|
||
}
|
||
|
||
function subscribeRealtime() {
|
||
if (supa == null) return
|
||
|
||
// 注意:当前的 aksupa 实现可能不支持实时订阅
|
||
// 这里简化为定期刷新数据
|
||
console.log('实时订阅功能暂不可用,将使用定期刷新')
|
||
|
||
// 可以在这里添加定期刷新逻辑
|
||
// setInterval(() => {
|
||
// loadHistoryData()
|
||
// }, 30000) // 每30秒刷新一次
|
||
}
|
||
function updateDeviceStatus() {
|
||
const currentDeviceInfo = deviceInfo.value
|
||
if (currentDeviceInfo == null) return
|
||
|
||
const status = currentDeviceInfo.status ?? 'offline'
|
||
if (status == 'online') {
|
||
deviceStatus.value = '在线'
|
||
deviceStatusClass.value = 'status-online'
|
||
} else {
|
||
deviceStatus.value = '离线'
|
||
deviceStatusClass.value = 'status-offline'
|
||
}
|
||
}
|
||
function updateRealtimeMetrics() {
|
||
const metrics : Array<UTSJSONObject> = []
|
||
|
||
// 从最新数据中提取各类指标
|
||
const typeMap = new Map<string, SensorMeasurement>()
|
||
|
||
for (let i : Int = 0; i < historyData.value.length; i++) {
|
||
const item = historyData.value[i]
|
||
const type = item.measurement_type ?? ''
|
||
if (type !== '' && !typeMap.has(type)) {
|
||
typeMap.set(type, item)
|
||
}
|
||
}
|
||
|
||
// 构建指标数组
|
||
typeMap.forEach((value : SensorMeasurement, key : string) => {
|
||
const metric = new UTSJSONObject()
|
||
metric.set('label', getTypeLabel(key))
|
||
metric.set('value', formatValue(value))
|
||
metric.set('unit', value.unit ?? '')
|
||
metrics.push(metric)
|
||
})
|
||
console.log(historyData)
|
||
realtimeMetrics.value = metrics
|
||
}
|
||
|
||
function updateChartWithData(chartData : Array<SensorMeasurement>) {
|
||
const chartValues : Array<number> = []
|
||
const chartLabels : Array<string> = []
|
||
// 过滤当前图表类型的数据
|
||
const filteredData : Array<SensorMeasurement> = []
|
||
for (let i : Int = 0; i < chartData.length; i++) {
|
||
const item = chartData[i]
|
||
const type = item.measurement_type ?? ''
|
||
if (type == activeChartType.value) {
|
||
filteredData.push(item)
|
||
}
|
||
}
|
||
|
||
// 取最近20个数据点
|
||
const recentData = filteredData.slice(0, 20).reverse()
|
||
for (let i : Int = 0; i < recentData.length; i++) {
|
||
const item = recentData[i]
|
||
const rawData = item.raw_data
|
||
if (rawData !== null) {
|
||
let value : number = 0
|
||
|
||
// 根据数据类型提取数值
|
||
if (activeChartType.value == 'heart_rate') {
|
||
value = rawData.getNumber('bpm') ?? 0
|
||
} else if (activeChartType.value == 'steps') {
|
||
value = rawData.getNumber('count') ?? 0
|
||
} else if (activeChartType.value == 'spo2') {
|
||
value = rawData.getNumber('spo2') ?? 0
|
||
} else if (activeChartType.value == 'temp') {
|
||
value = rawData.getNumber('temp') ?? 0
|
||
} else if (activeChartType.value == 'bp') {
|
||
value = rawData.getNumber('systolic') ?? 0
|
||
}
|
||
|
||
chartValues.push(value)
|
||
// 格式化时间标签
|
||
const timeStr = item.measured_at ?? ''
|
||
const time = new Date(timeStr)
|
||
const label = time.getHours().toString().padStart(2, '0') + ':' +
|
||
time.getMinutes().toString().padStart(2, '0')
|
||
chartLabels.push(label)
|
||
}
|
||
}
|
||
|
||
// 更新图表配置
|
||
const option = new UTSJSONObject()
|
||
option.set('type', 'line')
|
||
option.set('data', chartValues)
|
||
option.set('labels', chartLabels)
|
||
option.set('color', getChartColor(activeChartType.value))
|
||
|
||
chartOption.value = option
|
||
}
|
||
|
||
function updateChart() {
|
||
updateChartWithData(historyData.value)
|
||
}
|
||
|
||
async function analyzeData() {
|
||
try {
|
||
// 调用分析服务获取分析结果
|
||
const response = await SenseDataService.getAnalysisResults(userId, 'ai_analysis')
|
||
if (response.status >= 200 && response.status < 300 && response.data !== null && Array.isArray(response.data)) {
|
||
const dataArray = response.data as Array<any>
|
||
if (dataArray.length > 0) {
|
||
const analysis = dataArray[0] as UTSJSONObject // 获取最新的分析结果
|
||
analysisSummary.value = analysis.getString('summary') ?? '分析完成'
|
||
const recommArray = analysis.getArray('recommendations')
|
||
recommendations.value = Array.isArray(recommArray) ? recommArray as Array<string> : []
|
||
showAnalysis.value = true
|
||
} else {
|
||
// 如果没有现成的分析结果,显示默认信息
|
||
analysisSummary.value = '暂无分析数据,请稍后重试'
|
||
recommendations.value = ['建议定期监测健康数据', '保持良好的作息习惯', '如有异常及时就医']
|
||
showAnalysis.value = true
|
||
}
|
||
} else {
|
||
}
|
||
} catch (e) {
|
||
console.log('AI分析失败:', e)
|
||
analysisSummary.value = '分析服务暂时不可用,请稍后重试'
|
||
recommendations.value = []
|
||
showAnalysis.value = true
|
||
}
|
||
}
|
||
|
||
function viewDetail(item : SensorMeasurement) {
|
||
// 跳转到详情页面
|
||
const id = item.id ?? ''
|
||
uni.navigateTo({
|
||
url: `/pages/sense/detail?id=${id}`
|
||
})
|
||
}
|
||
function closeAnalysis() {
|
||
showAnalysis.value = false
|
||
|
||
}
|
||
|
||
|
||
async function loadDeviceInfo() {
|
||
isLoading.value = true
|
||
error.value = ''
|
||
|
||
try {
|
||
const response = await SenseDataService.getDeviceById(deviceId)
|
||
console.log(response)
|
||
if (response.status >= 200 && response.status < 300 && Array.isArray(response.data)) {
|
||
const dataArray = response.data as Array<any>
|
||
if (dataArray.length > 0) {
|
||
deviceInfo.value = dataArray[0] as DeviceInfo
|
||
updateDeviceStatus()
|
||
} else {
|
||
error.value = '加载设备信息失败'
|
||
}
|
||
} else {
|
||
error.value = '加载设备信息失败'
|
||
}
|
||
} catch (e) {
|
||
error.value = '加载设备信息失败: ' + (typeof e == 'string' ? e : e?.message ?? '未知错误')
|
||
console.log('加载设备信息失败:', e)
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
|
||
// 专门为图表加载少量历史数据
|
||
async function loadChartData() {
|
||
try {
|
||
const params : SensorDataParams = {
|
||
device_id: deviceId,
|
||
user_id: userId,
|
||
limit: 100, // 为图表获取10条数据用于趋势显示
|
||
offset: 0
|
||
}
|
||
|
||
const response = await SenseDataService.getMeasurements(params)
|
||
if (response.status >= 200 && response.status < 300 && response.data !== null) {
|
||
console.log(response)
|
||
// 将图表数据单独存储,不覆盖历史记录列表
|
||
const chartHistoryData = response.data as Array<SensorMeasurement>
|
||
updateChartWithData(chartHistoryData)
|
||
}
|
||
} catch (e) {
|
||
console.log('加载图表数据失败:', e)
|
||
// 如果图表数据加载失败,使用现有的历史数据
|
||
updateChart()
|
||
}
|
||
|
||
}
|
||
async function loadHistoryData() {
|
||
isLoading.value = true
|
||
error.value = ''
|
||
try {
|
||
const params : SensorDataParams = {
|
||
device_id: deviceId,
|
||
user_id: userId,
|
||
limit: 1, // 设置为 1,会自动使用 single() 方法优化查询
|
||
offset: 0
|
||
}
|
||
|
||
const response = await SenseDataService.getMeasurements(params)
|
||
if (response.status >= 200 && response.status < 300 && response.data !== null) {
|
||
historyData.value = response.data as Array<SensorMeasurement>
|
||
updateRealtimeMetrics()
|
||
// 图表需要更多数据,单独加载
|
||
await loadChartData()
|
||
} else {
|
||
error.value = '加载历史数据失败'
|
||
historyData.value = []
|
||
}
|
||
} catch (e) {
|
||
error.value = '加载历史数据失败: ' + (typeof e == 'string' ? e : e?.message ?? '未知错误')
|
||
console.log('加载历史数据失败:', e)
|
||
historyData.value = []
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
// 加载更多历史记录
|
||
async function loadMoreHistory() {
|
||
isLoading.value = true
|
||
try {
|
||
const params : SensorDataParams = {
|
||
device_id: deviceId,
|
||
user_id: userId,
|
||
limit: 50, // 加载更多历史数据
|
||
offset: 0
|
||
}
|
||
|
||
const response = await SenseDataService.getMeasurements(params)
|
||
if (response.status >= 200 && response.status < 300 && response.data !== null) {
|
||
historyData.value = response.data as Array<SensorMeasurement>
|
||
} else {
|
||
error.value = '加载更多历史数据失败'
|
||
}
|
||
} catch (e) {
|
||
error.value = '加载更多历史数据失败: ' + (typeof e == 'string' ? e : e?.message ?? '未知错误')
|
||
console.log('加载更多历史数据失败:', e)
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
function switchChart(type : string) {
|
||
activeChartType.value = type
|
||
// 重新加载图表数据以显示新类型的趋势
|
||
loadChartData()
|
||
}
|
||
// 刷新数据函数
|
||
function refreshData() {
|
||
loadDeviceInfo()
|
||
loadHistoryData()
|
||
}
|
||
|
||
onMounted(() => { // 获取设备ID参数
|
||
const pages = getCurrentPages()
|
||
if (pages.length > 0) {
|
||
const currentPage = pages[pages.length - 1]
|
||
const options = currentPage.options
|
||
const deviceIdParam = options?.device_id ?? ''
|
||
if (deviceIdParam !== '') {
|
||
deviceId = deviceIdParam
|
||
}
|
||
}
|
||
|
||
loadDeviceInfo()
|
||
loadHistoryData()
|
||
})
|
||
onUnmounted(() => {
|
||
// 清理资源
|
||
})
|
||
function formatTime(timeStr : string) : string {
|
||
if (timeStr == '') return '--'
|
||
|
||
const time = new Date(timeStr)
|
||
const now = new Date()
|
||
const diff = now.getTime() - time.getTime()
|
||
|
||
if (diff < 60000) { // 1分钟内
|
||
return '刚刚'
|
||
} else if (diff < 3600000) { // 1小时内
|
||
const minutes = Math.floor(diff / 60000)
|
||
return `${minutes}分钟前`
|
||
} else if (diff < 86400000) { // 24小时内
|
||
const hours = Math.floor(diff / 3600000)
|
||
return `${hours}小时前`
|
||
} else {
|
||
const month = (time.getMonth() + 1).toString().padStart(2, '0')
|
||
const day = time.getDate().toString().padStart(2, '0')
|
||
const hour = time.getHours().toString().padStart(2, '0')
|
||
const minute = time.getMinutes().toString().padStart(2, '0')
|
||
return `${month}-${day} ${hour}:${minute}`
|
||
}
|
||
} // 导航方法
|
||
function navigateToPage(pageName : string) {
|
||
// 验证必要参数
|
||
if (deviceId == '' || userId == '') {
|
||
uni.showToast({
|
||
title: '缺少必要参数',
|
||
icon: 'none'
|
||
})
|
||
console.log('导航失败: 缺少deviceId或userId', { deviceId, userId })
|
||
return
|
||
}
|
||
|
||
let url = ''
|
||
switch (pageName) {
|
||
case 'analysis':
|
||
// 分析页面需要用户ID和可选的设备ID
|
||
url = `/pages/sense/analysis?user_id=${userId}&device_id=${deviceId}`
|
||
break
|
||
case 'devices':
|
||
// 设备管理页面不需要特定参数,但传递用户ID以便管理
|
||
url = `/pages/sense/devices?user_id=${userId}`
|
||
break
|
||
case 'simulator':
|
||
// 模拟器页面需要设备ID和用户ID
|
||
url = `/pages/sense/simulator?device_id=${deviceId}&user_id=${userId}`
|
||
break
|
||
case 'settings':
|
||
// 设置页面需要设备ID和用户ID
|
||
url = `/pages/sense/settings?device_id=${deviceId}&user_id=${userId}`
|
||
break
|
||
case 'index':
|
||
// 当前页面,不需要跳转
|
||
return
|
||
default:
|
||
console.log('未知页面:', pageName)
|
||
uni.showToast({
|
||
title: '未知页面',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
console.log('准备导航到:', url)
|
||
uni.navigateTo({
|
||
url: url,
|
||
success: (res) => {
|
||
console.log('导航成功:', res)
|
||
},
|
||
fail: (error) => {
|
||
console.log('导航失败:', error)
|
||
uni.showToast({
|
||
title: '页面跳转失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.sense-container {
|
||
display: flex;
|
||
flex: 1;
|
||
padding: 20rpx;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
padding: 20rpx;
|
||
background-color: #ffffff;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
|
||
.header-actions {
|
||
flex-direction: row;
|
||
}
|
||
|
||
.refresh-btn,
|
||
.analyze-btn {
|
||
padding: 16rpx 24rpx;
|
||
margin-left: 16rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.refresh-btn {
|
||
background-color: #f0f0f0;
|
||
color: #666666;
|
||
}
|
||
|
||
.analyze-btn {
|
||
background-color: #409EFF;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.device-card,
|
||
.realtime-card,
|
||
.chart-card,
|
||
.history-card {
|
||
margin-bottom: 20rpx;
|
||
padding: 24rpx;
|
||
background-color: #ffffff;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.device-title,
|
||
.card-title {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.card-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.load-more-btn {
|
||
padding: 12rpx 20rpx;
|
||
background-color: #409EFF;
|
||
color: #ffffff;
|
||
border-radius: 6rpx;
|
||
font-size: 24rpx;
|
||
border: none;
|
||
}
|
||
|
||
.device-info {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.device-name {
|
||
font-size: 28rpx;
|
||
color: #666666;
|
||
}
|
||
|
||
.device-status {
|
||
font-size: 26rpx;
|
||
padding: 8rpx 16rpx;
|
||
border-radius: 16rpx;
|
||
}
|
||
|
||
.status-online {
|
||
background-color: #E8F5E8;
|
||
color: #52C41A;
|
||
}
|
||
|
||
.status-offline {
|
||
background-color: #FFF1F0;
|
||
color: #FF4D4F;
|
||
}
|
||
|
||
.metrics-grid {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.metric-item {
|
||
width: 48%;
|
||
padding: 20rpx;
|
||
margin-bottom: 16rpx;
|
||
background-color: #f8f9fa;
|
||
border-radius: 8rpx;
|
||
align-items: center;
|
||
}
|
||
|
||
.metric-label {
|
||
font-size: 24rpx;
|
||
color: #999999;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.metric-value {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
|
||
.metric-unit {
|
||
font-size: 22rpx;
|
||
color: #666666;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.chart-tabs {
|
||
flex-direction: row;
|
||
margin-bottom: 20rpx;
|
||
border-bottom: 2rpx solid #f0f0f0;
|
||
}
|
||
|
||
.tab-btn {
|
||
flex: 1;
|
||
padding: 20rpx;
|
||
background-color: transparent;
|
||
border: none;
|
||
color: #666666;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.tab-btn.active {
|
||
color: #409EFF;
|
||
border-bottom: 4rpx solid #409EFF;
|
||
}
|
||
|
||
.chart-component {
|
||
height: 400rpx;
|
||
}
|
||
|
||
.history-list {
|
||
max-height: 600rpx;
|
||
}
|
||
|
||
.history-item {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx 0;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
.history-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.history-type {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.history-value {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #409EFF;
|
||
}
|
||
|
||
.history-time {
|
||
font-size: 24rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.empty-history {
|
||
padding: 60rpx 0;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 26rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.analysis-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-content {
|
||
width: 80%;
|
||
max-height: 70%;
|
||
background-color: #ffffff;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx;
|
||
}
|
||
|
||
.modal-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24rpx;
|
||
border-bottom: 2rpx solid #f0f0f0;
|
||
padding-bottom: 16rpx;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
|
||
.close-btn {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border-radius: 30rpx;
|
||
background-color: #f0f0f0;
|
||
color: #666666;
|
||
font-size: 36rpx;
|
||
border: none;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-body {
|
||
flex: 1;
|
||
}
|
||
|
||
.analysis-summary {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
line-height: 1.6;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.recommendations {
|
||
background-color: #f8f9fa;
|
||
padding: 20rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.rec-title {
|
||
font-size: 26rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.rec-item {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
line-height: 1.5;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
/* 导航菜单样式 */
|
||
.nav-menu {
|
||
margin-bottom: 20rpx;
|
||
background-color: #ffffff;
|
||
border-radius: 12rpx;
|
||
padding: 16rpx;
|
||
}
|
||
|
||
.nav-info {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12rpx 16rpx;
|
||
margin-bottom: 12rpx;
|
||
background-color: #f8f9fa;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.nav-device-info {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
}
|
||
|
||
.nav-device-status {
|
||
font-size: 22rpx;
|
||
font-weight: bold;
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.nav-device-status.status-online {
|
||
background-color: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.nav-device-status.status-offline {
|
||
background-color: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.nav-tabs {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.nav-tab {
|
||
flex: 1;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 16rpx 8rpx;
|
||
margin: 0 4rpx;
|
||
border-radius: 8rpx;
|
||
background-color: transparent;
|
||
border: none;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.nav-tab.active {
|
||
background-color: #007AFF;
|
||
}
|
||
|
||
.nav-tab:hover {
|
||
background-color: #f0f0f0;
|
||
}
|
||
|
||
.nav-tab.active:hover {
|
||
background-color: #0056CC;
|
||
}
|
||
|
||
.nav-icon {
|
||
font-size: 32rpx;
|
||
margin-bottom: 8rpx;
|
||
color: #666666;
|
||
}
|
||
|
||
.nav-tab.active .nav-icon {
|
||
color: #ffffff;
|
||
}
|
||
|
||
.nav-text {
|
||
font-size: 22rpx;
|
||
color: #666666;
|
||
text-align: center;
|
||
}
|
||
|
||
.nav-tab.active .nav-text {
|
||
color: #ffffff;
|
||
font-weight: bold;
|
||
}
|
||
</style> |