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

实例:预算管理(Budget)|风格:进度面板

一、设计理念:预算页要「一眼知道花超没有」

预算管理的 UI 目标是财务仪表盘——顶部「总进度」(花了多少/剩多少/超支预警)+ 中部「分类进度条」(每类花到哪了)+ 底部「支出明细」(具体流水)。实例 17 的风格「进度面板」:深色总预算卡 + 分类进度条列表 + 支出明细,超支用红色高亮——「进度可视化 + 超支预警」是核心视觉

页面信息架构:

  1. 标题栏:预算管理 + 当前月份 + 刷新;
  2. 总预算进度卡(深色):总进度百分比 + 已花/总预算 + 进度条 + 剩余或超支文案;
  3. 分类预算明细:每分类进度条 + 「已花/预算」+ 超支标签;
  4. 分类支出明细:选中分类的流水(金额红字 + 日期备注 + 删除);
  5. 新增支出弹窗:金额 + 备注。

二、页面骨架与核心状态

@State budgets: BudgetWithSpend[] = [];
@State expenses: Expense[] = [];
@State budgetTotal: number = 0;
@State spendTotal: number = 0;
@State selectedCategory: string = '餐饮';
@State addVisible: boolean = false;
@State fAmount: string = '';
@State fNote: string = '';

核心状态:budgets(预算+进度)、expenses(选中分类明细)、budgetTotal/spendTotal(总预算/总支出)、selectedCategory(选中分类——决定明细列表)。

build 骨架(Scroll 包裹——页面内容多需滚动):

Column() {
  // 标题栏
  Scroll() {
    Column() {
      // 总预算进度卡(深色)
      // 分类预算明细(进度条列表)
      // 分类支出明细(流水)
    }
  }
  // 新增支出弹窗
}

三、总预算进度卡:深色仪表盘

顶部总进度卡用深色渐变(与页面浅色形成对比,突出仪表感):

Column() {
  Text('本月总支出进度').fontSize(13).fontColor('#9CA3AF')
  Stack({ alignContent: Alignment.Center }) {
    Row().width(110).height(110).borderRadius(55).backgroundColor('#1F2937')
    Column() {
      Text(`${this.totalPercent()}%`).fontSize(28).fontWeight(FontWeight.Bold).fontColor(Color.White)
      Text(`¥${this.spendTotal.toFixed(0)} / ¥${this.budgetTotal.toFixed(0)}`).fontSize(11).fontColor('#9CA3AF').margin({ top: 4 })
    }
  }
  // 进度条
  Row() {
    Row() {
      Row().width('100%').height(10).borderRadius(5)
        .backgroundColor(this.totalPercent() > 100 ? '#EF4444' : '#4ADE80')
    }
    .width(`${this.totalPercent()}%`)
    .height(10).borderRadius(5)
  }
  .width('80%').height(10).borderRadius(5).backgroundColor('#374151').margin({ top: 12 })
  Text(this.totalPercent() > 100 ? '⚠️ 已超支,请控制开支!' : `剩余可用 ¥${(this.budgetTotal - this.spendTotal).toFixed(2)}`)
    .fontSize(13).fontWeight(FontWeight.Medium)
    .fontColor(this.totalPercent() > 100 ? '#F87171' : '#4ADE80')
    .margin({ top: 10 })
}
.width('94%').padding(18)
.borderRadius(18)
.linearGradient({ angle: 180, colors: [['#0F172A', 0], ['#1E293B', 1]] })
.margin({ top: 10 })

深色卡片三要素

  1. 大数字百分比:28fp 白色大字(64%)居中在 110×110 深灰圆内——仪表盘中心;
  2. 已花/总预算¥2453 / ¥7500 灰字——对照数字;
  3. 进度条 + 状态文案:绿色条(正常)/ 红色条 + 「⚠️ 已超支」(超支)——状态一眼可辨。

linearGradient 深色渐变angle: 180 + ['#0F172A', 0]['#1E293B', 1]——ArkUI 线性渐变(首现于本实例),深蓝黑渐变卡片从浅灰页面中「浮出」——仪表盘的视觉焦点。

totalPercent 计算(页面层):

private totalPercent(): number {
  if (this.budgetTotal <= 0) {
    return 0;
  }
  return Math.min(100, Math.round(this.spendTotal / this.budgetTotal * 100));
}

进度封顶 100:Math.min(100, …)——进度条宽度不溢出;超支由文案区分> 100 判断显示超支文案而非百分比)。「视觉封顶 + 逻辑判断」分离

四、分类预算明细:进度条列表

每分类一张白卡——分类名 + 进度/超支标签 + 金额 + 进度条:

