Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,374 @@
<template>
<view class="consultations-page">
<text class="title">全部诊疗记录</text>
<!-- 查询区域 -->
<view class="search-section">
<view class="search-input-group">
<input
v-model="searchQuery"
placeholder="请输入患者姓名或诊疗ID"
class="search-input"
@confirm="onInputConfirm"
/>
<button class="search-btn" @click="performSearch">查询</button>
</view>
<view class="filter-group">
<picker
mode="date"
:value="startDate"
@change="onStartDateChange"
class="date-picker date-picker-left"
>
<view class="picker-display">
开始日期: {{ startDate != '' ? startDate : '请选择' }}
</view>
</picker>
<picker
mode="date"
:value="endDate"
@change="onEndDateChange"
class="date-picker"
>
<view class="picker-display">
结束日期: {{ endDate != '' ? endDate : '请选择' }}
</view>
</picker>
</view>
</view>
<!-- 诊疗记录列表 -->
<view class="records-list" v-if="records.length > 0">
<view
v-for="record in records"
:key="record.id"
class="record-item"
:class="getUrgencyClass(record.urgency)"
>
<view class="record-header">
<view class="record-header-left">
<text class="urgency-tag" v-if="record.urgency != 'normal'">{{ getUrgencyText(record.urgency) }}</text>
<text class="patient-name">{{ record.patientName }}</text>
</view>
<text class="consultation-date">{{ record.date }}</text>
</view>
<view class="record-content">
<text class="diagnosis">{{ record.diagnosis }}</text>
</view>
<view class="record-actions">
<button class="detail-btn" @click="viewDetail(record)">查看详情</button>
</view>
</view>
</view>
<view class="empty" v-else>暂无数据</view>
<!-- 加载状态 -->
<view class="loading" v-if="loading">加载中...</view>
</view>
</template>
<script setup lang="uts">
// 全部诊疗记录页面
import { ref, onMounted } from 'vue'
// 定义诊疗记录类
type RecordItem = {
id : number,
patientName : string,
date : string,
diagnosis : string,
urgency : string // emergency, urgent, normal
}
// 数据
const searchQuery = ref('')
const startDate = ref('')
const endDate = ref('')
const records = ref([] as RecordItem[])
const loading = ref(false)
/**
* 查询诊疗记录
*/
function performSearch() {
if (loading.value) return
loading.value = true
// 模拟异步请求
setTimeout(() => {
try {
// 暂时使用模拟数据
const mockRecords : RecordItem[] = [
{
id: 1,
patientName: '张三',
date: '2024-01-15',
diagnosis: '感冒,发热,建议休息多喝水',
urgency: 'normal'
},
{
id: 2,
patientName: '李四',
date: '2024-01-14',
diagnosis: '高血压,建议定期检查',
urgency: 'urgent'
},
{
id: 3,
patientName: '王五',
date: '2024-01-16',
diagnosis: '急性胸痛,疑心肌梗死,需紧急处置',
urgency: 'emergency'
}
]
// 过滤数据(模拟查询)
records.value = mockRecords.filter((record : RecordItem) : boolean => {
const matchesQuery = searchQuery.value == '' ||
record.patientName.includes(searchQuery.value) ||
record.id.toString().includes(searchQuery.value)
const matchesDate = (startDate.value == '' || record.date >= startDate.value) &&
(endDate.value == '' || record.date <= endDate.value)
return matchesQuery && matchesDate
})
} catch (e : any) {
console.error('查询失败:', e)
uni.showToast({
title: '查询失败',
icon: 'none'
})
} finally {
loading.value = false
}
}, 200)
}
// 输入确认搜索
function onInputConfirm(_ : any) {
performSearch()
}
// 日期选择器变化
function onStartDateChange(e : any) {
startDate.value = e.detail.value as string
}
function onEndDateChange(e : any) {
endDate.value = e.detail.value as string
}
// 查看详情
function viewDetail(record : RecordItem) {
uni.navigateTo({
url: `/pages/ec/doctor/consultation-detail?id=${record.id}`
})
}
/**
* 获取紧急程度对应的样式类
*/
function getUrgencyClass(urgency : string) : string {
if (urgency == 'emergency') {
return 'record-item-emergency'
} else if (urgency == 'urgent') {
return 'record-item-urgent'
}
return 'record-item-normal'
}
/**
* 获取紧急程度文字
*/
function getUrgencyText(urgency : string) : string {
if (urgency == 'emergency') {
return '【紧急】'
} else if (urgency == 'urgent') {
return '【优先】'
}
return ''
}
// 页面加载时获取所有记录
onMounted(() => {
performSearch()
})
</script>
<style scoped>
.consultations-page {
padding: 30px;
background-color: #f5f5f5;
min-height: 100vh;
}
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
display: block;
}
.search-section {
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.search-input-group {
display: flex;
flex-direction: row;
margin-bottom: 15px;
}
.search-input {
flex: 1;
padding: 10px;
border-width: 1px;
border-style: solid;
border-color: #dddddd;
border-radius: 4px;
font-size: 14px;
margin-right: 10px;
}
.search-btn {
padding-left: 20px;
padding-right: 20px;
background-color: #007aff;
color: #ffffff;
border-radius: 4px;
font-size: 14px;
}
.filter-group {
display: flex;
flex-direction: row;
}
.date-picker {
flex: 1;
}
.date-picker-left {
margin-right: 15px;
}
.picker-display {
padding: 10px;
border-width: 1px;
border-style: solid;
border-color: #dddddd;
border-radius: 4px;
background-color: #f9f9f9;
font-size: 14px;
}
.records-list {
background-color: #ffffff;
border-radius: 8px;
}
.record-item {
padding: 15px;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: #eeeeee;
}
.record-item-emergency {
background-color: #fff1f0;
}
.record-item-urgent {
background-color: #fff7e6;
}
.record-item-normal {
background-color: #ffffff;
}
.record-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.record-header-left {
display: flex;
flex-direction: row;
align-items: center;
}
.urgency-tag {
font-size: 14px;
font-weight: bold;
margin-right: 4px;
}
.record-item-emergency .urgency-tag {
color: #ff4d4f;
}
.record-item-urgent .urgency-tag {
color: #fa8c16;
}
.patient-name {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.consultation-date {
font-size: 14px;
color: #666666;
}
.record-content {
margin-bottom: 10px;
}
.diagnosis {
font-size: 14px;
color: #555555;
line-height: 20px;
}
.record-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.detail-btn {
padding-left: 12px;
padding-right: 12px;
background-color: #28a745;
color: #ffffff;
border-radius: 4px;
font-size: 12px;
}
.empty {
color: #aaaaaa;
text-align: center;
margin-top: 60px;
font-size: 16px;
}
.loading {
text-align: center;
margin-top: 20px;
color: #666666;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,932 @@
<!--
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>

View File

@@ -0,0 +1,166 @@
<template>
<view class="emergency-page">
<view class="header">
<text class="title">急诊管理</text>
<button class="add-btn" @click="showAddEmergency"> 新建急诊</button>
</view>
<view class="stats">
<text>今日急诊: {{ stats.today }}</text>
<text>处理中: {{ stats.processing }}</text>
<text>已解决: {{ stats.resolved }}</text>
</view>
<scroll-view class="emergency-list" scroll-y="true">
<view v-for="item in emergencies" :key="item.id" class="emergency-item" :class="item.severity">
<view class="item-header">
<text class="type">{{ getTypeText(item.emergency_type) }}</text>
<text class="severity">{{ getSeverityText(item.severity) }}</text>
<text class="status">{{ getStatusText(item.status) }}</text>
</view>
<view class="item-content">
<text class="elder">患者: {{ item.elder_name }}</text>
<text class="desc">{{ item.description }}</text>
</view>
<view class="item-footer">
<text class="time">{{ formatDateTime(item.occurred_at) }}</text>
<button v-if="item.status==='active'" class="handle-btn" @click="handleEmergency(item)">处理</button>
</view>
</view>
<view v-if="emergencies.length===0" class="empty-state">
<text>暂无急诊事件</text>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { formatDateTime } from '../types_new.uts'
type Emergency = {
id: string
appointment_id: string
elder_id: string
elder_name: string
doctor_id: string
emergency_type: string
severity: string
status: string
description: string
occurred_at: string
handled_at: string
handler_notes: string
created_at: string
updated_at: string
}
type EmergencyStats = {
today: number
processing: number
resolved: number
}
const emergencies = ref<Emergency[]>([])
const stats = ref<EmergencyStats>({ today: 0, processing: 0, resolved: 0 })
onMounted(() => {
loadEmergencies()
loadStats()
})
const loadEmergencies = async () => {
const result = await supa
.from('ec_emergencies')
.select('*')
.order('occurred_at', { ascending: false })
.limit(30)
.executeAs<Emergency[]>()
if (result.error == null && result.data != null) {
emergencies.value = result.data
}
}
const loadStats = async () => {
const todayStart = new Date()
todayStart.setHours(0,0,0,0)
const todayEnd = new Date()
todayEnd.setHours(23,59,59,999)
const todayResult = await supa
.from('ec_emergencies')
.select('*', { count: 'exact' })
.gte('occurred_at', todayStart.toISOString())
.lte('occurred_at', todayEnd.toISOString())
.executeAs<Emergency[]>()
const processingResult = await supa
.from('ec_emergencies')
.select('*', { count: 'exact' })
.eq('status', 'processing')
.executeAs<Emergency[]>()
const resolvedResult = await supa
.from('ec_emergencies')
.select('*', { count: 'exact' })
.eq('status', 'resolved')
.executeAs<Emergency[]>()
stats.value = {
today: todayResult.count ?? 0,
processing: processingResult.count ?? 0,
resolved: resolvedResult.count ?? 0
}
}
const getTypeText = (type: string): string => {
const map = new Map([
['fall', '跌倒'],
['stroke', '中风'],
['cardiac', '心脏'],
['other', '其他']
])
return map.get(type) ?? type
}
const getSeverityText = (sev: string): string => {
const map = new Map([
['low', '低'],
['medium', '中'],
['high', '高'],
['critical', '危急']
])
return map.get(sev) ?? sev
}
const getStatusText = (status: string): string => {
const map = new Map([
['active', '待处理'],
['processing', '处理中'],
['resolved', '已解决'],
['cancelled', '已取消']
])
return map.get(status) ?? status
}
const showAddEmergency = () => {
uni.navigateTo({ url: '/pages/ec/doctor/emergency-form' })
}
const handleEmergency = (item: Emergency) => {
uni.navigateTo({ url: `/pages/ec/doctor/emergency-handle?id=${item.id}` })
}
</script>
<style scoped>
.emergency-page { padding: 20px; background: #f5f5f5; min-height: 100vh; }
.header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.title { font-size: 20px; font-weight: bold; }
.add-btn { background: #ff4757; color: #fff; border: none; border-radius: 16px; padding: 8px 18px; font-size: 15px; }
.stats { display: flex; flex-direction: row; gap: 18px; margin-bottom: 12px; }
.emergency-list { background: #fff; border-radius: 10px; }
.emergency-item { padding: 14px 10px; border-bottom: 1px solid #f0f0f0; }
.emergency-item:last-child { border-bottom: none; }
.item-header { display: flex; flex-direction: row; gap: 10px; margin-bottom: 6px; }
.type { font-size: 14px; font-weight: 600; }
.severity { font-size: 13px; color: #e67e22; }
.status { font-size: 13px; color: #3498db; }
.item-content { font-size: 13px; color: #555; margin-bottom: 6px; }
.elder { color: #888; margin-right: 10px; }
.desc { color: #555; }
.item-footer { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
.time { font-size: 12px; color: #999; }
.handle-btn { background: #27ae60; color: #fff; border: none; border-radius: 12px; padding: 6px 14px; font-size: 13px; }
.empty-state { padding: 30px 0; text-align: center; color: #999; }
</style>

View File

@@ -0,0 +1,106 @@
<template>
<view class="health-reports-page">
<view class="header">
<text class="title">健康报告</text>
</view>
<view class="content">
<view v-if="reports.length === 0" class="empty-state">
<text class="empty-text">暂无健康报告</text>
</view>
<view v-for="report in reports" :key="report.id" class="report-item">
<text class="report-title">{{ report.title }}</text>
<text class="report-date">{{ formatDate(report.created_at) }}</text>
<text class="report-summary">{{ report.summary }}</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { formatDate } from '../types_new.uts'
type HealthReport = {
id: string
elder_id: string
elder_name: string
title: string
summary: string
created_at: string
}
const reports = ref<Array<HealthReport>>([])
onMounted(() => {
loadReports()
})
const loadReports = async () => {
try {
const result = await supa
.from('ec_health_reports')
.select('*')
.order('created_at', { ascending: false })
.limit(20)
.executeAs<HealthReport[]>()
if (result.error == null && result.data != null) {
reports.value = result.data
}
} catch (error) {
console.error('加载健康报告失败:', error)
}
}
</script>
<style scoped>
.health-reports-page {
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
}
.header {
margin-bottom: 20px;
}
.title {
font-size: 22px;
font-weight: bold;
color: #333;
}
.content {
background: #fff;
border-radius: 10px;
padding: 16px;
}
.report-item {
border-bottom: 1px solid #f0f0f0;
padding: 12px 0;
}
.report-item:last-child {
border-bottom: none;
}
.report-title {
font-size: 16px;
font-weight: 600;
color: #222;
}
.report-date {
font-size: 12px;
color: #888;
margin-left: 10px;
}
.report-summary {
font-size: 13px;
color: #555;
margin-top: 6px;
display: block;
}
.empty-state {
text-align: center;
padding: 40px 0;
}
.empty-text {
color: #aaa;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<view class="medical-records">
<view class="header">
<text class="header-title">病历管理</text>
</view>
<scroll-view class="records-list" scroll-y="true" :style="{ height: '600px' }">
<view
v-for="record in records"
:key="record.id"
class="record-item"
@click="openRecord(record)"
>
<view class="record-header">
<text class="patient-name">{{ record.elder_name }}</text>
<text class="visit-date">{{ formatDateTime(record.visit_date) }}</text>
</view>
<view class="record-content">
<text class="visit-type">类型: {{ getVisitTypeText(record.visit_type) }}</text>
<text class="chief-complaint">主诉: {{ record.chief_complaint || '无' }}</text>
<text class="diagnosis">诊断: {{ record.diagnosis || '未填写' }}</text>
</view>
</view>
<view v-if="records.length === 0" class="empty-state">
<text class="empty-text">暂无病历记录</text>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { formatDateTime } from '../types_new.uts'
type MedicalRecord = {
id: string
elder_id: string
elder_name: string
visit_type: string
visit_date: string
chief_complaint: string
diagnosis: string
}
const records = ref<Array<MedicalRecord>>([])
onMounted(() => {
loadRecords()
})
const loadRecords = async () => {
try {
const result = await supa
.from('ec_medical_records')
.select('id, elder_id, elder_name, visit_type, visit_date, chief_complaint, diagnosis')
.order('visit_date', { ascending: false })
.executeAs<MedicalRecord[]>()
if (result.error == null && result.data != null) {
records.value = result.data
}
} catch (error) {
console.error('加载病历失败:', error)
}
}
const getVisitTypeText = (type: string): string => {
const typeMap = new Map([
['routine', '常规'],
['emergency', '急诊'],
['consultation', '会诊'],
['follow_up', '复诊']
])
return typeMap.get(type) ?? type
}
const openRecord = (record: MedicalRecord) => {
uni.navigateTo({ url: `/pages/ec/doctor/medical-record-detail?id=${record.id}` })
}
</script>
<style scoped>
.medical-records {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
margin-bottom: 16px;
}
.header-title {
font-size: 20px;
font-weight: bold;
}
.records-list {
background-color: #fff;
border-radius: 10px;
}
.record-item {
padding: 14px 10px;
border-bottom: 1px solid #f0f0f0;
}
.record-item:last-child {
border-bottom: none;
}
.record-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.patient-name {
font-size: 15px;
font-weight: 600;
color: #333;
}
.visit-date {
font-size: 13px;
color: #666;
}
.record-content {
font-size: 13px;
color: #555;
}
.visit-type, .chief-complaint, .diagnosis {
display: block;
margin-bottom: 4px;
}
.empty-state {
padding: 30px 0;
text-align: center;
}
.empty-text {
font-size: 13px;
color: #999;
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<view class="medication-management-page">
<text class="title">用药管理</text>
<view class="empty">暂无数据</view>
</view>
</template>
<script setup lang="uts">
// 用药管理页面骨架
</script>
<style scoped>
.medication-management-page {
padding: 30px;
background: #f5f5f5;
min-height: 100vh;
}
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
}
.empty {
color: #aaa;
text-align: center;
margin-top: 60px;
}
</style>

View File

@@ -0,0 +1,163 @@
<template>
<view class="patient-queue">
<view class="header">
<text class="header-title">今日患者队列</text>
</view>
<scroll-view class="queue-list" scroll-y="true" :style="{ height: '600px' }">
<view
v-for="appointment in queue"
:key="appointment.id"
class="queue-item"
:class="{ 'current': isCurrent(appointment), 'completed': appointment.status === 'completed' }"
@click="openAppointment(appointment)"
>
<view class="queue-time">
<text class="time-text">{{ formatTime(appointment.scheduled_time) }}</text>
<text class="status-text" :class="appointment.status">{{ getStatusText(appointment.status) }}</text>
</view>
<view class="queue-details">
<text class="patient-name">{{ appointment.elder_name }}</text>
<text class="room-info" v-if="appointment.room_number">房间: {{ appointment.room_number }}</text>
</view>
</view>
<view v-if="queue.length === 0" class="empty-state">
<text class="empty-text">今日暂无预约</text>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { formatTime, getTodayStart, getTodayEnd } from '../types_new.uts'
type Appointment = {
id: string
elder_id: string
elder_name: string
scheduled_time: string
status: string
room_number: string
}
const queue = ref<Array<Appointment>>([])
onMounted(() => {
loadQueue()
})
const loadQueue = async () => {
try {
const result = await supa
.from('ec_appointments')
.select('id, elder_id, elder_name, scheduled_time, status, room_number')
.gte('scheduled_time', getTodayStart())
.lte('scheduled_time', getTodayEnd())
.order('scheduled_time', { ascending: true })
.executeAs<Appointment[]>()
if (result.error == null && result.data != null) {
queue.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 isCurrent = (appointment: Appointment): boolean => {
const now = new Date()
const t = new Date(appointment.scheduled_time)
return Math.abs(now.getTime() - t.getTime()) <= 30 * 60 * 1000 && appointment.status === 'in_progress'
}
const openAppointment = (appointment: Appointment) => {
uni.navigateTo({ url: `/pages/ec/doctor/appointment-detail?id=${appointment.id}` })
}
</script>
<style scoped>
.patient-queue {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
margin-bottom: 16px;
}
.header-title {
font-size: 20px;
font-weight: bold;
}
.queue-list {
background-color: #fff;
border-radius: 10px;
}
.queue-item {
padding: 14px 10px;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
align-items: flex-start;
}
.queue-item:last-child {
border-bottom: none;
}
.queue-item.current {
background-color: #e8f5e8;
border-left: 4px solid #4caf50;
}
.queue-item.completed {
opacity: 0.6;
}
.queue-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; }
.queue-details {
flex: 1;
margin-left: 10px;
}
.patient-name {
font-size: 15px;
font-weight: 600;
color: #333;
}
.room-info {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.empty-state {
padding: 30px 0;
text-align: center;
}
.empty-text {
font-size: 13px;
color: #999;
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<view class="prescriptions">
<view class="header">
<text class="header-title">处方管理</text>
</view>
<scroll-view class="prescription-list" scroll-y="true" :style="{ height: '600px' }">
<view
v-for="item in prescriptions"
:key="item.id"
class="prescription-item"
@click="openPrescription(item)"
>
<view class="prescription-header">
<text class="patient-name">{{ item.elder_name }}</text>
<text class="medication-name">{{ item.medication_name }}</text>
</view>
<view class="prescription-content">
<text class="dosage">剂量: {{ item.dosage || '未填写' }}</text>
<text class="status">状态: {{ getStatusText(item.status) }}</text>
<text class="date-range">{{ item.start_date }} ~ {{ item.end_date || '...' }}</text>
</view>
</view>
<view v-if="prescriptions.length === 0" class="empty-state">
<text class="empty-text">暂无处方记录</text>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
type Prescription = {
id: string
elder_id: string
elder_name: string
medication_name: string
dosage: string
status: string
start_date: string
end_date: string
}
const prescriptions = ref<Array<Prescription>>([])
onMounted(() => {
loadPrescriptions()
})
const loadPrescriptions = async () => {
try {
const result = await supa
.from('ec_medications')
.select('id, elder_id, elder_name, medication_name, dosage, status, start_date, end_date')
.order('created_at', { ascending: false })
.executeAs<Prescription[]>()
if (result.error == null && result.data != null) {
prescriptions.value = result.data
}
} catch (error) {
console.error('加载处方失败:', error)
}
}
const getStatusText = (status: string): string => {
const statusMap = new Map([
['active', '进行中'],
['completed', '已完成'],
['discontinued', '已停用']
])
return statusMap.get(status) ?? status
}
const openPrescription = (item: Prescription) => {
uni.navigateTo({ url: `/pages/ec/doctor/prescription-detail?id=${item.id}` })
}
</script>
<style scoped>
.prescriptions {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
margin-bottom: 16px;
}
.header-title {
font-size: 20px;
font-weight: bold;
}
.prescription-list {
background-color: #fff;
border-radius: 10px;
}
.prescription-item {
padding: 14px 10px;
border-bottom: 1px solid #f0f0f0;
}
.prescription-item:last-child {
border-bottom: none;
}
.prescription-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.patient-name {
font-size: 15px;
font-weight: 600;
color: #333;
}
.medication-name {
font-size: 14px;
color: #666;
}
.prescription-content {
font-size: 13px;
color: #555;
}
.dosage, .status, .date-range {
display: block;
margin-bottom: 4px;
}
.empty-state {
padding: 30px 0;
text-align: center;
}
.empty-text {
font-size: 13px;
color: #999;
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<view class="schedule-page">
<text class="title">今日日程</text>
<view class="empty">暂无数据</view>
</view>
</template>
<script setup lang="uts">
// 日程页面骨架
</script>
<style scoped>
.schedule-page {
padding: 30px;
background: #f5f5f5;
min-height: 100vh;
}
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
}
.empty {
color: #aaa;
text-align: center;
margin-top: 60px;
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<view class="vital-signs-page">
<text class="title">生命体征</text>
<view class="empty">暂无数据</view>
</view>
</template>
<script setup lang="uts">
// 生命体征页面骨架
</script>
<style scoped>
.vital-signs-page {
padding: 30px;
background: #f5f5f5;
min-height: 100vh;
}
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
}
.empty {
color: #aaa;
text-align: center;
margin-top: 60px;
}
</style>