1239 lines
24 KiB
Plaintext
1239 lines
24 KiB
Plaintext
<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>
|