在当今快节奏的生活中,健康管理变得越来越重要。本文将带领大家使用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官方文档或加入开发者社区讨论。祝你编码愉快!

