Healthcare Application Development Case Study
This case study demonstrates how to develop a comprehensive healthcare application using uni-app, covering patient management, appointment scheduling, medical records, and telemedicine features.
Project Overview
Application Features
- Patient registration and profile management
- Doctor appointment scheduling
- Medical record management
- Prescription tracking
- Telemedicine video consultations
- Health monitoring and reminders
- Emergency contact system
- Insurance and billing integration
Technical Architecture
- Frontend: uni-app with Vue.js
- Backend: Node.js with Express
- Database: MongoDB for patient data
- Real-time: WebSocket for consultations
- Storage: Cloud storage for medical files
- Authentication: JWT with role-based access
Core Implementation
1. Patient Registration System
vue
<template>
<view class="patient-registration">
<uni-forms ref="form" :model="patientData" :rules="rules">
<uni-forms-item label="Full Name" name="fullName" required>
<uni-easyinput v-model="patientData.fullName" placeholder="Enter full name" />
</uni-forms-item>
<uni-forms-item label="Date of Birth" name="dateOfBirth" required>
<uni-datetime-picker v-model="patientData.dateOfBirth" type="date" />
</uni-forms-item>
<uni-forms-item label="Gender" name="gender" required>
<uni-data-select
v-model="patientData.gender"
:localdata="genderOptions"
placeholder="Select gender"
/>
</uni-forms-item>
<uni-forms-item label="Phone Number" name="phone" required>
<uni-easyinput v-model="patientData.phone" type="number" placeholder="Enter phone number" />
</uni-forms-item>
<uni-forms-item label="Emergency Contact" name="emergencyContact">
<uni-easyinput v-model="patientData.emergencyContact" placeholder="Emergency contact number" />
</uni-forms-item>
<uni-forms-item label="Medical History" name="medicalHistory">
<textarea v-model="patientData.medicalHistory" placeholder="Brief medical history" />
</uni-forms-item>
<uni-forms-item label="Allergies" name="allergies">
<uni-easyinput v-model="patientData.allergies" placeholder="Known allergies" />
</uni-forms-item>
</uni-forms>
<button @click="registerPatient" class="register-btn">Register Patient</button>
</view>
</template>
<script>
export default {
data() {
return {
patientData: {
fullName: '',
dateOfBirth: '',
gender: '',
phone: '',
emergencyContact: '',
medicalHistory: '',
allergies: ''
},
genderOptions: [
{ value: 'male', text: 'Male' },
{ value: 'female', text: 'Female' },
{ value: 'other', text: 'Other' }
],
rules: {
fullName: {
rules: [{ required: true, errorMessage: 'Full name is required' }]
},
dateOfBirth: {
rules: [{ required: true, errorMessage: 'Date of birth is required' }]
},
gender: {
rules: [{ required: true, errorMessage: 'Gender is required' }]
},
phone: {
rules: [
{ required: true, errorMessage: 'Phone number is required' },
{ pattern: /^1[3-9]\d{9}$/, errorMessage: 'Invalid phone number format' }
]
}
}
}
},
methods: {
async registerPatient() {
try {
const valid = await this.$refs.form.validate()
if (valid) {
const response = await uni.request({
url: 'https://api.healthcare.com/patients/register',
method: 'POST',
data: this.patientData,
header: {
'Authorization': `Bearer ${this.getToken()}`
}
})
if (response.data.success) {
uni.showToast({
title: 'Registration successful',
icon: 'success'
})
uni.navigateTo({
url: '/pages/patient/profile'
})
}
}
} catch (error) {
uni.showToast({
title: 'Registration failed',
icon: 'error'
})
}
},
getToken() {
return uni.getStorageSync('authToken')
}
}
}
</script>
2. Appointment Scheduling
vue
<template>
<view class="appointment-booking">
<uni-calendar
v-model="selectedDate"
:insert="false"
:lunar="false"
:start-date="startDate"
:end-date="endDate"
@change="onDateChange"
/>
<view class="doctor-selection">
<uni-section title="Select Doctor" type="line">
<uni-list>
<uni-list-item
v-for="doctor in availableDoctors"
:key="doctor.id"
:title="doctor.name"
:note="doctor.specialization"
:thumb="doctor.avatar"
clickable
@click="selectDoctor(doctor)"
/>
</uni-list>
</uni-section>
</view>
<view class="time-slots" v-if="selectedDoctor">
<uni-section title="Available Time Slots" type="line">
<view class="time-grid">
<button
v-for="slot in timeSlots"
:key="slot.time"
:class="['time-slot', { 'selected': selectedTime === slot.time, 'disabled': !slot.available }]"
:disabled="!slot.available"
@click="selectTime(slot.time)"
>
{{ slot.time }}
</button>
</view>
</uni-section>
</view>
<view class="appointment-details" v-if="selectedTime">
<uni-forms ref="appointmentForm" :model="appointmentData">
<uni-forms-item label="Reason for Visit" required>
<textarea v-model="appointmentData.reason" placeholder="Describe your symptoms or reason for visit" />
</uni-forms-item>
<uni-forms-item label="Appointment Type">
<uni-data-select
v-model="appointmentData.type"
:localdata="appointmentTypes"
placeholder="Select appointment type"
/>
</uni-forms-item>
</uni-forms>
<button @click="bookAppointment" class="book-btn">Book Appointment</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
selectedDate: '',
selectedDoctor: null,
selectedTime: '',
startDate: new Date().toISOString().split('T')[0],
endDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
availableDoctors: [],
timeSlots: [],
appointmentData: {
reason: '',
type: 'consultation'
},
appointmentTypes: [
{ value: 'consultation', text: 'General Consultation' },
{ value: 'followup', text: 'Follow-up' },
{ value: 'checkup', text: 'Regular Check-up' },
{ value: 'emergency', text: 'Emergency' }
]
}
},
onLoad() {
this.loadAvailableDoctors()
},
methods: {
async loadAvailableDoctors() {
try {
const response = await uni.request({
url: 'https://api.healthcare.com/doctors/available',
method: 'GET',
header: {
'Authorization': `Bearer ${this.getToken()}`
}
})
this.availableDoctors = response.data.doctors
} catch (error) {
console.error('Failed to load doctors:', error)
}
},
onDateChange(event) {
this.selectedDate = event.fulldate
if (this.selectedDoctor) {
this.loadTimeSlots()
}
},
selectDoctor(doctor) {
this.selectedDoctor = doctor
this.selectedTime = ''
if (this.selectedDate) {
this.loadTimeSlots()
}
},
async loadTimeSlots() {
try {
const response = await uni.request({
url: 'https://api.healthcare.com/appointments/slots',
method: 'GET',
data: {
doctorId: this.selectedDoctor.id,
date: this.selectedDate
},
header: {
'Authorization': `Bearer ${this.getToken()}`
}
})
this.timeSlots = response.data.slots
} catch (error) {
console.error('Failed to load time slots:', error)
}
},
selectTime(time) {
this.selectedTime = time
},
async bookAppointment() {
if (!this.appointmentData.reason.trim()) {
uni.showToast({
title: 'Please provide reason for visit',
icon: 'error'
})
return
}
try {
const response = await uni.request({
url: 'https://api.healthcare.com/appointments/book',
method: 'POST',
data: {
doctorId: this.selectedDoctor.id,
date: this.selectedDate,
time: this.selectedTime,
reason: this.appointmentData.reason,
type: this.appointmentData.type
},
header: {
'Authorization': `Bearer ${this.getToken()}`
}
})
if (response.data.success) {
uni.showToast({
title: 'Appointment booked successfully',
icon: 'success'
})
uni.navigateTo({
url: '/pages/appointments/confirmation?id=' + response.data.appointmentId
})
}
} catch (error) {
uni.showToast({
title: 'Failed to book appointment',
icon: 'error'
})
}
},
getToken() {
return uni.getStorageSync('authToken')
}
}
}
</script>
3. Medical Records Management
vue
<template>
<view class="medical-records">
<uni-section title="Medical Records" type="line">
<view class="records-filter">
<uni-data-select
v-model="filterType"
:localdata="recordTypes"
placeholder="Filter by type"
@change="filterRecords"
/>
<uni-datetime-picker
v-model="dateRange"
type="daterange"
@change="filterRecords"
/>
</view>
<uni-list>
<uni-list-item
v-for="record in filteredRecords"
:key="record.id"
:title="record.title"
:note="`${record.date} - Dr. ${record.doctor}`"
:rightText="record.type"
clickable
@click="viewRecord(record)"
>
<template v-slot:body>
<view class="record-preview">
<text class="record-summary">{{ record.summary }}</text>
<view class="record-attachments" v-if="record.attachments.length">
<uni-tag
v-for="attachment in record.attachments"
:key="attachment.id"
:text="attachment.name"
size="small"
/>
</view>
</view>
</template>
</uni-list-item>
</uni-list>
</uni-section>
<uni-fab
:pattern="fabPattern"
@trigger="onFabClick"
/>
</view>
</template>
<script>
export default {
data() {
return {
records: [],
filteredRecords: [],
filterType: '',
dateRange: [],
recordTypes: [
{ value: '', text: 'All Records' },
{ value: 'consultation', text: 'Consultation' },
{ value: 'lab_result', text: 'Lab Results' },
{ value: 'prescription', text: 'Prescription' },
{ value: 'imaging', text: 'Imaging' },
{ value: 'vaccination', text: 'Vaccination' }
],
fabPattern: {
color: '#007AFF',
icon: 'plus',
text: 'Add Record'
}
}
},
onLoad() {
this.loadMedicalRecords()
},
methods: {
async loadMedicalRecords() {
try {
uni.showLoading({ title: 'Loading records...' })
const response = await uni.request({
url: 'https://api.healthcare.com/medical-records',
method: 'GET',
header: {
'Authorization': `Bearer ${this.getToken()}`
}
})
this.records = response.data.records
this.filteredRecords = this.records
uni.hideLoading()
} catch (error) {
uni.hideLoading()
uni.showToast({
title: 'Failed to load records',
icon: 'error'
})
}
},
filterRecords() {
let filtered = this.records
if (this.filterType) {
filtered = filtered.filter(record => record.type === this.filterType)
}
if (this.dateRange.length === 2) {
const startDate = new Date(this.dateRange[0])
const endDate = new Date(this.dateRange[1])
filtered = filtered.filter(record => {
const recordDate = new Date(record.date)
return recordDate >= startDate && recordDate <= endDate
})
}
this.filteredRecords = filtered
},
viewRecord(record) {
uni.navigateTo({
url: `/pages/medical-records/detail?id=${record.id}`
})
},
onFabClick() {
uni.navigateTo({
url: '/pages/medical-records/add'
})
},
getToken() {
return uni.getStorageSync('authToken')
}
}
}
</script>
4. Telemedicine Video Consultation
vue
<template>
<view class="video-consultation">
<view class="video-container">
<live-player
id="remoteVideo"
:src="remoteVideoUrl"
mode="RTC"
autoplay
muted="false"
@statechange="onRemoteVideoStateChange"
class="remote-video"
/>
<live-pusher
id="localVideo"
:url="localVideoUrl"
mode="RTC"
autopush
muted="false"
enable-camera
@statechange="onLocalVideoStateChange"
class="local-video"
/>
</view>
<view class="consultation-info">
<view class="doctor-info">
<image :src="doctorInfo.avatar" class="doctor-avatar" />
<view class="doctor-details">
<text class="doctor-name">Dr. {{ doctorInfo.name }}</text>
<text class="doctor-specialty">{{ doctorInfo.specialization }}</text>
</view>
</view>
<view class="consultation-timer">
<text class="timer">{{ formatTime(consultationTime) }}</text>
</view>
</view>
<view class="control-panel">
<button
:class="['control-btn', { 'active': !isMuted }]"
@click="toggleMute"
>
<uni-icons :type="isMuted ? 'mic-filled' : 'mic'" size="24" />
</button>
<button
:class="['control-btn', { 'active': !isCameraOff }]"
@click="toggleCamera"
>
<uni-icons :type="isCameraOff ? 'videocam-filled' : 'videocam'" size="24" />
</button>
<button class="control-btn switch-camera" @click="switchCamera">
<uni-icons type="loop" size="24" />
</button>
<button class="control-btn end-call" @click="endConsultation">
<uni-icons type="phone" size="24" />
</button>
</view>
<view class="chat-panel" v-if="showChat">
<scroll-view class="chat-messages" scroll-y>
<view
v-for="message in chatMessages"
:key="message.id"
:class="['message', message.sender === 'patient' ? 'sent' : 'received']"
>
<text class="message-text">{{ message.text }}</text>
<text class="message-time">{{ formatMessageTime(message.timestamp) }}</text>
</view>
</scroll-view>
<view class="chat-input">
<input
v-model="newMessage"
placeholder="Type a message..."
@confirm="sendMessage"
/>
<button @click="sendMessage" class="send-btn">Send</button>
</view>
</view>
<button class="chat-toggle" @click="toggleChat">
<uni-icons type="chat" size="20" />
</button>
</view>
</template>
<script>
export default {
data() {
return {
consultationId: '',
doctorInfo: {},
remoteVideoUrl: '',
localVideoUrl: '',
consultationTime: 0,
timer: null,
isMuted: false,
isCameraOff: false,
showChat: false,
chatMessages: [],
newMessage: '',
socketConnection: null
}
},
onLoad(options) {
this.consultationId = options.id
this.initializeConsultation()
},
onUnload() {
this.endConsultation()
},
methods: {
async initializeConsultation() {
try {
// Get consultation details
const response = await uni.request({
url: `https://api.healthcare.com/consultations/${this.consultationId}`,
method: 'GET',
header: {
'Authorization': `Bearer ${this.getToken()}`
}
})
this.doctorInfo = response.data.doctor
this.remoteVideoUrl = response.data.remoteVideoUrl
this.localVideoUrl = response.data.localVideoUrl
// Initialize WebSocket for chat
this.initializeWebSocket()
// Start consultation timer
this.startTimer()
} catch (error) {
uni.showToast({
title: 'Failed to initialize consultation',
icon: 'error'
})
}
},
initializeWebSocket() {
this.socketConnection = uni.connectSocket({
url: `wss://api.healthcare.com/consultations/${this.consultationId}/chat`,
header: {
'Authorization': `Bearer ${this.getToken()}`
}
})
this.socketConnection.onMessage((res) => {
const message = JSON.parse(res.data)
this.chatMessages.push(message)
})
},
startTimer() {
this.timer = setInterval(() => {
this.consultationTime++
}, 1000)
},
toggleMute() {
this.isMuted = !this.isMuted
const livePusher = uni.createLivePusherContext('localVideo', this)
if (this.isMuted) {
livePusher.mute()
} else {
livePusher.unmute()
}
},
toggleCamera() {
this.isCameraOff = !this.isCameraOff
const livePusher = uni.createLivePusherContext('localVideo', this)
if (this.isCameraOff) {
livePusher.stop()
} else {
livePusher.start()
}
},
switchCamera() {
const livePusher = uni.createLivePusherContext('localVideo', this)
livePusher.switchCamera()
},
async endConsultation() {
try {
await uni.request({
url: `https://api.healthcare.com/consultations/${this.consultationId}/end`,
method: 'POST',
header: {
'Authorization': `Bearer ${this.getToken()}`
}
})
if (this.timer) {
clearInterval(this.timer)
}
if (this.socketConnection) {
uni.closeSocket()
}
uni.navigateBack()
} catch (error) {
console.error('Failed to end consultation:', error)
}
},
toggleChat() {
this.showChat = !this.showChat
},
sendMessage() {
if (!this.newMessage.trim()) return
const message = {
id: Date.now(),
text: this.newMessage,
sender: 'patient',
timestamp: new Date()
}
this.chatMessages.push(message)
if (this.socketConnection) {
uni.sendSocketMessage({
data: JSON.stringify(message)
})
}
this.newMessage = ''
},
formatTime(seconds) {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
},
formatMessageTime(timestamp) {
return new Date(timestamp).toLocaleTimeString()
},
onRemoteVideoStateChange(e) {
console.log('Remote video state:', e.detail)
},
onLocalVideoStateChange(e) {
console.log('Local video state:', e.detail)
},
getToken() {
return uni.getStorageSync('authToken')
}
}
}
</script>
Platform-Specific Optimizations
iOS Optimizations
javascript
// iOS-specific health data integration
// #ifdef APP-PLUS-IOS
import { HealthKit } from '@/utils/healthkit'
export default {
methods: {
async syncHealthData() {
try {
const healthData = await HealthKit.requestHealthData([
'heartRate',
'bloodPressure',
'weight',
'steps'
])
await this.uploadHealthData(healthData)
} catch (error) {
console.error('Health data sync failed:', error)
}
}
}
}
// #endif
Android Optimizations
javascript
// Android-specific health sensors
// #ifdef APP-PLUS-ANDROID
export default {
methods: {
async initializeHealthSensors() {
const sensors = await plus.android.requestPermissions([
'android.permission.BODY_SENSORS',
'android.permission.ACTIVITY_RECOGNITION'
])
if (sensors.granted) {
this.startHealthMonitoring()
}
}
}
}
// #endif
Performance Optimization
Data Caching Strategy
javascript
// utils/healthcareCache.js
class HealthcareCache {
constructor() {
this.cache = new Map()
this.cacheTimeout = 5 * 60 * 1000 // 5 minutes
}
set(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
})
}
get(key) {
const cached = this.cache.get(key)
if (!cached) return null
if (Date.now() - cached.timestamp > this.cacheTimeout) {
this.cache.delete(key)
return null
}
return cached.data
}
// Cache medical records for offline access
cacheMedicalRecords(records) {
this.set('medical_records', records)
uni.setStorageSync('offline_medical_records', records)
}
// Cache appointment data
cacheAppointments(appointments) {
this.set('appointments', appointments)
uni.setStorageSync('offline_appointments', appointments)
}
}
export default new HealthcareCache()
Image Optimization for Medical Files
javascript
// utils/imageOptimizer.js
export function optimizeMedicalImage(imagePath) {
return new Promise((resolve, reject) => {
uni.compressImage({
src: imagePath,
quality: 80,
width: 1024,
height: 1024,
success: (res) => {
resolve(res.tempFilePath)
},
fail: reject
})
})
}
export function generateThumbnail(imagePath) {
return new Promise((resolve, reject) => {
uni.compressImage({
src: imagePath,
quality: 60,
width: 200,
height: 200,
success: (res) => {
resolve(res.tempFilePath)
},
fail: reject
})
})
}
Security Implementation
Data Encryption
javascript
// utils/encryption.js
import CryptoJS from 'crypto-js'
export function encryptMedicalData(data, key) {
return CryptoJS.AES.encrypt(JSON.stringify(data), key).toString()
}
export function decryptMedicalData(encryptedData, key) {
const bytes = CryptoJS.AES.decrypt(encryptedData, key)
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8))
}
// Secure storage for sensitive medical data
export function secureStore(key, data) {
const encryptionKey = uni.getStorageSync('userEncryptionKey')
const encrypted = encryptMedicalData(data, encryptionKey)
uni.setStorageSync(key, encrypted)
}
export function secureRetrieve(key) {
const encryptionKey = uni.getStorageSync('userEncryptionKey')
const encrypted = uni.getStorageSync(key)
return decryptMedicalData(encrypted, encryptionKey)
}
Testing Strategy
Unit Tests
javascript
// tests/healthcare.test.js
import { mount } from '@vue/test-utils'
import PatientRegistration from '@/pages/patient/registration.vue'
describe('Patient Registration', () => {
test('validates required fields', async () => {
const wrapper = mount(PatientRegistration)
await wrapper.find('.register-btn').trigger('click')
expect(wrapper.vm.$refs.form.validate).toHaveBeenCalled()
})
test('formats phone number correctly', () => {
const wrapper = mount(PatientRegistration)
wrapper.setData({
patientData: { phone: '1234567890' }
})
expect(wrapper.vm.formatPhoneNumber('1234567890')).toBe('(123) 456-7890')
})
})
Deployment Considerations
HIPAA Compliance
- Implement end-to-end encryption for all medical data
- Use secure authentication with multi-factor authentication
- Maintain audit logs for all data access
- Ensure data backup and disaster recovery procedures
- Regular security assessments and penetration testing
Scalability Planning
- Implement horizontal scaling for video consultation servers
- Use CDN for medical image delivery
- Database sharding for patient records
- Load balancing for API endpoints
- Caching strategies for frequently accessed data
This healthcare application demonstrates the comprehensive features needed for a medical platform, including patient management, appointment scheduling, telemedicine capabilities, and robust security measures to protect sensitive medical information.