会拍你的番茄钟:我在 Rokid Glasses 上写了一个会「盯」着你的 AIUI 智能体

戴上眼镜专注 25 分钟:我用 AIUI 写了一个会「盯」着你的番茄钟

一个跑在 Rokid Glasses 上的 AIUI :说一句话就能改设置,摄像头会拍你,陀螺仪会管你的脖子。


一、项目简介

解决什么问题

番茄工作法的规则很简单:专注 25 分钟,休息 5 分钟。但真正执行起来,失败点几乎从来不在"不知道方法",而在三件小事上:

  1. 手会去摸手机。 手机上装着番茄钟 App,所以"开始专注"这个动作本身就发生在干扰源上。
  2. 设置很麻烦。 想临时把专注时间从 25 分钟改成 45 分钟,要解锁、找 App、点进设置、滑轮盘------还没开始专注,注意力已经碎了。
  3. 久坐伤颈椎。 计时器只管时间,不管人。连续种了四颗番茄,脖子已经僵了,没人提醒。

PomodoroTimer(番茄钟) 是把这三件事一起解决的尝试。它不是一个"把手机 App 搬到眼镜上"的移植,而是利用智能眼镜永远在视野里、永远在头上这两个物理特性,重新设计了一遍番茄钟:

  • 时间倒计时悬浮在视野角落,抬头就能看到,不需要拿起任何东西;
  • 设置调整全部走语音,一句话改时长、开白噪音、开专注模式;
  • 开启"AI 监督"后,眼镜会每隔几秒静默拍一张照,用端侧大模型判断你是否在玩手机,是就白屏警告;
  • 每完成 N 颗番茄,陀螺仪引导你做一次米字操,用头部转动去追屏幕上的光点。

目标用户

  • 长时间伏案的知识工作者:程序员、设计师、写作者、学生;
  • 已经在用手环/手表做番茄钟,但希望"提醒更贴身"的人;
  • Rokid Glasses 用户中希望有一个能真正用起来、而不是三天就删掉的效率类 Agent。

一句话概括

手机上的番茄钟需要你去打开它;眼镜上的番茄钟,是在你需要的时候自己出现。


二、场景与用户意图

场景一:坐下来准备开工

用户戴上眼镜,说一句 「乐奇,打开番茄钟」。首屏直接进入计时界面:绿色倒计时数字、20 格进度条、一行🍅图标,倒计时自动开始。同时 TTS 会随机播一句开场白------代码里写了 11 句,从「嗨,准备好开始专注了吗?」到「先完成再完美,迈出第一步」随机挑一句。

智能体如何承接 :onLoad 里同步读取本地设置 → 立刻启动 setInterval 秒表 → 初始化 IMU → 判断是否播放开场白。所有异步的存储读取都不阻塞首屏渲染,这是刻意的设计:用户戴上眼镜的前一秒,计时就必须已经在走。

场景二:临时想改成 45 分钟

用户说 「乐奇,工作四十五分钟」。

这里有一个关键的工程细节:用户说的是中文数字"四十五",不是阿拉伯数字。而且口语可能是「专注四十五分钟」「工作时间改成四十五分钟」「搞个四十五分钟的」------表述完全没有边界。

智能体如何承接 :先走关键字正则解析 (零延迟),cnNumToInt() 把中文数字转成整数;如果正则没命中,才把整句话交给端侧大模型 做意图解析,返回结构化 JSON。命中后立刻改设置、持久化,并且如果当前正处在工作中,就按新时长重开这一段计时------用户不需要退出重进。

场景三:专注模式开着,忍不住低头看手机

用户事先说了 「乐奇,监督我不玩手机」 。接下来每隔 2 秒,眼镜会静默拍一张照,交给端侧模型判断。一旦判定画面里的人在玩手机,屏幕瞬间被一张全屏遮罩糊住,视野被强行收窄,同时语音提示。遮罩持续 3 秒后自动消失,之后检测间隔拉长到 5 秒------给用户时间把手机放下。

场景四:种完第 4 颗番茄,该活动脖子了

