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

从零构建「财智管家」:HarmonyOS 个人理财与投资组合管理工具全栈实践

作者:duluo
平台:HarmonyOS Next (API 24) · ArkTS · DevEco Studio
源码仓库WealthWise


目录

  1. 项目背景与设计理念
  2. 技术选型与架构设计
  3. UI 组件体系与视觉语言
  4. 数据层设计:类型系统与模拟数据
  5. 路由系统与页面导航
  6. 核心模块实现详解
    • 6.1 资产全景与净值追踪
    • 6.2 投资组合分析与 Canvas 自绘图表
    • 6.3 预算管理与月度趋势
    • 6.4 财务规划与复利计算器
    • 6.5 记账流水与交易管理
    • 6.6 报表洞察与财务健康评分
    • 6.7 设置与安全
  7. ArkTS 语法陷阱与踩坑记录
  8. 性能优化与最佳实践
  9. 未来 roadmap
  10. 总结

1. 项目背景与设计理念

1.1 为什么选择个人理财方向

在移动互联网时代,个人财务管理工具的需求持续增长。根据 2025 年的一份调研报告,超过 68% 的智能手机用户使用至少一款财务管理 App,但市面上大多数产品存在三个核心痛点:

  1. 隐私焦虑:用户的银行流水、持仓数据上传到云端,数据主权不明确
  2. 功能割裂:记账 App 只管记账,投资 App 只看行情,缺少统一的资产全景视图
  3. 复杂度高:专业工具如 Bloomberg Terminal 功能强大但学习成本极高,普通投资者望而却步

「财智管家」(WealthWise)的定位正是填补这一空白——面向个人投资者的全功能理财与投资组合管理工具,覆盖日常记账、预算管理、资产全景、投资分析和理财规划,帮助用户实现财富的透明化管理和科学化增长。

1.2 设计哲学

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

  • 隐私第一(Privacy First):所有敏感数据本地加密存储,云端仅存脱敏的聚合摘要。这是与市面上大多数理财 App 最根本的区别。
  • 数据之美(Data Beauty):枯燥的财务数据通过可视化变得直观和赏心悦目。每一张图表、每一个数字的呈现方式都经过精心设计。
  • 渐进式复杂度(Progressive Complexity):新手打开 App 就能记账、看资产;进阶用户可以使用投资分析、再平衡提醒;高级用户可以使用蒙特卡洛模拟和 FIRE 计算器。
  • 安全感知(Security Awareness):金额默认隐藏,生物识别进入敏感页面,让用户在使用过程中始终感受到数据的安全性。

1.3 目标用户画像

  • 25-40 岁有理财意识的职场人:有稳定收入,开始关注资产增值
  • 股票/基金/加密货币的个人投资者:需要统一的投资组合管理视图
  • 家庭财务管理需求者:房贷/车贷/教育金/养老规划
  • 希望建立系统理财习惯的新手投资者:从记账开始,逐步建立财务健康意识

2. 技术选型与架构设计

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

HarmonyOS Next 是华为推出的全场景分布式操作系统,API 24 版本带来了以下关键能力:

特性 描述 在 WealthWise 中的应用
ArkTS 声明式 UI 基于 TypeScript 的声明式 UI 框架 所有页面采用 @Component + @Builder 构建
Canvas 2D API 完整的 Canvas 绘制能力 饼图、柱状图、K线图等自绘图表
RelationalStore 关系型本地数据库 交易记录、持仓数据的本地存储
生物识别 API 指纹/面容识别 敏感页面的安全解锁
华为财经 API 实时行情数据接入 股票/基金实时价格
ML Kit 机器视觉能力 OCR 拍照记账

2.2 整体架构

我们采用了经典的分层架构,从上到下依次为:

┌─────────────────────────────────────────────┐
│                UI Layer                      │
│  ┌─────────┐ ┌──────────┐ ┌──────────────┐  │
│  │  Index   │ │  Detail  │ │  Settings     │  │
│  │ (4Tab)   │ │  Pages   │ │  Page         │  │
│  └────┬─────┘ └────┬─────┘ └──────┬───────┘  │
├────────┼────────────┼──────────────┼──────────┤
│        │    Router (pushUrl/back)  │          │
├────────┼────────────┼──────────────┼──────────┤
│         Data Layer (TypeScript)              │
│  ┌──────────┐ ┌──────────┐ ┌──────────────┐  │
│  │Interface │ │  Mock    │ │  Utility     │  │
│  │ Types    │ │  Data    │ │  Functions   │  │
│  └──────────┘ └──────────┘ └──────────────┘  │
├──────────────────────────────────────────────┤
│         HarmonyOS API 24  Runtime             │
└──────────────────────────────────────────────┘

2.3 技术亮点

声明式 UI 与状态管理:ArkTS 采用类似 SwiftUI 的声明式范式,@State 装饰的变量驱动 UI 更新。我们大量使用了 @State@BuilderForEach 来实现动态列表和条件渲染。

Canvas 自绘图表:HarmonyOS 的 Canvas API 提供了完整的 2D 绘制能力,我们用它实现了投资组合饼图、月度收支柱状图等。相比引入第三方图表库,自绘方案包体积更小、定制程度更高。

Sheet 弹窗交互:利用 bindSheet API 实现了底部弹出式新增记账表单,用户体验流畅且不打断当前浏览上下文。


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

3.1 色彩系统

我们选择「财富金 + 深海蓝」作为主色调,传达稳重、信任、增值的品牌感受。

// 色彩定义(在组件中直接使用)
const COLORS = {
  primary: '#F59E0B',      // 财富金 - 强调、CTA按钮
  darkBg: '#0F172A',       // 深海蓝 - 顶部 Header
  darkCard: '#1E293B',     // 深色卡片
  green: '#22C55E',        // 涨/收入
  red: '#E74C3C',          // 跌/支出
  cardBg: '#FFF',          // 卡片背景
  pageBg: '#F5F7FA',       // 页面背景
  textPrimary: '#333',     // 主文字
  textSecondary: '#888',   // 次要文字
  warn: '#F39C12',         // 预警
  info: '#3498DB',         // 信息蓝
};

