HarmonyOS 办公自动化助手开发实践:基于 ArkTS 的 AI 工具类应用架构全解


在这里插入图片描述
在这里插入图片描述

摘要

本文记录了一款基于 HarmonyOS 6.0 + ArkTS 开发的"办公自动化助手"应用的完整开发过程。该应用面向办公场景中常见的数据处理"脏活累活"(如跨表数据匹配、批量处理、数据合并等),根据用户描述的任务和数据量规模,自动匹配 Excel 函数组合、VBA 宏或 Python 脚本三种技术方案,并以"Step-by-Step"傻瓜式操作指南呈现给用户。文章涵盖 ArkTS 严格模式下的类型安全建模、@Observed 响应式数据驱动的 MVVM 架构、@Builder 声明式 UI 碎片模式、Flex 流式标签布局、SymbolGlyph 系统符号使用、可展开代码块的交互设计、容错处理与降级方案、Prompt 工程中的输出结构化设计等技术要点。全文提供完整的代码示例,所有代码零编译错误。


一、项目背景与需求分析

1.1 问题定义

在日常办公场景中,Excel 数据处理是一个高频但低价值的重复性工作。大量办公人员面临的核心问题不是"不会用 Excel",而是"不知道用什么方法最高效"。例如:

  • 需要将两个表格按客户ID匹配数据时,很多人会手动复制粘贴
  • 数据量超过几万行时,Excel 函数卡顿,不知道应该用 VBA 或 Python
  • 找到方法后,操作步骤记不住,需要反复搜索教程
  • 复制代码运行时报错,不知道如何排查

这类问题的本质是技术方案与用户之间存在"最后一公里"的距离——用户知道自己要做什么,但不知道用什么工具、按什么步骤做。

1.2 产品定位

"办公自动化助手"的核心定位是:一个方案决策器 + 操作指引生成器,而非 Excel 教程 APP 或代码编辑器。它解决三个层面的问题:

  1. 方案选择:根据数据量(小于/大于1万行)和数据格式(Excel/CSV/数据库),自动选择最合适的技术方案
  2. 操作指引:用非技术语言(“双击打开”"按 Alt+F11"“右键选择性粘贴”)逐步引导用户完成操作
  3. 容错兜底:在提供的代码中内置 IFERROROn Error Resume Nexttry-except 等容错机制,防止用户因报错卡住

1.3 核心交互流程

应用的交互链路设计为极简的四步流程:

  1. 用户在文本框中用自然语言描述数据处理任务
  2. 选择数据量规模(小于/大于1万行)和数据格式(Excel/CSV/数据库)
  3. 点击"生成方案"按钮
  4. 系统输出方案类型标签、方案概述、分步骤操作指南(含可展开代码块)、容错处理说明和注意事项

整个流程设计遵循"输入最少、输出最快、步骤最少"的原则,用户无需注册登录,打开即可使用。


二、整体架构设计:MVVM 分层模式

2.1 为什么选择 MVVM

在 HarmonyOS 应用开发中,选择合适的架构模式至关重要。本应用采用 MVVM(Model-View-ViewModel)架构的变体——Model-Service-View 三层结构,原因如下:

  1. 数据模型独立:方案类型、步骤信息、请求参数等数据结构需要被 View 层和 Service 层共同引用,独立的 Model 层保证类型一致性
  2. 业务逻辑解耦:AI 调用、Prompt 构建、响应解析、Mock 数据生成等逻辑集中在 Service 层,UI 层不掺杂业务判断
  3. UI 声明式渲染:ArkTS 的声明式 UI 天然适合 View 层,通过 @State 和 @Observed 装饰器实现数据驱动的自动更新
  4. 可测试性:Service 层的纯逻辑方法可以独立于 UI 进行测试

2.2 目录结构

entry/src/main/ets/
├── models/          # 数据模型层(Model)
│   └── OfficeModel.ets     # 枚举类型、@Observed 数据类
├── services/        # 业务逻辑层(Service/ViewModel)
│   └── OfficeService.ets   # Prompt 管理、AI 调用、响应解析、Mock 数据
├── common/          # 公共常量
│   └── Constants.ets       # 颜色、文案、配置常量
└── pages/           # 视图层(View)
    └── Index.ets           # 主页面,@Builder 组织 UI 碎片

三层之间的依赖关系严格遵循单向依赖:View → Service → Model,Model 不依赖任何层,Service 仅依赖 Model,View 依赖 Service 和 Model。

2.3 三层职责划分

层级 文件 职责 关键技术
Model OfficeModel.ets 定义数据结构和类型枚举 enum、@Observed class
Service OfficeService.ets Prompt 工程、方案生成、响应解析 JSON 解析、Mock 降级
Common Constants.ets 集中管理颜色和文案常量 static readonly
View Index.ets UI 渲染、用户交互、状态管理 @State、@Builder、ForEach、Flex

三、Model 层:类型安全的数据建模

3.1 枚举类型定义方案边界

办公自动化场景中,方案类型、数据规模、数据格式都是有限集合,非常适合用 TypeScript 的 enum 进行建模。相比字符串字面量,枚举提供了编译期类型检查和自动补全支持。

export enum SolutionType {
  EXCEL_FUNCTION = 'Excel 函数方案',
  VBA_MACRO = 'VBA 一键宏',
  PYTHON_SCRIPT = 'Python 脚本'
}

export enum DataScale {
  SMALL = '小于1万行',
  LARGE = '大于1万行'
}