用户设定了"每 2 颗番茄提醒一次颈椎"。第 2 颗番茄结束、进入休息时,屏幕切成黑底,出现 3×3 光点和一条引导线,光环从中心飞向目标点,用户用头部转动把光点准星推过去。8 个方向走完一遍,弹出「✓ 做操完成!」。

场景五:摘下眼镜收工

按下返回键,屏幕不是直接退出,而是一句随机鼓励语 + 「今天共种下了 N 个番茄,要好好休息哦~」,3 秒后自动退出应用。

这五个场景构成了这个项目的全部产品逻辑:开工、调参、防分心、护身体、收工。后面所有的技术实现,都是为这五件事服务的。


三、方案设计

3.1 AIUI 智能体定义

AIUI 是 Rokid 智能硬件上的应用开发框架,运行时叫 Ink ,模板使用 WXML/WXSS 方言 ,页面推荐用 .ink 单文件组件(SFC)编写。智能体的身份与能力声明写在 AGENTS.md 里:

markdown 复制代码
# 番茄钟 (Pomodoro Timer)

一个简洁的番茄工作法计时器,支持工作/休息切换、番茄数量统计和每日累计时长记录。

## Capabilities
- **Permissions**: microphone / camera / network / audio

全局配置在 app.json,注册三个页面并声明设备权限:

json 复制代码
{
  "pages": ["pages/index/index", "pages/settings/settings", "pages/stats/stats"],
  "window": {
    "navigationBarTitleText": "番茄钟",
    "navigationBarStyle": "custom",
    "viewport": { "width": "device-width" }
  },
  "permissions": ["GEOLOCATION", "CAMERA", "RECORD_AUDIO", "INTERNET"]
}

能力调用关系可以画成一张图:

scss 复制代码
                    ┌───────────────────────────────┐
                    │   onVoiceWakeup(语音唤醒)      │
                    └───────────────┬───────────────┘
                                    │
                     ┌──────────────┴──────────────┐
                     │  SpeechRecognition(ASR)     │
                     └──────────────┬──────────────┘
                                    │ 识别文本
                     ┌──────────────┴──────────────┐
                     │  ① 关键字正则(零延迟)        │
                     │  ② LanguageModel(兜底)      │
                     └──────────────┬──────────────┘
                                    │ 结构化命令 JSON
        ┌───────────┬───────────┬───┴───────┬────────────┬───────────┐
        ▼           ▼           ▼           ▼            ▼           ▼
    改时长/TTS  白噪音开关   专注模式   AI 监督开关   米字操      系统存储
   (wx.speech) (AudioPlayer) (Gyroscope)  (Camera +   (Gyroscope  (wx.setStorage)
                                          LanguageModel) + Canvas)

3.2 页面与信息架构

页面 行数 职责 主要能力
pages/index/index.ink 2716 计时主界面、专注遮罩、米字操、语音交互、AI 监督 陀螺仪、TTS、Canvas、Camera、ASR、LLM、AudioPlayer
pages/settings/settings.ink 950 11 张卡片横向焦点导航 + 4 种弹窗 陀螺仪手势、按键事件
pages/stats/stats.ink 446 2.5D 等距菜园可视化 Canvas 2D

三个页面之间没有全局事件总线 ,跨页通信靠 storage 里的标记位(_resetTimer、_showBye、_testNeckExercise、_workActive),由目标页在 onShow 时读取并消费。

3.3 状态流转

计时主界面是一个很小的状态机,但有几个"分支"值得说明:

vbnet 复制代码
      ┌──────────┐   倒计时归零   ┌──────────┐
      │  工作中   │ ───────────▶ │  休息中   │
      │  work    │ ◀─────────── │   rest   │
      └────┬─────┘   倒计时归零   └────┬─────┘
           │                          │
     番茄 +1、写存储              检查颈椎保护周期
           │                          │
           │                    ┌─────┴─────┐
           │                    ▼           ▼
           │              「锻炼」模式   「仅提醒」模式
           │              启动米字操    弹提示框
           │
   ┌───────┴────────┬──────────────┐
   ▼                ▼              ▼
专注模式 ON      AI 监督 ON      返回键
跳过提醒横幅     每 2s 拍照判定    告别屏 → 退出
+ 跳过语音      判分心则弹遮罩

