从 0 到 1,我用小程序 + 云开发打造了一个“记忆瓶子”,记录那些重要的日子!

缘起:为什么需要一个"记忆瓶子"?

市面上有很多纪念日 APP,但它们常常伴随着各种广告弹窗、冗余功能,或者在UI设计上未能满足我个人对于"简洁与美"的追求。我希望能有一个小而美的应用,能够:

  • 纯粹地记录重要的公历或农历纪念日。
  • 能够自由设置重复提醒。
  • 界面美观,甚至可以自定义背景图。
  • 最重要的是,没有多余的干扰,安静地守护那些珍贵的回忆。

于是,"记忆瓶子"的构思便在脑海中逐渐成形。在深入技术细节之前,欢迎扫码快速体验'记忆瓶子'!

技术选型:原生小程序 + 云开发的"双剑合璧"

作为一名开发者,选择合适的技术栈是项目成功的关键。

  • 微信小程序: 无疑是触达用户最便捷的平台之一。其轻量级、无需下载安装的特性,以及微信生态内丰富的接口能力(订阅消息、用户授权等),都让它成为首选。
  • 微信云开发: 这次我选择了"All in 云开发"的模式。云开发提供了数据库、云存储、云函数等一站式服务,大大降低了后端开发和运维的复杂度。对于个人开发者而言,免费额度友好,上手成本极低,能够让我们更专注于前端和业务逻辑的实现。

这套组合让我在短时间内能够快速迭代,将产品想法落地。

核心功能与技术亮点

1. 公农历转换与复杂日期计算的"艺术"

"纪念日"的核心就是日期。但它远非简单的加减法,公历、农历、重复、指定年数,甚至还有时区问题,都让日期计算变得异常复杂。

痛点 : 用户可能想记录一个农历生日,每年自动提醒;或者一个固定公历的周年纪念日;又或者是一个只发生一次的特殊事件。如何精准地计算出这些纪念日的"下一个发生日期",并与当前日期进行比对,是小程序的核心挑战。 解决方案: 我引入了两个强大的日期处理库:

  • solarlunar: 用于精准地进行公历和农历之间的转换。尤其是在处理闰月等复杂情况时,它的表现非常可靠。
  • dayjs 及其 utc/timezone 插件 : 这两个插件对于处理跨时区(尤其是中国大陆)的日期计算至关重要。我发现,仅仅使用 new Date() 可能会在服务器和用户手机之间产生时区差异,导致"今天"的判断不准确。通过 dayjs().tz('Asia/Shanghai').startOf('day') 能够确保在云函数和前端都以统一的"上海时间"零点作为基准,从而确保倒计时和提醒的精确性。

核心逻辑片段(getList 云函数中计算 nextOccurrenceTimestamp 的部分简化版):

js 复制代码
// 假设 item.timestamp 是事件的UTC时间戳
const eventDayjsUTC = dayjs(item.timestamp);
const eventInTimeZone = eventDayjsUTC.tz(TARGET_TIMEZONE); // TARGET_TIMEZONE = 'Asia/Shanghai'
const todayStartInTimeZone = dayjs().tz(TARGET_TIMEZONE).startOf('day');

let nextSolarYear, nextSolarMonth, nextSolarDay;

if (item.dateType === 'lunar' && item.isRepeat) {
    // 农历重复纪念日的复杂计算...
    let nextSolar = solarlunar.lunar2solar(todayStartInTimeZone.year(), item.lunarMonth, item.lunarDay, item.isLeapMonth || false);
    // ... 如果今年已过,则计算明年的 ...
    if (dayjs.tz(`${nextSolar.year}-${nextSolar.month}-${nextSolar.day}`, TARGET_TIMEZONE).valueOf() < todayStartInTimeZone.valueOf()) {
        nextSolar = solarlunar.lunar2solar(todayStartInTimeZone.year() + 1, item.lunarMonth, item.lunarDay, item.isLeapMonth || false);
    }
    nextSolarYear = nextSolar.year;
    nextSolarMonth = nextSolar.month;
    nextSolarDay = nextSolar.day;

} else if (item.dateType === 'solar' && item.isRepeat) {
    // 公历重复纪念日的计算...
    nextSolarYear = todayStartInTimeZone.year();
    nextSolarMonth = eventInTimeZone.month() + 1;
    nextSolarDay = eventInTimeZone.date();
    // ... 如果今年已过,则计算明年的 ...
    if (dayjs.tz(`${nextSolarYear}-${nextSolarMonth}-${nextSolarDay}`, TARGET_TIMEZONE).valueOf() < todayStartInTimeZone.valueOf()) {
        nextSolarYear++;
    }
} else {
    // 一次性纪念日的计算...
    nextSolarYear = eventInTimeZone.year();
    nextSolarMonth = eventInTimeZone.month() + 1;
    nextSolarDay = eventInTimeZone.date();
}

