一、鸿蒙技术栈与声明式UI范式背景

HarmonyOS 6.1.1 作为华为全场景分布式操作系统的最新演进版本,其应用开发框架已经进入高度成熟阶段。在这个版本中,ArkUI 声明式范式成为构建用户界面的主流方式,它借鉴了现代前端框架的声明式编程思想,同时又针对鸿蒙的跨设备分布式场景做了深度优化。开发者通过描述界面"是什么"而非"怎么做"的方式,让框架接管渲染管线的调度,从而实现高效的 UI 更新与差异比对。

ArkTS 是鸿蒙生态专属的编程语言,它在 TypeScript 的基础上做了严格的静态类型约束与运行时增强。与普通 TypeScript 不同,ArkTS 在编译期会进行更激进的类型检查,禁止使用某些动态特性(如 any 类型的滥用、运行时对象结构变更等),这种设计取舍换来的是 AOT 编译优化的更大空间与更低的运行时开销。在 HarmonyOS ArkTS API 24 中,ArkTS 的类型系统进一步强化了接口约束,所有组件状态、数据模型都必须以明确的 interfaceclass 声明,这正是本应用中大量使用 interface 定义数据模型的根本原因。

HarmonyOS 6.1.1 的新特性集中体现在几个方面。首先是 ArkUI 组件库的持续扩充,ProgressFlexScroll 等容器组件在 API 24 层级获得了更丰富的属性与样式控制能力。其次是动画系统的优化,animation 修饰器可以更细粒度地附加到单个属性上,配合 Curve 枚举提供的多种缓动曲线,开发者无需手写定时器即可实现丝滑的过渡效果。再者是状态管理体系的完善,@State@Builder@Component 等装饰器构成了从组件内部状态到 UI 片段复用的完整链路。

声明式 UI 的核心思想是"数据驱动视图"。当状态变量发生变化时,框架自动触发依赖该变量的 UI 片段重新渲染,这一过程不需要开发者手动调用 setStateinvalidate 之类的方法。在 HarmonyOS ArkTS API 24 中,这种响应式机制通过 VDOM(虚拟 DOM)的 diff 算法实现,框架会精准定位变化的节点并最小化地更新真实组件树。本应用中,Tab 切换、弹窗显隐、金额翻牌、柱状图增长等动效,全部依赖这一响应式机制。

从业务角度看,这是一个企业人事场景下的薪薪资管家应用(产品名"薪朋友")。它面向已入职员工,提供工资条查询、个税测算、调薪轨迹追溯、社保公积金明细、积分商城兑换、个人薪酬档案等六大核心功能。应用采用底部六 Tab 架构,融合金融级紫色视觉风格,配合密码验证弹窗、申诉表单、调薪申请、积分兑换确认、删除警示等多种交互弹窗,以及柱状图、进度环、占比条、翻牌缩放等数据可视化动效,是一个功能密度极高的鸿蒙原生应用范例。

渲染错误: Mermaid 渲染失败: Parse error on line 11: ...put 基础组件] D --> D1[@Component 组件化] ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'

二、应用整体架构与数据模型设计

2.1 数据类型定义层

应用首先定义了七组核心数据接口,每一组对应一个业务域的数据结构。这种"接口先行"的设计模式是 ArkTS 开发的推荐实践——在构建 UI 之前,先明确数据形态,可以让后续的 ForEach 渲染、状态绑定有清晰的类型约束。

第一组是 SalaryMonth 接口,它描述了工资条按月份的汇总信息。字段包括 id(唯一标识)、month(月份字符串,如 '2026-08')、net(税后实发金额,字符串带千分位格式)、total(税前总额)、state(发放状态,如 '待发''已发')。金额使用字符串而非数字类型,是因为展示层需要保留千分位逗号和小数点,避免在渲染时反复做数字格式化。

第二组是 SalaryPart 接口,对应单月工资的构成明细项。字段 name 是项目名称(如"基本工资"),icon 是 emoji 图标,amount 是带正负号的金额字符串(如 '+16,000.00'),kind 区分 '应发''扣款' 两种类别。这个 kind 字段在渲染时用于决定金额文字的颜色——应发为绿色、扣款为红色,是数据驱动视觉样式的典型体现。

第三组 TaxStep 描述个税七级超额累进税率表,字段 range 是应纳税所得额区间,rate 是税率,deduct 是速算扣除数。第四组 RaiseRecord 描述调薪历史记录,第五组 InsuranceRow 描述五险一金明细行,第六组 PointGoods 描述积分商城商品,第七组 YearSalary 描述全年薪资走势数据。

interface SalaryMonth {
  id: number
  month: string
  net: string
  total: string
  state: string
}

在这里插入图片描述

这段代码定义了 SalaryMonth 接口。idnumber 类型,作为 ForEach 渲染时的 key 生成依据;month 用字符串而非 Date 对象,是因为鸿蒙组件的时间格式化能力有限,直接预格式化为展示字符串更高效。nettotal 均为字符串,内含千分位格式,避免运行时格式化开销。state 用中文字符串而非枚举,是为了直接渲染到 Text 组件而无需映射层。

ArkUI 技术点:interface 与类型安全
在 ArkTS 中,interface 不仅是一种类型约束,更是框架进行依赖追踪的基础。当 @State 变量绑定了一个 interface 数组时,框架能精确感知数组元素的结构变化。与 anyobject 类型不同,interface 让编译器和运行时都明确知道有哪些字段,这是 ArkTS 强类型设计的精髓。

2.2 静态数据常量定义

在类型定义之后,应用声明了七组静态常量数据。这些数据用 const 声明且为字面量数组,意味着它们在应用生命周期内不可变,适合作为渲染的"只读数据源"。

const SALARY_MONTHS: SalaryMonth[] = [
  { id: 1, month: '2026-08', net: '18,642.50', total: '24,830.00', state: '待发' },
  { id: 2, month: '2026-07', net: '18,458.00', total: '24,680.00', state: '已发' },
  // ...
]

在这里插入图片描述

这里定义了 8 个月的工资条汇总记录。注意第一条 id: 1 对应 '2026-08' 即当前月,状态为 '待发',其余为 '已发'。这种"最新一条特殊"的设计,让 UI 能在列表第一项展示不同的状态色(橙色"待发"标签 vs 绿色"已发"标签),体现数据驱动样式差异的思路。

const SALARY_PARTS: SalaryPart[] = [
  { id: 1, name: '基本工资', icon: '💼', amount: '+16,000.00', kind: '应发' },
  // ...
  { id: 5, name: '养老保险', icon: '👴', amount: '-1,280.00', kind: '扣款' },
  // ...
]

在这里插入图片描述

这组数据定义了 8 月工资的 8 项构成。前 4 项为应发(基本工资、岗位津贴、绩效奖金、加班费),后 4 项为扣款(养老、医疗、公积金、个税)。icon 字段使用 emoji 而非图片资源,这是鸿蒙轻量化应用的一种资源策略——emoji 字符无需加载图片资源,渲染开销极低且跨设备一致。

SalaryMonth 月份汇总

工资条 Tab 列表

SalaryPart 构成明细

工资条 Tab 明细卡

申诉弹窗 选项

TaxStep 税率阶梯

个税 Tab 税率表

RaiseRecord 调薪记录

调薪 Tab 时间轴

InsuranceRow 五险一金

社保 Tab 明细表

PointGoods 积分商品

积分 Tab 商城网格

YearSalary 全年走势

个税 Tab 柱状图

2.3 Tab 枚举与元数据

enum SalaryTab { SLIP, TAX, RAISE, SOCIAL, POINT, MINE }

这里用 enum 定义了六个 Tab 的枚举常量。ArkTS 的 enum 本质是数值枚举,SLIP 对应 0TAX 对应 1,依此类推。使用枚举而非魔术数字,让 if (this.currentTab === SalaryTab.SLIP) 这样的判断语句具有自解释性。

interface SalaryTabMeta {
  icon: string
  label: string
}

const SALARY_TABS: SalaryTabMeta[] = [
  { icon: '💵', label: '工资条' },
  { icon: '🧾', label: '个税' },
  { icon: '📈', label: '调薪' },
  { icon: '🛡️', label: '社保' },
  { icon: '🎁', label: '积分' },
  { icon: '👤', label: '我的' }
]

SalaryTabMeta 是 Tab 的元数据结构,iconlabel 分别对应底部 Tab 的图标和文字。SALARY_TABS 数组的顺序与 SalaryTab 枚举值严格对应——这种"枚举值即数组下标"的设计,让 Tab 渲染和切换判断可以通过同一个索引完成,非常精巧。

三、主入口组件 SalaryBookApp 深度解析

3.1 组件声明与状态定义

@Entry
@Component
struct SalaryBookApp {
  @State currentTab: SalaryTab = SalaryTab.SLIP

在这里插入图片描述

@Entry 装饰器标记这是应用的入口组件,鸿蒙框架会为它创建页面路由并加载到窗口中。@Component 装饰器声明这是一个自定义组件,可以被其他组件引用(虽然入口组件通常不被引用)。struct 关键字定义组件结构体——注意 ArkTS 用 struct 而非 class,因为组件是值语义的轻量结构,不需要继承和多态。

@State currentTab: SalaryTab = SalaryTab.SLIP 是整个应用的核心状态。@State 装饰器的作用是声明一个组件内部的可观察状态变量,当它的值变化时,所有引用它的 UI 片段会自动重新渲染。初始值设为 SalaryTab.SLIP,即应用启动时默认展示工资条 Tab。

ArkUI 技术点:@State 装饰器详解
@State 是 ArkUI 状态管理的基础装饰器,它具备以下特性:1)仅限组件内部使用,是组件私有状态;2)支持基本类型、对象类型、数组类型的观察;3)当值发生变化时,触发依赖该变量的 build 片段重新执行;4)对于对象类型,会做浅层属性观察;5)对于数组类型,会观察增删改操作。@State 变量的变化是声明式 UI 响应式更新的源动力。