export enum DataType {
  EXCEL = 'Excel 表格',
  CSV = 'CSV 文件',
  DATABASE = '数据库'
}

这里有一个关键的设计决策:枚举值使用中文而非英文。这是因为这些枚举值会直接显示在 UI 上(方案类型标签),使用中文避免了额外的映射逻辑。同时,在 Prompt 中也直接使用这些中文值,保证了从用户选择到 Prompt 构建到结果展示的全链路一致性。

3.2 @Observed 装饰器实现深度响应

在 ArkTS 中,普通的 class 对象不会触发 UI 自动更新。要让对象属性的变化能够驱动 UI 重新渲染,需要使用 @Observed 装饰器标记类:

@Observed
export class StepInfo {
  stepNumber: number = 0
  title: string = ''
  description: string = ''
  codeSnippet: string = ''

  constructor(stepNumber: number, title: string, description: string, codeSnippet: string) {
    this.stepNumber = stepNumber
    this.title = title
    this.description = description
    this.codeSnippet = codeSnippet
  }
}

@Observed
export class SolutionResult {
  solutionType: SolutionType = SolutionType.EXCEL_FUNCTION
  overview: string = ''
  steps: StepInfo[] = []
  errorHandling: string = ''
  note: string = ''

  constructor(
    solutionType: SolutionType,
    overview: string,
    steps: StepInfo[],
    errorHandling: string,
    note: string
  ) {
    this.solutionType = solutionType
    this.overview = overview
    this.steps = steps
    this.errorHandling = errorHandling
    this.note = note
  }
}

技术要点解析:

@Observed 是 HarmonyOS 状态管理体系中的核心装饰器之一。它的作用是让被装饰的类实例具备"可观察"能力——当实例的属性发生变化时,所有使用了该属性的 UI 组件会自动重新渲染。与 @State 不同的是,@Observed 可以观察嵌套对象的深层属性变化。例如,当 SolutionResult 中的 steps 数组中某个 StepInfocodeSnippet 被修改时,UI 能够正确感知并更新。

在本应用中,SolutionResult 对象通过 @State result 持有,而 StepInfo 数组是 SolutionResult 的嵌套属性。通过 @Observed 标记 StepInfo 类,确保了步骤卡片的展开/收起状态能够正确触发 UI 更新。

3.3 请求对象与辅助类

export class TaskRequest {
  description: string = ''
  dataScale: DataScale = DataScale.SMALL
  dataType: DataType = DataType.EXCEL
}

export class CategoryOption {
  label: string
  value: string
  selected: boolean

  constructor(label: string, value: string, selected: boolean) {
    this.label = label
    this.value = value
    this.selected = selected
  }
}

TaskRequest 是一个纯数据传输对象(DTO),用于在 View 层和 Service 层之间传递用户输入。它没有使用 @Observed 装饰器,因为它不需要被 UI 直接观察——它只在用户点击"生成方案"时被创建一次,传递给 Service 层使用。


四、Service 层:Prompt 工程与方案生成

4.1 防御性 Prompt 设计

AI 应用的核心在于 Prompt 设计。本应用的 Prompt 采用了角色定义 + 任务指令 + 输出格式三段式结构:

private systemPrompt: string =
  `# Role: 办公自动化专家
你精通 Excel 函数、VBA 宏和 Python Pandas。
# Task: 生成傻瓜式解决方案
用户会描述数据处理中的"脏活累活"。
1. **方案判断**:如果数据量小于1万行,提供【Excel函数组合方案】(如 Index+Match);
   如果数据量大,提供【VBA一键宏代码】或【Python脚本】。
2. **操作演示**:使用 "Step-by-Step" 口头禅,用非技术语言告诉用户点击哪里、粘贴什么。
3. **容错处理**:必须在代码中加入 On Error Resume Next 或 try-except,防止用户因报错卡住。

# Output Format:
请用以下JSON格式输出(不要输出任何其他内容):
{
  "solutionType": "Excel 函数方案" | "VBA 一键宏" | "Python 脚本",
  "overview": "方案概述",
  "steps": [
    { "stepNumber": 1, "title": "步骤标题", "description": "详细操作说明",
      "codeSnippet": "代码片段" }
  ],
  "errorHandling": "容错处理说明",
  "note": "注意事项"
}`

Prompt 设计的几个关键考量:

  1. 角色锚定:开头明确 “Role: 办公自动化专家”,为模型设定专业身份,避免输出过于泛化
  2. 决策规则明确:用数据量(1万行)作为方案选择的硬性分界线,避免模型在技术选型上产生歧义
  3. 语言约束:"Step-by-Step"口头禅和"非技术语言"的要求,确保输出面向非技术用户
  4. 容错强制:明确要求代码中必须包含容错机制,这是产品的核心差异化点
  5. 结构化输出:强制 JSON 格式输出,便于前端解析渲染,避免 Markdown 格式的不稳定性

4.2 User Prompt 动态构建

User Prompt 不是固定模板,而是根据用户选择动态拼接的:

private buildUserPrompt(request: TaskRequest): string {
  const scaleText = request.dataScale === DataScale.SMALL ?
    '数据量小于1万行,请优先使用 Excel 函数组合方案' :
    '数据量大于1万行,请提供 VBA 宏或 Python 脚本方案'
  const typeText = `数据格式:${request.dataType}`
  const descText = `任务描述:${request.description}`

  return `${scaleText}\n${typeText}\n${descText}`
}