// 最终构建下一个事件的 Day.js 对象
const nextEventDateStr = `${nextSolarYear}-${String(nextSolarMonth).padStart(2,'0')}-${String(nextSolarDay).padStart(2,'0')}`;
const nextEventStartInTimeZone = dayjs.tz(nextEventDateStr, TARGET_TIMEZONE);
const nextOccurrenceTimestamp = nextEventStartInTimeZone.valueOf();

// ... 剩余的倒计时计算 ...

订阅消息:不错过每一个重要提醒

纪念日如果没有提醒,就失去了意义。"记忆瓶子"集成了微信小程序的订阅消息功能。

实现:

  • 用户在小程序内通过 wx.requestSubscribeMessage 授权接收提醒。
  • 我部署了一个定时触发的云函数 checkReminders。该云函数会每天运行,遍历所有用户的纪念日,计算每个纪念日的目标提醒日期(即"下一个发生日期"减去"提前提醒天数")。
  • 如果目标提醒日期恰好是今天,云函数便会调用 cloud.openapi.subscribeMessage.send 发送订阅消息。
  • 踩坑提示 : 在发送订阅消息时,有一个关键参数 miniprogramState初期为了调试方便,我将其设为 'developer',导致用户点击消息后跳转到开发版小程序! 正式上线后,务必将其改为 'formal',确保用户跳转到正式发布版。

checkReminders 云函数核心片段:

js 复制代码
// 假设 nextOccurrenceTimestamp 已经计算好
const targetReminderTimestamp = nextOccurrenceTimestamp - (reminderDaysBefore * ONE_DAY_MS);

if (targetReminderTimestamp === todayTimestamp) { // todayTimestamp 是目标时区今天的0点时间戳
    // 构建消息数据
    const messageData = { /* ... */ };
    remindersToSend.push({
        touser: item._openid,
        templateId: REMINDER_TEMPLATE_ID,
        page: `pages/detail/index?id=${item._id}`,
        data: messageData,
        miniprogramState: 'formal' // 【关键】确保跳转到正式版!
    });
}

云存储与 Base64 图片上传:打造个性化背景

为了让每个纪念日卡片都能拥有独特的视觉效果,我引入了自定义背景图功能。然而,云开发免费版存储空间有限,直接上传大量图片并不是最优解。

巧妙的解决方案:

  • 用户选择图片后,小程序端会先对图片进行压缩处理(减少数据量)。
  • 接着,将压缩后的图片数据转为 Base64 编码字符串
  • 这个 Base64 字符串会被直接作为纪念日记录的一个字段,存储在云数据库中(而非云存储)。
  • 前端渲染时,直接将这个 Base64 字符串赋值给 <img> 标签的 src 属性,图片便能直接显示。

【好处】 : 这种方式巧妙地绕过了云存储的容量限制,对于图片数量不多的个人应用来说,大大节约了成本,并且部署简单,加载速度也很快。

UI/UX 优化与样式攻坚战:Vant Weapp 的"爱恨交织"

为了快速构建美观的界面,我选择了 Vant Weapp 组件库。它提供了丰富的组件和完善的文档,确实提升了开发效率。然而,在详情页的底部操作按钮布局上,我遭遇了一场漫长而痛苦的"攻坚战"!

问题描述 : 在详情页,我希望"编辑"和"删除"两个按钮能够并排显示,且平分底部空间,左右留出相等的内边距 。在开发者工具模拟器上,通过 display: flex; gap: 24rpx; 配合 <van-button custom-class="action-button" />.action-button { flex: 1; min-width: 0; box-sizing: border-box; } 似乎完美解决了。然而,在真机上,按钮的布局却始终是"左边有空白,右边没有",呈现出不均衡甚至轻微溢出的情况!

