基于HarmonyOS API 24的ArkTS声明式UI实战:HarmonyOS 6.1.1薪薪资管家应用的多Tab架构与状态驱动动效深度解析
一、鸿蒙技术栈与声明式UI范式背景
HarmonyOS 6.1.1 作为华为全场景分布式操作系统的最新演进版本,其应用开发框架已经进入高度成熟阶段。在这个版本中,ArkUI 声明式范式成为构建用户界面的主流方式,它借鉴了现代前端框架的声明式编程思想,同时又针对鸿蒙的跨设备分布式场景做了深度优化。开发者通过描述界面"是什么"而非"怎么做"的方式,让框架接管渲染管线的调度,从而实现高效的 UI 更新与差异比对。
ArkTS 是鸿蒙生态专属的编程语言,它在 TypeScript 的基础上做了严格的静态类型约束与运行时增强。与普通 TypeScript 不同,ArkTS 在编译期会进行更激进的类型检查,禁止使用某些动态特性(如 any 类型的滥用、运行时对象结构变更等),这种设计取舍换来的是 AOT 编译优化的更大空间与更低的运行时开销。在 HarmonyOS ArkTS API 24 中,ArkTS 的类型系统进一步强化了接口约束,所有组件状态、数据模型都必须以明确的 interface 或 class 声明,这正是本应用中大量使用 interface 定义数据模型的根本原因。
HarmonyOS 6.1.1 的新特性集中体现在几个方面。首先是 ArkUI 组件库的持续扩充,Progress、Flex、Scroll 等容器组件在 API 24 层级获得了更丰富的属性与样式控制能力。其次是动画系统的优化,animation 修饰器可以更细粒度地附加到单个属性上,配合 Curve 枚举提供的多种缓动曲线,开发者无需手写定时器即可实现丝滑的过渡效果。再者是状态管理体系的完善,@State、@Builder、@Component 等装饰器构成了从组件内部状态到 UI 片段复用的完整链路。
声明式 UI 的核心思想是"数据驱动视图"。当状态变量发生变化时,框架自动触发依赖该变量的 UI 片段重新渲染,这一过程不需要开发者手动调用 setState 或 invalidate 之类的方法。在 HarmonyOS ArkTS API 24 中,这种响应式机制通过 VDOM(虚拟 DOM)的 diff 算法实现,框架会精准定位变化的节点并最小化地更新真实组件树。本应用中,Tab 切换、弹窗显隐、金额翻牌、柱状图增长等动效,全部依赖这一响应式机制。
从业务角度看,这是一个企业人事场景下的薪薪资管家应用(产品名"薪朋友")。它面向已入职员工,提供工资条查询、个税测算、调薪轨迹追溯、社保公积金明细、积分商城兑换、个人薪酬档案等六大核心功能。应用采用底部六 Tab 架构,融合金融级紫色视觉风格,配合密码验证弹窗、申诉表单、调薪申请、积分兑换确认、删除警示等多种交互弹窗,以及柱状图、进度环、占比条、翻牌缩放等数据可视化动效,是一个功能密度极高的鸿蒙原生应用范例。
二、应用整体架构与数据模型设计
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 接口。id 为 number 类型,作为 ForEach 渲染时的 key 生成依据;month 用字符串而非 Date 对象,是因为鸿蒙组件的时间格式化能力有限,直接预格式化为展示字符串更高效。net 和 total 均为字符串,内含千分位格式,避免运行时格式化开销。state 用中文字符串而非枚举,是为了直接渲染到 Text 组件而无需映射层。
ArkUI 技术点:interface 与类型安全
在 ArkTS 中,interface不仅是一种类型约束,更是框架进行依赖追踪的基础。当@State变量绑定了一个interface数组时,框架能精确感知数组元素的结构变化。与any或object类型不同,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 字符无需加载图片资源,渲染开销极低且跨设备一致。
2.3 Tab 枚举与元数据
enum SalaryTab { SLIP, TAX, RAISE, SOCIAL, POINT, MINE }
这里用 enum 定义了六个 Tab 的枚举常量。ArkTS 的 enum 本质是数值枚举,SLIP 对应 0,TAX 对应 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 的元数据结构,icon 和 label 分别对应底部 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 方法与整体布局

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同上)、layoutWeight。Row常用于横向并排的标签、金额左右分布、按钮组等场景。Row和Column可以互相嵌套,构建复杂的二维布局。
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——idx 是 ForEach 的索引参数,类型为 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 中"交互→状态变更→视图更新"闭环的关键一环。
四、工资条 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) 确保遮罩层在普通内容之上,但低于弹窗本体(弹窗 zIndex 为 999)。
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(最大长度)、backgroundColor、borderRadius。onChange是值变化回调,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(...) }传入子组件实现自定义内容。常用属性:backgroundColor、borderRadius、width、height、type(取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 支持换行,适合长文本说明场景。
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内部只能有一个直接子元素(通常是Column或Row),所有可滚动内容放在这个子元素内部。本应用所有 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.showLock 为 true 时,先渲染遮罩层(传入关闭回调),再渲染密码弹窗。当 this.showAppeal 为 true 时,同理渲染申诉遮罩和申诉抽屉。由于 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 控制专项扣除编辑弹窗显隐;childEdu、houseLoan、elderly 分别绑定三项专项扣除的输入值,初值为标准扣除额;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 })
每组输入项的结构:标签 Text、Row 内含"¥"符号、TextInput 输入框、标准值说明。TextInput 的 text: 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 的高度变化模拟柱体。
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 })
这行审批流程提示非常重要——让用户了解申请的后续流转路径,管理心理预期。在真实企业应用中,这种流程透明化是提升用户体验的关键。
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 像素的圆角小方块。这种"色块图例"是数据可视化的标准配套元素。
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(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 驱动兑换后不可退回提示的动画。注意 pickedGoods 和 pickedPoints 的初值对应第一件商品,但实际弹窗打开时会通过点击商品重新赋值。
@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(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)
})
}
}
---

### 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 像素的倍数。这种"色板统一、圆角统一、间距统一"的设计规范让应用在视觉上高度一致。
更多推荐

所有评论(0)