Column() {
  Text('分类预算明细').fontSize(16).fontWeight(FontWeight.Bold)
  ForEach(this.budgets, (bw: BudgetWithSpend) => {
    Column() {
      Row() {
        Text(bw.budget.category).fontSize(14).fontWeight(FontWeight.Medium)
        Text(bw.overrun ? '超支!' : `${bw.percent}%`)
          .fontSize(11).padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .borderRadius(8)
          .backgroundColor(bw.overrun ? '#FEE2E2' : '#E5F6EC')
          .fontColor(bw.overrun ? '#DC2626' : '#059669')
        Blank()
        Text(`¥${bw.spent.toFixed(0)} / ¥${bw.budget.amount.toFixed(0)}`).fontSize(12).fontColor('#6B7280')
      }.width('100%')
      Row() {
        Row() {
          Row().width('100%').height(10).borderRadius(5)
            .backgroundColor(bw.overrun ? '#EF4444' : bw.budget.color)
        }
        .width(`${bw.percent}%`)
        .height(10).borderRadius(5)
      }
      .width('100%').height(10).borderRadius(5).backgroundColor('#E5E7EB').margin({ top: 6 })
    }
    .width('100%').padding(12).backgroundColor(Color.White).borderRadius(10).margin({ top: 8 })
    .onClick(() => this.switchCategory(bw.budget.category))
  }, (bw: BudgetWithSpend) => `${bw.budget.id}-${bw.budget.category}`)
}
.width('94%').padding(16).backgroundColor(Color.White).borderRadius(16).margin({ top: 12 })
.alignItems(HorizontalAlign.Start)

进度条卡五要素

  1. 分类名:14fp 中等字重;
  2. 状态标签:「超支!」红底红字 vs 「62%」绿底绿字——超支标红的核心视觉;
  3. 已花/预算¥1380 / ¥1500 灰字——Blank() 撑到右侧;
  4. 进度条:分类色条(超支红色)铺在浅灰轨道上——宽度 ${percent}%
  5. 点卡切分类:onClick switchCategory——明细列表联动。

进度条的宽度绑定Row().width(${bw.percent}%)——百分比字符串宽度(ArkUI 支持百分比宽度),外层轨道 width('100%')「嵌套 Row 实现进度条」:内层填充条宽度百分比 = 进度。双层进度条结构(轨道 + 填充)是 ArkUI 无 Progress 组件的自制方案(或直接用 Progress 组件——落地用自制 Row 更可控)。

超支的颜色切换bw.overrun ? '#EF4444' : bw.budget.color——超支覆盖分类色(红色优先),状态标签同步红绿切换。

五、分类支出明细:流水列表

底部「选中分类的支出流水」:

Column() {
  Row() {
    Text(`${this.selectedCategory} 支出明细`).fontSize(16).fontWeight(FontWeight.Bold)
    Blank()
    Text('+').fontSize(22).fontColor('#3B82F6').onClick(() => {
      this.fAmount = ''; this.fNote = '';
      this.addVisible = true;
    })
  }.width('100%')
  if (this.expenses.length === 0) {
    Text('该分类暂无支出').fontSize(13).fontColor('#999999').margin({ top: 12 })
  } else {
    ForEach(this.expenses, (e: Expense) => {
      Row({ space: 10 }) {
        Column({ space: 2 }) {
          Text(`${e.amount.toFixed(2)}`).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#DC2626')
          Text(`${this.fmtTime(e.spendTime)}${e.note ? ' · ' + e.note : ''}`).fontSize(11).fontColor('#999999')
        }.alignItems(HorizontalAlign.Start).layoutWeight(1)
        Text('✕').fontSize(14).fontColor('#CCCCCC').onClick(() => this.onDelete(e.id))
      }
      .width('100%').padding(12).backgroundColor(Color.White).borderRadius(10).margin({ top: 8 })
    }, (e: Expense) => `e-${e.id}`)
  }
}
.width('94%').padding(16).margin({ top: 12, bottom: 24 })
.alignItems(HorizontalAlign.Start)

流水行三要素

  1. 金额红字-¥28.50 15fp 红色粗体——支出即红色(财务惯例);
  2. 日期 + 备注6月24日 · 奶茶(有备注才显示「· 备注」)——条件拼接;
  3. 删除 ✕:灰色小叉。

空态expenses.length === 0 显示「该分类暂无支出」——空列表友好提示(选中分类无流水时不显示空区域)。

六、新增支出弹窗

if (this.addVisible) {
  Column() {
    Text(`💰 记一笔${this.selectedCategory}支出`).fontSize(18).fontWeight(FontWeight.Bold)
    TextInput({ placeholder: '金额(元)*', text: this.fAmount }).type(InputType.Number)
      .margin({ top: 12 }).onChange((v: string) => this.fAmount = v)
    TextInput({ placeholder: '备注(可选)', text: this.fNote })
      .margin({ top: 8 }).onChange((v: string) => this.fNote = v)
    Row({ space: 8 }) {
      Button('取消').layoutWeight(1).backgroundColor('#EEF2F7').fontColor('#555555')
        .onClick(() => this.addVisible = false)
      Button('保存').layoutWeight(1).backgroundColor('#3B82F6')
        .onClick(() => this.onAdd())
    }.margin({ top: 16 })
  }
  .padding(20).borderRadius(16).backgroundColor(Color.White).width('88%')
  .position({ x: '6%', y: '30%' })
}

