Skip to content

医疗健康应用实战案例

本文将介绍如何使用 uni-app 开发一个功能完善的医疗健康应用,包括应用架构设计、核心功能实现和关键代码示例。

1. 应用概述

1.1 功能特点

医疗健康应用是移动互联网时代的重要应用类型,主要功能包括:

  • 健康数据管理:记录和分析体重、血压、血糖等健康指标
  • 在线问诊:远程咨询医生、图文问诊、视频问诊
  • 预约挂号:线上预约医院科室、医生门诊
  • 健康资讯:医疗知识科普、健康生活指导
  • 药品服务:药品信息查询、在线购药、用药提醒
  • 个人健康档案:电子病历、检查报告管理

1.2 技术架构

前端技术栈

  • uni-app:跨平台开发框架,实现一次开发多端运行
  • Vue.js:响应式数据绑定,提供组件化开发模式
  • Vuex:状态管理,处理复杂组件间通信
  • echarts:数据可视化,展示健康数据趋势图表
  • uni-ui:官方UI组件库,提供统一的界面设计

后端技术栈

  • 云函数:处理业务逻辑,提供无服务器计算能力
  • 云数据库:存储用户健康数据和医疗信息
  • 第三方API:接入医疗资源和服务(如医院API、药品数据库等)

2. 项目结构

├── components            // 自定义组件
│   ├── health-card       // 健康卡片组件
│   ├── doctor-item       // 医生列表项组件
│   ├── data-chart        // 数据图表组件
│   └── appointment-card  // 预约卡片组件
├── pages                 // 页面文件夹
│   ├── index             // 首页
│   ├── health            // 健康数据页
│   ├── consult           // 在线问诊页
│   ├── hospital          // 医院挂号页
│   ├── medicine          // 药品服务页
│   └── user              // 用户中心
├── static                // 静态资源
├── store                 // Vuex 状态管理
│   ├── index.js          // 组装模块并导出
│   ├── health.js         // 健康数据状态
│   ├── consult.js        // 问诊相关状态
│   └── user.js           // 用户相关状态
├── utils                 // 工具函数
│   ├── request.js        // 请求封装
│   ├── date.js           // 日期处理
│   └── permission.js     // 权限处理
├── App.vue               // 应用入口
├── main.js               // 主入口
├── manifest.json         // 配置文件
└── pages.json            // 页面配置

3. 核心功能实现

3.1 健康数据管理

健康数据管理是医疗健康应用的基础功能,包括各类健康指标的记录、分析和可视化展示。

健康数据状态管理

js
// store/health.js
export default {
  namespaced: true,
  state: {
    healthData: {
      weight: [],      // 体重数据
      bloodPressure: [], // 血压数据
      bloodSugar: [],  // 血糖数据
      heartRate: [],   // 心率数据
      steps: []        // 步数数据
    },
    loading: false
  },
  mutations: {
    SET_HEALTH_DATA(state, { type, data }) {
      state.healthData[type] = data;
    },
    ADD_HEALTH_RECORD(state, { type, record }) {
      // 添加新记录到对应类型的数组头部
      state.healthData[type].unshift(record);
      
      // 按时间排序
      state.healthData[type].sort((a, b) => b.time - a.time);
    },
    SET_LOADING(state, status) {
      state.loading = status;
    }
  },
  actions: {
    // 获取健康数据
    async getHealthData({ commit, state }) {
      commit('SET_LOADING', true);
      
      try {
        const { result } = await uniCloud.callFunction({
          name: 'health',
          data: { action: 'getHealthData' }
        });
        
        // 设置各类健康数据
        Object.keys(result.data).forEach(type => {
          commit('SET_HEALTH_DATA', { 
            type, 
            data: result.data[type] || [] 
          });
        });
        
        return result.data;
      } catch (error) {
        console.error('获取健康数据失败', error);
        throw error;
      } finally {
        commit('SET_LOADING', false);
      }
    },
    
    // 添加健康记录
    async addHealthRecord({ commit }, record) {
      try {
        const { result } = await uniCloud.callFunction({
          name: 'health',
          data: { 
            action: 'addHealthRecord',
            record
          }
        });
        
        // 更新本地状态
        commit('ADD_HEALTH_RECORD', { 
          type: record.type, 
          record: result.data 
        });
        
        return result.data;
      } catch (error) {
        console.error('添加健康记录失败', error);
        throw error;
      }
    }
  },
  getters: {
    // 获取最新的健康数据
    getLatestData: state => type => {
      const data = state.healthData[type] || [];
      return data.length > 0 ? data[0] : null;
    },
    
    // 获取健康数据趋势
    getDataTrend: state => type => {
      const data = state.healthData[type] || [];
      if (data.length < 2) return 'stable';
      
      const latest = data[0].value;
      const previous = data[1].value;
      
      if (latest > previous) return 'up';
      if (latest < previous) return 'down';
      return 'stable';
    }
  }
};

健康数据页面实现

