记忆洗牌是时光胶囊里最有仪式感的功能——用户点击按钮系统随机抽取一个历史胶囊展示。用户不知道下一张会显示哪个胶囊这种随机重逢的设计让回顾历史变成了拆盲盒式的体验。这个页面的代码量不大但随机算法、卡片翻转、空状态处理都有值得拆解的细节。完整效果一、记忆洗牌的入口两个入口位置入口位置触发方式标题栏 时间线 Tab 右上角点击跳转设置页设置 Tab 列表项点击跳转// 入口 1标题栏Text().fontSize(20).width(40).height(40).onClick(()this.goToShuffle())// 入口 2设置页Row(){Text( 记忆洗牌).fontSize(FontSize.md)Blank()Text(›).fontSize(FontSize.xxl).fontColor(this.getColors().textSecondary)}.onClick(()this.goToShuffle())两个入口指向同一个页面——但使用场景不同。标题栏入口适合我想随机看看的冲动式操作设置页入口适合我找找这个功能的探索式操作。两种用户行为都有对应的入口。路由跳转privategoToShuffle():void{router.pushUrl({url:pages/MemoryShuffle})}没有传参数——MemoryShuffle 自己从数据库获取所有胶囊。和详情页不同传 capsuleId洗牌页需要的是全部胶囊的数据不是某一个。所以不需要路由参数页面自己查询。二、随机算法的设计Fisher-Yates 洗牌算法// 推断的洗牌算法privateshuffleArray(array:Capsule[]):Capsule[]{constresultarray.slice()// 复制原数组for(letiresult.length-1;i0;i--){constjMath.floor(Math.random()*(i1))consttempresult[i]result[i]result[j]result[j]temp}returnresult}Fisher-Yates 是标准的均匀洗牌算法——保证每种排列出现的概率相等。slice()复制原数组——不修改数据库里的顺序。为什么不直接用 Math.random() 排序// 错误做法按 random 排序array.sort(()Math.random()-0.5)sort(() Math.random() - 0.5)不是均匀洗牌——它有 bias。某些排列出现的概率比其他排列高。Fisher-Yates 保证均匀性——每个胶囊出现在每个位置的概率都是 1/n。洗牌队列的管理privatequeue:Capsule[][]// 洗牌队列privatecurrentIndex:number0// 当前展示位置privateinitQueue():void{this.queuethis.shuffleArray(this.database.getAllCapsules())this.currentIndex0}洗牌一次顺序全部确定——不是每次都随机取一个。用户点击下一张时按队列顺序展示下一个。这保证了1同一次洗牌不会重复展示同一个胶囊2所有胶囊都会被展示一遍。三、卡片展示的三种状态从 CapsuleState 推断// 导入import{getCapsuleState,CapsuleState}from../model/Capsule洗牌展示的卡片需要区分胶囊状态状态展示方式交互SEALED 倒计时显示这个胶囊还在封印中READY显示心情 引导开启可以点击开启OPENED显示内容 心情 时间完整展示封印中胶囊的特殊处理// 推断的封印中展示Column(){Text().fontSize(64)Text(这个胶囊还在封印中).fontSize(FontSize.md).fontColor(this.getColors().textSecondary)Text(距离开启还有 this.countdownText(capsule.unlockAt)).fontSize(FontSize.sm).fontColor(this.getColors().primary)}洗到封印中的胶囊不是失败——而是提醒。它告诉用户你还有一个胶囊在等待增加期待感。这比只展示已开启的胶囊更有意义——随机性包括了还没到时间的情况。四、卡片的视觉设计大卡片模式// 推断的洗牌卡片结构Column(){// 心情 emojiText(capsule.mood).fontSize(72).margin({bottom:Spacing.lg})// 内容Text(capsule.content).fontSize(FontSize.lg).fontColor(this.getColors().text).lineHeight(28).textAlign(TextAlign.Center).margin({bottom:Spacing.lg})// 标签Flex({wrap:FlexWrap.Wrap,justifyContent:FlexAlign.Center}){ForEach(capsule.tags,(tag:string){Text(#tag).fontSize(FontSize.sm).fontColor(this.getColors().primary).margin({right:Spacing.xs,bottom:Spacing.xs})})}// 时间信息Text(this.formatDate(capsule.createdAt)).fontSize(FontSize.xs).fontColor(this.getColors().textSecondary).margin({top:Spacing.md})}.width(100%).padding(Spacing.xl).backgroundColor(this.getColors().surface).borderRadius(BorderRadius.lg)和 CapsuleCard 的对比特性CapsuleCard列表洗牌卡片尺寸小卡片紧凑大卡片宽松内容截取前50字完整内容心情小 emoji20px大 emoji72px信息密度高多字段低聚焦内容用途快速浏览沉浸阅读洗牌卡片的核心是沉浸感——大字号、大 emoji、大间距。用户不需要快速扫描信息而是坐下来慢慢读一篇来自过去的信。五、翻页交互下一张按钮// 推断的翻页 UIRow(){Text( 再来一张).fontSize(FontSize.md).fontColor(#FFFFFF).fontWeight(FontWeight.Bold)}.width(100%).height(48).justifyContent(FlexAlign.Center).backgroundColor(this.getColors().primary).borderRadius(BorderRadius.lg).onClick(()this.nextCard())nextCard 的逻辑privatenextCard():void{this.currentIndexif(this.currentIndexthis.queue.length){// 队列用完重新洗牌this.initQueue()}}队列用完后自动重新洗牌——用户可以无限刷。不会出现没有更多了的死胡同。重新洗牌时顺序重新随机——用户不会看到完全相同的顺序。进度指示// 推断的进度显示Text(${this.currentIndex1}/${this.queue.length}).fontSize(FontSize.xs).fontColor(this.getColors().textSecondary)显示3 / 12格式——告诉用户当前是第几张总共多少张。进度感让用户知道还剩多少没看控制浏览节奏。六、空状态处理没有胶囊时的洗牌页if(this.queue.length0){Column(){Text().fontSize(64).margin({bottom:Spacing.md})Text(还没有胶囊可洗).fontSize(FontSize.lg).fontColor(this.getColors().text).fontWeight(FontWeight.Bold).margin({bottom:Spacing.sm})Text(先去封印几个胶囊吧).fontSize(FontSize.sm).fontColor(this.getColors().textSecondary)}.width(100%).layoutWeight(1).justifyContent(FlexAlign.Center)}和首页空状态同样的设计模式——图标标题引导文字。但没有开始封印按钮——洗牌页是独立页面跳转回首页再切到封印 Tab 比直接改 currentTab 更合理因为 currentTab 是首页的状态洗牌页无法直接修改。只有一个胶囊时只有一个胶囊也能洗——洗牌结果就是它自己。不需要特殊处理——Fisher-Yates 对单元素数组不做任何交换直接返回原数组。七、记忆洗牌的心理设计随机性的价值模式用户心理适用场景时间线有序“我想找某个特定胶囊”有目的的回顾洗牌随机“我想看看命运给我什么”无目的的偶遇随机性创造了命运感——用户觉得是命运让我看到这个胶囊而不是我选择了这个胶囊。这种心理让回顾历史变得更有仪式感——就像翻开一副随机抽到的塔罗牌。为什么洗牌比列表更有温度列表是我管理数据的心态——用户像管理员一样浏览记录。洗牌是我和记忆对话的心态——用户像读者一样接收来自过去的信。产品设计的目标是后者——时光胶囊不是一个数据库管理工具它是一个情感载体。八、页面的嵌套结构Column 页面容器 ├→ Row (标题栏) 返回按钮 标题 ├→ Column (内容区) │ ├→ 洗牌卡片 心情 内容 标签 │ ├→ 进度指示 3 / 12 │ └→ 下一张按钮 再来一张 └→ 底部信息 创建时间嵌套深度 3 层——最浅的页面之一。因为洗牌页只有一个核心功能——展示一张卡片。不需要复杂布局Column 纵向排列就够了。九、与其他功能的数据关系洗牌页的数据来源Database.getAllCapsules() → shuffleArray() 洗牌 → queue[] 洗牌队列 → queue[currentIndex] 当前展示洗牌页不修改数据库——它是只读的。用户在洗牌页看到胶囊后如果想开启需要跳转到详情页操作。洗牌页只负责展示不负责操作。从洗牌到详情的跳转// 推断点击洗牌卡片跳转详情.onClick((){setCurrentCapsuleId(capsule.id)router.pushUrl({url:pages/CapsuleDetail,params:{capsuleId:capsule.id}})})洗到一个胶囊后用户可以点进去看详情。跳转逻辑和首页完全一致——setCurrentCapsuleIdrouter.pushUrl。复用同一套路由模式。十、洗牌功能的扩展可能当前版本随机展示所有胶囊封印中胶囊特殊展示队列用完自动重新洗牌点击跳转详情页未来可能的扩展扩展方向实现方式价值按心情筛选queue capsules.filter(c c.mood ‘’)“我想看开心的回忆”按时间筛选queue capsules.filter(c c.createdAt oneWeekAgo)“我想看最近的回忆”分享卡片Canvas 截图 分享 API把回忆分享给朋友每日推送每天随机展示一个胶囊被动回忆增加打开率当前版本足够简单——但架构为扩展留了空间。只需要改initQueue()的筛选逻辑就能实现按心情/时间筛选。不需要改页面 UI——卡片展示逻辑不变。