日期选择是移动应用中的高频需求。无论是用户注册时填写生日、差旅应用中筛选日期范围、还是日程管理中设定提醒时间,都离不开日期选择器。HarmonyOS NEXT ArkUI 提供了 DatePicker 组件,以三段式滚轮交互让用户直观地选择年月日。

本文将深入讲解 DatePicker 的核心 API,并通过一个完整的"日期计算器"实战案例,展示如何在真实场景中使用日期选择器——包括年龄计算和日期差计算两个实用功能。

关键词:HarmonyOS、ArkUI、DatePicker、日期选择器、日期计算、年龄计算

一、DatePicker 组件概览

DatePicker 是 ArkUI 中专门用于日期选择的组件,采用三段滚轮(年-月-日)的交互模式。用户上下滑动每一列即可快速定位到目标日期。这种交互在移动端已有十几年的历史,用户学习成本极低。

与 TextClock(显示当前时间)和 TextTimer(计时器)不同,DatePicker 的核心功能是"让用户选择一个日期"。它不显示当前系统时间,也不进行计时——它的职责单一而清晰:选择日期。

二、DatePicker 核心 API

2.1 构造函数

DatePicker({
  start: Date,    // 可选日期范围的起始
  end: Date,      // 可选日期范围的终止
  selected: Date  // 当前选中的日期
})

三个参数的含义:

  • start:用户可以选择的最早日期。例如生日选择场景中,start 通常设为 new Date(1900, 0, 1)(1900 年 1 月 1 日)
  • end:用户可以选择的最晚日期。例如生日选择时,end 设为 new Date()(今天,因为不能选择未来日期作为生日)
  • selected:当前选中的日期,传入一个 JavaScript Date 对象

注意事项:JavaScript 的 Date 构造函数中,月份参数是 0-based(0 = 一月,11 = 十二月)。而用户看到的月份是 1-based(1 月、2 月…12 月)。在 DatePicker 的 onChange 回调中,返回的 DatePickerResult.month 也是 0-based。这个 0-based 月份约定是 DatePicker 开发中最容易出错的地方。

2.2 onChange 回调

当用户滑动滚轮改变了选中的日期时,onChange 回调被触发:

DatePicker({ ... })
  .onChange((value: DatePickerResult) => {
    // value.year  — 年份 (number)
    // value.month — 月份 (0-based, 0-11)
    // value.day   — 日期 (1-31)
  })

DatePickerResult 接口包含三个字段:yearmonthday。注意 month 是 0-based 的(即真实月份减 1),在显示给用户时需要加 1。

2.3 日期范围限制

合理设置 startend 不仅能防止用户选择无效日期,还能提升用户体验。例如:

  • 生日选择start: new Date(1900, 0, 1), end: new Date() — 不能选择未来日期
  • 预约日期start: new Date(), end: new Date(2027, 11, 31) — 只能选择未来日期
  • 历史查询start: new Date(2000, 0, 1), end: new Date() — 限定在特定历史范围
    在这里插入图片描述

三、实战:日期计算器

我们将实现一个双模式日期计算器:

  1. 年龄计算模式:用户选择出生日期,自动计算精确年龄(岁 + 月 + 天)
  2. 日期差模式:用户选择开始和结束日期,计算两者之间的天数差

3.1 状态设计

页面需要管理两种模式的日期状态,通过 calcMode 区分当前模式:

@State calcMode: number = 0;      // 0=年龄计算, 1=日期差
@State diffSlot: number = 0;      // 日期差模式下的当前编辑槽位

// 年龄模式 - 出生日期
@State birYear: number = 1995;
@State birMonth: number = 5;      // 0-based (June)
@State birDay: number = 15;

// 日期差模式 - 开始和结束日期
@State startYear: number = 2026;
@State startMonth: number = 0;    // January
@State startDay: number = 1;
@State endYear: number = 2026;
@State endMonth: number = 6;      // July
@State endDay: number = 5;

@State ageResult: string = '';
@State diffResult: string = '';

3.2 模式切换

使用两个胶囊按钮实现模式切换,选中项填充品牌蓝色:

Row() {
  Row() {
    Text('年龄计算')
      .fontSize(14)
      .fontColor(this.calcMode === 0 ? '#FFFFFF' : '#666677')
      .fontWeight(this.calcMode === 0 ? FontWeight.Bold : FontWeight.Normal)
      .padding({ top: 8, bottom: 8, left: 20, right: 20 })
      .borderRadius(18)
      .backgroundColor(this.calcMode === 0 ? '#1677FF' : '#F2F3F5')
      .onClick(() => { this.calcMode = 0; this.updateCalc(); })
  }
  Row() {
    Text('日期差')
      .fontSize(14)
      .fontColor(this.calcMode === 1 ? '#FFFFFF' : '#666677')
      .fontWeight(this.calcMode === 1 ? FontWeight.Bold : FontWeight.Normal)
      .padding({ top: 8, bottom: 8, left: 20, right: 20 })
      .borderRadius(18)
      .backgroundColor(this.calcMode === 1 ? '#1677FF' : '#F2F3F5')
      .onClick(() => { this.calcMode = 1; this.updateCalc(); })
  }
}

3.3 年龄计算逻辑

年龄计算的核心逻辑在 updateCalc() 方法中:

updateCalc(): void {
  if (this.calcMode === 0) {
    const birth = new Date(this.birYear, this.birMonth, this.birDay);
    const now = new Date();
    let age = now.getFullYear() - birth.getFullYear();
    const m = now.getMonth() - birth.getMonth();
    if (m < 0 || (m === 0 && now.getDate() < birth.getDate())) {
      age = age - 1;
    }
    const totalMonths = (now.getFullYear() - birth.getFullYear()) * 12 +
      (now.getMonth() - birth.getMonth());
    const totalDays = Math.floor(
      (now.getTime() - birth.getTime()) / (1000 * 60 * 60 * 24)
    );
    this.ageResult = age.toString().concat(' 岁 · ',
      totalMonths.toString(), ' 个月 · ',
      totalDays.toString(), ' 天');
  }
}

这段逻辑的要点:

  1. 年份差:当前年份减去出生年份
  2. 修正:如果今年生日还没到(当前月份小于出生月份,或同月但日期未到),年龄减 1
  3. 多维度展示:同时展示"岁"、“月”、"天"三个维度,让结果更丰富

3.4 日期差计算逻辑

计算两个日期之间的相差天数:

const d1 = new Date(this.startYear, this.startMonth, this.startDay);
const d2 = new Date(this.endYear, this.endMonth, this.endDay);
const diffDays = Math.floor(
  Math.abs(d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24)
);
const diffWeeks = (diffDays / 7).toFixed(1);
this.diffResult = diffDays.toString().concat(' 天 ≈ ',
  diffWeeks, ' 周');

核心思路:两个 Date 对象的 getTime() 返回毫秒时间戳,差值除以一天的毫秒数(86400000)即得到天数。

3.5 日期差模式的双槽设计

日期差模式需要选择开始和结束两个日期,但同一时间只能有一个 DatePicker 在操作。解决方案是引入"槽位切换"——两个日期卡片,点击哪个卡片,DatePicker 就编辑哪个日期的值:

Column() {
  Text('开始')
    .fontSize(11).fontColor('#9999AA').margin({ bottom: 2 })
  Text(this.fmtDate(this.startYear, this.startMonth, this.startDay))
    .fontSize(15).fontColor('#1a1a2e').fontWeight(FontWeight.Medium)
}
.padding(12)
.backgroundColor(this.diffSlot === 0 ? '#EEF3FF' : '#F2F3F5')
.borderRadius(10)
.onClick(() => { this.diffSlot = 0; })