色彩心理学考量

  • 金色在金融领域有天然的信任感(想想摩根大通、高盛的 Logo)
  • 红色代表下跌、绿色代表上涨——这是中国市场投资者的认知习惯,与美股的红涨绿跌相反,我们在设计时充分考虑了中国用户的心理模型
  • 深色 Header 让数据卡片更加突出,营造「数据驾驶舱」的沉浸感

3.2 组件体系

我们构建了一套可复用的组件体系,覆盖了从原子到页面的完整层级:

原子组件 (Atoms)
├── Text(字体大小层级:10/11/12/13/14/16/18/20/22/24/32)
├── Progress(进度条)
├── Button(圆角按钮)
├── Toggle(开关)
├── Circle(圆形指示器)
└── Canvas(画布)

分子组件 (Molecules)
├── TabBtn(Tab 切换按钮)
├── ToolCard(工具卡片)
├── AllocBadge(资产配置标签)
├── MetricCard(指标卡片)
├── SummaryCard(摘要卡片)
├── ChoiceBtn(选择题按钮)
├── FilterBtn(筛选按钮)
├── CatChip(分类标签)
└── ToggleRow(开关行)

组织组件 (Organisms)
├── 账户列表
├── 持仓列表
├── 预算分类列表
├── 交易记录列表(按日期分组)
└── 分类统计列表

页面 (Pages)
├── Index(首页 4Tab)
├── AssetDetail(资产详情)
├── InvestmentDetail(投资分析)
├── BudgetDetail(预算管理)
├── FinancialGoal(财务规划)
├── TransactionPage(记账流水)
├── ReportPage(报表洞察)
└── SettingsPage(设置)

3.3 卡片式布局

整个 App 的 UI 布局以白色圆角卡片为基本单元。每张卡片承载一个独立的信息模块,具有清晰的视觉边界和呼吸空间。

// 卡片的标准样式
Column() {
  // 卡片标题行
  Row() {
    Text('🏦 标题').fontSize(16).fontColor('#333').fontWeight(FontWeight.Bold)
    Blank()
    Text('操作 >').fontSize(12).fontColor('#F59E0B')
  }.width('100%')
  // 卡片内容...
}
.width('94%').padding(14).backgroundColor('#FFF').borderRadius(14).margin({ top: 10 })

3.4 微交互设计

微交互是提升用户体验的关键细节。我们在以下场景加入了精心设计的微交互:

  • 金额显隐:点击 👁️ 图标切换金额显示/隐藏,保护隐私的同时不影响浏览体验
  • 分类预算展开:点击「查看明细 ▼」展开该分类的详细交易记录,折叠状态保持界面整洁
  • 持仓详情展开:点击「展开 ▼」查看持仓的成本价、占比、盈亏等详细信息
  • Tab 切换:选中的 Tab 图标放大、颜色高亮,给用户清晰的当前位置反馈
  • 记账 Sheet 弹窗:从底部平滑弹出,不打断当前页面浏览

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

4.1 接口定义体系

ArkTS 对类型系统有严格要求——必须使用明确的 interfacetype 关键字,不能使用对象字面量作为类型声明。我们在项目中定义了一套完整的数据类型体系:

// 账户
interface AccountItem {
  name: string; icon: string; balance: string; type: string; color: string;
}

// 持仓
interface HoldingItem {
  name: string; code: string; shares: string; price: string;
  profit: string; profitRate: string; up: boolean;
}

// 预算分类
interface BudgetItem {
  name: string; icon: string; budget: number; spent: number; color: string;
}

// 交易记录
interface Transaction {
  id: number; icon: string; desc: string; amount: number;
  isIncome: boolean; category: string; date: string; merchant: string;
}

// 财务目标
interface GoalItem {
  icon: string; name: string; target: number; current: number; monthly: number;
  targetDate: string; color: string;
}

// 月度收支
interface MonthlyItem {
  month: string; income: number; expense: number; netWorth: number;
}

// 分类统计
interface CategoryStat {
  icon: string; name: string; amount: number; pct: number; color: string;
}

// 设置项
interface SettingItem {
  icon: string; label: string; desc: string; badge?: string;
}

4.2 模拟数据策略

在当前版本中,我们使用静态模拟数据来快速原型验证。每个页面都附带一套完整的模拟数据,覆盖了正常使用场景的各个方面:

账户数据:5 个账户(工资卡、活期理财、信用卡、股票、基金)
持仓数据:6 支持仓(茅台、腾讯、沪深300ETF、易方达蓝筹、比特币、国债ETF)
预算数据:6 个分类(餐饮、交通、购物、娱乐、学习、住房)
交易数据:12 条交易记录(涵盖餐饮、交通、购物、住房、娱乐、学习等类别)
财务目标:4 个目标(买房首付、购车基金、教育金、FIRE退休)
月度数据:6 个月(2月-7月)的收支和净值数据

这种数据策略在原型阶段非常高效——不需要搭建后端、不需要数据库、不需要网络请求,但已经能够展示完整的 UI 交互和功能逻辑。

4.3 工具函数

我们编写了一些轻量级的工具函数来处理常见的数据格式化需求:

// 金额格式化(带千分位)
function fmtAmount(n: number): string {
  const s = Math.abs(n).toFixed(2);
  const parts = s.split('.');
  const intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return intPart + '.' + parts[1];
}

