引言:敏捷开发时代的看板文化与鸿蒙原生体验

在当今快速迭代的软件研发环境中,敏捷开发已经成为主流的研发协作范式,而看板(Kanban Board)作为敏捷方法论中最直观、最高效的可视化工具之一,几乎贯穿了每一个现代研发团队的日常协作流程。看板的核心思想源自丰田生产系统的精益制造理念——通过将工作流程中的各个阶段以"列"的形式可视化呈现,让团队成员能够一目了然地掌握任务的流转状态、瓶颈所在和整体进度。随着移动办公的普及和跨端协同需求的增长,将看板工具从桌面端延伸到移动端乃至全场景设备,已成为提升团队协作效率的关键一环。

在这里插入图片描述

HarmonyOS(鸿蒙操作系统)作为华为面向万物互联时代打造的分布式操作系统,其应用开发框架 ArkUI 提供了一套基于声明式编程范式(Declarative UI)的 UI 开发体系。ArkTS 作为 ArkUI 的开发语言,在 TypeScript 的基础上进行了扩展和优化,引入了 @Entry@Component@State@Builder 等一系列装饰器,使得开发者能够以更加简洁、高效、类型安全的方式构建高性能的跨端应用界面。ArkTS 的声明式语法让开发者只需描述"界面应该是什么样子",而框架会自动处理状态变化与视图更新的映射关系,极大地降低了 UI 编程的心智负担。

本文将对一个基于 HarmonyOS ArkTS 编写的项目看板应用进行逐段深度解析。该应用完整地实现了看板的核心功能场景,涵盖了任务的多列看板视图、任务列表视图、项目统计数据可视化以及个人项目中心等多个功能模块。应用采用了 Deep Purple(深紫 #4527A0)、Pink(玫红 #C2185B)、Teal(青色 #00838F)三色为主的配色体系,搭配浅紫色背景(#F3E5F5),营造出既专业又富有活力的视觉氛围。在技术实现上,该应用充分运用了 ArkTS 的状态管理机制、@Builder 构建器复用模式、ForEach 列表渲染、Scroll 滚动容器、模态弹窗、Stack 层叠布局等核心特性,是一个研究 ArkTS 声明式 UI 编程的极佳案例。

通过本文的逐段拆解,读者将深入理解:ArkTS 如何通过接口(Interface)定义严格的数据模型;如何利用配置化的常量映射表实现 UI 样式与业务逻辑的解耦;如何通过 @State 装饰器实现组件内部状态的响应式驱动;如何借助 @Builder 构建器实现 UI 片段的复用与组合;以及如何通过 Stack + zIndex 的方式实现自定义模态弹窗等高级交互模式。无论你是刚刚接触鸿蒙开发的新手,还是希望深入掌握 ArkTS 声明式编程精髓的进阶开发者,相信本文都能为你提供有价值的参考与启发。


一、数据模型层:接口定义与类型体系

1.1 列元信息接口 ColumnMeta

interface ColumnMeta {
  label: string
  icon: string
  color: string
  bg: string
}

在这里插入图片描述

这是整个应用数据模型体系中最基础的接口之一,用于描述看板中每一"列"(Column)的元数据信息。在敏捷看板中,列代表了任务所处的不同工作流阶段——例如"待办"、“进行中”、“审核中”、“已完成"等。ColumnMeta 接口定义了四个字段:label 存储列的显示名称(如"待办”);icon 存储该列对应的 Emoji 图标符号(如"📋"),用于在 UI 中快速识别;color 是该列的主题色值,用于列标题文字、任务计数徽章等元素的前景色着色;bg 则是该列的背景色值,用于为整列区域渲染淡色背景底,从而在视觉上将不同列区分开来。

将列的视觉属性抽象为接口的好处是显而易见的:它实现了"数据驱动 UI"的设计思想。后续在渲染看板时,只需将列名作为 key 传入配置映射表,即可获取该列对应的全部样式信息,无需在 UI 代码中硬编码任何颜色值或图标字符串。这种做法使得配色方案的调整、新列的增删都变得非常便捷——只需修改配置表即可,无需触碰任何组件代码。这正是"配置优于编码"(Configuration over Code)这一软件设计原则在 ArkTS 开发中的具体体现。

1.2 优先级元信息接口 PriorityMeta

interface PriorityMeta {
  label: string
  color: string
  level: number
}

在这里插入图片描述