当前编辑的槽位使用蓝色背景(#EEF3FF),另一个槽位使用灰色背景。这种视觉暗示清晰地向用户传达了"我正在编辑哪个日期"。

3.6 日期格式化工具方法

将 0-based 的月份转换为人类可读的日期字符串:

fmtDate(year: number, month: number, day: number): string {
  return year.toString().concat('-',
    (month + 1).toString().padStart(2, '0'), '-',
    day.toString().padStart(2, '0'));
}

这里有两个关键转换:

  1. month + 1 — 将 0-based 月份转为 1-based
  2. .padStart(2, '0') — 确保月份和日期始终显示为两位数(如 “05” 而非 “5”)

3.7 DatePicker 集成

根据当前模式和槽位,渲染对应的 DatePicker:

if (this.calcMode === 0) {
  // 年龄模式:选择出生日期,end 限制为今天
  DatePicker({
    start: new Date(1900, 0, 1),
    end: new Date(),
    selected: new Date(this.birYear, this.birMonth, this.birDay)
  })
    .onChange((value: DatePickerResult) => { this.onBirChange(value); })
} else {
  // 日期差模式:根据 diffSlot 切换 start/end 日期
  if (this.diffSlot === 0) {
    DatePicker({
      start: new Date(1900, 0, 1),
      end: new Date(2100, 11, 31),
      selected: new Date(this.startYear, this.startMonth, this.startDay)
    })
      .onChange((value: DatePickerResult) => { this.onStartChange(value); })
  } else {
    DatePicker({
      start: new Date(1900, 0, 1),
      end: new Date(2100, 11, 31),
      selected: new Date(this.endYear, this.endMonth, this.endDay)
    })
      .onChange((value: DatePickerResult) => { this.onEndChange(value); })
  }
}

注意年龄计算模式中,end 参数设置为 new Date()(当前日期),防止用户选择未来日期。而在日期差模式中,end 设置为远未来,允许自由选择任意日期。
在这里插入图片描述

四、结果展示设计

计算结果用一个卡片承载,使用暖色背景增强视觉吸引力:

// 年龄计算结果
Row() {
  Text('🎂').fontSize(28).margin({ right: 12 })
  Column() {
    Text(this.fmtDate(this.birYear, this.birMonth, this.birDay))
      .fontSize(13).fontColor('#9999AA').margin({ bottom: 4 })
    Text(this.ageResult)
      .fontSize(22).fontColor('#1a1a2e').fontWeight(FontWeight.Bold)
  }
  .alignItems(HorizontalAlign.Start)
}
.width('100%').padding(16).backgroundColor('#FFF8F0').borderRadius(12)

// 日期差计算结果
Text(this.diffResult)
  .fontSize(18).fontColor('#FF5722').fontWeight(FontWeight.Bold)

两种模式的结果展示风格不同:

  • 年龄模式使用浅暖色背景(#FFF8F0)和较大的字体,强调"这是一个温和的个人信息"
  • 日期差模式的结果使用橙红字体(#FF5722),强调"这是一个计算结果"

五、完整交互流程

  1. 默认进入年龄计算模式:DatePicker 显示默认出生日期 1995-06-15,结果区显示计算出的年龄
  2. 切换模式:点击"日期差"按钮,DatePicker 切换到日期差模式,默认编辑开始日期
  3. 编辑开始日期:滑动 DatePicker 滚轮,结果自动更新
  4. 切换到结束日期:点击"结束"卡片,DatePicker 切换为编辑结束日期
  5. 查看天数差:两个日期都选好后,结果区显示精确的相差天数、周数、月数
  6. 切换回年龄模式:点击"年龄计算"按钮

六、设计要点总结

6.1 0-based 月份陷阱

这是 DatePicker 开发中最常见的坑。记牢这条规则:

  • DatePickerResult.month 是 0-based(0-11)
  • new Date(year, month, day) 的 month 也是 0-based
  • 展示给用户时需要 month + 1

6.2 日期范围的用户体验

合理设置 startend 不仅防止无效输入,还是一种隐性的用户引导。例如生日选择器将 end 设为今天,用户会自然地理解"生日不能选未来"。

6.3 槽位切换的视觉暗示

在日期差模式中,通过背景色区分"当前编辑的槽位"和"另一个槽位",是一种低成本高收益的 UX 设计。用户无需阅读说明文字,凭视觉就能理解交互逻辑。

6.4 计算触发时机

本 Demo 在 DatePicker 的 onChange 回调中触发计算,实现"滑动即计算"的即时反馈。对于日期计算器这类工具型页面,即时反馈比"选择后点击计算按钮"的交互路径更高效。

七、总结

DatePicker 是 ArkUI 中一个交互模式经典、API 设计简洁的组件。几个月参数(start、end、selected)加上一个 onChange 回调,就能覆盖绝大多数日期选择场景。

本文通过"日期计算器"这个实战案例,展示了:

  1. DatePicker 的基本用法和参数配置
  2. 年龄计算的精确算法(年份差 + 本月生日修正)
  3. 日期差计算(毫秒差 → 天/周/月)
  4. 双模式切换的页面设计
  5. 0-based 月份的注意事项

DatePicker 常与 TextPicker(时间选择)配合使用,组合成完整的日期时间选择。在后续文章中,我们将继续探索 TimePicker 和 TextPicker 的用法,以及如何将三者组合为功能完备的时间日期输入方案。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