基于uniapp+vue的微信吃药提醒小程序开发实战
2026/9/15 8:34:50 网站建设 项目流程

我前前后后做了几个健康类的小程序,说实话,吃药提醒这个需求看着简单,真正做起来涉及的坑一点不比电商项目少。这个项目用的是 uniapp + vue 这套组合,一套代码同时跑微信小程序、H5 和 App,对个人开发者来说性价比很高。这篇文章就围绕"微信小程序 uniapp+vue 个人健康 吃药提醒服药"这个项目,把我从设计到上架的完整思路、关键代码、踩坑记录都整理出来。适合正在做毕设、个人健康类产品、或者第一次用 uniapp 开发微信小程序的同学参考,里面很多细节是官方文档不会写清楚的。

1. 项目整体设计思路

1.1 为什么选 uniapp + vue 而不是原生开发

先说结论:如果目标是微信小程序单平台,原生其实也够用;但如果想兼顾 H5、iOS、Android,甚至以后要打包成 App,uniapp 的优势就很明显了。这个项目的核心用户场景是慢性病患者和家里的老人,他们可能今天在微信里打开小程序,明天在手机浏览器里收到提醒,所以我一开始就把跨端作为刚需。

uniapp 的底层是 vue 语法,我用的组合式 API(vue3 写法)写起来确实比原生小程序的 setData 舒服很多。数据绑定是响应式的,页面逻辑可以像写普通 vue 组件一样拆分子组件,不用在 Page() 里堆一大坨生命周期函数。而且 uniapp 封装了 uni.request、uni.setStorageSync、uni.navigateTo 这些统一 API,同一个项目编译到不同平台时,业务代码几乎不用改。

还要提一个现实原因:原生小程序没有直接可用的"定时本地通知"能力,必须靠订阅消息或者服务端推送。而 uniapp 可以通过 plus(HTML5+) API 在 App 端实现本地通知,微信小程序端走订阅消息,H5 端用浏览器 Notification API。一套业务逻辑,三套触达方案,这种灵活性只有跨端框架能给你。

1.2 页面结构和功能拆分

这个项目不需要复杂的页面,核心就四个:

  • 首页:展示今天的用药计划、服药状态(已服/未服/漏服)、下一次提醒倒计时。
  • 药品列表:所有药品的管理入口,支持增删改查,展示药品名称、剂量、剩余数量。
  • 添加/编辑药单:录入药品信息、选择提醒时间、设置服药频次。
  • 用药记录:按日期查看历史服药记录,方便回顾依从性。

页面少不代表逻辑简单。吃药提醒的核心难点不在 UI,而在"时间调度"和"消息触达"这两件事上。时间调度的意思是,用户设置了每天 8:00 吃降压药,你要在那一刻把提醒送到用户面前;消息触达的意思是,微信小程序不能像原生 App 一样在后台弹本地通知,你得想办法让用户真的收到提醒。

在动手写代码之前,我建议先把这两个问题想清楚,因为你后面所有代码都是围绕它们展开的。功能拆分上我做了个原则:页面只管数据和交互,提醒逻辑单独抽成一个 utils 模块,这样后续加新平台或者改提醒策略,不会把页面代码搞得一团糟。

2. 核心功能实现:吃药提醒的数据与调度

2.1 药品数据模型设计

数据是这个小程序的地基。我用的本地存储方案,因为个人健康数据量不大,不需要后端数据库,用 uni.setStorageSync + uni.getStorageSync 就够了。但数据模型一定要设计好,否则后面加功能会非常痛苦。

我最终确定了这样的数据模型:

// 药单数据模型 { id: 'med_20250101_001', // 唯一ID,用时间戳+随机数生成 name: '苯磺酸氨氯地平片', // 药品名称 dosage: '5mg', // 单次剂量 frequency: 'daily', // 频次:daily每周|weekly|specificDays weekDays: [1,3,5], // frequency为weekly时生效,周一三五 timeSlots: ['08:00', '20:00'], // 提醒时间点,一天可能吃两次 stockCount: 28, // 剩余片数 stockWarning: 7, // 低于这个数量时提醒补药 startDate: '2025-01-01', // 开始服药日期 endDate: '2025-01-31', // 结束日期,非必填 remindersEnabled: true, // 总开关 createdAt: 1735689600000 }