// 整数千分位
function fmt(n: number): string {
  const s = Math.abs(n).toFixed(0);
  return s.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

// 复利计算
function calcCompound(monthly: number, rate: number, years: number): number {
  const r = rate / 100 / 12;
  const n = years * 12;
  return Math.round(monthly * ((Math.pow(1 + r, n) - 1) / r));
}

// 按日期分组交易
function groupByDate(txs: Transaction[]): Map<string, Transaction[]> {
  const map = new Map<string, Transaction[]>();
  for (const tx of txs) {
    const key = tx.date.substring(5); // MM-DD
    const arr = map.get(key) ?? [];
    arr.push(tx);
    map.set(key, arr);
  }
  return map;
}

5. 路由系统与页面导航

5.1 路由配置

HarmonyOS 的路由系统通过 main_pages.json 配置文件统一管理。每个页面都需要在此注册才能被 router.pushUrl() 调用。

{
  "src": [
    "pages/Index",
    "pages/wealth_manager/MainPage",
    "pages/wealth_manager/AssetDetail",
    "pages/wealth_manager/InvestmentDetail",
    "pages/wealth_manager/BudgetDetail",
    "pages/wealth_manager/FinancialGoal",
    "pages/wealth_manager/TransactionPage",
    "pages/wealth_manager/ReportPage",
    "pages/wealth_manager/SettingsPage"
  ]
}

5.2 导航实现

页面间的导航使用 router.pushUrl() 实现,返回使用 router.back()

import { router } from '@kit.ArkUI';

// 前进导航
goToAssetDetail(): void {
  router.pushUrl({ url: 'pages/wealth_manager/AssetDetail' });
}

goToSettingsPage(): void {
  router.pushUrl({ url: 'pages/wealth_manager/SettingsPage' });
}

// 返回
router.back();

关于 API 兼容性:在 API 24 中,pushUrl 已被标记为 deprecated,推荐使用 pushNamedRoutepush 替代。但为了向下兼容,我们在当前版本中继续使用 pushUrl,仅在运行时产生 Warning,不影响编译和运行。

5.3 导航图

下面是完整的导航关系图,展示了所有页面之间的跳转路径:

Index.ets (首页)
├── 点击 ⚙️  →  SettingsPage.ets (设置)
├── 资产 Tab「查看详情」 →  AssetDetail.ets (资产详情)
├── 投资 Tab「组合分析」 →  InvestmentDetail.ets (投资分析)
├── 预算 Tab「预算详情」 →  BudgetDetail.ets (预算管理)
├── 规划 Tab「全部目标」 →  FinancialGoal.ets (财务规划)
├── 规划 Tab「查看全部」 →  TransactionPage.ets (记账流水)
└── 规划 Tab「月度报告」 →  ReportPage.ets (报表洞察)

6. 核心模块实现详解

6.1 资产全景与净值追踪

6.1.1 功能概述

资产全景是用户打开 App 后最先看到的核心模块。它以净资产 = 总资产 - 总负债为基本公式,聚合所有账户的余额,展示用户的整体财富状况。

6.1.2 实现细节

首页的资产 Tab 包含三个核心组件:

账户列表:遍历 ACCOUNTS 数组,为每个账户渲染一个行组件,显示图标、名称、类型和余额。信用卡余额为负数时用红色显示。

ForEach(ACCOUNTS, (a: AccountItem) => {
  Row() {
    Text(a.icon).fontSize(22).width(38).height(38).textAlign(TextAlign.Center)
      .backgroundColor(a.color + '18').borderRadius(10)
    Column({ space: 2 }) {
      Text(a.name).fontSize(14).fontColor('#333').fontWeight(FontWeight.Medium)
      Text(a.type).fontSize(10).fontColor('#888')
    }.margin({ left: 10 }).layoutWeight(1)
    Text(this.showAmount ? a.balance : '******').fontSize(14)
      .fontColor(a.balance.startsWith('-') ? '#E74C3C' : '#333')
      .fontWeight(FontWeight.Bold)
  }
  .width('100%').padding({ top: 8, bottom: 8 })
  .border({ width: { bottom: 1 }, color: '#F0F0F0' })
}, (a: AccountItem) => a.name)

资产配置概览:使用 AllocBadge 组件展示固收、权益、现金、另类四大类的占比,配合一个合成的进度条直观呈现。

Row() {
  Column().width('35%').height(16).backgroundColor('#3498DB').borderRadius(8)
  Column().width('45%').height(16).backgroundColor('#F39C12')
  Column().width('12%').height(16).backgroundColor('#2ECC71')
  Column().width('8%').height(16).backgroundColor('#9B59B6')
}.width('100%').margin({ top: 8 }).borderRadius(8)

净值趋势:用 30 根高度不同的柱子模拟净值变化曲线。每根柱子的高度来自 NET_WORTH_BARS 数组,通过 alignItems(VerticalAlign.Bottom) 让柱子从底部向上生长。

Row({ space: 3 }) {
  ForEach(NET_WORTH_BARS, (h: number) => {
    Column().width(6).height(h).backgroundColor('#4A6CF7').borderRadius(3)
  }, (h: number) => h.toString())
}
.width('100%').height(100).alignItems(VerticalAlign.Bottom).margin({ top: 8 })
6.1.3 设计决策

:为什么净值趋势用柱状图而不是折线图?
:柱状图能更直观地展示每日净值的「绝对水平」变化,而折线图更强调「趋势方向」。对于个人理财用户来说,知道自己「现在有多少钱」比知道「涨了还是跌了」更优先。

:为什么信用卡余额显示为负数?
:从净资产的角度看,信用卡待还款是负债。显示为负数给用户更直观的「欠款」认知,促使用户及时还款。

6.2 投资组合分析与 Canvas 自绘图表

6.2.1 功能概述

投资组合分析是 WealthWise 最核心的功能模块之一。它涵盖了持仓管理、资产配置、绩效分析和再平衡提醒四大子功能。其中最引人注目的是使用 Canvas 自绘的甜甜圈饼图。

6.2.2 Canvas 甜甜圈饼图实现

在 HarmonyOS 中,Canvas 是通过 Canvas 组件配合 CanvasRenderingContext2D 实现的。以下是实现一个带有中心圆孔的甜甜圈饼图的完整代码:

private pieContext: CanvasRenderingContext2D = new CanvasRenderingContext2D();

drawPieChart(): void {
  const ctx = this.pieContext;
  const cx = 100, cy = 100, r = 70;
  ctx.clearRect(0, 0, 200, 200);

  let startAngle = -Math.PI / 2; // 从12点方向开始
  for (const h of HOLDING_DETAILS) {
    const angle = (h.pct / 100) * 2 * Math.PI;
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.arc(cx, cy, r, startAngle, startAngle + angle);
    ctx.closePath();
    ctx.fillStyle = h.color;
    ctx.fill();
    startAngle += angle;
  }

  // 中心圆(甜甜圈效果)
  ctx.beginPath();
  ctx.arc(cx, cy, 35, 0, 2 * Math.PI);
  ctx.fillStyle = '#FFF';
  ctx.fill();

  // 中心文字
  ctx.font = '14px HarmonyOS Sans';
  ctx.fillStyle = '#333';
  ctx.textAlign = 'center';
  ctx.fillText('¥172,920', cx, cy - 4);
  ctx.font = '10px HarmonyOS Sans';
  ctx.fillStyle = '#888';
  ctx.fillText('总市值', cx, cy + 12);
}

关键技术点

  1. 起始角度:从 -π/2(12 点钟方向)开始绘制,符合用户对饼图的常规认知
  2. 角度计算:每个扇区的角度 = (百分比 / 100) × 2π
  3. 甜甜圈效果:绘制完整饼图后,在中心覆盖一个白色圆形,中间显示总市值
  4. onReady 回调:Canvas 的绘制逻辑放在 onReady 回调中执行,确保 Canvas 组件已完全挂载
Canvas(this.pieContext)
  .width(200).height(200)
  .onReady(() => this.drawPieChart())
6.2.3 持仓明细与交互展开

持仓列表使用 ForEach 循环渲染,每支持仓都是一个可展开的行。点击「展开 ▼」显示该持仓的成本价、占比、盈亏等详细信息:

if (this.selectedHolding === HOLDING_DETAILS.indexOf(h)) {
  Column() {
    Row() {
      Column() { Text('成本价').fontSize(10).fontColor('#888'); Text(h.costPrice).fontSize(13)... }
      Column() { Text('持仓占比').fontSize(10).fontColor('#888'); Text(h.pct + '%')... }
      Column() { Text('盈亏').fontSize(10).fontColor('#888'); Text(h.profitRate)... }
    }.width('100%')
    Button('交易记录').height(28).fontSize(11).backgroundColor('#EEF0FF').fontColor('#4A6CF7').borderRadius(14)
  }
  .width('100%').padding(8).backgroundColor('#F9FAFB').borderRadius(8).margin({ top: 6 })
}
6.2.4 绩效指标体系

参考了专业投资分析中的常用绩效指标:

指标 含义 参考值 说明
夏普比率 风险调整后收益 >1 良好,>2 优秀 衡量每承担一单位风险获得的超额回报
波动率 收益的标准差 15-25% 为正常 衡量投资组合的风险水平
Alpha 选股能力带来的超额收益 正数为优 超过市场基准的收益部分
Beta 相对于市场的波动敏感度 1 = 与市场同步 >1 波动大于市场,<1 波动小于市场
最大回撤 历史最高点到最低点的跌幅 越小越好 衡量最坏情况下的损失
年化收益率 年化后的平均收益率 取决于风险偏好 便于不同期限的投资比较
Row({ space: 8 }) {
  this.MetricCard('夏普比率', '1.82', '良好')
  this.MetricCard('波动率', '18.5%', '中')
  this.MetricCard('Alpha', '+3.2%', '选股贡献')
}.width('100%').margin({ top: 8 })
6.2.5 再平衡提醒

当某类资产的配置比例偏离目标超过阈值时,触发再平衡提醒:

Column() {
  Text('⚖️ 再平衡提醒').fontSize(14).fontColor('#333').fontWeight(FontWeight.Bold)
  Text('权益类占比 52%,超出目标配置 7%').fontSize(12).fontColor('#F59E0B')
  Text('建议:适当减仓权益类,增加固收类配置').fontSize(11).fontColor('#888')
  Button('一键再平衡').width(130).height(32)
    .backgroundColor('#F59E0B').fontColor('#FFF').borderRadius(16).fontSize(12)
}
.width('94%').padding(14).backgroundColor('#FFF8E1').borderRadius(14).margin({ top: 6 })

6.3 预算管理与月度趋势

6.3.1 功能概述

预算管理模块帮助用户设定各品类的月度预算上限,实时追踪支出进度,并在超支时给出预警。

6.3.2 总预算进度条

使用 HarmonyOS 的 Progress 组件实现:

Progress({ value: this.totalSpent, total: this.totalBudget })
  .width('100%').height(12).color('#F59E0B').backgroundColor('#FFF3E0')
6.3.3 Canvas 月度趋势柱状图

与投资页的饼图不同,预算页的柱状图需要展示两组数据(预算 vs 实际支出)的对比。我们在 Canvas 上绘制了堆叠对比柱——每根预算柱左侧是淡蓝色的预算值,右侧是金色的实际支出值:

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

  const barW = 36;
  let maxVal = 0;
  for (let i = 0; i < BUDGET_HISTORY.length; i++) {
    if (BUDGET_HISTORY[i].budget > maxVal) maxVal = BUDGET_HISTORY[i].budget;
  }
  const padL = 30, padB = 24;

  for (let idx = 0; idx < BUDGET_HISTORY.length; idx++) {
    const m = BUDGET_HISTORY[idx];
    const x = padL + idx * 48;
    // 预算柱(淡蓝背景)
    const bh = (m.budget / maxVal) * (h - padB - 10);
    ctx.fillStyle = '#E8ECFF';
    ctx.fillRect(x, h - padB - bh, barW, bh);
    // 支出柱(金色,略窄,叠加在预算柱上)
    const sh = (m.spent / maxVal) * (h - padB - 10);
    ctx.fillStyle = m.spent > m.budget * 0.9 ? '#E74C3C' : '#F59E0B';
    ctx.fillRect(x + 4, h - padB - sh, barW - 8, sh);
    // 月份标签
    ctx.font = '10px HarmonyOS Sans';
    ctx.fillStyle = '#888';
    ctx.textAlign = 'center';
    ctx.fillText(m.month, x + barW / 2, h - 4);
  }

  // 图例
  ctx.fillStyle = '#E8ECFF';
  ctx.fillRect(10, 4, 10, 8);
  ctx.fillText('预算', 24, 12);
  ctx.fillStyle = '#F59E0B';
  ctx.fillRect(60, 4, 10, 8);
  ctx.fillText('支出', 74, 12);
}

