一、引言

人是靠期待活着的。生日、纪念日、旅行、毕业、节日——这些未来事件给我们时间感,让平凡的日常有了方向。"倒数日"这种 App 之所以流行,原因在于它把抽象的时间变成了具象的数字:"还有 3 天"比"下周三"更有紧迫感,"已过 12 天"比"两周前"更有纪念意义。

从技术角度看,倒数日涉及的核心问题是日期运算——两个日期之间相差多少天、今天是哪个日期、如何判断某个事件是将来还是过去、如何按距离今天的远近排序。这些运算不依赖后端,全部可以在前端用 JavaScript 的 Date API 完成。

本文将用 ArkUI 从零构建一个倒数日追踪器。功能包括:添加事件(名称 + 图标 + 目标日期)、自动计算剩余天数、四色距离标记(今天/明天红色、3 天内品红、7 天内蓝色、更远绿色)、已过事件灰色标记、自动按距离排序(最近的排最前)。配色严格避免了黄色系(红/品红/蓝/绿四色方案),确保所有文字与背景保持足够对比度。

阅读完本文,你将能够:

  • 使用 Date API 进行日期差值计算
  • 实现四色距离标记系统(根据剩余天数动态着色)
  • 构建按距离自动排序的列表
  • 处理"已过事件"的特殊显示逻辑
  • 构建图标选择器 + 日期输入的表单弹窗

二、数据模型与日期运算

2.1 事件实体

interface CountdownEvent {
  id: number;
  name: string;
  icon: string;
  date: string; // YYYY-MM-DD
}

日期用 YYYY-MM-DD 格式存储——与日记本相同策略。8 个预设图标涵盖常见事件类型:生日🎂、纪念日💍、毕业🎓、旅行✈️、搬家🏠、节日🎄、庆祝🎉、自定义📅。

2.2 核心日期函数

两个函数构成日期系统的全部基础:

function todayStr(): string {
  const d = new Date();
  return `${d.getFullYear()}-${(d.getMonth()+1).toString().padStart(2,'0')}-${d.getDate().toString().padStart(2,'0')}`;
}

function daysBetween(from: string, to: string): number {
  const f = new Date(from);
  const t = new Date(to);
  return Math.floor((t.getTime() - f.getTime()) / (1000 * 60 * 60 * 24));
}

todayStr() 返回当天日期的字符串(如 2026-06-11),用于构建"今天"的参照点。

daysBetween() 计算两个日期之间相差的整天数。核心公式:(t.getTime() - f.getTime()) / (1000 * 60 * 60 * 24)getTime() 返回毫秒时间戳,差值除以 86400000(一天的毫秒数)得到天数。Math.floor 向下取整——因为我们只关心完整的"天",不关心小时和分钟。

正值表示目标日期在未来(“还有 N 天”),负值表示目标日期已过(“已过 N 天”),零表示今天。

2.3 动态排序

事件列表按距今天的距离排序——最近的排在最前:

sortedEvents(): CountdownEvent[] {
  return [...this.events].sort((a, b) => {
    const da = daysBetween(todayStr(), a.date);
    const db = daysBetween(todayStr(), b.date);
    if (da < 0 && db >= 0) return 1;   // a 已过,b 将来 → a 排后
    if (db < 0 && da >= 0) return -1;   // b 已过,a 将来 → a 排前
    return da - db;                      // 都将来或都已过 → 按距离
  });
}

排序规则:

  1. 将来的事件排在已过事件前面
  2. 将来事件按距离近 → 远排列(今天排第一,28 天后排最后)
  3. 已过事件按距离近 → 远排列(昨天排最前,1 年前的排最后)

用户打开页面后,最紧迫的事件(今天、明天)自然出现在第一位,不需要手动调整。
在这里插入图片描述

三、四色距离标记

3.1 颜色设计

用四种颜色标记事件的距离:

距离 颜色 色值 语义
已过 灰色 #888899 “这件事已经发生了”
0-1 天 红色 #FF4D4F “就是今天/明天!紧迫”
2-3 天 品红 #EB2F96 “快到了,做好准备”
4-7 天 蓝色 #1677FF “还有一周,心里有数”
8 天以上 绿色 #52C41A “还早,轻松等待”

四种颜色构成了一个情感梯度:红色 = 紧迫感,品红 = 关注,蓝色 = 平常,绿色 = 从容。用户不需要阅读"还有 X 天"的文字,光看颜色就能感知每件事的紧迫程度。