vue
<!-- pages/health/health.vue -->
<template>
  <view class="health-page">
    <!-- 顶部数据卡片 -->
    <scroll-view scroll-x class="card-scroll">
      <view class="card-list">
        <health-card 
          v-for="(item, index) in healthCards" 
          :key="index"
          :data="item"
          @click="showDetail(item.type)"
        ></health-card>
      </view>
    </scroll-view>
    
    <!-- 数据图表 -->
    <view class="chart-section">
      <view class="section-header">
        <text class="section-title">{{currentType.name}}趋势</text>
        <view class="time-filter">
          <text 
            class="time-item" 
            :class="{ active: timeRange === 'week' }"
            @tap="changeTimeRange('week')"
          >周</text>
          <text 
            class="time-item" 
            :class="{ active: timeRange === 'month' }"
            @tap="changeTimeRange('month')"
          >月</text>
          <text 
            class="time-item" 
            :class="{ active: timeRange === 'year' }"
            @tap="changeTimeRange('year')"
          >年</text>
        </view>
      </view>
      
      <view class="chart-container">
        <data-chart 
          :chartData="chartData" 
          :chartType="currentType.type"
          :unit="currentType.unit"
        ></data-chart>
      </view>
    </view>
    
    <!-- 数据记录 -->
    <view class="record-section">
      <view class="section-header">
        <text class="section-title">历史记录</text>
        <text class="add-btn" @tap="showAddModal">+ 添加记录</text>
      </view>
      
      <view class="record-list">
        <view 
          class="record-item" 
          v-for="(item, index) in recordList" 
          :key="index"
        >
          <view class="record-info">
            <text class="record-value">{{item.value}}{{currentType.unit}}</text>
            <text class="record-time">{{formatTime(item.time)}}</text>
          </view>
          <view class="record-status" :class="getStatusClass(item.value)">
            {{getStatusText(item.value)}}
          </view>
        </view>
      </view>
    </view>
    
    <!-- 添加记录弹窗 -->
    <uni-popup ref="addPopup" type="bottom">
      <view class="add-panel">
        <view class="panel-header">
          <text class="panel-title">添加{{currentType.name}}记录</text>
          <text class="close-btn" @tap="closeAddPanel">×</text>
        </view>
        
        <view class="add-content">
          <view class="input-group">
            <text class="input-label">{{currentType.name}}</text>
            <input 
              type="digit" 
              v-model="newRecord.value" 
              :placeholder="`请输入${currentType.name}`" 
              class="input"
            />
            <text class="input-unit">{{currentType.unit}}</text>
          </view>
          
          <view class="input-group">
            <text class="input-label">时间</text>
            <picker 
              mode="date" 
              :value="newRecord.date" 
              @change="onDateChange"
              class="date-picker"
            >
              <view class="picker-value">{{newRecord.date}}</view>
            </picker>
          </view>
          
          <view class="input-group">
            <text class="input-label">备注</text>
            <input 
              type="text" 
              v-model="newRecord.note" 
              placeholder="请输入备注(选填)" 
              class="input"
            />
          </view>
          
          <button class="save-btn" @tap="saveRecord">保存</button>
        </view>
      </view>
    </uni-popup>
  </view>
</template>

<script>
import healthCard from '@/components/health-card/health-card.vue';
import dataChart from '@/components/data-chart/data-chart.vue';
import uniPopup from '@/components/uni-popup/uni-popup.vue';
import { formatTime } from '@/utils/date.js';
import { mapState, mapActions } from 'vuex';

