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



从零构建「财智管家」:HarmonyOS 个人理财与投资组合管理工具全栈实践
作者:duluo
平台:HarmonyOS Next (API 24) · ArkTS · DevEco Studio
源码仓库:WealthWise
目录
- 项目背景与设计理念
- 技术选型与架构设计
- UI 组件体系与视觉语言
- 数据层设计:类型系统与模拟数据
- 路由系统与页面导航
- 核心模块实现详解
- 6.1 资产全景与净值追踪
- 6.2 投资组合分析与 Canvas 自绘图表
- 6.3 预算管理与月度趋势
- 6.4 财务规划与复利计算器
- 6.5 记账流水与交易管理
- 6.6 报表洞察与财务健康评分
- 6.7 设置与安全
- ArkTS 语法陷阱与踩坑记录
- 性能优化与最佳实践
- 未来 roadmap
- 总结
1. 项目背景与设计理念
1.1 为什么选择个人理财方向
在移动互联网时代,个人财务管理工具的需求持续增长。根据 2025 年的一份调研报告,超过 68% 的智能手机用户使用至少一款财务管理 App,但市面上大多数产品存在三个核心痛点:
- 隐私焦虑:用户的银行流水、持仓数据上传到云端,数据主权不明确
- 功能割裂:记账 App 只管记账,投资 App 只看行情,缺少统一的资产全景视图
- 复杂度高:专业工具如 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、@Builder 和 ForEach 来实现动态列表和条件渲染。
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 对类型系统有严格要求——必须使用明确的 interface 或 type 关键字,不能使用对象字面量作为类型声明。我们在项目中定义了一套完整的数据类型体系:
// 账户
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,推荐使用 pushNamedRoute 或 push 替代。但为了向下兼容,我们在当前版本中继续使用 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);
}
关键技术点:
- 起始角度:从
-π/2(12 点钟方向)开始绘制,符合用户对饼图的常规认知 - 角度计算:每个扇区的角度 = (百分比 / 100) × 2π
- 甜甜圈效果:绘制完整饼图后,在中心覆盖一个白色圆形,中间显示总市值
- 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 超支预警系统
我们在三个层级实现了预算预警:
- 分类进度条颜色变化:
spent > budget * 90%时变为红色,> 70%时变为橙色 - 文字提示:超支分类下方显示 ⚠️ 预算即将超支
- 进度条数值:实时显示 已用 / 总额
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 搜索与筛选
实现了两级数据过滤:
- 类型筛选:全部 / 支出 / 收入,通过
filterType状态控制 - 文本搜索:对交易描述和分类进行模糊匹配
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 方法中使用 const 或 let 声明变量。需要把数据预处理逻辑放在 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 — 不能使用 any 或 unknown 类型
// ❌ 错误
ForEach(items, (item: any) => { ... })
// ✅ 正确:必须定义明确的 interface
ForEach(items, (item: ItemType) => { ... })
7.2 UI 构建区域限制
问题:在 build 方法的 UI 区域中,不能使用 const、let、var 等声明语句
// ❌ 错误
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 开发范式,积累了以下关键经验:
- 声明式 UI 的思维转变:从命令式的
setText()/setVisibility()到声明式的@State+if/else,需要彻底重构 UI 编程思维 - 类型系统的严格性:ArkTS 的类型系统比标准 TypeScript 更加严格,需要提前定义清晰的接口体系
- Canvas 自绘能力:HarmonyOS 的 Canvas API 功能完备,足以支撑复杂的金融图表需求
- 模拟数据驱动原型:在缺少后端的情况下,通过精心构造的模拟数据可以快速验证 UI 和交互设计
- 渐进式复杂度设计:从记账到投资分析,从简单到复杂,让不同层次的用户都能找到适合自己的功能
更多推荐


所有评论(0)