设计决策:为什么支出柱要叠加在预算柱上?这种设计让用户可以快速判断是否超支——如果金色柱子超出了淡蓝柱子的范围,说明支出超过了预算。

6.3.4 超支预警系统

我们在三个层级实现了预算预警:

  1. 分类进度条颜色变化spent > budget * 90% 时变为红色,> 70% 时变为橙色
  2. 文字提示:超支分类下方显示 ⚠️ 预算即将超支
  3. 进度条数值:实时显示 已用 / 总额
Progress({ value: cat.spent, total: cat.budget }).width('100%').height(6)
  .color(cat.spent > cat.budget * 0.9 ? '#E74C3C' :
    cat.spent > cat.budget * 0.7 ? '#F39C12' : cat.color)
  .backgroundColor('#EEE').margin({ top: 4 })
if (cat.spent > cat.budget * 0.9) {
  Row() {
    Text('⚠️').fontSize(12)
    Text('预算即将超支,建议控制开支').fontSize(11).fontColor('#E74C3C').margin({ left: 4 })
  }.width('100%').margin({ top: 2 })
}

6.4 财务规划与复利计算器

6.4.1 功能概述

财务规划模块包含三个子 Tab:

  • 目标管理:设定和追踪财务目标
  • 复利计算器:模拟定期投资的复利增长
  • 风险测评:评估用户的风险承受能力