export default {
  components: {
    healthCard,
    dataChart,
    uniPopup
  },
  data() {
    return {
      healthTypes: [
        { type: 'weight', name: '体重', unit: 'kg' },
        { type: 'bloodPressure', name: '血压', unit: 'mmHg' },
        { type: 'bloodSugar', name: '血糖', unit: 'mmol/L' },
        { type: 'heartRate', name: '心率', unit: 'bpm' },
        { type: 'steps', name: '步数', unit: '步' }
      ],
      currentTypeIndex: 0,
      timeRange: 'week',
      chartData: [],
      recordList: [],
      newRecord: {
        value: '',
        date: this.formatDate(new Date()),
        note: ''
      }
    }
  },
  computed: {
    ...mapState('health', ['healthData']),
    
    // 当前选中的健康数据类型
    currentType() {
      return this.healthTypes[this.currentTypeIndex];
    },
    
    // 健康数据卡片
    healthCards() {
      return this.healthTypes.map(type => {
        const data = this.healthData[type.type] || [];
        const latest = data.length > 0 ? data[0] : null;
        
        return {
          type: type.type,
          name: type.name,
          value: latest ? latest.value : '--',
          unit: type.unit,
          trend: this.calculateTrend(type.type),
          time: latest ? latest.time : null
        };
      });
    }
  },
  onLoad() {
    // 加载健康数据
    this.loadHealthData();
  },
  methods: {
    ...mapActions('health', ['getHealthData', 'addHealthRecord']),
    
    // 加载健康数据
    async loadHealthData() {
      try {
        await this.getHealthData();
        this.updateChartData();
        this.updateRecordList();
      } catch (e) {
        console.error('加载健康数据失败', e);
        uni.showToast({
          title: '加载数据失败',
          icon: 'none'
        });
      }
    },
    
    // 显示详情
    showDetail(type) {
      const index = this.healthTypes.findIndex(item => item.type === type);
      if (index !== -1) {
        this.currentTypeIndex = index;
        this.updateChartData();
        this.updateRecordList();
      }
    },
    
    // 切换时间范围
    changeTimeRange(range) {
      this.timeRange = range;
      this.updateChartData();
    },
    
    // 更新图表数据
    updateChartData() {
      const type = this.currentType.type;
      const data = this.healthData[type] || [];
      
      // 根据时间范围筛选数据
      const now = new Date();
      let startTime;
      
      switch (this.timeRange) {
        case 'week':
          startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
          break;
        case 'month':
          startTime = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
          break;
        case 'year':
          startTime = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
          break;
      }
      
      // 筛选时间范围内的数据
      const filteredData = data.filter(item => new Date(item.time) >= startTime);
      
      // 格式化图表数据
      this.chartData = filteredData.map(item => ({
        time: this.formatChartTime(item.time),
        value: item.value
      })).reverse();
    },
    
    // 更新记录列表
    updateRecordList() {
      const type = this.currentType.type;
      this.recordList = (this.healthData[type] || []).slice(0, 20);
    },
    
    // 计算趋势
    calculateTrend(type) {
      const data = this.healthData[type] || [];
      if (data.length < 2) return 'stable';
      
      const latest = data[0].value;
      const previous = data[1].value;
      
      if (latest > previous) return 'up';
      if (latest < previous) return 'down';
      return 'stable';
    },
    
    // 获取状态样式
    getStatusClass(value) {
      const type = this.currentType.type;
      
      // 根据不同类型设置不同的正常范围
      let isNormal = true;
      
      switch (type) {
        case 'weight':
          // 假设BMI在18.5-24之间为正常
          // 这里简化处理,实际应用中应考虑身高等因素
          isNormal = value >= 50 && value <= 70;
          break;
        case 'bloodPressure':
          // 假设收缩压90-140,舒张压60-90为正常
          isNormal = value >= 90 && value <= 140;
          break;
        case 'bloodSugar':
          // 假设空腹血糖3.9-6.1为正常
          isNormal = value >= 3.9 && value <= 6.1;
          break;
        case 'heartRate':
          // 假设心率60-100为正常
          isNormal = value >= 60 && value <= 100;
          break;
      }
      
      return isNormal ? 'normal' : 'abnormal';
    },
    
    // 获取状态文本
    getStatusText(value) {
      const statusClass = this.getStatusClass(value);
      return statusClass === 'normal' ? '正常' : '异常';
    },
    
    // 显示添加记录弹窗
    showAddModal() {
      this.newRecord = {
        value: '',
        date: this.formatDate(new Date()),
        note: ''
      };
      this.$refs.addPopup.open();
    },
    
    // 关闭添加记录弹窗
    closeAddPanel() {
      this.$refs.addPopup.close();
    },
    
    // 日期选择变更
    onDateChange(e) {
      this.newRecord.date = e.detail.value;
    },
    
    // 保存记录
    async saveRecord() {
      if (!this.newRecord.value) {
        uni.showToast({
          title: `请输入${this.currentType.name}`,
          icon: 'none'
        });
        return;
      }
      
      try {
        const value = parseFloat(this.newRecord.value);
        if (isNaN(value)) {
          uni.showToast({
            title: '请输入有效数值',
            icon: 'none'
          });
          return;
        }
        
        // 构建记录数据
        const record = {
          type: this.currentType.type,
          value,
          time: new Date(`${this.newRecord.date} 00:00:00`).getTime(),
          note: this.newRecord.note
        };
        
        // 添加记录
        await this.addHealthRecord(record);
        
        // 更新图表和列表
        this.updateChartData();
        this.updateRecordList();
        
        // 关闭弹窗
        this.closeAddPanel();
        
        uni.showToast({
          title: '添加成功',
          icon: 'success'
        });
      } catch (e) {
        console.error('添加记录失败', e);
        uni.showToast({
          title: '添加失败,请重试',
          icon: 'none'
        });
      }
    },
    
    // 格式化时间
    formatTime(timestamp) {
      return formatTime(timestamp);
    },
    
    // 格式化日期
    formatDate(date) {
      const year = date.getFullYear();
      const month = String(date.getMonth() + 1).padStart(2, '0');
      const day = String(date.getDate()).padStart(2, '0');
      return `${year}-${month}-${day}`;
    },
    
    // 格式化图表时间
    formatChartTime(timestamp) {
      const date = new Date(timestamp);
      
      switch (this.timeRange) {
        case 'week':
          return `${date.getMonth() + 1}/${date.getDate()}`;
        case 'month':
          return `${date.getMonth() + 1}/${date.getDate()}`;
        case 'year':
          return `${date.getMonth() + 1}月`;
      }
    }
  }
}
</script>

3.2 在线问诊功能

在线问诊功能允许用户与医生进行远程咨询,包括选择科室、医生和发起咨询等功能。

问诊状态管理

