HarmonyOS 办公自动化助手开发实践:基于 ArkTS 的 AI 工具类应用架构全解
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万行)和数据格式(Excel/CSV/数据库),自动选择最合适的技术方案
- 操作指引:用非技术语言(“双击打开”"按 Alt+F11"“右键选择性粘贴”)逐步引导用户完成操作
- 容错兜底:在提供的代码中内置
IFERROR、On Error Resume Next、try-except等容错机制,防止用户因报错卡住
1.3 核心交互流程
应用的交互链路设计为极简的四步流程:
- 用户在文本框中用自然语言描述数据处理任务
- 选择数据量规模(小于/大于1万行)和数据格式(Excel/CSV/数据库)
- 点击"生成方案"按钮
- 系统输出方案类型标签、方案概述、分步骤操作指南(含可展开代码块)、容错处理说明和注意事项
整个流程设计遵循"输入最少、输出最快、步骤最少"的原则,用户无需注册登录,打开即可使用。
二、整体架构设计:MVVM 分层模式
2.1 为什么选择 MVVM
在 HarmonyOS 应用开发中,选择合适的架构模式至关重要。本应用采用 MVVM(Model-View-ViewModel)架构的变体——Model-Service-View 三层结构,原因如下:
- 数据模型独立:方案类型、步骤信息、请求参数等数据结构需要被 View 层和 Service 层共同引用,独立的 Model 层保证类型一致性
- 业务逻辑解耦:AI 调用、Prompt 构建、响应解析、Mock 数据生成等逻辑集中在 Service 层,UI 层不掺杂业务判断
- UI 声明式渲染:ArkTS 的声明式 UI 天然适合 View 层,通过 @State 和 @Observed 装饰器实现数据驱动的自动更新
- 可测试性: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 数组中某个 StepInfo 的 codeSnippet 被修改时,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 设计的几个关键考量:
- 角色锚定:开头明确 “Role: 办公自动化专家”,为模型设定专业身份,避免输出过于泛化
- 决策规则明确:用数据量(1万行)作为方案选择的硬性分界线,避免模型在技术选型上产生歧义
- 语言约束:"Step-by-Step"口头禅和"非技术语言"的要求,确保输出面向非技术用户
- 容错强制:明确要求代码中必须包含容错机制,这是产品的核心差异化点
- 结构化输出:强制 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 类中有三个好处:
- 一致性:相同的文案不会出现不同版本
- 可维护性:修改文案不需要在多个文件中查找替换
- 国际化友好:未来如果需要支持多语言,只需要替换常量值即可
六、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)
}
布局采用经典的"固定头部 + 可滚动内容区"模式:
- Header(固定):应用标题和副标题,始终可见
- Scroll 区域(可滚动):包含输入卡片和结果区域,使用
layoutWeight(1)占据剩余空间 - ScrollBar 隐藏:设置
scrollBar(BarState.Off)隐藏滚动条,提供更干净的视觉效果 - Spring 回弹效果:设置
edgeEffect(EdgeEffect.Spring)实现 iOS 风格的弹性滚动回弹 - 条件渲染:
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 })
}
设计亮点:
- TextArea 双向绑定:通过
{ text: this.taskDescription }初始化内容,通过onChange回调更新状态,实现双向数据流 - Placeholder 引导:placeholder 文本给出具体示例(“需要把两个表格中相同客户ID的订单金额合并到一起…”),降低用户的输入门槛
- 标签选择器替代下拉框:数据量和数据格式使用可见的标签按钮而非传统下拉框,减少一次点击操作,选项一目了然
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 })
}
交互设计解析:
-
Step 编号标签:蓝色胶囊标签显示 “Step 1”、“Step 2” 等编号,提供清晰的视觉序列感。相比普通数字序号,胶囊标签更加突出和美观。
-
条件渲染的"展开代码"按钮:只有当步骤包含代码片段时(
step.codeSnippet !== ''),才显示"展开代码"按钮。纯文字操作步骤(如"打开 Excel")不需要代码块,不显示该按钮。 -
展开/收起状态管理:
expandedSteps数组按索引独立管理每个步骤的展开状态,点击"展开代码"/"收起"时切换对应索引的布尔值。 -
代码块暗色主题:代码块使用深色背景(
#1E293B)+ 亮色文字(#E2E8F0)+ 等宽字体(fontFamily('monospace')),模拟 IDE 的代码编辑器风格,提升代码的可读性。 -
layoutWeight 布局弹性:步骤标题使用
layoutWeight(1)占据行内剩余空间,确保"展开代码"按钮始终靠右对齐。
6.8 SymbolGlyph 系统符号
在容错处理和注意事项两个提示卡片中,使用了 HarmonyOS 内置的 SymbolGlyph 系统符号图标:
SymbolGlyph($r('sys.symbol.exclamationmark_triangle_fill'))
.fontSize(16)
.fontColor(['#CA8A04'])
SymbolGlyph 使用要点:
-
资源引用方式:系统符号通过
$r('sys.symbol.xxx')引用,格式为sys.symbol.+ 符号名称。符号名称使用蛇形命名法(snake_case),如exclamationmark_triangle_fill、info_circle。 -
fontColor 类型要求:SymbolGlyph 的
fontColor属性接受ResourceColor[](颜色数组)类型,而非单个颜色字符串。这是因为系统符号支持多层颜色渲染(类似 Apple 的 SF Symbols 的多色模式)。即使只需要单色,也需要传入数组形式,如['#CA8A04']。这是开发中容易踩坑的点——传入字符串会导致类型错误。 -
符号选择:容错处理使用
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)
}
交互流程分析:
-
输入验证:使用
trim()检查任务描述是否为空。如果为空,弹出 AlertDialog 提示用户,直接 return 不执行后续逻辑。这里有一个 ArkTS API 的注意点:AlertDialog 的 confirm 参数必须包含action回调函数,即使是空函数action: () => {},否则会报类型错误。 -
状态重置:生成开始时,将
isGenerating设为 true(触发按钮状态更新),并将所有步骤的展开状态重置为关闭(防止上一次的展开状态影响新结果)。 -
模拟异步延迟:使用
setTimeout模拟 800ms 的网络请求延迟。这个延迟有两个作用:一是给用户"正在思考"的感知,避免结果瞬间出现显得没有处理过程;二是为后续接入真实 AI API 留出异步接口。 -
结果更新:在 setTimeout 回调中,调用 Service 层生成方案,更新
result、hasResult、isGenerating三个状态变量。这些状态变化会自动触发 UI 重新渲染。 -
自动滚动定位:使用
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 防御性编程:多层容错设计
应用在多个层面实现了容错保护:
- 输入层:AlertDialog 验证空输入
- API 层:try-catch 包裹 AI 调用,失败时降级到 Mock 数据
- 解析层:try-catch 包裹 JSON 解析,解析失败返回空结果对象
- 代码层:生成的方案代码内置 IFERROR / On Error Resume Next / try-except
- 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 可扩展方向
- 接入真实 AI API:将
callAI方法替换为真实的 HTTP 请求(如使用 HarmonyOS 的 @ohos.net.http 模块),对接大语言模型 API - 历史记录功能:使用 @ohos.data.preferences 存储用户的历史查询记录,支持回看
- 方案收藏:用户可收藏常用的方案模板,一键复制代码
- 方案分享:生成的方案可导出为图片或文本,分享给同事
- 更多方案类型:扩展支持 Power Query、SQL 语句、WPS 宏等更多技术方案
- 语音输入:集成 HarmonyOS 语音识别能力,支持语音描述任务
- 多端适配:利用 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 集成等应用开发的核心环节,代码量适中,结构清晰,零编译错误,可直接作为学习参考和二次开发的基础。
更多推荐


所有评论(0)