这种动态构建的好处是:

  • 数据规模信息作为"引导信号"放在 Prompt 最前面,利用大模型对开头内容注意力权重更高的特性
  • 数据格式信息帮助模型判断输出 Excel 方案还是 CSV/Python 方案
  • 任务描述作为核心需求放在最后,符合大模型"近因效应"

4.3 方案类型自动判定

private determineSolutionType(dataScale: DataScale): SolutionType {
  if (dataScale === DataScale.SMALL) {
    return SolutionType.EXCEL_FUNCTION
  }
  return SolutionType.VBA_MACRO
}

这里在 Service 层做了一次确定性的方案类型判定,作为 AI 返回结果解析失败时的 fallback 值。这种"确定性逻辑 + AI 智能"的混合模式是 AI 应用开发中的常见实践——能用代码确定判断的,就不依赖 AI 判断。

4.4 JSON 响应解析与类型安全

大模型返回的 JSON 字符串需要被解析为强类型的 SolutionResult 对象。由于 AI 输出存在不确定性(可能返回非 JSON 内容、字段缺失、类型错误等),解析过程必须具备容错能力:

private parseResponse(responseText: string, fallbackType: SolutionType): SolutionResult {
  try {
    const json = JSON.parse(responseText) as Record<string, Object>
    const solutionTypeStr = json['solutionType'] as string
    let solutionType = fallbackType
    if (solutionTypeStr === 'VBA 一键宏') {
      solutionType = SolutionType.VBA_MACRO
    } else if (solutionTypeStr === 'Python 脚本') {
      solutionType = SolutionType.PYTHON_SCRIPT
    }

    const overview = json['overview'] as string
    const stepsRaw = json['steps'] as Array<Record<string, Object>>
    const steps: StepInfo[] = []
    if (stepsRaw) {
      for (let i = 0; i < stepsRaw.length; i++) {
        steps.push(new StepInfo(
          stepsRaw[i]['stepNumber'] as number,
          stepsRaw[i]['title'] as string,
          stepsRaw[i]['description'] as string,
          stepsRaw[i]['codeSnippet'] as string
        ))
      }
    }

    const errorHandling = json['errorHandling'] as string
    const note = json['note'] as string

    return new SolutionResult(solutionType, overview, steps, errorHandling, note)
  } catch (e) {
    return new SolutionResult(fallbackType, '', [], '', '')
  }
}

解析策略说明:

  • 使用 try-catch 包裹整个解析过程,任何 JSON 解析错误或字段访问异常都会被捕获
  • solutionType 解析时设置了 fallbackType 作为默认值,即使用户返回的方案类型字符串不匹配,也不会导致解析失败
  • steps 数组遍历前先做非空判断,防止 null/undefined 导致运行时错误
  • 每个字段通过 as 关键字做类型断言,在 ArkTS 严格模式下满足类型检查要求

4.5 Mock 数据降级策略

在网络不可用、API 调用失败、或 API Key 未配置的情况下,应用需要能够正常展示演示功能。buildMockSolution 方法内置了三种方案的完整示例数据:

private buildMockSolution(request: TaskRequest): SolutionResult {
  const isSmall = request.dataScale === DataScale.SMALL

  if (isSmall) {
    return new SolutionResult(
      SolutionType.EXCEL_FUNCTION,
      '使用 VLOOKUP + IFERROR 函数组合,在 Excel 中直接完成数据匹配,无需编写代码。',
      [
        new StepInfo(1, '打开 Excel 表格',
          'Step-by-Step:双击打开你的 Excel 文件。确保两个表格在同一个工作簿中...', ''),
        new StepInfo(2, '确定匹配列和取值列',
          'Step-by-Step:找到两个表格中相同的那一列(比如"客户ID"或"订单号")...', ''),
        new StepInfo(3, '在新列中输入 VLOOKUP 公式',
          'Step-by-Step:在 Sheet1 中需要显示结果的第一个单元格...',
          '=IFERROR(VLOOKUP(A2, Sheet2!A:B, 2, FALSE), "未找到")'),
        // ... 更多步骤
      ],
      'IFERROR 函数的作法是:当 VLOOKUP 找不到匹配项时,不会显示 #N/A 错误...',
      '1. VLOOKUP 的第一个参数必须是两个表格都有的"共同列"\n2. ...'
    )
  }
  // VBA 和 Python 方案类似...
}

Mock 数据的价值不仅在于开发调试,更在于产品体验的"保底"——即使 AI 服务不可用,用户仍然能看到完整的功能演示,理解应用的价值。

值得注意的是,三种 Mock 方案中的代码都内置了容错处理:

  • Excel 方案:IFERROR(VLOOKUP(...), "未找到")
  • VBA 方案:On Error Resume Next
  • Python 方案:try-except FileNotFoundError / Exception

这与 Prompt 中对 AI 的容错要求保持一致,确保了产品体验的统一性。


五、常量层:集中式配置管理

5.1 颜色系统设计

export class AppConstants {
  static readonly COLOR_PRIMARY: string = '#2563EB'
  static readonly COLOR_PRIMARY_BG: string = '#EFF6FF'
  static readonly COLOR_EXCEL: string = '#16A34A'
  static readonly COLOR_EXCEL_BG: string = '#F0FDF4'
  static readonly COLOR_VBA: string = '#CA8A04'
  static readonly COLOR_VBA_BG: string = '#FEFCE8'
  static readonly COLOR_PYTHON: string = '#7C3AED'
  static readonly COLOR_PYTHON_BG: string = '#F5F3FF'
  static readonly COLOR_BG: string = '#F8FAFC'
  static readonly COLOR_CARD: string = '#FFFFFF'
  static readonly COLOR_TEXT: string = '#0F172A'
  static readonly COLOR_TEXT_SECONDARY: string = '#64748B'
  static readonly COLOR_BORDER: string = '#E2E8F0'
  static readonly COLOR_CODE_BG: string = '#1E293B'
}

