鸿蒙ArkTS声明式UI实战解析:厂区理化实验室检测分析系统的全栈构建
引言:鸿蒙开发背景与ArkTS语言体系
在万物互联的时代浪潮下,华为鸿蒙操作系统(HarmonyOS)凭借其分布式架构、一次开发多端部署的核心优势,已经成长为智能终端领域最具影响力的操作系统之一。鸿蒙操作系统不仅仅是一个手机操作系统,它更是一个面向全场景的分布式操作系统,覆盖了手机、平板、智慧屏、智能穿戴、车机等多种终端设备。对于开发者而言,鸿蒙提供了一套全新的开发范式——基于ArkUI框架的声明式UI开发模式,配合ArkTS这一专为鸿蒙生态设计的编程语言,使得开发者能够以极少的代码量构建出功能丰富、交互流畅、视觉精美的应用程序。
ArkTS是在TypeScript基础上扩展而来的编程语言,它继承了TypeScript的静态类型检查、接口定义、泛型等核心特性,同时针对鸿蒙的声明式UI框架进行了深度定制和优化。在ArkTS中,开发者通过struct结构体来定义组件,通过一系列装饰器(如@Entry、@Component、@State、@Builder、@Prop、@Link等)来声明组件的元数据和行为特征。这种编程范式与传统的命令式UI开发有着本质区别——开发者不再需要手动操作DOM节点或调用大量的API来更新视图,而是只需要声明界面的状态和数据,框架会自动追踪状态变化并驱动UI的重新渲染。这种"数据驱动视图"的思想,极大地简化了复杂界面的构建过程,也减少了因手动操作视图而引入的各类Bug。
声明式UI范式的核心思想可以用一句话概括:“界面是状态的函数映射”。在ArkTS中,开发者定义的每一个@State状态变量,都会被框架自动监听。当这些变量的值发生变化时,框架会自动触发依赖该变量的UI组件进行重新渲染,这个过程是声明式的、响应式的、自动化的。这意味着开发者只需要关注"界面应该长什么样"以及"数据如何变化",而不需要关心"如何从旧界面过渡到新界面"——框架会自动完成这些繁琐的工作。在本案例代码中,我们可以看到大量的@State变量被用于控制弹窗的显示与隐藏(如showAdd、showEdit、showDel、showDetail),控制当前选中的标签页(currentTab),以及存储列表数据(如sampleList、instList、reagList等),这些都是声明式UI范式的典型应用场景。
ArkUI组件体系是鸿蒙声明式UI的基石。ArkUI提供了丰富的内置组件,从最基础的容器组件(Column、Row、Stack、Flex、Scroll等)到功能组件(Text、Image、Button、TextInput、Progress、Toggle等),再到布局能力(layoutWeight、justifyContent、alignItems等),形成了一套完整、高效、易用的组件化开发体系。容器组件负责组织子组件的排布方式,功能组件负责呈现具体的内容,布局能力则负责微调组件之间的空间分配和对齐关系。在本案例代码中,我们能够看到几乎全部ArkUI核心组件的综合运用——从顶部的导航卡片到中部的统计面板,从底部的列表渲染到弹出的模态对话框,每一个界面模块都充分运用了ArkUI组件体系的能力。更为精妙的是,代码通过@Builder装饰器将可复用的UI片段封装为独立的构建函数,通过ForEach实现列表数据的动态渲染,通过条件分支(if-else)实现标签页的切换逻辑,这些高级技巧共同构建出了一个功能完备、结构清晰的工业级实验室管理应用界面。
一、数据模型层:接口定义与类型安全
1.1 六大数据结构接口
在任何一座现代化的厂区理化实验室中,检测分析工作涉及的核心实体无外乎样品、仪器、试剂、检测任务、报告和人员。在ArkTS中,我们通过interface关键字来定义这些实体的数据结构,确保整个应用在编译期就能获得完整的类型安全保障。
interface SampleItem {
code: string
name: string
type: string
source: string
state: string
num: number
}
interface InstItem {
code: string
name: string
model: string
calib: string
state: string
}
interface ReagItem {
code: string
name: string
spec: string
stock: number
warn: number
state: string
}
interface TestItem {
code: string
name: string
method: string
deadline: string
state: string
}
interface ReportItem {
code: string
name: string
type: string
time: string
state: string
}
interface MyItem {
icon: string
title: string
desc: string
}

这段代码定义了六个核心接口,分别对应实验室管理系统中的六大数据实体。SampleItem描述了样品信息,包含编号(code)、名称(name)、类型(type)、来源(source)、状态(state)和数量(num)六个字段;InstItem描述了仪器信息,特别包含了校准日期(calib)字段,这对于实验室仪器的合规管理至关重要;ReagItem描述了试剂信息,除了库存数量(stock)之外还特别包含了预警阈值(warn),这是一个典型的库存预警模型。
在ArkTS中,interface是纯类型声明,编译后不会产生任何运行时代码。它仅用于编译期的类型检查,确保开发者在编写代码时不会出现类型不匹配的错误。这与传统的JavaScript运行时类型检查形成了鲜明对比——在JavaScript中,类型错误往往只在运行时才暴露出来,而ArkTS借助TypeScript的类型系统,将这些问题提前到了编译阶段。
从数据建模的角度来看,这六个接口的设计遵循了"最小完备"原则:每个接口只包含该实体在当前业务场景中真正需要的字段,不多也不少。例如SampleItem没有包含采样日期、保存条件等字段(这些信息在详情弹窗中通过硬编码的方式展示),这是因为列表视图中并不需要展示这些详细字段。这种按需建模的思路,使得数据结构保持精简,减少了不必要的数据传输和存储开销。
1.2 类型安全在列表渲染中的价值
当这些接口定义完成后,它们将被用于标注@State状态变量的类型。例如在SampleView组件中,sampleList被声明为SampleItem[]类型,这意味着该数组中的每一个元素都必须符合SampleItem的结构定义。当后续代码通过ForEach遍历该数组并渲染列表项时,编译器能够确保每个it.code、it.name等属性访问都是类型安全的——如果开发者误写了不存在的属性名(如it.price),编译器会立即报错。
TypeScript/ArkTS的类型系统不仅仅是"编译期检查工具",它更是一种"自文档化"的机制。当其他开发者阅读sampleList: SampleItem[]这行代码时,无需跳转到初始化代码,就能立刻知道该列表中每个元素包含哪些字段、各自是什么类型。这大大提升了团队协作中的代码可读性和可维护性。
二、入口组件:@Entry与@Component的协作
2.1 组件入口的定义
在ArkTS中,一个页面(即一个路由可达的视图)必须有一个被@Entry装饰器标注的组件作为入口。@Entry装饰器告诉框架:"这个组件是一个页面的根组件,可以被路由系统直接加载。"而@Component装饰器则声明了一个自定义组件,使该struct可以被其他组件引用或在build方法中渲染。
@Entry
@Component
struct Index {
@State currentTab: number = 0
private tabs: string[] = ['样品', '仪器', '试剂', '检测', '报告', '我的']
private icons: string[] = ['🧪', '🔬', '🧴', '📋', '📄', '👤']
@Builder tabCard(t: string, icon: string, desc: string, i: number) {
Column() {
Text(icon).fontSize(30)
Text(t).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#312E81').margin({ top: 8 })
Text(desc).fontSize(9).fontColor('#94A3B8').margin({ top: 4 })
}.width('30%').padding({ top: 18, bottom: 18 }).margin({ right: 8 })
.backgroundColor('#FFFFFF').borderRadius(16)
.onClick(() => {
this.currentTab = i
})
}
// ...
}
这段代码展示了入口组件的核心结构。@State currentTab是页面的核心状态变量,初始值为0,表示默认显示第一个标签页(样品管理)。当用户点击某个工作台卡片时,onClick回调会将currentTab的值修改为对应索引值,框架检测到状态变化后自动重新渲染build方法中依赖currentTab的条件分支,从而实现标签页的切换。
private关键字声明的tabs和icons数组分别存储了六个功能模块的名称和图标。这里需要注意一个重要的区别:private成员变量不会被框架的状态管理系统追踪,也就是说修改private变量不会触发UI刷新。只有被@State装饰的变量才会被纳入响应式系统。在这个入口组件中,tabs和icons是静态数据,不需要变化,因此使用private声明即可。
@Entry装饰器与@Component装饰器的组合使用是鸿蒙ArkTS开发的入门基石。@Entry标识页面入口,@Component标识自定义组件。一个页面有且仅有一个@Entry组件,但可以有任意多个@Component组件。@Entry组件本身也是一个@Component组件,只是它额外获得了路由加载的能力。
2.2 @Builder构建函数的复用设计
@Builder是ArkTS中用于定义可复用UI片段的装饰器。被@Builder修饰的方法可以在build方法中被多次调用,实现UI结构的复用。在本案例中,tabCard就是一个典型的@Builder函数——它接收四个参数(标签名、图标、描述、索引),根据参数生成一个完整的卡片组件。
tabCard内部的实现逻辑非常精妙:它用Column容器将图标Text、标题Text和描述Text纵向排列,设置了30%的宽度、统一的上下内边距和右侧外边距,背景为白色并带有16的圆角。最关键的是底部的onClick回调:当用户点击该卡片时,会将传入的索引i赋值给currentTab状态变量,从而触发标签页切换。
@Builder函数与普通方法的最大区别在于:@Builder方法内部使用的是ArkUI的声明式UI语法(即组件的链式调用语法),而不是普通的代码逻辑。@Builder方法的返回值不是具体的对象,而是一段"UI描述"——框架会根据这段描述来创建和更新视图。这使得@Builder成为ArkTS中实现组件化复用的核心工具。
2.3 build方法中的标签页切换逻辑
build方法是每个@Component组件必须实现的核心方法,它返回该组件的UI结构。在Index组件的build方法中,我们可以看到完整的页面布局架构:
build() {
Column() {
Column() {
Row() {
Column() {
Text('理化实验室').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('厂区质检中心 · 检测分析').fontSize(11).fontColor('#C7D2FE').margin({ top: 4 })
}.layoutWeight(1)
Text('').layoutWeight(1)
Text('🧪').fontSize(30)
}.width('100%')
Row() {
Text('今日在检').fontSize(10).fontColor('#C7D2FE')
Text('').layoutWeight(1)
Text('26 项').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}.width('100%').margin({ top: 14 }).padding(10).backgroundColor('#FFFFFF26').borderRadius(12)
}.width('100%').padding(20).backgroundColor('#4338CA').borderRadius({ bottomLeft: 24, bottomRight: 24 })
Column() {
Text('选择工作台').fontSize(12).fontColor('#94A3B8').width('100%').margin({ bottom: 10 })
Flex({ wrap: FlexWrap.Wrap }) {
this.tabCard('样品', '🧪', '收样登记', 0)
this.tabCard('仪器', '🔬', '设备台账', 1)
this.tabCard('试剂', '🧴', '库存领用', 2)
this.tabCard('检测', '📋', '任务执行', 3)
this.tabCard('报告', '📄', '签发归档', 4)
this.tabCard('我的', '👤', '个人中心', 5)
}.width('100%')
Row() {
Text('▼ 点击上方卡片进入对应工作台').fontSize(10).fontColor('#A5B4FC')
}.width('100%').margin({ top: 16 }).justifyContent(FlexAlign.Center)
}.width('100%').padding(16).layoutWeight(1)
if (this.currentTab === 0) {
SampleView()
} else if (this.currentTab === 1) {
InstView()
} else if (this.currentTab === 2) {
ReagView()
} else if (this.currentTab === 3) {
TestView()
} else if (this.currentTab === 4) {
ReportView()
} else {
MyView()
}
}.width('100%').height('100%').backgroundColor('#F4F6FB')
}