排查过程:

  • 检查 flex 容器和子项的 padding, margin, box-sizing
  • 尝试使用 calc() 函数精确计算宽度。
  • 利用真机调试 功能,我发现罪魁祸首竟然是微信小程序底层对原生 button 注入的默认样式wx-button:not([size=mini]) { width: 184px; ...; margin-left: auto; margin-right: auto; }。这个固定宽度和 auto 外边距在 Vant Weapp 复杂的组件结构内部,以某种难以覆盖的方式生效,破坏了我的 flex: 1 布局!

最终解决方案 : 在尝试了各种 !important 覆盖、多层选择器、甚至修改 Vant 内部样式(失败告终)之后,我做出了一个决定:放弃使用 van-button 来实现底部操作按钮

我选择用两个原生的 <view> 标签来模拟按钮:

html 复制代码
<view class="action-button-wrapper">
  <view class="custom-action-button custom-edit-button" hover-class="button-hover" bindtap="onEdit">
    <text>编 辑</text>
  </view>
  <view class="custom-action-button custom-delete-button" hover-class="button-hover" bindtap="onDelete">
    <text>删 除</text>
  </view>
</view>

并结合以下 CSS 样式:

css 复制代码
.action-button-wrapper {
  display: flex;
  gap: 24rpx;
  padding: 40rpx 24rpx;
  box-sizing: border-box;
  width: 100%;
}
.custom-action-button {
  flex: 1;
  min-width: 0;
  box-sizing: border-box;
  height: 80rpx;
  border-radius: 40rpx;
  font-size: 28rpx;
  font-weight: bold;
  color: #FFFFFF;
  text-align: center;
  line-height: 80rpx;
}
.custom-edit-button { background-color: #C8B6B6; }
.custom-delete-button { background-color: #ee0a24; }
.button-hover { opacity: 0.85; }

这套方案简单、直接,让我对样式有了 100% 的掌控力,最终在真机上完美实现了预期布局。有时候,回归原生是最可靠的解决方案!

版本更新机制:确保用户始终使用最新功能

为了避免用户因为小程序缓存而无法体验最新功能或修复的 Bug,我在 app.js 中集成了 wx.getUpdateManager()。它能检测到小程序新版本下载完成后,弹窗提示用户重启。

关键点:

  • 监听器 onUpdateReady 必须在 onLaunch 时就设置好。
  • 【非常重要】 每次上传新版本前,务必手动修改项目根目录下 project.config.json 文件中的 "version" 字段(如从 1.0.0 改为 1.1.0),否则微信后台无法识别为新版本,更新机制也无法触发。

产品未来展望

"记忆瓶子"才刚刚起步,未来还有很多有趣的功能等待探索:

  • 共享空间: 允许多人(如情侣、家人)共同创建和维护纪念日,分享彼此的珍贵时刻。
  • 更多主题与精美分享: 提供更丰富的 UI 主题、背景素材,并支持一键生成精美的分享图片。
  • 年度总结与历史上的今天: 增加趣味性功能,回顾一年的点滴,或发现"历史上的今天"发生了什么。

总结

开发"记忆瓶子"的过程,是一个不断学习、不断解决问题的过程。它让我对小程序和云开发有了更深的理解,也再次体会到作为一名开发者,能够将自己的想法变为现实的乐趣。

如果你也对这个小程序感兴趣,欢迎扫码体验,并留下宝贵的反馈和建议!你的每一次使用和反馈,都将是"记忆瓶子"继续成长的动力。再次邀请体验和反馈

感谢阅读!

相关推荐
非凡ghost3 小时前
Subtitle Edit(字幕编辑软件) 中文绿色版
前端·javascript·后端
扎瓦斯柯瑞迫3 小时前
cursor: 10分钟魔改环境、优雅获取Token
前端·javascript·后端
王六岁3 小时前
🐍 前端开发 0 基础学 Python 入门指南:条件语句篇
前端·python
San303 小时前
CSS3 星球大战:用前端技术打造震撼的3D动画效果
前端·css·html
用户12039112947263 小时前
从零构建一个HTML5敲击乐Web应用:前端开发最佳实践指南
前端
Violet_YSWY3 小时前
将axios、async、Promise联系在一起讲一下&讲一下.then 与其关系
开发语言·前端·javascript
San303 小时前
扩展卡片效果:用 Flexbox 和 CSS 过渡打造惊艳交互体验
前端·javascript·css
写代码的皮筏艇3 小时前
JS数据类型转换
前端·javascript
Qinana3 小时前
🌟 从 CSS 到 Stylus:打造更优雅、高效的样式开发体验
前端·javascript·css