Files
akmon/pages/sense/nav.uvue
2026-01-20 08:04:15 +08:00

807 lines
18 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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="nav-container">
<!-- 头部标题 -->
<view class="header">
<text class="main-title">智能传感器中心</text>
<text class="sub-title">运动与健康数据监控平台</text>
</view>
<!-- 快速状态卡片 -->
<view class="status-cards">
<view class="status-card">
<text class="card-icon">📱</text>
<text class="card-title">设备状态</text>
<text class="card-value">{{ deviceStatus.active }}/{{ deviceStatus.total }}</text>
<text class="card-unit">台在线</text>
</view>
<view class="status-card">
<text class="card-icon">📊</text>
<text class="card-title">今日数据</text>
<text class="card-value">{{ todayData.count }}</text>
<text class="card-unit">条记录</text>
</view>
<view class="status-card">
<text class="card-icon">⚡</text>
<text class="card-title">实时监控</text>
<text class="card-value">{{ realtimeStatus ? '开启' : '关闭' }}</text>
<text class="card-unit">{{ realtimeStatus ? '🟢' : '🔴' }}</text>
</view>
</view>
<!-- 主要功能导航 -->
<view class="main-nav">
<text class="nav-section-title">主要功能</text>
<view class="nav-grid">
<view class="nav-item large" @click="navigateTo('/pages/sense/index')">
<view class="nav-icon-container">
<text class="nav-icon">📈</text>
</view>
<text class="nav-title">实时监控</text>
<text class="nav-desc">查看传感器实时数据与趋势</text>
</view>
<view class="nav-item large" @click="navigateTo('/pages/sense/analysis')">
<view class="nav-icon-container">
<text class="nav-icon">🧠</text>
</view>
<text class="nav-title">数据分析</text>
<text class="nav-desc">AI智能分析与健康评估</text>
</view>
<view class="nav-item large" @click="navigateTo('/pages/sense/healthble')">
<view class="nav-icon-container">
<text class="nav-icon">🩺</text>
</view>
<text class="nav-title">蓝牙仪表</text>
<text class="nav-desc">连接手环并处理报警/录音</text>
</view>
</view>
</view>
<!-- 设备管理 -->
<view class="device-section">
<text class="nav-section-title">设备管理</text>
<view class="nav-grid">
<view class="nav-item" @click="navigateTo('/pages/sense/devices')">
<text class="nav-icon">📱</text>
<text class="nav-title">设备管理</text>
</view>
<view class="nav-item" @click="navigateTo('/pages/sense/settings')">
<text class="nav-icon">⚙️</text>
<text class="nav-title">传感器设置</text>
</view>
</view>
</view>
<!-- 数据管理 -->
<view class="data-section">
<text class="nav-section-title">数据管理</text>
<view class="data-options">
<button class="data-btn" @click="exportAllData">
<text class="btn-icon">📤</text>
<text class="btn-text">导出数据</text>
</button>
<button class="data-btn" @click="syncData">
<text class="btn-icon">🔄</text>
<text class="btn-text">同步数据</text>
</button>
<button class="data-btn" @click="showDataStats">
<text class="btn-icon">📊</text>
<text class="btn-text">数据统计</text>
</button>
</view>
</view>
<!-- 最近数据预览 -->
<view class="recent-section">
<view class="recent-header">
<text class="nav-section-title">最近测量</text>
<button class="more-btn" @click="navigateTo('/pages/sense/index')">查看更多</button>
</view>
<scroll-view class="recent-list" scroll-x>
<view class="recent-item" v-for="(item, index) in recentData" :key="index" @click="viewDetail(item)">
<text class="recent-type">{{ getTypeLabel(item.getString('measurement_type') ?? '') }}</text>
<text class="recent-value">{{ formatValue(item) }}</text>
<text class="recent-time">{{ formatTime(item.getString('measured_at') ?? '') }}</text>
</view>
</scroll-view>
</view>
<!-- 健康提醒 -->
<view class="reminder-section" v-if="healthReminders.length > 0">
<text class="nav-section-title">健康提醒</text>
<view class="reminder-list">
<view class="reminder-item" v-for="(reminder, index) in healthReminders" :key="index">
<text class="reminder-icon">{{ reminder.icon }}</text>
<view class="reminder-content">
<text class="reminder-title">{{ reminder.title }}</text>
<text class="reminder-desc">{{ reminder.description }}</text>
</view>
<button class="reminder-btn" @click="handleReminder(reminder)">{{ reminder.action }}</button>
</view>
</view>
</view>
<!-- 数据统计弹窗 -->
<view class="stats-modal" v-if="showStats" @click="closeStats">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">数据统计</text>
<button class="close-btn" @click="closeStats">×</button>
</view>
<view class="modal-body">
<view class="stats-grid">
<view class="stats-item" v-for="(stat, index) in dataStats" :key="index">
<text class="stats-label">{{ stat.label }}</text>
<text class="stats-value">{{ stat.value }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import AkSupa from '@/components/supadb/aksupa.uts'
// 响应式数据
const deviceStatus = ref({
active: 0,
total: 0
})
const todayData = ref({
count: 0
})
const realtimeStatus = ref<boolean>(false)
const recentData = ref<Array<UTSJSONObject>>([])
const healthReminders = ref<Array<UTSJSONObject>>([])
const showStats = ref<boolean>(false)
const dataStats = ref<Array<UTSJSONObject>>([])
const userId = 'eed3824b-bba1-4309-8048-19d17367c084'
let supa: AkSupa | null = null
onMounted(() => {
initSupa()
loadDashboardData()
})
function initSupa() {
const supaUrl = getApp().globalData.supabaseUrl ?? ''
const supaKey = getApp().globalData.supabaseKey ?? ''
supa = new AkSupa(supaUrl, supaKey)
}
async function loadDashboardData() {
await loadDeviceStatus()
await loadTodayData()
await loadRecentData()
await loadHealthReminders()
checkRealtimeStatus()
}
async function loadDeviceStatus() {
if (supa === null) return
try {
const result = await supa.from('ak_devices')
.eq('user_id', userId)
.execute()
if (result.data !== null && Array.isArray(result.data)) {
const devices = result.data as Array<UTSJSONObject>
deviceStatus.value.total = devices.length
deviceStatus.value.active = devices.filter(d => d.getString('status') === 'active').length
}
} catch (e) {
console.log('加载设备状态失败:', e)
}
}
async function loadTodayData() {
if (supa === null) return
try {
const today = new Date()
today.setHours(0, 0, 0, 0)
const todayStr = today.toISOString()
const result = await supa.from('ss_sensor_measurements')
.eq('user_id', userId)
.gte('measured_at', todayStr)
.execute()
if (result.data !== null && Array.isArray(result.data)) {
todayData.value.count = result.data.length
}
} catch (e) {
console.log('加载今日数据失败:', e)
}
}
async function loadRecentData() {
if (supa === null) return
try {
const result = await supa.from('ss_sensor_measurements')
.eq('user_id', userId)
.order('measured_at', { ascending: false })
.limit(10)
.execute()
if (result.data !== null && Array.isArray(result.data)) {
recentData.value = result.data as Array<UTSJSONObject>
}
} catch (e) {
console.log('加载最近数据失败:', e)
}
}
async function loadHealthReminders() {
// 模拟健康提醒数据
const reminders: Array<UTSJSONObject> = []
// 检查是否有异常数据需要关注
if (recentData.value.length > 0) {
const latestHeartRate = recentData.value.find(item =>
item.getString('measurement_type') === 'heart_rate'
)
if (latestHeartRate !== null) {
const rawData = latestHeartRate.getJSON('raw_data')
if (rawData !== null) {
const bpm = rawData.getNumber('bpm') ?? 0
if (bpm > 100) {
const reminder = new UTSJSONObject()
reminder.set('icon', '❤️')
reminder.set('title', '心率偏高提醒')
reminder.set('description', `最近测量心率${bpm}次/分钟,建议注意休息`)
reminder.set('action', '查看详情')
reminder.set('type', 'heart_rate_high')
reminders.push(reminder)
}
}
}
// 添加运动提醒
const stepsToday = recentData.value.filter(item =>
item.getString('measurement_type') === 'steps'
)
if (stepsToday.length === 0) {
const reminder = new UTSJSONObject()
reminder.set('icon', '🚶')
reminder.set('title', '运动提醒')
reminder.set('description', '今天还没有运动记录,建议进行适量运动')
reminder.set('action', '开始运动')
reminder.set('type', 'exercise_reminder')
reminders.push(reminder)
}
}
healthReminders.value = reminders
}
function checkRealtimeStatus() {
// 模拟检查实时监控状态
realtimeStatus.value = true
}
function navigateTo(url: string) {
uni.navigateTo({ url })
}
function viewDetail(item: UTSJSONObject) {
const id = item.getString('id') ?? ''
uni.navigateTo({
url: `/pages/sense/detail?id=${id}`
})
}
async function exportAllData() {
uni.showLoading({
title: '准备导出...'
})
// 模拟导出过程
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '数据导出完成',
icon: 'success'
})
}, 2000)
}
async function syncData() {
uni.showLoading({
title: '同步数据中...'
})
try {
// 模拟数据同步
await new Promise<void>((resolve) => {
setTimeout(resolve, 1500)
})
uni.hideLoading()
uni.showToast({
title: '数据同步完成',
icon: 'success'
})
// 刷新数据
await loadDashboardData()
} catch (e) {
uni.hideLoading()
uni.showToast({
title: '同步失败',
icon: 'error'
})
}
}
function showDataStats() {
// 构建统计数据
const stats: Array<UTSJSONObject> = []
const totalMeasurements = new UTSJSONObject()
totalMeasurements.set('label', '总测量次数')
totalMeasurements.set('value', recentData.value.length.toString())
stats.push(totalMeasurements)
const dataTypes = new Set<string>()
recentData.value.forEach(item => {
const type = item.getString('measurement_type') ?? ''
if (type !== '') {
dataTypes.add(type)
}
})
const uniqueTypes = new UTSJSONObject()
uniqueTypes.set('label', '数据类型')
uniqueTypes.set('value', dataTypes.size.toString())
stats.push(uniqueTypes)
const activeDevices = new UTSJSONObject()
activeDevices.set('label', '活跃设备')
activeDevices.set('value', deviceStatus.value.active.toString())
stats.push(activeDevices)
dataStats.value = stats
showStats.value = true
}
function closeStats() {
showStats.value = false
}
function handleReminder(reminder: UTSJSONObject) {
const type = reminder.getString('type') ?? ''
if (type === 'heart_rate_high') {
navigateTo('/pages/sense/analysis')
} else if (type === 'exercise_reminder') {
navigateTo('/pages/sense/index')
}
}
// 工具函数
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 formatValue(item: UTSJSONObject): string {
const rawData = item.getJSON('raw_data')
const type = item.getString('measurement_type') ?? ''
if (rawData === null) return '--'
if (type === 'heart_rate') {
const bpm = rawData.getNumber('bpm') ?? 0
return bpm.toString() + ' bpm'
} 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}`
}
return '--'
}
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 < 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 days = Math.floor(diff / 86400000)
return `${days}天前`
}
}
return {
deviceStatus,
todayData,
realtimeStatus,
recentData,
healthReminders,
showStats,
dataStats,
navigateTo,
viewDetail,
exportAllData,
syncData,
showDataStats,
closeStats,
handleReminder,
getTypeLabel,
formatValue,
formatTime
}
}
}
</script>
<style scoped>
.nav-container {
flex: 1;
padding: 20rpx;
background-color: #f5f5f5;
}
.header {
align-items: center;
margin-bottom: 30rpx;
padding: 40rpx 20rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16rpx;
}
.main-title {
font-size: 40rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 8rpx;
}
.sub-title {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.status-cards {
flex-direction: row;
justify-content: space-between;
margin-bottom: 30rpx;
}
.status-card {
flex: 1;
align-items: center;
padding: 24rpx;
margin: 0 8rpx;
background-color: #ffffff;
border-radius: 12rpx;
}
.card-icon {
font-size: 48rpx;
margin-bottom: 12rpx;
}
.card-title {
font-size: 24rpx;
color: #666666;
margin-bottom: 8rpx;
}
.card-value {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 4rpx;
}
.card-unit {
font-size: 22rpx;
color: #999999;
}
.main-nav, .device-section, .data-section, .recent-section, .reminder-section {
margin-bottom: 30rpx;
}
.nav-section-title {
font-size: 30rpx;
font-weight: bold;
color: #333333;
margin-bottom: 20rpx;
}
.nav-grid {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.nav-item {
width: 48%;
align-items: center;
padding: 24rpx;
margin-bottom: 16rpx;
background-color: #ffffff;
border-radius: 12rpx;
}
.nav-item.large {
width: 100%;
flex-direction: row;
align-items: center;
padding: 32rpx;
margin-bottom: 16rpx;
}
.nav-icon-container {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background-color: #f0f8ff;
justify-content: center;
align-items: center;
margin-right: 24rpx;
}
.nav-item.large .nav-icon-container {
margin-bottom: 0;
}
.nav-icon {
font-size: 64rpx;
}
.nav-item.large .nav-icon {
font-size: 48rpx;
}
.nav-title {
font-size: 28rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
}
.nav-item.large .nav-title {
font-size: 32rpx;
margin-bottom: 12rpx;
}
.nav-desc {
font-size: 24rpx;
color: #666666;
text-align: center;
line-height: 1.4;
}
.nav-item.large .nav-desc {
text-align: left;
}
.data-options {
flex-direction: row;
justify-content: space-around;
}
.data-btn {
flex: 1;
flex-direction: column;
align-items: center;
padding: 24rpx;
margin: 0 8rpx;
background-color: #ffffff;
border-radius: 12rpx;
border: none;
}
.btn-icon {
font-size: 48rpx;
margin-bottom: 12rpx;
}
.btn-text {
font-size: 26rpx;
color: #333333;
}
.recent-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.more-btn {
padding: 12rpx 20rpx;
background-color: #409EFF;
color: #ffffff;
border-radius: 20rpx;
font-size: 24rpx;
border: none;
}
.recent-list {
flex-direction: row;
}
.recent-item {
width: 240rpx;
align-items: center;
padding: 24rpx;
margin-right: 16rpx;
background-color: #ffffff;
border-radius: 12rpx;
}
.recent-type {
font-size: 24rpx;
color: #666666;
margin-bottom: 8rpx;
}
.recent-value {
font-size: 28rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
}
.recent-time {
font-size: 22rpx;
color: #999999;
}
.reminder-list {
flex-direction: column;
}
.reminder-item {
flex-direction: row;
align-items: center;
padding: 24rpx;
margin-bottom: 16rpx;
background-color: #ffffff;
border-radius: 12rpx;
}
.reminder-icon {
font-size: 48rpx;
margin-right: 20rpx;
}
.reminder-content {
flex: 1;
}
.reminder-title {
font-size: 28rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
}
.reminder-desc {
font-size: 24rpx;
color: #666666;
line-height: 1.4;
}
.reminder-btn {
padding: 16rpx 24rpx;
background-color: #409EFF;
color: #ffffff;
border-radius: 20rpx;
font-size: 24rpx;
border: none;
}
.stats-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%;
background-color: #ffffff;
border-radius: 16rpx;
}
.modal-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.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 {
padding: 32rpx;
}
.stats-grid {
flex-direction: column;
}
.stats-item {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.stats-item:last-child {
border-bottom: none;
}
.stats-label {
font-size: 28rpx;
color: #333333;
}
.stats-value {
font-size: 28rpx;
font-weight: bold;
color: #409EFF;
}
</style>