在这里插入图片描述

在这里插入图片描述

从零构建「时光织」:HarmonyOS 个人日程与时间规划管理工具全栈实践

作者:duluo
平台:HarmonyOS Next (API 24) · ArkTS · DevEco Studio
应用名称:时光织 / TimeWeave
定位:融合 GTD + 时间块 + 番茄钟三大方法论的个人日程与时间规划管理工具


目录

  1. 项目背景与设计理念
  2. 技术选型与架构设计
  3. UI 组件体系与视觉语言
  4. 数据类型系统与模拟数据设计
  5. 路由系统与页面导航
  6. 核心模块实现详解
    • 6.1 首页四 Tab 导航架构
    • 6.2 日程 Tab:周日期选择器 + 时间线 + 习惯打卡
    • 6.3 任务 Tab:GTD 四象限 + 快速添加
    • 6.4 专注 Tab:番茄钟计时器 + setInterval 实时更新
    • 6.5 任务管理详情页:收件箱 / 项目 / 四象限 / 标签
    • 6.6 专注统计页:Canvas 柱状图 + 高效时段分析
    • 6.7 周报复盘页:Canvas 饼图 + 完成率 + AI 建议
  7. ArkTS 语法陷阱与踩坑记录
  8. 性能优化与最佳实践
  9. 未来 roadmap
  10. 总结

1. 项目背景与设计理念

1.1 为什么选择时间管理方向

在信息过载的时代,时间管理成为了现代人最大的痛点之一。根据 2025 年的一项调研,超过 73% 的知识工作者表示「每天忙得不可开交,但回顾一天却想不起做了什么」。我们发现了市面上时间管理工具的三大缺口:

  1. 方法论割裂:GTD 工具只管任务,番茄钟只管计时,日历只管安排——用户需要在三个 App 之间来回切换
  2. 复盘缺失:大多数工具只关注「规划」,不关注「复盘」。用户不知道自己的时间花在了哪里,更不知道如何优化
  3. 学习成本高:Notion、Obsidian 功能强大但配置复杂,普通用户难以坚持

「时光织」(TimeWeave)的定位正是将 GTD + 时间块 + 番茄钟三大方法论融合到一个应用中,让用户从「规划 → 执行 → 复盘」形成一个完整闭环。名字「时光织」寓意将碎片化的时间编织成有序的锦缎。

1.2 设计哲学

我们的设计围绕四个核心原则展开:

  • 闭环设计(Closed Loop):规划 → 执行 → 记录 → 复盘 → 再规划,形成完整的时间管理闭环
  • 渐进式复杂度(Progressive Complexity):新用户打开 App 就能用番茄钟和任务列表;进阶用户可以使用 GTD 四象限和时间块规划;高级用户可以利用复盘数据优化时间分配
  • 沉浸式专注(Immersive Focus):进入专注模式时,界面切换到深色沉浸视图,减少干扰
  • 数据驱动复盘(Data-Driven Review):自动记录专注数据、任务完成率,每周生成可视化周报

1.3 目标用户画像

  • 职场人士:需要高效规划工作日,管理多个项目并行
  • 学生群体:需要管理课程/自习/考试安排
  • 自由职业者:需要自主安排项目时间,保持自律
  • 时间管理初学者:希望养成规划习惯,从番茄钟开始

2. 技术选型与架构设计

2.1 为什么选择 HarmonyOS Next (API 24)

特性 描述 在时光织中的应用
ArkTS 声明式 UI 基于 TypeScript 的声明式 UI 框架 全部页面采用 @Component + @State + @Builder
Canvas 2D API 完整 Canvas 绘制能力 专注柱状图、周报时间饼图
Progress 组件 原生进度条组件 任务完成率、时间分布、番茄钟进度
Toggle 组件 原生开关组件 任务标记完成/未完成
bindSheet 底部 Sheet 弹窗 快速添加任务(预留接口)
setInterval 定时器 API 番茄钟倒计时实时更新

2.2 整体架构

┌────────────────────────────────────────────────┐
│                 UI Layer                        │
│  ┌──────────┐ ┌──────────┐ ┌────────────────┐  │
│  │MainPage  │ │TaskDetail│ │FocusStats      │  │
│  │(4Tab Hub)│ │Page      │ │Page            │  │
│  └────┬─────┘ └────┬─────┘ └───────┬────────┘  │
│  ┌────┴────┐       │               │           │
│  │WeeklyRep│       │               │           │
│  │ortPage  │       │               │           │
│  └─────────┘       │               │           │
├────────────────────┼───────────────┼───────────┤
│         Router (pushUrl / back)                │
├────────────────────┼───────────────┼───────────┤
│              Data Layer                         │
│  ┌──────────┐ ┌──────────┐ ┌────────────────┐  │
│  │Interface │ │  Mock    │ │  Utility       │  │
│  │ Types    │ │  Data    │ │  Functions     │  │
│  └──────────┘ └──────────┘ └────────────────┘  │
├────────────────────────────────────────────────┤
│            HarmonyOS API 24 Runtime             │
└────────────────────────────────────────────────┘

2.3 核心架构决策

4Tab 单页面架构:与财富管理 App 类似,时光织也采用 4Tab 导航(日程/任务/专注/统计)作为首页。但有一个关键差异——专注中视图覆盖全局:当用户开始番茄钟后,无论当前在哪个 Tab,都会切换到全屏的沉浸式专注视图。这是通过一个 @State isFocusing: boolean 状态来实现的:

// 核心状态切换逻辑
build() {
  Column() {
    // Header
    // Tab 栏
    Scroll() {
      Column() {
        if (!this.isFocusing) {
          if (this.tabIdx === 0) { this.TimelineTab() }
          else if (this.tabIdx === 1) { this.TaskTab() }
          else if (this.tabIdx === 2) { this.FocusTab() }
          else { this.StatsTab() }
        } else {
          this.FocusingView()  // 全屏沉浸式专注视图
        }
      }
    }
  }
}

