Initial commit of akmon project
This commit is contained in:
951
pages/sense/index.uvue
Normal file
951
pages/sense/index.uvue
Normal file
@@ -0,0 +1,951 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user