
前言很多工具型 App 都会有“知识库”“建议库”“技巧列表”但大多数最后都变成没人点的静态页面。原因很简单用户打开应用时不想读百科他想要一个当下能执行的答案。喵汪星球的陪玩模块就是围绕这个目标设计的不是给用户一堆文章而是直接回答“今天玩什么”。用户可以随机换一个可以收藏也可以从更多建议里选择一个玩法。每条内容都包含时长、道具、目标和注意事项。这篇重点讲内容型功能的产品化实现。内容型功能的关键先给答案陪玩页的设计不是先展示分类筛选而是先展示一个主推荐Builder PlayPage() { Column({ space: Theme.spaceXL }) { this.PageHeader(今天玩什么, this.playHeaderText()) this.EntertainmentNotice( 该功能以娱乐为主陪玩建议请结合宠物年龄、体力和现场状态选择 ) Column({ space: Theme.spaceL }) { Stack({ alignContent: Alignment.BottomStart }) { this.PlayHeroImage(this.currentPlayTitle()) Column({ space: Theme.spaceS }) { Text(this.currentPlayTitle()) Text(this.currentPlayMeta()) Row({ space: Theme.spaceS }) { this.Tag(放电, primary) this.Tag(低门槛, warm) this.Tag(可收藏, blue) } } } Row({ space: Theme.spaceM }) { Text(换一个) Text(this.isFavoritePlay(this.currentPlayTitle()) ? 取消收藏 : 收藏) } } } }这个结构像一个“推荐卡”而不是文章列表。用户打开页面后马上知道今天可以玩什么再决定要不要换一个或收藏。内容功能常见误区是把所有内容摊给用户。更好的方式是先给一个可执行答案再提供更多选择。内容模型四个字段就够用陪玩建议模型很克制interface PlaySuggestion { title: string meta: string goal: string caution: string }示例数据private readonly playCards: PlaySuggestion[] [ { title: 纸团追逐, meta: 10 分钟 · 纸团 / 逗猫棒, goal: 放电 · 缓解无聊, caution: 结束后收走小物件避免误吞 }, { title: 嗅闻寻宝, meta: 12 分钟 · 零食 / 嗅闻垫, goal: 探索 · 建立亲密关系, caution: 零食量计入当天总摄入 }, { title: 慢速跟随, meta: 8 分钟 · 无需道具, goal: 低强度 · 老年宠物友好, caution: 避免强迫互动观察呼吸频率 } ]这四个字段刚好覆盖用户决策title玩什么。meta多久、需要什么。goal为什么玩。caution注意什么。移动端内容不适合一上来写长文。结构化字段能让用户扫一眼就行动。当前推荐playIndex 而不是复制对象项目用playIndex保存当前推荐State playIndex: number 0 private currentPlay(): PlaySuggestion { if (this.playIndex 0 || this.playIndex this.playCards.length) { return this.playCards[0] } return this.playCards[this.playIndex] } private currentPlayTitle(): string { return this.currentPlay().title } private currentPlayMeta(): string { return this.currentPlay().meta }为什么不直接把当前玩法对象存到状态里因为玩法内容是静态数组playIndex更轻也更容易持久化。内容更新时只要 index 合法页面就能重新拿到最新内容。如果 index 越界回退到第一条。恢复状态时也做了保护if (this.playIndex 0 || this.playIndex this.playCards.length) { this.playIndex 0 }这是内容库迭代时很有用的小细节。以后删掉某条玩法旧用户本地保存的 index 也不会导致页面异常。随机换一个避免“没变化”的尴尬换一个推荐private nextPlay(): void { let nextIndex Math.floor(Math.random() * this.playCards.length) if (this.playCards.length 1 nextIndex this.playIndex) { nextIndex (nextIndex 1) % this.playCards.length } this.playIndex nextIndex this.saveState() }这里特意避免连续随机到同一条。因为用户点击“换一个”后如果内容没变他会怀疑按钮失效。这是内容型功能里非常细微但重要的体验随机不是数学意义上的纯随机而是用户感知上的“发生变化”。收藏从单字段升级到列表早期可以只保存一个收藏标题State favoritePlayTitle: string 后来升级为收藏列表interface FavoritePlayItem { title: string } State favoritePlayItems: FavoritePlayItem[] []判断是否收藏private isFavoritePlay(title: string): boolean { for (let index 0; index this.favoritePlayItems.length; index) { if (this.favoritePlayItems[index].title title) { return true } } return false }切换收藏private toggleFavoritePlay(title: string): void { if (this.isFavoritePlay(title)) { this.favoritePlayItems this.favoritePlayItems.filter( (item: FavoritePlayItem) item.title ! title ) } else { this.favoritePlayItems this.favoritePlayItems.concat([{ title: title }]) } this.favoritePlayTitle this.favoritePlayItems.length 0 ? this.favoritePlayItems[0].title : this.saveState() }这里保留favoritePlayTitle是为了兼容旧字段。恢复状态时会把旧收藏迁移到新列表if (this.favoritePlayTitle ! !this.favoritePlayListContains(this.favoritePlayItems, this.favoritePlayTitle)) { this.favoritePlayItems this.favoritePlayItems.concat([ { title: this.favoritePlayTitle } ]) }本地 App 的字段演进一定要考虑旧数据。用户升级后不应该因为你改了收藏结构就丢收藏。图片映射用标题选择不同玩法图玩法图不是动态下载而是用本地资源Builder PlayHeroImage(title: string) { if (title 嗅闻寻宝 || title 零食路线 || title 毛毯藏物) { Image($r(app.media.play_sniff)) .width(100%) .height(100%) .objectFit(ImageFit.Cover) } else if (title 慢速跟随 || title 窗边观察 || title 梳毛奖励) { Image($r(app.media.play_slow)) .width(100%) .height(100%) .objectFit(ImageFit.Cover) } else { Image($r(app.media.play_paper)) .width(100%) .height(100%) .objectFit(ImageFit.Cover) } }这是一个 MVP 取舍不为每一条玩法都准备独立图片而是把玩法归到几类视觉资源里。这样既有视觉丰富度又不会增加太多资源成本。如果后续内容库变大可以把图片字段加入模型interface PlaySuggestion { title: string meta: string goal: string caution: string image: Resource }首版为了快速交付用标题映射就够了。更多建议列表不是重复而是切换入口主推荐下面是更多玩法Column({ space: Theme.spaceM }) { ForEach(this.playCards, (item: PlaySuggestion, index: number) { this.PlayRow(item, index) }, (item: PlaySuggestion) item.title) }每个玩法行都能点击设置为当前推荐Builder PlayRow(item: PlaySuggestion, index: number) { Row({ space: Theme.spaceM }) { this.PlayThumb(item.title) Column({ space: Theme.spaceS }) { Row() { Text(item.title) Blank() Text(this.isFavoritePlay(item.title) ? 已收藏 : 收藏) } Text(item.meta) Row({ space: Theme.spaceS }) { this.Tag(item.goal.split( · )[0], primary) this.Tag(item.meta.split( · )[0], warm) } Text(item.caution) } } .backgroundColor(index this.playIndex ? Theme.primaryPale : Theme.surface) .onClick(() { this.playIndex index this.saveState() }) }列表不是单纯“更多内容”而是主推荐的切换器。选中项用primaryPale高亮用户能理解当前推荐和列表之间的关系。风险提示是内容质量的一部分陪玩内容每条都有caution结束后收走小物件避免误吞 零食量计入当天总摄入 避免强迫互动观察呼吸频率 确认窗户锁好避免高处跌落 遇到抗拒立即暂停少量多次这比单纯写“怎么玩”更重要。养宠建议的价值不只是让用户陪宠物玩还要告诉用户什么时候该停、什么东西不能用、哪些情况要观察。内容型功能不是“塞文案”而是把专业边界产品化。从 ArkTS 数组到内容库的演进当前playCards写在代码里适合首版。后续可以迁移为resources/rawfile/play_suggestions.json - KnowledgeRepository - PlayService - PlayPage进一步可以支持按猫/狗过滤。按幼年、成年、老年过滤。按目标过滤放电、安抚、嗅闻、亲密关系。收藏优先推荐。最近玩过的内容短期内不重复。每日固定推荐避免刷新后频繁变化。但不要过早复杂化。首版最重要的是让用户打开页面就能马上行动。这部分最容易踩的坑第一内容不要只做长列表。主推荐能显著提升使用率。第二随机推荐要避免连续重复。用户感知比数学随机更重要。第三收藏结构升级时要兼容旧字段。第四长标题、长注意事项要加maxLines和textOverflow。第五图片资源要有兜底不要让某条内容因为没图就空白。第六内容建议要有风险提示尤其是涉及运动、零食、小物件的玩法。本篇小结陪玩模块的技术难度不在 API而在产品化用结构化模型代替长文本。用主推荐回答“今天玩什么”。用playIndex管理当前推荐。用随机切换制造轻量探索感。用收藏沉淀用户偏好。用本地图片资源提升视觉完整度。用风险提示守住内容边界。下一篇进入健康模块。健康症状库更敏感它要帮用户整理异常信息但不能替代兽医诊断。