3.2 build 方法与整体布局

SalaryBookApp build

Column 根容器

顶部头部 Column - 紫色渐变

内容区 Column - layoutWeight 1

底部 Tab Row - 6个

左:税前金额

右:税后到手

快捷标签 Row

if SLIP: SlipListContent

if TAX: TaxCalcContent

if RAISE: RaiseHistoryContent

if SOCIAL: SocialFundContent

if POINT: PointMallContent

else: SalaryMineContent

在这里插入图片描述

build() 方法是每个 ArkUI 组件必须实现的方法,它返回组件的 UI 结构。在这里,根容器是一个 Column,它纵向排列三个区块:顶部头部、中间内容区、底部 Tab 栏。这种"头-体-尾"的三段式结构是移动端应用的经典布局。

build() {
  Column() {
    // ===== 顶部头部(金融风,无动画) =====
    Column() {

Column 没有显式设置宽高,但通过子组件的 .width('100%').height('100%') 链式调用撑满屏幕。背景色 #F8F5FA 是一种极淡的紫调白,营造金融级柔和氛围。

3.3 顶部头部组件

Column() {
  Row() {
    Column() {
      Text('2026年8月 · 税前').fontSize(10).fontColor('#E1BEE7')
      Row() {
        Text('¥').fontSize(14).fontColor('#FFFFFF').margin({ top: 6 })
        Text('24,830.00').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
      }
      .margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Start).layoutWeight(1)

在这里插入图片描述

这里是顶部头部的左侧区块。外层 Column 通过 .alignItems(HorizontalAlign.Start) 让子元素左对齐,.layoutWeight(1) 占据剩余宽度。内部嵌套了一个 Row 容器,放置"¥"符号和金额数字。注意"¥"的 fontSize(14) 比金额数字的 fontSize(24) 小,并通过 .margin({ top: 6 }) 微调垂直对齐,实现"小符号+大数字"的经典金融金额展示效果。

ArkUI 技术点:Column 组件详解
Column 是 ArkUI 的纵向线性布局容器,子元素从上到下排列。常用属性包括:alignItems(设置子元素的水平对齐方式,取 HorizontalAlign.Start/Center/End)、justifyContent(设置子元素在纵向的分布方式,取 FlexAlign.Start/Center/End/SpaceBetween/SpaceAround/SpaceEvenly)、layoutWeight(设置弹性权重,按比例分配剩余空间)。Column 是构建纵向卡片、列表项、表单分组的基础容器。

ArkUI 技术点:Row 组件详解
Row 是横向线性布局容器,子元素从左到右排列。常用属性与 Column 对称:alignItems(取 VerticalAlign.Top/Center/Bottom)、justifyContent(取 FlexAlign 同上)、layoutWeightRow 常用于横向并排的标签、金额左右分布、按钮组等场景。RowColumn 可以互相嵌套,构建复杂的二维布局。

    Column() {
      Text('税后到手').fontSize(10).fontColor('#E1BEE7')
      Text('¥18,642.50').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FFD54F').margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.End)
  }
  .width('100%').padding({ left: 18, right: 18, top: 16 })

在这里插入图片描述

右侧区块通过 .alignItems(HorizontalAlign.End) 让文字右对齐。税后金额用 #FFD54F(金色)而非白色,形成视觉层次——金色"税后到手"比白色"税前"更抢眼,引导用户关注实际到手金额。这是金融产品设计的视觉心理学应用。

  Row() {
    ForEach(['当月工资', '年终奖', '调薪记录', '社保基数'], (q: string) => {
      Text(q).fontSize(9).fontColor('#F3E5F5')
        .backgroundColor('rgba(255,255,255,0.18)')
        .borderRadius(10).padding({ left: 9, right: 9, top: 4, bottom: 4 })
        .margin({ right: 8 })
    }, (q: string) => q)
  }
  .width('100%').padding({ left: 18, top: 12, bottom: 14 })
}
.width('100%').linearGradient({ angle: 135, colors: [['#4A148C', 0], ['#7B1FA2', 1]] })

这里是头部底部的快捷标签条。ForEach 遍历四个标签字符串,每个 Text 设置了半透明白色背景 rgba(255,255,255,0.18)、圆角 10,形成胶囊标签样式。整个头部 Column 使用 .linearGradient() 设置 135 度线性渐变背景,从深紫 #4A148C 到中紫 #7B1FA2,营造金融级深邃质感。

ArkUI 技术点:ForEach 组件详解
ForEach 是 ArkUI 的列表渲染组件,用于根据数据数组循环生成 UI 节点。它接受三个参数:1)数据源数组;2)渲染回调函数 (item, index) => UI,返回单项的 UI 结构;3)键值生成函数 item => string,用于 diff 算法识别元素身份。第三个参数非常关键——良好的 key 设计能避免不必要的节点重建,提升列表更新性能。ForEach 是声明式 UI"数据驱动列表"的核心载体。

ArkUI 技术点:linearGradient 线性渐变
.linearGradient() 是 ArkUI 的背景渐变属性,参数包括 angle(渐变角度,0-360 度)和 colors(颜色断点数组,每个元素为 [颜色值, 位置],位置 0-1)。本例 angle: 135 表示从左上到右下的对角渐变,[['#4A148C', 0], ['#7B1FA2', 1]] 表示起点深紫、终点中紫。渐变背景是金融类应用营造质感的关键手段。

3.4 内容区与 Tab 切换逻辑

Column() {
  if (this.currentTab === SalaryTab.SLIP) {
    SlipListContent()
  } else if (this.currentTab === SalaryTab.TAX) {
    TaxCalcContent()
  } else if (this.currentTab === SalaryTab.RAISE) {
    RaiseHistoryContent()
  } else if (this.currentTab === SalaryTab.SOCIAL) {
    SocialFundContent()
  } else if (this.currentTab === SalaryTab.POINT) {
    PointMallContent()
  } else {
    SalaryMineContent()
  }
}
.layoutWeight(1).width('100%')

在这里插入图片描述

内容区使用 if-else if-else 条件渲染,根据 this.currentTab 的值动态决定展示哪个子组件。这是 ArkUI 声明式条件渲染的典型写法——当 currentTab 变化时,框架自动销毁旧组件实例、创建新组件实例。.layoutWeight(1) 让内容区占据头部和底部 Tab 之间的全部剩余高度。

值得注意的设计细节:这里用的是 if-else 而非 Visibility 显隐控制。if-else 会销毁/重建组件,适合 Tab 这种"同一时刻只展示一个"的场景;Visibility 保留组件实例只控制可见性,适合需要保留状态的场景。Tab 切换选择 if-else 是正确的——每次切换都是全新的组件实例,状态隔离清晰。

3.5 底部 Tab 栏

Row() {
  ForEach(SALARY_TABS, (t: SalaryTabMeta, idx: number) => {
    Column() {
      Text(t.icon).fontSize(18)
        .animation({ duration: 200, curve: Curve.EaseOut })
      Text(t.label).fontSize(9)
        .fontColor(this.currentTab === idx ? '#7B1FA2' : '#9E9E9E')
        .margin({ top: 2 })
        .fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Center)
    .padding({ top: 7, bottom: 7 })
    .onClick(() => {
      this.currentTab = idx as SalaryTab
    })
  }, (t: SalaryTabMeta) => t.label)
}
.width('100%').backgroundColor('#FFFFFF')
.border({ width: { top: 1 }, color: '#F3E5F5' })

底部 Tab 栏使用 Row 横向排列六个 Column,每个 Column 包含图标和文字。.layoutWeight(1) 让六个 Tab 等分宽度。关键在于 onClick 回调中 this.currentTab = idx as SalaryTab——idxForEach 的索引参数,类型为 number,通过 as SalaryTab 类型断言转为枚举。这个赋值触发 @State 响应式更新,内容区立即切换。

选中态的视觉处理非常巧妙:fontColor 用三元表达式 this.currentTab === idx ? '#7B1FA2' : '#9E9E9E' 区分选中紫色与未选灰色,fontWeight 同理区分加粗与常规。图标 Text 附加了 .animation({ duration: 200, curve: Curve.EaseOut }),当 Tab 切换时图标会有 200 毫秒的缓出过渡动画,增加交互的生动感。

ArkUI 技术点:animation 动画修饰器
.animation() 是 ArkUI 的属性动画修饰器,参数包括 duration(动画时长,毫秒)、curve(缓动曲线,取 Curve 枚举)、delay(延迟)、iterations(重复次数)、playMode(播放模式)。当被修饰的属性值变化时,框架自动插值生成平滑过渡。Curve.EaseOut 是先快后慢的缓出曲线,适合元素进入动画。.animation() 的精妙之处在于它是"声明式动画"——开发者只描述目标状态,过渡过程由框架自动完成。

ArkUI 技术点:onClick 点击事件
.onClick() 是 ArkUI 的通用点击事件绑定方法,接受一个箭头函数作为回调。在回调中可以访问组件的 this 上下文(即组件实例),修改 @State 变量触发 UI 更新。这是声明式 UI 中"交互→状态变更→视图更新"闭环的关键一环。

渲染错误: Mermaid 渲染失败: Parse error on line 4: ...ab = idx] C --> D[@State 变化触发响应式更新] ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'

四、工资条 Tab(SlipListContent)逐行剖析

4.1 组件状态与 Builder 设计