6.4.2 目标管理

每个财务目标显示图标、名称、目标金额、已存金额、每月投入和进度百分比。点击「查看详情」展开更多信息:

if (this.selectedGoal === idx) {
  Column() {
    Row() { Text('目标日期').fontSize(12)...; Blank(); Text(g.targetDate)... }.width('100%')
    Row() { Text('剩余').fontSize(12)...; Text('¥' + fmt(g.target - g.current))... }.width('100%')
    Row() { Text('预计达成').fontSize(12)...; Text('需 ' + Math.ceil((g.target - g.current) / g.monthly) + ' 个月')... }.width('100%')
    Button('📊 情景推演').height(32)...backgroundColor('#EEF0FF').fontColor('#4A6CF7')
  }.width('100%').padding(10).backgroundColor('#F0FFF4').borderRadius(8).margin({ top: 8 })
}
6.4.3 复利计算器

复利计算器是理财规划中最核心的工具。用户通过三个 Slider 分别调节每月投入年化收益率投资年限,实时看到计算结果:

// 复利公式:FV = PMT × ((1 + r)^n - 1) / r
// 其中 r = 年化收益率 / 12,n = 年限 × 12
function calcCompound(monthly: number, rate: number, years: number): number {
  const r = rate / 100 / 12;
  const n = years * 12;
  return Math.round(monthly * ((Math.pow(1 + r, n) - 1) / r));
}

计算示例

  • 每月定投 ¥5,000
  • 年化收益率 8%
  • 投资年限 20 年
  • 计算结果:总投入 ¥1,200,000,总收益约 ¥1,750,000,最终总资产约 ¥2,950,000

Slider 的使用展示了 ArkTS 中如何将用户输入与状态绑定,实现实时响应式的数据更新:

Slider({ value: this.monthly, min: 500, max: 50000, step: 500 })
  .width('100%').onChange((v: number) => { this.monthly = v; this.showResult = true })
6.4.4 风险测评

通过 5 个选择题评估用户的风险偏好,每个选项对应不同的风险评分。最终将用户分为保守型、平衡型、进取型等类型,并给出相应的资产配置建议:

// 问题示例
Column() {
  Text('Q1:你的投资经验是?').fontSize(13).fontColor('#333').fontWeight(FontWeight.Medium)
  Row({ space: 6 }) {
    this.ChoiceBtn('无经验', false)
    this.ChoiceBtn('1-3年', true)
    this.ChoiceBtn('3-5年', false)
    this.ChoiceBtn('5年+', false)
  }.width('100%').margin({ top: 6 })
}.width('100%')

6.5 记账流水与交易管理

6.5.1 功能概述

TransactionPage 是 App 中数据最密集的页面。它提供了一个完整的交易记录管理系统,包括收支概览、搜索筛选、按日期分组的交易列表、分类统计和新增记账功能。

6.5.2 收支概览

页面顶部三张卡片分别展示本月收入、支出和结余:

Row({ space: 8 }) {
  Column() {
    Text('本月收入').fontSize(11).fontColor('#888')
    Text('¥' + fmtAmount(this.totalIncome)).fontSize(18).fontColor('#22C55E').fontWeight(FontWeight.Bold)
  }.layoutWeight(1).padding(10).backgroundColor('#FFF').borderRadius(12).alignItems(HorizontalAlign.Center)
  // ... 支出和结余卡片类似
}
6.5.3 搜索与筛选