这个设计决策让番茄钟体验更加沉浸——用户开始专注后,所有的 Tab 和任务列表都会隐藏,只剩下计时器和当前专注任务。


3. UI 组件体系与视觉语言

3.1 色彩系统

时光织采用「沉稳蓝 + 活力青」的主色调,传达冷静、专注、高效的品牌感受。

const COLORS = {
  primary: '#1ABC9C',       // 活力青 - 强调、CTA按钮、焦点色
  darkBg: '#2C3E50',        // 沉稳蓝 - 顶部 Header
  darkCard: '#34495E',      // 深色卡片
  pageBg: '#F0F2F5',        // 页面背景
  cardBg: '#FFF',           // 卡片背景
  textPrimary: '#333',      // 主文字
  textSecondary: '#888',    // 次要文字
  // 时间块类型色
  focus: '#3498DB',         // 专注块 - 蓝色
  break: '#2ECC71',         // 休息块 - 绿色
  buffer: '#95A5A6',        // 缓冲块 - 灰色
  meeting: '#E67E22',       // 会议块 - 橙色
  personal: '#9B59B6',      // 个人块 - 紫色
};

色彩心理学考量

  • 沉稳蓝 #2C3E50 作为背景色,营造「深海般」的冷静氛围,帮助用户进入专注状态
  • 活力青 #1ABC9C 作为强调色,在深色背景上格外醒目,指引用户的操作焦点
  • 每种时间块类型有独立的色彩标识,用户在时间线上一眼就能区分不同类型的块

3.2 组件体系

原子组件 (Atoms)
├── Text(字号:9/10/11/12/13/14/15/16/18/20/22/24/48/56/64)
├── Progress(进度条)
├── Button(圆角按钮)
├── Toggle(开关 / 勾选框)
├── Circle(圆形指示器)
├── Divider(分割线)
├── Canvas(画布)
└── TextInput(文本输入框)

分子组件 (Molecules)
├── TabBtn(Tab 切换按钮)
├── QuadrantCard(四象限卡片)
├── PriorityBadge(优先级标签)
├── FocusModeBtn(专注模式按钮)
├── FocusStatCard(专注统计卡片)
├── StatCard(通用统计卡片)
├── DistBar(时间分布条)
├── OverviewCard(总览卡片)
├── RangeBtn(时间范围按钮)
├── HourBar(时段热度柱)
├── TagGroup(标签组)
└── TaskRow(任务行 + 展开详情)

组织组件 (Organisms)
├── 周日期选择器(7 天动态计算)
├── 时间线列表(时间块 + 类型标识)
├── 习惯打卡网格(5 个习惯)
├── 任务列表(带优先级和项目标签)
├── 番茄钟计时器(倒计时 + 进度环)
├── 四象限矩阵(2×2 网格)
├── 项目分组列表(带进度条)
├── 本周专注柱状图(Canvas 自绘)
└── 时间分布饼图(Canvas 自绘,甜甜圈效果)

页面 (Pages)
├── MainPage(首页 4Tab)
├── TaskDetailPage(任务管理详情)
├── FocusStatsPage(专注统计)
└── WeeklyReportPage(周报复盘)

3.3 卡片式布局

沿用与 WealthWise 一致的卡片式布局体系,白色圆角卡片承载独立信息模块:

Column() {
  Row() {
    Text('📈 本周专注趋势').fontSize(16).fontColor('#333').fontWeight(FontWeight.Bold)
    Blank()
    Text('周报 >').fontSize(12).fontColor('#1ABC9C')
  }.width('100%')
  // 卡片内容...
}
.width('94%').padding(14).backgroundColor('#FFF').borderRadius(14).margin({ top: 10 })

3.4 微交互设计

  • 任务勾选:点击 ⬜ 切换为 ✅,文字自动添加删除线,给予完成任务的成就感
  • 专注状态切换:开始专注 → 全屏沉浸深色视图;结束专注 → 恢复 Tab 导航
  • Tab 切换:选中 Tab 图标放大、颜色变亮(活力青),未选中保持灰色
  • 周日期选择:选中日期高亮为圆形绿色背景,当天自动定位
  • 展开详情:点击任务行右侧 > 展开描述和子任务,折叠状态保持界面清爽
  • 优先级 Badge:高优先级红色、中优先级橙色、低优先级灰色,视觉层级清晰

4. 数据类型系统与模拟数据设计

4.1 接口定义体系

时光织定义了 7 个核心数据接口:

// 时间块
interface TimeBlock {
  time: string;      // 'HH:mm'
  title: string;     // 事件名称
  type: string;      // 'focus' | 'break' | 'buffer' | 'meeting' | 'personal'
  duration: string;  // '30min' | '2h'
  color: string;     // 类型对应色
}

// 任务
interface TaskItem {
  id: number; title: string; priority: string; due: string;
  done: boolean; project: string; estMin: number;
}

// 习惯
interface HabitItem {
  icon: string; name: string; done: boolean; streak: number; color: string;
}

// 专注记录
interface FocusSession {
  date: string;    // '07/03'
  minutes: number;  // 专注时长
  completed: number; // 完成番茄数
}

// 任务详情(GTD 完整字段)
interface TaskDetail {
  id: number; title: string; priority: string; status: string;
  project: string; estMin: number; due: string; tags: string[];
  desc: string; subtasks: string[];
}

// 专注记录详情
interface FocusRecord {
  time: string; task: string; minutes: number; completed: boolean;
}

// 周报数据
interface TimeDistItem {
  label: string; icon: string; minutes: number; pct: number; color: string;
}

4.2 模拟数据策略

与 WealthWise 一样,我们使用静态模拟数据进行快速原型开发。数据覆盖了完整的「一天」使用场景:

时间块:12 个(7:00-20:00,涵盖专注/休息/缓冲/会议/个人)
任务:6 个(高/中/低三种优先级,4 个项目)
习惯:5 个(阅读/跑步/喝水/冥想/写日记)
专注记录:7 天(日均 3-5h 专注)
周报数据:6 个时间分类
今日专注记录:5 条

