全局计时器、智能提醒与UI交互实现
本文分享「灶台导航」小程序中多菜谱烹饪的运行机制,包括全局计时器设计、智能提醒触发逻辑以及页面交互的完整实现。
一、全局计时器实现
1.1 计时器完整代码
这个计时器类支持开始、暂停、继续、停止,并且支持注册多个回调函数:
/** * 多菜谱全局计时器 * 核心功能:统一时间基准、支持暂停/继续、每秒通知所有监听者 */ class GlobalCookTimer { constructor() { this.startTime = null // 开始时间戳(毫秒) this.elapsedTime = 0 // 已经过的秒数 this.intervalId = null // 定时器ID this.isRunning = false // 是否运行中 this.isPaused = false // 是否暂停中 this.pauseTime = 0 // 暂停时的时间戳 this.callbacks = [] // 回调函数列表 } /** * 开始计时 */ start() { this.startTime = Date.now() this.isRunning = true this.isPaused = false this.intervalId = setInterval(() => { this.tick() }, 1000) } /** * 暂停 */ pause() { if (!this.isRunning || this.isPaused) return this.isPaused = true this.pauseTime = Date.now() clearInterval(this.intervalId) }/** * 继续(核心:补偿暂停时间) * 原理:startTime 向后移动 pauseDuration,仿佛暂停从未发生 */ resume() { if (!this.isPaused) return // 计算暂停了多久 const pauseDuration = Date.now() - this.pauseTime // 调整开始时间,补偿暂停时长 this.startTime += pauseDuration this.isPaused = false this.intervalId = setInterval(() => { this.tick() }, 1000) } /** * 停止 */ stop() { clearInterval(this.intervalId) this.isRunning = false this.isPaused = false }/** * 每秒执行一次,更新经过时间并通知回调 */ tick() { this.elapsedTime = Math.floor((Date.now() - this.startTime) / 1000) this.callbacks.forEach(cb => cb(this.elapsedTime)) } /** * 注册回调 */ onTick(callback) { this.callbacks.push(callback) }/** * 获取当前时间(支持暂停时正确返回) */ getElapsed() { if (this.isPaused) { return Math.floor((this.pauseTime - this.startTime) / 1000) } return Math.floor((Date.now() - this.startTime) / 1000) } /** * 销毁 */ destroy() { this.stop() this.callbacks = [] } }1.2 暂停补偿原理图解
用户暂停10分钟的场景: 暂停前: startTime = 12:00:00 当前经过时间 = 300秒 当前真实时间 = 12:05:00 用户点击暂停,10分钟后点击继续: 暂停时长 = 600秒 新startTime = 12:00:00 + 600 = 12:10:00 恢复后计算经过时间: 当前真实时间 = 12:15:00 经过时间 = 12:15:00 - 12:10:00 = 300秒 ✅ 效果:仿佛那10分钟从未存在过,计时器继续从300秒开始
1.3 页面集成代码
// pages/cook/cook.js Page({ data: { schedule: null, elapsedSeconds: 0, currentTasks: [], completedSteps: [] }, globalTimer: null, onLoad(options) { const recipeIds = JSON.parse(options.recipes || '[]') this.loadAndCalculate(recipeIds) }, onUnload() { // 页面销毁时清理计时器,防止内存泄漏 if (this.globalTimer) { this.globalTimer.destroy() } }, async loadAndCalculate(recipeIds) { const recipes = await this.loadRecipes(recipeIds) const schedule = calculateSchedule(recipes) this.setData({ schedule }) this.initTimer(schedule) }, initTimer(schedule) { this.globalTimer = new GlobalCookTimer() this.globalTimer.onTick((elapsed) => { this.onTimerTick(elapsed) }) this.globalTimer.start() }, onTimerTick(elapsed) { const { schedule } = this.data this.setData({ elapsedSeconds: elapsed }) // 获取当前应该执行的任务 const currentTasks = this.getCurrentTasks(elapsed) if (currentTasks.length > 0) { this.setData({ currentTasks }) this.checkNewTasks(currentTasks) } this.checkCompletedTasks(elapsed) // 检查是否全部完成 if (elapsed >= schedule.totalDuration) { this.onAllComplete() } }, /** * 获取当前时间点正在进行的任务 * 核心逻辑:判断 elapsed 是否在步骤的 [start, end) 区间内 */ getCurrentTasks(elapsed) { const tasks = [] this.data.schedule.recipes.forEach(recipe => { recipe.steps.forEach(step => { if (elapsed >= step.globalStartTime && elapsed < step.globalEndTime) { tasks.push({ recipeId: recipe.recipeId, recipeName: recipe.recipeName, step, remaining: step.globalEndTime - elapsed // 剩余秒数 }) } }) }) return tasks } })二、提醒机制实现
2.1 提醒管理器完整代码
/** * 提醒管理器 * 功能:检测关键节点,触发语音提醒,防止重复提醒 */ class ReminderManager { constructor(ttsQueue) { this.ttsQueue = ttsQueue // 语音队列,处理异步播放 this.reminded = new Set() // 已提醒过的任务ID,用于去重 } /** * 检查是否需要提醒(每秒调用一次) */ check(elapsed, schedule) { // 检查即将开始的任务(提前30秒提醒) schedule.timeline.forEach(event => { if (event.type === 'start') { const timeToStart = event.time - elapsed if (timeToStart > 0 && timeToStart <= 30 && !this.reminded.has(`start-${event.time}`)) { this.reminded.add(`start-${event.time}`) this.remindTaskStart(event) } } }) // 检查即将结束的任务(提前10秒提醒) schedule.timeline.forEach(event => { if (event.type === 'end') { const timeToEnd = event.time - elapsed if (timeToEnd > 0 && timeToEnd <= 10 && !this.reminded.has(`end-${event.time}`)) { this.reminded.add(`end-${event.time}`) this.remindTaskEnd(event) } } }) } remindTaskStart(event) { const text = `${event.recipeName}即将开始第${event.step.order}步` this.ttsQueue.add(text) } remindTaskEnd(event) { const text = `${event.recipeName}第${event.step.order}步即将完成` this.ttsQueue.add(text) } reset() { this.reminded.clear() } }2.2 提前量设计说明
| 提醒类型 | 提前量 | 设计原因 |
|---|---|---|
| 任务开始 | 30秒 | 给用户30秒准备时间,比如洗手、拿食材 |
| 任务结束 | 10秒 | 防止烧干锅,10秒足够用户走到灶台前 |
| 剩余时间 | 10分钟/5分钟/1分钟 | 整点提醒,让用户把握整体进度 |
2.3 周期性时间提醒
/** * 周期性时间提醒 * 在关键剩余时间点提醒用户 */ function setupPeriodicReminder(timer, schedule) { // 关键时间节点(秒):10分钟、5分钟、1分钟、30秒、10秒 const reminderPoints = [600, 300, 60, 30, 10] const reminded = new Set() timer.onTick(elapsed => { const remaining = schedule.totalDuration - elapsed reminderPoints.forEach(point => { // 当剩余时间首次小于等于某个节点时触发提醒 if (remaining <= point && remaining > point - 5 && !reminded.has(point)) { reminded.add(point) announceRemaining(remaining) } }) }) } function announceRemaining(seconds) { let text if (seconds >= 600) { text = `还有${Math.floor(seconds / 60)}分钟` } else if (seconds >= 60) { text = `还有${Math.floor(seconds / 60)}分钟` } else { text = `还有${seconds}秒` } ttsQueue.add(text) }三、UI 展示
3.1 页面模板代码
<!-- 多菜谱烹饪页面 --> <view class="multi-cook-container"> <!-- 时间概览 --> <view class="time-overview"> <text class="elapsed">{{elapsedDisplay}}</text> <text class="total">/ {{totalDurationDisplay}}</text> </view> <!-- 进度条 --> <view class="progress-bar"> <view class="progress-fill" style="width: {{progress}}%"></view> </view> <!-- 当前任务列表 --> <view class="current-tasks"> <view class="task-card" wx:for="{{currentTasks}}" wx:key="recipeId"> <view class="task-header"> <text class="recipe-name">{{item.recipeName}}</text> <text class="step-label">步骤 {{item.step.order}}</text> </view> <view class="task-content">{{item.step.content}}</view> <view class="task-footer"> <text class="remaining">剩余 {{item.remaining}}秒</text> </view> </view> </view> <!-- 控制按钮 --> <view class="controls"> <button wx:if="{{!isPaused}}" bindtap="onPause">暂停</button> <button wx:if="{{isPaused}}" bindtap="onResume">继续</button> </view> </view>3.2 UI 交互说明
| UI 元素 | 数据绑定 | 更新频率 | 作用 |
|---|---|---|---|
| 时间概览 | elapsedDisplay | 每秒 | 让用户知道当前进度 |
| 进度条 | progress% | 每秒 | 视觉化整体进度 |
| 任务卡片 | currentTasks | 任务变化时 | 显示当前该做什么 |
| 剩余时间 | remaining | 每秒 | 紧迫感提示 |
| 控制按钮 | isPaused | 点击时 | 暂停/继续计时 |
3.3 任务卡片显示逻辑
可以显示多个卡片:比如红烧肉在炖(wait类型),同时番茄蛋汤在准备(cook类型),两个任务可以并行
剩余时间倒计时:每秒递减,让用户感知紧迫程度
完成任务自动移除:当 elapsed >= globalEndTime 时,该任务卡片消失
四、总结
| 模块 | 核心职责 | 关键代码 | 设计要点 |
|---|---|---|---|
| 全局计时器 | 统一时间基准,支持暂停 | GlobalCookTimer 类 | 暂停补偿算法 |
| 任务检测 | 实时判断当前该做什么 | getCurrentTasks() | 区间比较 + 状态机 |
| 提醒机制 | 关键节点语音播报 | ReminderManager 类 | 提前量 + 去重队列 |
| UI交互 | 信息展示与用户控制 | WXML + 数据绑定 | 多任务卡片 + 进度条 |
整套系统让用户可以"无脑"跟着指引走,系统自动告诉你接下来该做什么、什么时候做、还剩多久,大大降低了多菜同时烹饪的心智负担。
