Files
akmon/pages/ec/analytics/dashboard.uvue
2026-01-20 08:04:15 +08:00

1239 lines
24 KiB
Plaintext
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="analytics-dashboard">
<!-- Header -->
<view class="header">
<text class="header-title">数据分析</text>
<view class="header-actions">
<button class="action-btn" @click="exportReport">
<text class="btn-text">导出报告</text>
</button>
<button class="action-btn settings" @click="showSettingsModal">
<text class="btn-text">设置</text>
</button>
</view>
</view>
<!-- Date Range Selector -->
<view class="date-range-section">
<text class="section-title">时间范围</text>
<view class="date-range-controls">
<view class="quick-dates">
<button
v-for="period in quickPeriods"
:key="period.value"
class="quick-date-btn"
:class="{ 'active': selectedPeriod === period.value }"
@click="selectPeriod(period.value)"
>
<text class="btn-text">{{ period.label }}</text>
</button>
</view>
<view class="custom-date-range">
<input
class="date-input"
type="date"
v-model="customDateRange.start"
@change="onCustomDateChange"
/>
<text class="date-separator">至</text>
<input
class="date-input"
type="date"
v-model="customDateRange.end"
@change="onCustomDateChange"
/>
</view>
</view>
</view>
<!-- Key Metrics Overview -->
<view class="metrics-overview">
<text class="section-title">关键指标</text>
<view class="metrics-grid">
<view class="metric-card" v-for="metric in keyMetrics" :key="metric.key">
<text class="metric-icon">{{ metric.icon }}</text>
<text class="metric-value">{{ metric.value }}</text>
<text class="metric-label">{{ metric.label }}</text>
<view class="metric-trend" :class="metric.trend">
<text class="trend-icon">{{ getTrendIcon(metric.trend) }}</text>
<text class="trend-text">{{ metric.change }}%</text>
</view>
</view>
</view>
</view>
<!-- Charts Section -->
<view class="charts-section">
<text class="section-title">统计图表</text>
<!-- Health Trends Chart -->
<view class="chart-container">
<view class="chart-header">
<text class="chart-title">健康趋势</text>
<picker class="chart-filter" mode="selector" :value="selectedHealthMetric" :range="healthMetricLabels" @change="onHealthMetricChange">
<view class="picker-item">
<text class="picker-text">{{ healthMetricLabels[selectedHealthMetric] }}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<view class="chart-placeholder">
<text class="placeholder-text">健康趋势图表</text>
<text class="placeholder-desc">显示所选指标的变化趋势</text>
</view>
</view>
<!-- Activity Distribution Chart -->
<view class="chart-container">
<view class="chart-header">
<text class="chart-title">活动分布</text>
</view>
<view class="activity-stats">
<view class="activity-stat-item" v-for="activity in activityStats" :key="activity.type">
<view class="stat-bar">
<view class="stat-fill" :style="{ width: activity.percentage + '%', backgroundColor: activity.color }"></view>
</view>
<view class="stat-info">
<text class="stat-label">{{ activity.label }}</text>
<text class="stat-value">{{ activity.count }}次 ({{ activity.percentage }}%)</text>
</view>
</view>
</view>
</view>
<!-- Medication Compliance Chart -->
<view class="chart-container">
<view class="chart-header">
<text class="chart-title">用药依从性</text>
</view>
<view class="compliance-overview">
<view class="compliance-circle">
<text class="compliance-percentage">{{ medicationCompliance.percentage }}%</text>
<text class="compliance-label">总体依从性</text>
</view>
<view class="compliance-details">
<view class="compliance-item">
<text class="compliance-number">{{ medicationCompliance.taken }}</text>
<text class="compliance-text">已服用</text>
</view>
<view class="compliance-item">
<text class="compliance-number">{{ medicationCompliance.missed }}</text>
<text class="compliance-text">已错过</text>
</view>
<view class="compliance-item">
<text class="compliance-number">{{ medicationCompliance.total }}</text>
<text class="compliance-text">总计</text>
</view>
</view>
</view>
</view>
</view>
<!-- Care Quality Metrics -->
<view class="care-quality-section">
<text class="section-title">护理质量</text>
<view class="quality-metrics">
<view class="quality-card" v-for="quality in careQualityMetrics" :key="quality.category">
<text class="quality-title">{{ quality.category }}</text>
<view class="quality-score" :class="getQualityScoreClass(quality.score)">
<text class="score-value">{{ quality.score }}</text>
<text class="score-max">/100</text>
</view>
<view class="quality-details">
<view class="quality-item" v-for="item in quality.items" :key="item.name">
<text class="item-name">{{ item.name }}</text>
<view class="item-progress">
<view class="progress-bar">
<view class="progress-fill" :style="{ width: item.value + '%' }"></view>
</view>
<text class="item-value">{{ item.value }}%</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- Alerts and Recommendations -->
<view class="alerts-section" v-if="alerts.length > 0">
<text class="section-title">预警与建议</text>
<view class="alerts-list">
<view class="alert-item" v-for="alert in alerts" :key="alert.id" :class="alert.level">
<text class="alert-icon">{{ getAlertIcon(alert.level) }}</text>
<view class="alert-content">
<text class="alert-title">{{ alert.title }}</text>
<text class="alert-message">{{ alert.message }}</text>
<text class="alert-time">{{ formatDateTime(alert.created_at) }}</text>
</view>
<button class="alert-action" @tap="handleAlert(alert)" v-if="!alert.handled">
<text class="btn-text">处理</text>
</button>
</view>
</view>
</view>
<!-- Settings Modal -->
<view class="settings-modal" v-if="showSettings" @tap="hideSettingsModal">
<view class="modal-content" @tap.stop>
<view class="modal-header">
<text class="modal-title">分析设置</text>
<button class="close-btn" @tap="hideSettingsModal">
<text class="close-icon">×</text>
</button>
</view>
<view class="modal-body">
<view class="setting-section">
<text class="setting-title">显示设置</text>
<view class="setting-item">
<text class="setting-label">自动刷新</text>
<switch class="setting-switch" :checked="settings.autoRefresh" @change="onAutoRefreshChange" />
</view>
<view class="setting-item">
<text class="setting-label">显示详细数据</text>
<switch class="setting-switch" :checked="settings.showDetailedData" @change="onShowDetailedDataChange" />
</view>
</view>
<view class="setting-section">
<text class="setting-title">通知设置</text>
<view class="setting-item">
<text class="setting-label">异常数据提醒</text>
<switch class="setting-switch" :checked="settings.alertOnAbnormal" @change="onAlertOnAbnormalChange" />
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<style scoped>
.analytics-dashboard {
padding: 40rpx;
background-color: #f8f9fa;
min-height: 100vh;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 40rpx;
}
.header-title {
font-size: 48rpx;
font-weight: bold;
color: #333;
}
.header-actions {
display: flex;
gap: 20rpx;
}
.action-btn {
padding: 20rpx 30rpx;
border-radius: 20rpx;
border: none;
font-size: 28rpx;
font-weight: 600;
}
.action-btn:not(.settings) {
background: #007AFF;
color: white;
}
.action-btn.settings {
background: #f0f0f0;
color: #333;
}
.section-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 24rpx;
}
.date-range-section {
background: white;
padding: 40rpx;
border-radius: 24rpx;
margin-bottom: 40rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.date-range-controls {
display: flex;
flex-direction: column;
gap: 30rpx;
}
.quick-dates {
display: flex;
gap: 20rpx;
flex-wrap: wrap;
}
.quick-date-btn {
padding: 20rpx 30rpx;
border-radius: 20rpx;
border: 1rpx solid #ddd;
background: white;
color: #333;
font-size: 28rpx;
}
.quick-date-btn.active {
background: #007AFF;
color: white;
border-color: #007AFF;
}
.custom-date-range {
display: flex;
align-items: center;
gap: 20rpx;
}
.date-input {
flex: 1;
padding: 20rpx;
border: 1rpx solid #ddd;
border-radius: 12rpx;
font-size: 28rpx;
}
.date-separator {
font-size: 28rpx;
color: #666;
}
.metrics-overview {
margin-bottom: 40rpx;
}
.metrics-grid {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.metric-card {
flex: 1;
min-width: 280rpx;
background: white;
padding: 40rpx;
border-radius: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
position: relative;
}
.metric-icon {
font-size: 48rpx;
display: block;
margin-bottom: 12rpx;
}
.metric-value {
font-size: 48rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.metric-label {
font-size: 28rpx;
color: #666;
display: block;
}
.metric-trend {
position: absolute;
top: 20rpx;
right: 20rpx;
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 12rpx;
border-radius: 16rpx;
font-size: 24rpx;
}
.metric-trend.up {
background: #e8f5e8;
color: #4caf50;
}
.metric-trend.down {
background: #ffebee;
color: #f44336;
}
.metric-trend.stable {
background: #f5f5f5;
color: #666;
}
.charts-section {
margin-bottom: 40rpx;
}
.chart-container {
background: white;
border-radius: 24rpx;
padding: 40rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.chart-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
.chart-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.chart-filter {
background: #f0f0f0;
border-radius: 12rpx;
}
.picker-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 20rpx;
min-width: 200rpx;
}
.picker-text {
font-size: 26rpx;
color: #333;
}
.picker-arrow {
font-size: 20rpx;
color: #666;
}
.chart-placeholder {
height: 400rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 16rpx;
border: 2rpx dashed #ddd;
}
.placeholder-text {
font-size: 32rpx;
color: #666;
margin-bottom: 12rpx;
}
.placeholder-desc {
font-size: 26rpx;
color: #999;
}
.activity-stats {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.activity-stat-item {
display: flex;
align-items: center;
gap: 20rpx;
}
.stat-bar {
flex: 1;
height: 20rpx;
background: #f0f0f0;
border-radius: 10rpx;
overflow: hidden;
}
.stat-fill {
height: 100%;
border-radius: 10rpx;
transition: width 0.3s ease;
}
.stat-info {
width: 240rpx;
flex-shrink: 0;
}
.stat-label {
font-size: 28rpx;
color: #333;
display: block;
margin-bottom: 4rpx;
}
.stat-value {
font-size: 24rpx;
color: #666;
display: block;
}
.compliance-overview {
display: flex;
align-items: center;
gap: 60rpx;
}
.compliance-circle {
width: 200rpx;
height: 200rpx;
border-radius: 100rpx;
background: conic-gradient(#4caf50 0deg 252deg, #f0f0f0 252deg 360deg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
.compliance-circle::before {
content: '';
width: 140rpx;
height: 140rpx;
border-radius: 70rpx;
background: white;
position: absolute;
}
.compliance-percentage {
font-size: 36rpx;
font-weight: bold;
color: #333;
position: relative;
z-index: 1;
}
.compliance-label {
font-size: 24rpx;
color: #666;
position: relative;
z-index: 1;
}
.compliance-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.compliance-item {
display: flex;
align-items: center;
gap: 20rpx;
}
.compliance-number {
font-size: 32rpx;
font-weight: bold;
color: #007AFF;
width: 80rpx;
}
.compliance-text {
font-size: 28rpx;
color: #333;
}
.care-quality-section {
margin-bottom: 40rpx;
}
.quality-metrics {
display: flex;
flex-direction: column;
gap: 30rpx;
}
.quality-card {
background: white;
padding: 40rpx;
border-radius: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.quality-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 20rpx;
}
.quality-score {
display: flex;
align-items: baseline;
margin-bottom: 30rpx;
}
.score-value {
font-size: 72rpx;
font-weight: bold;
}
.score-max {
font-size: 36rpx;
color: #999;
margin-left: 8rpx;
}
.quality-score.excellent .score-value {
color: #4caf50;
}
.quality-score.good .score-value {
color: #2196f3;
}
.quality-score.average .score-value {
color: #ff9800;
}
.quality-score.poor .score-value {
color: #f44336;
}
.quality-details {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.quality-item {
display: flex;
align-items: center;
gap: 20rpx;
}
.item-name {
width: 200rpx;
font-size: 28rpx;
color: #333;
flex-shrink: 0;
}
.item-progress {
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
}
.progress-bar {
flex: 1;
height: 16rpx;
background: #f0f0f0;
border-radius: 8rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #007AFF;
border-radius: 8rpx;
transition: width 0.3s ease;
}
.item-value {
width: 80rpx;
font-size: 26rpx;
color: #666;
text-align: right;
}
.alerts-section {
margin-bottom: 40rpx;
}
.alerts-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.alert-item {
background: white;
padding: 30rpx;
border-radius: 16rpx;
display: flex;
align-items: flex-start;
gap: 20rpx;
border-left: 8rpx solid #ddd;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.alert-item.high {
border-left-color: #f44336;
}
.alert-item.medium {
border-left-color: #ff9800;
}
.alert-item.low {
border-left-color: #2196f3;
}
.alert-icon {
font-size: 40rpx;
flex-shrink: 0;
}
.alert-content {
flex: 1;
}
.alert-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.alert-message {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
line-height: 1.4;
}
.alert-time {
font-size: 24rpx;
color: #999;
display: block;
}
.alert-action {
padding: 16rpx 24rpx;
background: #007AFF;
color: white;
border-radius: 16rpx;
border: none;
font-size: 26rpx;
}
.settings-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 24rpx;
width: 90%;
max-width: 800rpx;
max-height: 80%;
overflow: hidden;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #eee;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.close-btn {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
background: #f0f0f0;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.close-icon {
font-size: 40rpx;
color: #666;
}
.modal-body {
padding: 40rpx;
max-height: 600rpx;
overflow-y: auto;
}
.setting-section {
margin-bottom: 40rpx;
}
.setting-section:last-child {
margin-bottom: 0;
}
.setting-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 20rpx;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.setting-item:last-child {
border-bottom: none;
}
.setting-label {
font-size: 30rpx;
color: #333;
}
.setting-switch {
transform: scale(0.8);
}
</style>
<script setup lang="uts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { formatDateTime } from '../types.uts'
import type { AnalyticsMetric, ActivityStat, Alert, CareQualityMetric } from '../types.uts'
// 数据状态
const keyMetrics = ref<AnalyticsMetric[]>([])
const activityStats = ref<ActivityStat[]>([])
const medicationCompliance = ref({
percentage: 85,
taken: 127,
missed: 23,
total: 150
})
const careQualityMetrics = ref<CareQualityMetric[]>([])
const alerts = ref<Alert[]>([])
// UI状态
const selectedPeriod = ref('week')
const customDateRange = ref({
start: '',
end: ''
})
const selectedHealthMetric = ref(0)
const showSettings = ref(false)
// 设置状态
const settings = ref({
autoRefresh: true,
showDetailedData: false,
alertOnAbnormal: true
})
// 刷新定时器
let refreshTimer: number | null = null
// 快速时间选择
const quickPeriods = [
{ label: '今天', value: 'today' },
{ label: '本周', value: 'week' },
{ label: '本月', value: 'month' },
{ label: '最近3个月', value: 'quarter' },
{ label: '自定义', value: 'custom' }
]
// 健康指标选项
const healthMetrics = [
{ label: '心率', value: 'heart_rate' },
{ label: '血压', value: 'blood_pressure' },
{ label: '体温', value: 'temperature' },
{ label: '血糖', value: 'blood_sugar' }
]
const healthMetricLabels = computed(() => healthMetrics.map(m => m.label))
// 辅助函数
function getTrendIcon(trend: string): string {
const icons = {
'up': '↗️',
'down': '↘️',
'stable': '→'
}
return icons[trend] || '→'
}
function getQualityScoreClass(score: number): string {
if (score >= 90) return 'excellent'
if (score >= 80) return 'good'
if (score >= 70) return 'average'
return 'poor'
}
function getAlertIcon(level: string): string {
const icons = {
'high': '🚨',
'medium': '⚠️',
'low': ''
}
return icons[level] || ''
}
// 事件处理
function selectPeriod(period: string) {
selectedPeriod.value = period
if (period === 'custom') {
// 显示自定义日期选择
const today = new Date()
const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
customDateRange.value.start = oneWeekAgo.toISOString().split('T')[0]
customDateRange.value.end = today.toISOString().split('T')[0]
}
loadAnalyticsData()
}
function onCustomDateChange() {
if (customDateRange.value.start && customDateRange.value.end) {
selectedPeriod.value = 'custom'
loadAnalyticsData()
}
}
function onHealthMetricChange(e: any) {
selectedHealthMetric.value = e.detail.value
// 这里可以重新加载对应指标的图表数据
}
function showSettingsModal() {
showSettings.value = true
}
function hideSettingsModal() {
showSettings.value = false
}
function onAutoRefreshChange(e: any) {
settings.value.autoRefresh = e.detail.value
if (settings.value.autoRefresh) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
}
function onShowDetailedDataChange(e: any) {
settings.value.showDetailedData = e.detail.value
}
function onAlertOnAbnormalChange(e: any) {
settings.value.alertOnAbnormal = e.detail.value
}
async function exportReport() {
try {
uni.showLoading({
title: '正在生成报告...'
})
const supa = (globalThis as any).supa
const result = await supa.executeAs('generate_analytics_report', {
period: selectedPeriod.value,
start_date: customDateRange.value.start,
end_date: customDateRange.value.end
})
uni.hideLoading()
if (result && result.length > 0) {
uni.showToast({
title: '报告已生成',
icon: 'success'
})
// 这里可以下载或分享报告
}
} catch (error) {
uni.hideLoading()
console.error('导出报告失败:', error)
uni.showToast({
title: '导出失败',
icon: 'error'
})
}
}
async function handleAlert(alert: Alert) {
try {
const supa = (globalThis as any).supa
await supa.executeAs('handle_alert', {
alert_id: alert.id
})
// 更新本地状态
const index = alerts.value.findIndex(a => a.id === alert.id)
if (index !== -1) {
alerts.value[index].handled = true
}
uni.showToast({
title: '已处理',
icon: 'success'
})
} catch (error) {
console.error('处理预警失败:', error)
uni.showToast({
title: '处理失败',
icon: 'error'
})
}
}
// 数据加载
async function loadAnalyticsData() {
await Promise.all([
loadKeyMetrics(),
loadActivityStats(),
loadMedicationCompliance(),
loadCareQualityMetrics(),
loadAlerts()
])
}
async function loadKeyMetrics() {
try {
const supa = (globalThis as any).supa
const result = await supa.executeAs('get_key_metrics', {
period: selectedPeriod.value,
start_date: customDateRange.value.start,
end_date: customDateRange.value.end
})
if (result && result.length > 0) {
keyMetrics.value = result
} else {
// 模拟数据
keyMetrics.value = [
{
key: 'total_elders',
label: '在院老人',
value: '145',
icon: '👥',
trend: 'up',
change: '3.2'
},
{
key: 'health_alerts',
label: '健康预警',
value: '8',
icon: '⚠️',
trend: 'down',
change: '12.5'
},
{
key: 'medication_compliance',
label: '用药依从性',
value: '94.2%',
icon: '💊',
trend: 'up',
change: '2.1'
},
{
key: 'care_satisfaction',
label: '护理满意度',
value: '4.8',
icon: '⭐',
trend: 'stable',
change: '0.1'
}
]
}
} catch (error) {
console.error('加载关键指标失败:', error)
}
}
async function loadActivityStats() {
try {
const supa = (globalThis as any).supa
const result = await supa.executeAs('get_activity_stats', {
period: selectedPeriod.value,
start_date: customDateRange.value.start,
end_date: customDateRange.value.end
})
if (result && result.length > 0) {
activityStats.value = result
} else {
// 模拟数据
activityStats.value = [
{
type: 'exercise',
label: '运动康复',
count: 89,
percentage: 35,
color: '#4caf50'
},
{
type: 'social',
label: '社交活动',
count: 67,
percentage: 26,
color: '#2196f3'
},
{
type: 'medical',
label: '医疗检查',
count: 43,
percentage: 17,
color: '#ff9800'
},
{
type: 'entertainment',
label: '娱乐活动',
count: 56,
percentage: 22,
color: '#9c27b0'
}
]
}
} catch (error) {
console.error('加载活动统计失败:', error)
}
}
async function loadMedicationCompliance() {
try {
const supa = (globalThis as any).supa
const result = await supa.executeAs('get_medication_compliance', {
period: selectedPeriod.value,
start_date: customDateRange.value.start,
end_date: customDateRange.value.end
})
if (result && result.length > 0) {
medicationCompliance.value = result[0]
}
} catch (error) {
console.error('加载用药依从性失败:', error)
}
}
async function loadCareQualityMetrics() {
try {
const supa = (globalThis as any).supa
const result = await supa.executeAs('get_care_quality_metrics', {
period: selectedPeriod.value,
start_date: customDateRange.value.start,
end_date: customDateRange.value.end
})
if (result && result.length > 0) {
careQualityMetrics.value = result
} else {
// 模拟数据
careQualityMetrics.value = [
{
category: '护理服务',
score: 92,
items: [
{ name: '及时响应', value: 95 },
{ name: '专业技能', value: 88 },
{ name: '服务态度', value: 94 }
]
},
{
category: '健康管理',
score: 88,
items: [
{ name: '监测频率', value: 92 },
{ name: '数据准确性', value: 85 },
{ name: '异常处理', value: 87 }
]
}
]
}
} catch (error) {
console.error('加载护理质量指标失败:', error)
}
}
async function loadAlerts() {
try {
const supa = (globalThis as any).supa
const result = await supa.executeAs('get_active_alerts', {
limit: 10
})
if (result && result.length > 0) {
alerts.value = result
}
} catch (error) {
console.error('加载预警信息失败:', error)
}
}
// 自动刷新
function startAutoRefresh() {
if (refreshTimer) {
clearInterval(refreshTimer)
}
refreshTimer = setInterval(() => {
loadAnalyticsData()
}, 5 * 60 * 1000) // 每5分钟刷新一次
}
function stopAutoRefresh() {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
}
// 生命周期
onMounted(async () => {
await loadAnalyticsData()
if (settings.value.autoRefresh) {
startAutoRefresh()
}
})
onUnmounted(() => {
stopAutoRefresh()
})
</script>