4.3 工具函数

// 分钟格式化:180 → '3h', 90 → '1h30min'
function fmtMin(m: number): string {
  if (m >= 60) {
    return Math.floor(m / 60) + 'h' + (m % 60 > 0 ? (m % 60) + 'min' : '');
  }
  return m + 'min';
}

// 获取今日日期(中文格式)
function getToday(): string {
  const d = new Date();
  return d.getFullYear() + '年' + (d.getMonth() + 1) + '月' +
    d.getDate() + '日 周' + ['日','一','二','三','四','五','六'][d.getDay()];
}

5. 路由系统与页面导航

5.1 路由配置

{
  "src": [
    "pages/Index",
    "pages/schedule_planner/MainPage",
    "pages/schedule_planner/TaskDetailPage",
    "pages/schedule_planner/FocusStatsPage",
    "pages/schedule_planner/WeeklyReportPage"
  ]
}

5.2 导航图

Index.ets(应用中心)
  └── 📅 时光织 →  schedule_planner/MainPage
                    ├── 任务 Tab「查看全部」 →  TaskDetailPage
                    ├── 专注 Tab「查看统计」 →  FocusStatsPage
                    └── 统计 Tab「周报」     →  WeeklyReportPage

5.3 返回导航

每个子页面顶栏都有 ← 返回 按钮,调用 router.back() 返回首页:

Text('← 返回').fontSize(14).fontColor('#1ABC9C')
  .onClick(() => router.back())

6. 核心模块实现详解

6.1 首页四 Tab 导航架构

首页是时光织的核心,采用 4Tab 导航结构。与传统的底部 Tab 不同,我们采用了顶部 Tab 栏的设计,紧跟在 Header 下方,让用户的视线自然地从品牌信息转移到功能切换:

Row() {
  this.TabBtn(0, '📅', '日程')
  this.TabBtn(1, '✅', '任务')
  this.TabBtn(2, '🍅', '专注')
  this.TabBtn(3, '📊', '统计')
}
.width('94%').padding(6)
.backgroundColor('#34495E').borderRadius(14)
.margin({ top: 2 })

当用户开始番茄钟后,通过 @State isFocusing 状态切换视图:

if (!this.isFocusing) {
  if (this.tabIdx === 0) { this.TimelineTab() }
  // ...
} else {
  this.FocusingView()
}

6.2 日程 Tab:周日期选择器 + 时间线 + 习惯打卡

6.2.1 周日期选择器

这是一个动态计算本周日期的组件。我们使用 weekDayNumbers getter 来计算当前周的 7 天日期:

get weekDayNumbers(): number[] {
  const today = new Date().getDay();
  return WEEKDAYS.map((_, idx) => this.selectedDate - (today - idx));
}

然后通过 ForEach 渲染 7 个日期按钮,选中的日期高亮为绿色:

ForEach(this.weekDayNumbers, (dayNum: number, idx: number) => {
  Column() {
    Text(WEEKDAYS[idx]).fontSize(10).fontColor('#888')
    Text(dayNum.toString()).fontSize(14)
      .fontColor(dayNum === this.selectedDate ? '#FFF' : '#333')
      .fontWeight(dayNum === this.selectedDate ? FontWeight.Bold : FontWeight.Regular)
  }
  .layoutWeight(1).padding({ top: 4, bottom: 4 })
  .backgroundColor(dayNum === this.selectedDate ? '#1ABC9C' : 'transparent')
  .borderRadius(8).alignItems(HorizontalAlign.Center)
  .onClick(() => { this.selectedDate = dayNum })
}, (d: number) => d.toString())

技术要点:这个组件展示了 ArkTS 中如何通过 getter 将动态计算逻辑从 UI 构建区域中分离出来。一开始我们把 const dayNum = this.selectedDate - (new Date().getDay() - idx) 写在了 ForEach 的回调中,触发了编译错误 Only UI component syntax can be written here。将计算逻辑抽取到 getter 后完美解决。

6.2.2 时间线

时间线是日程 Tab 的核心内容,展示一天的完整时间块规划。每个时间块行包含时间、类型色条、标题、时长:

ForEach(TIME_BLOCKS, (b: TimeBlock) => {
  Row() {
    Text(b.time).fontSize(11).fontColor('#888').width(44)
    Column().width(3).height(48).backgroundColor(b.color).borderRadius(2)
    Column({ space: 2 }) {
      Text(b.title).fontSize(13).fontColor('#333').fontWeight(FontWeight.Medium)
      Text(this.blockTypeLabel(b.type)).fontSize(10).fontColor(b.color)
    }.margin({ left: 8 }).layoutWeight(1)
    Text(b.duration).fontSize(11).fontColor('#AAA')
    Text('>').fontSize(12).fontColor('#DDD').margin({ left: 4 })
  }
  .width('100%').padding({ top: 5, bottom: 5 })
  .border({ width: { bottom: 1 }, color: '#F5F5F5' })
}, (b: TimeBlock) => b.time + b.title)

blockTypeLabel 方法将类型枚举转换为用户友好的标签:

blockTypeLabel(type: string): string {
  const map: Record<string, string> = {
    'focus': '🔵 专注', 'break': '🟢 休息',
    'buffer': '⚪ 缓冲', 'meeting': '🟠 会议', 'personal': '🟣 个人'
  };
  return map[type] ?? type;
}
6.2.3 习惯打卡

习惯打卡采用 5 列网格布局,每个习惯显示图标、完成状态、名称和连续天数:

Row({ space: 8 }) {
  ForEach(HABITS, (h: HabitItem) => {
    Column() {
      Text(h.icon).fontSize(24)
      Text(h.done ? '✅' : '⬜').fontSize(14).margin({ top: 4 })
      Text(h.name).fontSize(9).fontColor('#555').margin({ top: 2 })
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
      Text('🔥' + h.streak + '天').fontSize(8).fontColor('#F59E0B').margin({ top: 1 })
    }
    .layoutWeight(1).padding(6).backgroundColor('#FFF').borderRadius(10)
    .alignItems(HorizontalAlign.Center)
  }, (h: HabitItem) => h.name)
}.width('100%').margin({ top: 8 })