颜色系统采用了一套精心设计的语义化配色方案:

颜色用途 主色 背景色 语义
主题色 #2563EB(蓝) #EFF6FF(浅蓝) 主要按钮、步骤标签、信息提示
Excel 方案 #16A34A(绿) #F0FDF4(浅绿) Excel 函数方案标签
VBA 方案 #CA8A04(黄) #FEFCE8(浅黄) VBA 宏方案标签
Python 方案 #7C3AED(紫) #F5F3FF(浅紫) Python 脚本方案标签
代码块 #E2E8F0(亮字) #1E293B(深底) 暗色主题代码展示

三种方案类型分别使用绿、黄、紫三色区分,形成视觉上的快速识别。这种"颜色编码"设计让用户一眼就能判断出当前方案的技术栈类型,无需仔细阅读文字。

5.2 文案常量集中管理

static readonly APP_TITLE: string = '办公自动化助手'
static readonly APP_SUBTITLE: string = '描述你的数据处理任务,一键生成傻瓜式解决方案'
static readonly LABEL_DESCRIPTION: string = '任务描述'
static readonly PLACEHOLDER_DESCRIPTION: string = '例如:需要把两个表格中相同客户ID的订单金额合并到一起...'
static readonly BTN_GENERATE: string = '生成方案'
static readonly BTN_GENERATING: string = '生成中...'
static readonly BTN_REGENERATE: string = '重新生成'
static readonly EMPTY_WARNING: string = '请描述你的数据处理任务'

将所有用户可见的文案集中在 Constants 类中有三个好处:

  1. 一致性:相同的文案不会出现不同版本
  2. 可维护性:修改文案不需要在多个文件中查找替换
  3. 国际化友好:未来如果需要支持多语言,只需要替换常量值即可

六、View 层:声明式 UI 与 @Builder 模式

6.1 状态管理:@State 驱动的响应式 UI

主页面的状态变量声明如下:

@Entry
@Component
struct Index {
  @State taskDescription: string = ''
  @State dataScale: DataScale = DataScale.SMALL
  @State dataType: DataType = DataType.EXCEL
  @State isGenerating: boolean = false
  @State hasResult: boolean = false
  @State result: SolutionResult = new SolutionResult(SolutionType.EXCEL_FUNCTION, '', [], '', '')
  @State expandedSteps: boolean[] = [false, false, false, false, false, false]
  private service: OfficeService = new OfficeService()
  private scroller: Scroller = new Scroller()

每个 @State 变量的职责明确:

状态变量 类型 职责 触发的 UI 更新
taskDescription string 用户输入的任务描述文本 TextArea 内容绑定
dataScale DataScale 数据量选择 标签选中状态、方案类型判定
dataType DataType 数据格式选择 标签选中状态、方案类型判定
isGenerating boolean 是否正在生成中 按钮文字/颜色/可点击状态
hasResult boolean 是否已有结果 结果区域显示/隐藏
result SolutionResult 生成的方案结果 整个结果区域的内容渲染
expandedSteps boolean[] 各步骤代码块的展开状态 代码块显示/隐藏、"展开/收起"文字

关键设计点:

  • isGenerating 同时控制按钮的三个属性:文字(“生成中…”)、颜色(灰色)、可点击状态(禁用),防止用户重复点击
  • hasResult 作为条件渲染开关,控制结果区域和"重新生成"按钮的显示
  • expandedSteps 使用布尔数组独立管理每个步骤卡片的展开状态,最多支持6个步骤

6.2 整体布局结构

build() {
  Column() {
    this.buildHeader()
    Scroll(this.scroller) {
      Column() {
        this.buildInputCard()
        if (this.hasResult) {
          this.buildResultSection()
        }
      }.padding({ left: 16, right: 16, bottom: 32 })
    }
    .layoutWeight(1)
    .scrollBar(BarState.Off)
    .edgeEffect(EdgeEffect.Spring)
  }
  .width('100%')
  .height('100%')
  .backgroundColor(AppConstants.COLOR_BG)
}

布局采用经典的"固定头部 + 可滚动内容区"模式:

  1. Header(固定):应用标题和副标题,始终可见
  2. Scroll 区域(可滚动):包含输入卡片和结果区域,使用 layoutWeight(1) 占据剩余空间
  3. ScrollBar 隐藏:设置 scrollBar(BarState.Off) 隐藏滚动条,提供更干净的视觉效果
  4. Spring 回弹效果:设置 edgeEffect(EdgeEffect.Spring) 实现 iOS 风格的弹性滚动回弹
  5. 条件渲染if (this.hasResult) 控制结果区域的显示,初始状态只显示输入卡片

6.3 @Builder 模式:UI 碎片的组件化组织

ArkTS 的 @Builder 装饰器允许将 UI 片段定义为方法,在 build 函数中像调用组件一样引用。这是一种轻量级的组件化方案——不需要创建独立的 .ets 文件,就能将复杂页面拆分为多个逻辑独立的 UI 块。

本应用共使用了 9 个 @Builder 方法,将页面拆分为独立的 UI 碎片:

buildHeader()           —— 页面头部(标题+副标题)
buildInputCard()        —— 输入卡片(文本框+选择器+按钮)
buildSelectorRow()      —— 选择器行(标签+选项组)—— 可复用
buildResultSection()    —— 结果区域容器
buildSolutionBadge()    —— 方案类型标签
buildOverview()         —— 方案概述文本
buildStepsSection()     —— 步骤列表容器
buildStepCard()         —— 单个步骤卡片 —— ForEach 循环渲染
buildErrorHandling()    —— 容错处理提示卡片
buildNote()             —— 注意事项提示卡片

6.4 输入卡片:TextArea + 标签选择器

输入区域是用户与应用交互的核心入口。设计上采用了卡片式布局,将所有输入元素整合在一个白色圆角卡片中:

@Builder
buildInputCard() {
  Column() {
    Text(AppConstants.LABEL_DESCRIPTION)
      .fontSize(14)
      .fontWeight(FontWeight.Medium)
      .fontColor(AppConstants.COLOR_TEXT)
      .margin({ bottom: 8 })
      .alignSelf(ItemAlign.Start)

    TextArea({ text: this.taskDescription, placeholder: AppConstants.PLACEHOLDER_DESCRIPTION })
      .height(100)
      .fontSize(14)
      .backgroundColor(AppConstants.COLOR_BG)
      .borderRadius(10)
      .padding(12)
      .onChange((value: string) => {
        this.taskDescription = value
      })

    this.buildSelectorRow(AppConstants.LABEL_DATA_SCALE,
      ['小于1万行', '大于1万行'],
      this.dataScale === DataScale.SMALL ? 0 : 1,
      (index: number) => {
        this.dataScale = index === 0 ? DataScale.SMALL : DataScale.LARGE
      })

    this.buildSelectorRow(AppConstants.LABEL_DATA_TYPE,
      ['Excel 表格', 'CSV 文件', '数据库'],
      this.dataType === DataType.EXCEL ? 0 :
        (this.dataType === DataType.CSV ? 1 : 2),
      (index: number) => {
        if (index === 0) { this.dataType = DataType.EXCEL }
        else if (index === 1) { this.dataType = DataType.CSV }
        else { this.dataType = DataType.DATABASE }
      })

    Button(this.isGenerating ? AppConstants.BTN_GENERATING : AppConstants.BTN_GENERATE)
      .width('100%')
      .height(46)
      .fontSize(16)
      .backgroundColor(this.isGenerating ? '#94A3B8' : AppConstants.COLOR_PRIMARY)
      .enabled(!this.isGenerating)
      .onClick(() => { this.onGenerate() })
  }
  .width('100%')
  .padding(16)
  .backgroundColor(AppConstants.COLOR_CARD)
  .borderRadius(14)
  .margin({ top: 12 })
}

设计亮点:

  1. TextArea 双向绑定:通过 { text: this.taskDescription } 初始化内容,通过 onChange 回调更新状态,实现双向数据流
  2. Placeholder 引导:placeholder 文本给出具体示例(“需要把两个表格中相同客户ID的订单金额合并到一起…”),降低用户的输入门槛
  3. 标签选择器替代下拉框:数据量和数据格式使用可见的标签按钮而非传统下拉框,减少一次点击操作,选项一目了然

6.5 可复用的标签选择器:Flex 流式布局

标签选择器(buildSelectorRow)是一个可复用的 @Builder 方法,它接收标签文字、选项数组、当前选中索引和选择回调四个参数:

@Builder
buildSelectorRow(label: string, options: string[], selectedIndex: number,
                 onSelect: (index: number) => void) {
  Column() {
    Text(label)
      .fontSize(14)
      .fontWeight(FontWeight.Medium)
      .fontColor(AppConstants.COLOR_TEXT)
      .margin({ top: 14, bottom: 8 })
      .alignSelf(ItemAlign.Start)

    Flex({ wrap: FlexWrap.Wrap }) {
      ForEach(options, (option: string, index: number) => {
        Text(option)
          .fontSize(13)
          .fontColor(index === selectedIndex ? Color.White : AppConstants.COLOR_TEXT_SECONDARY)
          .backgroundColor(index === selectedIndex ?
            AppConstants.COLOR_PRIMARY : AppConstants.COLOR_BG)
          .borderRadius(20)
          .padding({ left: 16, right: 16, top: 7, bottom: 7 })
          .margin({ right: 10, bottom: 6 })
          .onClick(() => { onSelect(index) })
      }, (option: string, index: number) => `${index}`)
    }
  }
  .width('100%')
}

技术要点解析:

Flex 流式布局:使用 Flex({ wrap: FlexWrap.Wrap }) 实现自动换行的标签布局。当选项数量较多或屏幕宽度不足时,标签会自动换行排列,不会溢出屏幕。这里使用 Flex 而非 Row 的原因是 ArkTS 的 Row 组件不支持 flexWrap 属性——在之前的开发实践中我们发现了这个限制,Row 组件只能在水平方向排列子组件,超出部分会被截断。Flex 组件配合 FlexWrap.Wrap 才是实现流式标签布局的正确方式。

胶囊按钮设计:标签使用 20px 圆角(borderRadius(20)),配合左右 16px、上下 7px 的内边距,形成标准的"胶囊"形状。选中状态使用蓝色背景白色文字,未选中状态使用灰色背景灰色文字,视觉反馈明确。

间距处理:标签之间使用 margin({ right: 10, bottom: 6 }) 设置右侧和底部间距,而非 Flex 的 space 参数。这是因为 Flex 的 space 参数在 wrap 模式下对多行布局的间距控制不够精确,使用子组件自身 margin 是更可控的方案。

ForEach 唯一键:第三个参数 (option: string, index: number) => \${index}`` 为每个列表项提供唯一的 key,帮助 ArkUI 框架高效地进行虚拟 DOM diff。

6.6 方案类型徽章:颜色编码系统

@Builder
buildSolutionBadge() {
  Row() {
    Text(this.result.solutionType)
      .fontSize(13)
      .fontWeight(FontWeight.Medium)
      .fontColor(this.getSolutionColor())
      .backgroundColor(this.getSolutionBgColor())
      .borderRadius(6)
      .padding({ left: 12, right: 12, top: 5, bottom: 5 })
  }
  .width('100%')
  .margin({ top: 20 })
  .justifyContent(FlexAlign.Start)
}

方案类型徽章通过动态方法 getSolutionColor()getSolutionBgColor() 获取对应的颜色:

private getSolutionColor(): string {
  if (this.result.solutionType === SolutionType.EXCEL_FUNCTION) {
    return AppConstants.COLOR_EXCEL
  } else if (this.result.solutionType === SolutionType.VBA_MACRO) {
    return AppConstants.COLOR_VBA
  }
  return AppConstants.COLOR_PYTHON
}

Excel 方案显示绿色徽章,VBA 方案显示黄色徽章,Python 方案显示紫色徽章。颜色编码使用户在浏览结果时能快速建立"颜色-技术栈"的心理映射。

6.7 步骤卡片:可展开代码块的交互设计

步骤卡片是结果展示的核心组件,每个步骤包含编号标签、步骤标题、操作描述,以及可选的可展开代码块:

@Builder
buildStepCard(step: StepInfo, index: number) {
  Column() {
    Row() {
      Text(`Step ${step.stepNumber}`)
        .fontSize(12)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.White)
        .backgroundColor(AppConstants.COLOR_PRIMARY)
        .borderRadius(12)
        .padding({ left: 10, right: 10, top: 3, bottom: 3 })

      Text(step.title)
        .fontSize(15)
        .fontWeight(FontWeight.Medium)
        .fontColor(AppConstants.COLOR_TEXT)
        .margin({ left: 10 })
        .layoutWeight(1)

      if (step.codeSnippet !== '') {
        Text(this.expandedSteps[index] ? '收起' : '展开代码')
          .fontSize(12)
          .fontColor(AppConstants.COLOR_PRIMARY)
          .onClick(() => {
            this.expandedSteps[index] = !this.expandedSteps[index]
          })
      }
    }
    .width('100%')

    Text(step.description)
      .fontSize(13)
      .fontColor(AppConstants.COLOR_TEXT_SECONDARY)
      .lineHeight(20)
      .margin({ top: 8 })
      .width('100%')

    if (step.codeSnippet !== '' && this.expandedSteps[index]) {
      Text(step.codeSnippet)
        .fontSize(12)
        .fontColor('#E2E8F0')
        .backgroundColor(AppConstants.COLOR_CODE_BG)
        .borderRadius(8)
        .padding(12)
        .margin({ top: 8 })
        .width('100%')
        .fontFamily('monospace')
    }
  }
  .width('100%')
  .padding(14)
  .backgroundColor(AppConstants.COLOR_CARD)
  .borderRadius(10)
  .margin({ top: 8 })
}