js
// store/consult.js
export default {
  namespaced: true,
  state: {
    departments: [],
    doctors: [],
    consultations: [],
    loading: false
  },
  mutations: {
    SET_DEPARTMENTS(state, departments) {
      state.departments = departments;
    },
    SET_DOCTORS(state, doctors) {
      state.doctors = doctors;
    },
    SET_CONSULTATIONS(state, consultations) {
      state.consultations = consultations;
    },
    ADD_CONSULTATION(state, consultation) {
      state.consultations.unshift(consultation);
    },
    SET_LOADING(state, status) {
      state.loading = status;
    }
  },
  actions: {
    // 获取科室列表
    async getDepartments({ commit }) {
      try {
        const { result } = await uniCloud.callFunction({
          name: 'consult',
          data: { action: 'getDepartments' }
        });
        commit('SET_DEPARTMENTS', result.data);
        return result.data;
      } catch (error) {
        console.error('获取科室列表失败', error);
        throw error;
      }
    },
    
    // 获取医生列表
    async getDoctors({ commit }, { departmentId, sortBy, page = 1, pageSize = 10 }) {
      commit('SET_LOADING', true);
      
      try {
        const { result } = await uniCloud.callFunction({
          name: 'consult',
          data: { 
            action: 'getDoctors',
            departmentId,
            sortBy,
            page,
            pageSize
          }
        });
        
        if (page === 1) {
          commit('SET_DOCTORS', result.data);
        } else {
          commit('SET_DOCTORS', [...state.doctors, ...result.data]);
        }
        
        return result;
      } catch (error) {
        console.error('获取医生列表失败', error);
        throw error;
      } finally {
        commit('SET_LOADING', false);
      }
    },
    
    // 获取咨询记录
    async getConsultations({ commit }) {
      try {
        const { result } = await uniCloud.callFunction({
          name: 'consult',
          data: { action: 'getConsultations' }
        });
        commit('SET_CONSULTATIONS', result.data);
        return result.data;
      } catch (error) {
        console.error('获取咨询记录失败', error);
        throw error;
      }
    },
    
    // 创建咨询
    async createConsultation({ commit }, data) {
      try {
        const { result } = await uniCloud.callFunction({
          name: 'consult',
          data: { 
            action: 'createConsultation',
            ...data
          }
        });
        
        commit('ADD_CONSULTATION', result.data);
        return result.data;
      } catch (error) {
        console.error('创建咨询失败', error);
        throw error;
      }
    }
  }
};

在线问诊页面实现

vue
<!-- pages/consult/consult.vue -->
<template>
  <view class="consult-page">
    <!-- 科室选择 -->
    <view class="department-section">
      <view class="section-title">选择科室</view>
      <scroll-view scroll-x class="department-scroll">
        <view class="department-list">
          <view 
            class="department-item" 
            :class="{ active: currentDepartment === item.id }"
            v-for="item in departments" 
            :key="item.id"
            @tap="selectDepartment(item.id)"
          >
            <image :src="item.icon" class="department-icon"></image>
            <text class="department-name">{{item.name}}</text>
          </view>
        </view>
      </scroll-view>
    </view>
    
    <!-- 医生列表 -->
    <view class="doctor-section">
      <view class="section-header">
        <text class="section-title">推荐医生</text>
        <view class="filter-box">
          <text 
            class="filter-item" 
            :class="{ active: sortBy === 'rating' }"
            @tap="changeSortBy('rating')"
          >好评优先</text>
          <text 
            class="filter-item" 
            :class="{ active: sortBy === 'consultCount' }"
            @tap="changeSortBy('consultCount')"
          >咨询量优先</text>
        </view>
      </view>
      
      <view class="doctor-list">
        <doctor-item 
          v-for="item in doctorList" 
          :key="item.id"
          :doctor="item"
          @click="goToDoctorDetail(item)"
        ></doctor-item>
      </view>
      
      <!-- 加载更多 -->
      <uni-load-more :status="loadMoreStatus"></uni-load-more>
    </view>
    
    <!-- 快速咨询 -->
    <view class="quick-consult">
      <view class="quick-title">快速咨询</view>
      <view class="quick-desc">描述症状,立即获得医生回复</view>
      <button class="quick-btn" @tap="showQuickConsult">立即咨询</button>
    </view>
    
    <!-- 快速咨询弹窗 -->
    <uni-popup ref="quickPopup" type="center">
      <view class="quick-popup">
        <view class="popup-header">
          <text class="popup-title">快速咨询</text>
          <text class="close-btn" @tap="closeQuickPopup">×</text>
        </view>
        
        <view class="popup-content">
          <textarea 
            v-model="consultContent" 
            placeholder="请详细描述您的症状、持续时间等,以便医生更准确地为您解答" 
            class="consult-textarea"
          ></textarea>
          
          <view class="upload-section">
            <view class="upload-title">上传图片(选填)</view>
            <view class="upload-list">
              <view 
                class="upload-item" 
                v-for="(item, index) in uploadImages" 
                :key="index"
              >
                <image :src="item" mode="aspectFill" class="upload-image"></image>
                <text class="delete-icon" @tap="deleteImage(index)">×</text>
              </view>
              
              <view class="upload-add" @tap="chooseImage" v-if="uploadImages.length < 4">
                <text class="iconfont icon-add"></text>
              </view>
            </view>
          </view>
          
          <button class="submit-btn" @tap="submitConsult">提交咨询</button>
        </view>
      </view>
    </uni-popup>
  </view>