6.3 任务 Tab:GTD 四象限 + 快速添加

任务 Tab 是时光织中 GTD 方法论的体现,包含三个核心组件:

6.3.1 快速添加输入框

顶部提供一个 TextInput 和 Button 的组合,方便用户快速捕获想法:

Row({ space: 6 }) {
  TextInput({ placeholder: '📝 快速添加任务...' })
    .width(0).layoutWeight(1).height(40).fontSize(14)
    .placeholderColor('#AAA').backgroundColor('#FFF')
    .borderRadius(20).padding({ left: 16 })
  Button('+').width(40).height(40).backgroundColor('#1ABC9C')
    .fontColor('#FFF').fontSize(22).borderRadius(20)
}
.width('94%').margin({ top: 10 })
6.3.2 四象限矩阵

这是 GTD 中最经典的可视化工具。我们将 2×2 矩阵拆分为 4 个 QuadrantCard:

this.QuadrantCard('重要紧急', '3项', '#E74C3C')
this.QuadrantCard('重要不紧急', '2项', '#F39C12')
this.QuadrantCard('紧急不重要', '1项', '#3498DB')
this.QuadrantCard('不紧急不重要', '0项', '#95A5A6')

每个卡片是一个带颜色的 Column,显示分类名称和任务数量。

6.3.3 任务列表

任务列表展示今日所有任务,支持勾选完成(文字自动添加删除线):

Text(t.done ? '✅' : '⬜').fontSize(16)
  .onClick(() => { t.done = !t.done })
// ...
Text(t.title).fontSize(13).fontColor(t.done ? '#AAA' : '#333')
  .decoration({
    type: t.done ? TextDecorationType.LineThrough : TextDecorationType.None
  })

每个任务还展示了优先级 Badge、项目名称、预估时间和截止日期。

6.4 专注 Tab:番茄钟计时器 + setInterval 实时更新

专注模块是时光织的核心功能之一,包含番茄钟计时器和当日专注汇总。

6.4.1 番茄钟计时器

计时器的核心是一个 25 分钟的倒计时,使用 setInterval 每秒更新:

startFocus(): void {
  this.isFocusing = true;
  this.focusMinutes = 25;
  this.focusSeconds = 0;
  this.timerId = setInterval(() => {
    if (this.focusSeconds === 0) {
      if (this.focusMinutes === 0) {
        clearInterval(this.timerId);
        this.isFocusing = false;
        return;
      }
      this.focusMinutes--;
      this.focusSeconds = 59;
    } else {
      this.focusSeconds--;
    }
  }, 1000);
}

stopFocus(): void {
  clearInterval(this.timerId);
  this.isFocusing = false;
  this.focusMinutes = 25;
  this.focusSeconds = 0;
}

关键设计决策

  • 使用 setInterval 而非 setTimeout 递归——代码更简洁,易于管理
  • timerId 存储在类的私有字段中,方便 stopFocus() 清理
  • 倒计时结束时自动退出专注模式,触发 UI 切换回正常视图
6.4.2 计时显示格式化
get focusDisplay(): string {
  const m = (this.focusMinutes < 10 ? '0' : '') + this.focusMinutes;
  const s = (this.focusSeconds < 10 ? '0' : '') + this.focusSeconds;
  return m + ':' + s;
}
6.4.3 专注进度环

使用 Progress 组件实现视觉化的进度跟踪:

get focusProgress(): number {
  return ((25 * 60 - (this.focusMinutes * 60 + this.focusSeconds)) / (25 * 60)) * 100;
}

Progress({ value: this.focusProgress, total: 100 })
  .width(240).height(6).color('#1ABC9C')
  .backgroundColor('rgba(26,188,156,0.2)')
  .margin({ top: 12 })
6.4.4 沉浸式专注视图

当番茄钟开始后,整个 UI 切换到沉浸式深色视图:

@Builder
FocusingView() {
  Column() {
    Column() {
      Text('🎯 保持专注').fontSize(20).fontColor('#FFF').fontWeight(FontWeight.Bold)
      Text(this.focusDisplay).fontSize(64).fontColor('#1ABC9C').fontWeight(FontWeight.Bold)
        .margin({ top: 16 })
      Progress({ value: this.focusProgress, total: 100 })
        .width(240).height(6).color('#1ABC9C')
        .backgroundColor('rgba(26,188,156,0.2)')
        .margin({ top: 12 })
      Text('🔵 ' + this.focusTaskTitle).fontSize(14).fontColor('rgba(255,255,255,0.7)')
        .margin({ top: 12 })

      Row({ space: 16 }) {
        Button('⏹ 结束').width(120).height(44)
          .backgroundColor('rgba(231,76,60,0.8)').fontColor('#FFF').borderRadius(22).fontSize(14)
          .onClick(() => this.stopFocus())
        Button('⏸ 暂停').width(100).height(44)
          .backgroundColor('rgba(255,255,255,0.15)').fontColor('#FFF').borderRadius(22).fontSize(14)
      }.margin({ top: 20 })
    }
    .width('88%').padding(30)
    .backgroundColor('rgba(255,255,255,0.08)').borderRadius(24)
    .alignItems(HorizontalAlign.Center).margin({ top: 40 })
  }
  .width('100%').alignItems(HorizontalAlign.Center)
}
6.4.5 专注模式预设

提供三种专注/休息模式快速切换:

Row({ space: 8 }) {
  this.FocusModeBtn('🔵 专注', '25min', true)
  this.FocusModeBtn('🟢 短休息', '5min', false)
  this.FocusModeBtn('🟡 长休息', '15min', false)
}.width('100%').margin({ top: 10 })

