![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
作为一名既爱写代码又爱打台球的程序员,我一直想做一款"纯粹"的台球工具。市面上的 App 要么广告满天飞,要么功能单一------能计分的不能复盘,能复盘的体验太差。
于是,我决定自己造个轮子。完全根据我自己需要来定制化开发。
这款名为**"追分记"的小程序,不仅支持中式黑八/九球的复杂计分(断点续打、三人追分)、自定义训练模式(统计五分点等训练记录),还内置了一个基于物理交互的战术板**。
这篇文章我将从产品构思、AI 辅助设计、本地存储架构等维度,复盘这次独立开发的完整过程。
一、 从构思到落地:AI 如何加速全流程
在这次开发中,我尝试了一种新的工作流:全程 AI 开发。从灵感到 UI,再到代码落地,让AI 扮演"产品助理"、"UI 设计师"、开发工程师的角色。
1.1 需求构思:解决我自己的痛点
在写第一行代码前,我先整理了自己的"吐槽清单":
- 痛点 A:打九球追分,算大金、小金、犯规扣分太烧脑,容易算错。
- 痛点 B:打到一半朋友来电话,切出去回个消息,回来小程序重载了,比分全丢。
- 痛点 C:想给朋友讲刚才那杆球怎么走位,手边没球桌,干讲听不懂。
- 通点 D:自己平时去训练,也不知道训练的目标和效果。
基于此,确定了 MVP(最小可行性产品)的三大核心功能:智能计分器、断点续打系统、便携战术板。
1.2 AI 生成 产品初稿,再生成 UI:从 Prompt 到视觉稿
作为程序员,配色和 UI 往往是短板。这次我没有自己瞎折腾 CSS,而是利用 AI 生成设计灵感。
从一个新的项目开始,我让 Trae 帮我生成一个完整prd文档,包含需求分析,功能模块,页面细节。
然后让它开始实现这些功能,然而最后实现的UI界面效果很不理想。

于是,我把这个截图扔给 v0(v0.app/),让他按照这个截图的哪内容帮我从新生成一个页面UI,并且产出一个完整的配色,以便其他页面复用。提示词如下:
你现在是个专业的UI交互设计师,帮我优化一下这个页面的UI,然后生成一个静态的html。不允许使用tailwind css,图标库可以使用lucide。需要生成一个完整的配色方案。
最后它帮我产出了一个html文件,我打开一天,天啦,这也太牛逼 plus了。
这效果已经超出我的想象了。于是我立马让 Trae 把这个页面1:1还原到对应的小程序页面。
提示词:根据 index.html 1:1还原 index.wxml 的UI。注意: index.htm 通用的root样式可以提取到公共样式中进行复用。
拿到视觉基调后,我将其转化为全局 CSS 变量(app.wxss),确保全站风格统一:
css
page {
--bg-primary: #0a0f1a;
--text-primary: #f8fafc;
--accent-green: #10b981;
--card-bg: rgba(30, 41, 59, 0.7);
background: linear-gradient(160deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
}
从最初简陋的线框图,到引入 tdesign 组件库,再到配合 AI 生成的图标,产品的"颜值"经过了三轮大的迭代,最终呈现出一种"极客范儿"的精致感。
业务逻辑和UI进行了完美的融合,没有任何bug。简直完美。
二、 核心技术复盘 II:本地存储的"垃圾回收"机制
做"断点续打"功能时,我遇到了一个典型问题: 用户可能开了局,没打完就退出了;过几天又开了新局。久而久之,Storage 里会堆积大量废弃的比赛快照(Snapshot)。
为了解决这个问题,我设计了一套基于索引的 GC(垃圾回收)机制。
3.1 存储结构设计
- 索引表 (
ongoingMatches):记录所有"进行中"比赛的元数据(ID、时间、模式)。 - 快照数据 (
match_current_{id}):每场比赛的具体状态单独存一个 Key,防止单条数据过大。
3.2 自动 GC 实现
每次小程序启动或进入列表页时,触发 cleanupOrphans 函数:
javascript
// utils/storage.js
function cleanupOrphans() {
// 1. 获取"存活"的比赛 ID 集合
const list = ongoing.getList()
const activeIds = new Set(list.map(v => v.id))
// 2. 遍历 Storage 中所有的 Key
const keys = wx.getStorageInfoSync().keys
keys.forEach(k => {
// 3. 识别比赛快照 Key 格式
if (k.startsWith('zhongba_match_current_')) {
const id = Number(k.split('_').pop())
// 4. 如果 ID 不在存活列表中,视为"孤儿数据",直接删除
if (!activeIds.has(id)) {
safeRemove(k)
console.log(`[GC] Cleaned up orphan match: ${id}`)
}
}
})
}
这个机制保证了 Storage 永远只存储有用的数据,既节省空间又维护了整洁。
最后我还让它生成了一份数据快照,以便以后接入服务端的时候可以快速的创建数据库:

三、 细节体验:后台计时校准
小程序在切入后台(onHide)后,setInterval 定时器往往会暂停或变慢。为了保证比赛计时的准确性,不能单纯依赖定时器累加。
解决方案:时间戳校准法
- 记录时间点 :
onHide时,记录当前时间戳runningSince。 - 计算差值 :
onShow时,计算(Date.now() - runningSince)。 - 自动补齐 :将差值加到
elapsedTime中。
javascript
// pages/nine-ball/nine-ball.js
onShow() {
if (this.data.isRunning) {
const now = Date.now()
const rs = this.data.runningSince
const delta = Math.floor((now - rs) / 1000)
// 即使在后台挂起了一小时,回来时间也会瞬间补齐
if (delta > 0) {
this.setData({ elapsedTime: this.data.elapsedTime + delta })
}
}
}
四、 总结
"追分记"虽然只是一个工具类小程序,但"麻雀虽小,五脏俱全"。
- 从产品角度:它验证了从痛点出发,利用 AI 辅助设计快速落地的可行性。
- 从技术角度:本地存储治理、生命周期管理等前端/小程序开发的典型场景。
独立开发最迷人的地方,莫过于看着一个想法,在自己的键盘下一点点变成触手可及的现实。
如果你也是台球爱好者,或者对小程序开发感兴趣,欢迎在微信搜索**"追分记"**体验,也欢迎在评论区交流技术细节!





