【deepseek实战·28】倒计时看板:多个日子一起盯,环形进度走到零,Web Audio 替你响一声

0 阅读12分钟

开篇回顾

27 期做了元素周期表探索器,118 个元素点开看属性、能发音、能限时测验。26 期是共享歌单协作器,核心是零后端共享存储。28 期回到"每天都会用得上的小工具"——上期预告里说的**"时间的基本构成"**:倒计时看板。

一个页面同时盯好几个日子(妈妈生日、期末考、交稿 deadline),每个事件一个环形进度,精确到秒在走;走到零,浏览器桌面通知弹一下,Web Audio 现场合成两声"叮咚",卡片上留下一枚"✅ 已完成"徽章。四轮对话、~186 积分,全程在造物台完成。

引子

倒计时这类东西我写过很多个了:番茄钟是倒计时,投产前上线是倒计时,连"距离我不再年轻"都可以是个倒计时。但它们有个共同的毛病——一次只能盯一个。真到了要用的那天,你会同时有好几个日子在逼近:三个 deadline、一场考试、家里人的生日,然后就开始在手机日历里来回横跳。

我想做个同屏全盯的看板:加几个事件就有几个环,最急的自动排最前,逾期的不消失只是沉底置灰。每个环走完一圈,那天就到了。

幕一 · 一句话需求,AI 先拍板 10 条取舍

新建对话,需求发出:

"帮我做个盯日子的倒计时看板:能同时添加好几个事件(生日、考试、deadline 都行),每个事件画一个环形进度显示还剩多少,能在按天和按小时之间切换"

这轮它没直接写代码,先甩出一份**《需求要点》**,10 条取舍每条都指向一个具体的技术决策。我挑几条说:

  1. 环的含义被重新定义:"环的『满』= 你添加这个事件的那一刻,『空』= 到点的那一刻。"环形代表"从你添加它到现在,这条时间条还剩多少比例",每个事件自己跟自己比,不用外部固定窗口。它自己把代价也写出来了:同一个 deadline,两个人不同时间添加,环的饱满度会不一样。
  2. 天/小时切换是全局一个开关(看板顶部统一切换),不是每张卡各切各的。默认按天。
  3. 排序规则钉死:钉住的最前(钉住的内部按目标时间从近到远)→ 其余未过期按目标时间升序 → 已过期沉底置灰、不自动删除。
  4. 数据只存本浏览器 localStorage,免登录打开即用,换设备不跟随。
  5. 编辑改目标时间时不重置起点(createdAt 保留),删前必须二次确认。
  6. 提醒 = 桌面通知 + 页面内提示条 + WebAudio 合成提示音,不引外部音频文件;只触发一次,用 reminded 标记写回 localStorage,刷新不重复响。
  7. 模板库只是"预填",不绕过表单校验。
  8. 完成徽章和提醒同一次触发,之后把时间改回未来也不摘徽章——"徽章是『这个日子你等到了』的纪念"。
  9. 导出海报用 canvas 手绘,不引截图库、不引外部库。
  10. 农历节日写死 2025–2030 的公历映射表,取不到就退回近似公历日期。

110 秒后交货(~80 积分预估内),三个文件 index.html / style.css / app.js,base.css 没动。

幕二 · 第一版:环已经在屏上走了

第一版成品:

预览运行界面:

初始版的交互演示(拖拽看卡片、点"小时"切精度):

第一版就把核心做完了——加事件、环形进度、天/小时切换、编辑、删除、空状态、每秒刷新都在。而且它主动坦白了两个自己加的、没问我的决定:

  1. 首次打开会放两条示例事件(「示例 · 期末考」「示例 · 交稿 deadline」),方便一眼看懂怎么用,删掉即可;
  2. 环的过渡用 0.9s 线性,让秒针走起来是平滑的而不是一跳一跳。

第二条我留下了。第一条我让它改了——一个要发给别人用的东西,第一次打开塞两条假数据,装的是自己的日程,说的却不是自己的话。用户加第一个事件之前,屏幕上躺的是别人的 deadline。

幕三 · 三轮迭代:清示例 → 提醒+模板 → 徽章+钉住+海报

迭代 1:把示例数据清掉。 这一轮的回话很短,也很诚实——"核心功能其实上一轮已经全部做完了,这轮我只做了一处调整:去掉了首次打开自动塞进去的两条示例事件"。19 步的深度思考,最后产出的是删除。对一个刚做完的东西保持这种克制,我挺受用。