关键设计:专注模式和普通模式走两条不同的路径。 开启专注后,阶段切换时不弹提醒横幅、不播语音,直接在 3 秒后恢复计时。因为专注模式的产品含义是"我要把自己和外界隔开"------这时候弹一个全屏横幅、念一句「该休息了」,本身就是干扰。

3.4 数据设计

本地存储用 5 个 Key,全部走 wx.getStorageSync/setStorageSync:

Key 类型 内容
tomato_settings_v1 object 全部用户设置(时长、各开关、AI 监督目标)
tomato_stats_v1 number 累计专注秒数
tomato_today_v1 {date, sec} 当日专注秒数(带日期戳,跨天自动归零)
tomato_count_v1 {date, count} 今日番茄数
tomato_total_v1 number 累计番茄数

所有读取都包了一层 normalizeSettings() 做字段级校验与兜底:类型不对、超范围、枚举值非法,一律回落到默认值。这样即使用户手动改坏了存储,或者未来版本字段变更,应用也不会崩,只是恢复默认。


四、AIUI 实现

4.1 项目结构

bash 复制代码
PomodoroTimer/
├── AGENTS.md                 # 智能体身份 + 权限 + 技能声明
├── app.json                  # 页面注册、窗口配置、权限
├── app.js                    # 全局生命周期 + 工具函数
├── package.json
├── assets/
│   ├── audio/ezyZip.mp3      # 白噪音
│   └── images/warning.png    # 专注遮罩图
└── pages/
    ├── index/index.ink       # 主页面(2716 行)
    ├── settings/settings.ink # 设置页(950 行)
    └── stats/stats.ink       # 统计页(446 行)

.ink 单文件组件把四件事收在一个文件里:<script def> 页面配置、<script setup> 逻辑、<page> 模板、<style> 样式。

4.2 三个值得单独讲的关键实现

① 混合意图解析:关键字优先,大模型兜底

这是整个语音链路里我最满意的一处设计。纯规则方案扛不住口语的多样性,纯大模型方案又慢(用户说完要等 1--2 秒),所以做成两级:

javascript 复制代码
runVoiceCommand(text) {
  if (voiceBusy) return;
  voiceBusy = true;
  this.stopListenAnim();
  if (!text) { this._voiceDone({ cmd: 'none' }); return; }

  // ① 关键字优先:零延迟,命中直接响应
  var hit = this.keywordParse(text);
  if (hit.cmd !== 'none') { this._voiceDone(hit); return; }

  // ② 大模型兜底:进入"思考中"动效
  this.startProcessingAnim();
  if (IS_LM_REAL && this._voiceLm) {
    this._voiceLm.prompt(text)
      .then(raw => self._voiceDone(self.parseLmResult(raw)))
      .catch(() => self._voiceDone({ cmd: 'none' }));
  } else {
    this._voiceDone({ cmd: 'none' });
  }
}

大模型侧的 system prompt 写死了允许的命令白名单,并强制只输出 JSON:

javascript 复制代码
var LM_SYSTEM_PROMPT =
  '你是番茄钟的语音命令解析器。用户口语可能不标准,请解析成对番茄钟可执行的结构化命令。' +
  '只输出一个 JSON 对象,不要输出其他内容。\n' +
  '允许的命令:\n' +
  '- 修改工作时长:{"cmd":"set_work_min","value":15到60之间的整数}\n' +
  '- 开启专注:{"cmd":"set_focus","on":true};关闭专注:{"cmd":"set_focus","on":false}\n' +
  '- 开启 AI 监督并声明目标:{"cmd":"ai_supervise_open","targets":["玩手机","看视频"]}\n' +
  '...(其余命令省略)\n' +
  '- 无法解析:{"cmd":"none"}';

返回后用 /\{[\s\S]*\}/ 把 JSON 抠出来再 JSON.parse,容错模型多输出的解释性文字。

中文数字的处理单独写了一个转换函数,覆盖「一二两三...十百千」:

javascript 复制代码
function cnNumToInt(s) {
  if (/^\d+$/.test(str)) return parseInt(str, 10);
  var digits = { '零':0,'一':1,'二':2,'两':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9 };
  var total = 0, n = 0;
  for (...) {
    if (digits[c] !== undefined) n = digits[c];
    else if (c === '十') { total += (n === 0 ? 1 : n) * 10; n = 0; }
    ...
  }
  return total + n;
}
② AI 监督:静默拍照 + 干净会话

"拍你"这件事的技术难点不在调用相机,而在会话管理 和节奏控制。

会话管理 :如果一直往同一个 LLM 会话里塞图片,上下文会越来越长------判定变慢、历史互相污染。这里的做法是维护一个只含 system prompt 的"模板会话" ,每次判定时 clone() 一份干净副本,判定完立刻 destroy():

javascript 复制代码
executeAiJudge(photo) {
  var clone;
  try {
    clone = this._aiLm.clone();       // 干净副本,只含 system 历史
  } catch (e) {
    this._aiLm = null;                 // 模板失效 → 重置后重试
    return this.executeAiJudge(photo);
  }
  var promise = this._aiJudgeOnce(clone, photo);
  promise.then(function () { try { clone.destroy(); } catch (e) {} });
  return promise;
}

静默拍照 :takePhoto 传 enableSystemPreview: false,关闭系统取景预览,后台拍完不闪一下:

javascript 复制代码
cameraCtx.takePhoto({ quality: 'low', enableSystemPreview: false })

节奏控制 是最容易被忽略、但体验差别最大的一处。如果用固定 setInterval 每 2 秒拍一次,那么"拍照 → LLM 判定耗时 1.5s"会让实际周期变成 3.5 秒且抖动。改成递归 setTimeout------上一次判定完成后才开始计时下一次:

javascript 复制代码
_rearmAiDetect() {
  var wait = aiSlowNext ? AI_AFTER_HOLD_MS : AI_DETECT_INTERVAL_MS;
  aiSlowNext = false;
  aiTimer = setTimeout(function () { self.triggerAiDetect(); }, wait);
}

而且判定命中、弹了遮罩之后,下一次间隔从 2 秒拉长到 5 秒------否则用户刚被判分心,还没来得及把手机放下,下一张照片又拍过来了,遮罩会连成一片。

③ 米字操:IMU 投影约束 + 往返模式

"用头部转动去追光点"这件事,如果直接把陀螺仪角度映射到 2D 坐标,用户会到处乱飘,根本对不准。这里的做法是把头部动作投影到当前引导线上------只在一条线的一维空间里移动:

javascript 复制代码
// 约束到当前引导线上(投影 IMU 位移到线方向)
var ldx = tx - EXER_CX, ldy = ty - EXER_CY;
var llen = Math.sqrt(ldx*ldx + ldy*ldy);
ldx /= llen; ldy /= llen;

var rawX = exerYawAcc   * EXER_DEG_SCALE;   // 累积角度 → 像素
var rawY = exerPitchAcc * EXER_DEG_SCALE;
var proj = rawX * ldx + rawY * ldy;          // 投影
proj = clamp(proj, 0, llen);                 // 夹在线段内

exerCrossX = EXER_CX + ldx * proj;
exerCrossY = EXER_CY + ldy * proj;

一段米字操的完整过程是中心 → 目标 → 中心的往返:准星推到目标点(判定距离 24px 内)切到回程,回到中心才算这一个方向完成。8 个方向按「上→下→左→右→左上→右下→右上→左下」的顺序走,每段有 8 秒超时兜底。

视觉上做了这些细节:光环从中心飞到目标的旅行动画(24 帧约 0.8 秒)、移动方向反向的彗星拖尾、到达后的呼吸脉冲、目标切完后的 8 帧冻结期(EXER_READY_FRAMES)防止惯性残留导致直接连跳。

④ 陀螺仪数据的死区 + 泄漏积分

所有用到 IMU 的地方都共享同一套算法。原始角速度直接积分会漂移,所以做了两件事:

javascript 复制代码
// 泄漏积分 + 死区:死区挡住小噪声,泄漏把残留值往零拉
cumPitch *= Math.pow(GYRO_DECAY_PER_SEC, dt);   // 时间驱动衰减,与采样率无关
cumYaw   *= Math.pow(GYRO_DECAY_PER_SEC, dt);
if (Math.abs(gyroY) > GYRO_DEADBAND) cumYaw   -= gyroY * dt * (180 / Math.PI);
if (Math.abs(gyroX) > GYRO_DEADBAND) cumPitch -= gyroX * dt * (180 / Math.PI);

用 Math.pow(decay, dt) 而不是每帧乘一个固定系数,是为了让衰减与采样率无关------设备改用 10Hz 还是 15Hz,手感一致。不同场景用不同的死区和衰减系数(头部手势 0.15/0.74、专注追踪 0.05/0.74、米字操 0.04/0.50),在灵敏度和稳定性之间取不同的平衡点。

4.3 平台限制与绕行方案

Ink 的模板能力比标准小程序更受限,这个项目里有三处绕行值得记下来:

(1)不支持嵌套 ink:for → 20 格进度条和 12 个番茄图标改用扁平布尔字段 (p0..p19、t0..t11),JS 侧生成:

javascript 复制代码
function buildProgress(remain, total) {
  var filled = Math.ceil((remain / total) * 20);
  return { p0: filled >= 1, p1: filled >= 2, /* ... */ p19: filled >= 20 };
}

function mergeProgress(obj, progress) {   // 合并进 setData 对象
  obj.p0 = progress.p0; obj.p1 = progress.p1; /* ... */
  return obj;
}

(2)ink:for 在部分场景渲染不可靠 → 语音设置面板的 8 行改成显式字段 vp0..vp7,逐行独立绑定,换来了稳定的逐行淡入:

html 复制代码
<view class="voice-panel-row" style="opacity: {{ vp0Op }}">
  <text class="voice-panel-label">{{ vp0Label }}</text>
  <text class="voice-panel-value">{{ vp0Value }}</text>
</view>
<!-- vp1 ~ vp7 同理 -->

(3)CSS 动画渲染时序不可控 → 淡入淡出、聆听呼吸、进度闪烁全部改用 setInterval 逐帧写 opacity / height 字符串:

javascript 复制代码
// 聆听动效:连续正弦呼吸,30fps 平滑,不依赖 CSS keyframes
listenAnimTimer = setInterval(function () {
  var t = Date.now() * 0.006;
  var breath = 1 + 0.40 * Math.sin(t);        // 整体呼吸 0.6~1.4
  for (var i = 0; i < 5; i++) {
    var wave = Math.sin(t * 1.3 + i * 0.9);   // 每根错相位起伏
    h.push(Math.max(4, Math.round((base[i] + 2 * wave) * breath)));
  }
  self.setData({ lb0: h[0]+'px', /* ... */ lb4: h[4]+'px' });
}, 33);

五、交互与体验

5.1 主界面:一眼看完的信息层级

黑底、#40FF5E 主色绿、2px 边框、8--12px 圆角,是 AIUI 可穿戴设备的设计规范。主界面从上到下四层:

  1. 20 格进度条------不是常见的填充式进度条,而是 20 个独立方框,一格一秒地"退格",余光扫一眼就知道大概还剩多久,不需要读数字;
  2. 倒计时数字------40px 粗体,视觉重心;
  3. 阶段 + 统计行------「工作中」/「休息中」+ 今日或累计专注时长(可在设置里切换显示哪个);
  4. 🍅 图标行------12 个位置,每完成一颗番茄点亮一个,未满 12 颗时后面跟一个 🌱 幼苗。这个设计把"今天种了几颗番茄"变成了一个具体、有生长感的意象。

画面自动淡出 :主界面显示 5 秒后自动淡出(100ms 渐隐),进入"沉浸态",不干扰视野。此时如果开了「边角显示」,右下角会留一个低亮度的 18px 倒计时(rgba(64,255,94,0.35))。按确认键或按任意键立刻重新淡入------既不挡视线,又随时可查。

5.2 设置页:焦点导航 + 双输入通道

