医疗健康应用实战案例
本文将介绍如何使用 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 健康数据管理
健康数据管理是医疗健康应用的基础功能,包括各类健康指标的记录、分析和可视化展示。
健康数据状态管理
// 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';
}
}
};
健康数据页面实现
<!-- 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 在线问诊功能
在线问诊功能允许用户与医生进行远程咨询,包括选择科室、医生和发起咨询等功能。
问诊状态管理
// 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;
}
}
}
};
在线问诊页面实现
<!-- 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 预约挂号功能
预约挂号功能允许用户在线预约医院科室和医生门诊,提高就医效率。
预约挂号页面实现
<!-- 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 性能优化
医疗健康应用需要处理大量的健康数据和医疗信息,性能优化至关重要。
数据缓存策略
// 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'
};
图片懒加载与优化
<!-- 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 安全措施
医疗健康应用涉及用户敏感数据,安全措施尤为重要。
数据加密
// 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;
}
}
};
权限管理
// 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 用户体验优化
良好的用户体验是医疗健康应用成功的关键因素。
骨架屏实现
<!-- 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 开发要点总结
数据安全:医疗健康应用涉及用户敏感数据,必须采取严格的安全措施,包括数据加密、权限控制和隐私保护。
性能优化:健康数据可视化和医疗资源查询需要良好的性能支持,应采用合理的缓存策略、懒加载技术和按需加载。
用户体验:医疗健康应用的用户群体多样,应设计简洁直观的界面,提供清晰的操作引导和反馈。
跨端适配:不同设备和平台上的用户体验应保持一致,uni-app的跨端能力可以很好地解决这一问题。
数据可视化:健康数据的图表展示需要直观清晰,帮助用户理解自己的健康状况。
5.2 功能拓展方向
AI辅助诊断:结合人工智能技术,提供初步的症状分析和健康建议。
远程监护:通过连接智能设备,实现对老人、患者的远程健康监护。
社区互动:建立健康社区,让用户分享健康经验和互相鼓励。
个性化健康计划:根据用户的健康数据和目标,生成个性化的健康改善计划。
医疗区块链:利用区块链技术保障医疗数据的安全性和可信度。
5.3 商业化思路
增值服务:提供专业医生一对一咨询、个性化健康报告等付费服务。
医药电商:整合药品供应链,提供在线购药和送药上门服务。
保险合作:与保险公司合作,为用户提供健康保险优惠。
企业健康管理:为企业提供员工健康管理解决方案。
医疗机构合作:与医院、诊所合作,提供线上预约和转诊服务。