</template>

<script>
import doctorItem from '@/components/doctor-item/doctor-item.vue';
import uniLoadMore from '@/components/uni-load-more/uni-load-more.vue';
import uniPopup from '@/components/uni-popup/uni-popup.vue';
import { mapState, mapActions } from 'vuex';

export default {
  components: {
    doctorItem,
    uniLoadMore,
    uniPopup
  },
  data() {
    return {
      currentDepartment: 'all',
      sortBy: 'rating',
      page: 1,
      loadMoreStatus: 'more', // more, loading, noMore
      consultContent: '',
      uploadImages: []
    }
  },
  computed: {
    ...mapState('consult', ['departments', 'doctors', 'loading']),
    
    doctorList() {
      return this.doctors;
    }
  },
  onLoad() {
    // 初始化数据
    this.initData();
  },
  onReachBottom() {
    // 上拉加载更多
    this.loadMore();
  },
  methods: {
    ...mapActions('consult', ['getDepartments', 'getDoctors', 'createConsultation']),
    
    // 初始化数据
    async initData() {
      uni.showLoading({ title: '加载中' });
      
      try {
        // 加载科室
        await this.getDepartments();
        
        // 加载医生列表
        await this.loadDoctors(true);
      } catch (e) {
        console.error('初始化数据失败', e);
      } finally {
        uni.hideLoading();
      }
    },
    
    // 加载医生列表
    async loadDoctors(refresh = false) {
      if (refresh) {
        this.page = 1;
      }
      
      try {
        this.loadMoreStatus = 'loading';
        
        const result = await this.getDoctors({
          departmentId: this.currentDepartment,
          sortBy: this.sortBy,
          page: this.page
        });
        
        this.page++;
        this.loadMoreStatus = result.hasMore ? 'more' : 'noMore';
      } catch (e) {
        console.error('加载医生列表失败', e);
        this.loadMoreStatus = 'more';
      }
    },
    
    // 选择科室
    selectDepartment(id) {
      if (this.currentDepartment === id) return;
      
      this.currentDepartment = id;
      this.loadDoctors(true);
    },
    
    // 切换排序方式
    changeSortBy(sortBy) {
      if (this.sortBy === sortBy) return;
      
      this.sortBy = sortBy;
      this.loadDoctors(true);
    },
    
    // 加载更多
    loadMore() {
      if (this.loadMoreStatus !== 'more') return;
      this.loadDoctors();
    },
    
    // 跳转到医生详情
    goToDoctorDetail(doctor) {
      uni.navigateTo({
        url: `/pages/doctor/detail?id=${doctor.id}`
      });
    },
    
    // 显示快速咨询弹窗
    showQuickConsult() {
      this.consultContent = '';
      this.uploadImages = [];
      this.$refs.quickPopup.open();
    },
    
    // 关闭快速咨询弹窗
    closeQuickPopup() {
      this.$refs.quickPopup.close();
    },
    
    // 选择图片
    chooseImage() {
      uni.chooseImage({
        count: 4 - this.uploadImages.length,
        sizeType: ['compressed'],
        sourceType: ['album', 'camera'],
        success: (res) => {
          this.uploadImages = [...this.uploadImages, ...res.tempFilePaths];
        }
      });
    },
    
    // 删除图片
    deleteImage(index) {
      this.uploadImages.splice(index, 1);
    },
    
    // 提交咨询
    async submitConsult() {
      if (!this.consultContent.trim()) {
        uni.showToast({
          title: '请描述您的症状',
          icon: 'none'
        });
        return;
      }
      
      uni.showLoading({ title: '提交中' });
      
      try {
        // 上传图片
        let imageUrls = [];
        if (this.uploadImages.length > 0) {
          imageUrls = await this.uploadImages();
        }
        
        // 创建咨询
        const consultation = await this.createConsultation({
          content: this.consultContent,
          images: imageUrls,
          type: 'quick'
        });
        
        // 关闭弹窗
        this.closeQuickPopup();
        
        uni.showToast({
          title: '提交成功',
          icon: 'success'
        });
        
        // 跳转到咨询详情
        setTimeout(() => {
          uni.navigateTo({
            url: `/pages/consult/detail?id=${consultation.id}`
          });
        }, 1500);
      } catch (e) {
        console.error('提交咨询失败', e);
        uni.showToast({
          title: '提交失败,请重试',
          icon: 'none'
        });
      } finally {
        uni.hideLoading();
      }
    },
    
    // 上传图片
    async uploadImages() {
      return new Promise((resolve, reject) => {
        const uploadTasks = this.uploadImages.map(image => {
          return new Promise((resolveUpload, rejectUpload) => {
            uni.uploadFile({
              url: this.$api.baseUrl + '/api/upload',
              filePath: image,
              name: 'file',
              success: (uploadRes) => {
                const data = JSON.parse(uploadRes.data);
                if (data.code === 0) {
                  resolveUpload(data.data.url);
                } else {
                  rejectUpload(new Error(data.message || '上传失败'));
                }
              },
              fail: (err) => {
                rejectUpload(err);
              }
            });
          });
        });
        
        Promise.all(uploadTasks)
          .then(urls => {
            resolve(urls);
          })
          .catch(err => {
            reject(err);
          });
      });
    }
  }
}
</script>