设置页有 11 个条目,横向排成一条卡片带,当前项放大(100×90,3px 边框)+ 高亮,非当前项缩小(70×60)+ 半透明。下方一排小圆点指示位置,再下面是当前项的说明文案。

两套输入通道共用同一个分发函数:

  • 按键 :onKeyUp 里 detectAction(event) 把 event.code 归一化成 prev / next / confirm / back,再按「弹窗优先级 > 主菜单」的顺序分发;
  • 头部手势:左右转头(偏航角超 13°)切换焦点,向下点头(俯仰角超 8°)确认。

两者最终都调用同一个 setSettingFocus(i) / stepSetting()------这是关键,IMU 手势不是按键的模拟,而是和按键平级的输入源,共用一套焦点状态和一套动作分发。

javascript 复制代码
// 点头确认 → 和按下 Enter 键走完全相同的分支
if (NOD_COOLDOWN <= 0 && shakePitch > NOD_THRESHOLD) {
  shakePitch = 0; NOD_COOLDOWN = 10;
  if (self.data.confirmModalVisible) { /* 确认框处理 */ }
  else if (self.data.timePickerVisible) { self.confirmTimePicker(); }
  else if (self.data.modalVisible) { self.stepSettingModal(); }
  else { self.stepSetting(); }          // ← 与按键路径同一入口
}

每个条目都有独立的冷却帧数(转头后 30 帧、点头后 10 帧),防止回正时反向误触发。

5.3 语音交互:把"等待"变成"看得见"

语音交互最难的不是识别,是让用户知道现在发生了什么。这里做了三层反馈:

  1. 实时字幕 ------ASR 的 interimResults 打开,用户说话过程中识别到文字就立刻显示在屏幕中央偏下,让人确认"它在听我说话";
  2. 三态动效 ------同样是 5 根条,靠呼吸速度区分状态:聆听中(速度 0.006)、思考中(0.014,急促)、说话中(0.007,舒缓)。眼睛不需要读文字,看节奏就知道;
  3. 状态面板 ------左下角一块无边框面板,实时列出全部 8 项当前设置(工作时长、休息时长、语音提醒、专注模式、白噪音、边角显示、语音控制、AI 监督)。唤醒时逐行淡入,每行延迟 160ms,8 行约 1.3 秒全部显现。改完设置后,面板内容立即刷新,用户可以对照确认"我改的生效了吗"。

空闲自动消失:15 秒没有任何语音输入,面板和聆听动效一起淡出,不留痕迹。

打断机制:用户可以说「退出」「退下」「别说了」直接中止当前这一轮语音交互------终止识别、停掉所有动效、立即收起面板。这在识别错误、用户想重说的时候非常重要,不然要干等超时。

TTS 时长估算:语音播报没有结束回调,所以按「中文字数 × 260ms」估算播放时长,夹在 900--6200ms 之间,用这个时长控制"说话中"动效和面板的收起时机。

5.4 专注防分心:白色遮罩与迟滞

开启专注模式后,陀螺仪持续追踪头部偏离基准朝向的角度。超过阈值时,一张全屏遮罩盖住画面------「酷炫」模式是全屏警告图,「简洁」模式是纯白 。遮罩 pointer-events: none,不阻挡任何操作,只是把视野糊掉。

灵敏度提供三档:高 30°、中 60°、低 90°。

迟滞(hysteresis)是关键细节 :如果显示和隐藏用同一个阈值,用户头部停在阈值附近时遮罩会疯狂闪烁。所以显示用完整阈值,隐藏用一半阈值:

javascript 复制代码
if (self.data.focusOverlayVisible) {
  var hideThreshold = FOCUS_THRESHOLD * 0.5;    // 已显示 → 回正到一半才隐藏
  var stillAway = Math.abs(focusYaw) > hideThreshold || Math.abs(focusPitch) > hideThreshold;
  if (!stillAway) self.setData({ focusOverlayVisible: false });
} else {
  if (Math.abs(focusYaw) > FOCUS_THRESHOLD || Math.abs(focusPitch) > FOCUS_THRESHOLD) {
    self.setData({ focusOverlayVisible: true }); // 未显示 → 超过完整阈值才触发
  }
}