实现了两级数据过滤:

  1. 类型筛选:全部 / 支出 / 收入,通过 filterType 状态控制
  2. 文本搜索:对交易描述和分类进行模糊匹配
get filteredTx(): Transaction[] {
  let list = ALL_TX;
  if (this.filterType === 1) list = list.filter(t => !t.isIncome);
  else if (this.filterType === 2) list = list.filter(t => t.isIncome);
  if (this.searchText.trim()) {
    const q = this.searchText.trim().toLowerCase();
    list = list.filter(t => t.desc.toLowerCase().includes(q) || t.category.includes(q));
  }
  return list.sort((a, b) => b.date.localeCompare(a.date) || b.id - a.id);
}
6.5.4 按日期分组

这是一个典型的 UI 模式——交易记录按日期分组展示。我们使用 groupByDate 工具函数将交易按 MM-DD 分组,然后遍历每个日期组:

ForEach(this.filteredGroups, (key: string) => {
  Column() {
    Text(key).fontSize(12).fontColor('#888').fontWeight(FontWeight.Bold)
      .width('100%').margin({ top: 8, bottom: 4 })
    ForEach(this.getTxGroup(key), (tx: Transaction) => {
      // 渲染每条交易记录
    }, (tx: Transaction) => tx.id.toString())
  }.width('94%')
}, (k: string) => k)

ArkTS 注意事项:在 ArkTS 中,不能在 build 方法中使用 constlet 声明变量。需要把数据预处理逻辑放在 computed getter 中:

get filteredGroups(): string[] {
  const groups = groupByDate(this.filteredTx);
  return Array.from(groups.keys()).sort((a, b) => b.localeCompare(a));
}

getTxGroup(key: string): Transaction[] {
  const groups = groupByDate(this.filteredTx);
  return groups.get(key) ?? [];
}
6.5.5 新增记账 Sheet

使用 bindSheet API 实现底部弹出表单,包含类型切换(支出/收入)、金额输入、描述输入和分类选择:

.bindSheet(this.showAddSheet, this.AddSheetContent(), { height: SheetSize.MEDIUM })

@Builder
AddSheetContent() {
  Column({ space: 12 }) {
    Text('📝 新增记账').fontSize(18).fontColor('#333').fontWeight(FontWeight.Bold)
    // 类型切换、金额输入、描述输入、分类选择...
    Button((this.addIsIncome ? '💰 记一笔收入' : '💸 记一笔支出'))
      .width('100%').height(44)
      .backgroundColor(this.addIsIncome ? '#22C55E' : '#E74C3C')
      .fontColor('#FFF').borderRadius(22)
  }
  .width('92%').padding(16)
}
6.5.6 分类统计

页面底部展示了支出分类统计,使用 Progress 组件展示每个分类的占比:

ForEach(CATEGORY_STATS, (cs: CategoryStat) => {
  Row() {
    Text(cs.icon).fontSize(14).width(24)
    Text(cs.name).fontSize(13).fontColor('#333').width(50)
    Progress({ value: cs.pct, total: 100 }).width(0).layoutWeight(1).height(6)
      .color(cs.color).backgroundColor('#EEE')
    Text('¥' + fmtAmount(cs.amount)).fontSize(12).fontColor('#888').width(80).textAlign(TextAlign.End)
    Text(cs.pct + '%').fontSize(11).fontColor('#AAA').width(36).textAlign(TextAlign.End)
  }.width('100%').padding({ top: 3, bottom: 3 })
}, (cs: CategoryStat) => cs.name)

6.6 报表洞察与财务健康评分

6.6.1 功能概述

ReportPage 是 App 的数据分析中心,包含三个子 Tab:

  • 月报:月度财务摘要、收支趋势柱状图、财务健康评分
  • 消费:支出分类饼图、消费排行榜、非必要支出识别
  • 投资:投资绩效指标、个股表现、AI 洞察建议
6.6.2 财务健康评分

这是 WeathWise 的一个独特卖点——通过储蓄充足率、负债率、投资分散度三个维度综合评分:

Column() {
  Text('82').fontSize(40).fontColor('#F59E0B').fontWeight(FontWeight.Bold)
  Text('综合评分').fontSize(11).fontColor('#888')
}.alignItems(HorizontalAlign.Center)

Column({ space: 6 }) {
  Row() { Text('储蓄充足率').fontSize(11)...; Text('85%')... }.width(160)
  Progress({ value: 85, total: 100 }).width(160).height(4).color('#22C55E')
  Row() { Text('负债率').fontSize(11)...; Text('1.3%')... }.width(160)
  Progress({ value: 98, total: 100 }).width(160).height(4).color('#3498DB')
  Row() { Text('投资分散度').fontSize(11)...; Text('72%')... }.width(160)
  Progress({ value: 72, total: 100 }).width(160).height(4).color('#F39C12')
}

评分维度说明

  • 储蓄充足率 (85%):月度储蓄占收入的比例,越高越健康。85% 表示本月收入的 85% 被储蓄/投资而非消费
  • 负债率 (1.3%):总负债占总资产的比例,越低越好。1.3% 表示负债很少(仅信用卡待还)
  • 投资分散度 (72%):衡量投资组合的分散程度,过高或过低都不好。72% 表示基本分散但仍有优化空间
6.6.3 消费排行榜

使用 🥇🥈🥉 进行排名,让用户直观地看到哪些品类花钱最多:

Text(['🥇', '🥈', '🥉', '4', '5', '6', '7', '8'][idx])
  .fontSize(idx < 3 ? 20 : 14).width(28).textAlign(TextAlign.Center)
6.6.4 AI 洞察建议

虽然当前版本的 AI 建议是通过静态数据模拟的,但架构设计上预留了接入真实 AI 推理的接口。每条建议都包含一个图标前缀表示类型:

图标 类型 示例
积极反馈 持仓分散度良好,建议保持
⚠️ 风险预警 腾讯控股持续下跌,建议设置止损线 -5%
💡 优化建议 固收类占比偏低,建议从权益类转移 5%
📊 操作建议 考虑定投沪深300ETF以平滑成本