6.5 任务管理详情页:收件箱 / 项目 / 四象限 / 标签

TaskDetailPage 是 GTD 方法论的完整实现,包含 4 个子 Tab:

6.5.1 收件箱 Tab

GTD 的第一步是「捕获」——把所有想法放进收件箱。收件箱 Tab 展示所有 status === 'inbox' 的任务:

get inboxTasks(): TaskDetail[] {
  return ALL_TASKS.filter(t => t.status === 'inbox');
}

收件箱上方还提供了「📷 拍照」和「🎤 语音」入口(预留接口),方便快速捕获。

6.5.2 项目 Tab

按项目分组展示任务,每个项目带进度条:

ForEach(PROJECTS, (p: ProjectGroup) => {
  Column() {
    Row() {
      Text(p.name).fontSize(16).fontColor(p.color).fontWeight(FontWeight.Bold)
      Blank()
      Text(p.done + '/' + p.count).fontSize(13).fontColor('#888')
    }.width('100%')
    Progress({ value: p.done, total: p.count }).width('100%').height(6)
      .color(p.color).backgroundColor('#EEE').margin({ top: 4 })
    // 项目下的任务列表
    ForEach(ALL_TASKS.filter(t => t.project === p.name), ...)
  }
})
6.5.3 四象限 Tab

通过 getQuadrant() 方法动态计算每个象限的任务:

getQuadrant(urgentImp: boolean, imp: boolean): TaskDetail[] {
  return ALL_TASKS.filter(t => {
    if (t.status === 'done') return false;
    const hasUrgent = t.tags.includes('紧急') || t.priority === '高';
    const hasImportant = t.tags.includes('重要') || t.priority === '高' || t.priority === '中';
    if (urgentImp && imp) return hasUrgent && hasImportant;
    if (urgentImp && !imp) return hasUrgent && !hasImportant;
    if (!urgentImp && imp) return !hasUrgent && hasImportant;
    return !hasUrgent && !hasImportant;
  });
}