开启专注模式前会弹确认框,明确告知代价:「开启专注模式后,将无法获取到休息提示以及语音提示,并且会限制你的可视范围,确认要开启吗?」------不做静默的破坏性默认。

5.5 统计页:2.5D 等距菜园

统计页没有用图表,而是用 Canvas 画了一块 4×4 的等距(2.5D)菜园:

  • 地块:菱形土壤,76×38 的等距菱形做顶面,向下偏移 5px 画出"土壤厚度"的侧壁,顶面用四段线性渐变模拟光照,再加犁沟纹理和伪随机碎屑;
  • 已完成的番茄:径向渐变的 3D 番茄球体(高光斑 + 纵向瓣痕 + 5 瓣星形萼片 + 小茎 + 底部椭圆阴影);
  • 进行中的番茄 :一颗幼苗 (两片小叶 + 露珠),只出现在第 count 个地块上------表示"这一颗正在长";
  • 木牌:左上角「番茄农场」,右上角「今日 N 个」,木色底 + 深色边框 + 两颗螺钉 + 支柱。

顶部两行文字显示累计专注时长和累积收获番茄数。把"我今天专注了 3 小时 20 分"翻译成"我收获了 8 颗番茄",是这一页唯一的设计目标。

5.6 收工屏与颈椎提醒

按返回键不直接退出,而是显示一句随机鼓励语 + 「今天共种下了 N 个番茄,要好好休息哦~」,3 秒后自动退出。10 句鼓励语随机播放------给一天的使用一个收尾,而不是一个突然的黑屏。

颈椎提醒有两种模式(可关):

  • 仅提醒:全屏提示「活动一下颈椎吧」,按确认键关闭;
  • 锻炼:直接进入米字操。

周期可设为每 2 / 3 / 4 颗番茄一次,设置页还提供「直接锻炼 💆‍♀️」立即体验一次。


最后一点感想

做完这个项目,我最大的体会是:可穿戴设备的应用设计和手机是两个物种。

手机上的番茄钟,竞争的是"功能是否齐全";眼镜上的番茄钟,竞争的是"它能不能在你需要的时候,用最少的动作出现"。同样一个"改工作时长"的功能,手机上是"解锁 → 找 App → 进设置 → 滑轮盘"五步,眼镜上应该是一句话。同样一个"还剩多久",手机上是点亮屏幕看一眼,眼镜上应该是余光扫到的一格进度。

这个项目里我认为做得最对的一个决定,是把语音做成一等公民而不是附加功能------关键字优先保证常用指令零延迟,大模型兜底覆盖口语变体,三态动效把不可见的等待变成看得见的反馈,设置面板让结果可验证,退出命令让用户可以随时打断。这一整套加起来,才让"对着眼镜说一句话"这件事从"能用的噱头"变成"愿意一直用的交互"。

而做得最"不优雅"但最必要的部分,是那些和平台限制搏斗的代码:扁平布尔字段、显式绑定 8 行、setInterval 手写动画。这些代码不好看,但它们是在真实设备上跑得起来的代价。


相关推荐
高晶1 小时前
Android 计算器开发实战:从界面设计到核心逻辑
程序员·设计
高晶1 小时前
ARM 嵌入式 Linux 系统 U-Boot 开发与移植实战M
设计模式·程序员
AaronMan2 小时前
ZIP 压缩包为什么有时多一层目录?——看懂 ZIP 文件名与内部目录的关系
程序员
楠风测开2 小时前
MySQL 基础 + AI SQL 优化
程序员
Sperains2 小时前
换电脑不用重配:WSL 环境的备份、验证与迁移
程序员
楠风测开5 小时前
Linux 监控告警 + AI 智能监控
程序员
Solara6 小时前
库连续 35 天只写自己的日记:第 36 天的外采,进得了索引,进不了版本库
人工智能·程序员·ai编程
阿里嘎多学长6 小时前
2026-10-14 GitHub 热点项目精选
开发语言·程序员·github·代码托管
合尘猫6 小时前
给客服系统加一道闸:把「该转人工」从模糊判断变成可测拦截
人工智能·后端·程序员