这里有几个细节值得说明。frequency 字段我没有用简单的"一天几次"而是支持了每周指定星期几,是因为老年人经常有"隔天吃一次"或者"每周一三五吃"的医嘱。weekDays 用数字数组而不是字符串拼接,是为了方便做日期判断——new Date().getDay()返回的就是 0-6 的数字,直接weekDays.includes(day)就能判断今天是否需要吃药。

timeSlots 用字符串'08:00'而不是时间戳,是因为提醒规则是每天重复的,用「时+分」字符串做匹配最直观。判断逻辑也很简单:把当前时间的HH:mm转成字符串,看看在不在 timeSlots 里。

库存那里加了个stockWarning阈值,这是我在实际使用中加的——我妈吃降压药经常忘记看剩几片,直到断药才发现。有了阈值,首页可以显示"xx药品还剩 5 片,注意备药",这个小功能虽然简单,但对健康管理类应用来说价值很高。

2.2 提醒时间怎么算:定时触发的三种思路

这是整个项目最核心的技术点。小程序里的"定时提醒"不像程序员想象的那么简单,主要有三条路,我逐一分析利弊。

第一种:纯前端定时器(setInterval/setTimeout)。在小程序页面存活时可以用,但小程序一旦切后台或者被用户划掉,定时器就失效了。这个方法只适合做"页面内倒计时"这种装饰性功能,不能作为提醒的核心方案。

第二种:微信订阅消息(一次性订阅)。这是微信小程序目前唯一官方支持的"主动触达用户"方式。原理是用户主动授权一次,你就能给他发一条模板消息。授权一次只能发一条,所以如果用户设置了每天 8:00 和 20:00 两个提醒,你得提前让用户点击两次授权,分别对应两条消息。

第三种:服务端定时推送。把用户的提醒规则同步到你的服务器,服务器在指定时间调用微信订阅消息接口下发模板消息。这种方式最可靠,但个人开发者没有服务器时,也可以用云开发(uniCloud 或者微信云开发)里的定时触发器来实现。

我在项目里用的是「订阅消息 + 云函数定时触发器」的组合方案。原因很直接:用户不可能一直开着小程序界面等你发提醒,真正的提醒必须从服务端发起。微信云开发的定时触发器可以精确到分钟,免费额度对个人项目完全够用。后面我会详细讲这个方案怎么落地。

2.3 通知触达:订阅消息 vs 本地通知

这里必须区分两个概念:微信小程序的「订阅消息」和原生 App 的「本地通知」。很多第一次做小程序的人会把这两个搞混,觉得设置了提醒就能像闹钟一样响起来,实际上完全不是一回事。

微信订阅消息的限制非常明确:

  • 一次性订阅消息:用户每次点击授权,只能给你一次发送机会。用完就没了,下次要用户再点。
  • 长期订阅消息:目前只面向特定行业(如医疗、政务)开放申请,普通个人开发者基本申请不到。
  • 用户如果点了"总是保持以上选择,不再询问",那以后授权弹窗都不会再出现,这个坑后面我会细说。

所以我的思路是:把订阅消息当成"当天提醒"的一次性投递手段,用户在添加药单时,我明确告诉他"每天提醒需要每天授权",并且在药单列表里做一个"重新授权提醒"的按钮,方便用户每天顺手点一下。

如果以后打包成 App,就可以用 plus.push 做真正的本地通知,那个体验就接近原生闹钟了。H5 端可以用new Notification()做浏览器通知,但前提是用户得允许网站发送通知,而且浏览器得保持打开状态。跨端开发就是这样,每个平台都有脾气,你只能用不同的姿势去哄。

3. 实操环节:从零搭建关键页面

3.1 创建项目与基础配置