3.3 预约挂号功能

预约挂号功能允许用户在线预约医院科室和医生门诊,提高就医效率。

预约挂号页面实现

vue
<!-- pages/hospital/hospital.vue -->
<template>
  <view class="hospital-page">
    <!-- 搜索栏 -->
    <view class="search-bar">
      <text class="iconfont icon-search"></text>
      <input 
        type="text" 
        v-model="searchKeyword" 
        placeholder="搜索医院/科室" 
        confirm-type="search"
        @confirm="searchHospital"
        class="search-input"
      />
    </view>
    
    <!-- 定位 -->
    <view class="location-bar">
      <text class="iconfont icon-location"></text>
      <text class="location-text">{{location || '定位中...'}}</text>
      <text class="change-btn" @tap="changeLocation">切换</text>
    </view>
    
    <!-- 医院列表 -->
    <view class="hospital-list">
      <view 
        class="hospital-item" 
        v-for="item in hospitalList" 
        :key="item.id"
        @tap="goToHospitalDetail(item)"
      >
        <image :src="item.logo" class="hospital-logo"></image>
        <view class="hospital-info">
          <view class="hospital-name">{{item.name}}</view>
          <view class="hospital-level">{{item.level}}</view>
          <view class="hospital-address">
            <text class="iconfont icon-location"></text>
            <text class="address-text">{{item.address}}</text>
            <text class="distance">{{item.distance}}km</text>
          </view>
        </view>
      </view>
    </view>
    
    <!-- 加载更多 -->
    <uni-load-more :status="loadMoreStatus"></uni-load-more>
  </view>
</template>

<script>
import uniLoadMore from '@/components/uni-load-more/uni-load-more.vue';

export default {
  components: {
    uniLoadMore
  },
  data() {
    return {
      searchKeyword: '',
      location: '',
      hospitalList: [],
      page: 1,
      loadMoreStatus: 'more', // more, loading, noMore
      latitude: 0,
      longitude: 0
    }
  },
  onLoad() {
    // 获取位置
    this.getLocation();
  },
  onReachBottom() {
    // 上拉加载更多
    this.loadMore();
  },
  methods: {
    // 获取位置
    getLocation() {
      uni.getLocation({
        type: 'gcj02',
        success: (res) => {
          this.latitude = res.latitude;
          this.longitude = res.longitude;
          
          // 逆地理编码获取位置信息
          this.getLocationName(res.latitude, res.longitude);
          
          // 加载附近医院
          this.loadHospitals(true);
        },
        fail: () => {
          uni.showToast({
            title: '获取位置失败,请检查权限设置',
            icon: 'none'
          });
          
          // 使用默认位置
          this.location = '北京市';
          
          // 加载医院
          this.loadHospitals(true);
        }
      });
    },
    
    // 获取位置名称
    getLocationName(latitude, longitude) {
      uni.request({
        url: 'https://apis.map.qq.com/ws/geocoder/v1/',
        data: {
          location: `${latitude},${longitude}`,
          key: 'YOUR_MAP_KEY' // 实际应用中需要替换为真实的地图API密钥
        },
        success: (res) => {
          if (res.data.status === 0) {
            const address = res.data.result.address_component;
            this.location = `${address.city}${address.district}`;
          }
        }
      });
    },
    
    // 切换位置
    changeLocation() {
      uni.chooseLocation({
        success: (res) => {
          this.location = res.name || res.address;
          this.latitude = res.latitude;
          this.longitude = res.longitude;
          
          // 重新加载医院
          this.loadHospitals(true);
        }
      });
    },
    
    // 加载医院列表
    async loadHospitals(refresh = false) {
      if (refresh) {
        this.page = 1;
      }
      
      this.loadMoreStatus = 'loading';
      
      try {
        const { result } = await uniCloud.callFunction({
          name: 'hospital',
          data: { 
            action: 'getHospitals',
            latitude: this.latitude,
            longitude: this.longitude,
            keyword: this.searchKeyword,
            page: this.page,
            pageSize: 10
          }
        });
        
        const list = result.data || [];
        
        if (refresh) {
          this.hospitalList = list;
        } else {
          this.hospitalList = [...this.hospitalList, ...list];
        }
        
        this.page++;
        this.loadMoreStatus = list.length === 10 ? 'more' : 'noMore';
      } catch (e) {
        console.error('加载医院列表失败', e);
        this.loadMoreStatus = 'more';
        
        uni.showToast({
          title: '加载失败,请重试',
          icon: 'none'
        });
      }
    },
    
    // 搜索医院
    searchHospital() {
      this.loadHospitals(true);
    },
    
    // 加载更多
    loadMore() {
      if (this.loadMoreStatus !== 'more') return;
      this.loadHospitals();
    },
    
    // 跳转到医院详情
    goToHospitalDetail(hospital) {
      uni.navigateTo({
        url: `/pages/hospital/detail?id=${hospital.id}`
      });
    }
  }
}
</script>

