在当今快节奏的生活中,健康管理变得越来越重要。本文将带领大家使用Uniapp框架开发一个功能完整的健康打卡应用,该应用可以同时运行在iOS、Android、Web以及各种小程序平台。通过这个实战项目,你将掌握Uniapp的核心开发技巧和最佳实践。
一、项目概述与功能设计
我们的健康打卡应用将包含以下核心功能:
- 用户注册与登录系统
- 每日健康状态打卡(体温、症状记录)
- 健康数据统计与图表展示
- 打卡提醒与消息通知
- 多平台适配与数据同步
首先,我们在HBuilder X中创建一个新的Uniapp项目:
- 打开HBuilder X,选择”文件” -> “新建” -> “项目”
- 选择”uni-app”项目类型,输入项目名称”HealthCheckIn”
- 选择”默认模板”,点击”创建”
二、项目结构设计与配置
创建完成后,我们需要规划项目结构:
HealthCheckIn/ ├── common/ // 公共资源 │ ├── api.js // 接口封装 │ └── util.js // 工具函数 ├── components/ // 自定义组件 ├── pages/ // 页面文件 │ ├── index/ // 首页 │ ├── login/ // 登录页 │ ├── checkin/ // 打卡页 │ └── stats/ // 统计页 ├── static/ // 静态资源 ├── store/ // 状态管理 ├── App.vue // 应用配置 ├── main.js // 入口文件 ├── manifest.json // 应用配置 └── pages.json // 页面配置
三、用户登录与注册实现
首先,我们实现用户系统的前端界面。创建pages/login/login.vue:
<template> <view class="login-container"> <view class="form-container"> <view class="title">健康打卡</view> <view class="input-group" v-if="!isRegister"> <input class="input" v-model="loginForm.username" placeholder="请输入用户名" /> <input class="input" v-model="loginForm.password" placeholder="请输入密码" password /> </view> <view class="input-group" v-else> <input class="input" v-model="registerForm.username" placeholder="请设置用户名" /> <input class="input" v-model="registerForm.password" placeholder="请设置密码" password /> <input class="input" v-model="registerForm.confirmPassword" placeholder="请确认密码" password /> </view> <button class="btn primary" @click="handleSubmit">{{ isRegister ? '注册' : '登录' }}</button> <view class="switch-mode"> <text @click="isRegister = !isRegister"> {{ isRegister ? '已有账号?去登录' : '没有账号?去注册' }} </text> </view> </view> </view> </template> <script> export default { data() { return { isRegister: false, loginForm: { username: '', password: '' }, registerForm: { username: '', password: '', confirmPassword: '' } }; }, methods: { async handleSubmit() { if (this.isRegister) { await this.handleRegister(); } else { await this.handleLogin(); } }, async handleLogin() { // 登录逻辑 try { const res = await this.$api.login(this.loginForm); if (res.code === 200) { uni.setStorageSync('token', res.data.token); uni.showToast({ title: '登录成功' }); uni.switchTab({ url: '/pages/index/index' }); } } catch (error) { uni.showToast({ title: error.message, icon: 'none' }); } }, async handleRegister() { // 注册逻辑 if (this.registerForm.password !== this.registerForm.confirmPassword) { uni.showToast({ title: '两次密码输入不一致', icon: 'none' }); return; } try { const res = await this.$api.register(this.registerForm); if (res.code === 200) { uni.showToast({ title: '注册成功' }); this.isRegister = false; } } catch (error) { uni.showToast({ title: error.message, icon: 'none' }); } } } }; </script>
四、健康打卡功能实现
接下来实现核心的健康打卡功能。创建pages/checkin/checkin.vue:
<template> <view class="checkin-container"> <view class="date-display">{{ currentDate }}</view> <view class="form-card"> <view class="form-item"> <text class="label">体温测量</text> <view class="input-with-unit"> <input class="input" type="number" v-model="formData.temperature" placeholder="请输入体温" /> <text class="unit">°C</text> </view> </view> <view class="form-item"> <text class="label">是否有症状</text> <view class="symptom-options"> <view class="option-btn" :class="{ active: formData.hasSymptom === true }" @click="formData.hasSymptom = true" > 是 </view> <view class="option-btn" :class="{ active: formData.hasSymptom === false }" @click="formData.hasSymptom = false" > 否 </view> </view> </view> <view class="form-item" v-if="formData.hasSymptom"> <text class="label">症状描述</text> <textarea class="textarea" v-model="formData.symptomDesc" placeholder="请描述具体症状" maxlength="200" /> </view> <view class="form-item"> <text class="label">今日心情</text> <view class="mood-selector"> <view v-for="(mood, index) in moodOptions" :key="index" class="mood-option" :class="{ active: formData.mood === mood.value }" @click="formData.mood = mood.value" > <image class="mood-icon" :src="mood.icon" /> <text>{{ mood.label }}</text> </view> </view> </view> <button class="submit-btn" @click="handleSubmit">提交打卡</button> </view> <view class="history-card" v-if="todayCheckin"> <view class="card-title">今日已打卡</view> <view class="history-item"> <text>体温: {{ todayCheckin.temperature }}°C</text> <text>状态: {{ todayCheckin.hasSymptom ? '有症状' : '无症状' }}</text> </view> <view class="checkin-time">打卡时间: {{ todayCheckin.createTime }}</view> </view> </view> </template> <script> import { formatDate } from '@/common/util.js'; export default { data() { return { currentDate: formatDate(new Date(), 'yyyy年MM月dd日'), formData: { temperature: null, hasSymptom: false, symptomDesc: '', mood: 'normal' }, moodOptions: [ { value: 'happy', label: '开心', icon: '/static/mood/happy.png' }, { value: 'normal', label: '一般', icon: '/static/mood/normal.png' }, { value: 'sad', label: '低落', icon: '/static/mood/sad.png' } ], todayCheckin: null }; }, async onLoad() { await this.checkTodayCheckin(); }, methods: { async checkTodayCheckin() { try { const res = await this.$api.getTodayCheckin(); if (res.code === 200 && res.data) { this.todayCheckin = res.data; } } catch (error) { console.error('获取今日打卡记录失败', error); } }, async handleSubmit() { if (!this.formData.temperature) { uni.showToast({ title: '请填写体温', icon: 'none' }); return; } if (this.formData.temperature 42) { uni.showToast({ title: '体温值异常,请重新测量', icon: 'none' }); return; } try { const res = await this.$api.submitCheckin(this.formData); if (res.code === 200) { uni.showToast({ title: '打卡成功' }); this.todayCheckin = res.data; // 通知首页更新打卡状态 uni.$emit('checkinStatusChanged'); } } catch (error) { uni.showToast({ title: error.message, icon: 'none' }); } } } }; </script>
五、数据统计与图表展示
健康数据的可视化是应用的重要功能。我们使用uCharts插件实现图表展示:
<template> <view class="stats-container"> <view class="time-filter"> <text class="filter-item" :class="{ active: currentFilter === 'week' }" @click="changeFilter('week')" >近一周</text> <text class="filter-item" :class="{ active: currentFilter === 'month' }" @click="changeFilter('month')" >近一月</text> </view> <view class="chart-card"> <view class="card-title">体温趋势图</view> <qiun-data-charts type="line" :chartData="temperatureChartData" :opts="chartOpts" /> </view> <view class="chart-card"> <view class="card-title">心情分布</view> <qiun-data-charts type="pie" :chartData="moodChartData" :opts="pieChartOpts" /> </view> <view class="stats-summary"> <view class="summary-item"> <text class="value">{{ statsData.continuousDays }}</text> <text class="label">连续打卡天数</text> </view> <view class="summary-item"> <text class="value">{{ statsData.avgTemperature }}°C</text> <text class="label">平均体温</text> </view> <view class="summary-item"> <text class="value">{{ statsData.normalDays }}</text> <text class="label">无症状天数</text> </view> </view> </view> </template> <script> export default { data() { return { currentFilter: 'week', temperatureChartData: {}, moodChartData: {}, statsData: { continuousDays: 0, avgTemperature: 0, normalDays: 0 }, chartOpts: { color: ['#1890FF'], padding: [15, 15, 0, 15], enableScroll: false, legend: {}, xAxis: { disableGrid: true }, yAxis: { gridType: 'dash', dashLength: 2 }, extra: { line: { type: 'straight', width: 2 } } }, pieChartOpts: { padding: [15, 15, 0, 15], extra: { pie: { activeOpacity: 0.5, activeRadius: 10, offsetAngle: 0, labelWidth: 15, border: true, borderWidth: 1, borderColor: '#FFFFFF' } } } }; }, async onLoad() { await this.loadChartData(); await this.loadStatsData(); }, methods: { async changeFilter(filter) { this.currentFilter = filter; await this.loadChartData(); }, async loadChartData() { try { const res = await this.$api.getCheckinStats(this.currentFilter); if (res.code === 200) { this.temperatureChartData = this.formatTemperatureData(res.data.temperature); this.moodChartData = this.formatMoodData(res.data.mood); } } catch (error) { console.error('加载图表数据失败', error); } }, async loadStatsData() { try { const res = await this.$api.getStatsSummary(); if (res.code === 200) { this.statsData = res.data; } } catch (error) { console.error('加载统计数据失败', error); } }, formatTemperatureData(temperatureData) { // 格式化温度数据为uCharts需要的格式 const categories = []; const series = [{ data: [] }]; temperatureData.forEach(item => { categories.push(item.date); series[0].data.push(item.temperature); }); return { categories, series }; }, formatMoodData(moodData) { // 格式化心情数据为uCharts需要的格式 const series = []; Object.keys(moodData).forEach(mood => { series.push({ name: this.getMoodLabel(mood), data: moodData[mood] }); }); return { series }; }, getMoodLabel(moodValue) { const moodMap = { 'happy': '开心', 'normal': '一般', 'sad': '低落' }; return moodMap[moodValue] || '未知'; } } }; </script>
六、多平台适配与优化
Uniapp的强大之处在于一套代码多端发布。我们需要针对不同平台进行适配:
// 平台特定代码示例 export default { methods: { // 设置提醒功能 setReminder() { // #ifdef APP-PLUS this.setAppReminder(); // #endif // #ifdef MP-WEIXIN this.setMpReminder(); // #endif // #ifdef H5 this.setH5Reminder(); // #endif }, // APP端设置提醒 setAppReminder() { // 使用原生API创建本地通知 plus.push.createMessage('健康打卡提醒', '记得每天打卡哦', {}); }, // 微信小程序端设置提醒 setMpReminder() { // 使用微信订阅消息功能 wx.requestSubscribeMessage({ tmplIds: ['您的模板ID'], success(res) { console.log('订阅成功', res); } }); }, // H5端设置提醒 setH5Reminder() { // 使用浏览器通知API if ('Notification' in window && Notification.permission === 'granted') { new Notification('健康打卡提醒', { body: '记得每天打卡哦' }); } } } };
七、数据存储与云同步
为了实现多端数据同步,我们可以使用uniCloud提供的云开发能力:
// 云函数示例 - 提交打卡记录 'use strict'; exports.main = async (event, context) => { const { userId, checkinData } = event; // 验证用户身份 const userInfo = await uniCloud.getUserInfo(); if (userInfo.uid !== userId) { return { code: 403, message: '无权限操作' }; } // 检查今日是否已打卡 const db = uniCloud.database(); const today = new Date(); today.setHours(0, 0, 0, 0); const existingRecord = await db.collection('checkin_records') .where({ user_id: userId, create_time: db.command.gte(today) }) .get(); if (existingRecord.data.length > 0) { return { code: 400, message: '今日已打卡,请勿重复提交' }; } // 插入打卡记录 const record = { user_id: userId, temperature: checkinData.temperature, has_symptom: checkinData.hasSymptom, symptom_desc: checkinData.symptomDesc || '', mood: checkinData.mood, create_time: new Date(), update_time: new Date() }; const result = await db.collection('checkin_records').add(record); if (result.id) { // 更新用户连续打卡天数 await updateContinuousDays(userId); return { code: 200, data: record, message: '打卡成功' }; } else { return { code: 500, message: '打卡失败,请重试' }; } }; // 更新连续打卡天数 async function updateContinuousDays(userId) { const db = uniCloud.database(); const user = await db.collection('users').doc(userId).get(); if (user.data.length === 0) return; const userData = user.data[0]; const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); yesterday.setHours(0, 0, 0, 0); // 检查昨天是否打卡 const yesterdayRecord = await db.collection('checkin_records') .where({ user_id: userId, create_time: db.command.gte(yesterday) }) .get(); if (yesterdayRecord.data.length > 0) { // 昨天有打卡,连续天数+1 await db.collection('users').doc(userId).update({ continuous_days: userData.continuous_days + 1 }); } else { // 昨天没打卡,重置为1 await db.collection('users').doc(userId).update({ continuous_days: 1 }); } }
八、应用发布与部署
完成开发后,我们可以将应用发布到多个平台:
- 小程序发布:在HBuilder X中生成小程序包,上传至微信开发者工具
- APP发布:使用云打包或本地打包生成安装包
- H5发布:部署到服务器并通过浏览器访问
发布前需要进行以下优化:
- 压缩图片和静态资源
- 启用运行时代码压缩
- 配置合适的启动图和应用图标
- 测试各平台兼容性
结语
通过本教程,我们完成了一个功能完整的跨平台健康打卡应用。这个项目涵盖了Uniapp开发的核心概念,包括页面布局、组件开发、状态管理、API调用、多平台适配和云开发等。希望这个实战项目能帮助你掌握Uniapp开发技能,并在实际项目中灵活运用。
如果你想进一步扩展这个应用,可以考虑添加以下功能:
- 健康数据分析与报告生成
- 家人健康数据共享与关注
- 地理位置打卡与轨迹记录
- 健康知识推送与学习模块
开发过程中遇到问题,可以查阅Uniapp官方文档或加入开发者社区讨论。祝你编码愉快!