用 HBuilderX 创建 uniapp 项目时,模板我建议选择"默认模板(vue3)"。虽然 vue2 的生态也很成熟,但 vue3 的 Composition API 写起来更干净,而且 uniapp 官方对 vue3 的适配已经很稳定了。

创建完项目后,第一件事是配置manifest.json,这步很多新手会忽略。微信小程序端的配置要点:

{ "mp-weixin": { "appid": "你的小程序appid", "setting": { "urlCheck": false, "es6": true, "postcss": true, "minified": true }, "usingComponents": true, "permission": { "scope.userLocation": { "desc": "用于提供附近药店服务" } } } }

注意appid一定要换成你自己的,在微信公众平台注册小程序后就能拿到。urlCheck在开发阶段可以关掉,否则本地调试时请求 http 接口会被微信拦截;但上线前一定要打开,不然发布审核会被打回。

pages.json里我配置了三个 tabBar 页面(首页、药单、我的)和两个非 tab 页面(添加药单、记录详情)。tabBar 的图标我用的是简单的 PNG 图标,如果你不想自己做图,也可以用字体图标或者直接不加图标,纯文字 tabBar 也是允许的,只是不那么好看。

3.2 药品列表页:增删改查与状态展示

药品列表页是整个应用的入口,用户每天打开小程序第一眼看到的就是它。我用了一个很直观的卡片式布局,每张卡片显示:药品名、剂量、今日服用状态(用不同颜色标签区分)、下一次提醒时间、剩余库存。

核心代码大概是这样的:

<template> <view class="med-card" v-for="med in medList" :key="med.id"> <view class="med-info"> <text class="med-name">{{ med.name }}</text> <text class="med-dosage">{{ med.dosage }}</text> </view> <view class="med-status"> <text class="tag" :class="getStatusClass(med)">{{ getStatusText(med) }}</text> <text class="next-time" v-if="getNextTime(med)">下次 {{ getNextTime(med) }}</text> </view> <view class="med-actions"> <button size="mini" @click="handleEdit(med)">编辑</button> <button size="mini" type="warn" @click="handleDelete(med)">删除</button> </view> </view> </template>

getStatusText这个方法会判断当前时间点该药品是否需要服用。逻辑是:先找到今天该药品的提醒时间点,然后跟当前时间比较,还没到就显示"待服用",到了但用户没确认就显示"待确认",用户确认过就显示"已服用"。判断"是否超出服药时间"我用了一个容忍窗口:超过提醒时间 30 分钟还没确认,就标记为"漏服"。

删除药品时一定要加确认弹窗,用uni.showModal,因为误删药单对用药的人来说是很危险的。同时删除时要联动清理当天已经生成的相关提醒记录。

3.3 添加编辑药单:时间选择的细节

添加药单页面是表单最密集的地方,也是用户操作成本最高的地方。我的设计原则是:默认值尽量合理,用户少点几下就完成录入。

时间选择这里用 uniapp 自带的picker组件非常合适:

<picker mode="time" :value="timeSlot" @change="onTimeChange"> <view class="time-picker">{{ timeSlot || '选择时间' }}</view> </picker>

给用户设置多个服药时间时,我提供了一个"添加时间段"按钮,每次点击往timeSlots数组里 push 一个默认的 '08:00',用户再逐个调整。不建议用复杂的多选控件,老年人操作不来,最简单的时间选择器反而是最友好的。

频次选择我用的是单选按钮组,三个选项:每天、每周指定几天、按周期(比如隔天一次)。选"每周指定几天"时,下面会展开 7 个星期几的按钮,多选。选"按周期"时,会有一个数字输入框,让用户填"每 N 天吃一次"。

库存数量的录入建议用 stepper 组件而不是输入框,避免用户输入非法字符。在保存时做数据校验,包括:药名不能为空、至少设置一个提醒时间、若选择按周期服药则周期天数必须大于 0。校验失败时用uni.showToast给出具体提示,而不是笼统地弹"请检查输入"。