这段build方法是整个页面布局的核心。从外到内,它分为三个层次:
第一层是最外层的Column容器,设置了100%宽度和高度,背景色为#F4F6FB(浅蓝灰色),这是整个页面的根容器。
第二层分为三个子模块纵向排列:顶部是一个Column容器作为头部区域,设置了靛蓝色(#4338CA)背景和底部左右24的圆角,营造出一种沉浸式的头部效果。头部区域内部使用Row横向布局,左侧Column展示标题和副标题(通过layoutWeight(1)占据剩余空间),右侧放置了一个emoji图标。头部还嵌套了一个半透明的统计行,使用#FFFFFF26(带透明度的白色)作为背景色。
中间区域是一个Column容器,包含了一个"选择工作台"的标题文字和一个Flex容器。这里使用了Flex({ wrap: FlexWrap.Wrap })来创建一个可换行的弹性布局容器,六个tabCard通过this引用在Flex内部依次排列。FlexWrap.Wrap属性使得当卡片在一行排不下时会自动换到下一行,保证了不同屏幕尺寸下的适配。
第三层是标签页的内容区域,通过if-else条件分支来根据currentTab的值决定渲染哪个子组件。当currentTab为0时渲染SampleView,为1时渲染InstView,以此类推。这种基于条件渲染的标签页切换方式简单直接,在标签页数量较少的场景下非常实用。
Flex弹性布局是ArkUI提供的高级布局容器,它比Row和Column更加灵活。通过FlexWrap.Wrap属性,Flex容器可以实现自动换行——这在需要根据屏幕宽度自适应排列的场景下非常有用。Flex还支持direction(主轴方向)、justifyContent(主轴对齐)、alignItems(交叉轴对齐)等丰富的配置项,是构建响应式布局的利器。
layoutWeight是ArkUI布局系统中的权重分配机制。当一个容器内的多个子组件都设置了layoutWeight时,容器会根据权重比例分配剩余空间。在本案例中,头部区域的标题Column设置了layoutWeight(1)来占据左侧空间,而Text(‘’)设置layoutWeight(1)作为弹性占位符,将右侧的emoji图标推到最右边。这种"占位符+layoutWeight"的技巧在ArkUI开发中极为常见。
三、样品管理视图:SampleView组件全解析
3.1 状态管理与数据初始化
SampleView是六个子视图中功能最为丰富的组件之一,它负责样品的登记、编辑、删除、详情查看等全生命周期管理。让我们首先看它的状态变量定义和数据初始化:
@Component
struct SampleView {
@State sampleList: SampleItem[] = [
{ code: 'S-2608-01', name: '冷却水样', type: '水质', source: '车间A · 循环水池', state: '在检', num: 3 },
{ code: 'S-2608-02', name: '成品油样', type: '油品', source: '车间B · 灌装线', state: '待检', num: 2 },
{ code: 'S-2608-03', name: '原料粉末', type: '原料', source: '仓库 · 进厂批', state: '已出', num: 5 },
{ code: 'S-2608-04', name: '废液样本', type: '废水', source: '污水站 · 出口', state: '在检', num: 4 },
{ code: 'S-2608-05', name: '涂料样品', type: '成品', source: '车间C · 配色线', state: '待检', num: 2 },
{ code: 'S-2608-06', name: '压缩空气', type: '气体', source: '空压站 · 主管道', state: '已出', num: 1 },
{ code: 'S-2608-07', name: '金属切削液', type: '油品', source: '机加车间 · 储液槽', state: '待检', num: 3 },
{ code: 'S-2608-08', name: '饮用水', type: '水质', source: '食堂 · 净水出口', state: '在检', num: 2 },
{ code: 'S-2608-09', name: '清洗剂', type: '原料', source: '仓库 · 领用批', state: '待检', num: 2 },
{ code: 'S-2608-10', name: '烟气样本', type: '气体', source: '锅炉房 · 烟囱出口', state: '已出', num: 4 },
{ code: 'S-2608-11', name: '防腐涂层', type: '成品', source: '车间D · 喷涂线', state: '待检', num: 1 },
{ code: 'S-2608-12', name: '循环水补样', type: '水质', source: '车间A · 补水口', state: '在检', num: 3 }
]
@State showAdd: boolean = false
@State showEdit: boolean = false
@State showDel: boolean = false
@State showDetail: boolean = false
@State editIdx: number = 0
@State curCode: string = ''
@State curName: string = ''
@State curType: string = ''
@State newName: string = ''
@State newNum: string = ''
@State sampleList是组件中最核心的状态变量,它被初始化为一个包含12条样品记录的数组。每条记录都严格遵循SampleItem接口的定义,包含编号、名称、类型、来源、状态和数量六个字段。这些数据模拟了一个真实的厂区理化实验室的样品登记场景——从冷却水样到成品油样,从废液样本到压缩空气,覆盖了水质、油品、原料、废水、成品、气体等多种样品类型。
除了sampleList之外,该组件还定义了9个@State状态变量,用于控制各种交互行为:showAdd/showEdit/showDel/showDetail是四个布尔值,分别控制新增、编辑、删除、详情四个弹窗的显示与隐藏;editIdx记录当前正在编辑的列表项索引;curCode/curName/curType记录当前操作样品的编号、名称和类型;newName/newNum是新增弹窗中输入框的绑定值。
@State装饰器是ArkTS状态管理系统的核心。被@State装饰的变量会被框架自动追踪——当其值发生变化时,框架会自动重新渲染所有依赖该变量的UI组件。这种"状态变化驱动UI更新"的机制,是声明式UI区别于命令式UI的根本特征。在本案例中,当showAdd从false变为true时,build方法中的if (this.showAdd)条件分支会从"不渲染"变为"渲染",弹窗因此出现在屏幕上。
3.2 头部工具栏headBar的设计
@Builder headBar() {
Row() {
Text('样品管理').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('✚ 收样登记').fontSize(11).fontColor('#FFFFFF').padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#4F46E5').borderRadius(14).onClick(() => {
this.showAdd = true
})
}.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 8 })
}