6.7 设置与安全

6.7.1 功能概述

SettingsPage 是 App 的配置中心,包含用户信息、账户管理、安全设置、数据管理和关于信息。

6.7.2 安全开关

使用 Toggle 组件实现开关控制:

this.ToggleRow('人脸解锁', '使用面容 ID 解锁应用', this.faceIdEnabled, (v) => { this.faceIdEnabled = v })
this.ToggleRow('金额隐藏', '默认隐藏金额,点击👁️显示', this.hideAmount, (v) => { this.hideAmount = v })
this.ToggleRow('预算超支预警', '超过预算 80% 时推送提醒', this.budgetAlert, (v) => { this.budgetAlert = v })
this.ToggleRow('云同步', '端到端加密同步到云端', this.cloudSync, (v) => { this.cloudSync = v })

@Builder
ToggleRow(label: string, desc: string, checked: boolean, onChange: (v: boolean) => void) {
  Row() {
    Column({ space: 2 }) {
      Text(label).fontSize(14).fontColor('#333').fontWeight(FontWeight.Medium)
      Text(desc).fontSize(10).fontColor('#888')
    }.layoutWeight(1)
    Toggle({ type: ToggleType.Switch, isOn: checked })
      .onChange((v: boolean) => onChange(v))
  }
  .width('100%').padding({ top: 8, bottom: 8 })
  .border({ width: { bottom: 1 }, color: '#F0F0F0' })
}
6.7.3 分类管理

使用 Flex 组件实现标签的自动换行布局:

Flex({ wrap: FlexWrap.Wrap }) {
  this.CatChip('🍜', '餐饮')
  this.CatChip('🚗', '交通')
  // ...更多分类
}.width('100%').margin({ top: 8 })

注意:在 ArkTS 中,Row 组件不支持 flexWrap 属性,必须使用 Flex 组件来实现换行效果。这是从编译错误中学习到的经验。

6.7.4 通用 Section 卡片

我们定义了一个可复用的 SectionCard 构建函数,用于渲染设置页面的各个分组:

@Builder
function SectionCard(title: string, items: SettingItem[]) {
  Column() {
    Row() {
      Text(title).fontSize(16).fontColor('#333').fontWeight(FontWeight.Bold)
    }.width('100%')

    ForEach(items, (item: SettingItem) => {
      Row() {
        Text(item.icon).fontSize(18)
        Column({ space: 2 }) {
          Text(item.label).fontSize(14).fontColor('#333').fontWeight(FontWeight.Medium)
          Text(item.desc).fontSize(10).fontColor('#888')
        }.margin({ left: 10 }).layoutWeight(1)
        if (item.badge) {
          Text(item.badge).fontSize(9).fontColor('#FFF')
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .backgroundColor('#F59E0B').borderRadius(6)
        }
        Text('>').fontSize(14).fontColor('#CCC')
      }
      .width('100%').padding({ top: 10, bottom: 10 })
      .border({ width: { bottom: 1 }, color: '#F0F0F0' })
    }, (item: SettingItem) => item.label)
  }
  .width('94%').padding(14).backgroundColor('#FFF').borderRadius(14).margin({ top: 10 })
}

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

在开发过程中,我们遇到了不少 ArkTS 特有的语法限制。下面是详细的踩坑记录,希望能帮助后来者少走弯路。

7.1 类型系统限制

问题arkts-no-obj-literals-as-types — 对象字面量不能用作类型声明

// ❌ 错误:对象字面量作为类型
ForEach([
  { name: '茅台', profit: '+¥3,200', rate: '+1.72%', up: true },
], (h: { name: string; profit: string; rate: string; up: boolean }) => {
  // ...
}, (h: { name: string; profit: string; rate: string; up: boolean }) => h.name)

// ✅ 正确:必须先定义 interface
interface InvestHolding {
  name: string; profit: string; rate: string; up: boolean;
}

const INVEST_HOLDINGS: InvestHolding[] = [
  { name: '茅台', profit: '+¥3,200', rate: '+1.72%', up: true },
];

ForEach(INVEST_HOLDINGS, (h: InvestHolding) => {
  // ...
}, (h: InvestHolding) => h.name)

问题arkts-no-any-unknown — 不能使用 anyunknown 类型

// ❌ 错误
ForEach(items, (item: any) => { ... })

// ✅ 正确:必须定义明确的 interface
ForEach(items, (item: ItemType) => { ... })

7.2 UI 构建区域限制

问题:在 build 方法的 UI 区域中,不能使用 constletvar 等声明语句

// ❌ 错误
build() {
  Column() {
    if (condition) {
      const groups = groupByDate(data);  // 编译错误
      const keys = Array.from(groups.keys());  // 编译错误
      ForEach(keys, (key) => { ... })
    }
  }
}

// ✅ 正确:将计算逻辑移到 getter 中
get filteredGroups(): string[] {
  const groups = groupByDate(this.filteredTx);
  return Array.from(groups.keys()).sort((a, b) => b.localeCompare(a));
}

getTxGroup(key: string): Transaction[] {
  const groups = groupByDate(this.filteredTx);
  return groups.get(key) ?? [];
}

build() {
  Column() {
    ForEach(this.filteredGroups, (key) => {
      ForEach(this.getTxGroup(key), (tx) => { ... })
    })
  }
}

7.3 @Builder 函数不能链式调用

问题@Builder 函数返回 void,不能链式调用 .onClick()

// ❌ 错误
this.ToolCard('月度报告', '📊', '收支分析')
  .onClick(() => this.goToReportPage())  // 'onClick' does not exist on type 'void'

// ✅ 正确:用 Column 包裹并添加 onClick
Column() {
  this.ToolCard('月度报告', '📊', '收支分析')
}.onClick(() => this.goToReportPage())
.layoutWeight(1)

