起因:每天最难回答的问题
我相信每个程序员都经历过这个场景——中午十一点半,同事群里弹出消息:“今天吃啥?”
然后沉默。
漫长的沉默。
最后要么是某个人不耐烦地说"随便",要么是点了昨天刚吃过的那家。我自己也是这样,一个人的时候不想动脑子,两个人的时候互相推,一群人的时候更是灾难。
去年开始做 HarmonyOS 开发,一直想找个小项目练手。想来想去,干脆把"吃什么"这个每天都要面对的问题做成一个 App 吧。不是什么大项目,但足够把 ArkUI 的交互能力摸一遍。
这就是「决定今天吃什么」这个 App 的由来。
不只是随机:三种模式覆盖不同场景
市面上类似的工具不少,大多就是一个随机转盘,转一下出个结果。说实话这个功能五分钟就能写完,但实际用起来问题很多——随机出来的你不想吃怎么办?两个人意见不一致怎么办?
我设计了三种决策模式:random(直接随机)、wheel(转盘)、vote(投票淘汰)。
一个人用的时候,直接随机或者转盘就行,有仪式感,转完就走,别犹豫。
多人场景才是重点。投票模式的逻辑是这样的:每个人划掉自己不想吃的选项,划完之后剩下的候选列表再随机出一个。这个思路其实很简单——与其让所有人达成共识"我们要吃什么",不如让每个人表达"我不吃什么",排除法比选择法快得多。
投票模式的实现
投票这块我纠结了挺久。一开始想做在线实时投票,搞 WebSocket 同步什么的,后来想想这就是个吃饭工具,搞那么复杂谁用啊。最终方案是本地传手机,每人轮流划掉不想吃的,简单粗暴。
核心逻辑写在 vote.ts 里:
export function finishVote(session: VoteSession): string {
const entries = Object.entries(session.votes)
let max = -1
let winners: string[] = []
for (const [id, count] of entries) {
if (count > max) { max = count; winners = [id] }
else if (count === max) { winners.push(id) }
}
if (winners.length === 1) return winners[0]
// 平票直接均匀随机,都是幸存者,谁赢都公平
return winners[Math.floor(Math.random() * winners.length)]
}
```
有个细节值得说一下:平票的情况比我预想的多很多。两个人投票,经常剩两三家票数一样。这时候我没有让用户再投一轮(体验太差了),而是直接均匀随机从平票选项里抽一个。反正能活到最后一轮的都是大家不讨厌的,谁赢都行。测试下来大家接受度还不错。
## 历史排重:别让我连吃三天黄焖鸡
做完随机功能之后我自己用了一周,发现一个很烦的问题:转盘连续三天转到同一家店。概率上没毛病,体验上很崩溃。
所以加了个 `avoidRecentN` 的设置项。随机的时候会检查最近 N 次的历史记录,把已经吃过的餐厅权重直接置零,不参与本次随机。这个 N 默认是 3,用户可以自己调。
权重这块也顺便做了。每家餐厅有一个 `weight` 字段,喜欢的店可以调高权重,出现概率就大一些。不设置的话默认都是 1,完全均匀随机。
## 筛选过滤:候选列表的管理
餐厅数据全部存在本地,用的是 HarmonyOS 的轻量级持久化方案。每个餐厅有名称、菜系标签、价格区间、距离这些字段。
筛选逻辑我抽成了一个独立的 `getCandidates` 函数,支持按标签、预算、距离三个维度过滤。说白了就是一串 `.filter()` 链式调用,没什么花哨的。但这个功能实际用起来很有用——中午只想吃 20 块钱以内的,或者只想走路 500 米以内的,过滤完再随机,结果靠谱很多。
对了,餐厅列表里有个 `archived` 字段是后来加的。有些店倒闭了或者搬走了,直接删掉的话历史记录会断,所以做了软删除。这种小细节,不做没人说,做了用起来舒服。
## ArkUI 踩的几个坑
### 坑一:列表数据更新后 UI 不刷新
这个坑搞了我一个下午。现象是往餐厅列表里 push 了新数据,console 打出来数组确实变了,但页面上的 List 组件纹丝不动。
一开始以为是响应式没生效,翻了好几遍文档才搞明白——ArkUI 对数组的变化检测跟 Vue2 类似,直接 push 不会触发重新渲染,因为数组引用地址没变。
错误写法:
```arkts
// ❌ 这样写 UI 不会刷新
this.restaurants.push(newItem)
// ✅ 必须替换整个数组引用
this.restaurants = [...this.restaurants, newItem]
说实话有点难受,这个行为在文档里不太显眼,但一旦踩到就是半天起步。如果你用 ArkTS 做列表相关的功能,记住这个:任何数组变更都用展开运算符生成新数组。
坑二:转盘动画的缓动曲线
转盘模式是整个 App 里最费时间的部分。我需要一个"快速旋转→自然减速→停在某个扇区"的动画效果。
ArkTS 的 animateTo 支持自定义 curve,我试了好几个:
Curve.Linear:匀速旋转,停下来很突兀,像卡住了-
Curve.EaseOut:减速太早,转两圈就慢下来了,没有"用力甩"的感觉
-
Curve.Friction:这个最接近物理减速的效果,最终用的就是它
最终方案是先算好目标角度(根据weightedPick的结果反推扇区位置),然后用Curve.Friction做一个 3000ms 的旋转动画,转够 5-6 圈再停到目标位置。停下来的那一刻刚好指针指在中选的餐厅上,视觉上挺舒服的。
这里有个小技巧:目标角度要加上 360 * N(N 是你想让它多转几圈),不然动画只转一小段就到了,完全没有转盘的感觉。
转盘的上限问题
转盘模式还有个现实的限制:你不能往一个转盘上塞 50 个选项,那每格就一个像素宽,根本看不清。我设了一个 MAX_WHEEL_SEGMENTS 的上限,超过这个数量就取权重最高的前 N 个上盘。
如果你收藏了 50 家餐厅但只能上 12 个,那权重高的(你更喜欢的)优先上盘,逻辑上说得通。
现在的状态
App 已经上架华为应用市场,叫「决定今天吃什么」,当前版本 1.2.0。
说实话下载量还很少,毕竟没怎么推广。但我自己和身边几个同事每天都在用,午饭决策效率确实提高了。有个同事说他和女朋友用投票模式之后,吃饭这事儿上的争执少了很多——两个人都把不想吃的划掉,剩下的随机出来,谁也不怪谁。
我觉得这种小工具的价值就在这儿:解决的问题不大,但天天都碰到。
后续打算做的
有几个想法还没落地:
- 接入地图服务,自动获取附近餐厅,省去手动录入的麻烦
-
- 做一个 HarmonyOS 桌面卡片,早上瞄一眼就知道中午吃啥,这个场景太适合卡片了
-
- 餐后评分。现在
HistoryItem里已经预留了rating字段,但 UI 还没做。有了评分数据之后可以自动调整权重,吃了觉得不行的下次少出现
- 餐后评分。现在
对了,我一直在想一个问题:你们多人吃饭决策一般怎么解决的?我用的是"投票淘汰"这个思路——每人划掉不想吃的,剩下的随机选。你们觉得这个逻辑合理吗?还是说有更好的办法?评论区聊聊。
应用市场搜「决定今天吃什么」就能找到,开发者 ID 是 com.xun.eatwhat2。
305



被折叠的 条评论
为什么被折叠?



