1254 lines
30 KiB
Plaintext
1254 lines
30 KiB
Plaintext
<!-- 健康监测管理 - uts-android 兼容重构版 -->
|
||
<template>
|
||
<view class="health-monitoring">
|
||
<!-- Header -->
|
||
<view class="header">
|
||
<text class="header-title">健康监测</text>
|
||
<view class="header-actions">
|
||
<button class="action-btn" @click="showAddVitalSigns">
|
||
<text class="btn-text">➕ 记录体征</text>
|
||
</button>
|
||
<button class="action-btn" @click="showBulkEntry">
|
||
<text class="btn-text">📊 批量录入</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Stats Cards -->
|
||
<view class="stats-section-flex">
|
||
<view class="stat-card">
|
||
<view class="stat-icon">❤️</view>
|
||
<view class="stat-content">
|
||
<text class="stat-number">{{ stats.total_records_today }}</text>
|
||
<text class="stat-label">今日记录</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-card stat-card-alert">
|
||
<view class="stat-icon">⚠️</view>
|
||
<view class="stat-content">
|
||
<text class="stat-number">{{ stats.abnormal_readings }}</text>
|
||
<text class="stat-label">异常读数</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-card">
|
||
<view class="stat-icon">📈</view>
|
||
<view class="stat-content">
|
||
<text class="stat-number">{{ stats.pending_reviews }}</text>
|
||
<text class="stat-label">待审核</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-card stat-card-urgent">
|
||
<view class="stat-icon">🚨</view>
|
||
<view class="stat-content">
|
||
<text class="stat-number">{{ stats.critical_alerts }}</text>
|
||
<text class="stat-label">危急值</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Filter Section -->
|
||
<view class="filters-section">
|
||
<view class="filter-row-flex">
|
||
<view class="filter-group">
|
||
<text class="filter-label">患者筛选</text>
|
||
<button class="picker-btn" @click="showElderActionSheet">
|
||
<text class="picker-text">{{ selectedElder?.name ?? '全部患者' }}</text>
|
||
</button>
|
||
</view>
|
||
<view class="filter-group">
|
||
<text class="filter-label">体征类型</text>
|
||
<button class="picker-btn" @click="showVitalTypeActionSheet">
|
||
<text class="picker-text">{{ selectedVitalType?.label ?? '全部类型' }}</text>
|
||
</button>
|
||
</view>
|
||
<view class="filter-group">
|
||
<text class="filter-label">时间范围</text>
|
||
<button class="picker-btn" @click="showTimeRangeActionSheet">
|
||
<text class="picker-text">{{ selectedTimeRange?.label ?? '今日' }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
<view class="filter-row-flex">
|
||
<view class="filter-toggles-flex">
|
||
<button
|
||
class="toggle-btn"
|
||
:class="showAbnormalOnly ? 'active' : ''"
|
||
@click="toggleAbnormalOnly"
|
||
>
|
||
<text class="toggle-text">仅异常</text>
|
||
</button>
|
||
<button
|
||
class="toggle-btn"
|
||
:class="showCriticalOnly ? 'active' : ''"
|
||
@click="toggleCriticalOnly"
|
||
>
|
||
<text class="toggle-text">仅危急</text>
|
||
</button>
|
||
</view>
|
||
<button class="refresh-btn" @click="refreshData">
|
||
<text class="refresh-text">刷新</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Vital Signs List -->
|
||
<view class="vital-signs-section">
|
||
<view class="section-header">
|
||
<text class="section-title">生命体征记录 ({{ filteredVitalSigns.length }})</text>
|
||
<view class="view-modes">
|
||
<button
|
||
class="mode-btn"
|
||
:class="{ active: viewMode === 'list' }"
|
||
@click="setViewMode('list')"
|
||
>
|
||
<text class="mode-text">📋</text>
|
||
</button>
|
||
<button
|
||
class="mode-btn"
|
||
:class="{ active: viewMode === 'chart' }"
|
||
@click="setViewMode('chart')"
|
||
>
|
||
<text class="mode-text">📊</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- List View -->
|
||
<scroll-view
|
||
v-if="viewMode === 'list'"
|
||
class="vital-signs-list"
|
||
direction="vertical"
|
||
:style="{ height: '500px' }"
|
||
>
|
||
<view
|
||
v-for="(vital, idx) in filteredVitalSigns"
|
||
:key="vital.id"
|
||
class="vital-sign-item"
|
||
:class="{
|
||
'abnormal': vital.is_abnormal,
|
||
'critical': isCriticalReading(vital)
|
||
}"
|
||
@click="viewVitalDetail(vital)"
|
||
>
|
||
<view class="vital-header">
|
||
<view class="patient-info">
|
||
<text class="patient-name">{{ vital.elder_name }}</text>
|
||
<text class="record-time">{{ formatDateTime(vital.recorded_at) }}</text>
|
||
</view>
|
||
<view class="vital-status">
|
||
<view v-if="vital.is_abnormal" class="status-badge abnormal">
|
||
<text class="badge-text">异常</text>
|
||
</view>
|
||
<view v-if="isCriticalReading(vital)" class="status-badge critical">
|
||
<text class="badge-text">危急</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="vital-measurements">
|
||
<view v-if="vital.blood_pressure" class="measurement-item" :style="{ 'margin-right': showMeasurementMargin(vital, 'blood_pressure') }">
|
||
<text class="measurement-label">血压</text>
|
||
<text class="measurement-value" :class="{ abnormal: isBloodPressureAbnormal(vital.blood_pressure) }">
|
||
{{ vital.blood_pressure }} mmHg
|
||
</text>
|
||
</view>
|
||
<view v-if="vital.heart_rate" class="measurement-item" :style="{ 'margin-right': showMeasurementMargin(vital, 'heart_rate') }">
|
||
<text class="measurement-label">心率</text>
|
||
<text class="measurement-value" :class="{ abnormal: isHeartRateAbnormal(vital.heart_rate) }">
|
||
{{ vital.heart_rate }} bpm
|
||
</text>
|
||
</view>
|
||
<view v-if="vital.temperature" class="measurement-item" :style="{ 'margin-right': showMeasurementMargin(vital, 'temperature') }">
|
||
<text class="measurement-label">体温</text>
|
||
<text class="measurement-value" :class="{ abnormal: isTemperatureAbnormal(vital.temperature) }">
|
||
{{ vital.temperature }}°C
|
||
</text>
|
||
</view>
|
||
<view v-if="vital.oxygen_saturation" class="measurement-item" :style="{ 'margin-right': showMeasurementMargin(vital, 'oxygen_saturation') }">
|
||
<text class="measurement-label">血氧</text>
|
||
<text class="measurement-value" :class="{ abnormal: isOxygenAbnormal(vital.oxygen_saturation) }">
|
||
{{ vital.oxygen_saturation }}%
|
||
</text>
|
||
</view>
|
||
<view v-if="vital.blood_sugar" class="measurement-item">
|
||
<text class="measurement-label">血糖</text>
|
||
<text class="measurement-value" :class="{ abnormal: isBloodSugarAbnormal(vital.blood_sugar) }">
|
||
{{ vital.blood_sugar }} mmol/L
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="vital-footer">
|
||
<text class="recorded-by">记录者: {{ vital.recorded_by }}</text>
|
||
<view class="vital-actions">
|
||
<button class="action-btn-small" @click.stop="editVital(vital)">
|
||
<text class="btn-text">编辑</text>
|
||
</button>
|
||
<button class="action-btn-small" @click.stop="addFollowUp(vital)">
|
||
<text class="btn-text">跟进</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="vital.notes" class="vital-notes">
|
||
<text class="notes-text">备注: {{ vital.notes }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="filteredVitalSigns.length === 0" class="empty-state">
|
||
<text class="empty-text">暂无生命体征记录</text>
|
||
<button class="add-btn" @click="showAddVitalSigns">
|
||
<text class="btn-text">添加记录</text>
|
||
</button>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- Chart View -->
|
||
<view v-if="viewMode === 'chart'" class="chart-section">
|
||
<view class="chart-filters">
|
||
<button
|
||
v-for="type in chartTypes"
|
||
:key="type.value"
|
||
class="chart-type-btn"
|
||
:class="{ active: selectedChartType === type.value }"
|
||
@click="setChartType(type.value)"
|
||
>
|
||
<text class="btn-text">{{ type.label }}</text>
|
||
</button>
|
||
</view>
|
||
<view class="chart-container">
|
||
<text class="chart-title">{{ getChartTitle() }}</text>
|
||
<!-- 这里应该放置图表组件 -->
|
||
<view class="chart-placeholder">
|
||
<text class="placeholder-text">图表视图开发中...</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Health Alerts -->
|
||
<view class="alerts-section" v-if="healthAlerts.length > 0">
|
||
<view class="section-header">
|
||
<text class="section-title">健康提醒</text>
|
||
<button class="view-all-btn" @click="showAllAlerts">
|
||
<text class="btn-text">查看全部</text>
|
||
</button>
|
||
</view>
|
||
<scroll-view class="alerts-list" scroll-y="true" :style="{ height: '200px' }">
|
||
<view
|
||
v-for="alert in healthAlerts"
|
||
:key="alert.id"
|
||
class="alert-item"
|
||
:class="alert.severity"
|
||
@click="handleAlert(alert)"
|
||
>
|
||
<view class="alert-header">
|
||
<text class="alert-title">{{ alert.title }}</text>
|
||
<text class="alert-time">{{ formatTime(alert.created_at) }}</text>
|
||
</view>
|
||
<view class="alert-content">
|
||
<text class="alert-description">{{ alert.description }}</text>
|
||
<text class="alert-patient">患者: {{ alert.elder_name }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
|
||
import {
|
||
formatTime,
|
||
formatDate,
|
||
formatDateTime,
|
||
getTodayStart,
|
||
getTodayEnd,
|
||
getRecentDate,
|
||
getDaysAgo
|
||
} from '../types_new.uts'
|
||
|
||
// 数据类型定义
|
||
type HealthStats = {
|
||
total_records_today: number
|
||
abnormal_readings: number
|
||
pending_reviews: number
|
||
critical_alerts: number
|
||
}
|
||
|
||
type VitalSign = {
|
||
id: string
|
||
elder_id: string
|
||
elder_name: string
|
||
blood_pressure: string
|
||
heart_rate: number
|
||
temperature: number
|
||
blood_sugar: number
|
||
weight: number
|
||
height: number
|
||
oxygen_saturation: number
|
||
recorded_by: string
|
||
recorded_at: string
|
||
notes: string
|
||
is_abnormal: boolean
|
||
}
|
||
|
||
type HealthAlert = {
|
||
id: string
|
||
elder_id: string
|
||
elder_name: string
|
||
title: string
|
||
description: string
|
||
severity: string
|
||
alert_type: string
|
||
status: string
|
||
created_at: string
|
||
}
|
||
|
||
type Elder = {
|
||
id: string
|
||
name: string
|
||
room_number: string
|
||
bed_number: string
|
||
}
|
||
|
||
type FilterOption = {
|
||
value: string
|
||
label: string
|
||
}
|
||
|
||
// 响应式数据
|
||
const stats = ref<HealthStats>({
|
||
total_records_today: 0,
|
||
abnormal_readings: 0,
|
||
pending_reviews: 0,
|
||
critical_alerts: 0
|
||
})
|
||
|
||
const vitalSigns = ref<VitalSign[]>([])
|
||
const healthAlerts = ref<HealthAlert[]>([])
|
||
const elders = ref<Elder[]>([])
|
||
|
||
const selectedElderIndex = ref<number>(-1)
|
||
const selectedVitalTypeIndex = ref<number>(-1)
|
||
const selectedTimeRangeIndex = ref<number>(0)
|
||
const showAbnormalOnly = ref<boolean>(false)
|
||
const showCriticalOnly = ref<boolean>(false)
|
||
const viewMode = ref<string>('list')
|
||
const selectedChartType = ref<string>('blood_pressure')
|
||
|
||
// 筛选选项
|
||
const vitalTypeOptions = ref<FilterOption[]>([
|
||
{ value: 'all', label: '全部类型' },
|
||
{ value: 'blood_pressure', label: '血压' },
|
||
{ value: 'heart_rate', label: '心率' },
|
||
{ value: 'temperature', label: '体温' },
|
||
{ value: 'blood_sugar', label: '血糖' },
|
||
{ value: 'oxygen_saturation', label: '血氧' },
|
||
{ value: 'weight', label: '体重' }
|
||
])
|
||
|
||
const timeRangeOptions = ref<FilterOption[]>([
|
||
{ value: 'today', label: '今日' },
|
||
{ value: '3days', label: '近3天' },
|
||
{ value: '7days', label: '近7天' },
|
||
{ value: '30days', label: '近30天' }
|
||
])
|
||
|
||
const chartTypes = ref<FilterOption[]>([
|
||
{ value: 'blood_pressure', label: '血压趋势' },
|
||
{ value: 'heart_rate', label: '心率趋势' },
|
||
{ value: 'temperature', label: '体温趋势' },
|
||
{ value: 'blood_sugar', label: '血糖趋势' },
|
||
{ value: 'oxygen_saturation', label: '血氧趋势' }
|
||
])
|
||
|
||
// 计算属性
|
||
const elderOptions = computed<Elder[]>(() => {
|
||
return [{ id: 'all', name: '全部患者', room_number: '', bed_number: '' } as Elder, ...elders.value]
|
||
})
|
||
|
||
const selectedElder = computed<Elder | null>(() => {
|
||
if (selectedElderIndex.value < 0 || selectedElderIndex.value >= elderOptions.value.length) {
|
||
return null
|
||
}
|
||
return elderOptions.value[selectedElderIndex.value]
|
||
})
|
||
|
||
const selectedVitalType = computed<FilterOption | null>(() => {
|
||
if (selectedVitalTypeIndex.value < 0 || selectedVitalTypeIndex.value >= vitalTypeOptions.value.length) {
|
||
return null
|
||
}
|
||
return vitalTypeOptions.value[selectedVitalTypeIndex.value]
|
||
})
|
||
|
||
const selectedTimeRange = computed<FilterOption | null>(() => {
|
||
if (selectedTimeRangeIndex.value < 0 || selectedTimeRangeIndex.value >= timeRangeOptions.value.length) {
|
||
return null
|
||
}
|
||
return timeRangeOptions.value[selectedTimeRangeIndex.value]
|
||
})
|
||
|
||
const filteredVitalSigns = computed<VitalSign[]>(() => {
|
||
let filtered = vitalSigns.value
|
||
|
||
// 按患者筛选
|
||
if (selectedElder.value && selectedElder.value.id !== 'all') {
|
||
filtered = filtered.filter(vital => vital.elder_id === selectedElder.value!.id)
|
||
}
|
||
|
||
// 按体征类型筛选
|
||
if (selectedVitalType.value && selectedVitalType.value.value !== 'all') {
|
||
const type = selectedVitalType.value.value
|
||
filtered = filtered.filter(vital => {
|
||
switch (type) {
|
||
case 'blood_pressure': return vital.blood_pressure !== ''
|
||
case 'heart_rate': return vital.heart_rate > 0
|
||
case 'temperature': return vital.temperature > 0
|
||
case 'blood_sugar': return vital.blood_sugar > 0
|
||
case 'oxygen_saturation': return vital.oxygen_saturation > 0
|
||
case 'weight': return vital.weight > 0
|
||
default: return true
|
||
}
|
||
})
|
||
}
|
||
|
||
// 按时间范围筛选
|
||
if (selectedTimeRange.value) {
|
||
const now = new Date()
|
||
let startDate: string
|
||
switch (selectedTimeRange.value.value) {
|
||
case 'today':
|
||
startDate = getTodayStart()
|
||
break
|
||
case '3days':
|
||
startDate = getDaysAgo(3)
|
||
break
|
||
case '7days':
|
||
startDate = getDaysAgo(7)
|
||
break
|
||
case '30days':
|
||
startDate = getDaysAgo(30)
|
||
break
|
||
default:
|
||
startDate = getTodayStart()
|
||
}
|
||
filtered = filtered.filter(vital => vital.recorded_at >= startDate)
|
||
}
|
||
|
||
// 仅显示异常
|
||
if (showAbnormalOnly.value) {
|
||
filtered = filtered.filter(vital => vital.is_abnormal)
|
||
}
|
||
|
||
// 仅显示危急
|
||
if (showCriticalOnly.value) {
|
||
filtered = filtered.filter(vital => isCriticalReading(vital))
|
||
}
|
||
|
||
return filtered.sort((a, b) => new Date(b.recorded_at).getTime() - new Date(a.recorded_at).getTime())
|
||
})
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
loadData()
|
||
})
|
||
|
||
// 加载数据
|
||
const loadData = async () => {
|
||
await Promise.all([
|
||
loadStats(),
|
||
loadVitalSigns(),
|
||
loadHealthAlerts(),
|
||
loadElders()
|
||
])
|
||
}
|
||
|
||
// 加载统计数据
|
||
const loadStats = async () => {
|
||
try {
|
||
// 今日记录总数
|
||
const todayResult = await supa
|
||
.from('ec_vital_signs')
|
||
.select('*', { count: 'exact' })
|
||
.gte('recorded_at', getTodayStart())
|
||
.lte('recorded_at', getTodayEnd())
|
||
.executeAs<VitalSign[]>()
|
||
|
||
// 异常读数
|
||
const abnormalResult = await supa
|
||
.from('ec_vital_signs')
|
||
.select('*', { count: 'exact' })
|
||
.eq('is_abnormal', true)
|
||
.gte('recorded_at', getDaysAgo(7))
|
||
.executeAs<VitalSign[]>()
|
||
|
||
// 待审核记录
|
||
const pendingResult = await supa
|
||
.from('ec_vital_signs')
|
||
.select('*', { count: 'exact' })
|
||
.eq('status', 'pending_review')
|
||
.executeAs<VitalSign[]>()
|
||
|
||
// 危急值提醒
|
||
const criticalResult = await supa
|
||
.from('ec_health_alerts')
|
||
.select('*', { count: 'exact' })
|
||
.eq('severity', 'critical')
|
||
.eq('status', 'active')
|
||
.executeAs<HealthAlert[]>()
|
||
|
||
stats.value = {
|
||
total_records_today: todayResult.count ?? 0,
|
||
abnormal_readings: abnormalResult.count ?? 0,
|
||
pending_reviews: pendingResult.count ?? 0,
|
||
critical_alerts: criticalResult.count ?? 0
|
||
}
|
||
} catch (error) {
|
||
console.error('加载统计数据失败:', error)
|
||
}
|
||
}
|
||
|
||
// 加载生命体征记录
|
||
const loadVitalSigns = async () => {
|
||
try {
|
||
const result = await supa
|
||
.from('ec_vital_signs')
|
||
.select(`
|
||
id,
|
||
elder_id,
|
||
elder_name,
|
||
blood_pressure,
|
||
heart_rate,
|
||
temperature,
|
||
blood_sugar,
|
||
weight,
|
||
height,
|
||
oxygen_saturation,
|
||
recorded_by,
|
||
recorded_at,
|
||
notes,
|
||
is_abnormal
|
||
`)
|
||
.gte('recorded_at', getDaysAgo(30))
|
||
.order('recorded_at', { ascending: false })
|
||
.limit(200)
|
||
.executeAs<VitalSign[]>()
|
||
|
||
if (result.error == null && result.data != null) {
|
||
vitalSigns.value = result.data
|
||
}
|
||
} catch (error) {
|
||
console.error('加载生命体征记录失败:', error)
|
||
}
|
||
}
|
||
|
||
// 加载健康提醒
|
||
const loadHealthAlerts = async () => {
|
||
try {
|
||
const result = await supa
|
||
.from('ec_health_alerts')
|
||
.select(`
|
||
id,
|
||
elder_id,
|
||
elder_name,
|
||
title,
|
||
description,
|
||
severity,
|
||
alert_type,
|
||
status,
|
||
created_at
|
||
`)
|
||
.in('severity', ['high', 'critical'])
|
||
.eq('status', 'active')
|
||
.order('created_at', { ascending: false })
|
||
.limit(10)
|
||
.executeAs<HealthAlert[]>()
|
||
|
||
if (result.error == null && result.data != null) {
|
||
healthAlerts.value = result.data
|
||
}
|
||
} catch (error) {
|
||
console.error('加载健康提醒失败:', error)
|
||
}
|
||
}
|
||
|
||
// 加载患者列表(采用 supa 查询风格,含 count,类型安全、判空、默认值健壮)
|
||
const loadElders = async () => {
|
||
try {
|
||
const eldersResult = await supa
|
||
.from('ec_elders')
|
||
.select('*', { count: 'exact' })
|
||
.eq('status', 'active')
|
||
.order('room_number', { ascending: true })
|
||
.executeAs<Elder>()
|
||
|
||
if (eldersResult.error == null && eldersResult.data != null) {
|
||
// 兼容单条/多条数据返回
|
||
if (Array.isArray(eldersResult.data)) {
|
||
elders.value = eldersResult.data
|
||
} else if (eldersResult.data) {
|
||
elders.value = [eldersResult.data]
|
||
} else {
|
||
elders.value = []
|
||
}
|
||
} else {
|
||
elders.value = []
|
||
}
|
||
} catch (error) {
|
||
elders.value = []
|
||
console.error('加载患者列表失败:', error)
|
||
}
|
||
}
|
||
|
||
// 辅助函数
|
||
const isCriticalReading = (vital: VitalSign): boolean => {
|
||
// 定义危急值标准
|
||
if (vital.blood_pressure) {
|
||
const [systolic, diastolic] = vital.blood_pressure.split('/').map(Number)
|
||
if (systolic >= 180 || systolic <= 90 || diastolic >= 110 || diastolic <= 60) {
|
||
return true
|
||
}
|
||
}
|
||
if (vital.heart_rate && (vital.heart_rate >= 120 || vital.heart_rate <= 50)) {
|
||
return true
|
||
}
|
||
if (vital.temperature && (vital.temperature >= 39.0 || vital.temperature <= 35.0)) {
|
||
return true
|
||
}
|
||
if (vital.oxygen_saturation && vital.oxygen_saturation <= 90) {
|
||
return true
|
||
}
|
||
if (vital.blood_sugar && (vital.blood_sugar >= 13.9 || vital.blood_sugar <= 3.9)) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
const isBloodPressureAbnormal = (bp: string): boolean => {
|
||
if (!bp) return false
|
||
const [systolic, diastolic] = bp.split('/').map(Number)
|
||
return systolic >= 140 || systolic <= 90 || diastolic >= 90 || diastolic <= 60
|
||
}
|
||
|
||
const isHeartRateAbnormal = (hr: number): boolean => {
|
||
return hr >= 100 || hr <= 60
|
||
}
|
||
|
||
const isTemperatureAbnormal = (temp: number): boolean => {
|
||
return temp >= 37.3 || temp <= 36.0
|
||
}
|
||
|
||
const isOxygenAbnormal = (spo2: number): boolean => {
|
||
return spo2 <= 95
|
||
}
|
||
|
||
const isBloodSugarAbnormal = (bs: number): boolean => {
|
||
return bs >= 11.1 || bs <= 4.0
|
||
}
|
||
|
||
const getChartTitle = (): string => {
|
||
const type = chartTypes.value.find(t => t.value === selectedChartType.value)
|
||
const patient = selectedElder.value && selectedElder.value.id !== 'all' ? selectedElder.value.name + ' - ' : ''
|
||
return patient + (type?.label ?? '趋势图')
|
||
}
|
||
|
||
// 事件处理
|
||
const refreshData = () => {
|
||
loadData()
|
||
uni.showToast({ title: '数据已刷新', icon: 'success' })
|
||
}
|
||
|
||
const onElderChange = (e: any) => {
|
||
selectedElderIndex.value = e.detail.value
|
||
}
|
||
|
||
const onVitalTypeChange = (e: any) => {
|
||
selectedVitalTypeIndex.value = e.detail.value
|
||
}
|
||
|
||
const onTimeRangeChange = (e: any) => {
|
||
selectedTimeRangeIndex.value = e.detail.value
|
||
}
|
||
|
||
const toggleAbnormalOnly = () => {
|
||
showAbnormalOnly.value = !showAbnormalOnly.value
|
||
if (showAbnormalOnly.value) {
|
||
showCriticalOnly.value = false
|
||
}
|
||
}
|
||
|
||
const toggleCriticalOnly = () => {
|
||
showCriticalOnly.value = !showCriticalOnly.value
|
||
if (showCriticalOnly.value) {
|
||
showAbnormalOnly.value = false
|
||
}
|
||
}
|
||
|
||
const setViewMode = (mode: string) => {
|
||
viewMode.value = mode
|
||
}
|
||
|
||
const setChartType = (type: string) => {
|
||
selectedChartType.value = type
|
||
}
|
||
|
||
const showAddVitalSigns = () => {
|
||
uni.navigateTo({ url: '/pages/ec/nurse/vital-signs-entry' })
|
||
}
|
||
|
||
const showBulkEntry = () => {
|
||
uni.navigateTo({ url: '/pages/ec/nurse/bulk-entry' })
|
||
}
|
||
|
||
const showAllAlerts = () => {
|
||
uni.navigateTo({ url: '/pages/ec/health/alerts' })
|
||
}
|
||
|
||
const viewVitalDetail = (vital: VitalSign) => {
|
||
uni.navigateTo({
|
||
url: `/pages/ec/nurse/vital-detail?id=${vital.id}`
|
||
})
|
||
}
|
||
|
||
const editVital = (vital: VitalSign) => {
|
||
uni.navigateTo({
|
||
url: `/pages/ec/nurse/vital-signs-entry?id=${vital.id}`
|
||
})
|
||
}
|
||
|
||
const addFollowUp = (vital: VitalSign) => {
|
||
uni.navigateTo({
|
||
url: `/pages/ec/health/follow-up?vitalId=${vital.id}`
|
||
})
|
||
}
|
||
|
||
const handleAlert = (alert: HealthAlert) => {
|
||
uni.navigateTo({
|
||
url: `/pages/ec/health/alert-detail?id=${alert.id}`
|
||
})
|
||
}
|
||
|
||
const showElderActionSheet = () => {
|
||
const options = elderOptions.value.map(e => e.name)
|
||
uni.showActionSheet({
|
||
itemList: options,
|
||
success: (res:any) => {
|
||
selectedElderIndex.value = res.tapIndex
|
||
}
|
||
})
|
||
}
|
||
const showVitalTypeActionSheet = () => {
|
||
const options = vitalTypeOptions.value.map(v => v.label)
|
||
uni.showActionSheet({
|
||
itemList: options,
|
||
success: (res:any) => {
|
||
selectedVitalTypeIndex.value = res.tapIndex
|
||
}
|
||
})
|
||
}
|
||
const showTimeRangeActionSheet = () => {
|
||
const options = timeRangeOptions.value.map(t => t.label)
|
||
uni.showActionSheet({
|
||
itemList: options,
|
||
success: (res:any) => {
|
||
selectedTimeRangeIndex.value = res.tapIndex
|
||
}
|
||
})
|
||
}
|
||
|
||
// 兼容 margin 间距(替代 gap/伪类)
|
||
const showMeasurementMargin = (vital: VitalSign, type: string): string => {
|
||
// 只给前几个加 margin-right,最后一个不加
|
||
let types = [] as string[]
|
||
if (vital.blood_pressure) types.push('blood_pressure')
|
||
if (vital.heart_rate) types.push('heart_rate')
|
||
if (vital.temperature) types.push('temperature')
|
||
if (vital.oxygen_saturation) types.push('oxygen_saturation')
|
||
if (vital.blood_sugar) types.push('blood_sugar')
|
||
const idx = types.indexOf(type)
|
||
return idx !== -1 && idx < types.length - 1 ? '15px' : '0'
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.health-monitoring {
|
||
padding: 20px;
|
||
background-color: #f5f5f5;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 30px;
|
||
padding: 20px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border-radius: 15px;
|
||
color: white;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
}
|
||
|
||
.action-btn {
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
color: white;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.action-btn:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.btn-text {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.stats-section-flex {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.stat-card {
|
||
flex: 1;
|
||
min-width: 120px;
|
||
padding: 20px;
|
||
background-color: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
margin-right: 15px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.stat-card:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.stat-card-alert {
|
||
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
|
||
color: white;
|
||
}
|
||
|
||
.stat-card-urgent {
|
||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
||
color: white;
|
||
}
|
||
|
||
.stat-icon {
|
||
font-size: 24px;
|
||
width: 40px;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: rgba(0, 0, 0, 0.1);
|
||
border-radius: 50%;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.stat-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
display: block;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
opacity: 0.7;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.filters-section {
|
||
background-color: white;
|
||
border-radius: 12px;
|
||
padding: 12px 10px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.filter-row-flex {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.filter-row-flex:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.filter-group {
|
||
flex: 1;
|
||
width: 120px;
|
||
min-width: 90px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.filter-group:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.filter-label {
|
||
font-size: 13px;
|
||
color: #666;
|
||
margin-bottom: 2px;
|
||
display: block;
|
||
}
|
||
|
||
.picker-btn {
|
||
background: none;
|
||
border: none;
|
||
padding: 0;
|
||
text-align: left;
|
||
}
|
||
|
||
.picker-text {
|
||
font-size: 13px;
|
||
color: #333;
|
||
padding: 6px 8px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
background-color: #f9f9f9;
|
||
display: block;
|
||
}
|
||
|
||
.filter-toggles-flex {
|
||
display: flex;
|
||
flex-direction: row;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.toggle-btn {
|
||
padding: 6px 12px;
|
||
border-radius: 12px;
|
||
border: 1px solid #ddd;
|
||
background-color: #f9f9f9;
|
||
color: #666;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.toggle-btn:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.toggle-btn.active {
|
||
background: #667eea;
|
||
color: white;
|
||
border: 1px solid #667eea;
|
||
}
|
||
|
||
.refresh-btn {
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
border: 1px solid #52c41a;
|
||
background-color: #52c41a;
|
||
color: white;
|
||
}
|
||
|
||
.refresh-text {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.vital-signs-section, .alerts-section {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.view-modes {
|
||
display: flex;
|
||
flex-direction: row;
|
||
}
|
||
|
||
.mode-btn {
|
||
padding: 6px 12px;
|
||
border-radius: 12px;
|
||
border: 1px solid #ddd;
|
||
background-color: #f9f9f9;
|
||
color: #666;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.mode-btn:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.mode-btn.active {
|
||
background: #667eea;
|
||
color: white;
|
||
border: 1px solid #667eea;
|
||
}
|
||
|
||
.view-all-btn {
|
||
padding: 6px 12px;
|
||
border-radius: 15px;
|
||
border: 1px solid #ddd;
|
||
background-color: white;
|
||
color: #666;
|
||
}
|
||
|
||
.vital-signs-list, .alerts-list {
|
||
background-color: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.vital-sign-item {
|
||
padding: 20px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.vital-sign-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.abnormal {
|
||
background-color: #fff7e6;
|
||
border-left: 4px solid #faad14;
|
||
}
|
||
|
||
.critical {
|
||
background-color: #fff2f0;
|
||
border-left: 4px solid #ff4d4f;
|
||
}
|
||
|
||
.vital-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.patient-info {
|
||
.patient-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
display: block;
|
||
}
|
||
|
||
.record-time {
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin-top: 5px;
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.vital-status {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 4px 8px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.abnormal .badge-text {
|
||
color: #faad14;
|
||
}
|
||
|
||
.critical .badge-text {
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
.vital-measurements {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.vital-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.recorded-by {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.vital-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.action-btn-small {
|
||
padding: 4px 8px;
|
||
border-radius: 12px;
|
||
border: 1px solid #ddd;
|
||
background-color: white;
|
||
color: #666;
|
||
}
|
||
|
||
.notes-text {
|
||
font-size: 14px;
|
||
color: #666;
|
||
font-style: italic;
|
||
}
|
||
|
||
.chart-section {
|
||
background-color: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.chart-filters {
|
||
display: flex;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.chart-type-btn {
|
||
padding: 6px 12px;
|
||
border-radius: 12px;
|
||
border: 1px solid #ddd;
|
||
background-color: #f9f9f9;
|
||
color: #666;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.chart-type-btn:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.chart-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 20px;
|
||
display: block;
|
||
}
|
||
|
||
.chart-placeholder {
|
||
height: 300px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #f9f9f9;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.placeholder-text {
|
||
color: #999;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.alert-item {
|
||
padding: 15px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
border-left: 4px solid #ddd;
|
||
}
|
||
|
||
.alert-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.high {
|
||
border-left-color: #faad14;
|
||
background-color: #fff7e6;
|
||
}
|
||
|
||
.critical {
|
||
border-left-color: #ff4d4f;
|
||
background-color: #fff2f0;
|
||
}
|
||
|
||
.alert-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.alert-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.alert-time {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.alert-content {
|
||
.alert-description {
|
||
font-size: 14px;
|
||
color: #555;
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.alert-patient {
|
||
font-size: 12px;
|
||
color: #888;
|
||
}
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 60px 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 16px;
|
||
color: #999;
|
||
margin-bottom: 20px;
|
||
display: block;
|
||
}
|
||
|
||
.add-btn {
|
||
padding: 12px 24px;
|
||
border-radius: 20px;
|
||
border: none;
|
||
background-color: #1890ff;
|
||
color: white;
|
||
}
|
||
|
||
.btn-text {
|
||
font-size: 14px;
|
||
color: white;
|
||
}
|
||
</style>
|