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

933 lines
23 KiB
Plaintext
Raw 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.
<!--
UTS-Android 兼容性开发规范(重要,所有开发成员须遵循)
- 表单优先用form变量声明用let/const不能用var。
- 跟template交互的变量尽量用一维变量。
- 不用foreach/map/safeget只用for和UTSJSONObject。
- 数组类型用Array<Type>,不用简写[]。
- 不用interface只用type。
- 判断空用 !== null不用!。
- 不支持undefined变量为null时需判空。
- 逻辑或用??(空值合并),不用||。
- for循环i需指定Int类型for (let i:Int = 0; ...)
- 不支持Intersection Type、Index Signature。
- picker用picker-view或uni.showActionSheet。
- scroll-view用direction="vertical"。
- CSS只用display:flex; 不用gap、grid、calc()、伪类、vh等。
- 复杂数据交互用utils/utis下的UTSJSONObject。
- 时间选择用uni_modules/lime-date-time-picker。
-->
<template>
<view class="doctor-dashboard">
<!-- Header -->
<view class="header">
<text class="header-title">医生工作台</text>
<text class="header-subtitle">{{ currentTime }}</text>
<view class="header-actions">
<button class="action-btn emergency" @click="showEmergencyPage">
<text class="btn-text">🚨 急诊</text>
</button>
<button class="action-btn" @click="showNewConsultation">
<text class="btn-text"> 新建诊疗</text>
</button>
</view>
</view>
<!-- Stats Cards -->
<view class="stats-section">
<view class="stat-card">
<view class="stat-icon">👥</view>
<view class="stat-content">
<text class="stat-number">{{ stats.today_patients }}</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_consultations }}</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.prescriptions_today }}</text>
<text class="stat-label">今日处方</text>
</view>
</view>
<view class="stat-card urgent">
<view class="stat-icon">🚨</view>
<view class="stat-content">
<text class="stat-number">{{ stats.urgent_cases }}</text>
<text class="stat-label">紧急病例</text>
</view>
</view>
</view>
<!-- Quick Actions -->
<view class="quick-actions">
<text class="section-title">快速操作</text>
<view class="actions-grid">
<button class="quick-action-btn" @click="showPatientQueue">
<text class="action-icon">👥</text>
<text class="action-text">患者队列</text>
</button>
<button class="quick-action-btn" @click="showMedicalRecords">
<text class="action-icon">📋</text>
<text class="action-text">病历管理</text>
</button>
<button class="quick-action-btn" @click="showPrescriptions">
<text class="action-icon">💊</text>
<text class="action-text">处方管理</text>
</button>
<button class="quick-action-btn" @click="showHealthReports">
<text class="action-icon">📊</text>
<text class="action-text">健康报告</text>
</button>
<button class="quick-action-btn" @click="showVitalSigns">
<text class="action-icon">❤️</text>
<text class="action-text">生命体征</text>
</button>
<button class="quick-action-btn" @click="showMedicationManagement">
<text class="action-icon">💉</text>
<text class="action-text">用药管理</text>
</button>
</view>
</view>
<!-- Today's Schedule -->
<view class="schedule-section">
<view class="section-header">
<text class="section-title">今日日程 ({{ todaySchedule.length }})</text>
<button class="view-all-btn" @click="showFullSchedule">
<text class="btn-text">查看全部</text>
</button>
</view>
<scroll-view class="schedule-list" scroll-y="true" :style="{ height: '400px' }">
<view
v-for="appointment in todaySchedule"
:key="appointment.id"
class="schedule-item"
:class="{
'current': isCurrentAppointment(appointment),
'urgent': appointment.priority === 'urgent',
'completed': appointment.status === 'completed'
}"
@click="openAppointment(appointment)"
>
<view class="appointment-time">
<text class="time-text">{{ formatTime(appointment.scheduled_time) }}</text>
<text class="status-text" :class="appointment.status">{{ getStatusText(appointment.status) }}</text>
</view>
<view class="appointment-details">
<text class="patient-name">{{ appointment.elder_name }}</text>
<text class="appointment-type">{{ getAppointmentTypeText(appointment.appointment_type) }}</text>
<text class="chief-complaint" v-if="appointment.chief_complaint">{{ appointment.chief_complaint }}</text>
<text class="room-info" v-if="appointment.room_number">房间: {{ appointment.room_number }}</text>
</view>
<view class="appointment-actions">
<button
v-if="appointment.status === 'scheduled'"
class="start-btn"
@click.stop="startConsultation(appointment)"
>
<text class="btn-text">开始诊疗</text>
</button>
<button
v-if="appointment.status === 'in_progress'"
class="continue-btn"
@click.stop="continueConsultation(appointment)"
>
<text class="btn-text">继续</text>
</button>
<button
v-if="appointment.status === 'completed'"
class="view-btn"
@click.stop="viewConsultation(appointment)"
>
<text class="btn-text">查看</text>
</button>
</view>
</view>
<view v-if="todaySchedule.length === 0" class="empty-state">
<text class="empty-text">今日暂无安排</text>
</view>
</scroll-view>
</view>
<!-- Recent Consultations -->
<view class="recent-section">
<view class="section-header">
<text class="section-title">最近诊疗记录</text>
<button class="view-all-btn" @click="showAllConsultations">
<text class="btn-text">查看全部</text>
</button>
</view>
<scroll-view class="recent-list" scroll-y="true" :style="{ height: '300px' }">
<view
v-for="consultation in recentConsultations"
:key="consultation.id"
class="consultation-item"
@click="viewConsultationDetails(consultation)"
>
<view class="consultation-header">
<text class="patient-name">{{ consultation.elder_name }}</text>
<text class="consultation-date">{{ formatDateTime(consultation.scheduled_time) }}</text>
</view>
<view class="consultation-content">
<text class="diagnosis">诊断: {{ consultation.diagnosis || '未填写' }}</text>
<text class="treatment">治疗: {{ consultation.treatment || '未填写' }}</text>
</view>
<view class="consultation-meta">
<text class="consultation-type">{{ getConsultationTypeText(consultation.consultation_type) }}</text>
<text class="follow-up" v-if="consultation.follow_up_date">复诊: {{ formatDate(consultation.follow_up_date) }}</text>
</view>
</view>
<view v-if="recentConsultations.length === 0" class="empty-state">
<text class="empty-text">暂无诊疗记录</text>
</view>
</scroll-view>
</view>
<!-- Urgent Alerts -->
<view class="alerts-section" v-if="urgentAlerts.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">
<view
v-for="alert in urgentAlerts"
: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, onMounted, onUnmounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import {
formatTime,
formatDate,
formatDateTime,
getCurrentTimeString,
getTodayStart,
getTodayEnd,
getRecentDate,
getSeverityText
} from '../types_new.uts'
// 数据类型定义
type DoctorStats = {
today_patients: number
pending_consultations: number
prescriptions_today: number
urgent_cases: number
}
type Appointment = {
id: string
elder_id: string
elder_name: string
doctor_id: string
scheduled_time: string
appointment_type: string
chief_complaint: string
status: string
priority: string
room_number: string
estimated_duration: number
created_at: string
}
type Consultation = {
id: string
elder_id: string
elder_name: string
doctor_id: string
scheduled_time: string
consultation_type: string
chief_complaint: string
diagnosis: string
treatment: string
prescription: string
follow_up_date: string
notes: string
created_at: string
}
type HealthAlert = {
id: string
elder_id: string
elder_name: string
title: string
description: string
severity: string
alert_type: string
status: string
created_at: string
}
// 响应式数据
const currentTime = ref<string>('')
const stats = ref<DoctorStats>({
today_patients: 0,
pending_consultations: 0,
prescriptions_today: 0,
urgent_cases: 0
})
const todaySchedule = ref<Appointment[]>([])
const recentConsultations = ref<Consultation[]>([])
const urgentAlerts = ref<HealthAlert[]>([])
let timeInterval: number = 0
// 生命周期
onMounted(() => {
loadData()
updateCurrentTime()
timeInterval = setInterval(updateCurrentTime, 60000) // 每分钟更新时间
})
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
})
// 更新当前时间
const updateCurrentTime = () => {
currentTime.value = getCurrentTimeString()
}
// 加载数据
const loadData = async () => {
await Promise.all([
loadStats(),
loadTodaySchedule(),
loadRecentConsultations(),
loadUrgentAlerts()
])
}
// 加载统计数据
const loadStats = async () => {
try {
// 今日患者数
const patientsResult = await supa
.from('ec_appointments')
.select('*', { count: 'exact' })
.gte('scheduled_time', getTodayStart())
.lte('scheduled_time', getTodayEnd())
.executeAs<Appointment[]>()
// 待诊疗数量
const pendingResult = await supa
.from('ec_appointments')
.select('*', { count: 'exact' })
.in('status', ['scheduled', 'in_progress'])
.executeAs<Appointment[]>()
// 今日处方数量
const prescriptionsResult = await supa
.from('ec_medications')
.select('*', { count: 'exact' })
.gte('created_at', getTodayStart())
.lte('created_at', getTodayEnd())
.executeAs<any[]>()
// 紧急病例数量
const urgentResult = await supa
.from('ec_health_alerts')
.select('*', { count: 'exact' })
.in('severity', ['high', 'critical'])
.eq('status', 'active')
.executeAs<HealthAlert[]>()
stats.value = {
today_patients: patientsResult.count ?? 0,
pending_consultations: pendingResult.count ?? 0,
prescriptions_today: prescriptionsResult.count ?? 0,
urgent_cases: urgentResult.count ?? 0
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
// 加载今日日程
const loadTodaySchedule = async () => {
try {
const result = await supa
.from('ec_appointments')
.select(`
id,
elder_id,
elder_name,
doctor_id,
scheduled_time,
appointment_type,
chief_complaint,
status,
priority,
room_number,
estimated_duration,
created_at
`)
.gte('scheduled_time', getTodayStart())
.lte('scheduled_time', getTodayEnd())
.order('scheduled_time', { ascending: true })
.executeAs<Appointment[]>()
if (result.error == null && result.data != null) {
todaySchedule.value = result.data
}
} catch (error) {
console.error('加载今日日程失败:', error)
}
}
// 加载最近诊疗记录
const loadRecentConsultations = async () => {
try {
const result = await supa
.from('ec_consultations')
.select(`
id,
elder_id,
elder_name,
doctor_id,
scheduled_time,
consultation_type,
chief_complaint,
diagnosis,
treatment,
prescription,
follow_up_date,
notes,
created_at
`)
.gte('scheduled_time', getRecentDate(7))
.order('scheduled_time', { ascending: false })
.limit(10)
.executeAs<Consultation[]>()
if (result.error == null && result.data != null) {
recentConsultations.value = result.data
}
} catch (error) {
console.error('加载最近诊疗记录失败:', error)
}
}
// 加载紧急提醒
const loadUrgentAlerts = 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(5)
.executeAs<HealthAlert[]>()
if (result.error == null && result.data != null) {
urgentAlerts.value = result.data
}
} catch (error) {
console.error('加载紧急提醒失败:', error)
}
}
// 辅助函数
const getStatusText = (status: string): string => {
const statusMap = new Map([
['scheduled', '已预约'],
['in_progress', '进行中'],
['completed', '已完成'],
['cancelled', '已取消'],
['no_show', '未到']
])
return statusMap.get(status) ?? status
}
const getAppointmentTypeText = (type: string): string => {
const typeMap = new Map([
['routine', '常规检查'],
['follow_up', '复诊'],
['emergency', '急诊'],
['consultation', '会诊'],
['physical', '体检']
])
return typeMap.get(type) ?? type
}
const getConsultationTypeText = (type: string): string => {
const typeMap = new Map([
['initial', '初诊'],
['follow_up', '复诊'],
['emergency', '急诊'],
['consultation', '会诊'],
['phone', '电话咨询']
])
return typeMap.get(type) ?? type
}
const isCurrentAppointment = (appointment: Appointment): boolean => {
const now = new Date()
const appointmentTime = new Date(appointment.scheduled_time)
const diff = Math.abs(now.getTime() - appointmentTime.getTime())
return diff <= 30 * 60 * 1000 && appointment.status === 'in_progress' // 30分钟内
}
// 事件处理
const showEmergencyPage = () => {
uni.navigateTo({ url: '/pages/ec/doctor/emergency' })
}
const showNewConsultation = () => {
uni.navigateTo({ url: '/pages/ec/doctor/consultation-form' })
}
const showPatientQueue = () => {
uni.navigateTo({ url: '/pages/ec/doctor/patient-queue' })
}
const showMedicalRecords = () => {
uni.navigateTo({ url: '/pages/ec/doctor/medical-records' })
}
const showPrescriptions = () => {
uni.navigateTo({ url: '/pages/ec/doctor/prescriptions' })
}
const showHealthReports = () => {
uni.navigateTo({ url: '/pages/ec/doctor/health-reports' })
}
const showVitalSigns = () => {
uni.navigateTo({ url: '/pages/ec/doctor/vital-signs' })
}
const showMedicationManagement = () => {
uni.navigateTo({ url: '/pages/ec/doctor/medication-management' })
}
const showFullSchedule = () => {
uni.navigateTo({ url: '/pages/ec/doctor/schedule' })
}
const showAllConsultations = () => {
uni.navigateTo({ url: '/pages/ec/doctor/consultations' })
}
const showAllAlerts = () => {
uni.navigateTo({ url: '/pages/ec/doctor/alerts' })
}
const openAppointment = (appointment: Appointment) => {
uni.navigateTo({
url: `/pages/ec/doctor/appointment-detail?id=${appointment.id}`
})
}
const startConsultation = async (appointment: Appointment) => {
try {
await supa
.from('ec_appointments')
.update({ status: 'in_progress' })
.eq('id', appointment.id)
.executeAs<any>()
uni.navigateTo({
url: `/pages/ec/doctor/consultation?appointmentId=${appointment.id}`
})
} catch (error) {
console.error('开始诊疗失败:', error)
uni.showToast({ title: '操作失败', icon: 'error' })
}
}
const continueConsultation = (appointment: Appointment) => {
uni.navigateTo({
url: `/pages/ec/doctor/consultation?appointmentId=${appointment.id}`
})
}
const viewConsultation = (appointment: Appointment) => {
uni.navigateTo({
url: `/pages/ec/doctor/consultation-detail?appointmentId=${appointment.id}`
})
}
const viewConsultationDetails = (consultation: Consultation) => {
uni.navigateTo({
url: `/pages/ec/doctor/consultation-detail?id=${consultation.id}`
})
}
const handleAlert = (alert: HealthAlert) => {
uni.navigateTo({
url: `/pages/ec/doctor/alert-detail?id=${alert.id}`
})
}
</script>
<style scoped>
.doctor-dashboard {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 20px;
padding: 16px;
background-color: #667eea;
border-radius: 12px;
color: #fff;
}
.header-title {
font-size: 22px;
font-weight: bold;
}
.header-subtitle {
font-size: 14px;
opacity: 0.8;
margin-top: 5px;
}
.header-actions {
display: flex;
flex-direction: row;
margin-top: 10px;
}
.action-btn {
padding: 8px 16px;
border-radius: 20px;
border: none;
background-color: #fff;
color: #667eea;
margin-right: 10px;
}
.action-btn.emergency {
background-color: #ff4757;
color: #fff;
}
.action-btn:last-child {
margin-right: 0;
}
.stats-section {
display: flex;
flex-wrap: wrap;
flex-direction:row;
margin-bottom: 20px;
}
.stat-card {
flex: 1 1 140px;
background-color: #fff;
border-radius: 10px;
padding: 16px;
margin-right: 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
}
.stat-card:last-child {
margin-right: 0;
}
.stat-card.urgent {
background-color: #ff6b6b;
color: #fff;
}
.stat-icon {
font-size: 22px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
border-radius: 50%;
margin-right: 10px;
}
.stat-card.urgent .stat-icon {
background-color: #fff2f0;
color: #ff6b6b;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 20px;
font-weight: bold;
display: block;
}
.stat-label {
font-size: 13px;
opacity: 0.7;
margin-top: 4px;
}
.quick-actions {
margin-bottom: 20px;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
color: #333;
}
.actions-grid {
display: flex;
flex-wrap: wrap;
flex-direction:row;
}
.quick-action-btn {
width: 46%;
margin-right: 4%;
margin-bottom: 12px;
padding: 16px 0;
background-color: #fff;
border-radius: 10px;
border: none;
display: flex;
flex-direction: column;
align-items: center;
}
.quick-action-btn:nth-child(2n) {
margin-right: 0;
}
.action-icon {
font-size: 20px;
margin-bottom: 6px;
}
.action-text {
font-size: 13px;
color: #666;
}
.schedule-section, .recent-section, .alerts-section {
margin-bottom: 20px;
}
.section-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.view-all-btn {
padding: 6px 12px;
border-radius: 12px;
border: 1px solid #ddd;
background-color: #fff;
color: #666;
font-size: 12px;
}
.schedule-list, .recent-list, .alerts-list {
background-color: #fff;
border-radius: 10px;
}
.schedule-item {
padding: 14px 10px;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
align-items: flex-start;
}
.schedule-item:last-child {
border-bottom: none;
}
.schedule-item.current {
background-color: #e8f5e8;
border-left: 4px solid #4caf50;
}
.schedule-item.urgent {
background-color: #fffbe6;
border-left: 4px solid #fdcb6e;
}
.schedule-item.completed {
opacity: 0.6;
}
.appointment-time {
width: 80px;
flex-shrink: 0;
}
.time-text {
font-size: 15px;
font-weight: 600;
color: #333;
}
.status-text {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.status-text.scheduled { color: #3498db; }
.status-text.in_progress { color: #f39c12; }
.status-text.completed { color: #27ae60; }
.status-text.cancelled { color: #e74c3c; }
.appointment-details {
flex: 1;
margin-left: 10px;
}
.patient-name {
font-size: 15px;
font-weight: 600;
color: #333;
}
.appointment-type {
font-size: 13px;
color: #666;
margin-top: 4px;
}
.chief-complaint {
font-size: 12px;
color: #888;
margin-top: 4px;
}
.room-info {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.appointment-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: 10px;
}
.start-btn, .continue-btn, .view-btn {
padding: 6px 14px;
border-radius: 16px;
border: none;
font-size: 13px;
color: #fff;
margin-bottom: 6px;
}
.start-btn { background-color: #27ae60; }
.continue-btn { background-color: #f39c12; }
.view-btn { background-color: #3498db; }
.consultation-item {
padding: 14px 10px;
border-bottom: 1px solid #f0f0f0;
}
.consultation-item:last-child {
border-bottom: none;
}
.consultation-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.consultation-date {
font-size: 13px;
color: #666;
}
.diagnosis, .treatment {
font-size: 13px;
color: #555;
margin-bottom: 4px;
}
.consultation-meta {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.consultation-type, .follow-up {
font-size: 12px;
color: #888;
}
.alert-item {
padding: 10px;
border-bottom: 1px solid #f0f0f0;
border-left: 4px solid #ddd;
}
.alert-item:last-child {
border-bottom: none;
}
.alert-item.high {
border-left-color: #f39c12;
background-color: #fef9e7;
}
.alert-item.critical {
border-left-color: #e74c3c;
background-color: #fdf2f2;
}
.alert-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.alert-title {
font-size: 13px;
font-weight: 600;
color: #333;
}
.alert-time {
font-size: 12px;
color: #666;
}
.alert-description {
font-size: 13px;
color: #555;
margin-bottom: 4px;
}
.alert-patient {
font-size: 12px;
color: #888;
}
.empty-state {
padding: 30px 0;
text-align: center;
}
.empty-text {
font-size: 13px;
color: #999;
}
</style>