交互设计解析:

  1. Step 编号标签:蓝色胶囊标签显示 “Step 1”、“Step 2” 等编号,提供清晰的视觉序列感。相比普通数字序号,胶囊标签更加突出和美观。

  2. 条件渲染的"展开代码"按钮:只有当步骤包含代码片段时(step.codeSnippet !== ''),才显示"展开代码"按钮。纯文字操作步骤(如"打开 Excel")不需要代码块,不显示该按钮。

  3. 展开/收起状态管理expandedSteps 数组按索引独立管理每个步骤的展开状态,点击"展开代码"/"收起"时切换对应索引的布尔值。

  4. 代码块暗色主题:代码块使用深色背景(#1E293B)+ 亮色文字(#E2E8F0)+ 等宽字体(fontFamily('monospace')),模拟 IDE 的代码编辑器风格,提升代码的可读性。

  5. layoutWeight 布局弹性:步骤标题使用 layoutWeight(1) 占据行内剩余空间,确保"展开代码"按钮始终靠右对齐。

6.8 SymbolGlyph 系统符号

在容错处理和注意事项两个提示卡片中,使用了 HarmonyOS 内置的 SymbolGlyph 系统符号图标:

SymbolGlyph($r('sys.symbol.exclamationmark_triangle_fill'))
  .fontSize(16)
  .fontColor(['#CA8A04'])

SymbolGlyph 使用要点:

  1. 资源引用方式:系统符号通过 $r('sys.symbol.xxx') 引用,格式为 sys.symbol. + 符号名称。符号名称使用蛇形命名法(snake_case),如 exclamationmark_triangle_fillinfo_circle

  2. fontColor 类型要求:SymbolGlyph 的 fontColor 属性接受 ResourceColor[](颜色数组)类型,而非单个颜色字符串。这是因为系统符号支持多层颜色渲染(类似 Apple 的 SF Symbols 的多色模式)。即使只需要单色,也需要传入数组形式,如 ['#CA8A04']。这是开发中容易踩坑的点——传入字符串会导致类型错误。

  3. 符号选择:容错处理使用 exclamationmark_triangle_fill(警告三角形),配合黄色配色方案;注意事项使用 info_circle(信息圆圈),配合蓝色配色方案。图标语义与卡片功能一一对应。

6.9 提示卡片:分色语义化设计

容错处理卡片和注意事项卡片采用了不同的配色方案,形成视觉区分:

容错处理卡片(黄色系警告风格):

@Builder
buildErrorHandling() {
  Column() {
    Row() {
      SymbolGlyph($r('sys.symbol.exclamationmark_triangle_fill'))
        .fontSize(16)
        .fontColor(['#CA8A04'])
      Text(AppConstants.SECTION_ERROR)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .margin({ left: 6 })
    }
    Text(this.result.errorHandling)
      .fontSize(13)
      .fontColor(AppConstants.COLOR_TEXT_SECONDARY)
      .lineHeight(20)
  }
  .width('100%')
  .padding(14)
  .backgroundColor('#FEFCE8')
  .borderRadius(10)
  .margin({ top: 16 })
  .border({ width: 1, color: '#FDE68A' })
}

注意事项卡片(蓝色系信息风格):

@Builder
buildNote() {
  Column() {
    Row() {
      SymbolGlyph($r('sys.symbol.info_circle'))
        .fontSize(16)
        .fontColor([AppConstants.COLOR_PRIMARY])
      Text(AppConstants.SECTION_NOTE)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .margin({ left: 6 })
    }
    Text(this.result.note)
      .fontSize(13)
      .fontColor(AppConstants.COLOR_TEXT_SECONDARY)
      .lineHeight(20)
  }
  .width('100%')
  .padding(14)
  .backgroundColor(AppConstants.COLOR_PRIMARY_BG)
  .borderRadius(10)
  .margin({ top: 12 })
  .border({ width: 1, color: '#BFDBFE' })
}

两张卡片的布局结构完全一致,但通过不同的背景色、边框色、图标色形成语义区分:黄色代表"警告/需要注意",蓝色代表"信息/补充说明"。边框使用比背景色深两个色阶的同色系颜色,增强卡片的边界感。

6.10 交互逻辑:输入验证与自动滚动

onGenerate 方法是核心的交互处理函数:

private onGenerate(): void {
  if (this.taskDescription.trim() === '') {
    AlertDialog.show({
      title: '提示',
      message: AppConstants.EMPTY_WARNING,
      confirm: {
        value: '确定',
        fontColor: AppConstants.COLOR_PRIMARY,
        action: () => {}
      }
    })
    return
  }

  this.isGenerating = true
  this.expandedSteps = [false, false, false, false, false, false]

  const request = new TaskRequest()
  request.description = this.taskDescription
  request.dataScale = this.dataScale
  request.dataType = this.dataType

  setTimeout(() => {
    this.result = this.service.generateSolutionSync(request)
    this.hasResult = true
    this.isGenerating = false
    this.scroller.scrollEdge(Edge.Bottom)
  }, 800)
}

交互流程分析:

  1. 输入验证:使用 trim() 检查任务描述是否为空。如果为空,弹出 AlertDialog 提示用户,直接 return 不执行后续逻辑。这里有一个 ArkTS API 的注意点:AlertDialog 的 confirm 参数必须包含 action 回调函数,即使是空函数 action: () => {},否则会报类型错误。

  2. 状态重置:生成开始时,将 isGenerating 设为 true(触发按钮状态更新),并将所有步骤的展开状态重置为关闭(防止上一次的展开状态影响新结果)。

  3. 模拟异步延迟:使用 setTimeout 模拟 800ms 的网络请求延迟。这个延迟有两个作用:一是给用户"正在思考"的感知,避免结果瞬间出现显得没有处理过程;二是为后续接入真实 AI API 留出异步接口。

  4. 结果更新:在 setTimeout 回调中,调用 Service 层生成方案,更新 resulthasResultisGenerating 三个状态变量。这些状态变化会自动触发 UI 重新渲染。

  5. 自动滚动定位:使用 this.scroller.scrollEdge(Edge.Bottom) 将 Scroll 组件自动滚动到底部,确保用户能看到新生成的结果。这是一个重要的 UX 细节——如果结果区域超出屏幕,用户不需要手动滚动就能看到输出。


七、关键技术亮点总结

7.1 ArkTS 严格模式下的类型安全

本项目完全遵循 ArkTS 严格模式的要求:

  • 禁止 any/unknown 类型:所有变量、参数、返回值都有明确的类型注解
  • 枚举替代字符串字面量:SolutionType、DataScale、DataType 使用 enum 定义,方案类型的取值在编译期确定
  • 对象字面量对应明确类:所有数据结构都有对应的 class 定义(StepInfo、SolutionResult、TaskRequest),不使用匿名对象类型
  • 类型断言:在 JSON 解析等边界处使用 as 关键字进行显式类型转换,满足类型检查器的要求
  • 显式导入导出:所有跨文件引用使用明确的 import/export 语句

7.2 @Builder 轻量级组件化

@Builder 是 HarmonyOS 声明式 UI 中非常实用的代码组织方式。相比创建独立的 @Component 子组件,@Builder 有以下优势:

  • 零样板代码:不需要创建新文件、不需要 @Component 装饰器、不需要定义 @Prop 输入参数
  • 直接访问父组件状态:@Builder 方法内可以直接访问父组件的 @State 变量和 private 方法,不需要属性传递
  • 参数传递灵活:可以接收任意类型和数量的参数(如 buildStepCard 接收 StepInfo 和 index)
  • 适合页面内聚的 UI 逻辑:当 UI 片段只在一个页面内使用时,@Builder 比独立组件更轻量

本应用将一个复杂的单页 UI 拆分为 9 个 @Builder 方法,每个方法职责单一、代码行数可控,主 build 函数简洁清晰。

7.3 防御性编程:多层容错设计

应用在多个层面实现了容错保护:

  1. 输入层:AlertDialog 验证空输入
  2. API 层:try-catch 包裹 AI 调用,失败时降级到 Mock 数据
  3. 解析层:try-catch 包裹 JSON 解析,解析失败返回空结果对象
  4. 代码层:生成的方案代码内置 IFERROR / On Error Resume Next / try-except
  5. UI 层:代码块按需展开,步骤展开状态独立管理,不会因为某一步骤的异常影响其他步骤

7.4 颜色语义化与视觉层次

应用的视觉设计遵循"信息分层"原则:

  • 背景层#F8FAFC 浅灰蓝,提供柔和的页面底色
  • 卡片层#FFFFFF 白色,14px 圆角,承载核心内容
  • 标签层:蓝色/绿色/黄色/紫色,通过色块快速传递分类信息
  • 文字层#0F172A 深黑(标题)→ #64748B 灰色(正文)→ 白色(反色文字),三级文字层次
  • 代码层#1E293B 深色背景,与普通内容形成强烈对比

7.5 Flex 流式布局的正确使用

在 HarmonyOS ArkTS 中实现标签/Chip 流式布局时,需要注意:

  • 不要使用 Row 组件:Row 不支持 wrap 属性,子组件超出宽度会被截断
  • 使用 Flex + FlexWrap.Wrap:这是实现自动换行的正确方式
  • 子组件间距用 margin:不要依赖 Flex 的 space 参数,在多行场景下使用子组件 margin 更可控
  • ForEach 设置唯一 key:使用 index 作为 key(对于静态列表足够),帮助框架优化渲染性能

八、项目文件清单与扩展方向

8.1 完整文件清单

文件 行数 职责
[OfficeModel.ets](file:///c:/Users/l/DevEcoStudioProjects/MyApplication/entry/src/main/ets/models/OfficeModel.ets) 72 数据模型定义,含3个枚举和4个类
[OfficeService.ets](file:///c:/Users/l/DevEcoStudioProjects/MyApplication/entry/src/main/ets/services/OfficeService.ets) 222 业务逻辑层,含Prompt管理、方案生成、Mock数据
[Constants.ets](file:///c:/Users/l/DevEcoStudioProjects/MyApplication/entry/src/main/ets/common/Constants.ets) 36 颜色、文案、配置常量集中管理
[Index.ets](file:///c:/Users/l/DevEcoStudioProjects/MyApplication/entry/src/main/ets/pages/Index.ets) 383 主页面,含9个@Builder方法和完整交互逻辑
合计 713 零编译错误,ArkTS 严格模式兼容

8.2 可扩展方向

  1. 接入真实 AI API:将 callAI 方法替换为真实的 HTTP 请求(如使用 HarmonyOS 的 @ohos.net.http 模块),对接大语言模型 API
  2. 历史记录功能:使用 @ohos.data.preferences 存储用户的历史查询记录,支持回看
  3. 方案收藏:用户可收藏常用的方案模板,一键复制代码
  4. 方案分享:生成的方案可导出为图片或文本,分享给同事
  5. 更多方案类型:扩展支持 Power Query、SQL 语句、WPS 宏等更多技术方案
  6. 语音输入:集成 HarmonyOS 语音识别能力,支持语音描述任务
  7. 多端适配:利用 HarmonyOS 一次开发多端部署能力,适配平板和折叠屏设备

九、结语

"办公自动化助手"是一个典型的 AI 工具类应用——它的核心价值不在于技术复杂度,而在于对用户痛点的精准把握和对交互体验的精细打磨。在技术实现层面,这个应用展示了 HarmonyOS 6.0 + ArkTS 在构建工具类应用时的完整最佳实践:

  • 架构层面:MVVM 分层确保代码职责清晰、易于维护和扩展
  • 状态管理层面:@State + @Observed 的组合实现了高效的响应式数据驱动
  • UI 层面:@Builder 模式将复杂页面拆解为可组合的声明式碎片
  • 交互层面:输入验证、加载状态、自动滚动、可展开代码块等细节打磨出流畅的用户体验
  • AI 层面:结构化 Prompt + JSON 输出约束 + Mock 降级构建了可靠的 AI 调用链路
  • 工程层面:常量集中管理、类型安全、多层容错确保代码质量和稳定性

从代码量看,整个应用仅 700 余行 ArkTS 代码,却实现了从输入到输出的完整 AI 工具链路。这得益于 ArkTS 声明式 UI 的高效表达能力和 HarmonyOS 框架提供的丰富组件库。对于希望入门 HarmonyOS AI 应用开发的开发者来说,这个项目是一个很好的起点——它涵盖了数据建模、状态管理、UI 构建、用户交互、AI 集成等应用开发的核心环节,代码量适中,结构清晰,零编译错误,可直接作为学习参考和二次开发的基础。

Logo

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

更多推荐