3.4 首页今日概览:让用户一眼看懂该吃啥

首页的定位是"今日用药摘要",用户打开就能看到今天该吃哪几种药、几点吃、哪些已经吃了。

这里有个交互细节值得分享:我做了"服药打卡"按钮,用户在吃完药后点击对应时间点的打卡按钮,状态会变更为"已服用",同时写入本地记录。打卡这个动作非常重要,它不只是一个状态变更,更是用户和产品之间的信任回路——用户点了打卡,系统就知道你吃了,明天就不会再重复提醒这件事。而且打卡数据累积下来,就是非常有价值的用药依从性报告,这些数据可以按周、按月生成统计图,告诉用户"你这周按时服药率是 92%",对慢性病患者来说这种正向反馈非常有激励作用。

首页还有一个"补录提醒"区域,处理的是用户忘记打卡的情况。比如用户上午的药忘记点打卡,下午打开首页时,系统会显示"今天 8:00 的药未确认,是否补录?"点击补录后可以选择"已服用"或"未服用"。这个设计是为了保证历史数据的准确性,因为很多人是到晚上才想起来打开小程序。

3.5 数据存储与版本迁移方案

所有数据都用uni.setStorageSync存储,key 命名要有规范。我的方案是分三个 key:med_list存所有药品、med_records存服药打卡记录、settings存全局设置(比如是否开启提醒总开关)。

// 服药打卡记录 { '2025-01-01_08:00': { medId: 'med_20250101_001', status: 'taken', // taken | missed | skipped takenAt: 1735689600000 // 打卡时间戳 } }

这里用了日期_时间作为 key,好处是可以直接通过键名判断是否已打卡,不用遍历全表。记录会越积越多,我做了个策略:只保留最近 90 天的打卡记录,超过的自动清理,避免本地存储爆掉。小程序本地存储上限是 10MB,按一天几条记录的量级,90 天的数据量完全没问题。

版本迁移是我吃过亏的地方。第一次上线后我又改了数据字段,结果老用户的本地数据和新代码对不上,直接白屏。后来我加了一个简单的版本号机制:

const STORAGE_VERSION = 'v2'; function checkStorageVersion() { const ver = uni.getStorageSync('storage_version'); if (ver !== STORAGE_VERSION) { // 执行迁移逻辑,把老数据结构转成新结构 migrateData(); uni.setStorageSync('storage_version', STORAGE_VERSION); } }

每次数据结构调整就升级版本号,迁移逻辑写在migrateData里。这个方法虽然土,但对没有后端的纯本地存储应用来说,是最可靠的数据保护手段。

4. 订阅消息接入:最容易被坑的一环

4.1 申请模板与参数绑定

订阅消息的第一步是在微信公众平台申请模板。在"功能-订阅消息"里选择模板,比如"用药提醒通知"。申请通过后,你会得到一个模板 ID,类似xxxxx_xxxxx_xxxxx。打开模板详情能看到模板字段,比如药品名称用药时间用药剂量温馨提示

注意:模板字段的 key 是固定的(如thing1time2phrase3),你在代码里填充数据时必须用这个 key。我贴一下发送订阅消息的云函数代码:

// 云函数:sendRemind const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event) => { const { openid, page, data } = event try { const result = await cloud.openapi.subscribeMessage.send({ touser: openid, templateId: '你的模板ID', page: page, data: { thing1: { value: data.medName }, time2: { value: data.time }, thing3: { value: data.dosage }, thing4: { value: data.tips } } }) return { code: 0, result } } catch (e) { return { code: -1, msg: e.message } } }

这里最大的限制是:thing类型字段的值不能超过 20 个字符,time类型必须是YYYY-MM-DD HH:mm:ss格式。药品名称超过 20 个字要截断,时间格式要拼好,这些都是很容易被审核忽略的细节。

4.2 定时触发器:云函数的定时任务配置

云函数配置定时触发器有两种方式:一是在cloudfunctions/sendRemind/config.json里写:

{ "triggers": [ { "name": "remindEveryHour", "type": "timer", "config": "0 0 * * * * *" } ] }

这个 cron 表达式是七段式的秒 分 时 日 月 周 年0 0 * * * * *表示每个小时的整点触发。但实际上这样不够灵活,因为你无法预知用户设置的是什么时间点。

我的方案是:定时触发器只负责"扫描任务",每 10 分钟执行一次,找出接下来 10 分钟内需要发送的提醒,然后逐个发送。云函数里先查数据库里所有用户的提醒规则,算出当前 10 分钟窗口内有哪些提醒到期,再调用订阅消息接口发送。

这里有个请求限额的问题:云函数一次调用最多发多少条订阅消息没有硬性限制,但为了稳妥,我加了一个循环内延迟发送(每次间隔 200ms),避免触发微信的接口频率限制。实测下来这个方案很稳,基本误差控制在 1 分钟以内。

4.3 授权策略:把"一次性"变成"习惯"

订阅消息最大的痛点就是一次性授权。用户配合度低的话,你第二天就发不出提醒了。我的解决方案是双管齐下:

一方面,在添加药单成功后立即弹授权框,让用户授权对应数量的订阅消息。用户设置了 2 个提醒时间,就需要请求 2 次授权,用uni.requestSubscribeMessage可以一次请求多条:

uni.requestSubscribeMessage({ tmplIds: ['模板1', '模板2'], success(res) { // res['模板1'] 可能取值 'accept' | 'reject' | 'ban' if (res['模板1'] === 'accept') { // 授权成功 } } })

另一方面,在首页做一个明显的"重新订阅提醒"入口。很多用户第一次授权时没搞懂,直接点了拒绝;第二天没收到提醒才意识到问题。这个入口要用提示文案讲清楚:"开启后每天按时提醒您服药",配合一个授权状态的角标(已授权 N 条 / 剩余 N 条),让用户心里有数。

还有个细节:如果用户在授权弹窗里点了"总是保持以上选择,不再询问",那么后续再调requestSubscribeMessage会直接返回ban,弹窗不会再出现。这种情况只能在页面里引导用户去微信的"设置-订阅消息"里手动打开。我在代码里做了ban状态的专门提示,避免用户以为小程序坏了。

5. 常见问题与排查实录

5.1 首页加载白屏,console 报错"uni is not defined"

这个坑出现在 HBuilderX 内置浏览器预览时。原因是我在 main.js 里引用了uni全局变量,但 H5 端在某些情况下uni对象初始化有延迟。解决方案:不要在最外层直接使用 uni API,而是放进生命周期钩子里,或者用onLoad之后再访问。另外检查一下是否用错了代码目录结构,uniapp 的页面一定要放在pages目录下,组件放在components目录下,目录结构错了也会导致白屏。

5.2 时间选择器在 iOS 上显示 12 小时制

有用户反馈在 iPhone 上picker mode="time"显示的是 "上午 08:00" 这种格式,而安卓是 "08:00"。这是因为 iOS 的时区/区域设置导致picker组件跟随系统语言。解决办法:选择完时间后,用Date对象重新格式化:

function formatTime(str) { // 输入可能是 '08:00' 或 '上午08:00' const match = str.match(/(\d{1,2}):(\d{2})/) if (!match) return '08:00' let h = parseInt(match[1]) if (str.includes('下午') && h < 12) h += 12 if (str.includes('上午') && h === 12) h = 0 return `${String(h).padStart(2, '0')}:${match[2]}` }

这个兼容逻辑很丑但是有效,iOS 和安卓传回来的字符串格式我都处理了一遍。

5.3 真机调试时订阅消息总是提示"模板不存在"

这个坑非常隐蔽。你在微信公众平台申请的模板 ID,和你在云函数里写的模板 ID,如果不在同一个环境(开发版/体验版/正式版)下测试,就会报模板不存在。正确的排查顺序是:先在微信公众平台的"开发管理-开发设置"里确认 AppID 是否一致;再检查模板 ID 是否复制完整(注意前后不要有空格);最后确认云函数部署的环境是否和当前小程序运行的环境一致。我第一次就是因为在开发版里测试,但云函数部署到了正式环境,导致一直报错。

5.4 数据不显示,或显示的是"缓存"里的旧数据

小程序本地存储是有缓存机制的,而且uni.getStorageSync不会主动失效。我遇到过一次:用户改了药单数据过后重新进入还是旧数据。排查后发现是我把med_listmed_records两个 key 的名字写混了,在一个页面里读的是medList,另一个页面写的是med_list,大小写和下划线不一致导致数据没读到。这种低级错误统称"key 命名不一致",建议全项目统一用一个常量文件管理所有 storage key:

// constants/storageKeys.js export const STORAGE_KEYS = { MED_LIST: 'med_list', MED_RECORDS: 'med_records', SETTINGS: 'settings', STORAGE_VERSION: 'storage_version' }

5.5 用户反馈收不到提醒,但代码逻辑没问题

这种情况大概率是订阅消息授权次数用完了。订阅消息是"一次性"的,用户授权 1 条就发 1 条。如果用户设置了每天 2 次提醒,但你只请求了 1 次授权,第二天第二条就会发送失败。我在云函数里把所有发送失败的记录都写到日志里,并且在「提醒中心」页面展示"今日可用提醒次数",用户一目了然。从产品角度来说,这个透明化的设计比让用户莫名收不到消息好得多。

6. 打包上架与后续扩展

6.1 微信小程序发布前的检查清单

在 HBuilderX 里点"发行-小程序-微信",会生成一个unpackage/dist/dev/mp-weixin目录,然后用微信开发者工具导入这个目录。但发布前有几个检查项我每次都过一遍:

  • manifest.jsonmp-weixin.appid是否正确,这是最高频的错误。
  • 所有请求的接口域名是否已配置到微信公众平台的"服务器域名"里,并且是 HTTPS。
  • 设置里urlCheck是否已改回true
  • 体验版测试时,订阅消息模板 ID 是否体验版可用的那个。
  • 审核时小程序里不能有"测试"字样,页面文案要完整。

6.2 从微信小程序扩展到 App 和 H5

如果你想把项目扩展到 App 端,在 HBuilderX 里点"发行-原生App-云打包"即可。App 端你可以用plus.push.createMessage实现真正的本地通知,到时间点直接在手机通知栏弹消息,体验比微信订阅消息好太多。H5 端配合NotifyAPI 也能做浏览器推送。

但要注意,跨端不是零成本。H5 端没有小程序的wx.cloud云函数能力,你需要额外部署云函数或者改用其他服务端方案;App 端没有uni.requestSubscribeMessage这个方法,需要在代码里加上平台判断:

// #ifdef MP-WEIXIN uni.requestSubscribeMessage({ ... }) // #endif // #ifdef APP-PLUS plus.push.createMessage('该吃药了', '记得服用降压药', { delay: 1000 }) // #endif

uniapp 的条件编译就是干这个用的,同一个文件里写多个平台的分支代码,打包时只保留对应平台的代码,非常实用。

6.3 后续功能扩展建议

这个项目做完了之后其实还有很多可以延伸的方向。我做了一个「健康数据统计」模块,把用户的打卡记录按周汇总,生成一个简单的依从性折线图——用的 canvas 手绘,没引图表库,小程序里引 ECharts 太重了。还可以加的功能:家人关怀模式(子女远程查看父母服药情况)、购药提醒(按库存自动生成附近药店导航)、药品相互作用查询(两张药品说明书的关系数据)。

我个人实际做下来的体会是,健康类小程序的核心不是功能堆砌,而是"让用户坚持用下去"。吃药提醒这个场景天然高频,你只要把"提醒—打卡—反馈"这个闭环做顺滑,用户留存不会差。以上这套基于 uniapp + vue 的实现方案,不管是拿来练手做毕设,还是后续想商业化落地,骨架都是够用的,剩下的就是往里面填你自己的业务细节了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询