4. 应用优化与最佳实践

4.1 性能优化

医疗健康应用需要处理大量的健康数据和医疗信息,性能优化至关重要。

数据缓存策略

js
// utils/cache.js
/**
 * 缓存管理工具
 * 支持设置过期时间、版本控制等功能
 */
export default {
  /**
   * 设置缓存
   * @param {String} key 缓存键
   * @param {*} data 缓存数据
   * @param {Number} expires 过期时间(秒),默认7天
   */
  set(key, data, expires = 7 * 24 * 60 * 60) {
    const cache = {
      data,
      expires: Date.now() + expires * 1000,
      version: this.version
    };
    uni.setStorageSync(key, JSON.stringify(cache));
  },
  
  /**
   * 获取缓存
   * @param {String} key 缓存键
   * @returns {*} 缓存数据,过期或不存在返回null
   */
  get(key) {
    const cacheStr = uni.getStorageSync(key);
    if (!cacheStr) return null;
    
    try {
      const cache = JSON.parse(cacheStr);
      
      // 检查版本
      if (cache.version !== this.version) {
        uni.removeStorageSync(key);
        return null;
      }
      
      // 检查是否过期
      if (cache.expires < Date.now()) {
        uni.removeStorageSync(key);
        return null;
      }
      
      return cache.data;
    } catch (e) {
      uni.removeStorageSync(key);
      return null;
    }
  },
  
  /**
   * 移除缓存
   * @param {String} key 缓存键
   */
  remove(key) {
    uni.removeStorageSync(key);
  },
  
  /**
   * 清空所有缓存
   */
  clear() {
    uni.clearStorageSync();
  },
  
  // 缓存版本,用于控制缓存更新
  version: '1.0.0'
};

图片懒加载与优化

vue
<!-- components/lazy-image/lazy-image.vue -->
<template>
  <image 
    :src="showImage ? src : placeholder" 
    :mode="mode" 
    :lazy-load="true"
    @load="onImageLoad"
    @error="onImageError"
    :class="{ 'fade-in': showImage && loaded }"
    :style="imageStyle"
  ></image>
</template>

<script>
export default {
  props: {
    src: {
      type: String,
      required: true
    },
    mode: {
      type: String,
      default: 'aspectFill'
    },
    placeholder: {
      type: String,
      default: '/static/images/placeholder.png'
    },
    width: {
      type: [Number, String],
      default: '100%'
    },
    height: {
      type: [Number, String],
      default: 'auto'
    }
  },
  data() {
    return {
      showImage: false,
      loaded: false,
      error: false
    }
  },
  computed: {
    imageStyle() {
      return {
        width: typeof this.width === 'number' ? `${this.width}rpx` : this.width,
        height: typeof this.height === 'number' ? `${this.height}rpx` : this.height
      };
    }
  },
  mounted() {
    // 创建一个交叉观察器
    this.createIntersectionObserver();
  },
  beforeDestroy() {
    // 销毁交叉观察器
    if (this.observer) {
      this.observer.disconnect();
    }
  },
  methods: {
    // 创建交叉观察器
    createIntersectionObserver() {
      this.observer = uni.createIntersectionObserver(this);
      
      this.observer
        .relativeToViewport()
        .observe('.lazy-image', (res) => {
          if (res.intersectionRatio > 0) {
            // 图片进入可视区域
            this.showImage = true;
            
            // 停止观察
            this.observer.disconnect();
            this.observer = null;
          }
        });
    },
    
    // 图片加载完成
    onImageLoad() {
      this.loaded = true;
      this.$emit('load');
    },
    
    // 图片加载失败
    onImageError() {
      this.error = true;
      this.$emit('error');
    }
  }
}
</script>

