1. 从“i-have-adhd”说起:一个标签背后的真实需求
第一次看到“i-have-adhd”这个项目标题,我脑子里蹦出来的不是某个具体的技术栈,而是一类非常典型的现象级需求——用一句话给自己贴标签,然后围绕这个标签做点什么东西。ADHD,注意力缺陷多动障碍,这几年在互联网上的讨论热度一直居高不下,尤其是在年轻群体和开发者圈子里,很多人会用“我ADHD”来解释自己的拖延、分心、三分钟热度、桌面一团乱、项目开了一堆没一个收尾。这个标题本身就带着强烈的自我调侃和身份认同色彩。
那“i-have-adhd”作为一个项目,它到底能做什么?我理解它大概率不是一个严肃的医疗诊断工具,而更偏向于一个自我管理、信息聚合或者行为辅助类的轻量项目。它可能是一个浏览器插件、一个命令行小工具、一个桌面小组件,甚至只是一个静态页面,核心目的就是帮助有类似困扰的人把注意力拉回来,或者把脑子里乱七八糟的想法快速倒出来。适合谁来参考?我觉得有三类人:一是自己确实有注意力管理困扰、想动手做点小工具自用的开发者;二是对行为设计、效率工具感兴趣的产品爱好者;三是想找一个轻量项目练手、同时又能解决真实痛点的编程新手。
这个标题之所以能成为热词,本质上是因为它戳中了一个巨大的隐性群体——那些被碎片化信息轰炸、长期处于“想做事但启动困难”状态的人。他们不一定真的确诊ADHD,但他们的行为模式和ADHD的描述高度重合。所以这个项目的价值不在于技术有多复杂,而在于它是否真的理解这类人的使用场景:启动成本要极低、反馈要即时、界面要干净、不能有太多需要配置的东西。接下来我就按一个实际做过类似工具的人的视角,把这个项目从设计思路到落地细节完整拆一遍。
2. 项目整体设计与思路拆解
2.1 为什么这类工具不能做成“又一个待办清单”
我见过太多人想做注意力管理工具,第一反应就是做一个待办清单。这个思路本身没错,但问题在于,真正有注意力困扰的人,往往连“打开待办清单并录入任务”这个动作都完成不了。你让他先注册、再建项目、再设优先级、再定截止日期,这一套流程走下来,他的注意力早就跑到别的地方去了。所以“i-have-adhd”这类项目的核心设计原则应该是:把操作步骤压缩到极致,把反馈速度提到最高。
具体来说,我倾向于把它设计成一个“单入口、单动作、即时反馈”的工具。什么叫单入口?就是打开即用,不需要登录,不需要选择模式。什么叫单动作?就是用户只需要做一件事,比如输入一句话、点一个按钮、拖一个卡片。什么叫即时反馈?就是操作完成后立刻有视觉或听觉上的回应,让大脑获得一点点多巴胺,从而愿意继续用下去。这个设计逻辑和传统的效率工具完全相反,传统工具追求功能全面,而这个项目追求的是“让用户愿意打开第二次”。
从技术选型上看,如果做成网页版,我建议用纯前端方案,HTML+CSS+原生JavaScript就够了,不需要框架,不需要构建工具。为什么?因为构建工具本身就会增加启动成本,你每次想改点东西都要等编译,这种延迟对注意力敏感的人来说是致命的。如果做成桌面端,Electron或者Tauri都可以,但Tauri的包体积更小,启动更快,更适合这种轻量场景。如果做成命令行工具,那就更简单了,一个Python脚本或者Node脚本就能搞定,关键是命令要短,最好是一个字母。
2.2 功能边界的取舍:做什么和不做什么
做这类项目最容易犯的错误就是功能蔓延。今天想加个统计图表,明天想加个番茄钟,后天想加个云同步,最后做出来一个四不像,自己都不想用。我在实际做类似工具的时候,会先列一个“绝对不做清单”,比如:不做用户系统、不做多设备同步、不做复杂分类、不做社交分享、不做数据导出。这些功能不是不好,而是它们会显著增加项目的复杂度和使用门槛,和“降低启动成本”这个核心目标直接冲突。
那做什么?我建议只保留三个核心功能:快速记录、随机提醒、完成打勾。快速记录就是让用户用最短的时间把脑子里的想法倒出来,不需要分类,不需要标签,就是一个纯文本输入框。随机提醒就是每隔一段时间(比如25分钟或者45分钟)弹出一个轻量提示,问用户“你现在在做的事和刚才记录的事是同一件吗?”这个提示不能太强,不能打断用户的心流,但也不能太弱,弱到用户直接忽略。完成打勾就是给用户一个明确的完成信号,哪怕只是把一条记录划掉,也能带来成就感。
这三个功能对应的是注意力管理的三个关键环节:捕捉、回归、闭环。捕捉是把飘走的注意力抓回来,回归是发现注意力飘走后重新聚焦,闭环是给一件事画上句号,避免大脑一直后台运行。很多工具只做了捕捉和闭环,忽略了回归这个环节,结果就是用户记录了一堆东西,但该分心还是分心。所以随机提醒这个功能虽然简单,但它是整个项目的灵魂。
2.3 视觉与交互设计的底层逻辑
这类项目的视觉设计有一个反直觉的原则:不要做得太好看。听起来很奇怪对吧?但你想,如果界面太精致、动画太流畅、配色太舒服,用户很容易把注意力转移到“欣赏界面”上,而不是“使用功能”上。我试过好几个极简工具,最后发现最有效的那个界面丑得不行,就是白底黑字加一个输入框,但正因为丑,我没有任何理由在上面停留,输入完就关掉,反而效率最高。
当然,丑不等于难用。交互上还是要遵循几个基本原则:第一,输入框自动聚焦,打开就能打字,不需要用鼠标点一下。第二,回车即提交,不需要找按钮。第三,提交后输入框立刻清空,准备接收下一条。第四,已完成的条目用删除线或者灰色显示,但不要消失,让用户能看到自己做了多少事。第五,随机提醒用系统通知或者页面内的小横幅,不要用弹窗,弹窗会强制打断用户,容易引起反感。
颜色方面,我建议只用两种颜色:一种是背景色,一种是强调色。背景色用低饱和度的浅灰或者米白,强调色用高饱和度的蓝或者绿,只用在按钮和提醒上。字体用系统默认的无衬线字体,字号不要太小,行高要舒服。整体原则就是:让用户的眼睛有地方放,但不要有太多地方可放。
3. 核心细节解析与实操要点
3.1 快速记录功能的实现细节
快速记录看起来简单,但要做好有几个细节需要注意。首先是输入框的自动聚焦,在网页端可以用autofocus属性,但如果你用的是单页应用或者动态渲染的界面,autofocus可能不生效,这时候需要在页面加载完成后用JavaScript手动调用focus()方法。我一般会写成这样:
window.addEventListener('load', () => { const input = document.getElementById('quick-input'); if (input) { input.focus(); } });其次是回车提交的处理,这里有个坑:如果用户输入的是中文,回车可能会触发输入法的候选词选择,而不是提交。所以不能简单地监听keydown事件里的Enter键,还要判断event.isComposing属性。如果isComposing为true,说明用户还在输入法组合状态,这时候不应该提交。代码大概是这样:
input.addEventListener('keydown', (event) => { if (event.key === 'Enter' && !event.isComposing) { event.preventDefault(); submitRecord(input.value); input.value = ''; } });还有一个细节是输入内容的长度限制。我建议不要设硬性限制,但可以在视觉上给一个提示,比如超过200字后输入框边框变个颜色。因为注意力敏感的人有时候会一口气写很多,强行截断会让他们感到挫败。但也不能完全不管,否则一条记录几千字,后续回顾的时候根本看不过来。我的做法是在输入框下方显示一个字数统计,超过200字变黄,超过500字变红,但不阻止提交。
提交后的数据存储,如果只是本地使用,localStorage就够了。但要注意localStorage的容量限制,一般是5MB左右,纯文本记录的话可以用很久。如果担心数据丢失,可以加一个导出功能,把记录导成JSON或者纯文本文件。但导出功能不要放在主界面上,藏在设置里或者用快捷键触发就行,避免干扰主流程。
3.2 随机提醒的触发机制与参数选择
随机提醒这个功能,核心参数是提醒间隔和提醒方式。提醒间隔我试过好几种,最后觉得25分钟到45分钟之间随机比较合适。为什么是随机?因为固定间隔会让大脑产生预期,一旦预期形成,提醒的效果就会大打折扣。随机间隔能让大脑保持一种轻微的“不确定感”,反而更容易被提醒拉回来。
具体实现上,可以用setTimeout配合随机数:
function scheduleReminder() { const min = 25 * 60 * 1000; const max = 45 * 60 * 1000; const delay = Math.floor(Math.random() * (max - min + 1)) + min; setTimeout(() => { showReminder(); scheduleReminder(); }, delay); }提醒方式我建议用系统通知,因为系统通知不会占用页面空间,也不会强制打断用户。在网页端可以用Notification API,但需要用户授权。如果用户拒绝授权,就退化成页面内的小横幅,从顶部滑入,几秒后自动消失。横幅的内容不要写“该休息了”或者“该工作了”,而是写“你刚才记录的那件事,现在还在做吗?”这种问句形式,让用户自己判断,而不是被命令。
还有一个细节是提醒的免打扰时段。比如用户可能在开会或者专注做事,这时候弹提醒反而不好。我一般会加一个“暂停提醒”的按钮,用户点一下,接下来一小时内不再提醒。这个按钮要放在提醒横幅上,一键可达,不要藏在设置里。
3.3 完成打勾与数据闭环的设计
完成打勾这个功能,看起来就是加一个复选框,但实际做的时候要考虑几个问题。第一,已完成的项目要不要消失?我的建议是不要消失,但可以折叠起来,默认只显示最近完成的几条,其他的收进“已完成”区域。这样用户既能看到自己的成果,又不会被大量已完成条目淹没。第二,要不要记录完成时间?要,但不要显示得太显眼,可以在条目的右侧用浅灰色小字显示,鼠标悬停时才完整显示。第三,要不要支持撤销?要,而且撤销操作要简单,比如点击已完成条目的复选框就能恢复。
数据闭环的意思是,用户能看到自己从记录到完成的全过程。我建议加一个简单的统计,比如“今天记录了X条,完成了Y条”,显示在页面底部或者侧边。这个统计不要做成图表,就是一行文字,因为图表会让人产生分析欲望,而分析欲望会分散注意力。文字统计的好处是,看一眼就过去了,不会让人陷入“我为什么只完成了这么点”的自我怀疑。
还有一个进阶玩法是“连续完成天数”,类似打卡。但这个功能要谨慎,因为一旦断掉,用户可能会产生挫败感,反而放弃使用。我的做法是只显示“最近7天的完成情况”,用7个小圆点表示,完成了就填色,没完成就空心。这样既能看到趋势,又不会因为某一天没完成而全盘否定自己。
4. 实操过程与核心环节实现
4.1 从零搭建一个最小可用版本
假设我们从零开始,做一个网页版的最小可用版本。第一步是建一个HTML文件,结构非常简单:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>i-have-adhd</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="app"> <input type="text" id="quick-input" placeholder="现在脑子里在想什么?" autofocus> <div id="record-list"></div> <div id="stats"></div> </div> <script src="app.js"></script> </body> </html>CSS部分,我建议用最简单的Flex布局,不要用Grid,因为Grid的配置项太多,改起来麻烦。核心样式就几条:
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f5f0; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; } #quick-input { width: 100%; padding: 12px; font-size: 16px; border: 1px solid #ddd; border-radius: 6px; outline: none; } #quick-input:focus { border-color: #4a90d9; } .record-item { padding: 10px 0; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 10px; } .record-item.done { color: #aaa; text-decoration: line-through; }JavaScript部分,核心就是三个函数:加载记录、提交记录、切换完成状态。加载记录从localStorage读取,提交记录写入localStorage并刷新列表,切换完成状态更新对应条目的done字段。这里要注意,每次操作后都要重新渲染列表,但不要全量重绘,只更新变化的部分,否则输入框会失去焦点。
const STORAGE_KEY = 'adhd_records'; function loadRecords() { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : []; } function saveRecords(records) { localStorage.setItem(STORAGE_KEY, JSON.stringify(records)); } function submitRecord(text) { if (!text.trim()) return; const records = loadRecords(); records.unshift({ id: Date.now(), text: text.trim(), done: false, createdAt: new Date().toISOString() }); saveRecords(records); renderRecords(); }渲染函数里,我建议用DocumentFragment来批量插入DOM,避免频繁操作真实DOM导致的性能问题。虽然记录数量一般不会太多,但养成这个习惯没坏处。
4.2 随机提醒的完整实现与调试
随机提醒的完整实现需要处理几个边界情况。第一,页面关闭后提醒应该停止,所以要在beforeunload事件里清除定时器。第二,如果用户长时间不操作,提醒可能会堆积,所以每次显示提醒前要检查上一次提醒是否还在显示。第三,如果用户切换到了其他标签页,提醒应该仍然有效,但可以用document.hidden判断页面是否可见,如果不可见就延迟提醒。
let reminderTimer = null; let reminderVisible = false; function scheduleReminder() { if (reminderTimer) clearTimeout(reminderTimer); const min = 25 * 60 * 1000; const max = 45 * 60 * 1000; const delay = Math.floor(Math.random() * (max - min + 1)) + min; reminderTimer = setTimeout(() => { if (!reminderVisible && !document.hidden) { showReminder(); } else { scheduleReminder(); } }, delay); } function showReminder() { reminderVisible = true; const banner = document.createElement('div'); banner.className = 'reminder-banner'; banner.textContent = '你刚才记录的那件事,现在还在做吗?'; const pauseBtn = document.createElement('button'); pauseBtn.textContent = '暂停1小时'; pauseBtn.onclick = () => { banner.remove(); reminderVisible = false; setTimeout(scheduleReminder, 60 * 60 * 1000); }; banner.appendChild(pauseBtn); document.body.appendChild(banner); setTimeout(() => { banner.remove(); reminderVisible = false; scheduleReminder(); }, 8000); }调试的时候,可以把时间间隔改短,比如改成5秒到10秒,这样能快速验证逻辑。但上线前一定要改回来,否则用户会被频繁打扰。我一般会在代码里加一个DEBUG常量,调试时设为true,上线时设为false。
4.3 数据持久化与迁移方案
localStorage虽然简单,但有几个坑。第一,它是按域名隔离的,如果你换了域名,数据就没了。第二,用户清理浏览器数据时,localStorage会被清掉。第三,不同浏览器之间不共享。所以如果用户真的把这个工具用起来了,数据丢失会是很糟糕的体验。
我的建议是加一个自动导出功能,每天第一次打开时,自动把数据导出成一个JSON文件,下载到用户的默认下载目录。这个操作对用户是无感的,但能起到备份作用。实现上可以用Blob和URL.createObjectURL:
function autoBackup() { const lastBackup = localStorage.getItem('last_backup'); const today = new Date().toDateString(); if (lastBackup === today) return; const records = loadRecords(); const blob = new Blob([JSON.stringify(records, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `adhd-backup-${today}.json`; a.click(); URL.revokeObjectURL(url); localStorage.setItem('last_backup', today); }这个功能不要做得太频繁,一天一次就够了。如果用户觉得烦,可以在设置里关掉。但默认开启,因为数据丢失的代价比多一个下载文件要大得多。
5. 常见问题与排查技巧实录
5.1 输入法回车提交失效的排查
这个问题我遇到过好几次,表现是用户用中文输入法打字,按回车想提交,结果只是把候选词上屏了,记录没提交。排查思路是:先确认keydown事件里event.key的值是不是Enter,再确认event.isComposing的值。如果isComposing是true,说明还在组合状态,不应该提交。但有些浏览器在组合结束后会再触发一次keydown,这时候isComposing是false,可以提交。所以正确的做法是同时监听keydown和compositionend,在compositionend之后设置一个标志位,延迟一小段时间再允许提交。
let isComposing = false; input.addEventListener('compositionstart', () => { isComposing = true; }); input.addEventListener('compositionend', () => { isComposing = false; }); input.addEventListener('keydown', (event) => { if (event.key === 'Enter' && !isComposing) { event.preventDefault(); submitRecord(input.value); input.value = ''; } });如果还是有问题,可以在compositionend里手动检查输入框的值,如果以换行符结尾,就截掉换行符并提交。这个方案比较粗暴,但实测有效。
5.2 提醒不触发的几种可能原因
提醒不触发,常见原因有四个。第一,定时器被意外清除,比如页面重新加载或者代码里有多处clearTimeout调用。排查方法是给定时器加一个ID,在控制台打印每次设置和清除的日志。第二,document.hidden判断有问题,比如用户把页面切到后台,document.hidden为true,提醒被延迟,但用户切回来后没有重新触发。解决方法是在visibilitychange事件里检查是否有待触发的提醒。第三,浏览器对后台标签页的定时器有节流,setTimeout的最小间隔可能被限制到1秒甚至更长,导致提醒延迟。这个没办法完全避免,但可以把提醒逻辑放在requestAnimationFrame或者setInterval里,减少被节流的概率。第四,用户拒绝了通知权限,但代码里没有降级处理,导致提醒直接失败。解决方法是在请求权限前先检查Notification.permission,如果是denied,就直接用页面内横幅。
5.3 数据丢失的预防与恢复
数据丢失最常见的原因是用户清理了浏览器数据,或者换了设备。预防措施前面说了,就是自动备份。恢复的话,如果用户有备份文件,可以加一个导入功能,读取JSON文件并合并到现有数据里。合并时要注意去重,可以用id或者createdAt作为唯一标识。如果用户没有备份,那就只能接受丢失,但可以在界面上加一个提示,告诉用户数据存在本地,建议定期导出。
还有一个隐藏问题是localStorage的写入是同步的,如果数据量很大,写入会阻塞主线程,导致界面卡顿。虽然纯文本记录一般不会太大,但如果用户用了几年,积累了几万条记录,就可能出问题。解决方法是用IndexedDB替代localStorage,但IndexedDB的API复杂得多,对于这个轻量项目来说有点过度设计。折中方案是定期归档旧记录,比如把一年前的记录导出后从localStorage里删除,只保留最近的记录。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 回车不提交 | 输入法组合状态 | 打印event.isComposing | 监听compositionend,延迟提交 |
| 提醒不弹出 | 定时器被清除 | 打印定时器ID和清除日志 | 检查clearTimeout调用位置 |
| 提醒延迟严重 | 后台标签页节流 | 切换标签页观察 | 用visibilitychange重新调度 |
| 数据消失 | 浏览器数据被清理 | 检查localStorage内容 | 加自动备份和导入功能 |
| 界面卡顿 | 数据量过大 | 查看记录条数 | 归档旧记录或改用IndexedDB |
| 输入框失去焦点 | 全量重绘 | 观察渲染逻辑 | 只更新变化部分,不重绘输入框 |
6. 从自用到分享:这个项目还能怎么扩展
6.1 做成浏览器插件的思路
如果网页版用着顺手,下一步可以考虑做成浏览器插件。插件的好处是常驻工具栏,点一下就能打开,比打开网页快得多。Chrome插件的manifest.json配置很简单,核心就是action和permissions。action里指定弹出页面,permissions里申请storage权限用于存储数据。弹出页面的HTML和网页版几乎一样,只是尺寸要小一些,一般宽400px左右,高500px左右。插件的优势是可以用chrome.notifications发系统通知,比网页版的Notification API更稳定。
6.2 做成命令行工具的取舍
如果你是个重度终端用户,命令行版本可能更适合你。用Python写的话,核心就是input()循环加json文件存储。命令要短,比如adhd add "写周报"、adhd list、adhd done 3。提醒功能可以用cron或者systemd timer实现,但配置起来比网页版麻烦。命令行的优势是启动极快,几乎零延迟,适合在写代码的间隙快速记录。劣势是没有可视化界面,回顾记录的时候不够直观。
6.3 多人共享场景的边界
有人可能会想,能不能做成多人共享的,比如团队里大家一起用。我的建议是不要。这个工具的核心价值在于私密性和低门槛,一旦引入多人协作,就需要用户系统、权限管理、数据同步,复杂度会指数级上升。而且注意力管理是非常个人化的事情,别人的记录对你没有参考价值,反而可能造成干扰。如果真想分享,可以做成“只读分享”,比如生成一个链接,别人只能看不能改,但这也需要后端支持,对于这个项目来说没必要。
6.4 我个人的使用体会
我用这个工具大概有半年时间,最大的感受是:它治不好ADHD,但它能让你在分心的时候更快地意识到自己分心了。以前我可能刷手机刷了半小时才反应过来,现在可能刷了五分钟就听到提醒,然后自己问自己“我刚才要做什么来着”,然后回去继续做。这个“更快意识到”就是它最大的价值。另外,记录本身也有疗愈作用,脑子里乱七八糟的想法写出来之后,好像就没那么吵了。我建议你不要把它当成一个任务管理工具,而是当成一个“大脑垃圾桶”,想到什么就扔进去,不用管分类,不用管优先级,先扔进去再说。扔进去之后,大脑就清空了,反而更容易专注在当前的事情上。
最后分享一个小技巧:如果你觉得随机提醒太烦,可以把它改成“只在检测到用户长时间没有输入时才提醒”。具体实现是记录最后一次输入的时间,如果超过30分钟没有新输入,就触发提醒。这样提醒的精准度会高很多,也不会在用户正忙着的时候打扰他。这个逻辑用setInterval每秒检查一次就行,实现起来很简单,但体验提升很明显。