Uniapp开发教程:构建跨平台健康打卡应用 | 前端实战指南

2025-09-04 0 645

在当今快节奏的生活中,健康管理变得越来越重要。本文将带领大家使用Uniapp框架开发一个功能完整的健康打卡应用,该应用可以同时运行在iOS、Android、Web以及各种小程序平台。通过这个实战项目,你将掌握Uniapp的核心开发技巧和最佳实践。

一、项目概述与功能设计

我们的健康打卡应用将包含以下核心功能:

  • 用户注册与登录系统
  • 每日健康状态打卡(体温、症状记录)
  • 健康数据统计与图表展示
  • 打卡提醒与消息通知
  • 多平台适配与数据同步

首先,我们在HBuilder X中创建一个新的Uniapp项目:

  1. 打开HBuilder X,选择”文件” -> “新建” -> “项目”
  2. 选择”uni-app”项目类型,输入项目名称”HealthCheckIn”
  3. 选择”默认模板”,点击”创建”

二、项目结构设计与配置

创建完成后,我们需要规划项目结构:

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
    });
  }
}
    

八、应用发布与部署

完成开发后,我们可以将应用发布到多个平台:

  1. 小程序发布:在HBuilder X中生成小程序包,上传至微信开发者工具
  2. APP发布:使用云打包或本地打包生成安装包
  3. H5发布:部署到服务器并通过浏览器访问

发布前需要进行以下优化:

  • 压缩图片和静态资源
  • 启用运行时代码压缩
  • 配置合适的启动图和应用图标
  • 测试各平台兼容性

结语

通过本教程,我们完成了一个功能完整的跨平台健康打卡应用。这个项目涵盖了Uniapp开发的核心概念,包括页面布局、组件开发、状态管理、API调用、多平台适配和云开发等。希望这个实战项目能帮助你掌握Uniapp开发技能,并在实际项目中灵活运用。

如果你想进一步扩展这个应用,可以考虑添加以下功能:

  • 健康数据分析与报告生成
  • 家人健康数据共享与关注
  • 地理位置打卡与轨迹记录
  • 健康知识推送与学习模块

开发过程中遇到问题,可以查阅Uniapp官方文档或加入开发者社区讨论。祝你编码愉快!

Uniapp开发教程:构建跨平台健康打卡应用 | 前端实战指南
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 uniapp Uniapp开发教程:构建跨平台健康打卡应用 | 前端实战指南 https://www.taomawang.com/web/uniapp/1025.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务