<style>
.fade-in {
  animation: fadeIn 0.5s;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
</style>

4.2 安全措施

医疗健康应用涉及用户敏感数据,安全措施尤为重要。

数据加密

js
// utils/crypto.js
import CryptoJS from 'crypto-js';

/**
 * 数据加密工具
 */
export default {
  /**
   * 加密数据
   * @param {*} data 要加密的数据
   * @param {String} key 加密密钥
   * @returns {String} 加密后的字符串
   */
  encrypt(data, key) {
    const dataStr = typeof data === 'object' ? JSON.stringify(data) : String(data);
    return CryptoJS.AES.encrypt(dataStr, key).toString();
  },
  
  /**
   * 解密数据
   * @param {String} ciphertext 加密后的字符串
   * @param {String} key 解密密钥
   * @returns {*} 解密后的数据
   */
  decrypt(ciphertext, key) {
    try {
      const bytes = CryptoJS.AES.decrypt(ciphertext, key);
      const decryptedData = bytes.toString(CryptoJS.enc.Utf8);
      
      try {
        // 尝试解析为JSON
        return JSON.parse(decryptedData);
      } catch (e) {
        // 不是JSON格式,直接返回字符串
        return decryptedData;
      }
    } catch (e) {
      console.error('解密失败', e);
      return null;
    }
  }
};

权限管理

js
// utils/permission.js
/**
 * 权限管理工具
 */
export default {
  /**
   * 检查是否已登录
   * @returns {Boolean} 是否已登录
   */
  isLoggedIn() {
    return Boolean(uni.getStorageSync('token'));
  },
  
  /**
   * 检查是否有指定权限
   * @param {String} permission 权限名称
   * @returns {Boolean} 是否有权限
   */
  hasPermission(permission) {
    const userInfo = uni.getStorageSync('userInfo');
    if (!userInfo) return false;
    
    try {
      const userInfoObj = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo;
      const permissions = userInfoObj.permissions || [];
      
      return permissions.includes(permission);
    } catch (e) {
      return false;
    }
  },
  
  /**
   * 检查并请求权限
   * @param {String} scope 权限作用域
   * @returns {Promise} 权限请求结果
   */
  async requestPermission(scope) {
    return new Promise((resolve, reject) => {
      uni.getSetting({
        success: (res) => {
          if (res.authSetting[`scope.${scope}`]) {
            // 已授权
            resolve(true);
          } else {
            // 未授权,发起授权请求
            uni.authorize({
              scope: `scope.${scope}`,
              success: () => {
                resolve(true);
              },
              fail: (err) => {
                // 用户拒绝授权
                uni.showModal({
                  title: '提示',
                  content: '需要您授权才能使用该功能',
                  confirmText: '去设置',
                  success: (modalRes) => {
                    if (modalRes.confirm) {
                      // 跳转到设置页
                      uni.openSetting({
                        success: (settingRes) => {
                          if (settingRes.authSetting[`scope.${scope}`]) {
                            resolve(true);
                          } else {
                            reject(new Error('用户拒绝授权'));
                          }
                        },
                        fail: () => {
                          reject(new Error('打开设置页失败'));
                        }
                      });
                    } else {
                      reject(new Error('用户取消授权'));
                    }
                  }
                });
              }
            });
          }
        },
        fail: (err) => {
          reject(err);
        }
      });
    });
  }
};

4.3 用户体验优化

良好的用户体验是医疗健康应用成功的关键因素。

骨架屏实现

vue
<!-- components/skeleton/skeleton.vue -->
<template>
  <view class="skeleton" :class="{ 'animate': animate }">
    <view 
      class="skeleton-item" 
      v-for="(item, index) in items" 
      :key="index"
      :style="{
        width: item.width,
        height: item.height,
        marginTop: item.marginTop,
        marginLeft: item.marginLeft,
        borderRadius: item.borderRadius
      }"
    ></view>
  </view>
</template>

<script>
export default {
  props: {
    items: {
      type: Array,
      required: true
    },
    animate: {
      type: Boolean,
      default: true
    }
  }
}
</script>

<style lang="scss">
.skeleton {
  position: relative;
  overflow: hidden;
  
  .skeleton-item {
    background-color: #f2f2f2;
    position: relative;
    overflow: hidden;
    
    &::after {
      content: '';
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent);
      transform: translateX(-100%);
    }
  }
  
  &.animate {
    .skeleton-item::after {
      animation: shimmer 1.5s infinite;
    }
  }
}

@keyframes shimmer {
  100% {
    transform: translateX(100%);
  }
}
</style>

5. 总结与拓展

5.1 开发要点总结

  1. 数据安全:医疗健康应用涉及用户敏感数据,必须采取严格的安全措施,包括数据加密、权限控制和隐私保护。

  2. 性能优化:健康数据可视化和医疗资源查询需要良好的性能支持,应采用合理的缓存策略、懒加载技术和按需加载。

  3. 用户体验:医疗健康应用的用户群体多样,应设计简洁直观的界面,提供清晰的操作引导和反馈。

  4. 跨端适配:不同设备和平台上的用户体验应保持一致,uni-app的跨端能力可以很好地解决这一问题。

  5. 数据可视化:健康数据的图表展示需要直观清晰,帮助用户理解自己的健康状况。

5.2 功能拓展方向

  1. AI辅助诊断:结合人工智能技术,提供初步的症状分析和健康建议。

  2. 远程监护:通过连接智能设备,实现对老人、患者的远程健康监护。

  3. 社区互动:建立健康社区,让用户分享健康经验和互相鼓励。

  4. 个性化健康计划:根据用户的健康数据和目标,生成个性化的健康改善计划。

  5. 医疗区块链:利用区块链技术保障医疗数据的安全性和可信度。

5.3 商业化思路

  1. 增值服务:提供专业医生一对一咨询、个性化健康报告等付费服务。

  2. 医药电商:整合药品供应链,提供在线购药和送药上门服务。

  3. 保险合作:与保险公司合作,为用户提供健康保险优惠。

  4. 企业健康管理:为企业提供员工健康管理解决方案。

  5. 医疗机构合作:与医院、诊所合作,提供线上预约和转诊服务。

参考资源

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