注意这里完全避免了黄色/金色——红色 → 品红 → 蓝色 → 绿色的渐变中,没有黄色参与。品红(#EB2F96)在 2-3 天的位置替代了本来可能使用橙色/黄色的角色,且白字对比度远优于黄色。

3.2 动态文字标签

除了颜色,每个事件还有对应的文字标签:

proximityLabel(days: number): string {
  if (days < 0) return `已过 ${Math.abs(days)}`;
  if (days === 0) return '今天!';
  if (days === 1) return '明天';
  return `还有 ${days}`;
}

"今天!“和"明天"是两个特殊情况——它们给人最强的紧迫感和期待感,所以用特殊的文字而非简单的"还有 0 天"或"还有 1 天”。感叹号增加了情感色彩——今天发生的事值得一个感叹号。

3.3 半透明背景色

每个事件卡片的图标区域也有对应的半透明背景色:

proximityBg(days: number): string {
  if (days < 0) return '#88889915';
  if (days <= 1) return '#FF4D4F15';
  if (days <= 3) return '#EB2F9615';
  if (days <= 7) return '#1677FF15';
  return '#52C41A15';
}

15(十六进制)= 约 8% 不透明度。这是一个非常淡的背景色——刚好能感知到色相,但不会和白色卡片背景产生冲突。如果背景色太深(比如 30-50% 不透明度),会和卡片文字争夺视觉注意力。
在这里插入图片描述

四、事件管理

4.1 添加事件

点击右下角 FAB("添加事件"蓝色按钮)弹出底部表单,包含三个输入区:

图标选择器:8 个 emoji 图标排列成行。选中态蓝底浅蓝边框(#1677FF22 + 1.5vp 边框),未选中态透明。与日记本的心情选择器不同,这里用边框区分选中态(而非不透明度)——因为图标都需要完整辨认,不适合降低不透明度。

名称输入TextInput 组件,占位文字"事件名称,如’生日派对’"。限制单行。

日期输入TextInput 组件,占位文字"日期,如 2026-12-25"。使用自由文本输入而非 DatePicker 的原因是:DatePicker 选择未来几个月甚至几年的日期需要多次滑动,不如直接输入 YYYY-MM-DD 快。

底部两个按钮——取消(灰色)和确认添加(蓝色,输入为空时灰色禁用)。

4.2 删除事件

每条事件卡片右侧有 × 按钮。点击直接删除——与日记本的删除逻辑相同,“可见即确认”,不需要额外的确认弹窗。

五、UI 设计

5.1 深色标题栏 + 白色卡片列表

与记账本类似的布局架构:

  • 深色标题栏(#1a1a2e):标题"⏳ 倒数日" + 事件计数
  • 浅灰背景(#F2F3F5):白色事件卡片列表
  • 蓝色 FAB:右下角悬浮

5.2 事件卡片布局

每张卡片水平排列四个元素:

  1. 图标区(56×56vp 圆角方块,淡色背景):大号 emoji
  2. 信息区(flex 1):事件名称(加粗深色)+ 日期(浅灰小字)
  3. 天数标签:动态文字 + 动态颜色(红色/品红/蓝色/绿色/灰色)
  4. 删除按钮:浅灰 ×

卡片之间用 1vp 的细微间隙分隔,第一张卡片顶部与列表顶部对齐(borderRadius 特殊处理)。

5.3 空状态

当事件列表为空时,显示空状态引导:📅 emoji + “还没有倒数事件” + “点击右下角 + 添加一个”。空状态是用户教育的一部分——告诉用户这个页面是做什么的、怎么开始使用。

六、完整代码结构

CountdownPage
├── Stack(根容器)
│   ├── Column(主界面)
│   │   ├── Row(标题栏:⏳ 倒数日 + N 个事件)
│   │   └── Scroll(事件列表)
│   │       ├── if 空态 → 空状态引导
│   │       └── ForEach → Row(事件卡片)
│   │           ├── Column(图标 + 半透明色底)
│   │           ├── Column(名称 + 日期)
│   │           ├── Text(还有/已过 N 天,动态色)
│   │           └── Text(删除 ×)
│   ├── Button(FAB:"+ 添加事件")
│   └── if showAdd → Column(底部弹窗)
│       ├── Row(8 个图标选择)
│       ├── TextInput(事件名称)
│       ├── TextInput(日期 YYYY-MM-DD)
│       └── Row(取消 + 确认添加)

七、总结

本文从零构建了一个倒数日追踪器。与前几篇的功能型应用不同,倒数日的核心是日期运算 + 视觉距离编码

核心要点回顾:

  1. 日期运算todayStr() 返回当天日期字符串,daysBetween() 计算两个日期的整天差。全部使用原生 Date API,无需第三方库。

  2. 四色距离标记:红色(0-1 天)= 紧迫,品红(2-3 天)= 关注,蓝色(4-7 天)= 平常,绿色(8 天+)= 从容。已过事件灰色。颜色构成情感梯度,用户不需要读文字就能感知紧迫程度。

  3. 自动排序sortedEvents() 将将来事件排在已过事件前面,同类事件按距离升序排列。用户打开页面后最近的事件自然出现在第一位。

  4. 配色安全:四色方案中完全没有黄色/金色——红色 → 品红 → 蓝色 → 绿色的情感梯度中,品红替代了传统方案中的橙色/黄色位置。所有颜色与白字对比度均达标。

  5. 动态标签:"今天!"和"明天"使用特殊文字而非数字,增加情感色彩和紧迫感。

  6. 图标选择器:用边框区分选中态(而非不透明度),因为所有图标都需要完整辨认。

倒数日是"小而美"的典型——一个界面、一个列表、几个事件卡片。但日期运算、距离排序、颜色编码、情感标签这些细节组合在一起,形成了一个用户愿意每天打开看的工具。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