@Component
struct SlipListContent {
  @State showLock: boolean = false
  @State unlocked: boolean = false
  @State pwdInput: string = ''
  @State showAppeal: boolean = false
  @State appealItem: string = '绩效奖金'
  @State flipScale: number = 1.0

SlipListContent 是工资条 Tab 的内容组件。它定义了六个 @State 变量:showLock 控制密码验证弹窗显隐,unlocked 标记是否已解锁工资条,pwdInput 绑定密码输入框文本,showAppeal 控制申诉弹窗显隐,appealItem 记录当前选中的申诉项目,flipScale 驱动金额翻牌缩放动效。这六个状态变量覆盖了该 Tab 的全部交互行为。

@Builder modalOverlay(onClose: () => void) {
  Column() {
    Column().width('100%').height('100%').backgroundColor('rgba(0,0,0,0.55)')
      .onClick(() => {
        onClose()
      })
  }
  .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(998)
}

@Builder modalOverlay 是一个可复用的 UI 片段,它生成一个半透明黑色遮罩层。@Builder 装饰器的作用是将一段 UI 结构封装为可调用的函数,接受参数(这里是 onClose 回调),在 build() 中通过 this.modalOverlay(...) 调用。遮罩层使用 rgba(0,0,0,0.55) 半透明黑色,点击时触发 onClose 关闭弹窗。.zIndex(998) 确保遮罩层在普通内容之上,但低于弹窗本体(弹窗 zIndex999)。

ArkUI 技术点:@Builder 装饰器详解
@Builder 是 ArkUI 的 UI 片段复用装饰器,用于将重复的 UI 结构封装为函数。与 @Component 不同,@Builder 不创建独立组件实例,而是在调用处内联展开 UI 结构。它接受参数,可以实现参数化的 UI 复用。本应用中所有弹窗的遮罩层都复用 modalOverlay 这个 @Builder,避免了在每个弹窗中重复编写遮罩代码。@Builder 是 ArkUI 中仅次于 @Component 的重要复用机制。

4.2 密码验证弹窗

@Builder pwdModal() {
  Column() {
    Column() {
      Text('🔒').fontSize(32).margin({ top: 18 })
      Text('输入工资查询密码').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#212121')
        .margin({ top: 10 })
      Text('首次查看需验证身份,密码为人事系统 6 位数字')
        .fontSize(10).fontColor('#9E9E9E').margin({ top: 6 })

pwdModal 是密码验证弹窗的 @Builder。外层 Column 全屏居中(通过 .alignItems(HorizontalAlign.Center).position({ y: '30%' })),内层 Column 是白色圆角卡片。顶部放置锁图标 🔒、标题"输入工资查询密码"、副标题说明。三段文字字号递减(32→15→10),形成视觉层次。

      Row() {
        TextInput({ placeholder: '• • • • • •', text: this.pwdInput })
          .fontSize(16).height(44).backgroundColor('#F3E5F5')
          .borderRadius(10).textAlign(TextAlign.Center)
          .type(InputType.Number)
          .width('60%').margin({ top: 16 })
          .onChange((v: string) => {
            this.pwdInput = v
          })
      }
      .justifyContent(FlexAlign.Center)

密码输入使用 TextInput 组件。.type(InputType.Number) 限定为数字键盘,符合 6 位纯数字密码场景。.textAlign(TextAlign.Center) 让输入文字居中。text: this.pwdInput 实现了受控输入——输入框的值与 @State pwdInput 双向绑定,.onChange 回调将输入值同步到状态。外层 Row.justifyContent(FlexAlign.Center) 让输入框在水平方向居中。

ArkUI 技术点:TextInput 输入组件
TextInput 是 ArkUI 的文本输入组件,常用属性包括:placeholder(占位提示文字)、text(受控值)、type(输入类型,取 InputType.Normal/Number/Password/Email/PhoneNumber 等)、textAlign(文字对齐)、maxLength(最大长度)、backgroundColorborderRadiusonChange 是值变化回调,onSubmit 是提交回调。在金融场景中,InputType.Number 配合 maxLength 可以实现验证码/密码输入。

      Button() {
        Text('验证并查看').fontSize(14).fontColor('#FFFFFF')
      }
      .width('60%').height(40).backgroundColor('#7B1FA2').borderRadius(20)
      .margin({ top: 16, bottom: 20 })
      .onClick(() => {
        this.showLock = false
        this.unlocked = true
      })
    }
    .width('82%').backgroundColor('#FFFFFF').borderRadius(16)
  }
  .width('100%').alignItems(HorizontalAlign.Center)
  .position({ y: '30%' }).zIndex(999)
}

验证按钮使用 Button 组件,内部嵌套 Text 定义按钮文字。按钮使用 .borderRadius(20) 实现完全圆角(高度 40,圆角 20 即胶囊形)。.onClick 回调中,先设置 this.showLock = false 关闭弹窗,再设置 this.unlocked = true 解锁工资条。这两个状态变更触发响应式更新:弹窗消失、工资条明细显示。

ArkUI 技术点:Button 组件详解
Button 是 ArkUI 的按钮组件,可以用两种方式定义内容:1)Button('文字') 直接传字符串;2)Button() { Text(...) } 传入子组件实现自定义内容。常用属性:backgroundColorborderRadiuswidthheighttype(取 ButtonType.Capsule/Circle/Normal)。.shadow() 可添加阴影。按钮配合 .onClick() 是最基础的交互入口。

4.3 申诉工资条弹窗(底部抽屉)