headBar是一个@Builder构建函数,它渲染了一个Row容器作为工具栏。Row容器内部从左到右依次排列了三个元素:标题文字"样品管理"、一个layoutWeight(1)的空Text作为弹性占位符、以及一个"收样登记"按钮。
这里的"按钮"实际上是一个Text组件,通过padding设置内边距、backgroundColor设置背景色、borderRadius设置圆角,使其看起来像一个按钮。这是ArkUI中常见的"用Text模拟按钮"的手法——当按钮的样式需求比较特殊时,直接使用Text组件配合样式属性往往比Button组件更加灵活。点击该"按钮"后,onClick回调会将showAdd状态设置为true,从而触发新增弹窗的显示。
Row是ArkUI中的横向布局容器,它将其子组件从左到右依次排列。与Column(纵向排列)对应,Row是构建水平方向界面的基础。在Row内部,子组件可以通过layoutWeight来分配剩余空间。在本案例中,Text(‘’).layoutWeight(1)作为弹性占位符,将右侧的"收样登记"按钮推到容器的最右端,实现了左标题右按钮的经典工具栏布局。
3.3 统计卡片statCard的数据面板
@Builder statCard() {
Row() {
Column() {
Text('待检').fontSize(10).fontColor('#64748B')
Text('4').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#D97706').margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('在检').fontSize(10).fontColor('#64748B')
Text('5').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#4F46E5').margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('已出').fontSize(10).fontColor('#64748B')
Text('3').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#15803D').margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('今日收样').fontSize(10).fontColor('#64748B')
Text('12').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#7C3AED').margin({ top: 4 })
}.layoutWeight(1)
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

statCard渲染了一个四列统计面板,展示"待检"、“在检”、“已出”、"今日收样"四项关键指标。每个指标由一个小标题和一个大数字组成,数字使用不同的颜色来传达语义含义:橙色#D97706表示待检(需要关注)、靛蓝色#4F46E5表示在检(进行中)、绿色#15803D表示已出(已完成)、紫色#7C3AED表示今日收样(新增数据)。
在布局技术上,这里使用了Row容器包裹四个Column子组件,每个Column都设置了layoutWeight(1),使得四个统计指标在水平方向上等宽分布。这种"Row + layoutWeight(1)"的组合是构建等宽统计面板的经典模式,在ArkUI开发中被广泛使用。
在ArkUI的颜色体系中,颜色不仅仅是视觉装饰,更是信息传递的媒介。通过语义化颜色编码——绿色代表成功/完成、红色代表警告/危险、橙色代表注意/待处理、蓝色代表进行中/信息——用户可以在不阅读文字的情况下快速理解数据状态。这种"色彩语义化"的设计理念在企业级应用中尤为重要,它能够显著降低用户的信息获取成本。
3.4 柱状图typeChart的实现技巧
本案例代码中一个非常值得品味的细节是:ArkUI并没有内置柱状图组件,但开发者通过巧妙地组合Column容器和固定高度的子Column,手动"绘制"出了柱状图效果:
@Builder typeChart() {
Column() {
Row() {
Text('样品类型分布').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('本月 146 件').fontSize(10).fontColor('#64748B')
}.width('100%')
Row() {
Column() {
Text('水质').fontSize(9).fontColor('#64748B')
Text('38').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#0EA5E9').margin({ top: 4 })
Column().width(22).height(60).backgroundColor('#38BDF8').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('油品').fontSize(9).fontColor('#64748B')
Text('32').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#8B5CF6').margin({ top: 4 })
Column().width(22).height(50).backgroundColor('#A78BFA').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('原料').fontSize(9).fontColor('#64748B')
Text('41').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4F46E5').margin({ top: 4 })
Column().width(22).height(64).backgroundColor('#6366F1').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('气体').fontSize(9).fontColor('#64748B')
Text('20').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#0D9488').margin({ top: 4 })
Column().width(22).height(34).backgroundColor('#2DD4BF').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('成品').fontSize(9).fontColor('#64748B')
Text('15').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#D97706').margin({ top: 4 })
Column().width(22).height(28).backgroundColor('#FBBF24').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
}.width('100%').margin({ top: 12 }).padding(10).backgroundColor('#F5F3FF').borderRadius(12)
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}

这段代码的核心思路是:每个"柱子"实际上是一个Column容器,内部从上到下依次排列类型名称、数值文字和一个空白的Column(作为柱体)。柱体是一个空Column,设置了固定的width(22)和height(60/50/64/34/28),不同的高度代表不同的数值大小。柱体顶部设置了borderRadius({ topLeft: 6, topRight: 6 })来产生圆角效果,使其看起来更像柱状图。
在ArkUI开发中,当框架没有提供你需要的图表组件时,不要急着引入第三方库——很多时候,通过巧妙的容器嵌套和尺寸控制,就能用纯ArkUI语法"画"出你想要的图表。这种"原生优先"的思维不仅能减少依赖、缩小包体积,还能保证图表的渲染性能与原生组件保持一致。本案例中的柱状图就是一个极好的示范:用空Column做柱体,用height做数据映射,用backgroundColor做分类着色,用borderRadius做视觉美化,思路简单但效果出众。
3.5 列表卡片sampleCard的复合布局
sampleCard是样品列表中每一项的渲染模板,它展示了ArkUI复合布局的精髓:
@Builder sampleCard(it: SampleItem, i: number) {
Row() {
Column() {
Text(it.code.substring(2, 4)).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
.width(44).height(44).textAlign(TextAlign.Center).backgroundColor(this.getColor(i)).borderRadius(14)
}.justifyContent(FlexAlign.Center)
Column() {
Row() {
Text(it.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text(it.type).fontSize(10).fontColor('#4338CA').margin({ left: 8 }).padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#E0E7FF').borderRadius(8)
Text('').layoutWeight(1)
Text(it.state).fontSize(10).fontColor(this.stateColor(it.state)).padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(this.stateBg(it.state)).borderRadius(10)
}.width('100%')
Row() {
Text(it.source).fontSize(11).fontColor('#475569')
Text(' · ').fontSize(11).fontColor('#CBD5E1')
Text(it.num + ' 件').fontSize(11).fontColor('#475569')
}.width('100%').margin({ top: 6 })
Progress({ value: Math.min(it.num * 18, 96), total: 100, type: ProgressType.Linear })
.color(this.getColor(i)).backgroundColor('#E2E8F0').height(5).margin({ top: 6 })
}.layoutWeight(1).margin({ left: 12 })
Column() {
Text('···').fontSize(16).fontColor('#94A3B8').margin({ bottom: 2 })
Text('详情').fontSize(9).fontColor('#64748B')
}.margin({ left: 6 }).onClick((e: ClickEvent) => {
this.curCode = it.code
this.curName = it.name
this.curType = it.type
this.showDetail = true
})
}.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(14).margin({ bottom: 10 })
.onClick(() => {
this.editIdx = i
this.curCode = it.code
this.curName = it.name
this.curType = it.type
this.newNum = it.num + ''
this.showEdit = true
})
}
这张卡片的布局结构可以拆解为三个区域:
左侧是编号图标区域:一个44x44的圆角方块,背景色通过getColor(i)方法动态获取(根据索引在颜色数组中循环取色),内部居中显示样品编号的缩写(通过it.code.substring(2, 4)截取编号的第3-4位字符)。
中间是信息区域:包含两行文字和一条进度条。第一行是样品名称、类型标签和状态标签,类型标签用浅蓝色背景的小标签呈现,状态标签的颜色和背景根据状态值动态变化(通过stateColor和stateBg方法)。第二行是来源和数量信息。最后一行是一个Progress线性进度条,value值通过Math.min(it.num * 18, 96)计算得到——这意味着数量越多进度条越长,但不超过96%。
右侧是详情入口:一个"···"图标和"详情"文字,点击后触发详情弹窗。
Progress是ArkUI内置的进度组件,支持线性(Linear)、环形(Ring)、圆形(Eclipse)等多种类型。在本案例中,ProgressType.Linear创建了一个线性进度条,通过value/total属性控制进度比例,通过color和backgroundColor属性分别设置已完成部分和未完成部分的颜色。Progress组件在企业级应用中极为常用,可用于展示任务进度、加载状态、数据填充率等场景。
值得注意的是,sampleCard同时绑定了两个不同层级的onClick事件:外层Row的onClick用于触发编辑弹窗(点击卡片任何位置都会打开编辑),右侧Column的onClick用于触发详情弹窗(只有点击"详情"按钮才会打开详情)。在ArkUI中,事件冒泡是默认行为——当子组件和父组件都绑定了onClick时,子组件的事件会先触发,然后冒泡到父组件。但在本案例中,两个onClick回调设置了不同的状态变量(showDetail vs showEdit),因此两个弹窗会同时弹出。这实际上可能是一个设计上的小瑕疵——在实际开发中,应该使用e.stopPropagation()或调整事件绑定策略来避免冒泡冲突。
3.6 新增弹窗addModal的表单交互
@Builder addModal() {
Column() {
Row() {
Text('收样登记').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('✕').fontSize(18).fontColor('#94A3B8').onClick(() => {
this.showAdd = false
})
}.width('100%')
Text('样品名称').fontSize(11).fontColor('#64748B').margin({ top: 14 }).width('100%')
TextInput({ placeholder: '如:冷却水样', text: this.newName }).width('100%').height(38).fontSize(13)
.backgroundColor('#F8FAFC').borderRadius(10).margin({ top: 6 }).onChange((v: string) => {
this.newName = v
})
Text('样品数量').fontSize(11).fontColor('#64748B').margin({ top: 12 }).width('100%')
TextInput({ placeholder: '如:3', text: this.newNum }).width('100%').height(38).fontSize(13)
.backgroundColor('#F8FAFC').borderRadius(10).margin({ top: 6 }).onChange((v: string) => {
this.newNum = v
})
Row() {
ForEach(['水质', '油品', '原料', '气体', '成品'], (t: string, i: number) => {
Text(t).fontSize(11).fontColor(this.curType === t ? '#FFFFFF' : '#4F46E5').padding({ left: 12, right: 12, top: 5, bottom: 5 })
.backgroundColor(this.curType === t ? '#4F46E5' : '#EEF2FF').borderRadius(12).margin({ right: 8 })
.onClick(() => {
this.curType = t
})
}, (t: string, i: number) => t + i)
}.width('100%').margin({ top: 12 })
Button({ type: ButtonType.Normal }) {
Text('确认登记').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}.width('100%').height(40).backgroundColor('#4F46E5').borderRadius(20).margin({ top: 16 })
.onClick(() => {
let nm: string = this.newName === '' ? '冷却水样' : this.newName
let nt: number = parseInt(this.newNum, 10)
let n: number = isNaN(nt) ? 3 : nt
this.sampleList.unshift({ code: 'S-2608-' + (this.sampleList.length + 1), name: nm, type: this.curType === '' ? '水质' : this.curType, source: '现场采样', state: '待检', num: n })
this.newName = ''
this.newNum = ''
this.curType = ''
this.showAdd = false
})
}.width('84%').padding(18).backgroundColor('#FFFFFF').borderRadius(16)
}
addModal是一个新增样品的表单弹窗。它的结构非常清晰:顶部是标题行(包含标题文字和关闭按钮),中间是两个TextInput输入框(样品名称和样品数量)以及一组类型选择标签,底部是确认按钮。
这里有几个重要的技术细节值得深入分析:
首先,TextInput组件通过onChange回调实现了双向数据绑定。当用户在输入框中输入文字时,onChange回调会被触发,回调参数v是当前输入框的最新值,开发者将其赋值给@State变量newName或newNum,从而实现了"输入即更新"的效果。但需要注意,这种"手动双向绑定"与Vue.js的v-model有本质区别——在ArkTS中,需要开发者手动在onChange回调中更新状态变量。
TextInput是ArkUI提供的基础输入组件,支持placeholder(占位提示文字)、text(当前值)等属性。在实际开发中,TextInput的text属性与@State变量的同步是一个需要特别注意的点:如果text属性绑定了某个@State变量,但变量变化时没有触发TextInput的重新渲染,就会出现"状态更新了但输入框没变"的问题。在本案例中,开发者通过将text设置为this.newName并在onChange中更新newName,形成了一个完整的双向绑定循环。
其次,类型选择标签使用了ForEach来渲染五个可选项。ForEach的第一个参数是数据数组,第二个参数是项渲染函数(接收项数据和索引),第三个参数是键值生成函数(用于Diff算法的优化)。当用户点击某个标签时,curType被设置为该标签的值,然后UI根据curType === t的条件判断来切换标签的选中/非选中样式。
ForEach是ArkUI列表渲染的核心组件。它接收三个参数:数据源数组、项渲染函数和键值生成函数。键值生成函数的返回值用于框架的Diff算法——当数据源发生变化时,框架通过比较键值来判断哪些项需要新增、哪些需要删除、哪些需要更新。在本案例中,键值生成函数返回t + i(类型名+索引),这是一个简单但有效的键值策略。在实际开发中,如果数据项有唯一的id字段,应该优先使用id作为键值,以获得最佳的Diff性能。
最后,确认按钮的onClick回调实现了完整的"新增样品"业务逻辑:解析输入值(对空值和非法数字做了容错处理),通过unshift方法将新记录添加到sampleList数组的头部,清空输入状态,关闭弹窗。unshift方法将新元素添加到数组头部,这意味着新登记的样品会出现在列表的最上方,符合"最新数据优先展示"的用户体验原则。
3.7 编辑、删除与详情弹窗的对比分析
@Builder editModal() {
Column() {
Row() {
Text('编辑样品').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('✕').fontSize(18).fontColor('#94A3B8').onClick(() => {
this.showEdit = false
})
}.width('100%')
Text('当前样品:' + this.curName + '(' + this.curCode + ')').fontSize(12).fontColor('#475569').margin({ top: 14 }).width('100%')
Text('样品数量').fontSize(11).fontColor('#64748B').margin({ top: 12 }).width('100%')
TextInput({ placeholder: '输入数量', text: this.newNum }).width('100%').height(38).fontSize(13)
.backgroundColor('#F8FAFC').borderRadius(10).margin({ top: 6 }).onChange((v: string) => {
this.newNum = v
})
Row() {
ForEach(['待检', '在检', '已出'], (t: string, i: number) => {
Text(t).fontSize(11).fontColor(this.curType === t ? '#FFFFFF' : '#4F46E5').padding({ left: 12, right: 12, top: 5, bottom: 5 })
.backgroundColor(this.curType === t ? '#4F46E5' : '#EEF2FF').borderRadius(12).margin({ right: 8 })
.onClick(() => {
this.curType = t
})
}, (t: string, i: number) => t + i)
}.width('100%').margin({ top: 12 })
Button({ type: ButtonType.Normal }) {
Text('保存修改').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}.width('100%').height(40).backgroundColor('#7C3AED').borderRadius(20).margin({ top: 16 })
.onClick(() => {
let old: SampleItem = this.sampleList[this.editIdx]
let nt: number = parseInt(this.newNum, 10)
let n: number = isNaN(nt) ? old.num : nt
this.sampleList.splice(this.editIdx, 1, { code: old.code, name: old.name, type: old.type, source: old.source, state: this.curType === '' ? old.state : this.curType, num: n })
this.showEdit = false
})
}.width('84%').padding(18).backgroundColor('#FFFFFF').borderRadius(16)
}

editModal的实现模式与addModal非常相似,但有两个关键区别:第一,编辑弹窗在打开时已经通过sampleCard的onClick预设了editIdx、curCode、curName等状态值,因此弹窗中可以显示当前正在编辑的样品信息。第二,保存时使用splice方法替换数组中的指定元素,而不是unshift添加新元素。
splice是JavaScript/ArkTS数组操作的核心方法之一。splice(this.editIdx, 1, {…})的含义是:从editIdx位置开始,删除1个元素,然后在该位置插入一个新元素——本质上就是"替换"操作。这种"先取出旧数据,再拼接修改后的新数据,最后splice替换"的模式,在ArkTS的状态管理中非常重要,因为它确保了数组引用的变化,从而触发@State的响应式更新。
在ArkTS的响应式系统中,@State数组变量的变化检测机制有一个重要特点:只有"引用层面"的变化(如整个数组重新赋值、splice/unshift/push等操作)才能可靠地触发UI更新。如果只是修改数组中某个元素的属性(如this.sampleList[0].name = ‘新名称’),在某些情况下可能不会触发UI刷新。因此,使用splice进行"整体替换"是一种更安全、更可靠的数组更新方式。
删除弹窗delModal和详情弹窗detailModal的结构与前两者类似,但业务逻辑不同。delModal的核心逻辑是使用findIndex方法根据curCode查找要删除的元素索引,然后使用splice(idx, 1)删除该元素。detailModal则不涉及数据修改,只是展示样品的详细信息。
@Builder delModal() {
Column() {
Row() {
Text('⚠ 删除确认').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#B45309')
Text('').layoutWeight(1)
Text('✕').fontSize(18).fontColor('#94A3B8').onClick(() => {
this.showDel = false
})
}.width('100%')
Text('确定删除「' + this.curName + '」的登记记录吗?').fontSize(13).fontColor('#475569').margin({ top: 14 }).width('100%')
Text('删除后样品记录将不可恢复。').fontSize(11).fontColor('#94A3B8').margin({ top: 8 }).width('100%')
Row() {
Button({ type: ButtonType.Normal }) {
Text('取消').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#64748B')
}.layoutWeight(1).height(40).backgroundColor('#F1F5F9').borderRadius(20).onClick(() => {
this.showDel = false
})
Button({ type: ButtonType.Normal }) {
Text('确认删除').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}.layoutWeight(1).height(40).backgroundColor('#E11D48').borderRadius(20).margin({ left: 12 }).onClick(() => {
let idx: number = this.sampleList.findIndex((it: SampleItem) => it.code === this.curCode)
if (idx >= 0) {
this.sampleList.splice(idx, 1)
}
this.showDel = false
})
}.width('100%').margin({ top: 16 })
}.width('84%').padding(18).backgroundColor('#FFFFFF').borderRadius(16)
}
删除弹窗使用了一个"取消+确认"的双按钮布局,两个按钮各占layoutWeight(1)的宽度,等宽排列。确认按钮使用红色背景(#E11D48),传达危险操作的视觉提示。删除逻辑中使用了findIndex加splice的组合——先通过findIndex根据code查找元素在数组中的实际位置,再用splice删除该位置的元素。这种方式比直接使用editIdx更安全,因为在某些边缘情况下(如列表顺序变化),editIdx可能与实际元素位置不对应。
Button组件是ArkUI的基础交互组件。ButtonType.Normal指定了按钮的视觉风格——Normal类型没有默认的背景色和阴影,开发者可以完全自定义其外观。在本案例中,所有按钮都使用了ButtonType.Normal,然后通过backgroundColor、borderRadius等属性自定义样式,这使得按钮的视觉风格与整体应用的靛蓝/紫色主题保持了一致性。
3.8 Stack层叠布局与模态遮罩
SampleView的build方法使用了一个精妙的架构——Stack层叠布局来实现模态弹窗的遮罩效果:
build() {
Stack() {
Column() {
Scroll() {
Column() {
this.headBar()
this.effectBar()
this.statCard()
this.batchBoard()
this.typeChart()
this.noticeBoard()
this.sourceBoard()
Row() {
Text('样品清单').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('共 ' + this.sampleList.length + ' 件').fontSize(10).fontColor('#64748B')
}.width('100%').padding({ left: 4, right: 4, top: 10, bottom: 8 })
ForEach(this.sampleList, (it: SampleItem, i: number) => {
this.sampleCard(it, i)
}, (it: SampleItem, i: number) => it.code + i)
Row() {
Button({ type: ButtonType.Normal }) {
Text('🗑 删除样品').fontSize(12).fontColor('#E11D48')
}.layoutWeight(1).height(38).backgroundColor('#FFE4E6').borderRadius(19)
.onClick(() => {
this.editIdx = this.sampleList.length - 1
this.curCode = this.sampleList[this.sampleList.length - 1].code
this.curName = this.sampleList[this.sampleList.length - 1].name
this.showDel = true
})
}.width('100%').margin({ top: 4, bottom: 20 })
}.width('100%')
}.scrollBar(BarState.Off)
}.width('100%').layoutWeight(1)
if (this.showAdd) {
Column() {
this.addModal()
}.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('#00000040')
.onClick(() => {
this.showAdd = false
})
}
if (this.showEdit) {
Column() {
this.editModal()
}.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('#00000040')
.onClick(() => {
this.showEdit = false
})
}
if (this.showDel) {
Column() {
this.delModal()
}.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('#00000040')
.onClick(() => {
this.showDel = false
})
}
if (this.showDetail) {
Column() {
this.detailModal()
}.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('#00000040')
.onClick(() => {
this.showDetail = false
})
}
}.width('100%').height('100%').backgroundColor('#F4F6FB')
}

Stack是ArkUI中的层叠布局容器,它将所有子组件堆叠在一起,后面的子组件会覆盖在前面的子组件之上。这一特性使得Stack成为实现模态弹窗、遮罩层、悬浮按钮等"浮层"效果的理想选择。在本案例中,Stack的第一层是正常的页面内容(Scroll包裹的列表),第二层是条件渲染的遮罩弹窗——当showAdd为true时,遮罩层渲染在页面内容之上,实现了模态效果。
这个build方法的核心架构可以分为两层:
第一层(底层)是Column容器,内部包含一个Scroll滚动容器。Scroll内部是一个Column,依次调用headBar()、effectBar()、statCard()、batchBoard()、typeChart()、noticeBoard()、sourceBoard()等@Builder函数来渲染各个信息面板,然后是样品列表标题行、ForEach列表渲染、以及底部的删除按钮。Scroll容器通过scrollBar(BarState.Off)隐藏了滚动条,保持了界面的整洁。
第二层(浮层)是四个条件渲染的遮罩弹窗。每个弹窗都是一个全屏的Column容器,设置了#00000040(半透明黑色)背景色作为遮罩,justifyContent(FlexAlign.Center)使弹窗内容垂直居中。点击遮罩区域(弹窗外部)会关闭弹窗——这是通过在遮罩Column上绑定onClick实现的。
Scroll是ArkUI的滚动容器组件,当其内容超出视口大小时,用户可以通过滑动来查看被遮挡的内容。scrollBar(BarState.Off)用于隐藏滚动条——在企业级应用中,隐藏原生滚动条、改用自定义的滚动指示器或不显示任何指示器,是一种常见的视觉优化手段。Scroll容器默认只支持纵向滚动,如果需要横向滚动,可以通过scrollable(ScrollDirection.Horizontal)来设置。
3.9 辅助方法:颜色循环与状态映射
SampleView中定义了三个辅助方法,用于动态计算颜色:
getColor(i: number): string {
let arr: string[] = ['#4F46E5', '#7C3AED', '#0EA5E9', '#0D9488', '#D97706', '#E11D48']
return arr[i % arr.length]
}
stateColor(s: string): string {
if (s === '已出') {
return '#15803D'
}
if (s === '在检') {
return '#4F46E5'
}
return '#D97706'
}
stateBg(s: string): string {
if (s === '已出') {
return '#DCFCE7'
}
if (s === '在检') {
return '#EEF2FF'
}
return '#FFFBEB'
}

getColor方法实现了颜色循环取值的功能:传入一个索引值i,在一个包含6种颜色的数组中通过取模运算(i % arr.length)循环取色。这种设计使得列表中每一项的编号图标背景色各不相同,在视觉上形成丰富的色彩变化,避免了单调感。
stateColor和stateBg方法实现了状态到颜色的映射:根据样品状态(已出/在检/待检)返回对应的文字颜色和背景颜色。已出对应绿色系(#15803D文字 + #DCFCE7背景),在检对应靛蓝色系(#4F46E5文字 + #EEF2FF背景),待检对应橙色系(#D97706文字 + #FFFBEB背景)。这种"状态-颜色映射"的封装方式使得颜色管理集中统一,如果将来需要调整配色方案,只需修改这两个方法即可,无需在散布于各处的UI代码中逐一搜索替换。
四、仪器管理视图:InstView组件分析
4.1 仪器台账数据结构
InstView组件负责管理实验室的仪器设备台账。与SampleView相比,InstView的数据结构和交互模式非常相似,但针对仪器管理的特殊需求做了调整:
@Component
struct InstView {
@State instList: InstItem[] = [
{ code: 'I-01', name: '气相色谱仪', model: 'GC-2014', calib: '2026-03-15', state: '在用' },
{ code: 'I-02', name: '原子吸收光谱', model: 'AA-7000', calib: '2026-05-20', state: '在用' },
{ code: 'I-03', name: '紫外分光光度计', model: 'UV-1900', calib: '2026-02-11', state: '空闲' },
{ code: 'I-04', name: 'pH 计', model: 'PHS-3C', calib: '2026-06-01', state: '在用' },
{ code: 'I-05', name: '电子天平', model: 'ME204', calib: '2026-04-18', state: '在用' },
{ code: 'I-06', name: '恒温干燥箱', model: 'DHG-9240', calib: '2026-01-30', state: '空闲' },
{ code: 'I-07', name: '离心机', model: 'TDZ4-WS', calib: '2026-07-12', state: '维修' },
{ code: 'I-08', name: '电导率仪', model: 'DDS-307', calib: '2026-06-25', state: '在用' },
{ code: 'I-09', name: '水分测定仪', model: 'MA35', calib: '2026-03-08', state: '空闲' },
{ code: 'I-10', name: 'COD 消解仪', model: 'DRB200', calib: '2026-05-02', state: '在用' },
{ code: 'I-11', name: '显微镜', model: 'CX23', calib: '2026-02-28', state: '空闲' },
{ code: 'I-12', name: '全自动滴定仪', model: 'ZDJ-4B', calib: '2026-07-30', state: '在用' }
]
instList包含了12台实验室仪器,每台仪器都有编号、名称、型号、校准日期和状态五个字段。仪器状态分为三种:在用(正在使用中)、空闲(可预约使用)、维修(故障维修中)。校准日期(calib)是一个特别重要的字段——在实验室管理中,仪器必须在校准有效期内使用,超期仪器需要锁定停用。
4.2 校准到期提醒面板calibBoard
@Builder calibBoard() {
Column() {
Row() {
Text('校准到期提醒').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('2 台临期').fontSize(10).fontColor('#D97706').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#FFFBEB').borderRadius(10)
}.width('100%')
Row() {
Text('🔧').fontSize(16)
Column() {
Text('全自动滴定仪 · 校准 2026-07-30').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('已联系计量院,周五上门').fontSize(9).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 10 })
Text('3 天内').fontSize(9).fontColor('#D97706').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#FFFBEB').borderRadius(10)
}.width('100%').margin({ top: 10 }).padding(10).backgroundColor('#FFFBEB').borderRadius(12)
Row() {
Text('🔧').fontSize(16)
Column() {
Text('电子天平 · 校准 2026-04-18').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('检定证书已归档,下次 10-18').fontSize(9).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 10 })
Text('已校').fontSize(9).fontColor('#15803D').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#DCFCE7').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#F0FDF4').borderRadius(12)
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
calibBoard面板展示了校准即将到期的仪器信息。面板的设计运用了"色彩分区"策略:即将到期的仪器卡片使用橙色背景(#FFFBEB),已校准完成的仪器卡片使用绿色背景(#F0FDF4)。每张卡片左侧是工具emoji图标,中间是仪器名称和校准日期/备注,右侧是状态标签。这种行级背景色的变化使得信息的重要性一目了然——橙色背景的卡片需要用户重点关注,绿色背景的卡片则可以安心略过。
4.3 仪器预约面板reserveBoard
@Builder reserveBoard() {
Column() {
Row() {
Text('仪器预约').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('今日 6 台').fontSize(10).fontColor('#64748B')
}.width('100%')
Row() {
Text('⏱').fontSize(14)
Column() {
Text('气相色谱仪 · 有机样检测').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#475569')
Text('预约人 李工 · 09:00-11:30').fontSize(9).fontColor('#64748B').margin({ top: 3 })
}.layoutWeight(1).margin({ left: 8 })
Text('进行中').fontSize(9).fontColor('#4338CA').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#E0E7FF').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#EEF2FF').borderRadius(12)
Row() {
Text('⏱').fontSize(14)
Column() {
Text('原子吸收 · 金属离子测定').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#475569')
Text('预约人 王工 · 13:30-15:00').fontSize(9).fontColor('#64748B').margin({ top: 3 })
}.layoutWeight(1).margin({ left: 8 })
Text('待机').fontSize(9).fontColor('#D97706').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#FEF3C7').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFDF5').borderRadius(12)
reserveBoard面板展示了仪器预约信息。这里值得注意的是状态标签的设计:进行中的预约使用靛蓝色标签(#4338CA文字 + #E0E7FF背景),待机的预约使用橙色标签(#D97706文字 + #FEF3C7背景)。每行卡片的背景色也根据状态微调——进行中使用浅靛蓝背景,待机使用极浅的黄色背景。这种"行背景色+标签颜色"的双重色彩编码,使得预约状态的识别变得极为快速和直观。
4.4 仪器卡片的Progress进度条
InstView中的instCard与SampleView中的sampleCard结构基本一致,但Progress进度条的计算方式有所不同:
Progress({ value: Math.min((i + 3) * 12, 96), total: 100, type: ProgressType.Linear })
.color(this.getColor(i)).backgroundColor('#E2E8F0').height(5).margin({ top: 6 })
这里的value计算公式是Math.min((i + 3) * 12, 96),其中i是列表项的索引值。这意味着第一项的进度值为36%,第二项为48%,以此类推,最大不超过96%。这种基于索引的进度值计算方式虽然不是基于真实数据的,但在视觉上为每张卡片创造了不同的进度状态,避免了所有卡片进度条完全相同的单调感。progress条的颜色也通过getColor(i)动态获取,与编号图标的颜色保持一致,形成了色彩的统一性。
五、试剂管理视图:ReagView组件分析
5.1 库存预警模型
ReagView负责管理实验室的化学试剂库存。与样品和仪器不同,试剂管理的核心是库存预警——当库存数量低于预警阈值时,需要及时提醒采购补货。这一业务需求体现在ReagItem接口的stock和warn两个字段上:
@State reagList: ReagItem[] = [
{ code: 'R-01', name: '无水乙醇', spec: '500mL/瓶', stock: 86, warn: 20, state: '充足' },
{ code: 'R-02', name: '浓硫酸', spec: '2.5L/瓶', stock: 14, warn: 10, state: '正常' },
{ code: 'R-03', name: '氢氧化钠', spec: '500g/瓶', stock: 6, warn: 10, state: '偏低' },
{ code: 'R-04', name: '酚酞指示剂', spec: '100mL/瓶', stock: 32, warn: 5, state: '充足' },
{ code: 'R-05', name: '盐酸', spec: '2.5L/瓶', stock: 18, warn: 10, state: '正常' },
{ code: 'R-06', name: '高锰酸钾', spec: '500g/瓶', stock: 4, warn: 8, state: '偏低' },
// ... 更多数据
]
试剂状态分为三种:充足(库存远超预警线)、正常(库存略高于预警线)、偏低(库存低于预警线)。在reagCard的Progress进度条中,有一个特别值得关注的条件判断逻辑:
Progress({ value: Math.min(it.stock * 4, 96), total: 100, type: ProgressType.Linear })
.color(it.stock < it.warn ? '#E11D48' : this.getColor(i)).backgroundColor('#E2E8F0').height(5).margin({ top: 6 })
这里Progress的color属性使用了一个三元表达式:当it.stock < it.warn(库存低于预警线)时,进度条颜色变为红色(#E11D48);否则使用正常的循环颜色。这是一个非常实用的设计——用户无需仔细阅读库存数字,只需扫一眼进度条的颜色就能判断哪些试剂需要紧急补货。红色进度条直接发出视觉警报,这种"视觉预警"的设计在工业管理系统中极为常见。
在UI设计中,"条件样式"是一种强大的信息传递手段。通过在样式属性(如color、backgroundColor、fontSize等)中使用条件表达式,可以根据数据状态动态改变组件的视觉表现。在本案例中,Progress的color属性根据stock与warn的关系动态切换——这种"数据驱动样式"的思想是声明式UI的精髓所在。
5.2 双人双锁领用面板stockBoard
在危化品管理中,"双人双锁"是一项强制性的安全规范——某些危险化学品必须由两人同时在场、各持一把钥匙才能开启库房。ReagView中的stockBoard面板专门展示了这一合规执行情况:
@Builder stockBoard() {
Column() {
Row() {
Text('双人双锁领用').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('合规执行').fontSize(10).fontColor('#15803D')
}.width('100%')
Row() {
Text('🔐').fontSize(14)
Text('浓硫酸领用 · 经手 张工 / 陈工').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('10:20').fontSize(10).fontColor('#64748B')
}.width('100%').padding({ top: 8 })
Row() {
Text('🔐').fontSize(14)
Text('氢氧化钠领用 · 经手 王工 / 刘工').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('11:05').fontSize(10).fontColor('#64748B')
}.width('100%').padding({ top: 6 })
Row() {
Text('🔐').fontSize(14)
Text('硝酸银领用 · 经手 李工 / 赵工').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('14:40').fontSize(10).fontColor('#64748B')
}.width('100%').padding({ top: 6 })
Row() {
Text('📋').fontSize(14)
Text('领用台账实时同步库管系统。').fontSize(11).fontColor('#64748B').margin({ left: 8 }).layoutWeight(1)
}.width('100%').padding({ top: 6 })
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
这个面板的设计非常简洁:每行使用🔐(锁)emoji图标表示双人双锁操作,文字描述中包含了经手人姓名(两人)和时间,右端展示操作时间。最后一行使用📋图标展示一条合规提醒。整个面板的背景为白色,没有使用行级背景色变化——这是因为双人双锁的所有记录都是"已合规执行"的正常状态,不需要通过颜色变化来突出异常。
5.3 供应商批次面板supplyBoard
@Builder supplyBoard() {
Column() {
Row() {
Text('供应商批次').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('本月 6 批次').fontSize(10).fontColor('#64748B')
}.width('100%')
Row() {
Text('🚚').fontSize(14)
Column() {
Text('无水乙醇 · 批次 E260826').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#475569')
Text('上海化工 · 到货 8/26 · 验收通过').fontSize(9).fontColor('#64748B').margin({ top: 3 })
}.layoutWeight(1).margin({ left: 8 })
Text('合格').fontSize(9).fontColor('#15803D').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#DCFCE7').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#F0FDF4').borderRadius(12)
Row() {
Text('🚚').fontSize(14)
Column() {
Text('浓硫酸 · 批次 S260823').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#475569')
Text('金陵试剂 · 到货 8/23 · 复核中').fontSize(9).fontColor('#64748B').margin({ top: 3 })
}.layoutWeight(1).margin({ left: 8 })
Text('复核中').fontSize(9).fontColor('#D97706').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#FEF3C7').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFDF5').borderRadius(12)
supplyBoard面板展示了供应商批次到货验收情况。每张卡片展示了批次编号、供应商、到货日期和验收状态。验收通过的批次使用绿色标签和浅绿色背景,正在复核的批次使用橙色标签和浅黄色背景。最后一行特别注明了危化品到货须双人验收的合规要求。这种设计将业务合规要求与数据展示融为一体,既传递了信息,又强化了规范意识。
六、检测任务视图:TestView组件分析
6.1 检测任务数据与状态流转
TestView负责管理实验室的检测任务。检测任务是实验室工作的核心环节——从样品登记到最终报告签发,检测任务是连接前后的中枢:
@State testList: TestItem[] = [
{ code: 'T-2608-01', name: '冷却水硬度测定', method: 'EDTA 滴定法', deadline: '今日 16:00', state: '执行中' },
{ code: 'T-2608-02', name: '成品油酸值分析', method: '电位滴定法', deadline: '今日 17:30', state: '待执行' },
{ code: 'T-2608-03', name: '原料水分检测', method: '烘箱称重法', deadline: '今日 15:00', state: '已完成' },
{ code: 'T-2608-04', name: '废液 COD 测定', method: '消解比色法', deadline: '明日 10:00', state: '待执行' },
// ... 更多任务
]
检测任务的状态分为三种:待执行(尚未开始)、执行中(正在检测)、已完成(检测结束)。每个任务都有截止时间(deadline),这是实验室管理的核心约束——超期未完成的任务可能影响生产决策。
6.2 周合格率图表passChart
@Builder passChart() {
Column() {
Row() {
Text('周合格率').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('本周 98.6%').fontSize(10).fontColor('#64748B')
}.width('100%')
Row() {
Column() {
Text('周一').fontSize(9).fontColor('#64748B')
Text('97%').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#0D9488').margin({ top: 4 })
Column().width(22).height(56).backgroundColor('#2DD4BF').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('周二').fontSize(9).fontColor('#64748B')
Text('99%').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#0D9488').margin({ top: 4 })
Column().width(22).height(60).backgroundColor('#14B8A6').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('周三').fontSize(9).fontColor('#64748B')
Text('100%').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#0D9488').margin({ top: 4 })
Column().width(22).height(64).backgroundColor('#0D9488').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
// ... 周四、周五
}.width('100%').margin({ top: 12 }).padding(10).backgroundColor('#F0FDFA').borderRadius(12)
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
passChart展示了周一到周五的检测合格率柱状图。与样品类型分布图类似,这里也使用了空Column作为柱体的技巧。不同的是,合格率柱状图的柱体颜色全部使用了青绿色系(#2DD4BF、#14B8A6、#0D9488),深浅不一——100%合格率的柱体颜色最深(#0D9488),97%合格率的颜色最浅(#2DD4BF)。这种同色系深浅变化既保持了视觉统一性,又通过颜色明度传递了数据差异。
6.3 质控考核榜qcBoard
@Builder qcBoard() {
Column() {
Row() {
Text('质控考核榜').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('季度').fontSize(10).fontColor('#64748B')
}.width('100%')
Row() {
Text('🥇').fontSize(16)
Column() {
Text('李工 · 检测 126 项').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('合格率 99.2% · 零差错').fontSize(9).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 10 })
Text('A+').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#D97706')
}.width('100%').margin({ top: 10 }).padding(10).backgroundColor('#FFFBEB').borderRadius(12)
Row() {
Text('🥈').fontSize(16)
Column() {
Text('王工 · 检测 112 项').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('合格率 98.4% · 差错 1').fontSize(9).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 10 })
Text('A').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#64748B')
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#F8FAFC').borderRadius(12)
Row() {
Text('🥉').fontSize(16)
Column() {
Text('赵工 · 检测 98 项').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('合格率 97.9% · 差错 1').fontSize(9).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 10 })
Text('A-').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#D97706')
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#F8FAFC').borderRadius(12)
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
qcBoard是一个质控考核排行榜,展示了季度考核前三名的检测人员信息。设计中使用了奖牌emoji(🥇🥈🥉)作为排名标识,第一名卡片使用浅黄色背景(#FFFBEB)突出显示,二三名使用浅灰色背景(#F8FAFC)弱化处理。右侧的考核等级(A+、A、A-)使用不同的颜色——A+用橙色(表示优秀中的优秀),A用灰色(表示普通优秀),A-用橙色(表示略低于A+)。
在企业级应用的UI设计中,排行榜是一种常见的激励型信息展示组件。通过视觉差异(背景色、图标、字号等)来区分排名,能够在用户浏览时产生直观的竞争感知。本案例中第一名使用特殊背景色、奖牌图标和不同等级颜色,构成了一个层次分明的排行榜视觉体系。
6.4 今日任务面板taskBoard的紧急标记
@Builder taskBoard() {
Column() {
Row() {
Text('今日任务').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('8 项').fontSize(10).fontColor('#64748B')
}.width('100%')
Row() {
Text('⏰').fontSize(14)
Text('冷却水硬度测定 · 截点 16:00').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('急').fontSize(9).fontColor('#E11D48').padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FFE4E6').borderRadius(8)
}.width('100%').padding({ top: 8 })
Row() {
Text('⏰').fontSize(14)
Text('循环水浊度检测 · 截点 16:30').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('急').fontSize(9).fontColor('#E11D48').padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FFE4E6').borderRadius(8)
}.width('100%').padding({ top: 6 })
Row() {
Text('⏰').fontSize(14)
Text('成品油酸值分析 · 截点 17:30').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
}.width('100%').padding({ top: 6 })
taskBoard面板展示了今日的任务列表。这里有一个重要的设计细节:某些任务行右侧带有红色"急"标签(#E11D48文字 + #FFE4E6背景),表示该任务的截止时间临近,需要优先处理;而其他任务行则没有这个标签。这种"选择性标记"的设计使得紧急任务在视觉上自动跳出来,用户无需逐一阅读每行文字就能快速识别需要优先处理的任务。
七、报告管理视图:ReportView组件分析
7.1 报告签发流转体系
ReportView负责管理检验报告的签发和归档。在实验室管理中,报告签发是一个严格的流程——从检测完成到报告签发,需要经过检测人录入、复核人审核、技术负责人签发三个环节,最后才能归档:
@State repList: ReportItem[] = [
{ code: 'RPT-2608-01', name: '冷却水硬度检验报告', type: '水质', time: '08-28 15:20', state: '待签发' },
{ code: 'RPT-2608-02', name: '成品油酸值检验报告', type: '油品', time: '08-28 14:05', state: '待签发' },
{ code: 'RPT-2608-03', name: '原料水分检验报告', type: '原料', time: '08-28 11:40', state: '已签发' },
{ code: 'RPT-2608-04', name: '废液 COD 检验报告', type: '废水', time: '08-28 10:30', state: '已归档' },
// ... 更多报告
]
报告状态分为三种:待签发(等待技术负责人签发)、已签发(已签发但尚未归档)、已归档(已归入档案库)。这三种状态构成了报告从生成到归档的完整生命周期。
7.2 签发流转面板signBoard
@Builder signBoard() {
Column() {
Row() {
Text('签发流转').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('在线审批').fontSize(10).fontColor('#4F46E5')
}.width('100%')
Row() {
Text('📤').fontSize(14)
Text('原料水分报告 · 已送审 技术负责人').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('审批中').fontSize(9).fontColor('#D97706').padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FFFBEB').borderRadius(8)
}.width('100%').padding({ top: 8 })
Row() {
Text('📤').fontSize(14)
Text('循环水浊度报告 · 已签章').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('待归档').fontSize(9).fontColor('#4F46E5').padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#EEF2FF').borderRadius(8)
}.width('100%').padding({ top: 6 })
Row() {
Text('📤').fontSize(14)
Text('饮用水余氯报告 · 已发送车间').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('已完成').fontSize(9).fontColor('#15803D').padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#DCFCE7').borderRadius(8)
}.width('100%').padding({ top: 6 })
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
signBoard面板展示了报告签发的实时流转状态。三行记录分别代表三个不同的流转阶段:审批中(橙色标签)、待归档(靛蓝色标签)、已完成(绿色标签)。通过📤图标和状态标签的组合,用户可以直观地了解每份报告当前处于签发流程的哪个环节。这种"流程可视化"的设计在审批类应用中极为重要——它使得抽象的审批流程变得具象可见。
在企业级应用中,"流程状态可视化"是提升用户体验的关键设计。通过将业务流程的各个阶段映射为直观的视觉元素(颜色、图标、标签),用户可以在不阅读流程文档的情况下,快速理解当前状态和下一步操作。本案例中的签发流转面板就是一个典型的流程可视化设计——三行记录代表三个阶段,颜色编码代表状态类型,文字描述代表具体操作。
7.3 审核记录面板auditBoard的三态展示
@Builder auditBoard() {
Column() {
Row() {
Text('审核记录').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('近 7 日 18 条').fontSize(10).fontColor('#64748B')
}.width('100%')
Row() {
Text('🔍').fontSize(14)
Column() {
Text('报告 R-260826-07 · 水质常规').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#475569')
Text('陈工 已签发 · 08-26 16:40').fontSize(9).fontColor('#64748B').margin({ top: 3 })
}.layoutWeight(1).margin({ left: 8 })
Text('已签发').fontSize(9).fontColor('#15803D').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#DCFCE7').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#F0FDF4').borderRadius(12)
Row() {
Text('🔍').fontSize(14)
Column() {
Text('报告 R-260826-12 · 排放口监测').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#475569')
Text('孙工 复核中 · 08-26 15:20').fontSize(9).fontColor('#64748B').margin({ top: 3 })
}.layoutWeight(1).margin({ left: 8 })
Text('复核中').fontSize(9).fontColor('#D97706').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#FEF3C7').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFFDF5').borderRadius(12)
Row() {
Text('🔍').fontSize(14)
Column() {
Text('报告 R-260825-05 · 原料入厂检验').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#475569')
Text('已驳回 · 需补充原始记录 · 08-25 11:05').fontSize(9).fontColor('#E11D48').margin({ top: 3 })
}.layoutWeight(1).margin({ left: 8 })
Text('已驳回').fontSize(9).fontColor('#E11D48').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#FFE4E6').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FFF1F2').borderRadius(12)
Row() {
Text('📋').fontSize(14)
Text('三级审核:检测 → 复核 → 签发,全程留痕。').fontSize(11).fontColor('#64748B').margin({ left: 8 }).layoutWeight(1)
}.width('100%').padding({ top: 8 })
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
auditBoard面板展示了近7日的审核记录,包含了三种审核状态:已签发(绿色背景#F0FDF4)、复核中(浅黄色背景#FFFDF5)、已驳回(浅红色背景#FFF1F2)。三种状态分别用绿色、橙色、红色的标签和行背景色来区分,形成了一个完整的"三态展示"体系。最后还特别注明了"三级审核:检测→复核→签发,全程留痕"的流程说明,将业务规范直接嵌入到界面中。
7.4 月度签发趋势图trendChart
@Builder trendChart() {
Column() {
Row() {
Text('月度签发趋势').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('8 月 86 份').fontSize(10).fontColor('#64748B')
}.width('100%')
Row() {
Column() {
Text('4月').fontSize(9).fontColor('#64748B')
Text('58').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#6366F1').margin({ top: 4 })
Column().width(24).height(44).backgroundColor('#A5B4FC').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('5月').fontSize(9).fontColor('#64748B')
Text('63').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#6366F1').margin({ top: 4 })
Column().width(24).height(48).backgroundColor('#A5B4FC').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('6月').fontSize(9).fontColor('#64748B')
Text('71').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#6366F1').margin({ top: 4 })
Column().width(24).height(54).backgroundColor('#818CF8').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('7月').fontSize(9).fontColor('#64748B')
Text('79').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#6366F1').margin({ top: 4 })
Column().width(24).height(60).backgroundColor('#6366F1').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('8月').fontSize(9).fontColor('#64748B')
Text('86').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#4F46E5').margin({ top: 4 })
Column().width(24).height(66).backgroundColor('#4F46E5').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
}.width('100%').margin({ top: 12 }).padding(10).backgroundColor('#EEF2FF').borderRadius(12)
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
trendChart展示了4月到8月的报告签发数量趋势。柱体颜色从浅蓝(#A5B4FC)渐变到深蓝(#4F46E5),柱体高度从44渐增到66,清晰地展示了一个"逐月递增"的趋势。最后一个月份(8月)的柱体颜色最深(#4F46E5),数值文字颜色也更深(#4F46E5),在视觉上"当前月"自然成为了视觉焦点。这种"通过颜色深浅强调最新数据"的设计手法在趋势图表中非常常见。
八、个人中心视图:MyView组件分析
8.1 用户信息卡片userCard
@Builder userCard() {
Row() {
Column() {
Text('李').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').width(52).height(52)
.textAlign(TextAlign.Center).backgroundColor('#4F46E5').borderRadius(26)
}.justifyContent(FlexAlign.Center)
Column() {
Text('李工 · 分析一组').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('工号 HS-218 · 高级化验员').fontSize(10).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 12 })
Text('在岗').fontSize(9).fontColor('#15803D').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#DCFCE7').borderRadius(10)
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
userCard展示了当前登录用户的基本信息。左侧是一个52x52的圆形头像(通过borderRadius(26)实现圆形效果),内部居中显示用户姓氏"李",背景色为靛蓝色。中间是用户姓名和工号信息。右侧是"在岗"状态标签,使用绿色背景。
在ArkUI中,圆形效果可以通过borderRadius设置为元素宽高的一半来实现。例如一个52x52的方块,设置borderRadius(26)后就变成了一个完美的圆形。这种"方形+圆角"实现圆形的方式在性能上优于直接使用圆形组件,是ArkUI中创建圆形头像、圆形图标的标准做法。
8.2 证书展示墙certWall
@Builder certWall() {
Column() {
Row() {
Text('我的证书').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('3 张').fontSize(10).fontColor('#64748B')
}.width('100%')
Row() {
Text('🏅').fontSize(16)
Column() {
Text('化学检验员三级').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('有效期至 2028-06').fontSize(9).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 10 })
Text('已认证').fontSize(9).fontColor('#15803D').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#DCFCE7').borderRadius(10)
}.width('100%').margin({ top: 10 }).padding(10).backgroundColor('#F0FDF4').borderRadius(12)
Row() {
Text('📜').fontSize(16)
Column() {
Text('危化品安全管理培训').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('2026-03 结业 · 96 分').fontSize(9).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 10 })
Text('已认证').fontSize(9).fontColor('#15803D').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#DCFCE7').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#F0FDF4').borderRadius(12)
Row() {
Text('🧑🔬').fontSize(16)
Column() {
Text('计量内校员资质').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('有效期至 2027-11').fontSize(9).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 10 })
Text('已认证').fontSize(9).fontColor('#15803D').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#DCFCE7').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#F0FDF4').borderRadius(12)
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
certWall展示了用户持有的职业资格证书。每张证书卡片使用浅绿色背景(#F0FDF4)和绿色"已认证"标签,传达一种"合规、放心"的视觉感受。证书类型涵盖了职业资格(化学检验员三级)、安全培训(危化品安全管理)和技术资质(计量内校员)三个维度,体现了实验室从业人员需要具备的多元化资质体系。
8.3 值班排班面板dutyBoard
@Builder dutyBoard() {
Column() {
Row() {
Text('本周值班').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('今日:李工').fontSize(10).fontColor('#64748B')
}.width('100%')
Row() {
Text('📅').fontSize(14)
Text('周一 · 李工').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('✓').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#15803D')
}.width('100%').padding({ top: 8 })
Row() {
Text('📅').fontSize(14)
Text('周二 · 王工').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('✓').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#15803D')
}.width('100%').padding({ top: 6 })
Row() {
Text('📅').fontSize(14)
Text('周三 · 陈工').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('✓').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#15803D')
}.width('100%').padding({ top: 6 })
Row() {
Text('📅').fontSize(14)
Text('周四 · 赵工').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('✓').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#15803D')
}.width('100%').padding({ top: 6 })
Row() {
Text('📅').fontSize(14)
Text('周五 · 李工').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('今天').fontSize(9).fontColor('#4338CA').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#E0E7FF').borderRadius(10)
}.width('100%').padding({ top: 6 })
Row() {
Text('📅').fontSize(14)
Text('周六 · 王工(应急值守)').fontSize(11).fontColor('#475569').margin({ left: 8 }).layoutWeight(1)
Text('备勤').fontSize(9).fontColor('#D97706')
}.width('100%').padding({ top: 6 })
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
dutyBoard展示了本周的值班排班表。设计上采用了三种状态标记:已完成的日期右侧显示绿色✓标记,今天显示靛蓝色"今天"标签,周六(应急值守)显示橙色"备勤"文字。通过这三种状态标记,用户可以一目了然地了解本周值班的完成情况和当前状态。
8.4 通用设置卡片setCard的参数化设计
@Builder setCard(title: string, desc: string, action: string, onClick: () => void = () => {}) {
Row() {
Column() {
Text(title).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text(desc).fontSize(10).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1)
Text(action).fontSize(11).fontColor('#4F46E5')
}.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(14).margin({ bottom: 10 })
.onClick(() => {
onClick()
})
}
setCard是MyView中最具设计感的@Builder函数之一。它接收四个参数:title(标题)、desc(描述)、action(操作提示文字)、onClick(点击回调函数)。这种"参数化Builder"的设计模式使得同一个Builder函数可以被复用为四种不同的设置项——账号与安全、消息通知、隐私与权限、关于系统——只需传入不同的参数即可。
特别值得注意的是onClick参数的类型:() => void = () => {}。这是一个函数类型的参数,默认值是一个空函数。这种设计允许调用者在调用setCard时传入自定义的点击回调,实现了"行为参数化"。在build方法中的调用方式如下:
this.setCard('账号与安全', '修改密码、绑定手机', '去设置 ›', () => {
this.showAccount = true
})
this.setCard('消息通知', '任务提醒、签发通知', '去设置 ›', () => {
this.showNotify = true
})
this.setCard('隐私与权限', '定位、相机、数据说明', '去设置 ›', () => {
this.showPrivacy = true
})
this.setCard('关于系统', '版本 V2.1.0', '查看 ›', () => {
this.showAbout = true
})
在ArkTS中,@Builder函数支持接收函数类型的参数,这为组件的"行为复用"提供了强大的支持。通过将点击回调作为参数传入,同一个@Builder可以适配不同的业务逻辑,而不需要为每个业务场景编写独立的Builder。这种"参数化Builder+函数回调"的设计模式,是ArkTS中实现高复用性组件的核心技巧之一。
8.5 四种设置弹窗的统一架构
MyView中定义了四个设置弹窗:accountModal(账号与安全)、notifyModal(消息通知)、privacyModal(隐私与权限)、aboutModal(关于系统)。这四个弹窗的结构完全一致——顶部标题行(标题+关闭按钮),中间若干行信息文字,底部一个操作按钮。这种"统一架构"的设计使得代码风格高度一致,开发者阅读任何一个弹窗的代码后就能立刻理解其他弹窗的结构。
四个弹窗的显示/隐藏分别由四个@State布尔变量控制:showAccount、showNotify、showPrivacy、showAbout。在build方法的Stack层叠布局中,四个弹窗作为条件渲染的浮层依次排列。点击设置卡片时,对应的@State变量被设为true,弹窗显示;点击关闭按钮或遮罩区域时,变量被设为false,弹窗消失。
九、组件级技术点深度剖析
9.1 @State状态管理的响应式原理
在本案例的六个子视图中,每个视图都定义了大量的@State变量。这些变量可以分为三类:
第一类是列表数据变量(如sampleList、instList、reagList等),存储了各视图的核心业务数据。当通过unshift/splice等方法修改这些数组时,ForEach会自动重新渲染列表,反映最新的数据变化。
第二类是弹窗控制变量(如showAdd、showEdit、showDel、showDetail等),是布尔值,控制四种模态弹窗的显示与隐藏。当这些变量从false变为true时,Stack中的条件渲染分支会渲染出遮罩层和弹窗内容。
第三类是临时数据变量(如editIdx、curCode、curName、curType、newName、newNum等),用于在用户与列表项交互时暂存上下文信息。例如,当用户点击某个样品卡片时,该样品的索引、编号、名称、类型会被暂存到这些变量中,供编辑弹窗使用。
@State装饰器背后的响应式原理基于"观察者模式":框架在编译期会分析每个@State变量在build方法(及@Builder函数)中的使用情况,建立"变量-UI依赖"的映射关系。当变量值发生变化时,框架根据映射关系找到所有依赖该变量的UI组件,触发它们的重新渲染。这种"细粒度更新"机制确保了只有真正受影响的UI部分才会被更新,而不是整个页面重新渲染。
9.2 @Builder装饰器的编译原理
@Builder装饰器在ArkTS编译器中会被特殊处理。被@Builder修饰的方法不会被编译为普通的JavaScript函数,而是会被转换为一个"UI描述生成器"——当该方法被调用时,它不会执行传统的函数逻辑,而是向框架的渲染管线提交一段UI描述,由框架负责将其转化为实际的组件树。
这意味着@Builder方法中的代码(如Column()、Text()、Row()等)并不是在运行时创建对象,而是在编译期被解析为一种"声明式UI描述"。当@Builder方法被多次调用时(如tabCard被调用6次),框架会根据每次调用的参数生成不同的UI描述实例,实现了"同一套代码、不同的渲染结果"。
在本案例中,每个子视图都定义了大量的@Builder函数:headBar(工具栏)、statCard(统计卡片)、各种Board(信息面板)、Card(列表项卡片)、各种Modal(弹窗)。这些@Builder函数将复杂的UI结构封装为可复用的模块,使得build方法变得简洁清晰——只需要依次调用各个@Builder函数就能组装出完整的页面。
9.3 ForEach列表渲染的Diff机制
本案例中每个子视图都使用了ForEach来渲染列表数据。ForEach的完整签名是:
ForEach(
arr: Array,
itemGenerator: (item: any, index: number) => void,
keyGenerator?: (item: any, index: number) => string
)
在本案例中,ForEach的使用模式高度统一。以SampleView为例:
ForEach(this.sampleList, (it: SampleItem, i: number) => {
this.sampleCard(it, i)
}, (it: SampleItem, i: number) => it.code + i)
第一个参数this.sampleList是数据源数组。第二个参数是项渲染函数,接收每项数据和索引,调用this.sampleCard(it, i)来渲染列表项。第三个参数是键值生成函数,返回it.code + i(样品编号+索引)作为唯一键值。
ForEach的键值生成函数(keyGenerator)是性能优化的关键。当数据源发生变化时(如unshift添加新项、splice删除项),框架会对比变化前后的键值列表,通过Diff算法确定哪些项需要新增渲染、哪些需要删除、哪些可以复用。如果键值生成函数返回的是索引i(而不是唯一标识),那么在列表头部插入新项时,所有项的索引都会变化,导致整个列表重新渲染——这是性能 worst case。在本案例中,使用it.code + i作为键值,虽然包含了索引i,但因为code是唯一的,整体上仍能保证较好的Diff性能。最佳实践是使用纯唯一的id字段作为键值。
9.4 条件渲染与标签页切换
本案例中使用了两种条件渲染模式:
入口组件Index使用if-else条件分支来切换六个子视图:
if (this.currentTab === 0) {
SampleView()
} else if (this.currentTab === 1) {
InstView()
} else if (this.currentTab === 2) {
ReagView()
} else if (this.currentTab === 3) {
TestView()
} else if (this.currentTab === 4) {
ReportView()
} else {
MyView()
}
每个子视图内部使用独立的if条件来控制弹窗显示:
if (this.showAdd) {
Column() { this.addModal() }
.width('100%').height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#00000040')
.onClick(() => { this.showAdd = false })
}
在ArkUI的条件渲染中,if-else和if是两种不同的使用场景。if-else用于"多选一"的场景(如标签页切换,同一时刻只显示一个视图),if用于"可选"的场景(如弹窗,可能显示也可能不显示)。当条件为false时,if分支内的组件不会被创建和渲染,因此不会占用任何计算资源。当条件从false变为true时,组件才会被创建并挂载到组件树上。这种"惰性创建"机制确保了应用的运行时性能。
9.5 Stack层叠布局与zIndex层级控制
本案例的每个子视图都使用了Stack作为根容器,将正常内容和模态弹窗叠放在一起。Stack的子组件按照声明顺序从下到上层叠——先声明的在底层,后声明的在顶层。因此,正常内容(Scroll列表)在底层,弹窗遮罩在顶层。
在本案例中没有显式使用zIndex属性来控制层级,而是通过声明顺序隐式控制了层级关系。但在更复杂的场景中(如多个浮层需要精确控制叠放顺序),可以使用zIndex属性来显式指定组件的层级:
Stack是ArkUI三大基础布局容器之一(另外两个是Column和Row)。Stack的层叠特性使其成为实现"模态遮罩"、“悬浮操作按钮”、"图片上叠放文字"等浮层效果的首选容器。在ArkUI开发中,"Stack + 条件渲染"是构建模态弹窗的标准模式——Stack保证弹窗覆盖在内容之上,条件渲染保证弹窗只在需要时出现。
9.6 Scroll滚动容器与内容溢出处理
每个子视图的build方法都将列表内容包裹在Scroll容器中。Scroll容器的主要作用是处理内容溢出——当内容的总高度超过视口高度时,用户可以通过上下滑动来查看被遮挡的内容。在本案例中,每个子视图的内容都包含多个信息面板(headBar、statCard、各种Board)加上一个ForEach列表,总高度远远超过一屏,因此Scroll容器是必不可少的。
scrollBar(BarState.Off)用于隐藏滚动条。BarState枚举有三个值:On(始终显示)、Off(始终隐藏)、Auto(自动显示/隐藏)。在本案例中,开发者选择了Off来保持界面的整洁美观。在企业级应用中,隐藏原生滚动条、让内容自然滑动是一种常见的视觉优化策略。
Scroll容器在ArkUI中是一个"裁剪容器"——它会在自身边界处裁剪超出范围的内容,并通过滑动机制让用户访问被裁剪的部分。Scroll默认只支持纵向滚动,如果需要双向滚动,可以考虑使用List组件或自定义滚动控制器。在实际开发中,Scroll的嵌套使用需要特别注意"滚动事件冲突"问题——当外层和内层都有Scroll时,需要明确指定哪个Scroll优先响应滑动事件。
9.7 字符串截取在编号展示中的应用
在列表卡片中,样品编号、仪器编号等code字段经过了字符串截取处理后才显示在编号图标上。例如:
- SampleView中:it.code.substring(2, 4) —— 从’S-2608-01’中截取第3-4位字符,得到’26’
- InstView中:it.code.substring(2) —— 从’I-01’中截取第3位开始的所有字符,得到’01’
- TestView中:it.code.substring(8) —— 从’T-2608-01’中截取第9位开始的所有字符,得到’01’
- ReportView中:it.code.substring(10) —— 从’RPT-2608-01’中截取第11位开始的所有字符,得到’01’
substring是JavaScript/ArkTS中String对象的标准方法,用于截取字符串的指定部分。它接收两个参数:起始位置(含)和结束位置(不含),如果省略第二个参数则截取到字符串末尾。在本案例中,通过精心设计的截取位置,从不同格式的编号中提取出有意义的数字部分作为图标文字,这是一个既实用又灵活的字符串处理技巧。
十、核心技术对比汇总
下面通过一张完整的对比表格,系统梳理本案例中涉及的各类技术要素:
| 序号 | 技术要素 | 类别 | 作用描述 | 使用位置 | 关键参数/属性 |
|---|---|---|---|---|---|
| 1 | @Entry | 装饰器 | 标识页面入口组件,可被路由系统加载 | Index组件 | 无参数 |
| 2 | @Component | 装饰器 | 声明自定义组件,可被其他组件引用 | 全部7个struct | 无参数 |
| 3 | @State | 装饰器 | 声明响应式状态变量,值变化时触发UI更新 | 所有子视图 | 变量类型和初始值 |
| 4 | @Builder | 装饰器 | 定义可复用的UI构建函数 | headBar/statCard/各种Board/Card/Modal | 函数参数 |
| 5 | @Entry+@Component | 装饰器组合 | 标识页面级根组件 | Index | 两个装饰器叠加 |
| 6 | Column | 容器组件 | 纵向排列子组件 | 几乎所有UI结构 | width/padding/backgroundColor |
| 7 | Row | 容器组件 | 横向排列子组件 | 工具栏/统计面板/列表卡片 | width/padding/justifyContent |
| 8 | Stack | 容器组件 | 层叠排列子组件,用于模态弹窗 | 所有子视图的build方法 | width/height/backgroundColor |
| 9 | Flex | 容器组件 | 弹性布局,支持换行 | Index入口的卡片排列 | wrap: FlexWrap.Wrap |
| 10 | Scroll | 容器组件 | 滚动容器,处理内容溢出 | 所有子视图的列表区域 | scrollBar(BarState.Off) |
| 11 | Text | 基础组件 | 显示文字 | 标题/标签/数值/描述 | fontSize/fontWeight/fontColor |
| 12 | Button | 基础组件 | 按钮交互 | 弹窗中的操作按钮 | type: ButtonType.Normal |
| 13 | TextInput | 基础组件 | 文本输入 | 弹窗中的表单输入框 | placeholder/text/onChange |
| 14 | Progress | 基础组件 | 进度条 | 列表卡片底部进度条 | value/total/type: Linear |
| 15 | layoutWeight | 布局能力 | 权重分配,分配剩余空间 | 统计面板列等宽/占位符 | layoutWeight(1) |
| 16 | justifyContent | 布局能力 | 主轴对齐方式 | Stack中弹窗居中 | FlexAlign.Center |
| 17 | borderRadius | 样式属性 | 圆角效果 | 几乎所有卡片和按钮 | 数值或对象{topLeft等} |
| 18 | backgroundColor | 样式属性 | 背景色 | 所有组件 | 十六进制颜色值 |
| 19 | ForEach | 列表渲染 | 遍历数组渲染列表项 | 所有子视图的列表区域 | 数组/项函数/键值函数 |
| 20 | if-else | 条件渲染 | 条件分支渲染不同视图 | Index标签页切换 | currentTab === 0等 |
| 21 | if | 条件渲染 | 可选渲染弹窗 | 所有子视图的弹窗区域 | showAdd/showEdit等 |
| 22 | onClick | 事件绑定 | 点击事件回调 | 卡片/按钮/遮罩 | 箭头函数回调 |
| 23 | onChange | 事件绑定 | 输入变化回调 | TextInput | (v: string) => {} |
| 24 | interface | 类型定义 | 定义数据结构接口 | 6个数据模型接口 | 字段名: 类型 |
| 25 | unshift | 数组操作 | 头部添加元素 | 新增弹窗的确认逻辑 | unshift({…}) |
| 26 | splice | 数组操作 | 替换/删除元素 | 编辑/删除操作 | splice(idx, 1, {…}) |
| 27 | findIndex | 数组操作 | 查找元素索引 | 删除操作的元素定位 | (it) => it.code === this.curCode |
| 28 | substring | 字符串操作 | 截取子字符串 | 编号图标的文字提取 | substring(2, 4)等 |
| 29 | Math.min | 数学运算 | 取最小值 | Progress进度值封顶 | Math.min(val, 96) |
| 30 | parseInt | 类型转换 | 字符串转数字 | 数量输入值解析 | parseInt(str, 10) |
| 31 | getColor | 自定义方法 | 索引取色循环 | 列表卡片编号图标 | arr[i % arr.length] |
| 32 | stateColor | 自定义方法 | 状态到文字色映射 | 列表卡片状态标签 | if判断返回颜色 |
| 33 | stateBg | 自定义方法 | 状态到背景色映射 | 列表卡片状态标签 | if判断返回颜色 |
十一、总结
架构设计的层次分明
纵观整个案例代码,我们可以清晰地看到一个层次分明的架构设计。最外层是@Entry装饰的Index入口组件,负责头部展示、工作台卡片排列和标签页切换调度。中间层是六个@Component子视图(SampleView、InstView、ReagView、TestView、ReportView、MyView),每个子视图负责一个完整业务模块的全部交互逻辑。最内层是大量的@Builder构建函数,将工具栏、统计面板、信息看板、列表卡片、模态弹窗等UI结构封装为可复用的独立单元。这种"入口-子视图-Builder函数"的三层架构,使得代码的职责边界清晰、复用性高、可维护性强,是鸿蒙ArkTS开发中的经典架构模式。
状态管理的统一范式
六个子视图在状态管理上遵循了完全统一的设计范式:每个视图都定义了一个@State数组变量作为核心列表数据源,一组@State布尔变量作为弹窗开关,一组@State临时变量作为交互上下文。这种统一的状态管理范式使得代码风格高度一致——开发者理解了SampleView的状态管理后,就能毫无障碍地理解其他五个视图的状态逻辑。更重要的是,这种统一范式确保了状态变更的可预测性——每一个状态变量的变化都对应一个明确的用户操作(如点击卡片、提交表单、关闭弹窗),不存在"隐式状态变更"的风险。
视觉设计的色彩体系
在视觉设计层面,整个应用采用了一套精心设计的色彩体系。主色调以靛蓝色(#4338CA、#4F46E5、#312E81)为核心,辅以天蓝色(#0EA5E9)、紫色(#7C3AED、#8B5CF6)、青绿色(#0D9488、#2DD4BF)等次要色调,形成了一个"冷色调实验室"的视觉主题。状态色采用语义化编码——绿色系代表完成/正常/充足、橙色系代表待处理/偏低/临期、红色系代表危险/删除/驳回、蓝色系代表进行中/信息。背景色采用"白底+浅色区块"的层次设计——白色卡片浮在#F4F6FB的浅蓝灰背景上,卡片内部的子区块使用更浅的同色系背景(如#F5F3FF、#F0FDF4、#FFFBEB等),形成了清晰的视觉层次。
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,但是需要重启一下:

新建一个空白模板:

设置API为24的模板项目:
初始化项目,自动下载相关依赖:

完整代码:
// 主题:实验室冷调(靛蓝/天蓝/紫),浅色背景 #F4F6FB
// 6 Tab:样品 / 仪器 / 试剂 / 检测 / 报告 / 我的
interface SampleItem {
code: string
name: string
type: string
source: string
state: string
num: number
}
interface InstItem {
code: string
name: string
model: string
calib: string
state: string
}
interface ReagItem {
code: string
name: string
spec: string
stock: number
warn: number
state: string
}
interface TestItem {
code: string
name: string
method: string
deadline: string
state: string
}
interface ReportItem {
code: string
name: string
type: string
time: string
state: string
}
interface MyItem {
icon: string
title: string
desc: string
}
@Entry
@Component
struct Index {
@State currentTab: number = 0
private tabs: string[] = ['样品', '仪器', '试剂', '检测', '报告', '我的']
private icons: string[] = ['🧪', '🔬', '🧴', '📋', '📄', '👤']
@Builder tabCard(t: string, icon: string, desc: string, i: number) {
Column() {
Text(icon).fontSize(30)
Text(t).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#312E81').margin({ top: 8 })
Text(desc).fontSize(9).fontColor('#94A3B8').margin({ top: 4 })
}.width('30%').padding({ top: 18, bottom: 18 }).margin({ right: 8 })
.backgroundColor('#FFFFFF').borderRadius(16)
.onClick(() => {
this.currentTab = i
})
}
build() {
Column() {
Column() {
Row() {
Column() {
Text('理化实验室').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('厂区质检中心 · 检测分析').fontSize(11).fontColor('#C7D2FE').margin({ top: 4 })
}.layoutWeight(1)
Text('').layoutWeight(1)
Text('🧪').fontSize(30)
}.width('100%')
Row() {
Text('今日在检').fontSize(10).fontColor('#C7D2FE')
Text('').layoutWeight(1)
Text('26 项').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}.width('100%').margin({ top: 14 }).padding(10).backgroundColor('#FFFFFF26').borderRadius(12)
}.width('100%').padding(20).backgroundColor('#4338CA').borderRadius({ bottomLeft: 24, bottomRight: 24 })
Column() {
Text('选择工作台').fontSize(12).fontColor('#94A3B8').width('100%').margin({ bottom: 10 })
Flex({ wrap: FlexWrap.Wrap }) {
this.tabCard('样品', '🧪', '收样登记', 0)
this.tabCard('仪器', '🔬', '设备台账', 1)
this.tabCard('试剂', '🧴', '库存领用', 2)
this.tabCard('检测', '📋', '任务执行', 3)
this.tabCard('报告', '📄', '签发归档', 4)
this.tabCard('我的', '👤', '个人中心', 5)
}.width('100%')
Row() {
Text('▼ 点击上方卡片进入对应工作台').fontSize(10).fontColor('#A5B4FC')
}.width('100%').margin({ top: 16 }).justifyContent(FlexAlign.Center)
}.width('100%').padding(16).layoutWeight(1)
if (this.currentTab === 0) {
SampleView()
} else if (this.currentTab === 1) {
InstView()
} else if (this.currentTab === 2) {
ReagView()
} else if (this.currentTab === 3) {
TestView()
} else if (this.currentTab === 4) {
ReportView()
} else {
MyView()
}
}.width('100%').height('100%').backgroundColor('#F4F6FB')
}
}
@Component
struct SampleView {
@State sampleList: SampleItem[] = [
{ code: 'S-2608-01', name: '冷却水样', type: '水质', source: '车间A · 循环水池', state: '在检', num: 3 },
{ code: 'S-2608-02', name: '成品油样', type: '油品', source: '车间B · 灌装线', state: '待检', num: 2 },
{ code: 'S-2608-03', name: '原料粉末', type: '原料', source: '仓库 · 进厂批', state: '已出', num: 5 },
{ code: 'S-2608-04', name: '废液样本', type: '废水', source: '污水站 · 出口', state: '在检', num: 4 },
{ code: 'S-2608-05', name: '涂料样品', type: '成品', source: '车间C · 配色线', state: '待检', num: 2 },
{ code: 'S-2608-06', name: '压缩空气', type: '气体', source: '空压站 · 主管道', state: '已出', num: 1 },
{ code: 'S-2608-07', name: '金属切削液', type: '油品', source: '机加车间 · 储液槽', state: '待检', num: 3 },
{ code: 'S-2608-08', name: '饮用水', type: '水质', source: '食堂 · 净水出口', state: '在检', num: 2 },
{ code: 'S-2608-09', name: '清洗剂', type: '原料', source: '仓库 · 领用批', state: '待检', num: 2 },
{ code: 'S-2608-10', name: '烟气样本', type: '气体', source: '锅炉房 · 烟囱出口', state: '已出', num: 4 },
{ code: 'S-2608-11', name: '防腐涂层', type: '成品', source: '车间D · 喷涂线', state: '待检', num: 1 },
{ code: 'S-2608-12', name: '循环水补样', type: '水质', source: '车间A · 补水口', state: '在检', num: 3 }
]
@State showAdd: boolean = false
@State showEdit: boolean = false
@State showDel: boolean = false
@State showDetail: boolean = false
@State editIdx: number = 0
@State curCode: string = ''
@State curName: string = ''
@State curType: string = ''
@State newName: string = ''
@State newNum: string = ''
@Builder headBar() {
Row() {
Text('样品管理').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('✚ 收样登记').fontSize(11).fontColor('#FFFFFF').padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#4F46E5').borderRadius(14).onClick(() => {
this.showAdd = true
})
}.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 8 })
}
@Builder statCard() {
Row() {
Column() {
Text('待检').fontSize(10).fontColor('#64748B')
Text('4').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#D97706').margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('在检').fontSize(10).fontColor('#64748B')
Text('5').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#4F46E5').margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('已出').fontSize(10).fontColor('#64748B')
Text('3').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#15803D').margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('今日收样').fontSize(10).fontColor('#64748B')
Text('12').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#7C3AED').margin({ top: 4 })
}.layoutWeight(1)
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
@Builder effectBar() {
Row() {
Text('⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗ ⚗').fontSize(7)
.fontColor('#818CF8').opacity(0.4).letterSpacing(2)
}.width('100%').padding({ left: 16, right: 16 }).margin({ top: 8 }).height(14)
}
@Builder typeChart() {
Column() {
Row() {
Text('样品类型分布').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('本月 146 件').fontSize(10).fontColor('#64748B')
}.width('100%')
Row() {
Column() {
Text('水质').fontSize(9).fontColor('#64748B')
Text('38').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#0EA5E9').margin({ top: 4 })
Column().width(22).height(60).backgroundColor('#38BDF8').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('油品').fontSize(9).fontColor('#64748B')
Text('32').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#8B5CF6').margin({ top: 4 })
Column().width(22).height(50).backgroundColor('#A78BFA').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('原料').fontSize(9).fontColor('#64748B')
Text('41').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4F46E5').margin({ top: 4 })
Column().width(22).height(64).backgroundColor('#6366F1').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('气体').fontSize(9).fontColor('#64748B')
Text('20').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#0D9488').margin({ top: 4 })
Column().width(22).height(34).backgroundColor('#2DD4BF').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
Column() {
Text('成品').fontSize(9).fontColor('#64748B')
Text('15').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#D97706').margin({ top: 4 })
Column().width(22).height(28).backgroundColor('#FBBF24').borderRadius({ topLeft: 6, topRight: 6 }).margin({ top: 4 })
}.layoutWeight(1)
}.width('100%').margin({ top: 12 }).padding(10).backgroundColor('#F5F3FF').borderRadius(12)
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
@Builder batchBoard() {
Column() {
Row() {
Text('批量送检看板').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('').layoutWeight(1)
Text('3 批在途').fontSize(10).fontColor('#7C3AED').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#F3E8FF').borderRadius(10)
}.width('100%')
Row() {
Text('🧴').fontSize(16)
Column() {
Text('批次 B-087 · 进厂原料 12 件').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('采样员 李工 · 14:20 送抵').fontSize(9).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 10 })
Text('待分配').fontSize(9).fontColor('#7C3AED').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#F3E8FF').borderRadius(10)
}.width('100%').margin({ top: 10 }).padding(10).backgroundColor('#FAF5FF').borderRadius(12)
Row() {
Text('🥤').fontSize(16)
Column() {
Text('批次 B-088 · 成品抽检 8 件').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('采样员 赵工 · 15:05 送抵').fontSize(9).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 10 })
Text('分配中').fontSize(9).fontColor('#D97706').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#FFFBEB').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FAF5FF').borderRadius(12)
Row() {
Text('🧫').fontSize(16)
Column() {
Text('批次 B-089 · 废水比对 4 件').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text('采样员 王工 · 16:30 送抵').fontSize(9).fontColor('#64748B').margin({ top: 4 })
}.layoutWeight(1).margin({ left: 10 })
Text('待分配').fontSize(9).fontColor('#7C3AED').padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#F3E8FF').borderRadius(10)
}.width('100%').margin({ top: 8 }).padding(10).backgroundColor('#FAF5FF').borderRadius(12)
}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
@Builder noticeBoard() {
Column() {
Row() {
Text('采样通知').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#312E81')
Text(''). }.width('100%').height('100%').backgroundColor('#F4F6FB')
}
}

组件复用的Builder策略
@Builder装饰器在本案例中发挥了极致的复用价值。以SampleView为例,该视图定义了headBar、statCard、effectBar、typeChart、batchBoard、noticeBoard、sourceBoard、sampleCard、addModal、editModal、delModal、detailModal共12个@Builder函数,将整个视图的UI结构分解为12个独立的、可复用的构建单元。其中,setCard在MyView中更是通过"参数化Builder+函数回调"的模式实现了四种不同设置项的统一渲染。这种"Builder粒度细化"的策略不仅提升了代码的复用性,还使得每个Builder函数的职责单一、逻辑清晰,大大降低了后续维护和修改的成本。
声明式UI的开发效率
本案例代码总计约2800行,却实现了一个包含6个完整功能模块、每个模块都具备统计面板、信息看板、列表渲染、增删改查弹窗的完整企业级应用界面。这充分体现了声明式UI范式在开发效率上的巨大优势——开发者只需要声明"界面长什么样"和"状态如何变化",框架会自动处理UI的创建、更新和销毁。如果使用传统的命令式UI开发模式(如Android的View体系或iOS的UIKit),实现同样的功能至少需要两倍以上的代码量,而且需要大量的手动视图操作代码(findViewById、setText、setVisibility等),这些代码既冗长又容易出错。ArkTS的声明式UI范式将这些繁琐的工作交给了框架自动处理,让开发者能够将精力集中在业务逻辑和用户体验的设计上。
工业场景与移动技术的深度融合
本案例最值得称道的一点是将工业实验室管理的专业场景与鸿蒙移动开发技术进行了深度融合。样品的收样登记、仪器的校准提醒、试剂的双人双锁领用、检测任务的截止时间告警、报告的三级审核流转——这些实验室管理的专业流程被忠实地映射到了ArkUI的组件和交互模式中。Progress进度条不只是显示进度,而是根据库存与预警线的关系动态切换颜色来发出视觉警报;ForEach列表不只是渲染数据,而是通过条件样式让紧急任务和临期试剂自动"跳出来";模态弹窗不只是表单容器,而是承载了从收样登记到报告签发的完整业务流程。这种"技术服务业务、业务驱动技术"的设计理念,是工业级移动应用开发的最高境界。
更多推荐




所有评论(0)