弹窗标题带选中分类记一笔餐饮支出——用户知道记到哪类(从分类卡进入)。金额数字键盘:InputType.Number。onAdd 校验parseFloat!amount || amount <= 0 → Toast「请输入有效金额」——金额合法性校验(负数/零/非数字拒绝)。

七、UI 风格要素一览

风格项 取值 说明
页面背景 #F8FAFC 极浅灰蓝 财务清爽
总预算卡 深色渐变 #0F172A→#1E293B 仪表盘焦点
进度条 双层 Row 百分比宽 进度可视化
超支高亮 红底标签 + 红进度条 预警视觉
支出金额 红色粗体 -¥ 财务惯例
状态文案 剩余可用 / ⚠️ 已超支 对照提示

八、文章小结

本篇文章完成了实例 17 的 UI 层:深色总预算进度卡(大数字 + 进度条 + 剩余/超支文案)+ 分类进度条列表(状态标签 + 双层进度条 + 超支标红)+ 支出明细流水(红色金额 + 空态提示)+ 新增支出弹窗。设计核心是「进度可视化 + 超支预警」——深色仪表卡聚焦总进度、红色高亮预警超支分类;技术亮点是双层 Row 自制进度条(百分比宽度填充)与 linearGradient 深色渐变(首次登场)。

下一篇(17-3)深入数据层,讲解预算与支出的聚合计算——WHERE + SUM 分类聚合与进度/超支的派生逻辑。

九、补充:进度条与超支视觉

9.1 双层 Row 进度条 vs Progress 组件

本实例的进度条是双层 Row 自制方案(外层轨道 + 内层填充,宽度百分比),而不是 ArkUI 的 Progress 组件。对比:

维度 双层 Row 自制 Progress 组件
实现 Row().width('100%') 轨道 + 内层 width(\${percent}%`)` 填充 <Progress value={percent} total={100} />
颜色切换 backgroundColor 直接换色(超支红/分类色) color 属性或样式覆盖
圆角 双层各自 borderRadius(5) 自由控制 自带轨道圆角,定制受限
状态联动 填充色与标签同一三元判断,天然同步 需额外计算状态再分别设置
适用 进度 + 超支双色逻辑复杂时更可控 单纯进度展示更简洁

结论:双层 Row 适合「颜色随状态变化 + 与标签联动」的场景——超支时轨道填充色、状态标签、剩余文案三处同步变红,用同一个 overrun 判断驱动,避免多处状态不同步。

9.2 超支红色高亮的统一规则

实例 17 的红色体系是一套统一规则,所有超支视觉共用同一组色值:

用途 正常态 超支态
进度条填充 分类色 / #4ADE80 #EF4444
状态标签背景 #E5F6EC 绿底 #FEE2E2 红底
状态标签文字 #059669 #DC2626
顶部状态文案 #4ADE80 剩余可用 #F87171 ⚠️ 已超支

规则:进度条、标签、文案三处只认一个来源——bw.overrun(分类)或 totalPercent() > 100(总卡),不各自算一遍超支条件,避免「进度条红了标签还是绿」的割裂。

9.3 金额输入校验细节

新增支出弹窗的 onAdd 校验链:

private onAdd() {
  const amount = parseFloat(this.fAmount);
  if (!amount || amount <= 0) {
    promptAction.showToast({ message: '请输入有效金额' });
    return;
  }
  // 校验通过 → 插入支出 → 刷新列表与进度
}

要点parseFloat 容忍前导空格与小数(" 28.5" → 28.5);!amount 拦截 NaN/0/空串;amount <= 0 拦负数。校验失败只 Toast 不关闭弹窗——输入框保留,用户直接修改重提。

9.4 FAQ

Q1:进度条宽度用百分比字符串,小数点很多会不会布局异常?
不会。ArkUI 的 width() 接受 "62.5%" 这类字符串,会按容器宽度比例解析;Math.round 已把 percent 取整,宽度值始终是整数百分比,渲染稳定。

Q2:为什么总进度封顶 100,超支却还要单独判断?
进度条是视觉比例,超出 100% 会溢出轨道破坏布局,所以 Math.min(100, ...) 封顶;而超支是业务状态,必须用真实比例 > 100 判断才能显示「⚠️ 已超支」。两者分离:宽度永远 ≤ 100%,文案永远反映真实比例

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