@Builder appealModal() {
  Column() {
    Column() {
      Row() {
        Text('⚠️').fontSize(18)
        Text('工资条申诉').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121').margin({ left: 6 })
        Text('3个工作日内答复').fontSize(9).fontColor('#9E9E9E').margin({ left: 8 })
      }
      .width('100%').padding({ top: 16, left: 16 })

appealModal 是申诉弹窗,设计为底部抽屉样式(通过 .position({ x: 0, y: '45%' }) 定位在屏幕下半部分,.borderRadius({ topLeft: 22, topRight: 22 }) 顶部大圆角模拟从底部滑出的抽屉效果)。标题行用 Row 横向排列警告图标、标题文字和时效说明,三者字号递减(18→16→9)。

      Column() {
        Text('异议项目').fontSize(11).fontColor('#757575')
        Row() {
          ForEach(SALARY_PARTS, (p: SalaryPart) => {
            Text(p.name).fontSize(10)
              .fontColor(this.appealItem === p.name ? '#FFFFFF' : '#616161')
              .backgroundColor(this.appealItem === p.name ? '#7B1FA2' : '#F5F5F5')
              .borderRadius(10).padding({ left: 9, right: 9, top: 5, bottom: 5 })
              .margin({ right: 8 }).margin({ top: 8 })
              .onClick(() => {
                this.appealItem = p.name
              })
          }, (p: SalaryPart) => p.id.toString())
        }
      }
      .width('100%').padding({ left: 16, top: 12 })

这里渲染异议项目选择器。ForEach 遍历 SALARY_PARTS(8 个工资构成项),生成可点击的胶囊标签。选中态通过 this.appealItem === p.name 判断:选中时白字紫底,未选中时灰字浅灰底。点击某个标签时,this.appealItem = p.name 触发响应式更新,所有标签的样式重新计算。这是"单选标签组"的经典实现模式。

      Column() {
        Text('异议说明').fontSize(11).fontColor('#757575')
        TextArea({ placeholder: '请描述金额异议,例如:绩效奖金系数应为 1.2 而非 1.0' })
          .fontSize(12).height(76).backgroundColor('#F5F5F5').borderRadius(10)
          .margin({ top: 6 })
      }
      .width('100%').padding({ left: 16, right: 16, top: 14 })

异议说明使用 TextArea 多行文本输入组件。.height(76) 提供足够的多行输入空间。placeholder 提供输入引导示例。与 TextInput 不同,TextArea 支持换行,适合长文本说明场景。

渲染错误: Mermaid 渲染失败: Parse error on line 2: ...chart TB A[用户点击"申诉"] --> B[showAppea ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'STR'

4.4 主内容区:未解锁占位卡与已解锁明细

build() {
  Stack() {
    Scroll() {
      Column() {
        if (!this.unlocked) {
          Column() {
            Text('🔒').fontSize(44)
            Text('工资条已加密').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').margin({ top: 10 })
            Text('点击验证身份后查看明细').fontSize(10).fontColor('#9E9E9E').margin({ top: 6 })
            Button() {
              Text('验证身份').fontSize(13).fontColor('#FFFFFF')
            }
            .width('46%').height(38).backgroundColor('#7B1FA2').borderRadius(19)
            .margin({ top: 16, bottom: 18 })
            .onClick(() => {
              this.showLock = true
            })
          }
          .width('100%').backgroundColor('#FFFFFF').borderRadius(14)
          .padding({ top: 30, bottom: 20 }).margin({ left: 12, right: 12, top: 12 })

build() 方法使用 Stack 作为根容器(用于叠加弹窗层),内部嵌套 Scroll 滚动容器。当 !this.unlocked(未解锁)时,展示加密占位卡——大锁图标、"工资条已加密"标题、提示文字、"验证身份"按钮。点击按钮触发 this.showLock = true 弹出密码验证弹窗。这是"权限门禁"的典型 UI 模式。

ArkUI 技术点:Stack 布局详解
Stack 是 ArkUI 的堆叠布局容器,子元素从中心向外堆叠,后声明的子元素覆盖在前面的之上。常用属性:alignContent(子元素对齐方式,取 Alignment.Center/Start/End/Top/Bottom 等)。Stack 的核心价值在于"层叠"——本应用中用 Stack 将滚动内容层和弹窗层叠加,弹窗通过 zIndex 确保在最上层。Stack 是实现弹窗、遮罩、浮层的基础布局容器。

ArkUI 技术点:Scroll 组件详解
Scroll 是 ArkUI 的滚动容器组件,当内容超出视口时提供滚动能力。常用属性:scrollBar(滚动条显隐,取 BarState.On/Off/Auto)、scrollDirection(滚动方向,取 ScrollDirection.Vertical/Horizontal)、edgeEffect(边缘效果,取 EdgeEffect.None/Spring/Fade)。Scroll 内部只能有一个直接子元素(通常是 ColumnRow),所有可滚动内容放在这个子元素内部。本应用所有 Tab 内容都包裹在 Scroll 中,实现内容超出屏幕时的滚动浏览。

        } else {
          Column() {
            Row() {
              Text('2026年8月 工资明细').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
                .layoutWeight(1)
              Text('申诉').fontSize(10).fontColor('#7B1FA2')
                .onClick(() => {
                  this.showAppeal = true
                })
            }
            .width('100%').padding({ left: 14, right: 14, top: 12 })

已解锁后展示工资明细卡。标题行用 Row 左右分布——左侧标题 .layoutWeight(1) 撑满,右侧"申诉"文字按钮。点击"申诉"触发 this.showAppeal = true 弹出申诉抽屉。这种"标题+操作"的行布局是卡片头部的标准模式。

            ForEach(SALARY_PARTS, (p: SalaryPart, i: number) => {
              Row() {
                Text(p.icon).fontSize(18)
                Text(p.name).fontSize(12).fontColor('#212121').layoutWeight(1).margin({ left: 10 })
                Text(p.amount).fontSize(12).fontWeight(FontWeight.Bold)
                  .fontColor(p.kind === '应发' ? '#43A047' : '#F44336')
                  .animation({ duration: 300, curve: Curve.EaseOut })
                  .onClick(() => {
                    this.flipScale = 0.6
                    setTimeout(() => {
                      this.flipScale = 1.0
                    }, 300)
                  })
              }
              .width('100%').padding({ left: 14, right: 14, top: 10, bottom: 10 })

              if (i < SALARY_PARTS.length - 1) {
                Divider().color('#F5F5F5')
              }
            }, (p: SalaryPart) => p.id.toString())

这里用 ForEach 渲染 8 项工资构成。每项是一个 Row:emoji 图标、项目名称(.layoutWeight(1) 撑满中间)、金额数字。金额颜色由 p.kind 决定——应发绿色 #43A047、扣款红色 #F44336,这是数据驱动样式的核心体现。金额附加了 .animation(),当 flipScale 变化时产生 300 毫秒缓出过渡。

点击金额时,this.flipScale = 0.6 立即缩小,setTimeout 300 毫秒后恢复 1.0,形成"翻牌缩放"动效。Divider 在非最后一项后渲染分隔线,.color('#F5F5F5') 是极淡的灰色,视觉分隔但不突兀。

ArkUI 技术点:Divider 分割线组件
Divider 是 ArkUI 的分割线组件,用于在列表项之间提供视觉分隔。常用属性:color(颜色)、strokeWidth(线宽)、lineCap(线帽样式)。Divider 默认水平方向、宽度撑满父容器。在 ForEach 循环中,通过 if (i < array.length - 1) 条件渲染,避免最后一项后出现多余分割线。

4.5 历史工资条列表

          Column() {
            Text('🕘 历史工资条').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
              .width('100%').padding({ left: 14, top: 12 })

            ForEach(SALARY_MONTHS, (m: SalaryMonth) => {
              Row() {
                Column() {
                  Text(m.month).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#212121')
                  Text('税前 ¥' + m.total).fontSize(9).fontColor('#9E9E9E').margin({ top: 3 })
                }
                .alignItems(HorizontalAlign.Start).layoutWeight(1)

                Column() {
                  Text('¥' + m.net).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#7B1FA2')
                  Text('税后').fontSize(8).fontColor('#9E9E9E')
                }
                .alignItems(HorizontalAlign.End)

                Text(m.state).fontSize(9)
                  .fontColor(m.state === '已发' ? '#43A047' : '#FB8C00')
                  .backgroundColor(m.state === '已发' ? '#E8F5E9' : '#FFF3E0')
                  .borderRadius(8).padding({ left: 8, right: 8, top: 3, bottom: 3 })
                  .margin({ left: 10 })
              }
              .width('100%').padding({ left: 14, right: 14, top: 11, bottom: 11 })
              .onClick(() => {
                if (!this.unlocked) {
                  this.showLock = true
                }
              })
            }, (m: SalaryMonth) => m.id.toString())
          }
          .width('100%').backgroundColor('#FFFFFF').borderRadius(14)
          .margin({ left: 12, right: 12, top: 10, bottom: 20 })

历史工资条列表用 ForEach 渲染 SALARY_MONTHS 的 8 条记录。每行三段式布局:左侧月份+税前、右侧税后金额、最右侧状态标签。状态标签的颜色和背景色由 m.state 决定——"已发"绿色配浅绿底、"待发"橙色配浅橙底。这是状态徽章的标准设计模式。

点击事件 .onClick() 检查 !this.unlocked,如果未解锁则弹出密码验证弹窗。这意味着用户在未验证身份时点击历史记录会被拦截引导验证,是安全设计的体现。

4.6 弹窗叠加渲染

    if (this.showLock) {
      this.modalOverlay(() => {
        this.showLock = false
      })
      this.pwdModal()
    }

    if (this.showAppeal) {
      this.modalOverlay(() => {
        this.showAppeal = false
      })
      this.appealModal()
    }
  }
  .width('100%').height('100%')
}

Stack 的滚动内容之后,通过条件渲染叠加弹窗层。当 this.showLocktrue 时,先渲染遮罩层(传入关闭回调),再渲染密码弹窗。当 this.showAppealtrue 时,同理渲染申诉遮罩和申诉抽屉。由于 Stack 的堆叠特性,后渲染的弹窗层自然覆盖在滚动内容之上,配合 zIndex 确保层级正确。

这种"条件渲染弹窗"的模式是 ArkUI 实现弹窗的轻量方案——不依赖系统级 Dialog API,完全用组件树实现,灵活性高但需要手动管理遮罩、层级、关闭逻辑。优点是弹窗内容完全自定义,可以精确控制视觉与交互。

点击验证身份

输入密码点击验证

点击遮罩关闭

点击申诉

提交或取消

未解锁点击

未解锁

弹窗密码验证

已解锁

展示工资明细

弹窗申诉

展示历史列表

未解锁状态拦截

五、个税 Tab(TaxCalcContent)逐行剖析

5.1 状态与专项扣除编辑弹窗

@Component
struct TaxCalcContent {
  @State showEdit: boolean = false
  @State childEdu: string = '2000'
  @State houseLoan: string = '1000'
  @State elderly: string = '1500'
  @State barAnim: boolean = false

TaxCalcContent 定义了五个状态变量。showEdit 控制专项扣除编辑弹窗显隐;childEduhouseLoanelderly 分别绑定三项专项扣除的输入值,初值为标准扣除额;barAnim 驱动柱状图增长动画。前三个是字符串类型而非数字,与密码输入同理——展示层需要直接渲染,避免格式转换。

@Builder taxDeductModal() {
  Column() {
    Column() {
      Text('🧮 专项附加扣除编辑').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
        .width('100%').padding({ top: 16, left: 16 })

taxDeductModal 是专项扣除编辑弹窗。它采用居中弹出样式(.position({ y: '18%' })),不同于申诉弹窗的底部抽屉。弹窗内包含三组输入项:子女教育、住房贷款利息、赡养老人,每组结构相同。

      Column() {
        Text('子女教育(每月)').fontSize(11).fontColor('#757575')
        Row() {
          Text('¥').fontSize(12).fontColor('#7B1FA2')
          TextInput({ text: this.childEdu })
            .fontSize(13).height(36).backgroundColor('#F5F5F5').borderRadius(8)
            .type(InputType.Number)
            .onChange((v: string) => {
              this.childEdu = v
            })
            .margin({ left: 6 })
          Text('/ 月 · 2000 标准').fontSize(9).fontColor('#9E9E9E').margin({ left: 8 })
        }
        .margin({ top: 6 })
      }
      .width('100%').padding({ left: 16, right: 16, top: 12 })

每组输入项的结构:标签 TextRow 内含"¥"符号、TextInput 输入框、标准值说明。TextInputtext: this.childEdu 实现受控输入,.onChange 同步到状态变量。.type(InputType.Number) 限定数字键盘。右侧"2000 标准"提示用户标准扣除额,引导合理填写。三组输入项代码结构完全一致,仅状态变量和标签不同。

      Text('累计专项扣除 ¥4,500/月,预计全年节税 ¥5,400')
        .fontSize(9).fontColor('#7B1FA2').margin({ top: 12 })

这行汇总提示动态展示累计扣除额和预计节税金额。在实际应用中,这个值应该由三项输入实时计算得出,这里是静态展示。紫色文字 #7B1FA2 与主题色一致,强调节税价值。

5.2 个税概览卡

build() {
  Stack() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Column() {
              Text('本月应纳税所得额').fontSize(9).fontColor('#E1BEE7')
              Text('¥15,187.50').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start).layoutWeight(1)

            Column() {
              Text('适用税率').fontSize(9).fontColor('#E1BEE7')
              Text('20%').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFD54F').margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%').padding({ left: 16, right: 16, top: 14 })

个税概览卡采用与全局头部一致的紫色渐变背景,但在内容区内部。左侧展示应纳税所得额、右侧展示适用税率。两列结构对称,用 .alignItems 区分对齐方向。这种"双指标卡片"是金融数据展示的常见模式。

          Row() {
            Text('已申报专项附加扣除 ¥4,500').fontSize(9).fontColor('#F3E5F5')
              .backgroundColor('rgba(255,255,255,0.2)')
              .borderRadius(10).padding({ left: 8, right: 8, top: 3, bottom: 3 })
            Text('编辑').fontSize(9).fontColor('#FFD54F')
              .margin({ left: 10 })
              .onClick(() => {
                this.showEdit = true
              })
          }
          .width('100%').padding({ left: 16, bottom: 14, top: 10 })
        }
        .width('100%').linearGradient({ angle: 135, colors: [['#6A1B9A', 0], ['#9C27B0', 1]] })
        .borderRadius(14).margin({ left: 12, right: 12, top: 10 })

概览卡底部行展示已申报扣除额标签和"编辑"按钮。金色"编辑"文字点击触发 this.showEdit = true 弹出扣除编辑弹窗。整个卡片用 linearGradient 设置比全局头部稍浅的紫色渐变(#6A1B9A#9C27B0),在视觉层次上属于"次级头部"。

5.3 个税七级税率表

        Column() {
          Text('📊 个人所得税税率表(综合所得)').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            .width('100%').padding({ left: 14, top: 12 })

          Row() {
            Text('级数/月应纳税所得额').fontSize(9).fontColor('#9E9E9E').layoutWeight(1.6)
            Text('税率').fontSize(9).fontColor('#9E9E9E').layoutWeight(0.7)
            Text('速算扣除').fontSize(9).fontColor('#9E9E9E').layoutWeight(1)
          }
          .width('100%').padding({ left: 14, right: 14, top: 10 })

          ForEach(TAX_STEPS, (s: TaxStep, i: number) => {
            Row() {
              Text((i + 1).toString() + ' · ' + s.range).fontSize(9)
                .fontColor(i === 2 ? '#7B1FA2' : '#616161')
                .fontWeight(i === 2 ? FontWeight.Bold : FontWeight.Normal)
                .layoutWeight(1.6)
              Text(s.rate).fontSize(10)
                .fontColor(i === 2 ? '#7B1FA2' : '#F44336')
                .fontWeight(FontWeight.Bold).layoutWeight(0.7)
              Text(s.deduct).fontSize(9).fontColor('#616161').layoutWeight(1)
            }
            .width('100%').padding({ left: 14, right: 14, top: 8, bottom: 8 })
            .backgroundColor(i === 2 ? '#F3E5F5' : '#FFFFFF')
            .borderRadius(8)

            if (i === 2) {
              Text('← 您当前所处税档').fontSize(8).fontColor('#7B1FA2')
                .width('100%').padding({ left: 14, top: 2 })
            }
          }, (s: TaxStep) => s.id.toString())
        }

税率表用 ForEach 渲染 7 级税率。表头 Row 定义三列权重 1.6 : 0.7 : 1,每行 Row 使用相同的 layoutWeight 保证列对齐。关键设计是 i === 2(第三级,即 20% 税率档)被标记为"当前税档"——文字变紫色加粗、整行变浅紫背景、下方追加"← 您当前所处税档"提示。这种"高亮当前数据行"的设计让用户快速定位自己的税率位置。

5.4 全年薪资柱状图

        Column() {
          Text('📈 全年税前薪资走势').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            .width('100%').padding({ left: 14, top: 12 })

          Row() {
            ForEach(YEAR_SALARY, (y: YearSalary) => {
              Column() {
                Column()
                  .width(12)
                  .height(this.barAnim ? (y.amount / 28860 * 62).toFixed(0) + 'vp' : '1vp')
                  .backgroundColor(y.amount > 26000 ? '#FFD54F' : '#9C27B0')
                  .borderRadius({ topLeft: 3, topRight: 3 })
                  .animation({ duration: 600, curve: Curve.EaseOut })
                Text(y.month).fontSize(7).fontColor('#9E9E9E').margin({ top: 3 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
            }, (y: YearSalary) => y.id.toString())
          }
          .width('100%').padding({ left: 12, right: 12, top: 14, bottom: 14 })
          .alignItems(VerticalAlign.Bottom)
          .height(92)

这是应用最精彩的可视化实现——纯组件柱状图。ForEach 渲染 8 个月的数据柱,每根柱子是一个 Column 容器内含一个 Column(柱体)和 Text(月份标签)。柱体高度由 this.barAnim 驱动:动画启动前高度 '1vp'(几乎不可见),启动后高度按 y.amount / 28860 * 62 比例计算(除以最大值 28860 归一化,乘以 62 得到像素高度),.toFixed(0) 取整,拼接 'vp' 单位。

柱体颜色由 y.amount > 26000 ? '#FFD54F' : '#9C27B0' 决定——超过 26000 的月份(即含年终奖的 2 月)用金色,其余用紫色。这种条件着色让异常高值月份一眼可辨。.animation({ duration: 600, curve: Curve.EaseOut }) 让柱子从 1vp 增长到目标高度时有 600 毫秒的缓出效果,形成"柱子长出来"的动画。

外层 Row.alignItems(VerticalAlign.Bottom) 确保所有柱子底部对齐,.height(92) 限定图表区域高度。这是 ArkUI 中"纯组件实现图表"的经典手法——不依赖第三方图表库,用 Column 的高度变化模拟柱体。

组件 onAppear

setTimeout 150ms

barAnim = true

ForEach 8根柱子重新渲染

每根柱子高度从 1vp 变为目标值

animation 600ms EaseOut 插值

柱子从底部向上生长

金额>26000 的柱子金色, 其余紫色

          Text('2月含年终奖 ¥5,100 · 年度累计 ¥198,810')
            .fontSize(9).fontColor('#9E9E9E').padding({ bottom: 12 })
        }
        .width('100%').backgroundColor('#FFFFFF').borderRadius(14)
        .margin({ left: 12, right: 12, top: 10 })

图表底部附注说明 2 月含年终奖以及年度累计总额,帮助用户理解柱状图的数据含义。这种"图表+注解"的设计让数据可视化更易读。

5.5 省税建议列表

        Column() {
          Text('💡 省税小贴士').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            .width('100%').padding({ left: 14, top: 12 })

          ForEach(['个人养老金账户每年可税前扣除 12,000', '大病医疗支出超过 15,000 部分可扣除', '继续教育学历证书可按 400 元/月扣除', '住房租金按城市标准 1,100-1,500 元/月'], (t: string) => {
            Row() {
              Text('·').fontSize(14).fontColor('#7B1FA2')
              Text(t).fontSize(10).fontColor('#616161').margin({ left: 8 })
            }
            .width('100%').padding({ left: 14, right: 14, top: 7, bottom: 7 })
          }, (t: string) => t)
        }

省税建议用 ForEach 渲染 4 条贴士,每条是一个 Row:紫色项目符号"·"+灰色建议文字。数据源直接用字符串数组内联,ForEach 的 key 用字符串本身(因为内容唯一)。这种"知识列表"在金融应用中非常常见,用于科普税务知识。

5.6 柱状图动画触发

    .width('100%').height('100%')
    .onAppear(() => {
      setTimeout(() => {
        this.barAnim = true
      }, 150)
    })
  }
}

.onAppear() 是 ArkUI 组件的生命周期回调,在组件首次渲染完成后触发。这里在 onAppear 中用 setTimeout 延迟 150 毫秒后设置 this.barAnim = true,触发柱状图从 1vp 增长到目标高度的动画。延迟 150 毫秒是为了让组件完成首次渲染后再启动动画,避免动画与首次渲染冲突导致卡顿。这是 ArkUI 中"入场动画"的标准实现模式。

ArkUI 技术点:onAppear 生命周期
.onAppear() 是 ArkUI 组件的显示生命周期回调,在组件挂载并完成首次渲染后触发。与之对应的还有 .onDisappear()(组件销毁时触发)、.onAreaChange()(组件尺寸变化时触发)等。onAppear 常用于初始化数据请求、启动入场动画、注册事件监听等场景。本应用在多个 Tab 内容组件的 onAppear 中启动各自的动效,确保动画在组件可见后才开始。

六、调薪 Tab(RaiseHistoryContent)逐行剖析

6.1 状态与提薪申请弹窗

@Component
struct RaiseHistoryContent {
  @State showApply: boolean = false
  @State expectSalary: string = ''
  @State raiseReason: string = ''

RaiseHistoryContent 定义三个状态:showApply 控制提薪申请弹窗、expectSalary 记录选中的期望薪资选项、raiseReason 绑定调薪理由文本。注意 expectSalary 初值为空字符串,意味着弹窗打开时没有选项被选中,用户必须主动选择。

@Builder raiseModal() {
  Column() {
    Column() {
      Text('📈 发起调薪申请').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
        .width('100%').padding({ top: 16, left: 16 })

      Column() {
        Text('当前薪资:¥23,000 / 月(基本+岗位)').fontSize(10).fontColor('#9E9E9E')
      }
      .width('100%').padding({ left: 16, top: 8 })

raiseModal 是提薪申请弹窗,居中弹出(.position({ y: '14%' }))。顶部展示当前薪资作为参考基线,帮助用户合理设定期望值。这种"参考值+输入"的表单设计能降低用户的决策负担。

      Column() {
        Text('期望月薪(K)').fontSize(11).fontColor('#757575')
        Row() {
          ForEach(['25', '28', '30', '33'], (s: string) => {
            Text(s + 'K').fontSize(12)
              .fontColor(this.expectSalary === s ? '#FFFFFF' : '#616161')
              .backgroundColor(this.expectSalary === s ? '#7B1FA2' : '#F5F5F5')
              .borderRadius(12).padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .margin({ right: 10 })
              .onClick(() => {
                this.expectSalary = s
              })
          }, (s: string) => s)
        }
        .margin({ top: 8 })
      }

期望薪资用"标签选择"而非自由输入——ForEach 渲染 4 个预设选项(25K/28K/30K/33K),选中态白字紫底。这种"有限选项"设计避免了用户输入不合理的数值,同时简化了交互——点击即选中,比键盘输入更快。与申诉弹窗的异议项目选择器是同一种模式。

      Column() {
        Text('调薪理由').fontSize(11).fontColor('#757575')
        TextArea({ placeholder: '例如:负责核心项目上线,季度绩效连续两次 S', text: this.raiseReason })
          .fontSize(12).height(76).backgroundColor('#F5F5F5').borderRadius(10)
          .onChange((v: string) => {
            this.raiseReason = v
          })
          .margin({ top: 6 })
      }

调薪理由用 TextArea 多行输入,placeholder 提供优秀范例引导。text: this.raiseReason 实现受控输入。与密码弹窗的 TextInput 不同,这里用 TextArea 因为理由通常较长且需要换行。

      Text('申请将提交至直属主管 → 人事委员会 → CEO 审批')
        .fontSize(9).fontColor('#9E9E9E').margin({ top: 10 })

这行审批流程提示非常重要——让用户了解申请的后续流转路径,管理心理预期。在真实企业应用中,这种流程透明化是提升用户体验的关键。

用户点击发起调薪

showApply = true 弹窗

展示当前薪资参考

选择期望月薪标签

expectSalary 更新

选中项白字紫底

填写调薪理由

raiseReason 同步

点击提交申请

showApply = false 关闭

真实场景: 触发审批流

直属主管审核

人事委员会审议

CEO 终审

6.2 调薪轨迹时间轴

build() {
  Stack() {
    Scroll() {
      Column() {
        Column() {
          Text('🚀 三年调薪轨迹').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            .width('100%').padding({ left: 14, top: 12 })

          ForEach(RAISE_RECORDS, (r: RaiseRecord) => {
            Row() {
              Column() {
                Text('📈').fontSize(14)
                  .width(26).height(26).backgroundColor('#F3E5F5').borderRadius(13)
                  .textAlign(TextAlign.Center)
                Column().width(2).layoutWeight(1).backgroundColor('#E1BEE7').margin({ top: 2 })
              }
              .width(26).alignItems(HorizontalAlign.Center)
              .alignSelf(ItemAlign.Stretch)

调薪轨迹用时间轴样式呈现。每条记录是一个 Row:左侧是时间轴节点列,右侧是内容列。节点列包含一个 26x26 的圆形图标(borderRadius(13) 即圆)和一条垂直连接线(Column 宽 2 撑满剩余高度)。.alignSelf(ItemAlign.Stretch) 让节点列在 Row 中纵向拉伸,确保连接线能延伸到下一条记录。这种"图标+连接线"的时间轴结构是 ArkUI 中实现步骤/轨迹可视化的经典手法。

              Column() {
                Row() {
                  Text(r.date).fontSize(10).fontColor('#9E9E9E')
                  Text(r.kind).fontSize(8).fontColor('#7B1FA2').backgroundColor('#F3E5F5')
                    .borderRadius(4).padding({ left: 5, right: 5, top: 1, bottom: 1 }).margin({ left: 6 })
                }

                Row() {
                  Text(r.from).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#9E9E9E')
                  Text(' → ').fontSize(11).fontColor('#9E9E9E')
                  Text(r.to).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#7B1FA2')
                }
                .margin({ top: 4 })

                Text(r.reason).fontSize(9).fontColor('#757575').margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start).layoutWeight(1)
              .padding({ left: 12, bottom: 16 })
            }
            .width('100%')
            .padding({ left: 14, right: 14 })
            .alignItems(VerticalAlign.Top)
            .margin({ top: 10 })
          }, (r: RaiseRecord) => r.id.toString())
        }

内容列包含三行:日期+类型标签、薪资变化(from→to)、调薪原因。from 用灰色 #9E9E9E 表示旧值,to 用紫色 #7B1FA2 表示新值,箭头"→"用灰色,形成"旧→新"的视觉递进。类型标签用浅紫底紫字胶囊,与主题统一。.padding({ left: 12, bottom: 16 }) 确保内容与时间轴节点之间有间距,且每条记录之间有纵向间隔。

6.3 行业薪资对比条形图

        Column() {
          Text('📊 同岗位薪资对比').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            .width('100%').padding({ left: 14, top: 12 })

          ForEach(['您当前 23K', '本市均值 21.4K', 'P75 分位 26K', 'P90 分位 31K'], (c: string, i: number) => {
            Row() {
              Text(c.split(' ')[0]).fontSize(10).fontColor('#616161').width(70)
              Row() {
                Column()
                  .width(['56%', '52%', '63%', '75%'][i])
                  .height(12).backgroundColor(i === 0 ? '#7B1FA2' : '#CE93D8')
                  .borderRadius(6)
              }
              .width('62%').height(12).backgroundColor('#F5F5FA').borderRadius(6)

              Text(c.split(' ')[1]).fontSize(11).fontWeight(FontWeight.Bold)
                .fontColor(i === 0 ? '#7B1FA2' : '#616161').margin({ left: 8 })
            }
            .width('100%').padding({ left: 14, right: 14, top: 9, bottom: 9 })
          }, (c: string) => c)
        }

行业对比用水平条形图呈现。每行三段式:标签(width(70) 固定宽度)、条形图、数值。条形图由两层 Row 组成:外层浅灰底条(backgroundColor('#F5F5FA'))作为背景轨道,内层彩色条(backgroundColor 根据 i 区分)作为数据条,宽度从数组 ['56%', '52%', '63%', '75%'] 取值。用户自己(i === 0)用深紫 #7B1FA2,其余用浅紫 #CE93D8,让"自己"在对比中突出。

这种"轨道+填充"的水平条形图是 ArkUI 中另一种纯组件图表实现——不需要 Chart 组件,用 Row 的嵌套和宽度百分比即可实现。相比柱状图,水平条形图更适合展示标签较长的对比数据。

6.4 申请按钮与阴影

        Button() {
          Text('+ 发起调薪申请').fontSize(14).fontColor('#FFFFFF')
        }
        .width('60%').height(42).backgroundColor('#7B1FA2').borderRadius(21)
        .margin({ top: 18, bottom: 24 })
        .shadow({ radius: 8, color: 'rgba(123,31,162,0.35)', offsetY: 4 })
        .onClick(() => {
          this.showApply = true
        })

申请按钮是调薪 Tab 的核心操作入口。.shadow() 添加阴影:radius: 8 模糊半径、color: 'rgba(123,31,162,0.35)' 紫色半透明阴影、offsetY: 4 向下偏移 4 像素。这种"悬浮按钮"的阴影效果让按钮看起来有立体感和可点击感,是金融应用中 CTA(Call to Action)按钮的标准视觉处理。

ArkUI 技术点:shadow 阴影属性
.shadow() 是 ArkUI 的阴影修饰器,参数包括 radius(模糊半径)、color(阴影颜色)、offsetX/offsetY(偏移量)。阴影是营造组件层次感和深度感的关键手段。在金融类应用中,主操作按钮通常带有品牌色阴影(如本例的紫色阴影),既突出按钮又强化品牌识别。阴影的颜色使用 rgba 带透明度,避免纯黑阴影过于生硬。

七、社保 Tab(SocialFundContent)逐行剖析

7.1 社保基数卡与占比条动画

@Component
struct SocialFundContent {
  @State fundAnim: boolean = false

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Column() {
              Text('社保基数').fontSize(9).fontColor('#E1BEE7')
              Text('¥16,000').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start).layoutWeight(1)

            Column() {
              Text('公积金月缴存').fontSize(9).fontColor('#E1BEE7')
              Text('¥3,840').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFD54F').margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%').padding({ left: 16, right: 16, top: 14 })

SocialFundContent 只有一个状态 fundAnim,用于驱动占比条动画。社保基数卡结构与个税概览卡一致——左侧社保基数、右侧公积金月缴存,紫色渐变背景。这种跨 Tab 的卡片结构一致性是良好用户体验的体现——用户学会一种卡片的读法后,在其他 Tab 中能快速理解同类卡片。

          Row() {
            Column()
              .width(this.fundAnim ? '37%' : '0%').height(10)
              .backgroundColor('#FFD54F').borderRadius({ topLeft: 5, bottomLeft: 5 })
              .animation({ duration: 700, curve: Curve.EaseOut })
            Column()
              .width(this.fundAnim ? '63%' : '0%').height(10)
              .backgroundColor('#E1BEE7').borderRadius({ topRight: 5, bottomRight: 5 })
              .animation({ duration: 700, curve: Curve.EaseOut })
          }
          .width('100%').padding({ left: 16, right: 16, top: 12 })

这是占比条动画的实现——两个 Column 并排,宽度分别从 0% 动画增长到 37%63%,代表个人和单位的缴存比例。金色条 #FFD54F 代表个人部分,浅紫条 #E1BEE7 代表单位部分。.borderRadius 分别只设置相邻圆角(左条左圆角、右条右圆角),拼合后形成完整的圆角条。.animation({ duration: 700 }) 让宽度变化有 700 毫秒的平滑过渡,形成"条形从中间向两侧展开"的效果。

          Row() {
            Row() {
              Column().width(8).height(8).borderRadius(4).backgroundColor('#FFD54F')
              Text('个人 ¥3,600').fontSize(9).fontColor('#F3E5F5').margin({ left: 4 })
            }
            .margin({ right: 16 })

            Row() {
              Column().width(8).height(8).borderRadius(4).backgroundColor('#E1BEE7')
              Text('单位 ¥6,054').fontSize(9).fontColor('#F3E5F5').margin({ left: 4 })
            }
          }
          .width('100%').padding({ left: 16, bottom: 14, top: 8 })
        }
        .width('100%').linearGradient({ angle: 135, colors: [['#6A1B9A', 0], ['#8E24AA', 1]] })
        .borderRadius(14).margin({ left: 12, right: 12, top: 10 })

图例行用两个小色块+文字说明,帮助用户理解占比条的颜色含义。色块用 Column 设置 width(8).height(8).borderRadius(4) 实现——8x8 像素的圆角小方块。这种"色块图例"是数据可视化的标准配套元素。

组件 onAppear

setTimeout 150ms

fundAnim = true

两根占比条宽度从 0% 变为 37% 和 63%

animation 700ms EaseOut

条形平滑展开

金色个人 37%, 浅紫单位 63%

7.2 五险一金明细表

        Column() {
          Text('🛡️ 五险一金明细').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            .width('100%').padding({ left: 14, top: 12 })

          Row() {
            Text('险种').fontSize(9).fontColor('#9E9E9E').layoutWeight(1.2)
            Text('个人').fontSize(9).fontColor('#9E9E9E').layoutWeight(1)
            Text('单位').fontSize(9).fontColor('#9E9E9E').layoutWeight(1)
            Text('比例').fontSize(9).fontColor('#9E9E9E').layoutWeight(1)
          }
          .width('100%').padding({ left: 14, right: 14, top: 10 })

          ForEach(INSURANCE_ROWS, (r: InsuranceRow, i: number) => {
            Row() {
              Text(r.name).fontSize(11).fontColor('#212121').layoutWeight(1.2)
              Text('¥' + r.personal).fontSize(10).fontColor('#F44336').layoutWeight(1)
              Text('¥' + r.company).fontSize(10).fontColor('#43A047').layoutWeight(1)
              Text(r.rate).fontSize(9).fontColor('#9E9E9E').layoutWeight(1)
            }
            .width('100%').padding({ left: 14, right: 14, top: 10, bottom: 10 })
            .backgroundColor(i % 2 === 0 ? '#FAF5FC' : '#FFFFFF')
            .borderRadius(8)
          }, (r: InsuranceRow) => r.id.toString())

五险一金明细表用 ForEach 渲染 6 行数据。表头和每行使用相同的 layoutWeight 比例(1.2:1:1:1),保证列对齐。个人缴存金额用红色 #F44336、单位缴存用绿色 #43A047,这种"红扣绿加"的配色与工资条明细一致——个人缴存是"从工资中扣除"所以红色,单位缴存是"额外福利"所以绿色。

.backgroundColor(i % 2 === 0 ? '#FAF5FC' : '#FFFFFF') 实现斑马纹效果——偶数行浅紫底、奇数行白底,提升长表格的可读性。这种交替行背景是数据表格的经典设计模式。

          Divider().color('#F5F5F5').margin({ top: 8 })

          Row() {
            Text('合计').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#212121').layoutWeight(1.2)
            Text('¥3,600').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#F44336').layoutWeight(1)
            Text('¥6,054').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#43A047').layoutWeight(1)
            Text('-').fontSize(10).fontColor('#9E9E9E').layoutWeight(1)
          }
          .width('100%').padding({ left: 14, right: 14, top: 10, bottom: 12 })
        }

合计行用 Divider 分隔后追加,结构与数据行一致但字号更大(11 vs 10/9)、字重加粗,强调汇总性质。个人合计 ¥3,600、单位合计 ¥6,054,让用户一目了然地看到五险一金的总成本。

7.3 公积金贷款测算卡

        Column() {
          Text('🏠 公积金贷款额度测算').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            .width('100%').padding({ left: 14, top: 12 })

          Row() {
            Column() {
              Text('账户余额').fontSize(9).fontColor('#9E9E9E')
              Text('¥68,400').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#7B1FA2').margin({ top: 3 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)

            Column().width(1).height(30).backgroundColor('#E0E0E0')

            Column() {
              Text('可贷额度').fontSize(9).fontColor('#9E9E9E')
              Text('¥684,000').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#7B1FA2').margin({ top: 3 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)

            Column().width(1).height(30).backgroundColor('#E0E0E0')

            Column() {
              Text('连缴月数').fontSize(9).fontColor('#9E9E9E')
              Text('38 个月').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#7B1FA2').margin({ top: 3 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
          }
          .width('100%').padding({ top: 12, bottom: 14 })

贷款测算卡用三列等分布局展示关键指标。三列之间用 Column().width(1).height(30).backgroundColor('#E0E0E0') 渲染 1 像素宽的竖线作为分隔符,这是移动端"三栏指标卡"的标准设计——比边框更轻量、视觉更简洁。每列包含标签和数值,数值用紫色加粗强调。

          Text('按余额 10 倍与最高限额取低值,具体以公积金中心审批为准')
            .fontSize(9).fontColor('#9E9E9E').padding({ bottom: 14 })
        }
        .width('100%').backgroundColor('#FFFFFF').borderRadius(14)
        .margin({ left: 12, right: 12, top: 10, bottom: 20 })

底部附注说明测算规则和免责声明。在金融应用中,测算结果都应附带"以实际审批为准"的说明,避免用户将模拟值当作确定承诺。这是金融合规设计的体现。

社保 Tab 架构

社保基数卡 - 紫色渐变

五险一金明细表 - 斑马纹

公积金贷款测算 - 三栏指标

左: 社保基数

右: 公积金月缴存

占比条动画 fundAnim

表头: 险种/个人/单位/比例

6行数据 ForEach

合计行 Divider 分隔

账户余额

可贷额度

连缴月数

竖线分隔符

测算规则附注

八、积分 Tab(PointMallContent)逐行剖析

8.1 状态与积分兑换弹窗

@Component
struct PointMallContent {
  @State showExchange: boolean = false
  @State pickedGoods: string = '视频会员月卡'
  @State pickedPoints: number = 800
  @State exAnim: boolean = false

PointMallContent 定义四个状态:showExchange 控制兑换弹窗、pickedGoods 记录选中商品名、pickedPoints 记录选中商品所需积分、exAnim 驱动兑换后不可退回提示的动画。注意 pickedGoodspickedPoints 的初值对应第一件商品,但实际弹窗打开时会通过点击商品重新赋值。

@Builder exchangeModal() {
  Column() {
    Column() {
      Text('🎁 积分兑换确认').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
        .width('100%').padding({ top: 16, left: 16 })

      Row() {
        Text('📺').fontSize(36)
        Column() {
          Text(this.pickedGoods).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121')
          Row() {
            Text('有效期 30 天').fontSize(9).fontColor('#9E9E9E')
            if (this.exAnim) {
              Text('· 兑换后不可退回').fontSize(9).fontColor('#FB8C00').margin({ left: 4 })
            }
          }
          .margin({ top: 3 })
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 12 })
      }

exchangeModal 是积分兑换确认弹窗。商品信息行用 Row 展示大图标和商品名+说明。注意"兑换后不可退回"提示由 if (this.exAnim) 条件渲染——exAnim 在组件 onAppear 后 100 毫秒设为 true,这条提示会延迟出现,形成"先展示基础信息、再追加警示"的渐进式信息披露。

      Column() {
        Row() {
          Text('当前积分').fontSize(11).fontColor('#757575')
          Text('3,260').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#7B1FA2').margin({ left: 8 })
        }
        .width('100%').padding({ left: 14, top: 12 })

        Row() {
          Text('消耗积分').fontSize(11).fontColor('#757575')
          Text('-' + this.pickedPoints.toString()).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#F44336').margin({ left: 8 })
        }
        .width('100%').padding({ left: 14, top: 8 })

        Divider().color('#F5F5F5').margin({ top: 10, bottom: 10 })

        Row() {
          Text('兑换后剩余').fontSize(11).fontColor('#757575')
          Text((3260 - this.pickedPoints).toString() + ' 分').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#43A047').margin({ left: 8 })
        }
        .width('100%').padding({ left: 14, bottom: 12 })
      }
      .width('100%').backgroundColor('#FAF5FC').borderRadius(10)
      .margin({ left: 16, right: 16, top: 14 })

积分计算卡是兑换弹窗的核心信息区。三行数据:当前积分(紫色)、消耗积分(红色带负号)、兑换后剩余(绿色)。Divider 分隔消耗与剩余,强调"剩余"是计算结果。注意 (3260 - this.pickedPoints) 是运行时计算——当用户选择不同商品时,pickedPoints 变化,剩余积分自动重算。这是 ArkUI 响应式计算在数据展示中的体现。整卡用浅紫底 #FAF5FC 与白色弹窗背景区分。

8.2 积分总览卡与规则

      Row() {
        Column() {
          Text('当前积分').fontSize(10).fontColor('#E1BEE7')
          Row() {
            Text('3').fontSize(30).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text(' 分').fontSize(13).fontColor('#E1BEE7').margin({ top: 10 })
          }
        }
        .alignItems(HorizontalAlign.Start).layoutWeight(1)

        Column() {
          Text('本月新增 +480').fontSize(10).fontColor('#FFD54F')
          Text('签到 · 全勤 · 加班折算').fontSize(9).fontColor('#E1BEE7').margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%').padding(16)
      .linearGradient({ angle: 135, colors: [['#6A1B9A', 0], ['#AB47BC', 1]] })
      .borderRadius(14).margin({ left: 12, right: 12, top: 10 })

积分总览卡的设计有个巧妙之处——积分数字"3"用 fontSize(30) 超大字号,而"分"用 fontSize(13) 小字号并通过 .margin({ top: 10 }) 底部对齐。这种"大数字+小单位"的设计让积分值成为视觉焦点,同时单位不干扰数字阅读。右侧展示本月新增和来源说明,金色"+480"是积极信号。

      Row() {
        ForEach(['每日签到 +5', '全勤月 +100', '加班1h +10', '提案采纳 +200'], (r: string) => {
          Text(r).fontSize(9).fontColor('#7B1FA2').backgroundColor('#F3E5F5')
            .borderRadius(10).padding({ left: 8, right: 8, top: 4, bottom: 4 })
            .margin({ right: 8 })
        }, (r: string) => r)
      }
      .width('100%').padding({ left: 12, top: 12 })

积分规则用胶囊标签展示四条获取途径。浅紫底紫字的胶囊与全局主题统一。这组标签是"知识提示"而非可交互元素,纯展示积分获取规则。

8.3 兑换商城网格(Flex 布局)

      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(POINT_GOODS, (g: PointGoods) => {
          Column() {
            Row() {
              Text(g.icon).fontSize(30)

              if (g.hot) {
                Text('热门').fontSize(8).fontColor('#FFFFFF').backgroundColor('#F44336')
                  .borderRadius(6).padding({ left: 5, right: 5, top: 1, bottom: 1 })
                  .position({ x: 76, y: 0 })
              }
            }
            .width('100%').height(36)

            Text(g.name).fontSize(11).fontColor('#212121')
              .maxLines(1).margin({ top: 6 })

            Row() {
              Text(g.points.toString() + '分').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#7B1FA2')
                .layoutWeight(1)
              Text('兑换').fontSize(9).fontColor('#FFFFFF').backgroundColor('#7B1FA2')
                .borderRadius(9).padding({ left: 9, right: 9, top: 3, bottom: 3 })
                .onClick(() => {
                  this.pickedGoods = g.name
                  this.pickedPoints = g.points
                  this.showExchange = true
                })
            }
            .width('100%').margin({ top: 6 })
          }
          .width('46%').backgroundColor('#FFFFFF').borderRadius(12)
          .padding(10).margin(6)
        }, (g: PointGoods) => g.id.toString())
      }
      .width('100%').padding({ left: 6, right: 6, top: 8 })

这里是积分商城的核心——商品网格。使用 Flex({ wrap: FlexWrap.Wrap }) 实现换行排列,每个商品卡片宽度 46%,配合 .margin(6) 的间距,每行刚好放两个商品。FlexWrap.Wrap 让子元素在一行放不下时自动换行,这是实现"自适应网格"的关键。

ArkUI 技术点:Flex 组件详解
Flex 是 ArkUI 的弹性布局容器,是 CSS Flexbox 在鸿蒙中的实现。与 Row/Column 不同,Flex 支持子元素的换行排列。常用参数:wrap(换行模式,取 FlexWrap.NoWrap/Wrap/WrapReverse)、direction(主轴方向,取 FlexDirection.Row/RowReverse/Column/ColumnReverse)、justifyContent(主轴对齐)、alignItems(交叉轴对齐)。Flex 适合实现网格、标签流、自适应排列等场景。本例用 FlexWrap.Wrap 实现两列商品网格,是积分商城的典型布局。

每个商品卡片内部结构:图标行(带条件渲染的"热门"标签)、商品名、积分+兑换按钮行。"热门"标签用 .position({ x: 76, y: 0 }) 绝对定位到卡片右上角,这是 Stack 式的定位效果在 Row 内的实现——通过 position 脱离文档流定位。.maxLines(1) 限制商品名为单行,避免长名称破坏卡片高度。

点击"兑换"按钮时,三个状态同时更新:pickedGoods(商品名)、pickedPoints(所需积分)、showExchange(弹窗显隐)。这种"多状态联动"是复杂交互的典型模式——一个用户动作触发多个状态变更,最终反映为弹窗展示对应商品信息。

8.4 积分明细列表

      Column() {
        Text('🕘 积分明细').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
          .width('100%').padding({ left: 14, top: 12 })

        ForEach(['08-26 签到 +5', '08-25 全勤月奖励 +100', '08-24 加班 2 小时 +20', '08-20 优化提案采纳 +200', '08-15 节日活动 +80', '08-01 签到连续 7 天 +35'], (l: string) => {
          Row() {
            Text(l.split(' ')[0]).fontSize(10).fontColor('#9E9E9E').width(44)
            Text(l.substring(3)).fontSize(11).fontColor('#212121').layoutWeight(1)
            Text('+' + l.split('+')[1]).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#43A047')
          }
          .width('100%').padding({ left: 14, right: 14, top: 9, bottom: 9 })
        }, (l: string) => l)
      }

积分明细列表用字符串数组作为数据源,通过 .split(' ')[0] 提取日期、.substring(3) 提取描述、.split('+')[1] 提取积分值。这种"字符串解析渲染"是轻量数据处理的技巧——当数据结构简单且不需要复杂操作时,用字符串编码数据可以省去 interface 定义。但在真实工程中,更推荐用结构化数据以便维护。

每行三段式:日期(固定宽度 44)、描述(layoutWeight(1) 撑满)、积分(绿色加粗带"+"号)。绿色积分值让"获得"的积极信号一目了然。

积分 Tab 架构

积分总览卡

积分规则标签条

兑换商城 Flex 网格

积分明细列表

大数字 3 分

本月新增 +480

Flex Wrap 两列网格

热门标签 绝对定位

兑换按钮 触发弹窗

8件商品 ForEach

字符串解析渲染

日期/描述/积分三段

九、我的 Tab(SalaryMineContent)逐行剖析

9.1 状态与删除申诉弹窗

@Component
struct SalaryMineContent {
  @State showDelete: boolean = false
  @State ringAnim: boolean = false

SalaryMineContent 定义两个状态:showDelete 控制删除申诉警示弹窗、ringAnim 驱动年度收入进度环动画。这个 Tab 是用户个人薪酬档案的汇总页,包含个人卡片、进度环、申诉记录、功能列表四个区块。

@Builder deleteAppealModal() {
  Column() {
    Column() {
      Text('🗑️').fontSize(34).margin({ top: 18 })
      Text('删除这条申诉记录?').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#212121')
        .margin({ top: 10 })
      Text('申诉「绩效奖金系数有误」于 8月12日提交,\n删除后不可恢复,请谨慎操作。')
        .fontSize(11).fontColor('#9E9E9E').textAlign(TextAlign.Center).margin({ top: 8 })

deleteAppealModal 是删除确认弹窗,采用警示风格设计。顶部垃圾桶图标、加粗标题、说明文字(含 \n 换行)。.textAlign(TextAlign.Center) 让说明文字居中,符合警示弹窗的视觉规范——警示信息应该居中展示以引起注意。

  }, 150)
})

}
}


---
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/15e5702723f245328a53deb4a6a63897.png#pic_center)

### 10.7 总结

从 **状态管理维度** 看,整个应用基于 `@State` 单层组件内部状态构建,未使用 `@Prop`、`@Link`、`@Provide`/`@Consume` 等跨组件状态传递装饰器。这意味着每个 Tab 内容组件是独立的状态孤岛——Tab 切换时旧组件销毁、状态清空,新组件创建、状态初始化。这种设计的优势是组件隔离度高、无状态污染;劣势是无法跨 Tab 共享状态(如解锁状态不跨 Tab 保持)。在当前业务场景下,这种取舍是合理的——每个 Tab 的状态确实不需要共享。

从 **UI 复用维度** 看,应用通过 `@Builder` 实现了组件内 UI 片段复用(如 `modalOverlay` 遮罩),但未使用 `@Builder` 的全局复用形式(`@Builder MyBuilder` 函数式声明),也未使用自定义 `@Component` 做更高级的组件抽象。所有弹窗、卡片、列表项都在各自的 `@Component` 内部用 `@Builder` 封装。在中等复杂度应用中,这种"组件内复用"的抽象层次足够;当应用规模增长到需要跨组件复用卡片模板时,可以升级为全局 `@Builder` 或独立 `@Component`。

从 **动画系统维度** 看,应用全面采用声明式动画——`.animation()` 修饰器附加到属性上,当属性值变化时自动插值。所有动画的触发方式统一为"状态变量从初值变为目标值",动画曲线统一为 `Curve.EaseOut`。这种"一处定义、处处生效"的动画模式是 ArkUI 区别于命令式 UI 框架的核心优势。应用中未使用 `animateTo` 闭包式动画、`transition` 转场动画、`sharedTransition` 共享元素动画等高级动画 API,说明当前动效需求用基础声明式动画即可满足。

从 **布局体系维度** 看,应用覆盖了 ArkUI 的四大核心布局容器:`Column`(纵向排列)、`Row`(横向排列)、`Stack`(堆叠叠加)、`Flex`(弹性换行)。`Scroll` 作为滚动容器包裹所有可滚动内容。`layoutWeight` 弹性权重被大量用于比例分配空间。`.position()` 绝对定位用于弹窗定位和热门标签定位。`.margin()` 和 `.padding()` 精细控制间距。这套布局工具链足以构建任何中等复杂度的移动端界面。

从 **数据建模维度** 看,应用用 7 个 `interface` 定义了全部业务数据结构,用 1 个 `enum` 定义了 Tab 枚举,用 7 个 `const` 数组定义了静态数据源。所有数据都是强类型的,没有 `any` 类型滥用。金额数据用字符串预格式化,避免了运行时数字格式化的开销。这种"接口先行、静态常量、强类型约束"的数据建模方式是 ArkTS 推荐的最佳实践。

从 **交互设计维度** 看,应用实现了密码验证、表单编辑、标签选择、文本输入、列表点击、按钮触发等多种交互模式。所有交互都通过 `@State` 变量驱动 UI 更新,遵循"交互→状态变更→视图响应"的声明式闭环。弹窗的打开和关闭是状态驱动的,列表的选择是状态驱动的,动效的触发也是状态驱动的。这种"万物皆状态"的设计哲学是声明式 UI 的精髓。

从 **安全设计维度** 看,工资条 Tab 实现了密码验证门禁——未验证身份时展示加密占位卡,点击验证才弹出密码弹窗,验证通过后展示明细。这是金融类应用"敏感信息分级展示"的典型设计。删除申诉记录时也有二次确认弹窗,防止误操作。这种"敏感操作前置验证、破坏操作二次确认"的安全意识贯穿全应用。

从 **视觉设计维度** 看,应用采用统一的紫色金融风格——主色 `#7B1FA2`,渐变色从 `#4A148C` 到 `#AB47BC`,辅助色金色 `#FFD54F`、绿色 `#43A047`(正数/应发)、红色 `#F44336`(负数/扣款)、橙色 `#FB8C00`(警告/待发)。所有卡片用 `borderRadius(14)` 圆角,所有按钮用 `borderRadius(19-21)` 胶囊形。间距统一使用 12/14/16 像素的倍数。这种"色板统一、圆角统一、间距统一"的设计规范让应用在视觉上高度一致。
Logo

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

更多推荐