迭代 2:提醒 + 模板库。 这一轮拆出了两个新文件:templates.js(模板数据)和 reminder.js(通知权限/桌面通知/提示音/toast)。

🔔 提醒:点按钮申请权限,按钮会变成「🔔 提醒已开」/「🔕 通知被拒」/「🔕 不支持通知」。事件归零那一刻——桌面通知(标题是「🎂 妈妈生日 到点了!」)+ 浏览器合成两声"叮咚"+ 页面底部滑入一条提示条(可手动关,9 秒自动消失)。只响一次,响过记进数据里,刷新不重复响;把时间改到未来则重新允许提醒。通知被拒或浏览器不支持时不报错,页面内提示条和声音照常工作。

📋 模板库:分「法定节日」(元旦、春节、清明、劳动节、端午、中秋、国庆、圣诞)和「考试节点」(高考、中考、四六级、考研初试、期末考)两组,每项显示名称 + 今年日期,今年已过就自动顺延到明年。它在这里又主动标了"约":四六级按"6 月/12 月第三个周六"、考研按"12 月最后一个周六"推算,中考按 6/15 近似——"这些日期每年官方会微调,加之前你扫一眼"。

迭代 3:完成徽章 + 钉住置顶 + 导出海报。 新增 poster.js,其余改在 app.js / style.css / index.html。

✅ 完成徽章:归零那一刻,卡片上出现绿色「✅ 已完成」标签,和提醒是同一次触发。之后把时间改到未来,徽章也不摘。 📌 钉住置顶:每张卡右上角一个平时灰着、悬停才亮的图钉。钉住的卡左侧出现主题色竖条、边框阴影变蓝,永远排最前(钉住优先于"逾期沉底"——钉住的逾期事件也留在最前)。状态存 localStorage。 🖼 导出海报:点一下用 canvas 现场画一张 720 宽的竖版海报并下载 PNG——顶部标题 + 生成时间 + 事件总数,中间每行一个小环形 + emoji 名称 + 剩余 + 目标时间(钉住的带蓝条、完成的带徽章),底部一行水印。海报里的环形和单位跟着你当前的「按天/按小时」设置走。超过 12 个事件只画前 12 个并弹提示;一个事件都没有时按钮禁用。

最终版演示:

这期 AI 的收尾话术稳定发挥,四轮都以同一句结尾:"我没法看到页面实际效果/我没法看到实际画面,你打开看看……哪里别扭告诉我。"冒烟测试无 console error。诚实说明一处上期预告没完全兑现的地方:我说过"常驻桌面角落",交付的是网页内固定在底部的 toast 提示条,不是系统级桌面挂件——纯前端拿不到那个能力,这点它没含糊,我也没替它圆。

🔧 技术拆解

从对话记录里提炼 5 个能带走的点:

  1. 环的分母是自造的产品决策,不是数学必然。核心就三行:

    const remaining = target - Date.now();
    const total     = target - createdAt;              // 分母 = 事件自己的存在时长
    const remainFrac = total <= 0 ? 0 : Math.max(0, Math.min(1, remaining / total));
    

    环形进度最常见的做法是拿一个固定窗口当分母(今天到目标、月初到月底)。这里换成 target - createdAt,环就成了"这个日子已经过去多少、我还剩多少",每个事件自带归一化,天然不用配窗口。代价 AI 自己写在文档里了:不同人不同时间添加同一个 deadline,环饱满度不一样。如果你做的是"团队统一截止日"的产品,这个设计反而是错的——它该用固定窗口。分母的选择就是产品定位,别当成实现细节。

  2. SVG stroke-dasharray + stroke-dashoffset 是最省事的环形进度。不用 canvas、不用第三方库:

    const C = 2 * Math.PI * r;        // 半径 70 → 周长约 439.8
    el.style.strokeDasharray  = C;    // 虚线段 = 整圈
    el.style.strokeDashoffset = C * (1 - remainFrac);  // 亮着的长度 = 剩余
    

    更关键的是它给这两个属性加了 0.9s linear transition。每秒 tick 改一次 dashoffset,视觉上就是一个平滑扫动的秒针;没有 transition 就是每秒跳一格。后者更"准确",前者才叫"能看"。

  3. 每秒 tick 只改两处 DOM,不整卡重绘。文档里专门列了这条边界情况:定时器每秒重算,但不要每秒整卡重绘 DOM——只更新每张卡的数字文本和 stroke-dashoffset;只有"刚有事件过期"才重排一次顺序。

    // 文档里的排序钉死逻辑:四个关键字依次短路
    events.sort((a, b) => {
      if (!!a.pinned !== !!b.pinned) return a.pinned ? -1 : 1;  // 钉住最前
      const ae = a.target <= now, be = b.target <= now;
      if (ae !== be) return ae ? 1 : -1;                       // 未过期在前
      return ae ? b.target - a.target : a.target - b.target;   // 逾期按最近过期在前
    });
    

    这是定时器类功能的第一性原理:能局部改就别重建,重建会连带丢失 CSS transition、丢失焦点、丢失 hover 态,还白烧一遍 GC。

  4. reminded / done 双标记,把"提醒"和"纪念"拆成两件事。reminded 归零时置 true 并写回 localStorage(幂等,刷新不重复响);把目标时间改到未来时 reminded 重置为 false 以便再次提醒,但 done 不撤销。这一个"重置但不撤销"的细节,让徽章真正变成了纪念而不是状态。状态字段该分成"当前状态"和"历史事实"两类,只做前者就永远做不出纪念感。

  5. 全局精度开关 + 双层数字兜底,解决"0 天"的尴尬。大数字按全局单位走(按天 = Math.floor(remaining/DAY)),下方永远有一行精确小字「还有 X天 Y时 Z分」(0 段省略,<1 分钟显示"即将到来")。设计文档里的原话是这么写的:

    这样切到按天、还剩 5 小时时不会显示一个刺眼的"0 天"。

    大数字负责一眼可读,小字负责不撒谎,两层信息各司其职。单一精度 + 四舍五入是大多数倒计时做得难看的原因。

