Skip to content

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.

一次开发,多端部署 - 让跨平台开发更简单