PriorityMeta 接口用于描述任务优先级的元数据。在看板类项目管理工具中,优先级是一个核心维度的业务属性,它帮助团队成员快速识别哪些任务需要优先处理。该接口包含三个字段:label 是优先级的显示文本(如"紧急"、“重要”、“普通”、“低”);color 是该优先级对应的主题色,通常用于渲染优先级标签的背景色,使不同优先级的任务在视觉上具有强烈的辨识度——例如"紧急"用玫红色(#C2185B)表示警示,"低"用灰色(#9E9E9E)表示次要;level 是一个数值型的优先级等级(1-4),用于在业务逻辑中进行优先级的数值比较和排序,这种将语义标签与数值等级分离的设计使得排序逻辑可以直接基于数字运算,而展示逻辑则基于语义标签,两者各司其职。

值得注意的是,level 字段的存在体现了接口设计的"前瞻性"考量。虽然在本应用的当前版本中,任务的排序并未直接使用 level 字段,但保留这个数值型字段为未来的功能扩展(如按优先级自动排序、优先级筛选过滤等)预留了空间。这种在数据模型中预留扩展字段的做法,是良好接口设计的体现。

1.3 成员信息接口 MemberInfo

interface MemberInfo {
  name: string
  emoji: string
  role: string
  taskCount: number
  completedCount: number
  color: string
}

在这里插入图片描述

MemberInfo 接口定义了项目团队成员的数据结构。在一个协作型看板应用中,成员管理是不可或缺的功能模块。该接口的字段设计涵盖了成员的多个维度信息:name 是成员姓名;emoji 是成员的头像 Emoji(如"🧑‍💻"表示前端开发人员),这种用 Emoji 代替真实头像的做法在原型设计和轻量级应用中非常实用,既避免了图片资源的加载开销,又能传达一定的角色语义;role 是成员的职能角色描述(如"前端负责人"、“后端架构师”);taskCountcompletedCount 分别记录了该成员分配到的任务总数和已完成任务数,这两个字段是统计模块中"成员工作负载"图表的数据来源;color 是该成员的主题色,用于在统计图表中为每个成员渲染不同颜色的进度条。

这里特别值得关注的是 taskCountcompletedCount 的分离设计。通过这两个字段的比值(completedCount / taskCount),可以计算出成员的任务完成率,进而渲染出直观的进度条。这种"总量 + 完成量"的双字段设计是数据统计类应用中最常见的数据建模模式之一,它既能展示绝对数量,又能派生出比率指标,信息密度高且计算灵活。

1.4 任务项接口 TaskItem

interface TaskItem {
  id: number
  title: string
  description: string
  column: string
  priority: string
  assignee: string
  dueDate: string
  tags: string[]
  createdAt: string
  isCompleted: boolean
  storyPoints: number
}

在这里插入图片描述

TaskItem 是整个应用中最核心、字段最丰富的接口,它完整地描述了一个看板任务的全生命周期信息。我们来逐一分析每个字段的设计意图:

  • id: number:任务的唯一标识符,采用数值类型,用于在列表渲染、详情查看、编辑删除等场景中精确地定位特定任务。
  • title: string:任务标题,是任务最核心的摘要信息,在看板卡片和列表项中以加粗字体突出显示。
  • description: string:任务描述,提供任务的详细说明,在卡片视图中会截断显示两行,在详情弹窗中完整展示。
  • column: string:任务当前所在的看板列名称(如"待办"、"进行中"等),这个字段是看板列筛选的核心依据——应用通过 mockTasks.filter(t => t.column === '待办') 这样的方式将任务分配到对应的列中。
  • priority: string:任务优先级标签,与 PRIORITY_CONFIG 配置表的 key 对应,用于渲染优先级标签。
  • assignee: string:任务负责人姓名,与 ASSIGNEE_LIST 中的成员对应。
  • dueDate: string:任务截止日期,以字符串形式存储(如"2026-07-28"),在列表视图中以红色字体显示,暗示其时间敏感性。
  • tags: string[]:任务标签数组(如['前端', '核心功能']),用于任务的分类和检索,在卡片中显示前两个标签。
  • createdAt: string:任务创建日期,用于记录任务的时间线信息。
  • isCompleted: boolean:任务是否已完成的布尔标记,与 column 字段存在一定的语义重叠(column === '已完成' 通常意味着 isCompleted === true),但独立保留这个布尔字段可以支持更灵活的状态查询。
  • storyPoints: number:故事点(Story Points),这是敏捷开发中用于估算任务工作量的相对单位,本应用在统计模块中对所有任务的故事点进行汇总,展示项目的总工作量。

这个接口的设计体现了"富领域模型"的思想——将任务相关的所有信息聚合在一个接口中,避免了在多个组件之间传递零散的字段。同时,字段类型的选择也很考究:idnumber 而非 string 是因为数值比较比字符串比较更高效;日期用 string 而非 Date 对象是因为在 ArkTS 的声明式 UI 中,字符串可以直接用于文本渲染,无需额外的格式化转换;tagsstring[] 数组而非逗号分隔的字符串,体现了对标签这种"多值属性"的原生支持。


二、配置常量层:配置化驱动 UI 样式

2.1 看板列配置 BOARD_COLUMNS

const BOARD_COLUMNS: Record<string, ColumnMeta> = {
  '待办': { label: '待办', icon: '📋', color: '#4527A0', bg: '#EDE7F6' },
  '进行中': { label: '进行中', icon: '🔄', color: '#00838F', bg: '#E0F2F1' },
  '审核中': { label: '审核中', icon: '👀', color: '#FF8F00', bg: '#FFF3E0' },
  '已完成': { label: '已完成', icon: '✅', color: '#2E7D32', bg: '#E8F5E9' }
}

在这里插入图片描述

BOARD_COLUMNS 是一个 Record<string, ColumnMeta> 类型的常量对象,它将四个看板列的名称作为 key,将对应的 ColumnMeta 元数据作为 value,构成了一个完整的列配置映射表。这里使用 Record<string, ColumnMeta> 而非普通的对象字面量类型,是 TypeScript/ArkTS 中描述"字符串键到特定值类型映射"的标准方式,它提供了完整的类型安全保证——当你用 BOARD_COLUMNS['待办'] 访问时,编译器知道返回值一定是 ColumnMeta 类型,可以直接安全地访问 .color.bg 等属性。

从配色策略来看,四列采用了语义化的色彩体系:"待办"使用深紫色(#4527A0)配淡紫底(#EDE7F6),传达"待启动"的沉静感;"进行中"使用青色(#00838F)配淡青底(#E0F2F1),传达"进行中"的活力感;"审核中"使用琥珀色(#FF8F00)配淡橙底(#FFF3E0),传达"待确认"的警示感;"已完成"使用绿色(#2E7D32)配淡绿底(#E8F5E9),传达"已完成"的成就感。这种"主色 + 同色系浅色背景"的配色方案,既保证了列与列之间的视觉区分度,又维持了整体配色的和谐统一,是非常成熟的 UI 配色实践。

在整个应用的后续代码中,BOARD_COLUMNS 被广泛引用——在看板视图中渲染列标题和列背景、在列表视图中渲染任务的状态图标、在统计视图中渲染分布条形图的颜色等。这种"一处定义、多处引用"的配置化模式,确保了全局视觉风格的一致性,也大大降低了后期维护和调整的成本。

2.2 优先级配置 PRIORITY_CONFIG

const PRIORITY_CONFIG: Record<string, PriorityMeta> = {
  '紧急': { label: '紧急', color: '#C2185B', level: 4 },
  '重要': { label: '重要', color: '#FF6F00', level: 3 },
  '普通': { label: '普通', color: '#1565C0', level: 2 },
  '低': { label: '低', color: '#9E9E9E', level: 1 }
}

在这里插入图片描述

PRIORITY_CONFIG 的结构与 BOARD_COLUMNS 类似,是一个优先级元数据的映射表。四个优先级从高到低依次为"紧急"(level 4,玫红色 #C2185B)、“重要”(level 3,橙色 #FF6F00)、“普通”(level 2,蓝色 #1565C0)、“低”(level 1,灰色 #9E9E9E)。这里的设计亮点在于颜色与语义的匹配:玫红色作为最高优先级的警示色,橙色次之,蓝色代表常规优先级,灰色则表示最低优先级——这与交通信号灯的"红-黄-蓝/绿-灰"色彩语义体系高度一致,符合用户的直觉认知。

在实际 UI 渲染中,PRIORITY_CONFIG 被用于多处:在任务卡片中渲染优先级标签的背景色和文字、在新建任务弹窗中渲染优先级选择按钮的选中/未选中状态、在任务详情弹窗中渲染顶部优先级徽章、在删除确认弹窗中渲染任务优先级标签等。通过统一引用这个配置表,所有涉及优先级展示的 UI 元素都保持了视觉风格的一致性。

2.3 列表常量与可选值数组

const COLUMN_NAMES: string[] = ['待办', '进行中', '审核中', '已完成']
const PRIORITY_LIST: string[] = ['紧急', '重要', '普通', '低']
const ASSIGNEE_LIST: string[] = ['张三', '李四', '王五', '赵六']

在这里插入图片描述

这三个数组常量分别定义了列名列表、优先级列表和负责人列表。COLUMN_NAMESPRIORITY_LIST 的值与前面两个配置映射表的 key 一一对应,它们存在的意义是提供有序的遍历能力——Record 类型的对象虽然可以通过 Object.keys() 获取键名,但键的顺序在不同运行时中并不一定保证稳定,而数组天然具有确定的遍历顺序。ASSIGNEE_LIST 则定义了任务的可选负责人列表,在新建任务弹窗的"负责人"选择器中通过 ForEach 遍历渲染为可点击的选择按钮。

这种"映射表 + 有序列表"的双重数据结构设计是一种非常实用的模式:映射表用于通过 key 快速查找单个配置项的详细信息,列表用于按固定顺序遍历所有配置项。两者配合使用,既保证了查找效率,又保证了遍历顺序的可控性。


三、数据层:Mock 数据与辅助函数

3.1 任务 Mock 数据集

const mockTasks: TaskItem[] = [
  { id: 1, title: '用户登录模块开发', description: '实现手机号/邮箱登录功能', column: '进行中', priority: '紧急', assignee: '张三', dueDate: '2026-07-28', tags: ['前端', '核心功能'], createdAt: '2026-07-15', isCompleted: false, storyPoints: 8 },
  // ... 共25条任务数据
  { id: 25, title: '部署流水线优化', description: 'CI/CD流水线并行化加快构建', column: '待办', priority: '重要', assignee: '李四', dueDate: '2026-08-15', tags: ['运维', '效率'], createdAt: '2026-07-22', isCompleted: false, storyPoints: 5 }
]

mockTasks 是一个包含 25 条 TaskItem 数据的数组,它为整个应用提供了完整的模拟数据源。这 25 条任务数据覆盖了前端、后端、运维、测试、数据等多个技术方向,分布在"待办"、“进行中”、“审核中”、“已完成"四个列中,涵盖了"紧急”、“重要”、“普通”、"低"四种优先级,并分配给了四名不同的团队成员。这种"全覆盖"式的数据设计确保了应用在各个视图(看板、列表、统计)中都能展示丰富的内容,不会出现某个列或某个统计维度为空的尴尬情况。

从每条任务数据的字段填充来看,数据的真实感很强——任务标题如"用户登录模块开发"、“数据库索引优化”、"支付接口联调"等都是真实研发项目中常见的任务类型;描述文案简洁而准确;截止日期分布在 2026 年 7 月至 8 月之间,形成了一个合理的时间跨度;故事点的取值(2、3、5、8、13)遵循了敏捷开发中常用的斐波那契数列估算惯例,体现了对敏捷实践的深入理解。

需要特别说明的是,本应用作为原型/Demo 级别的实现,采用了前端 Mock 数据的方式提供数据源,而非通过 HTTP 请求从后端 API 获取。这种做法在应用开发早期阶段非常常见——它可以解除前端开发对后端 API 进度的依赖,让开发者能够快速验证 UI 设计和交互逻辑。在真实的生产环境中,只需将 mockTasks 替换为通过 @ohos.net.http 模块发起网络请求获取的数据即可,数据模型接口 TaskItem 的定义无需任何改动,这体现了数据模型层与数据获取层的良好解耦。

3.2 成员 Mock 数据集

const mockMembers: MemberInfo[] = [
  { name: '张三', emoji: '🧑‍💻', role: '前端负责人', taskCount: 8, completedCount: 4, color: '#4527A0' },
  { name: '李四', emoji: '👨‍🔧', role: '后端架构师', taskCount: 7, completedCount: 2, color: '#00838F' },
  { name: '王五', emoji: '💻', role: '后端开发', taskCount: 6, completedCount: 3, color: '#FF8F00' },
  { name: '赵六', emoji: '🎨', role: '全栈开发', taskCount: 5, completedCount: 1, color: '#C2185B' }
]

mockMembers 数组定义了四名团队成员的信息,每个成员的 color 字段分别对应应用四色主题体系中的一种主色,确保了成员在统计图表中的视觉辨识度。taskCountcompletedCount 的数值设计也颇具用心——张三作为前端负责人任务最多(8 项)且完成率最高(50%),李四作为后端架构师任务次之但完成率较低(约 28.6%),这些数据为统计模块的"成员工作负载"图表提供了有意义的可视化素材。

3.3 统计辅助函数

function getTaskTotal(): number { return 25 }
function getCompletedTotal(): number { return 5 }
function getInProgressTotal(): number { return 6 }
function getOverdueCount(): number { return 3 }
function getTotalStoryPoints(): number { return 138 }

这五个函数分别返回总任务数、已完成任务数、进行中任务数、逾期任务数和总故事点数。这里采用函数而非常量的设计选择值得探讨:虽然这些值在本应用中是固定的,但使用函数封装意味着在真实场景中,这些值可以从后端 API 动态获取或通过遍历 mockTasks 数组实时计算。例如,getTaskTotal() 在真实实现中可以写成 return mockTasks.lengthgetCompletedTotal() 可以写成 return mockTasks.filter(t => t.isCompleted).length。函数封装为这种从"静态返回"到"动态计算"的演进提供了平滑的过渡路径,调用方代码无需修改。

这种"以函数封装数据访问"的模式在软件工程中被称为"数据访问抽象层",它将数据的来源(常量、计算、网络请求)与数据的使用方解耦,是提高代码可维护性的重要手段。


四、Tab 枚举与入口组件

4.1 Tab 页枚举定义

enum KanbanTab {
  BOARD = 0,
  LIST = 1,
  STATS = 2,
  PROFILE = 3
}

KanbanTab 是一个数值型枚举,定义了应用底部导航栏的四个 Tab 页:BOARD(看板,值 0)、LIST(列表,值 1)、STATS(统计,值 2)、PROFILE(我的,值 3)。使用枚举而非魔法数字(magic number)来标识 Tab 页是编程最佳实践——它提供了语义化的名称,使得代码如 this.activeTab === KanbanTab.BOARDthis.activeTab === 0 具有更好的可读性和可维护性。枚举值从 0 开始递增,与数组索引的惯例一致,便于后续可能的扩展。

4.2 入口组件 KanbanApp 的状态声明

@Entry
@Component
struct KanbanApp {
  @State activeTab: KanbanTab = KanbanTab.BOARD

KanbanApp 是整个应用的入口组件,通过 @Entry 装饰器标记为页面入口,通过 @Component 装饰器声明为自定义组件。@State activeTab: KanbanTab = KanbanTab.BOARD 声明了一个响应式状态变量 activeTab,初始值为 KanbanTab.BOARD(即应用启动时默认显示看板视图)。

@State 是 ArkTS 状态管理体系中最基础、最核心的装饰器。被 @State 修饰的变量具有"响应式"特性——当该变量的值发生变化时,ArkUI 框架会自动重新调用 build() 方法中依赖该变量的 UI 部分,完成视图的增量更新。在本例中,activeTab 的变化会触发底部导航栏选中态的切换和内容区域的视图切换。这种"状态驱动视图"的声明式编程模型,使得开发者无需手动操作 DOM(在 ArkTS 中是组件树),只需修改状态数据,框架自动完成视图同步,极大地简化了 UI 编程的复杂度。

4.3 内容区域构建器 contentArea

@Builder contentArea() {
  Column() {
    if (this.activeTab === KanbanTab.BOARD) {
      BoardContent()
    } else if (this.activeTab === KanbanTab.LIST) {
      TaskListContent()
    } else if (this.activeTab === KanbanTab.STATS) {
      KanbanStatsContent()
    } else {
      KanbanProfileContent()
    }
  }
  .layoutWeight(1)
}

contentArea 是一个使用 @Builder 装饰器定义的构建器方法,它负责根据 activeTab 的当前值条件渲染对应的内容组件。@Builder 是 ArkTS 中实现 UI 片段复用的核心机制——它允许开发者将一段 UI 结构封装为可调用的方法,在 build() 方法或其他 @Builder 方法中通过 this.contentArea() 的方式引用。

这里采用 if-else if-else 的条件分支结构来选择渲染哪个子组件。当 activeTabBOARD 时渲染 BoardContent()(看板视图),为 LIST 时渲染 TaskListContent()(列表视图),为 STATS 时渲染 KanbanStatsContent()(统计视图),其余情况(即 PROFILE)渲染 KanbanProfileContent()(个人视图)。外层包裹的 Column().layoutWeight(1) 确保内容区域能够占据底部导航栏之上的全部剩余空间——layoutWeight(1) 表示在父容器(Column)中将剩余空间全部分配给该子组件,这是 ArkUI 中实现"弹性布局"的常用手法。

这种"入口组件根据状态条件渲染子组件"的模式,是 ArkTS 中实现多页面/多视图切换的标准做法。与传统的页面路由(Router)方式不同,这种条件渲染的方式不涉及页面的压栈/出栈,切换速度更快,适合底部 Tab 这种"平级视图"之间的切换场景。同时,由于子组件是独立声明的 @Component,它们各自拥有独立的状态空间和生命周期,互不干扰。

4.4 底部导航项构建器 tabItem

@Builder tabItem(icon: string, label: string, tab: KanbanTab, accent: string) {
  Column() {
    Text(icon).fontSize(20).opacity(this.activeTab === tab ? 1.0 : 0.4)
    Text(label).fontSize(10)
      .fontColor(this.activeTab === tab ? accent : '#999999')
      .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
      .margin({ top: 2 })
    if (this.activeTab === tab) {
      Column().width(18).height(3).backgroundColor(accent).borderRadius(2).margin({ top: 2 })
    }
  }
  .layoutWeight(1).alignItems(HorizontalAlign.Center)
  .padding({ top: 5, bottom: 5 })
  .onClick(() => { this.activeTab = tab })
}

tabItem 构建器是底部导航栏中单个 Tab 项的 UI 模板,它接收四个参数:icon(图标 Emoji)、label(文字标签)、tab(对应的枚举值)、accent(选中时的高亮主题色)。这个构建器充分展示了 ArkTS 中通过参数化 @Builder 实现 UI 复用的能力——四个 Tab 项共享同一个构建器模板,只需传入不同的参数即可生成不同外观的导航项。

构建器内部的 UI 逻辑非常精巧。图标 Text(icon) 的透明度根据选中状态在 1.0(完全不透明)和 0.4(半透明)之间切换,形成"选中高亮、未选中弱化"的视觉对比。文字标签同时调整了字体颜色(选中时使用 accent 主题色,未选中时使用灰色 #999999)和字重(选中时加粗 FontWeight.Bold,未选中时常规 FontWeight.Normal),从颜色和粗细两个维度强化选中态的辨识度。

最巧妙的设计在于底部的小指示条——当 this.activeTab === tabtrue 时,才会渲染一个宽 18、高 3 的圆角小条,背景色为 accent 主题色。这个指示条是当前选中 Tab 的视觉锚点,它随着 activeTab 的变化在不同 Tab 项下方"移动",为用户提供了清晰的导航位置反馈。这种"条件渲染指示条"的设计在很多主流 App 的底部导航栏中都能看到,是移动端 UI 设计的经典模式。

onClick(() => { this.activeTab = tab }) 是 Tab 切换的核心逻辑——点击任意 Tab 项时,将 activeTab 修改为对应的枚举值,触发状态更新和视图重渲染。由于 activeTab@State 修饰的,这一赋值操作会自动驱动 contentArea 中的条件分支重新求值、tabItem 中的选中态样式重新计算,整个 UI 切换过程由框架自动完成,开发者无需编写任何手动刷新代码。

4.5 入口组件 build 方法

build() {
  Column() {
    this.contentArea()
    Divider().color('#E0E0E0').strokeWidth(0.5)
    Row() {
      this.tabItem('📋', '看板', KanbanTab.BOARD, '#4527A0')
      this.tabItem('📝', '列表', KanbanTab.LIST, '#C2185B')
      this.tabItem('📊', '统计', KanbanTab.STATS, '#00838F')
      this.tabItem('👤', '我的', KanbanTab.PROFILE, '#1565C0')
    }
    .width('100%').backgroundColor('#FFFFFF').padding({ top: 4, bottom: 6 })
  }
  .width('100%').height('100%').backgroundColor('#F3E5F5')
}

build() 方法是每个 @Component 必须实现的核心方法,它定义了组件的 UI 结构。KanbanAppbuild() 方法构建了一个垂直布局的页面骨架:顶部是内容区域 this.contentArea()(占据弹性空间),中间是一条 0.5 像素宽的灰色分隔线 Divider,底部是水平排列的四个 Tab 导航项。

底部导航栏使用 Row 容器水平排列四个 tabItem,每个 Tab 项通过 layoutWeight(1) 平分宽度,确保四个 Tab 等宽分布。每个 Tab 的高亮色 accent 分别对应应用四色主题体系的一种主色——看板用深紫(#4527A0)、列表用玫红(#C2185B)、统计用青色(#00838F)、我的用蓝色(#1565C0),使得不同 Tab 在选中时呈现出不同的色彩个性,同时整体配色和谐统一。

整个页面的背景色设为 #F3E5F5(淡紫色),这是应用的主题背景色,为内容卡片提供了柔和的底色衬托。底部导航栏背景设为白色 #FFFFFF,与内容区域形成清晰的视觉边界分隔。这种"浅色背景 + 白色卡片/导航栏 + 阴影"的设计语言是 Material Design 风格的典型体现,在鸿蒙应用中也非常流行。


五、看板视图 BoardContent:多列横向滚动与模态弹窗

5.1 组件状态声明

@Component
struct BoardContent {
  @State showAddModal: boolean = false
  @State showDetailModal: boolean = false
  @State selectedTask: TaskItem | null = null
  @State formTitle: string = ''
  @State formPriority: string = '普通'
  @State formAssignee: string = '张三'
  @State formColumn: string = '待办'

BoardContent 是看板视图的核心组件,它声明了六个 @State 状态变量来管理组件内部的交互状态。这六个状态变量可以分为三组:

第一组是模态弹窗的显示控制:showAddModal 控制"新建任务"弹窗的显隐,showDetailModal 控制"任务详情"弹窗的显隐。这两个布尔变量是弹窗显示/隐藏的唯一数据源——当它们为 true 时对应的弹窗渲染,为 false 时弹窗移除。这种"布尔状态驱动弹窗显隐"的模式是 ArkTS 中实现模态交互的标准做法。

第二组是任务详情的数据载体:selectedTask 的类型是 TaskItem | null(联合类型,表示可以是 TaskItem 对象或 null),它存储当前被点击查看详情的任务对象。初始值为 null 表示没有选中任何任务。当用户点击某个任务卡片时,将该任务对象赋值给 selectedTask,详情弹窗中的所有信息(标题、描述、负责人、截止日期、故事点、标签等)都从 selectedTask 中读取渲染。

第三组是新建任务表单的受控状态:formTitle(任务标题输入值)、formPriority(优先级选择值,默认"普通")、formAssignee(负责人选择值,默认"张三")、formColumn(所在列选择值,默认"待办")。这些变量与表单控件双向绑定,用户在弹窗中的每次输入和选择都会实时更新对应的 @State 变量,而变量的变化又会驱动选中态样式的更新。

这种将表单状态集中声明在组件级别的做法,使得表单数据的管理清晰有序,也为后续的表单提交(将 formTitleformPriority 等组装成新的 TaskItem 对象)提供了便捷的数据来源。

5.2 模态背景构建器 modalBg

@Builder modalBg(onClose: () => void) {
  Column().width('100%').height('100%')
    .backgroundColor('rgba(0,0,0,0.5)').onClick(onClose)
}

modalBg 是一个高度复用的构建器,用于渲染模态弹窗的半透明遮罩层。它接收一个 onClose 回调函数作为参数,当用户点击遮罩区域时触发该回调(通常是关闭弹窗)。遮罩层是一个铺满父容器的 Column,背景色为 rgba(0,0,0,0.5)(半透明黑色),营造出弹窗弹出时背景变暗的"聚焦"效果。

这个构建器的设计体现了"关注点分离"的思想——将遮罩层的渲染逻辑抽取为独立的可复用构建器,使得"新建任务"弹窗、"任务详情"弹窗、"编辑任务"弹窗、"删除确认"弹窗都可以复用同一个遮罩层实现,避免了代码重复。参数化回调函数 onClose 的设计则使得每个弹窗可以传入自己的关闭逻辑(如 () => { this.showAddModal = false }() => { this.showDetailModal = false }),实现了"同一外观、不同行为"的复用模式。

5.3 新建任务弹窗 addTaskModal

@Builder addTaskModal() {
  Column() {
    this.modalBg(() => { this.showAddModal = false })
    Column() {
      Row() {
        Text('➕ 新建任务').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
        Row().layoutWeight(1)
        Text('✕').fontSize(20).fontColor('#999999').onClick(() => { this.showAddModal = false })
      }
      // ... 表单内容区域
      Row() {
        Text('取消').fontSize(14).fontColor('#888888')
          .backgroundColor('#F5F5F5').borderRadius(22)
          .padding({ left: 28, right: 28, top: 11, bottom: 11 })
          .onClick(() => { this.showAddModal = false })
        Text('创建任务').fontSize(14).fontColor('#FFFFFF')
          .backgroundColor('#4527A0').borderRadius(22)
          .padding({ left: 28, right: 28, top: 11, bottom: 11 }).margin({ left: 12 })
          .onClick(() => { this.showAddModal = false })
      }
      .width('100%').justifyContent(FlexAlign.Center)
    }
    .width('90%').backgroundColor('#FFFFFF').borderRadius(18)
    .position({ x: '5%', y: '15%' })
    .shadow({ radius: 20, color: '#33000000', offsetY: 4 })
  }
  .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}

addTaskModal 是新建任务弹窗的完整 UI 实现。弹窗的整体结构是一个全屏的 Column 容器(通过 positionzIndex(999) 确保覆盖在最顶层),内部先渲染半透明遮罩层 this.modalBg(...),再渲染弹窗主体。

弹窗主体是一个宽度 90%、白色背景、18 像素圆角的 Column,通过 position({ x: '5%', y: '15%' }) 定位在屏幕上方 15% 处(左右各留 5% 边距),并添加了 shadow 阴影效果(半径 20、颜色 #33000000、Y 轴偏移 4),营造出浮于内容之上的立体层次感。

弹窗的标题栏使用 Row 水平排列"➕ 新建任务"标题、弹性占位 Row().layoutWeight(1) 和关闭按钮"✕"。这里 Row().layoutWeight(1) 是一个常见的布局技巧——它作为一个不可见的弹性占位元素,将左右两端的元素推向两端,实现了"标题在左、关闭按钮在右"的经典弹窗标题栏布局。

表单区域包含了任务标题输入框(TextInput)、优先级选择按钮组(通过 ForEach 遍历 PRIORITY_LIST 渲染)和负责人选择按钮组(通过 ForEach 遍历 ASSIGNEE_LIST 渲染)。优先级选择按钮的样式根据 this.formPriority === p 的判断结果动态切换——选中时使用优先级对应的主题色作为背景、白色文字;未选中时使用浅灰背景、优先级主题色文字。这种"单选按钮组"的交互模式在移动端表单中非常常见,比传统的下拉选择器更加直观和易用。

底部的操作按钮栏使用 Row 水平排列"取消"和"创建任务"两个按钮,通过 justifyContent(FlexAlign.Center) 居中对齐。两个按钮都通过 onClick 回调将 showAddModal 设为 false 来关闭弹窗,体现了"取消"和"创建"两个操作都会关闭弹窗的交互逻辑(在真实应用中,"创建任务"按钮还应该在此处执行将表单数据组装为新任务并添加到任务列表的逻辑)。

5.4 任务详情弹窗 detailModal

@Builder detailModal() {
  Column() {
    this.modalBg(() => { this.showDetailModal = false })
    Column() {
      Row() {
        Column() {
          Text(PRIORITY_CONFIG[this.selectedTask?.priority ?? '普通']?.label ?? '').fontSize(12)
            .fontColor('#FFFFFF')
            .backgroundColor(PRIORITY_CONFIG[this.selectedTask?.priority ?? '普通']?.color ?? '#999999')
            .padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(10)
        }
        Column().layoutWeight(1)
        Text('✕').fontSize(20).fontColor('#999999').onClick(() => { this.showDetailModal = false })
      }
      // ... 标题、描述、信息行、标签行
      Row() {
        Text('✏️ 编辑').fontSize(13).fontColor('#FFFFFF')
          .backgroundColor('#FF8F00').borderRadius(18)
          .padding({ left: 20, right: 20, top: 9, bottom: 9 })
        Text('🗑️ 删除').fontSize(13).fontColor('#FFFFFF')
          .backgroundColor('#C2185B').borderRadius(18)
          .padding({ left: 20, right: 20, top: 9, bottom: 9 }).margin({ left: 8 })
      }
    }
    .width('92%').constraintSize({ maxHeight: '80%' }).backgroundColor('#FFFFFF').borderRadius(18)
    .position({ x: '4%', y: '10%' })
    .shadow({ radius: 20, color: '#33000000', offsetY: 4 })
  }
  .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}

detailModal 是任务详情弹窗的实现,它展示了一个任务的完整信息。这个弹窗的代码中有一个非常值得学习的技术细节——对 selectedTask 可能为 null 的安全处理。由于 selectedTask 的类型是 TaskItem | null,直接访问 this.selectedTask.priority 在 TypeScript 严格模式下会产生编译错误(因为 null 没有 priority 属性)。代码中采用了 this.selectedTask?.priority ?? '普通' 的空值合并模式:?. 是可选链操作符,当 selectedTasknull 时返回 undefined 而非抛出错误;?? 是空值合并操作符,当左侧为 nullundefined 时返回右侧的默认值 ‘普通’。这种"可选链 + 空值合并"的组合是 ArkTS/TypeScript 中处理可空对象属性访问的标准安全模式。

详情弹窗的信息展示区域采用了三列等宽的信息卡片布局,分别展示"负责人"(👤)、“截止日”(📅)和"故事点"(🎯),每个信息卡片包含图标、标签名和值三行内容。标签区域通过 ForEach 遍历 this.selectedTask?.tags ?? [] 渲染所有标签,以 #标签名 的格式展示在淡紫色背景的圆角小标签中。

弹窗底部提供了"编辑"(橙色 #FF8F00)和"删除"(玫红色 #C2185B)两个操作按钮,颜色与操作语义匹配——编辑用温和的橙色暗示可逆操作,删除用警示性的玫红色暗示不可逆操作。弹窗主体使用了 constraintSize({ maxHeight: '80%' }) 限制最大高度为屏幕的 80%,配合内部的 Scroll 滚动容器,确保即使任务描述很长,弹窗内容也能滚动查看而不会溢出屏幕。

5.5 任务卡片构建器 taskCard

@Builder taskCard(t: TaskItem) {
  Column() {
    Row() {
      Text(PRIORITY_CONFIG[t.priority]?.label ?? '').fontSize(9)
        .fontColor('#FFFFFF').backgroundColor(PRIORITY_CONFIG[t.priority]?.color ?? '#999999')
        .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(8)
      Row().layoutWeight(1)
      Text(t.storyPoints + 'pt').fontSize(9).fontColor('#888888')
    }
    .width('100%')
    Text(t.title).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
      .width('100%').margin({ top: 8 })
    Text(t.description).fontSize(10).fontColor('#888888')
      .width('100%').margin({ top: 4 }).maxLines(2)
      .textOverflow({ overflow: TextOverflow.Ellipsis })
    Row() {
      Text('👤' + t.assignee).fontSize(9).fontColor('#666666')
      Row().layoutWeight(1)
      Text('📅' + t.dueDate).fontSize(9).fontColor('#999999')
    }
    .width('100%').margin({ top: 8 })
    Row() {
      ForEach(t.tags.slice(0, 2), (tag: string) => {
        Text('#' + tag).fontSize(8).fontColor('#4527A0')
          .backgroundColor('#EDE7F6').padding({ left: 5, right: 5, top: 2, bottom: 2 })
          .borderRadius(6).margin({ right: 4 })
      })
    }
    .width('100%').margin({ top: 6 })
  }
  .width(180).padding(12).backgroundColor('#FFFFFF')
  .borderRadius(12).margin({ top: 6, bottom: 2 })
  .shadow({ radius: 3, color: '#15000000', offsetY: 2 })
  .onClick(() => { this.selectedTask = t; this.showDetailModal = true })
}

taskCard 构建器渲染单个任务卡片的完整 UI。卡片宽度固定为 180 像素,采用白色背景、12 像素圆角和轻微阴影,在列的淡色背景上呈现出"浮起"的卡片效果。卡片内部的信息布局自上而下依次为:优先级标签 + 故事点(同一行,优先级标签在左,故事点在右,中间用 layoutWeight(1) 占位推开)、任务标题(加粗)、任务描述(灰色小字,最多两行,超出部分省略号截断)、负责人 + 截止日期(同一行)、标签列表(最多显示前两个标签)。

这里有两个值得注意的技术细节。第一是 maxLines(2)textOverflow({ overflow: TextOverflow.Ellipsis }) 的配合使用——它们限制了描述文本最多显示两行,超出部分以省略号结尾。这是移动端列表/卡片中处理长文本的标准做法,确保卡片高度一致、布局整齐。第二是 t.tags.slice(0, 2) 的使用——通过数组的 slice 方法只取前两个标签渲染,避免标签过多导致卡片高度膨胀。

卡片的 onClick 事件处理器是详情弹窗的触发入口——点击任意卡片时,将该任务对象 t 赋值给 selectedTask,同时将 showDetailModal 设为 true,详情弹窗随即渲染并展示该任务的完整信息。这种"点击 → 设置选中数据 → 显示弹窗"的三步模式是列表项点击查看详情的经典交互流程。

5.6 看板列构建器 columnBlock

@Builder columnBlock(colName: string, tasks: TaskItem[]) {
  Column() {
    Row() {
      Text(BOARD_COLUMNS[colName]?.icon ?? '📋').fontSize(14)
      Text(colName).fontSize(13).fontWeight(FontWeight.Bold)
        .fontColor(BOARD_COLUMNS[colName]?.color ?? '#4527A0').margin({ left: 4 })
      Column() {
        Text(tasks.length.toString()).fontSize(11).fontColor('#FFFFFF')
      }
      .height(20).backgroundColor(BOARD_COLUMNS[colName]?.color ?? '#4527A0')
      .borderRadius(10).padding({ left: 8, right: 8 }).margin({ left: 6 })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
    }
    .width('100%')
    ForEach(tasks, (t: TaskItem) => {
      this.taskCard(t)
    })
  }
  .width(200).padding({ left: 8, right: 8, top: 10, bottom: 10 })
  .backgroundColor(BOARD_COLUMNS[colName]?.bg ?? '#F5F5F5')
  .borderRadius(14).margin({ left: 6, right: 6 })
}

columnBlock 构建器渲染看板中的单个列。它接收两个参数:colName(列名)和 tasks(该列下的任务数组)。列的宽度固定为 200 像素,背景色从 BOARD_COLUMNS 配置表中读取对应的淡色背景,圆角 14 像素,左右各有 6 像素的外边距,使相邻列之间保持间距。

列头部使用 Row 水平排列四个元素:列图标(Emoji)、列名称(加粗,颜色为列主题色)、弹性占位、任务计数徽章。任务计数徽章是一个高度 20 像素的圆角胶囊形 Column,背景色为列主题色,内部白色文字显示该列的任务数量 tasks.length。这个徽章是看板中非常重要的信息密度元素——它让团队成员一眼就能知道每列有多少任务,快速识别工作瓶颈。

列的主体部分通过 ForEach(tasks, (t: TaskItem) => { this.taskCard(t) }) 遍历传入的任务数组,为每个任务渲染一个 taskCard 卡片。ForEach 是 ArkTS 中实现列表渲染的核心组件——它接收一个数据数组和一个子项生成函数,自动为每个数据项生成对应的 UI 组件。当数据数组发生变化时(如新增、删除、排序),ForEach 会自动进行差异更新(Diff),只更新变化的部分,保证渲染性能。

5.7 看板视图 build 方法

build() {
  Stack() {
    Column() {
      Row() {
        Column() {
          Text('📋 项目看板').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#4527A0')
          Text(getTaskTotal() + '个任务 · ' + getCompletedTotal() + '已完成 · ' + getOverdueCount() + '逾期')
            .fontSize(11).fontColor('#888888').margin({ top: 2 })
        }
        Row().layoutWeight(1)
        Text('+').fontSize(24).fontColor('#FFFFFF')
          .backgroundColor('#4527A0').width(38).height(38).borderRadius(19)
          .textAlign(TextAlign.Center)
          .onClick(() => { this.showAddModal = true })
      }
      .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
      Scroll() {
        Row() {
          this.columnBlock('待办', mockTasks.filter((t: TaskItem): boolean => { return t.column === '待办' }))
          this.columnBlock('进行中', mockTasks.filter((t: TaskItem): boolean => { return t.column === '进行中' }))
          this.columnBlock('审核中', mockTasks.filter((t: TaskItem): boolean => { return t.column === '审核中' }))
          this.columnBlock('已完成', mockTasks.filter((t: TaskItem): boolean => { return t.column === '已完成' }))
        }
        .padding({ left: 6, right: 6, top: 6, bottom: 20 })
      }
      .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
      .layoutWeight(1)
    }
    .width('100%').height('100%')

    if (this.showAddModal) { this.addTaskModal() }
    if (this.showDetailModal) { this.detailModal() }
  }
  .width('100%').height('100%')
}

BoardContentbuild() 方法是整个看板视图的总装配。外层使用 Stack 容器——Stack 是 ArkUI 中的层叠布局容器,它的子元素会依次堆叠在一起,后声明的子元素覆盖在先声明的子元素之上。这里将看板主内容放在底层,将模态弹窗放在上层,实现了弹窗覆盖在内容之上的视觉效果。

看板主内容的顶部是标题栏,使用 Row 布局:左侧是"📋 项目看板"标题和任务统计摘要(总任务数、已完成数、逾期数,通过字符串拼接动态生成),右侧是一个圆形的"+"新建按钮(38x38 像素,深紫色背景,白色加号),点击后触发 showAddModal = true 打开新建任务弹窗。

主体部分是横向滚动的看板区域,使用 Scroll 容器包裹一个 RowRow 内水平排列四个 columnBlockscrollable(ScrollDirection.Horizontal) 设置滚动方向为水平,scrollBar(BarState.Off) 隐藏滚动条以保持界面整洁。每个列通过 mockTasks.filter(t => t.column === '待办') 这样的数组过滤操作从全部任务中筛选出属于该列的任务,实现了"数据分组"的逻辑——同一份任务数据按 column 字段的不同值被分发到四个列中渲染。

最关键的是 Stack 底部的两个条件渲染语句:if (this.showAddModal) { this.addTaskModal() }if (this.showDetailModal) { this.detailModal() }。当 showAddModalshowDetailModaltrue 时,对应的弹窗构建器会被调用并渲染在 Stack 的最上层,覆盖在看板内容之上。由于弹窗构建器内部设置了 zIndex(999),它们会确保显示在所有内容之上。这种"Stack 层叠 + 条件渲染 + zIndex 控制"的模式,是 ArkTS 中实现自定义模态弹窗的核心技术方案,相比系统提供的 bindSheetCustomDialog,它提供了更高的样式自定义自由度。


六、列表视图 TaskListContent:垂直列表与编辑删除交互

6.1 组件状态声明

@Component
struct TaskListContent {
  @State showDeleteConfirm: boolean = false
  @State showEditModal: boolean = false
  @State selectedTask: TaskItem | null = null
  @State editTitle: string = ''

TaskListContent 是任务列表视图的组件,它的状态声明与 BoardContent 有相似的结构,但也有关键的区别。该组件声明了四个 @State 变量:showDeleteConfirm 控制删除确认弹窗的显隐,showEditModal 控制编辑任务弹窗的显隐,selectedTask 存储当前被操作的任务对象(用于编辑和删除场景),editTitle 存储编辑弹窗中任务标题输入框的值。

BoardContent 相比,列表视图的交互焦点从"查看详情"转移到了"编辑"和"删除"两个操作上。这反映了看板视图和列表视图在交互设计上的差异化定位——看板视图侧重于任务的全局概览和状态流转,而列表视图侧重于对单个任务的直接操作(编辑、删除)。同一个 selectedTask 状态变量在两个组件中承担了不同的角色:在看板视图中它是详情弹窗的数据源,在列表视图中它是编辑/删除操作的目标对象。这种"状态变量复用、语义差异化"的设计体现了 ArkTS 组件化开发中状态管理的灵活性。

6.2 编辑任务弹窗 editModal

@Builder editModal() {
  Column() {
    this.modalBg(() => { this.showEditModal = false })
    Column() {
      Row() {
        Text('✏️ 编辑任务').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
        Row().layoutWeight(1)
        Text('✕').fontSize(20).fontColor('#999999').onClick(() => { this.showEditModal = false })
      }
      // ... 表单内容
      Column() {
        Text('任务标题').fontSize(12).fontColor('#888888').margin({ top: 14, left: 20 })
        TextInput({ placeholder: this.selectedTask?.title ?? '' })
          .placeholderColor('#BBBBBB').fontSize(14)
          .backgroundColor('#F5F0F7').borderRadius(10)
          .margin({ left: 20, right: 20, top: 4 })
          .onChange((v: string) => { this.editTitle = v })
        Text('描述').fontSize(12).fontColor('#888888').margin({ top: 14, left: 20 })
        TextArea({ placeholder: this.selectedTask?.description ?? '' })
          .placeholderColor('#BBBBBB').fontSize(13).height(60)
          .backgroundColor('#F5F0F7').borderRadius(10)
          .margin({ left: 20, right: 20, top: 4 })
      }
      // ... 底部按钮
    }
  }
}

editModal 是编辑任务弹窗的实现,它与新建任务弹窗在结构上高度相似,但有两个关键差异。第一个差异是表单的初始值——编辑弹窗中 TextInputplaceholder 直接使用了 this.selectedTask?.title ?? ''(即当前选中任务的标题),这样用户打开编辑弹窗时就能看到任务的原标题作为占位提示。在真实应用中,这里应该使用 TextInput({ text: this.selectedTask?.title ?? '' }) 将原标题作为初始值填入输入框,而非仅作为 placeholder。

第二个差异是引入了 TextArea 组件用于多行文本输入。TextAreaTextInput 的区别在于它支持多行文本输入和显示,适合编辑较长的任务描述。这里设置了 height(60) 固定高度,确保描述输入区域有足够的编辑空间。onChange((v: string) => { this.editTitle = v }) 回调将用户输入的值实时同步到 editTitle 状态变量,实现了受控组件的数据流。

编辑弹窗底部的"保存"按钮使用玫红色(#C2185B)背景,与列表视图的主题色保持一致,而新建任务弹窗的"创建任务"按钮使用深紫色(#4527A0),与看板视图的主题色一致。这种"按钮颜色跟随当前 Tab 主题色"的设计细节,增强了视图间的视觉差异化。

6.3 删除确认弹窗 deleteModal

@Builder deleteModal() {
  Column() {
    this.modalBg(() => { this.showDeleteConfirm = false })
    Column() {
      Text('⚠️').fontSize(48).margin({ top: 24 })
      Text('确认删除此任务?').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
      Row() {
        Text(PRIORITY_CONFIG[this.selectedTask?.priority ?? '普通']?.label ?? '').fontSize(11)
          .fontColor('#FFFFFF').backgroundColor(PRIORITY_CONFIG[this.selectedTask?.priority ?? '普通']?.color ?? '#999999')
          .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
        Text(this.selectedTask?.title ?? '').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 8 })
      }
      .backgroundColor('#FFF5F5').borderRadius(12)
      .padding({ left: 16, right: 16, top: 12, bottom: 12 }).margin({ top: 16 })
      Row() {
        Text('取消').fontSize(14).fontColor('#888888')
          .backgroundColor('#F5F5F5').borderRadius(22)
          .onClick(() => { this.showDeleteConfirm = false })
        Text('确认删除').fontSize(14).fontColor('#FFFFFF')
          .backgroundColor('#C2185B').borderRadius(22)
          .onClick(() => { this.showDeleteConfirm = false })
      }
    }
    .width('80%').backgroundColor('#FFFFFF').borderRadius(18)
    .position({ x: '10%', y: '38%' })
  }
}

deleteModal 是删除确认弹窗的实现,它是一个典型的"危险操作二次确认"对话框。弹窗的设计遵循了移动端确认对话框的最佳实践:顶部用大号警告图标"⚠️"(48 像素)引起用户注意,紧随其后的是确认问题"确认删除此任务?",然后在一个淡红色背景(#FFF5F5)的信息卡片中展示即将删除的任务的优先级标签和标题,让用户清楚地知道将要删除的是哪条任务。

底部的操作按钮中,"取消"使用灰色背景表示安全退回,"确认删除"使用玫红色(#C2185B)背景强调操作的不可逆性。这种"中性取消 + 警示确认"的按钮配色方案是危险操作确认弹窗的标准设计语言,能够有效降低用户误操作的风险。

弹窗的定位使用了 position({ x: '10%', y: '38%' }),使其在屏幕垂直方向上居中偏下,宽度为 80%(左右各留 10% 边距)。与新建/编辑弹窗(宽度 90%,定位在上方 15%-18%)相比,删除确认弹窗更窄、更居中,这种视觉上的差异帮助用户在潜意识中区分"表单输入弹窗"和"确认对话框"两种不同类型的交互。

6.4 列表项构建器 listItem

@Builder listItem(t: TaskItem) {
  Row() {
    Column() {
      Text(BOARD_COLUMNS[t.column]?.icon ?? '📋').fontSize(16)
    }
    .width(38).height(38)
    .backgroundColor(BOARD_COLUMNS[t.column]?.bg ?? '#F5F5F5')
    .borderRadius(10)
    .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
    Column() {
      Row() {
        Text(t.title).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#212121')
        Column() {
          Text(t.storyPoints + 'pt').fontSize(9).fontColor('#888888').margin({ left: 6 })
        }
      }
      Text(t.description).fontSize(10).fontColor('#999999').margin({ top: 2 })
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
      Row() {
        Text(t.assignee).fontSize(9).fontColor('#666666')
        Text('·').fontSize(9).fontColor('#CCCCCC').margin({ left: 4 })
        Text(t.dueDate).fontSize(9).fontColor('#F44336')
      }
      .margin({ top: 3 })
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
    Column() {
      Text('✏️').fontSize(14).fontColor('#C2185B')
        .onClick(() => { this.selectedTask = t; this.showEditModal = true })
      Text('🗑️').fontSize(14).fontColor('#F44336').margin({ top: 8 })
        .onClick(() => { this.selectedTask = t; this.showDeleteConfirm = true })
    }
    .alignItems(HorizontalAlign.End)
  }
  .width('100%').padding(12).backgroundColor('#FFFFFF')
  .borderRadius(12).margin({ left: 12, right: 12, top: 5 })
  .shadow({ radius: 2, color: '#10000000', offsetY: 1 })
}

listItem 构建器渲染列表视图中单个任务项的完整 UI。列表项采用三列水平布局:左侧是 38x38 像素的状态图标方块(背景色为该任务所属列的淡色背景,内部显示列图标 Emoji),中间是任务信息区域(占据弹性宽度),右侧是操作按钮区域(编辑和删除图标垂直排列)。

中间的信息区域包含了三行内容:第一行是任务标题(中等字重)和故事点(灰色小字,通过 margin({ left: 6 }) 与标题保持间距);第二行是任务描述(灰色小字,单行显示,超出省略号截断);第三行是负责人、分隔点和截止日期(红色 #F44336,暗示时间敏感性)。这里截止日期使用红色字体是一个细节设计——它提醒用户关注任务的时间节点,红色在视觉上传递了"时间紧迫"的信号。

右侧的操作区域包含两个图标按钮:编辑图标"✏️"(玫红色)和删除图标"🗑️"(红色),垂直排列,间距 8 像素。每个图标按钮的 onClick 事件都会先将当前任务 t 赋值给 selectedTask,再触发对应的弹窗显示状态。这种"同一列表项包含多个操作入口"的设计是列表视图的核心交互特征——用户无需进入详情页即可直接对任务进行编辑或删除操作,提高了操作效率。

与看板卡片相比,列表项的信息密度更高、布局更紧凑。看板卡片宽 180 像素、纵向排列信息、最多显示两行描述和两个标签;列表项占满屏幕宽度、横向排列信息、描述只显示一行、不显示标签。这种差异化的信息呈现策略适应了不同视图的使用场景:看板视图强调"全局概览、列间对比",列表视图强调"逐条浏览、快速操作"。

6.5 列表视图 build 方法

build() {
  Stack() {
    Column() {
      Text('📝 全部任务').fontSize(18).fontWeight(FontWeight.Bold)
        .width('100%').padding({ left: 16, top: 14, bottom: 8 })
      Scroll() {
        Column() {
          this.listItem(mockTasks[0])
          this.listItem(mockTasks[1])
          // ... 共25条
          this.listItem(mockTasks[24])
        }
        .padding({ bottom: 20 })
      }
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
    if (this.showEditModal) { this.editModal() }
    if (this.showDeleteConfirm) { this.deleteModal() }
  }
  .width('100%').height('100%')
}

TaskListContentbuild() 方法与 BoardContent 的结构类似,都采用了 Stack 层叠布局来管理主内容和模态弹窗。顶部是"📝 全部任务"标题,主体是一个垂直滚动的 Scroll 容器,内部通过 Column 垂直排列 25 个 listItem

这里有一个值得讨论的技术细节——列表项的渲染方式。在当前实现中,25 个列表项是通过 this.listItem(mockTasks[0])this.listItem(mockTasks[24]) 逐条手动调用的,而非使用 ForEach(mockTasks, (t) => { this.listItem(t) }) 自动遍历。这种手动展开的方式在数据量较小时可以正常工作,但当数据量增大时存在明显的扩展性问题——如果任务数量从 25 增加到 100,开发者需要手动添加 75 行代码。在真实应用中,应该使用 ForEachLazyForEach 来实现列表的动态渲染。ForEach 适用于数据量较小的场景,它会一次性渲染所有列表项;LazyForEach 适用于大数据量场景,它采用懒加载策略,只渲染可视区域内的列表项,大大提升了滚动性能和内存效率。

Stack 底部的条件渲染与看板视图相同——if (this.showEditModal) { this.editModal() }if (this.showDeleteConfirm) { this.deleteModal() } 分别控制编辑弹窗和删除确认弹窗的显示。这两个弹窗共享同一个 selectedTask 状态变量作为数据源,确保无论用户点击哪个列表项的编辑或删除按钮,弹窗中显示的都是对应任务的信息。


七、统计视图 KanbanStatsContent:数据可视化与图表渲染

7.1 统计视图 build 方法

@Component
struct KanbanStatsContent {
  build() {
    Column() {
      Text('📊 项目统计').fontSize(18).fontWeight(FontWeight.Bold)
        .width('100%').padding({ left: 16, top: 14, bottom: 8 })
      Scroll() {
        Column() {
          // ... 统计卡片区域
        }
        .padding({ bottom: 20 })
      }
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

KanbanStatsContent 是统计视图组件,值得注意的是它是四个内容组件中唯一没有声明任何 @State 状态变量的组件——因为统计视图是一个纯展示型页面,不涉及用户交互状态的管理。整个页面的数据全部来自全局的辅助函数(如 getTaskTotal()getCompletedTotal() 等)和 mockMembers 数组,通过纯函数式的方式计算并渲染。这种"无状态展示组件"的设计使得组件的行为完全可预测——给定相同的输入数据,渲染结果永远一致,不存在因状态变化导致的副作用。

7.2 顶部数据概览卡片

Row() {
  Column() {
    Text(getTaskTotal().toString()).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#4527A0')
    Text('总任务').fontSize(10).fontColor('#888888')
  }.layoutWeight(1).alignItems(HorizontalAlign.Center)
  Column() {
    Text(getInProgressTotal().toString()).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#00838F')
    Text('进行中').fontSize(10).fontColor('#888888')
  }.layoutWeight(1).alignItems(HorizontalAlign.Center)
  Column() {
    Text(getCompletedTotal().toString()).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
    Text('已完成').fontSize(10).fontColor('#888888')
  }.layoutWeight(1).alignItems(HorizontalAlign.Center)
  Column() {
    Text(getTotalStoryPoints().toString()).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#C2185B')
    Text('总故事点').fontSize(10).fontColor('#888888')
  }.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(16)
.margin({ left: 12, right: 12 }).padding({ top: 16, bottom: 16 })
.shadow({ radius: 4, color: '#10000000', offsetY: 2 })

顶部数据概览卡片是一个四列等宽的数据指标展示区域,使用 Row 水平排列四个 Column,每个 Column 通过 layoutWeight(1) 平分宽度、alignItems(HorizontalAlign.Center) 内容居中对齐。四个数据指标分别是:总任务数(25,深紫色)、进行中任务数(6,青色)、已完成任务数(5,绿色)和总故事点(138,玫红色),每个数值使用 24 像素加粗大字体突出显示,下方配以 10 像素的灰色标签说明。

四个指标的颜色分别对应应用四色主题体系,与看板列配置和 Tab 导航的主题色保持一致,形成了全局统一的色彩语义系统。这种"数字 + 标签 + 主题色"的数据卡片是仪表盘(Dashboard)设计中最经典的信息展示模式,它用最小的视觉空间传递了最核心的项目指标。

整个卡片使用白色背景、16 像素圆角和轻微阴影,浮于淡紫色页面背景之上,营造出现代仪表盘的卡片式视觉风格。左右各 12 像素的 margin 确保卡片与屏幕边缘保持安全间距。

7.3 任务分布条形图

Column() {
  Text('📊 任务分布').fontSize(13).fontWeight(FontWeight.Bold)
    .width('100%').padding({ left: 16, top: 14, bottom: 8 })
  Column() {
    Row() { Text('📋 待办').fontSize(11).fontColor('#4527A0').layoutWeight(1); Text('12项').fontSize(11).fontColor('#888888') }
    Row() { Column().width('48%').height(7).backgroundColor('#4527A0').borderRadius(4); Row().layoutWeight(1) }
    .width('100%').margin({ top: 4, bottom: 10 })
    Row() { Text('🔄 进行中').fontSize(11).fontColor('#00838F').layoutWeight(1); Text('6项').fontSize(11).fontColor('#888888') }
    Row() { Column().width('24%').height(7).backgroundColor('#00838F').borderRadius(4); Row().layoutWeight(1) }
    .width('100%').margin({ top: 4, bottom: 10 })
    Row() { Text('👀 审核中').fontSize(11).fontColor('#FF8F00').layoutWeight(1); Text('3项').fontSize(11).fontColor('#888888') }
    Row() { Column().width('12%').height(7).backgroundColor('#FF8F00').borderRadius(4); Row().layoutWeight(1) }
    .width('100%').margin({ top: 4, bottom: 10 })
    Row() { Text('✅ 已完成').fontSize(11).fontColor('#2E7D32').layoutWeight(1); Text('4项').fontSize(11).fontColor('#888888') }
    Row() { Column().width('16%').height(7).backgroundColor('#2E7D32').borderRadius(4); Row().layoutWeight(1) }
    .width('100%').margin({ top: 4 })
  }
  .padding({ left: 16, right: 16, bottom: 14 })
}

任务分布条形图是统计视图中最具技术含量的可视化组件。它使用纯 ArkTS 声明式 UI 实现了一个水平条形图,无需引入任何第三方图表库。每个条形由两行组成:第一行是标签行(列名 + 数量),使用 Row 水平排列,列名通过 layoutWeight(1) 左对齐,数量右对齐;第二行是进度条行,使用 Row 包裹一个有颜色的 Column(进度条本体)和一个 Row().layoutWeight(1)(剩余空间占位),实现进度条的"左对齐填充"效果。

进度条的宽度通过百分比字符串控制,如 width('48%') 表示该进度条占容器宽度的 48%。四个列的进度条宽度分别为 48%(待办,12项)、24%(进行中,6项)、12%(审核中,3项)和 16%(已完成,4项),这些百分比近似反映了各列任务数占总任务数的比例(12/25≈48%、6/25≈24%、3/25≈12%、4/25≈16%)。进度条高度统一为 7 像素,使用 borderRadius(4) 圆角,背景色为对应列的主题色,形成了与全局配色一致的视觉体系。

这种"用布局组件模拟图表"的技术方案在轻量级应用中非常实用。相比引入图表库(如 MpChart),纯 UI 组件实现的优势在于:包体积更小、渲染性能更好、样式自定义更灵活、与 ArkTS 声明式编程范式完全融合。缺点是只适用于简单的图表类型(如条形图、进度条),对于折线图、饼图、散点图等复杂图表仍需要使用 Canvas 绘制或第三方图表库。

7.4 成员工作负载图表

Column() {
  Text('👥 成员工作负载').fontSize(13).fontWeight(FontWeight.Bold)
    .width('100%').padding({ left: 16, top: 14, bottom: 8 })
  ForEach(mockMembers, (m: MemberInfo) => {
    Row() {
      Text(m.emoji).fontSize(24)
      Column() {
        Text(m.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
        Text(m.role).fontSize(10).fontColor('#888888').margin({ top: 2 })
        Row() {
          Column()
            .width((m.completedCount / m.taskCount * 120).toFixed(0) + 'vp')
            .height(6).backgroundColor(m.color).borderRadius(3)
          Row().layoutWeight(1)
        }
        .width('100%').height(6).backgroundColor('#F0F0F0').borderRadius(3).margin({ top: 4 })
      }
      .layoutWeight(1).padding({ left: 8 })
      Text(m.completedCount + '/' + m.taskCount).fontSize(12).fontColor('#888888')
    }
    .width('100%').padding({ top: 10, bottom: 10 })
    Divider().color('#F0F0F0')
  })
}

成员工作负载图表通过 ForEach 遍历 mockMembers 数组,为每个成员渲染一行工作负载信息。每行的布局是:左侧 Emoji 头像(24 像素)、中间成员信息区域(姓名 + 角色 + 完成进度条)、右侧任务完成比率文本(如"4/8")。

这个图表中最值得关注的技术细节是进度条宽度的动态计算:.width((m.completedCount / m.taskCount * 120).toFixed(0) + 'vp')。这行代码通过成员的 completedCount(已完成数)除以 taskCount(任务总数)得到完成率(0-1 之间的小数),乘以 120 得到以 vp(虚拟像素)为单位的进度条最大宽度,再通过 toFixed(0) 将结果四舍五入为整数,最后拼接 ‘vp’ 单位字符串。例如,张三的完成率为 4/8=0.5,进度条宽度为 0.5120=60vp;赵六的完成率为 1/5=0.2,进度条宽度为 0.2120=24vp。

进度条采用"双层叠加"的视觉效果——外层 Row 设置了灰色背景(#F0F0F0)作为进度槽,内层 Column 设置了成员主题色背景作为进度填充,两者通过相同的 height(6)borderRadius(3) 保持视觉对齐。内层进度条的宽度小于或等于外层进度槽的宽度,形成"部分填充"的进度条效果。这种"灰色底槽 + 彩色填充"的双层进度条是 UI 设计中最经典的进度可视化模式。

每个成员行之间使用 Divider 分隔线隔开,使列表结构清晰有序。Divider 是 ArkUI 提供的轻量级分隔线组件,默认为全宽 1 像素灰色线条,这里通过 .color('#F0F0F0') 自定义了颜色。


八、个人视图 KanbanProfileContent:项目中心与设置入口

8.1 个人视图 build 方法

@Component
struct KanbanProfileContent {
  build() {
    Column() {
      // ... 项目信息卡片
      // ... 项目设置列表
      Text('v2.0 · 项目看板 · 2026').fontSize(10).fontColor('#CCCCCC')
        .margin({ top: 16, bottom: 16 })
    }
    .width('100%').height('100%')
  }
}

KanbanProfileContent 是个人/项目中心视图组件,与统计视图一样,它也是一个无状态的纯展示组件。该视图的主要功能是展示当前项目的基本信息和提供项目设置的入口列表。

8.2 项目信息卡片

Column() {
  Row() {
    Column() {
      Text('🚀').fontSize(40)
    }
    .width(64).height(64).backgroundColor('#EDE7F6').borderRadius(32)
    .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
    Column() {
      Text('V2.0 发布项目').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
      Text('冲刺Sprint 3 · 第2周').fontSize(11).fontColor('#888888').margin({ top: 3 })
      Text(getCompletedTotal() + '/' + getTaskTotal() + ' 任务已完成').fontSize(10).fontColor('#4527A0').margin({ top: 2 })
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
  }
  .width('100%').padding(16)
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(16)
.margin({ left: 12, right: 12, top: 10 })
.shadow({ radius: 4, color: '#10000000', offsetY: 2 })

项目信息卡片采用水平布局:左侧是一个 64x64 像素的圆形头像区域(淡紫色背景 #EDE7F6,内部居中显示火箭 Emoji “🚀”,暗示项目的"发射/推进"意象),右侧是项目信息文本区域(占据弹性宽度,左对齐排列)。

右侧信息区域包含三行文本:项目名称"V2.0 发布项目"(18 像素加粗)、Sprint 冲刺信息"冲刺Sprint 3 · 第2周"(11 像素灰色)、任务完成进度"5/25 任务已完成"(10 像素深紫色)。其中任务完成进度通过 getCompletedTotal() + '/' + getTaskTotal() 动态拼接,将已完成数和总数以"分子/分母"的格式展示,直观地传达了项目的整体完成情况。深紫色的字体颜色与项目主题色呼应,在灰色信息行中形成视觉焦点。

8.3 项目设置列表

Column() {
  Text('⚙️ 项目设置').fontSize(13).fontWeight(FontWeight.Bold)
    .width('100%').padding({ left: 16, top: 14, bottom: 8 })
  Column() {
    Row() { Text('👥').fontSize(18); Text('成员管理').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('›').fontColor('#CCCCCC') }
    .width('100%').padding({ top: 12, bottom: 12, left: 4 })
    Divider().color('#F0F0F0')
    Row() { Text('🏷️').fontSize(18); Text('标签管理').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('›').fontColor('#CCCCCC') }
    .width('100%').padding({ top: 12, bottom: 12, left: 4 })
    Divider().color('#F0F0F0')
    Row() { Text('🔄').fontSize(18); Text('Sprint设置').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('›').fontColor('#CCCCCC') }
    .width('100%').padding({ top: 12, bottom: 12, left: 4 })
    Divider().color('#F0F0F0')
    Row() { Text('🔗').fontSize(18); Text('Webhook集成').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('›').fontColor('#CCCCCC') }
    .width('100%').padding({ top: 12, bottom: 12, left: 4 })
  }
  .padding({ left: 16, right: 16 })
}

项目设置列表是一个典型的"设置项列表"UI 模式,包含四个设置入口:成员管理(👥)、标签管理(🏷️)、Sprint 设置(🔄)和 Webhook 集成(🔗)。每个设置项使用 Row 水平排列三个元素:左侧 Emoji 图标(18 像素)、中间设置项名称(通过 layoutWeight(1) 占据弹性宽度并左对齐)、右侧箭头符号"›“(灰色 #CCCCCC,表示"可点击进入下一级”)。

设置项之间使用 Divider 分隔线隔开,形成清晰的列表分组视觉效果。这种"图标 + 名称 + 箭头"的设置项布局是 iOS/Android 设置页面的标准设计模式,在鸿蒙应用中同样适用。每个设置项的 padding({ top: 12, bottom: 12 }) 确保了足够的点击热区(约 44 像素高度),符合移动端触控交互的可用性 guidelines。

页面底部是一个版本信息文本"v2.0 · 项目看板 · 2026"(10 像素,浅灰色 #CCCCCC),居中显示在设置列表下方,提供了应用的版本和版权信息。这种在页面底部放置版本信息的做法是应用"关于"页面的常见设计。


九、总结:各模块关键技术点横向对比

通过对本应用四大功能模块(看板视图、列表视图、统计视图、个人视图)的逐段深度解析,我们可以清晰地看到 ArkTS 声明式 UI 编程在不同业务场景下的技术运用差异。以下表格从模块名称、核心功能、状态管理要点、关键组件、设计模式等维度对各模块进行了横向对比总结:

对比维度 看板视图(BoardContent) 列表视图(TaskListContent) 统计视图(KanbanStatsContent) 个人视图(KanbanProfileContent)
模块名称 BoardContent TaskListContent KanbanStatsContent KanbanProfileContent
核心功能 多列看板横向滚动展示、新建任务、查看任务详情 全部任务垂直列表浏览、编辑任务、删除任务确认 项目数据概览、任务分布条形图、成员工作负载可视化 项目基本信息展示、设置入口列表、版本信息
状态管理要点 6个@State变量:showAddModal、showDetailModal、selectedTask(TaskItem|null)、formTitle、formPriority、formAssignee、formColumn。涵盖弹窗控制、详情数据载体、表单受控状态三组 4个@State变量:showDeleteConfirm、showEditModal、selectedTask(TaskItem|null)、editTitle。聚焦编辑/删除两个操作场景 无@State变量。纯展示型组件,数据全部来自全局函数和mockMembers数组,无用户交互状态 无@State变量。纯展示型组件,数据来自全局函数,仅提供静态信息展示和设置入口
关键组件 Stack层叠布局、Scroll横向滚动、ForEach列表渲染、TextInput文本输入、Flex换行布局、position绝对定位、zIndex层级控制 Stack层叠布局、Scroll纵向滚动、TextInput/TextArea表单输入、ForEach按钮组渲染、Divider分隔线 Row/Column嵌套布局、ForEach成员遍历、纯UI条形图(Column宽度百分比)、Divider分隔线、Shadow阴影 Row/Column嵌套布局、Divider分隔线、圆形头像区域(borderRadius=width/2)、layoutWeight弹性占位
设计模式 Stack+条件渲染+zIndex自定义模态弹窗模式;@Builder参数化复用(modalBg、taskCard、columnBlock);配置表驱动样式(BOARD_COLORS);数据filter分组渲染 Stack+条件渲染自定义弹窗模式;@Builder复用(modalBg、listItem);危险操作二次确认对话框模式;受控表单组件模式 无状态展示组件模式;纯UI图表模拟模式(用布局组件代替图表库);双层进度条模式(灰色底槽+彩色填充);函数式数据计算 无状态展示组件模式;设置项列表模式(图标+名称+箭头);卡片式信息展示模式;动态数据拼接(函数调用+字符串拼接)
弹窗类型 新建任务弹窗(表单输入)、任务详情弹窗(信息展示+操作按钮) 编辑任务弹窗(表单输入)、删除确认弹窗(危险操作二次确认) 无弹窗 无弹窗
数据来源 mockTasks数组 + filter分组 + 全局函数(getTaskTotal等) mockTasks数组(逐条索引访问) 全局函数(getTaskTotal、getCompletedTotal等) + mockMembers数组 全局函数(getCompletedTotal、getTaskTotal) + 静态文本
滚动方向 水平滚动(ScrollDirection.Horizontal) 垂直滚动(默认方向) 垂直滚动 无滚动(内容固定)
主题色 深紫 #4527A0 玫红 #C2185B 青色 #00838F 蓝色 #1565C0
交互复杂度 高(多弹窗+表单+卡片点击+横向滚动) 中高(多弹窗+表单+列表项多操作入口) 低(纯展示,无交互) 低(纯展示,设置项无实际跳转)

安装DevEco Studio程序

在这里插入图片描述
选择目标安装目录:

在这里插入图片描述
设置环境变量,但是需要重启一下:

在这里插入图片描述
新建一个空白模板:

在这里插入图片描述
设置API为24的模板项目:
在这里插入图片描述
初始化项目,自动下载相关依赖:

在这里插入图片描述


完整代码:

// 123.ets - 项目看板 Kanban Board
// Deep Purple (#4527A0) | Pink (#C2185B) | Teal (#00838F) | Bg (#F3E5F5)

// ==================== Interfaces ====================
interface ColumnMeta {
  label: string
  icon: string
  color: string
  bg: string
}

interface PriorityMeta {
  label: string
  color: string
  level: number
}

interface MemberInfo {
  name: string
  emoji: string
  role: string
  taskCount: number
  completedCount: number
  color: string
}

interface TaskItem {
  id: number
  title: string
  description: string
  column: string
  priority: string
  assignee: string
  dueDate: string
  tags: string[]
  createdAt: string
  isCompleted: boolean
  storyPoints: number
}

// ==================== Config ====================
const BOARD_COLUMNS: Record<string, ColumnMeta> = {
  '待办': { label: '待办', icon: '📋', color: '#4527A0', bg: '#EDE7F6' },
  '进行中': { label: '进行中', icon: '🔄', color: '#00838F', bg: '#E0F2F1' },
  '审核中': { label: '审核中', icon: '👀', color: '#FF8F00', bg: '#FFF3E0' },
  '已完成': { label: '已完成', icon: '✅', color: '#2E7D32', bg: '#E8F5E9' }
}

const PRIORITY_CONFIG: Record<string, PriorityMeta> = {
  '紧急': { label: '紧急', color: '#C2185B', level: 4 },
  '重要': { label: '重要', color: '#FF6F00', level: 3 },
  '普通': { label: '普通', color: '#1565C0', level: 2 },
  '低': { label: '低', color: '#9E9E9E', level: 1 }
}

const COLUMN_NAMES: string[] = ['待办', '进行中', '审核中', '已完成']
const PRIORITY_LIST: string[] = ['紧急', '重要', '普通', '低']
const ASSIGNEE_LIST: string[] = ['张三', '李四', '王五', '赵六']

// ==================== Data ====================
const mockTasks: TaskItem[] = [
  { id: 1, title: '用户登录模块开发', description: '实现手机号/邮箱登录功能', column: '进行中', priority: '紧急', assignee: '张三', dueDate: '2026-07-28', tags: ['前端', '核心功能'], createdAt: '2026-07-15', isCompleted: false, storyPoints: 8 },
  { id: 2, title: '数据库索引优化', description: '对用户表和订单表添加复合索引', column: '待办', priority: '重要', assignee: '李四', dueDate: '2026-07-30', tags: ['后端', '性能'], createdAt: '2026-07-18', isCompleted: false, storyPoints: 5 },
  { id: 3, title: '首页UI改版', description: '根据新版设计稿更新首页布局和样式', column: '审核中', priority: '重要', assignee: '张三', dueDate: '2026-07-26', tags: ['前端', 'UI'], createdAt: '2026-07-10', isCompleted: false, storyPoints: 13 },
  { id: 4, title: '支付接口联调', description: '对接支付宝和微信支付接口', column: '进行中', priority: '紧急', assignee: '王五', dueDate: '2026-07-27', tags: ['后端', '支付'], createdAt: '2026-07-16', isCompleted: false, storyPoints: 8 },
  { id: 5, title: '消息推送功能', description: '实现App内消息推送和通知栏推送', column: '待办', priority: '普通', assignee: '赵六', dueDate: '2026-08-05', tags: ['后端', '推送'], createdAt: '2026-07-20', isCompleted: false, storyPoints: 8 },
  { id: 6, title: '用户反馈收集页', description: '开发用户意见反馈表单页面', column: '已完成', priority: '低', assignee: '张三', dueDate: '2026-07-22', tags: ['前端', '用户体验'], createdAt: '2026-07-08', isCompleted: true, storyPoints: 3 },
  { id: 7, title: '日志系统搭建', description: '搭建ELK日志收集和分析系统', column: '待办', priority: '重要', assignee: '李四', dueDate: '2026-08-10', tags: ['运维', '基础设施'], createdAt: '2026-07-19', isCompleted: false, storyPoints: 8 },
  { id: 8, title: '商品详情页优化', description: '优化图片加载速度和页面渲染性能', column: '审核中', priority: '普通', assignee: '张三', dueDate: '2026-07-29', tags: ['前端', '性能'], createdAt: '2026-07-12', isCompleted: false, storyPoints: 5 },
  { id: 9, title: 'API文档自动生成', description: '接入Swagger自动生成接口文档', column: '已完成', priority: '普通', assignee: '王五', dueDate: '2026-07-18', tags: ['后端', '工具'], createdAt: '2026-07-05', isCompleted: true, storyPoints: 3 },
  { id: 10, title: '灰度发布方案设计', description: '制定按用户比例逐步发布的方案', column: '待办', priority: '重要', assignee: '李四', dueDate: '2026-08-15', tags: ['运维', '架构'], createdAt: '2026-07-21', isCompleted: false, storyPoints: 5 },
  { id: 11, title: '搜索功能优化', description: '实现模糊搜索和搜索建议功能', column: '进行中', priority: '重要', assignee: '赵六', dueDate: '2026-08-02', tags: ['后端', '搜索'], createdAt: '2026-07-14', isCompleted: false, storyPoints: 8 },
  { id: 12, title: '单元测试补全', description: '核心业务逻辑单元测试覆盖率达到80%', column: '待办', priority: '普通', assignee: '王五', dueDate: '2026-08-20', tags: ['测试', '质量'], createdAt: '2026-07-22', isCompleted: false, storyPoints: 13 },
  { id: 13, title: 'iOS适配修复', description: '修复iOS端样式兼容性问题', column: '审核中', priority: '紧急', assignee: '张三', dueDate: '2026-07-25', tags: ['前端', '兼容性'], createdAt: '2026-07-20', isCompleted: false, storyPoints: 5 },
  { id: 14, title: 'Redis缓存重构', description: '统一缓存key规范和过期策略', column: '待办', priority: '重要', assignee: '李四', dueDate: '2026-08-05', tags: ['后端', '缓存'], createdAt: '2026-07-17', isCompleted: false, storyPoints: 5 },
  { id: 15, title: '数据导出功能', description: '支持Excel和CSV格式的数据导出', column: '进行中', priority: '普通', assignee: '赵六', dueDate: '2026-08-01', tags: ['后端', '数据'], createdAt: '2026-07-13', isCompleted: false, storyPoints: 5 },
  { id: 16, title: '权限系统完善', description: '细化角色权限颗粒度和操作日志', column: '待办', priority: '重要', assignee: '王五', dueDate: '2026-08-08', tags: ['后端', '安全'], createdAt: '2026-07-19', isCompleted: false, storyPoints: 8 },
  { id: 17, title: '注册页面改版', description: '优化注册流程减少用户流失', column: '已完成', priority: '紧急', assignee: '张三', dueDate: '2026-07-20', tags: ['前端', '用户体验'], createdAt: '2026-07-06', isCompleted: true, storyPoints: 5 },
  { id: 18, title: '数据库备份策略', description: '制定增量+全量备份方案', column: '待办', priority: '紧急', assignee: '李四', dueDate: '2026-07-31', tags: ['运维', '安全'], createdAt: '2026-07-22', isCompleted: false, storyPoints: 3 },
  { id: 19, title: '下拉刷新动画', description: '设计并实现下拉刷新自定义动画', column: '审核中', priority: '低', assignee: '张三', dueDate: '2026-08-03', tags: ['前端', '动画'], createdAt: '2026-07-18', isCompleted: false, storyPoints: 3 },
  { id: 20, title: '接口限流实现', description: '对高频接口添加令牌桶限流', column: '进行中', priority: '重要', assignee: '王五', dueDate: '2026-08-05', tags: ['后端', '安全'], createdAt: '2026-07-15', isCompleted: false, storyPoints: 5 },
  { id: 21, title: '暗黑模式支持', description: '全站支持深色/浅色主题切换', column: '待办', priority: '低', assignee: '赵六', dueDate: '2026-08-20', tags: ['前端', '体验'], createdAt: '2026-07-23', isCompleted: false, storyPoints: 8 },
  { id: 22, title: '埋点数据看板', description: '可视化展示核心埋点数据指标', column: '待办', priority: '普通', assignee: '李四', dueDate: '2026-08-12', tags: ['数据', '看板'], createdAt: '2026-07-21', isCompleted: false, storyPoints: 8 },
  { id: 23, title: '客服IM接入', description: '接入第三方客服即时通讯系统', column: '进行中', priority: '普通', assignee: '赵六', dueDate: '2026-08-10', tags: ['后端', '客服'], createdAt: '2026-07-12', isCompleted: false, storyPoints: 5 },
  { id: 24, title: '图片懒加载', description: '列表图片实现懒加载优化首屏', column: '已完成', priority: '普通', assignee: '张三', dueDate: '2026-07-19', tags: ['前端', '性能'], createdAt: '2026-07-07', isCompleted: true, storyPoints: 2 },
  { id: 25, title: '部署流水线优化', description: 'CI/CD流水线并行化加快构建', column: '待办', priority: '重要', assignee: '李四', dueDate: '2026-08-15', tags: ['运维', '效率'], createdAt: '2026-07-22', isCompleted: false, storyPoints: 5 }
]

const mockMembers: MemberInfo[] = [
  { name: '张三', emoji: '🧑‍💻', role: '前端负责人', taskCount: 8, completedCount: 4, color: '#4527A0' },
  { name: '李四', emoji: '👨‍🔧', role: '后端架构师', taskCount: 7, completedCount: 2, color: '#00838F' },
  { name: '王五', emoji: '💻', role: '后端开发', taskCount: 6, completedCount: 3, color: '#FF8F00' },
  { name: '赵六', emoji: '🎨', role: '全栈开发', taskCount: 5, completedCount: 1, color: '#C2185B' }
]

function getTaskTotal(): number { return 25 }
function getCompletedTotal(): number { return 5 }
function getInProgressTotal(): number { return 6 }
function getOverdueCount(): number { return 3 }
function getTotalStoryPoints(): number { return 138 }

// ==================== Tab Enum ====================
enum KanbanTab {
  BOARD = 0,
  LIST = 1,
  STATS = 2,
  PROFILE = 3
}

// ==================== Entry ====================
@Entry
@Component
struct KanbanApp {
  @State activeTab: KanbanTab = KanbanTab.BOARD

  @Builder contentArea() {
    Column() {
      if (this.activeTab === KanbanTab.BOARD) {
        BoardContent()
      } else if (this.activeTab === KanbanTab.LIST) {
        TaskListContent()
      } else if (this.activeTab === KanbanTab.STATS) {
        KanbanStatsContent()
      } else {
        KanbanProfileContent()
      }
    }
    .layoutWeight(1)
  }

  @Builder tabItem(icon: string, label: string, tab: KanbanTab, accent: string) {
    Column() {
      Text(icon).fontSize(20).opacity(this.activeTab === tab ? 1.0 : 0.4)
      Text(label).fontSize(10)
        .fontColor(this.activeTab === tab ? accent : '#999999')
        .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 2 })
      if (this.activeTab === tab) {
        Column().width(18).height(3).backgroundColor(accent).borderRadius(2).margin({ top: 2 })
      }
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Center)
    .padding({ top: 5, bottom: 5 })
    .onClick(() => { this.activeTab = tab })
  }

  build() {
    Column() {
      this.contentArea()
      Divider().color('#E0E0E0').strokeWidth(0.5)
      Row() {
        this.tabItem('📋', '看板', KanbanTab.BOARD, '#4527A0')
        this.tabItem('📝', '列表', KanbanTab.LIST, '#C2185B')
        this.tabItem('📊', '统计', KanbanTab.STATS, '#00838F')
        this.tabItem('👤', '我的', KanbanTab.PROFILE, '#1565C0')
      }
      .width('100%').backgroundColor('#FFFFFF').padding({ top: 4, bottom: 6 })
    }
    .width('100%').height('100%').backgroundColor('#F3E5F5')
  }
}

// ==================== Board Content (Kanban View) ====================
@Component
struct BoardContent {
  @State showAddModal: boolean = false
  @State showDetailModal: boolean = false
  @State selectedTask: TaskItem | null = null
  @State formTitle: string = ''
  @State formPriority: string = '普通'
  @State formAssignee: string = '张三'
  @State formColumn: string = '待办'

  @Builder modalBg(onClose: () => void) {
    Column().width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.5)').onClick(onClose)
  }

  @Builder addTaskModal() {
    Column() {
      this.modalBg(() => { this.showAddModal = false })
      Column() {
        Row() {
          Text('➕ 新建任务').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
          Row().layoutWeight(1)
          Text('✕').fontSize(20).fontColor('#999999').onClick(() => { this.showAddModal = false })
        }
        .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 10 })
        Divider().color('#F0F0F0')
        Column() {
          Text('任务标题').fontSize(12).fontColor('#888888').margin({ top: 14, left: 20 })
          TextInput({ placeholder: '输入任务标题...' })
            .placeholderColor('#BBBBBB').fontSize(14)
            .backgroundColor('#F5F0F7').borderRadius(10)
            .margin({ left: 20, right: 20, top: 4 })
            .onChange((v: string) => { this.formTitle = v })
          Text('优先级').fontSize(12).fontColor('#888888').margin({ top: 14, left: 20 })
          Row() {
            ForEach(PRIORITY_LIST, (p: string) => {
              Text(PRIORITY_CONFIG[p]?.label ?? p).fontSize(11)
                .fontColor(this.formPriority === p ? '#FFFFFF' : PRIORITY_CONFIG[p]?.color ?? '#666666')
                .backgroundColor(this.formPriority === p ? PRIORITY_CONFIG[p]?.color : '#F5F5F5')
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .borderRadius(18).margin({ left: 4, right: 4 })
                .onClick(() => { this.formPriority = p })
            })
          }
          .margin({ left: 16, right: 16, top: 6 })
          Text('负责人').fontSize(12).fontColor('#888888').margin({ top: 14, left: 20 })
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(ASSIGNEE_LIST, (a: string) => {
              Text(a).fontSize(12)
                .fontColor(this.formAssignee === a ? '#FFFFFF' : '#4527A0')
                .backgroundColor(this.formAssignee === a ? '#4527A0' : '#EDE7F6')
                .padding({ left: 16, right: 16, top: 8, bottom: 8 })
                .borderRadius(16).margin({ left: 4, right: 4, top: 4 })
                .onClick(() => { this.formAssignee = a })
            })
          }
          .margin({ left: 16, right: 16, top: 6 })
        }
        .layoutWeight(1)
        Row() {
          Text('取消').fontSize(14).fontColor('#888888')
            .backgroundColor('#F5F5F5').borderRadius(22)
            .padding({ left: 28, right: 28, top: 11, bottom: 11 })
            .onClick(() => { this.showAddModal = false })
          Text('创建任务').fontSize(14).fontColor('#FFFFFF')
            .backgroundColor('#4527A0').borderRadius(22)
            .padding({ left: 28, right: 28, top: 11, bottom: 11 }).margin({ left: 12 })
            .onClick(() => { this.showAddModal = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 20, right: 20, top: 14, bottom: 16 })
      }
      .width('90%').backgroundColor('#FFFFFF').borderRadius(18)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '5%', y: '15%' })
      .shadow({ radius: 20, color: '#33000000', offsetY: 4 })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder detailModal() {
    Column() {
      this.modalBg(() => { this.showDetailModal = false })
      Column() {
        Row() {
          Column() {
            Text(PRIORITY_CONFIG[this.selectedTask?.priority ?? '普通']?.label ?? '').fontSize(12)
              .fontColor('#FFFFFF')
              .backgroundColor(PRIORITY_CONFIG[this.selectedTask?.priority ?? '普通']?.color ?? '#999999')
              .padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(10)
          }
          Column().layoutWeight(1)
          Text('✕').fontSize(20).fontColor('#999999').onClick(() => { this.showDetailModal = false })
        }
        .width('100%').padding({ left: 20, right: 20, top: 18 })
        Text(this.selectedTask?.title ?? '').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
          .width('100%').padding({ left: 20, top: 10 })
        Scroll() {
          Column() {
            Text(this.selectedTask?.description ?? '').fontSize(13).fontColor('#666666')
              .width('100%').padding({ left: 20, right: 20, top: 10 })
              .padding({ top: 10, bottom: 10 }).backgroundColor('#F5F5F5').borderRadius(10)
            Row() {
              Column() {
                Text('👤').fontSize(16)
                Text('负责人').fontSize(9).fontColor('#999999').margin({ top: 2 })
                Text(this.selectedTask?.assignee ?? '').fontSize(12).fontColor('#212121').margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center).margin({ top: 12 })
              Column() {
                Text('📅').fontSize(16)
                Text('截止日').fontSize(9).fontColor('#999999').margin({ top: 2 })
                Text(this.selectedTask?.dueDate ?? '').fontSize(12).fontColor('#212121').margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center).margin({ top: 12 })
              Column() {
                Text('🎯').fontSize(16)
                Text('故事点').fontSize(9).fontColor('#999999').margin({ top: 2 })
                Text((this.selectedTask?.storyPoints ?? 0).toString()).fontSize(12).fontColor('#212121').margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center).margin({ top: 12 })
            }
            Row() {
              ForEach(this.selectedTask?.tags ?? [], (tag: string) => {
                Text('#' + tag).fontSize(10).fontColor('#4527A0')
                  .backgroundColor('#EDE7F6').padding({ left: 8, right: 8, top: 3, bottom: 3 })
                  .borderRadius(10).margin({ left: 3, right: 3 })
              })
            }
            .width('100%').margin({ top: 12 }).padding({ left: 20, right: 20 })
          }
          .padding({ bottom: 16 })
        }
        .layoutWeight(1)
        Row() {
          Text('✏️ 编辑').fontSize(13).fontColor('#FFFFFF')
            .backgroundColor('#FF8F00').borderRadius(18)
            .padding({ left: 20, right: 20, top: 9, bottom: 9 })
          Text('🗑️ 删除').fontSize(13).fontColor('#FFFFFF')
            .backgroundColor('#C2185B').borderRadius(18)
            .padding({ left: 20, right: 20, top: 9, bottom: 9 }).margin({ left: 8 })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 20, right: 20, top: 12, bottom: 14 })
      }
      .width('92%').constraintSize({ maxHeight: '80%' }).backgroundColor('#FFFFFF').borderRadius(18)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '4%', y: '10%' })
      .shadow({ radius: 20, color: '#33000000', offsetY: 4 })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder taskCard(t: TaskItem) {
    Column() {
      Row() {
        Text(PRIORITY_CONFIG[t.priority]?.label ?? '').fontSize(9)
          .fontColor('#FFFFFF').backgroundColor(PRIORITY_CONFIG[t.priority]?.color ?? '#999999')
          .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(8)
        Row().layoutWeight(1)
        Text(t.storyPoints + 'pt').fontSize(9).fontColor('#888888')
      }
      .width('100%')
      Text(t.title).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
        .width('100%').margin({ top: 8 })
      Text(t.description).fontSize(10).fontColor('#888888')
        .width('100%').margin({ top: 4 }).maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Row() {
        Text('👤' + t.assignee).fontSize(9).fontColor('#666666')
        Row().layoutWeight(1)
        Text('📅' + t.dueDate).fontSize(9).fontColor('#999999')
      }
      .width('100%').margin({ top: 8 })
      Row() {
        ForEach(t.tags.slice(0, 2), (tag: string) => {
          Text('#' + tag).fontSize(8).fontColor('#4527A0')
            .backgroundColor('#EDE7F6').padding({ left: 5, right: 5, top: 2, bottom: 2 })
            .borderRadius(6).margin({ right: 4 })
        })
      }
      .width('100%').margin({ top: 6 })
    }
    .width(180).padding(12).backgroundColor('#FFFFFF')
    .borderRadius(12).margin({ top: 6, bottom: 2 })
    .shadow({ radius: 3, color: '#15000000', offsetY: 2 })
    .onClick(() => { this.selectedTask = t; this.showDetailModal = true })
  }

  @Builder columnBlock(colName: string, tasks: TaskItem[]) {
    Column() {
      Row() {
        Text(BOARD_COLUMNS[colName]?.icon ?? '📋').fontSize(14)
        Text(colName).fontSize(13).fontWeight(FontWeight.Bold)
          .fontColor(BOARD_COLUMNS[colName]?.color ?? '#4527A0').margin({ left: 4 })
        Column() {
          Text(tasks.length.toString()).fontSize(11).fontColor('#FFFFFF')
        }
        .height(20).backgroundColor(BOARD_COLUMNS[colName]?.color ?? '#4527A0')
        .borderRadius(10).padding({ left: 8, right: 8 }).margin({ left: 6 })
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
      }
      .width('100%')
      ForEach(tasks, (t: TaskItem) => {
        this.taskCard(t)
      })
    }
    .width(200).padding({ left: 8, right: 8, top: 10, bottom: 10 })
    .backgroundColor(BOARD_COLUMNS[colName]?.bg ?? '#F5F5F5')
    .borderRadius(14).margin({ left: 6, right: 6 })
  }

  build() {
    Stack() {
      Column() {
        Row() {
          Column() {
            Text('📋 项目看板').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#4527A0')
            Text(getTaskTotal() + '个任务 · ' + getCompletedTotal() + '已完成 · ' + getOverdueCount() + '逾期').fontSize(11).fontColor('#888888').margin({ top: 2 })
          }
          Row().layoutWeight(1)
          Text('+').fontSize(24).fontColor('#FFFFFF')
            .backgroundColor('#4527A0').width(38).height(38).borderRadius(19)
            .textAlign(TextAlign.Center)
            .onClick(() => { this.showAddModal = true })
        }
        .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
        Scroll() {
          Row() {
            this.columnBlock('待办', mockTasks.filter((t: TaskItem): boolean => { return t.column === '待办' }))
            this.columnBlock('进行中', mockTasks.filter((t: TaskItem): boolean => { return t.column === '进行中' }))
            this.columnBlock('审核中', mockTasks.filter((t: TaskItem): boolean => { return t.column === '审核中' }))
            this.columnBlock('已完成', mockTasks.filter((t: TaskItem): boolean => { return t.column === '已完成' }))
          }
          .padding({ left: 6, right: 6, top: 6, bottom: 20 })
        }
        .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
        .layoutWeight(1)
      }
      .width('100%').height('100%')

      if (this.showAddModal) { this.addTaskModal() }
      if (this.showDetailModal) { this.detailModal() }
    }
    .width('100%').height('100%')
  }
}

// ==================== List Content ====================
@Component
struct TaskListContent {
  @State showDeleteConfirm: boolean = false
  @State showEditModal: boolean = false
  @State selectedTask: TaskItem | null = null
  @State editTitle: string = ''

  @Builder modalBg(onClose: () => void) {
    Column().width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.5)').onClick(onClose)
  }

  @Builder editModal() {
    Column() {
      this.modalBg(() => { this.showEditModal = false })
      Column() {
        Row() {
          Text('✏️ 编辑任务').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
          Row().layoutWeight(1)
          Text('✕').fontSize(20).fontColor('#999999').onClick(() => { this.showEditModal = false })
        }
        .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 10 })
        Divider().color('#F0F0F0')
        Column() {
          Text('任务标题').fontSize(12).fontColor('#888888').margin({ top: 14, left: 20 })
          TextInput({ placeholder: this.selectedTask?.title ?? '' })
            .placeholderColor('#BBBBBB').fontSize(14)
            .backgroundColor('#F5F0F7').borderRadius(10)
            .margin({ left: 20, right: 20, top: 4 })
            .onChange((v: string) => { this.editTitle = v })
          Text('描述').fontSize(12).fontColor('#888888').margin({ top: 14, left: 20 })
          TextArea({ placeholder: this.selectedTask?.description ?? '' })
            .placeholderColor('#BBBBBB').fontSize(13).height(60)
            .backgroundColor('#F5F0F7').borderRadius(10)
            .margin({ left: 20, right: 20, top: 4 })
        }
        .layoutWeight(1)
        Row() {
          Text('取消').fontSize(14).fontColor('#888888')
            .backgroundColor('#F5F5F5').borderRadius(22)
            .padding({ left: 28, right: 28, top: 11, bottom: 11 })
            .onClick(() => { this.showEditModal = false })
          Text('保存').fontSize(14).fontColor('#FFFFFF')
            .backgroundColor('#C2185B').borderRadius(22)
            .padding({ left: 28, right: 28, top: 11, bottom: 11 }).margin({ left: 12 })
            .onClick(() => { this.showEditModal = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 20, right: 20, top: 14, bottom: 16 })
      }
      .width('90%').backgroundColor('#FFFFFF').borderRadius(18)
      .alignItems(HorizontalAlign.Center).position({ x: '5%', y: '18%' })
      .shadow({ radius: 20, color: '#33000000', offsetY: 4 })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder deleteModal() {
    Column() {
      this.modalBg(() => { this.showDeleteConfirm = false })
      Column() {
        Text('⚠️').fontSize(48).margin({ top: 24 })
        Text('确认删除此任务?').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
        Row() {
          Text(PRIORITY_CONFIG[this.selectedTask?.priority ?? '普通']?.label ?? '').fontSize(11)
            .fontColor('#FFFFFF').backgroundColor(PRIORITY_CONFIG[this.selectedTask?.priority ?? '普通']?.color ?? '#999999')
            .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
          Text(this.selectedTask?.title ?? '').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 8 })
        }
        .backgroundColor('#FFF5F5').borderRadius(12)
        .padding({ left: 16, right: 16, top: 12, bottom: 12 }).margin({ top: 16 })
        Row() {
          Text('取消').fontSize(14).fontColor('#888888')
            .backgroundColor('#F5F5F5').borderRadius(22)
            .padding({ left: 28, right: 28, top: 11, bottom: 11 })
            .onClick(() => { this.showDeleteConfirm = false })
          Text('确认删除').fontSize(14).fontColor('#FFFFFF')
            .backgroundColor('#C2185B').borderRadius(22)
            .padding({ left: 28, right: 28, top: 11, bottom: 11 }).margin({ left: 12 })
            .onClick(() => { this.showDeleteConfirm = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 20, right: 20, top: 20, bottom: 20 })
      }
      .width('80%').backgroundColor('#FFFFFF').borderRadius(18)
      .alignItems(HorizontalAlign.Center).position({ x: '10%', y: '38%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder listItem(t: TaskItem) {
    Row() {
      Column() {
        Text(BOARD_COLUMNS[t.column]?.icon ?? '📋').fontSize(16)
      }
      .width(38).height(38)
      .backgroundColor(BOARD_COLUMNS[t.column]?.bg ?? '#F5F5F5')
      .borderRadius(10)
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
      Column() {
        Row() {
          Text(t.title).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#212121')
          Column() {
            Text(t.storyPoints + 'pt').fontSize(9).fontColor('#888888').margin({ left: 6 })
          }
        }
        Text(t.description).fontSize(10).fontColor('#999999').margin({ top: 2 })
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Row() {
          Text(t.assignee).fontSize(9).fontColor('#666666')
          Text('·').fontSize(9).fontColor('#CCCCCC').margin({ left: 4 })
          Text(t.dueDate).fontSize(9).fontColor('#F44336')
        }
        .margin({ top: 3 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
      Column() {
        Text('✏️').fontSize(14).fontColor('#C2185B')
          .onClick(() => { this.selectedTask = t; this.showEditModal = true })
        Text('🗑️').fontSize(14).fontColor('#F44336').margin({ top: 8 })
          .onClick(() => { this.selectedTask = t; this.showDeleteConfirm = true })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width('100%').padding(12).backgroundColor('#FFFFFF')
    .borderRadius(12).margin({ left: 12, right: 12, top: 5 })
    .shadow({ radius: 2, color: '#10000000', offsetY: 1 })
  }

  build() {
    Stack() {
      Column() {
        Text('📝 全部任务').fontSize(18).fontWeight(FontWeight.Bold)
          .width('100%').padding({ left: 16, top: 14, bottom: 8 })
        Scroll() {
          Column() {
            this.listItem(mockTasks[0])
            this.listItem(mockTasks[1])
            this.listItem(mockTasks[2])
            this.listItem(mockTasks[3])
            this.listItem(mockTasks[4])
            this.listItem(mockTasks[5])
            this.listItem(mockTasks[6])
            this.listItem(mockTasks[7])
            this.listItem(mockTasks[8])
            this.listItem(mockTasks[9])
            this.listItem(mockTasks[10])
            this.listItem(mockTasks[11])
            this.listItem(mockTasks[12])
            this.listItem(mockTasks[13])
            this.listItem(mockTasks[14])
            this.listItem(mockTasks[15])
            this.listItem(mockTasks[16])
            this.listItem(mockTasks[17])
            this.listItem(mockTasks[18])
            this.listItem(mockTasks[19])
            this.listItem(mockTasks[20])
            this.listItem(mockTasks[21])
            this.listItem(mockTasks[22])
            this.listItem(mockTasks[23])
            this.listItem(mockTasks[24])
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')
      if (this.showEditModal) { this.editModal() }
      if (this.showDeleteConfirm) { this.deleteModal() }
    }
    .width('100%').height('100%')
  }
}

// ==================== Stats Content ====================
@Component
struct KanbanStatsContent {
  build() {
    Column() {
      Text('📊 项目统计').fontSize(18).fontWeight(FontWeight.Bold)
        .width('100%').padding({ left: 16, top: 14, bottom: 8 })
      Scroll() {
        Column() {
          Row() {
            Column() {
              Text(getTaskTotal().toString()).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#4527A0')
              Text('总任务').fontSize(10).fontColor('#888888')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column() {
              Text(getInProgressTotal().toString()).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#00838F')
              Text('进行中').fontSize(10).fontColor('#888888')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column() {
              Text(getCompletedTotal().toString()).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
              Text('已完成').fontSize(10).fontColor('#888888')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column() {
              Text(getTotalStoryPoints().toString()).fontSize(24).fontWeight(FontWeight.Bold).fontColor('#C2185B')
              Text('总故事点').fontSize(10).fontColor('#888888')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
          }
          .width('100%').backgroundColor('#FFFFFF').borderRadius(16)
          .margin({ left: 12, right: 12 }).padding({ top: 16, bottom: 16 })
          .shadow({ radius: 4, color: '#10000000', offsetY: 2 })

          Column() {
            Text('📊 任务分布').fontSize(13).fontWeight(FontWeight.Bold)
              .width('100%').padding({ left: 16, top: 14, bottom: 8 })
            Column() {
              Row() { Text('📋 待办').fontSize(11).fontColor('#4527A0').layoutWeight(1); Text('12项').fontSize(11).fontColor('#888888') }
              Row() { Column().width('48%').height(7).backgroundColor('#4527A0').borderRadius(4); Row().layoutWeight(1) }
              .width('100%').margin({ top: 4, bottom: 10 })
              Row() { Text('🔄 进行中').fontSize(11).fontColor('#00838F').layoutWeight(1); Text('6项').fontSize(11).fontColor('#888888') }
              Row() { Column().width('24%').height(7).backgroundColor('#00838F').borderRadius(4); Row().layoutWeight(1) }
              .width('100%').margin({ top: 4, bottom: 10 })
              Row() { Text('👀 审核中').fontSize(11).fontColor('#FF8F00').layoutWeight(1); Text('3项').fontSize(11).fontColor('#888888') }
              Row() { Column().width('12%').height(7).backgroundColor('#FF8F00').borderRadius(4); Row().layoutWeight(1) }
              .width('100%').margin({ top: 4, bottom: 10 })
              Row() { Text('✅ 已完成').fontSize(11).fontColor('#2E7D32').layoutWeight(1); Text('4项').fontSize(11).fontColor('#888888') }
              Row() { Column().width('16%').height(7).backgroundColor('#2E7D32').borderRadius(4); Row().layoutWeight(1) }
              .width('100%').margin({ top: 4 })
            }
            .padding({ left: 16, right: 16, bottom: 14 })
          }
          .width('100%').backgroundColor('#FFFFFF').borderRadius(16)
          .margin({ left: 12, right: 12, top: 8 })
          .shadow({ radius: 4, color: '#10000000', offsetY: 2 })

          Column() {
            Text('👥 成员工作负载').fontSize(13).fontWeight(FontWeight.Bold)
              .width('100%').padding({ left: 16, top: 14, bottom: 8 })
            ForEach(mockMembers, (m: MemberInfo) => {
              Row() {
                Text(m.emoji).fontSize(24)
                Column() {
                  Text(m.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
                  Text(m.role).fontSize(10).fontColor('#888888').margin({ top: 2 })
                  Row() {
                    Column()
                      .width((m.completedCount / m.taskCount * 120).toFixed(0) + 'vp')
                      .height(6).backgroundColor(m.color).borderRadius(3)
                    Row().layoutWeight(1)
                  }
                  .width('100%').height(6).backgroundColor('#F0F0F0').borderRadius(3).margin({ top: 4 })
                }
                .layoutWeight(1).padding({ left: 8 })
                Text(m.completedCount + '/' + m.taskCount).fontSize(12).fontColor('#888888')
              }
              .width('100%').padding({ top: 10, bottom: 10 })
              Divider().color('#F0F0F0')
            })
          }
          .width('100%').backgroundColor('#FFFFFF').borderRadius(16)
          .margin({ left: 12, right: 12, top: 8 })
          .shadow({ radius: 4, color: '#10000000', offsetY: 2 })
        }
        .padding({ bottom: 20 })
      }
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

// ==================== Profile Content ====================
@Component
struct KanbanProfileContent {
  build() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('🚀').fontSize(40)
          }
          .width(64).height(64).backgroundColor('#EDE7F6').borderRadius(32)
          .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
          Column() {
            Text('V2.0 发布项目').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121')
            Text('冲刺Sprint 3 · 第2周').fontSize(11).fontColor('#888888').margin({ top: 3 })
            Text(getCompletedTotal() + '/' + getTaskTotal() + ' 任务已完成').fontSize(10).fontColor('#4527A0').margin({ top: 2 })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
        }
        .width('100%').padding(16)
      }
      .width('100%').backgroundColor('#FFFFFF').borderRadius(16)
      .margin({ left: 12, right: 12, top: 10 })
      .shadow({ radius: 4, color: '#10000000', offsetY: 2 })

      Column() {
        Text('⚙️ 项目设置').fontSize(13).fontWeight(FontWeight.Bold)
          .width('100%').padding({ left: 16, top: 14, bottom: 8 })
        Column() {
          Row() { Text('👥').fontSize(18); Text('成员管理').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('›').fontColor('#CCCCCC') }
          .width('100%').padding({ top: 12, bottom: 12, left: 4 })
          Divider().color('#F0F0F0')
          Row() { Text('🏷️').fontSize(18); Text('标签管理').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('›').fontColor('#CCCCCC') }
          .width('100%').padding({ top: 12, bottom: 12, left: 4 })
          Divider().color('#F0F0F0')
          Row() { Text('🔄').fontSize(18); Text('Sprint设置').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('›').fontColor('#CCCCCC') }
          .width('100%').padding({ top: 12, bottom: 12, left: 4 })
          Divider().color('#F0F0F0')
          Row() { Text('🔗').fontSize(18); Text('Webhook集成').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('›').fontColor('#CCCCCC') }
          .width('100%').padding({ top: 12, bottom: 12, left: 4 })
        }
        .padding({ left: 16, right: 16 })
      }
      .width('100%').backgroundColor('#FFFFFF').borderRadius(16)
      .margin({ left: 12, right: 12, top: 8 })
      .shadow({ radius: 4, color: '#10000000', offsetY: 2 })
      Text('v2.0 · 项目看板 · 2026').fontSize(10).fontColor('#CCCCCC')
        .margin({ top: 16, bottom: 16 })
    }
    .width('100%').height('100%')
  }
}


技术亮点回顾

纵观整个应用的源码实现,我们可以总结出以下几个值得深入学习的 ArkTS 技术亮点:

在这里插入图片描述

第一,配置化驱动 UI 的设计思想。 应用通过 BOARD_COLUMNSPRIORITY_CONFIG 两个 Record 类型的配置映射表,将列样式和优先级样式与 UI 渲染代码完全解耦。所有涉及列和优先级的 UI 元素(颜色、图标、背景色)都从配置表中动态读取,实现了"一处定义、处处引用"的配置化管理。这种模式使得配色方案的调整、新列/新优先级的增删只需修改配置表,无需触碰任何组件代码。

第二,@Builder 构建器的参数化复用模式。 应用中大量使用了参数化 @Builder 来实现 UI 片段的复用——modalBg(onClose) 复用了遮罩层、tabItem(icon, label, tab, accent) 复用了导航项、taskCard(t) 复用了任务卡片、columnBlock(colName, tasks) 复用了看板列、listItem(t) 复用了列表项。这种"将重复的 UI 结构抽取为参数化构建器"的做法,是 ArkTS 中控制代码重复、提升可维护性的核心手段。

第三,Stack + 条件渲染 + zIndex 的自定义模态弹窗方案。 应用没有使用系统提供的 CustomDialogbindSheet,而是通过 Stack 层叠布局将弹窗覆盖在内容之上,通过 @State 布尔变量 + if 条件渲染控制弹窗的显隐,通过 zIndex(999) 确保弹窗显示在最顶层。这种方案提供了最高的样式自定义自由度,是 ArkTS 中实现复杂模态交互的推荐做法。

第四,纯 UI 组件模拟图表的技术方案。 统计视图中的任务分布条形图和成员工作负载进度条,完全使用 RowColumn 等布局组件配合百分比/vp 宽度控制实现,无需引入任何第三方图表库。这种方案在轻量级应用中具有包体积小、渲染性能好、样式灵活的优势。

第五,可选链 + 空值合并的安全访问模式。 在处理 TaskItem | null 类型的 selectedTask 时,代码全程使用 this.selectedTask?.property ?? defaultValue 的安全访问模式,避免了空指针异常。这是 ArkTS/TypeScript 强类型系统中处理可空值的标准实践,值得在所有涉及可空对象属性访问的场景中推广。

第六,函数封装数据访问的抽象层模式。 getTaskTotal()getCompletedTotal() 等辅助函数将统计数据封装为函数调用,为从"静态返回"到"动态计算"或"网络请求"的演进预留了平滑的过渡路径,体现了数据访问抽象层的设计智慧。

Logo

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

更多推荐