顺带记一个工程细节:这期素材录制时,脚本合成最终版 mp4 报 height not divisible by 2 (1000x845)——H.264 的 yuv420p 要求宽高都为偶数,而 iframe 高度是随内容走的奇数。GIF 用的 scale=720:-1 恰好也翻成偶数所以没暴露。修法是加一层 -vf scale=trunc(iw/2)*2:trunc(ih/2)*2。偶发故障最好定位到"哪一步的参数是输入相关的",而不是加重试。

代码视图

六个文件:index.html(标题栏/分段控件/工具条/卡片网格/两个 <dialog>:表单 + 模板库/toast 容器)、style.css(环形、卡片网格、紧迫度绿橙红配色、钉住竖条、徽章、响应式)、app.js(状态 + localStorage + 渲染 + 每秒 tick + 增删改 + 钉住 + 徽章)、templates.js(节日/考试节点 + 农历公历映射表 2025–2030)、reminder.js(通知权限 + 桌面通知 + WebAudio 提示音 + toast)、poster.js(canvas 手绘竖版 PNG 下载)。零外部依赖,不引任何 CDN。

收尾

从"手机日历里来回横跳",到"同屏全盯、到点自动响、留着纪念徽章、能导出成图发给朋友",四轮对话(110s + 66s + 44s + 45s,~186 积分),无返修。28 期兑现了上期预告里的"多目标并行"和"完成音效",没兑现的是"常驻桌面角落"——那是网页内的提示条。

🔗 点击体验我的作品:时光看板 · 倒计时

推荐的体验路径:打开先点「📋 模板」加一个春节(看它怎么处理"今年已过就顺延")→ 点「🔔 提醒」授权 → 手动加一个 1 分钟后的事件等它归零,看通知、提示音、底部提示条三件套是不是都出来 → 卡片上点图钉看它跳到最前 → 点「🖼 导出海报」把看板存成图。因为开了「允许别人改造」,你可以复制到自己的造物台继续加功能——比如接一个日历订阅,或者把逾期沉底改成"下周再提醒一次"?

诚实说明:数据只存在你自己浏览器的 localStorage 里,换设备/换浏览器不跟随,也没有云端备份;环的进度基准是你添加事件那一刻,所以把 deadline 改到未来时环会先顶满再往下走;模板库里农历节日写死了 2025–2030 的公历映射,超出年份退回近似;四六级、考研、中考、期末考是按规则推算的近似日期,标了"约",加之前请自行核对官方公告。

利益相关:造物台是作者团队的产品。本文侧重记录 AI 编程的实战过程,链接仅作可交互 demo 体验。

下期预告:倒计时是把时间当"剩余量"在看,29 期换个方向——把时间当"发生过的事"看,做个时光胶囊 / 回忆相册类的东西,只准写不许改的那种。

抛个问题:你现在手机里同时盯着几个日子?如果给你一个只能留 3 个环的位置,你会留哪三个?评论区聊聊,我让 AI 照最高票的加进模板库。