四个象限各有独立的颜色:

  • 重要·紧急(红色 #E74C3C):立即处理
  • 重要·不紧急(橙色 #F39C12):计划处理
  • 不重要·紧急(蓝色 #3498DB):委派他人
  • 不重要·不紧急(灰色 #95A5A6):考虑放弃
6.5.4 标签 Tab

按标签组织任务,每个标签组显示对应任务列表:

this.TagGroup('🔥 重要', '#E74C3C', ALL_TASKS.filter(t => t.tags.includes('重要')))
this.TagGroup('⚡ 紧急', '#F39C12', ALL_TASKS.filter(t => t.tags.includes('紧急')))
this.TagGroup('🐛 Bug', '#3498DB', ALL_TASKS.filter(t => t.tags.includes('Bug')))
// ...
6.5.5 任务行展开详情

点击任务行右侧的 > 可以展开查看详情,包括描述和子任务列表:

if (this.showDetailId === t.id) {
  Column() {
    if (t.desc) { Text('📝 ' + t.desc).fontSize(11).fontColor('#888') }
    if (t.subtasks.length > 0) {
      ForEach(t.subtasks, (st: string) => {
        Row() { Text('☐ ' + st).fontSize(11).fontColor('#555') }.width('100%')
      }, (st: string) => st)
    }
    Row({ space: 6 }) {
      Button('✏️ 编辑').height(28).fontSize(11).backgroundColor('#EEF0FF').fontColor('#4A6CF7')
      Button('✅ 完成').height(28).fontSize(11).backgroundColor('#F0FFF4').fontColor('#22C55E')
      Text(t.due).fontSize(10).fontColor('#AAA')
    }.width('100%').margin({ top: 6 })
  }
  .width('100%').padding(8).backgroundColor('#F9FAFB').borderRadius(8).margin({ top: 4 })
}

6.6 专注统计页:Canvas 柱状图 + 高效时段分析

FocusStatsPage 提供三个时间维度的专注数据(今日/本周/本月),核心是 Canvas 自绘的柱状图。

6.6.1 Canvas 柱状图实现
private barCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D();

drawBar(): void {
  const ctx = this.barCtx;
  const w = 320, h = 160;
  ctx.clearRect(0, 0, w, h);

  const maxVal = Math.max(...WEEK_DATA.map(d => d.minutes), 1);
  const barW = 30, padL = 20, padB = 24;

  for (let i = 0; i < WEEK_DATA.length; i++) {
    const d = WEEK_DATA[i];
    const x = padL + i * 42;
    const bh = (d.minutes / maxVal) * (h - padB - 10);

    // 绘制柱体
    ctx.fillStyle = '#3498DB';
    ctx.fillRect(x, h - padB - bh, barW, bh);

    // 顶部数值标签
    ctx.font = '9px HarmonyOS Sans';
    ctx.fillStyle = '#888';
    ctx.textAlign = 'center';
    ctx.fillText(d.minutes + 'min', x + barW / 2, h - padB - bh - 4);

    // 底部星期标签
    ctx.fillText(d.label, x + barW / 2, h - 4);
  }
}

Canvas 绘制要点

  • clearRect 清除画布,避免绘制叠加
  • 柱体高度通过 (当前值 / 最大值) × (总高度 - 留白) 计算
  • 顶部数值标签与柱体上边缘保持 4px 间距
  • 底部星期标签与画布底部保持 4px 间距
6.6.2 总览统计卡片

六张统计卡片覆盖专注的各个维度:

Row({ space: 8 }) {
  this.OverviewCard('总专注时长', this.totalMinutes + 'min', '#3498DB')
  this.OverviewCard('完成番茄', this.totalSessions + '个', '#1ABC9C')
  this.OverviewCard('日均专注', this.avgMinutes + 'min', '#F39C12')
}.width('100%')
Row({ space: 8 }) {
  this.OverviewCard('最长连续日', '5天', '#9B59B6')
  this.OverviewCard('效率评分', '88分', '#E67E22')
  this.OverviewCard('中断次数', '3次', '#E74C3C')
}.width('100%').margin({ top: 6 })
6.6.3 高效时间段分析

通过 HourBar 组件展示 8 个时间段的热度:

Row() {
  this.HourBar('6-8', 0.3)
  this.HourBar('8-10', 0.8)
  this.HourBar('10-12', 0.6)
  this.HourBar('12-14', 0.2)
  this.HourBar('14-16', 0.7)
  this.HourBar('16-18', 0.4)
  this.HourBar('18-20', 0.3)
  this.HourBar('20-22', 0.5)
}.width('100%').margin({ top: 6 })

Text('💡 你的高效时段是 8:00-10:00 和 14:00-16:00')
  .fontSize(11).fontColor('#1ABC9C').width('100%').margin({ top: 6 })

每个 HourBar 是一个从底部向上的柱体,高度由 pct 参数控制(0-1 范围),让用户直观地看到自己一天中哪些时间段最专注。

6.7 周报复盘页:Canvas 饼图 + 完成率 + AI 建议

WeeklyReportPage 是时间管理闭环的最后一环——复盘。包含周评分、时间分布饼图、完成率和 AI 建议。

6.7.1 周评分

综合专注时长、任务完成率和习惯打卡率三个维度给出周评分(85 分),并与上一周对比:

Row() { Text('专注时长').fontSize(11).fontColor('#555'); Blank(); Text('28h ↑12%').fontSize(11).fontColor('#22C55E') }
Row() { Text('任务完成').fontSize(11).fontColor('#555'); Blank(); Text('75% ↑5%').fontSize(11).fontColor('#22C55E') }
Row() { Text('习惯打卡').fontSize(11).fontColor('#555'); Blank(); Text('68% ↓3%').fontSize(11).fontColor('#E74C3C') }

绿色箭头表示提升,红色箭头表示下降,用户一眼就能看到哪些方面做得好、哪些需要改进。

6.7.2 Canvas 甜甜圈饼图

与 WealthWise 的投资饼图类似,但展示的是时间分类分布:

drawPie(): void {
  const ctx = this.pieCtx;
  const cx = 80, cy = 80, r = 55;
  ctx.clearRect(0, 0, 160, 160);
  let startAngle = -Math.PI / 2;
  for (const d of TIME_DIST) {
    const angle = (d.pct / 100) * 2 * Math.PI;
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.arc(cx, cy, r, startAngle, startAngle + angle);
    ctx.closePath();
    ctx.fillStyle = d.color;
    ctx.fill();
    startAngle += angle;
  }
  // 中心甜甜圈
  ctx.beginPath();
  ctx.arc(cx, cy, 28, 0, 2 * Math.PI);
  ctx.fillStyle = '#FFF';
  ctx.fill();
  ctx.font = '12px HarmonyOS Sans';
  ctx.fillStyle = '#333';
  ctx.textAlign = 'center';
  ctx.fillText('本周', cx, cy - 4);
  ctx.fillText('37h', cx, cy + 10);
}
6.7.3 完成率统计

三个维度的完成率进度条:

ForEach(COMPLETIONS, (c: CompletionItem) => {
  Row() {
    Text(c.label).fontSize(13).fontColor('#333').width(100)
    Progress({ value: c.done, total: c.total }).width(0).layoutWeight(1).height(8)
      .color(c.color).backgroundColor('#EEE')
    Text(c.done + '/' + c.total).fontSize(12).fontColor('#888').width(48).textAlign(TextAlign.End)
  }.width('100%').padding({ top: 4, bottom: 4 })
}, (c: CompletionItem) => c.label)
6.7.4 AI 复盘建议

四条 AI 生成的复盘建议,涵盖正面反馈、问题预警和优化建议:

Column({ space: 6 }) {
  Row() { Text('✅ '); Text('本周专注时长相比上周增加 12%,继续保持').fontSize(12).fontColor('#555') }
  Row() { Text('⚠️ '); Text('习惯打卡率下降 3%,建议设定每日固定提醒').fontSize(12).fontColor('#F39C12') }
  Row() { Text('💡 '); Text('会议时间占比 12%,可考虑缩短为 45min').fontSize(12).fontColor('#3498DB') }
  Row() { Text('🎯 '); Text('下周一建议安排 1h 深度阅读,优先级设为高').fontSize(12).fontColor('#1ABC9C') }
}.width('100%').padding(10).backgroundColor('#F9FAFB').borderRadius(10).margin({ top: 8 })
6.7.5 下周规划

底部的「下周关键事项」为下周的规划提供了起点:

Column({ space: 4 }) {
  Row() { Text('📌').fontSize(12); Text('完成Q3产品路线图终稿(高优先级)').fontSize(12).fontColor('#555') }
  Row() { Text('📌').fontSize(12); Text('准备周五 VP 演示 PPT').fontSize(12).fontColor('#555') }
  Row() { Text('📌').fontSize(12); Text('跑步至少 4 次,恢复习惯打卡').fontSize(12).fontColor('#555') }
}.width('100%').padding(10).backgroundColor('#FFF8E1').borderRadius(10).margin({ top: 8 })

7. ArkTS 语法陷阱与踩坑记录

7.1 不能在 UI 构建区域使用 const/let

问题:在 @Builderbuild() 方法的 UI 区域中,不能使用 constletvar 声明。

// ❌ 错误
ForEach(WEEKDAYS, (wd: string, idx: number) => {
  const dayNum = this.selectedDate - (new Date().getDay() - idx); // 编译错误
  Column() { /* ... */ }
}, (wd: string) => wd)

// ✅ 正确:将计算逻辑抽取到 getter
get weekDayNumbers(): number[] {
  const today = new Date().getDay();
  return WEEKDAYS.map((_, idx) => this.selectedDate - (today - idx));
}

ForEach(this.weekDayNumbers, (dayNum: number, idx: number) => {
  Column() { /* ... */ }
}, (d: number) => d.toString())

7.2 @Builder 函数中的 this 引用

问题:在 @Builder 函数中引用 this 需要特别注意——如果 Builder 函数是类的成员方法(使用 this.BuilderName() 调用),this 是合法的。但如果 Builder 是独立的 function,则不能使用 this

在时光织中,所有 @Builder 都是类的成员方法(通过 this.XXX() 调用),因此使用 this.tabIdxthis.selectedDate 等状态是合法的。

7.3 Canvas 绘制时机

问题:Canvas 绘制必须在 onReady 回调中执行,不能在其他生命周期方法中调用。

// ❌ 错误
aboutToAppear(): void {
  this.drawPie(); // Canvas 可能还没准备好
}

// ✅ 正确
Canvas(this.pieCtx)
  .width(160).height(160)
  .onReady(() => this.drawPie())

7.4 Progress 组件的动态颜色

问题Progress 组件的 color 属性虽然接受动态值,但需要在条件渲染场景中注意:

// ❌ 错误用法(假设在 ForEach 中)
Progress({ value: cat.spent, total: cat.budget })
  .color(dynamicCondition ? '#E74C3C' : '#3498DB') // 可能不生效

// ✅ 正确:在 Progress 外部计算颜色
const progressColor = cat.spent > cat.budget * 0.9 ? '#E74C3C' :
  cat.spent > cat.budget * 0.7 ? '#F39C12' : cat.color;
Progress({ value: cat.spent, total: cat.budget })
  .color(progressColor)

虽然 ArkTS 支持表达式链式调用,但建议将复杂条件计算提前赋值给常量。

7.5 setInterval 与组件生命周期

问题:使用 setInterval 实现的番茄钟计时器,在组件销毁时需要清理定时器,否则可能导致内存泄漏和奇怪的 bug。

startFocus(): void {
  this.timerId = setInterval(() => {
    // 倒计时逻辑
  }, 1000);
}

stopFocus(): void {
  clearInterval(this.timerId); // 必须清理
  this.isFocusing = false;
}

虽然 ArkTS 当前不支持标准的 aboutToDisappear 生命周期清理,但在 stopFocus() 中显式清理是最安全的方式。


8. 性能优化与最佳实践

8.1 ForEach 的 key 生成策略

// 数字 ID 作为 key
ForEach(items, (item) => { ... }, (item: ItemType) => item.id.toString())

// 组合 key(当没有单字段唯一标识时)
ForEach(items, (item) => { ... }, (item: ItemType) => item.time + item.title)

唯一 key 帮助 ArkTS 框架准确追踪列表元素的变化,避免不必要的重渲染。

8.2 状态管理最小化

// ✅ 静态数据用 const(不触发重渲染)
const TIME_BLOCKS: TimeBlock[] = [ ... ];
const TASKS: TaskItem[] = [ ... ];

// ✅ 只在需要驱动 UI 更新时用 @State
@State tabIdx: number = 0;
@State isFocusing: boolean = false;
@State focusMinutes: number = 25;
@State focusSeconds: number = 0;
@State selectedDate: number = new Date().getDate();

8.3 计算属性(getter)替代方法调用

使用 getter 来计算派生状态,而不是在 build 方法中调用函数:

// ✅ 正确:getter 缓存计算结果
get focusDisplay(): string { ... }
get focusProgress(): number { ... }
get weekDayNumbers(): number[] { ... }

// ❌ 错误:每次渲染都重新计算
build() {
  Text(this.calcFocusDisplay()) // 每次渲染都调用方法
}

虽然 ArkTS 的 getter 并非严格「缓存」(每次访问都会重新计算),但相比在 build 方法中混合计算逻辑,getter 使代码更清晰、更易于维护。

8.4 布局性能

  • 使用 layoutWeight(1) 替代硬编码百分比,让布局计算交给框架
  • 使用 Blank() 组件填充剩余空间,替代空白的 Text 组件
  • 列表使用 Scroll 包裹,确保内容超出屏幕高度时可滚动
  • 使用 width('94%') + padding 保持卡片间一致性间距
  • Flex 组件替代 Row + flexWrap(In ArkTS, flexWrap 属性在 Row 上不存在,必须使用 Flex 组件)

9. 未来 Roadmap

9.1 短期计划(1-2 个月)

  • 番茄钟完整功能:暂停/继续、白噪音、专注统计持久化
  • 任务 CRUD:添加、编辑、删除、状态流转(收件箱→待办→完成)
  • 日历订阅:导入系统日历和第三方日历(ICS 格式)
  • 数据持久化:RelationalStore 替换模拟数据

9.2 中期计划(3-6 个月)

  • 拖拽调整时间块:在时间线上拖拽调整时间块长度和位置
  • AI 智能排程:输入任务列表,AI 自动分配到合适的时间块
  • 习惯追踪统计:连续天数、完成率趋势、习惯分类管理
  • 主屏幕 Widget:显示今日时间线和专注状态
  • 通知提醒:@ohos.notification + 前台常驻服务

9.3 长期愿景(6-12 个月)

  • 复盘对话:每周日 AI 和你对话式复盘,给出下周优化建议
  • 时间借贷:某任务超时从后续缓冲块「借」时间
  • 勿扰模式:专注期间自动开启系统勿扰 + 通知折叠
  • 华为手表协同:在手表端快捷查看时间线和专注控制
  • 团队协作:共享日历、分配任务、团队专注排行榜

10. 总结

10.1 项目数据统计

指标 数值
页面数量 4 个
代码行数 ~1,260 行 (Ets)
Interface 定义 7 个
Canvas 自绘图表 2 个(饼图、柱状图)
模拟数据量 6 类、40+ 条记录
编译耗时 ~300ms(增量)
主要编译错误修复 3 类(UI 区域限制、重复代码、Canvas 时机)

10.2 与 WealthWise 的架构对比

两个 App 虽同属一个项目,但在架构设计和用户体验上有显著的差异。通过对比可以更清晰地理解 ArkTS 在不同场景下的应用模式:

维度 WealthWise(财智管家) TimeWeave(时光织)
主色调 财富金 #F59E0B + 深海蓝 #0F172A 沉稳蓝 #2C3E50 + 活力青 #1ABC9C
品牌感觉 稳重、信任、增值 冷静、专注、高效
Tab 结构 4Tab(资产/投资/预算/规划) 4Tab(日程/任务/专注/统计)
特色交互 金额隐私保护(👁️切换) 沉浸式专注视图(覆盖全局)
核心算法 复利计算 FV = PMT × ((1+r)^n - 1)/r 番茄钟倒计时 setInterval
Canvas 图表 投资配置甜甜圈饼图 时间分布甜甜圈饼图
数据过滤 交易记录按日期分组 + 类型筛选 任务按 GTD 四象限动态分类
页面数量 9 个页面 4 个页面
模拟数据量 30+ 条记录 40+ 条记录
Header 背景 深海蓝渐变 沉稳蓝纯色
卡片圆角 14px 14px(一致)
共用组件模式 @Builder 成员方法 @Builder 成员方法

架构决策差异分析

1. 状态管理的复杂度

WealthWise 的核心状态是显隐(showAmount)和切换(tabIdxselectedAccount),状态之间相对独立。而 TimeWeave 的状态之间存在复杂联动——isFocusingtrue 时会覆盖所有 Tab 内容,而 focusMinutesfocusSeconds 每秒都在变化。这意味着 TimeWeave 的 UI 更新频率远高于 WealthWise。

2. 数据流向模式

WealthWise 的数据流向是「读取 → 显示」,用户交互主要是页面跳转和条件显隐,不涉及数据变更。TimeWeave 的数据流向引入了「用户操作 → 状态变更 → UI 重绘」的闭环——点击任务勾选框修改 t.done,点击专注按钮启动定时器,这些都直接驱动 UI 变化。

3. 页面间耦合度

WealthWise 的页面间耦合度较低——AssetDetail 不依赖 InvestmentDetail 的状态,每个页面独立运作。TimeWeave 的页面间存在一定的逻辑关联——MainPage 的专注统计数据与 FocusStatsPage 共享同一份数据源(虽然当前版本使用独立的模拟数据)。

4. 沉浸式体验设计

TimeWeave 的沉浸式专注视图是一个独特的设计决策。当用户开始番茄钟后,所有 Tab 内容和导航栏都被隐藏,取而代之的是一张全屏的深色专注卡片。这个设计模拟了「进入心流状态」的心理体验——外界干扰被屏蔽,只剩下计时器和当前任务。这种设计在 ArkTS 中通过一个简单的 if/else 状态判断即可实现,展示了声明式 UI 在状态驱动视图切换方面的天然优势。

10.3 从模拟数据到真实数据的演进路径

两个 App 当前都使用静态模拟数据。在实际产品化过程中,需要逐步替换为真实数据源。以下是推荐的演进路径:

第一阶段:LocalStorage + AppStorage

利用 HarmonyOS 的 LocalStorage 和 AppStorage 实现跨页面的状态共享。用户的偏好设置(如 showAmountfaceIdEnabled)可以用 AppStorage 持久化。

第二阶段:RelationalStore 数据库

使用 @ohos.data.relationalStore 创建本地关系型数据库,建立 tasktime_blockfocus_sessionhabit_record 等表。模拟数据作为数据库的初始种子数据。

第三阶段:端到端加密云同步

通过华为云或 WebDAV 实现多设备数据同步。同步前对敏感数据进行端到端加密,确保数据安全。

第四阶段:AI 能力接入

接入华为 AI 能力(ML Kit、预测服务),实现智能排程、语音添加任务、图片识别日程等功能。

10.4 核心收获

通过「时光织」TimeWeave 的开发,我们进一步加深了对 HarmonyOS Next (API 24) ArkTS 开发范式的理解,并总结了以下关键经验:

  1. 状态驱动的 UI 架构:通过 @State 驱动的 if/else 条件渲染,实现了从正常视图到沉浸式专注视图的无缝切换,这是 ArkTS 声明式 UI 相较于传统命令式 UI 的核心优势。只需修改一个布尔值 isFocusing,整个页面的布局和样式就随之改变,无需手动操作 DOM。

  2. Canvas 自绘图表复用:将 WealthWise 中的 Canvas 饼图绘制经验成功迁移到 TimeWeave 的时间分布图中。两个饼图的绘制算法完全一致,只有数据源和中心文字不同。这验证了 Canvas 组件的高可复用性,为未来建立统一的图表组件库奠定了基础。

  3. 数据层与 UI 层分离:通过在 getter 中预处理数据(如 weekDayNumbersfocusDisplayfocusProgress),保持 build() 方法的纯净和可读性。这是一个在 ArkTS 中非常重要的编程范式——由于在 UI 构建区域不允许使用 const/let,所有的数据处理逻辑都必须通过 getter 或方法前置。

  4. setInterval 计时器管理:番茄钟的实现展示了在 ArkTS 中管理定时器的标准模式——开始(startFocus)、更新(setInterval 回调)、清理(stopFocus 中的 clearInterval)。这是 App 中为数不多的涉及「实时更新」的场景,为未来开发计时器、秒表、动画等实时功能提供了参考。

  5. GTD 方法论的数字化实践:将 GTD 的「收集→整理→组织→执行→复盘」五个步骤转化为数字化的 Tab 和页面。收件箱对应「收集」,项目分组和标签对应「整理→组织」,任务列表和番茄钟对应「执行」,周报复盘对应「复盘」。这让 GTD 不再是纸上谈兵,而是真正可执行的时间管理系统。

  6. 错误恢复的经验教训:在开发过程中,Index.ets 文件意外被清空(0 字节),这是一个严重的问题。通过及时创建新的导航首页并重建文件内容,成功恢复了应用入口。这一经历提醒我们:在编辑文件时要格外小心 edit_file 操作的 old_string 唯一性,以及在进行批量文件操作时做好备份。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