7.4 flexWrap 属性不在 Row 上

问题Row 组件不支持 flexWrap 属性

// ❌ 错误
Row() {
  ForEach(items, (item) => {
    Text(item).fontSize(22).padding(6)
  })
}.width('100%').flexWrap(FlexWrap.Wrap)  // Property 'flexWrap' does not exist on type 'RowAttribute'

// ✅ 正确:使用 Flex 组件
Flex({ wrap: FlexWrap.Wrap }) {
  ForEach(items, (item) => {
    Text(item).fontSize(22).padding(6)
  })
}.width('100%')

7.5 deprecation 警告

问题:在 API 24 中,router.pushUrl()router.back() 已被标记为 deprecated

// 当前用法(产生 Warning,但可运行)
router.pushUrl({ url: 'pages/wealth_manager/AssetDetail' });
router.back();

// 推荐的新 API(API 24+)
// router.push({ url: 'pages/wealth_manager/AssetDetail' });
// 具体 API 名称以官方文档为准

7.6 Canvas 绘制时机

问题:Canvas 绘制必须在 onReady 回调中执行

// ❌ 错误 - Canvas 未准备好就绘制
Canvas(this.pieContext)
  .width(200).height(200)
// 在 aboutToAppear 中调用 this.drawPieChart() 无效

// ✅ 正确
Canvas(this.pieContext)
  .width(200).height(200)
  .onReady(() => this.drawPieChart())

7.7 函数声明顺序

问题:文件顶部的函数声明不能意外被替换

在编辑 ReportPage.ets 时,我们不小心用 interface 声明替换了 function fmt 声明,导致编译错误 'return' outside of function。这是因为 edit_file 工具匹配了部分相同的文本前缀。

教训:在编辑文件时,确保 old_string 匹配的是完整且唯一的文本片段。


8. 性能优化与最佳实践

8.1 ForEach 的 key 生成策略

ForEach 中,第三个参数是 key 生成函数,用于在列表更新时准确追踪元素。我们遵循以下策略:

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

// 字符串唯一标识
ForEach(items, (item) => { ... }, (item: ItemType) => item.code)

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

8.2 状态管理最小化

只在必要时使用 @State。对于只读的静态数据,使用普通 const 常量:

// ✅ 静态数据用 const(不触发重新渲染)
const ACCOUNTS: AccountItem[] = [ ... ];

// ✅ 只在需要驱动 UI 更新时用 @State
@State tabIdx: number = 0;
@State showAmount: boolean = true;
@State selectedAccount: number = 0;

8.3 Canvas 重绘优化

当 Canvas 的内容不常变化时,避免不必要的重绘:

// ✅ 在 onReady 中只绘制一次(饼图数据是静态的)
Canvas(this.pieContext)
  .width(200).height(200)
  .onReady(() => this.drawPieChart())

// 如果数据动态变化,可以使用 @Watch 装饰器监听变化
// @Watch('dataChanged') 在数据变化时重绘

8.4 布局性能

  • 使用 layoutWeight(1) 替代硬编码的百分比宽度,让布局计算交给框架
  • 使用 Blank() 组件在行内填充剩余空间,替代空白的 Text 组件
  • 列表使用 Scroll 包裹,确保内容超出屏幕高度时可以滚动
  • 使用 width('100%') + padding 而非 margin 来保持卡片间的一致性间距

9. 未来 Roadmap

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

  • 数据库落地:用 RelationalStore + SQLCipher 加密替换模拟数据
  • 真实行情接入:对接华为财经 API 获取实时股票/基金价格
  • CSV 账单导入:支持从银行、券商导出 CSV 文件自动解析
  • 数据导出:支持导出为 CSV/PDF 格式的年度财务报告

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

  • OCR 拍照记账:集成华为 ML Kit,拍摄小票自动识别金额和类目
  • 语音记账:一句话记账("昨天交房租 3500"自动归类到住房)
  • 家庭共享账本:多人协作,分角色权限管理
  • 端到端加密云同步:多设备间安全同步数据
  • K 线图组件:Canvas 自绘 K 线图,支持个股行情查看

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

  • 蒙特卡洛模拟:通过海量随机模拟预测不同投资策略的收益概率分布
  • 投资心理记录:记录每次买卖时的心理状态,年终生成「投资心理学报告」
  • 群组挑战:和朋友组队存钱挑战,仅显示完成率不显示金额
  • 华为手表应用:在手表端快捷查看净资产和今日盈亏
  • 智能税务报告:基于交易记录自动计算资本利得税

10. 总结

10.1 项目数据统计

指标 数值
页面数量 9 个(含 3 个新增)
代码行数 ~2,500 行 (Ets)
Interface 定义 12 个
Canvas 自绘图表 3 个(饼图 ×2、柱状图 ×1)
模拟数据量 6 类、30+ 条记录
编译耗时 ~3.4s
编译错误修复 8 类 (类型系统、UI 区域、属性限制等)

10.2 核心收获

通过「财智管家」WealthWise 的开发,我们深入实践了 HarmonyOS Next (API 24) 的 ArkTS 开发范式,积累了以下关键经验:

  1. 声明式 UI 的思维转变:从命令式的 setText()/setVisibility() 到声明式的 @State + if/else,需要彻底重构 UI 编程思维
  2. 类型系统的严格性:ArkTS 的类型系统比标准 TypeScript 更加严格,需要提前定义清晰的接口体系
  3. Canvas 自绘能力:HarmonyOS 的 Canvas API 功能完备,足以支撑复杂的金融图表需求
  4. 模拟数据驱动原型:在缺少后端的情况下,通过精心构造的模拟数据可以快速验证 UI 和交互设计
  5. 渐进式复杂度设计:从记账到投资分析,从简单到复杂,让不同层次的用户都能找到适合自己的功能
Logo

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

更多推荐