科技蓝与智慧绿的B端叙事——HarmonyOS ArkUI 智慧园区六场景工作台组件化设计实践
一、技术前言
在企业数字化转型的浪潮中,智慧园区作为物理空间与数字世界深度融合的典型场景,正经历着从传统物业管理向智能化运营管理的深刻变革。智慧园区管理平台需要整合考勤、访客、会议、报修、公告、员工服务等多维度业务能力,为园区内的企业员工、管理人员、访客等不同角色提供一站式数字化服务入口。
HarmonyOS 作为面向全场景的分布式操作系统,其声明式UI开发框架 ArkUI 为构建跨设备、高性能、体验一致的智慧园区应用提供了强有力的技术支撑。ArkUI 采用声明式UI范式,开发者只需描述UI的状态和结构,框架便会自动处理UI的渲染与更新。其核心特性包括:
声明式UI开发范式:ArkUI 摒弃了传统命令式UI中手动操作DOM节点的繁琐模式,采用类似 SwiftUI、Flutter 的声明式语法。开发者通过 @Component 装饰器标记组件结构体,在 build() 方法中描述UI结构,状态变更通过 @State、@Observed 等装饰器驱动视图自动刷新。这种"状态驱动视图"的编程模型大幅降低了UI开发的心智负担,提升了代码的可读性和可维护性。
组件化与组合机制:ArkUI 提供了 @Builder 装饰器,支持将UI片段抽离为可复用的构建函数,实现UI逻辑的模块化组织。配合 @CustomDialog、@BuilderParam 等高级特性,开发者可以构建层次清晰、职责明确的组件树。在B端复杂场景下,这种组合式架构能够有效支撑多业务模块的并行开发与统一管理。
状态管理体系:ArkUI 的状态管理方案覆盖了从组件内局部状态到跨组件共享状态的完整链路。@State 管理组件内部状态,@Observed 配合 @ObjectLink 实现对象级别的细粒度监听,@Provide/@Consume 支持跨层级的状态透传,AppStorage/LocalStorage 提供全局和页面级的状态存储。这套多层次的状态管理方案为复杂业务场景的数据流转提供了灵活而可靠的保障。
性能优化能力:ArkUI 在框架层面内置了多种性能优化机制。ForEach 的键值生成策略确保列表的高效diff更新,@Memo 支持计算结果缓存,LazyForEach 实现长列表的惰性加载。结合方舟编译器的AOT编译优化,ArkUI 应用能够在各类设备上保持流畅的交互体验。
本文将以"智园通"智慧园区管理平台为载体,深入剖析一个包含六大业务场景的B端工作台组件的完整实现。该应用以科技蓝(#2563EB)与智慧绿(#10B981)为主题色彩,通过浅色背景营造专业、清爽的视觉氛围。六个功能Tab分别采用宫格菜单、状态列表、卡片网格、时间轴、信息流、中心大卡等截然不同的布局方式,充分展现 ArkUI 框架在复杂B端场景下的组件化设计能力与UI表达潜力。
接下来,我们将从整体架构出发,逐层深入色彩体系、数据模型、组件结构、各Tab实现细节、图表系统、底部导航、弹窗机制等核心模块,力求呈现一幅完整而细致的技术全景图。






二、整体架构流程图
在深入代码细节之前,我们先从宏观视角来审视当前页面的整体架构设计。该智慧园区工作台采用了经典的分层架构模式,从底层的数据定义到顶层的UI渲染,形成了一条清晰的单向依赖链路。
架构解读
从架构图中我们可以清晰地看到六层结构的职责划分与协作关系:
第一层:数据定义层——这是整个应用的基础设施,包含颜色系统、Tab元数据和辅助函数三大模块。颜色系统定义了全局统一的色彩规范,确保所有UI组件在视觉上保持一致性。Tab元数据描述了底部导航栏的配置信息,为底部Tab栏提供数据支撑。辅助函数则封装了状态与颜色的映射逻辑,避免在UI代码中散落大量条件判断。
第二层:数据模型层——六个@Observed装饰的类构成了应用的核心业务数据模型。每个模型对应一个业务场景,封装了该场景下的所有数据字段。这些模型通过装饰器获得了响应式能力,当数据发生变化时,绑定的UI会自动刷新。
第三层:状态管理层——主组件内部通过@State装饰器管理着多种类型的状态变量,包括当前Tab索引、呼吸动画开关、弹窗显示状态以及各业务模块的数据列表。这些状态变量是连接数据模型与UI渲染的桥梁。
第四层:组件渲染层——这是用户直接感知的界面层,由九个核心@Builder函数组成。每个Builder函数负责一个独立的UI模块,通过组合基础组件(Column、Row、Text、Image等)构建出丰富的界面效果。六个Tab页面采用了完全不同的布局策略,展现了多样化的UI表达能力。
第五层:弹窗系统——应用设计了通用遮罩层和三种功能弹窗(新增、编辑、删除),形成了一套完整的模态交互体系。弹窗系统通过状态变量控制显示与隐藏,遵循统一的视觉规范。
第六层:主组件——Page804作为顶层组件,统筹协调所有子模块。它在build()方法中按照"头部-内容区-底部导航"的三段式结构组织页面,通过条件渲染实现Tab切换,将各功能模块有机地串联在一起。
这种分层架构的设计思想体现了关注点分离的原则:数据定义与UI渲染分离,业务模型与状态管理分离,通用组件与业务组件分离。每一层都有明确的职责边界,层与层之间通过清晰的接口进行交互,使得代码结构清晰、易于维护、便于扩展。
三、色彩体系设计
色彩是UI设计的灵魂,一套科学合理的色彩体系不仅能塑造产品的视觉辨识度,更能通过色彩语义引导用户的视觉焦点与操作行为。在B端产品中,色彩的功能性远大于其装饰性,需要兼顾专业感、可读性与交互反馈。
当前页面构建了一套完整的色彩调色板系统,通过ColorPalette接口定义色彩规范,再以COLORS常量对象提供全局统一的色值引用。
interface ColorPalette {
bg: string; card: string; chip: string;
title: string; sub: string; text3: string;
blue: string; blueD: string;
green: string; greenD: string;
orange: string; red: string;
line: string; tabOn: string; mask: string;
}
这段接口定义展示了色彩体系的完整构成。我们可以将这十六个颜色变量划分为四大类:
第一类:背景与容器色——包含bg(页面背景)、card(卡片背景)、chip(标签/胶囊背景)三个色值。页面背景采用浅灰蓝色#F0F4F8,营造清爽、专业的视觉基调。卡片背景使用纯白色#FFFFFF,在浅灰背景上形成清晰的层级对比。标签背景使用淡蓝色#E0ECFF,这是科技蓝的浅色版本,既保持了品牌色的关联性,又不会抢夺内容的视觉焦点。
第二类:文字层级色——包含title(标题文字)、sub(次要文字)、text3(辅助文字)三个色值,构成了三级文字灰度体系。标题色#1A2332是深灰蓝色,接近黑色但略带蓝调,在保持高对比度的同时避免了纯黑的生硬感。次要文字色#6B7A8F为中灰蓝色,用于描述性文本和辅助信息。辅助文字色#A0AEC0为浅灰蓝色,用于时间戳、标签等弱化信息。三级文字色的明度梯度合理,确保了信息层级的清晰表达。
第三类:品牌功能色——包含blue/blueD(科技蓝主色与深色)、green/greenD(智慧绿主色与深色)、orange(警示橙)、red(错误红)六组色值。科技蓝#2563EB是主品牌色,用于主要按钮、选中态、重要数据的强调。智慧绿#10B981是辅助品牌色,代表绿色、环保、高效的园区理念,用于正向状态、成功提示。橙色和红色作为语义色,分别对应警告/待处理状态和错误/危险状态。每种主色都配有深色版本(blueD、greenD),用于渐变效果、悬浮态或需要更深色值的场景。
第四类:结构与遮罩色——包含line(分割线)、tabOn(Tab选中色)、mask(遮罩层)三个色值。分割线色#E2E8F0是极浅的灰蓝色,在白色卡片上若隐若现,既起到了区域分隔的作用,又不会破坏界面的整体性。Tab选中色与科技蓝保持一致,强化品牌识别。遮罩层使用半透明黑色rgba(0,0,0,0.5),为弹窗提供合适的背景暗化效果。
const COLORS: ColorPalette = {
bg: '#F0F4F8', card: '#FFFFFF', chip: '#E0ECFF',
title: '#1A2332', sub: '#6B7A8F', text3: '#A0AEC0',
blue: '#2563EB', blueD: '#1D4ED8',
green: '#10B981', greenD: '#059669',
orange: '#F59E0B', red: '#EF4444',
line: '#E2E8F0', tabOn: '#2563EB',
mask: 'rgba(0,0,0,0.5)',
};
这套色彩体系的设计体现了以下几个特点:
蓝绿双主色策略:不同于传统B端产品单一主色的做法,当前页面采用科技蓝+智慧绿的双主色策略。蓝色传达科技感、专业感、信任感,绿色传达智慧、环保、高效的理念。两种颜色在不同场景中交替使用,蓝色多用于操作按钮、数据强调,绿色多用于正向状态、成功反馈,形成了丰富而有序的色彩节奏。
统一色相家族:整套色彩体系在色相上保持高度统一。无论是背景色、文字色还是分割线色,都带有微妙的蓝色倾向,形成了和谐的蓝灰色调家族。这种统一的色相管理确保了界面整体色调的一致性,避免了多种灰阶混用可能造成的视觉杂乱。
语义化色彩命名:色彩变量采用语义化命名方式,而非直接使用颜色名称。例如使用title而非darkGray,使用chip而非lightBlue。这种命名方式的优势在于,当设计风格调整时,只需修改色值而无需改变引用名称,大大降低了维护成本。同时,语义化命名也使得代码更具可读性,开发者可以直观理解每个颜色变量的用途。
色彩与功能深度绑定:色彩在该应用中不仅仅是视觉装饰,更是信息传达的重要载体。不同的状态(待确认、已预约、已完成、已取消等)通过不同的颜色进行标识,用户可以通过颜色快速识别信息类型。这种色彩语义化的设计大幅提升了信息的获取效率,在B端数据密集型场景中尤为重要。
四、Tab元数据与辅助数据
在ArkUI开发中,将UI配置数据与渲染逻辑分离是一种重要的工程实践。Tab元数据和各类辅助数据数组的设计,体现了数据驱动视图的开发理念。
4.1 Tab元数据定义
Tab导航是移动应用最常见的导航模式之一。当前页面定义了TabMeta接口来规范底部Tab的数据结构,并通过TAB_LIST数组统一管理六个Tab的配置信息。
interface TabMeta { name: string; icon: Resource; }
const TAB_LIST: TabMeta[] = [
{ name: '工作台', icon: $r('app.media.ic_public_0') },
{ name: '访客', icon: $r('app.media.ic_public_1') },
{ name: '会议室', icon: $r('app.media.ic_public_2') },
{ name: '报修', icon: $r('app.media.ic_public_3') },
{ name: '公告', icon: $r('app.media.ic_public_4') },
{ name: '我的', icon: $r('app.media.ic_public_5') },
];
TabMeta接口定义了两个核心属性:name表示Tab的显示名称,icon表示Tab的图标资源。图标使用$r()语法引用应用资源文件中的图片,这是HarmonyOS中访问资源的标准方式。通过app.media.ic_public_0到ic_public_5的命名约定,可以看出这是一组统一设计的公共图标资源。
六个Tab分别对应智慧园区的六大核心功能模块:工作台、访客、会议室、报修、公告、我的。这种排列顺序也暗合了用户的使用频率——工作台作为首页入口放在最左侧,个人中心放在最右侧,中间按业务关联性排列。
将Tab配置数据化为数组的好处是显而易见的:当需要调整Tab顺序、新增或删除Tab时,只需修改TAB_LIST数组即可,无需改动底部Tab栏的渲染逻辑。这种数据与视图分离的设计模式,是组件化开发的重要基石。
4.2 图表辅助数据
能耗统计图表是当前页面的一个重要数据可视化模块,它需要三组辅助数据来支撑柱状图的渲染。
const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_NAME: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
const ENERGY_VAL: number[] = [4200, 5100, 6300, 7800, 8500, 7200];
const MAX_VAL: number = 8500;
这四组数据各司其职:MONTH_IDX提供ForEach循环的索引数组,用于驱动六根柱状图的渲染;MONTH_NAME存储每个柱子下方的月份标签文本;ENERGY_VAL存储对应的能耗数值(单位为kWh);MAX_VAL记录能耗的峰值,用于计算每根柱子的相对高度。
从数据中我们可以读出一条完整的能耗趋势线:3月至7月能耗逐月攀升,从4200kWh增长至8500kWh的峰值,8月回落到7200kWh。这大致符合园区能耗的季节性规律——夏季气温升高导致空调用电增加,7月达到顶峰后略有回落。
将图表数据抽离为独立常量的设计方式,使得图表组件更加通用。如果后续需要切换数据维度(比如从月能耗切换到日能耗),只需替换数据源数组即可,图表的渲染逻辑无需改动。
4.3 工作台行索引数据
工作台页面采用四行三列的宫格布局,共十二个功能入口。为了便于ForEach循环渲染,代码中将每行的三个元素索引分别抽离为独立数组。
const WORKBENCH_ROW0: number[] = [0, 1, 2];
const WORKBENCH_ROW1: number[] = [3, 4, 5];
const WORKBENCH_ROW2: number[] = [6, 7, 8];
const WORKBENCH_ROW3: number[] = [9, 10, 11];
这四个数组分别对应四行功能菜单,每个数组元素是WORKBENCH_LIST中的索引值。例如第一行显示索引0、1、2对应的三个功能(考勤打卡、请假申请、报销审批),第二行显示索引3、4、5对应的功能,依此类推。
这种索引数组的设计虽然略显繁琐,但在ArkUI的ForEach机制下有其合理性。每个Row通过ForEach遍历对应行的索引数组,再通过索引从数据源中取出具体条目进行渲染。每行都有独立的键值生成器('r0_' + idx等),确保了列表diff的准确性。
当然,如果追求更简洁的写法,也可以通过二维数组或动态计算索引的方式来实现。但当前这种显式声明的方式胜在直观清晰,便于开发者快速定位和调整每行的内容。
五、工具函数
在UI开发中,经常会遇到"根据状态返回对应颜色"这类逻辑。如果将这些条件判断散落在UI代码的各个角落,不仅会造成代码冗余,还会给后续维护带来困难。将这类逻辑抽离为独立的工具函数,是提升代码质量的有效手段。
当前页面定义了三个颜色映射函数,分别对应访客、报修、会议室三个模块的状态色彩体系。
5.1 访客状态颜色映射
function visitorColor(s: string): string {
if (s === '待确认') return COLORS.orange;
if (s === '已预约') return COLORS.green;
if (s === '已离开') return COLORS.text3;
if (s === '已取消') return COLORS.red;
return COLORS.text3;
}
visitorColor函数接收访客状态字符串作为参数,返回对应的颜色值。四种状态的色彩语义如下:
- 待确认——返回橙色。表示需要用户关注和处理的待办事项,橙色具有较强的视觉警示作用,提醒用户及时确认访客申请。
- 已预约——返回绿色。表示访客预约已确认,流程正常推进,绿色传达安全、正常的信号。
- 已离开——返回辅助灰色。表示访客流程已完成且属于历史记录,使用弱化的灰色避免干扰当前信息。
- 已取消——返回红色。表示预约被取消,属于异常或负面状态,红色直观传达警告意味。
函数末尾设置了默认返回值COLORS.text3,确保在遇到未知状态时不会导致渲染异常,体现了防御式编程的思想。
5.2 报修状态颜色映射
function repairColor(s: string): string {
if (s === '待受理') return COLORS.orange;
if (s === '处理中') return COLORS.blue;
if (s === '已完成') return COLORS.green;
if (s === '已驳回') return COLORS.red;
return COLORS.text3;
}
repairColor函数对应报修工单的四种状态,色彩语义与访客状态略有不同:
- 待受理——橙色。表示报修申请尚未被受理,需要相关人员及时处理。
- 处理中——蓝色。表示工单正在处理过程中,蓝色代表专业的技术支持正在进行。
- 已完成——绿色。表示报修已圆满解决,绿色传达完成和满意的含义。
- 已驳回——红色。表示报修申请被驳回,红色表示需要用户重新提交或关注。
值得注意的是,"处理中"状态使用了科技蓝而非其他颜色,这与访客模块的设计形成了差异。在报修场景中,处理中是一个积极的进行时状态,使用品牌主色蓝色既符合B端产品的专业调性,又能与待受理(橙色)、已完成(绿色)形成清晰的区分。
5.3 会议室状态颜色映射
function meetingColor(s: string): string {
if (s === '空闲') return COLORS.green;
if (s === '使用中') return COLORS.blue;
if (s === '已预约') return COLORS.orange;
if (s === '维护中') return COLORS.red;
return COLORS.text3;
}
meetingColor函数对应会议室的四种状态,色彩分配同样遵循语义化原则:
- 空闲——绿色。表示会议室当前可用,绿色传达"畅通、可使用"的信号。
- 使用中——蓝色。表示会议室正在被使用,蓝色代表正式的会议进行中。
- 已预约——橙色。表示会议室已被预约但尚未开始使用,橙色起到提醒和预告的作用。
- 维护中——红色。表示会议室因设备维护等原因不可用,红色警示用户不可预约。
工具函数的设计价值
这三个工具函数虽然逻辑简单,但在架构层面具有重要价值:
统一色彩语义:将状态与颜色的映射关系集中管理,确保同一种状态在不同位置显示的颜色完全一致,避免了因手动编写条件判断可能导致的色彩不一致问题。
简化UI代码:UI渲染代码只需调用函数即可获得对应颜色,无需编写冗长的if-else链,使得UI代码更加简洁清晰,关注点更加聚焦于布局本身。
便于维护扩展:当需要调整某个状态的颜色或新增状态类型时,只需修改对应函数即可,无需在整个代码库中搜索替换,大幅降低了维护成本。
函数式设计思想:这三个函数都是纯函数——相同的输入始终产生相同的输出,没有副作用。纯函数具有良好的可测试性和可预测性,是函数式编程的重要基石。
六、数据模型层
数据模型是应用的骨架,它定义了业务数据的结构和行为。在ArkUI框架中,通过@Observed装饰器修饰的类可以获得响应式能力——当对象的属性发生变化时,所有依赖该属性的UI会自动更新。
当前页面定义了六个@Observed类,分别对应六个业务场景的数据模型。下面我们逐一进行分析。
6.1 WorkbenchItem——工作台条目模型
@Observed
export class WorkbenchItem {
icon: string; name: string; badge: number;
constructor(icon: string, name: string, badge: number) {
this.icon = icon; this.name = name; this.badge = badge;
}
}
WorkbenchItem是工作台宫格菜单的条目模型,包含三个属性:
- icon:功能图标的emoji字符表示。使用emoji作为图标是一种轻量级的图标方案,无需额外的图片资源,在开发原型或演示场景中非常便捷。
- name:功能名称,显示在图标下方。
- badge:角标数字,表示待处理事项的数量。当badge大于0时,图标右上角会显示红色角标提醒。
WORKBENCH_LIST数组预置了十二个工作台功能,涵盖考勤打卡、请假申请、报销审批、物资领用、用车申请、餐饮预订、工位管理、设备报修、访客登记、会议预约、数据报表、系统设置等园区办公的核心场景。其中部分功能带有角标数字,如报销审批(3条)、设备报修(5条)、访客登记(4条)等,直观提示用户有待处理的事项。
6.2 VisitorItem——访客记录模型
@Observed
export class VisitorItem {
name: string; reason: string; status: string; date: string; phone: string;
constructor(name: string, reason: string, status: string, date: string, phone: string) {
this.name = name; this.reason = reason; this.status = status;
this.date = date; this.phone = phone;
}
}
VisitorItem是访客管理的数据模型,描述了一次访客记录的完整信息,包含五个属性:
- name:访客姓名,用于识别访客身份。
- reason:来访事由,如商务洽谈、面试应聘、项目对接等,帮助接待人员了解来访目的。
- status:访客状态,取值包括待确认、已预约、已离开、已取消四种,配合
visitorColor函数实现状态色彩标识。 - date:来访日期时间,格式为
YYYY-MM-DD HH:mm,精确到分钟级别。 - phone:联系电话,中间四位用星号脱敏处理(如
138****1234),兼顾信息展示与隐私保护。
VISITOR_LIST预置了六条访客记录,涵盖了全部四种状态,数据设计非常完整。从记录中可以看到,当天有三位访客(待确认1位、已预约2位),历史记录有三位(已离开2位、已取消1位),模拟了真实的访客管理场景。
6.3 MeetingItem——会议室信息模型
@Observed
export class MeetingItem {
name: string; capacity: number; equipment: string; status: string; current: string; floor: string;
constructor(name: string, capacity: number, equipment: string, status: string, current: string, floor: string) {
this.name = name; this.capacity = capacity; this.equipment = equipment;
this.status = status; this.current = current; this.floor = floor;
}
}
MeetingItem是会议室预订的数据模型,包含六个属性,信息维度比前两个模型更加丰富:
- name:会议室名称,如"星河会议厅"、"云海会议室"等,赋予每个会议室独特的标识。
- capacity:容纳人数,数字类型,用户可根据参会人数选择合适的会议室。
- equipment:配备设备,用竖线分隔的字符串表示(如"投影|白板|视频会议"),快速展示会议室的硬件配置。
- status:使用状态,包括空闲、使用中、已预约、维护中四种,配合
meetingColor函数显示状态色标。 - current:当前会议内容,当状态为"使用中"或"已预约"时显示会议名称,状态为"维护中"时显示维护原因,空闲时显示占位符。
- floor:所在楼层,如"3F"、"5F"等,方便用户定位会议室位置。
MEETING_LIST预置了六个会议室,规模从4人的洽谈室到50人的培训教室不等,覆盖了不同规模的会议需求。会议室的命名也颇具匠心——星河会议厅、云海会议室、智慧会议室、创客空间等名称赋予了空间以想象力和品牌感。
6.4 RepairItem——报修工单模型
@Observed
export class RepairItem {
title: string; desc: string; status: string; time: string; reporter: string;
constructor(title: string, desc: string, status: string, time: string, reporter: string) {
this.title = title; this.desc = desc; this.status = status;
this.time = time; this.reporter = reporter;
}
}
RepairItem是设备报修的数据模型,包含五个属性:
- title:报修标题,简要描述故障类型,如"空调故障"、"打印机卡纸"等。
- desc:详细描述,说明故障的具体情况和位置信息,为维修人员提供参考。
- status:工单状态,包括待受理、处理中、已完成、已驳回四种,配合
repairColor函数显示状态颜色。 - time:报修时间,格式为
MM-DD HH:mm,记录工单发起的时间点。 - reporter:报修人信息,格式为"部门·姓名",便于维修人员联系对接。
REPAIR_LIST预置了六条报修记录,涵盖了空调、打印机、灯光、门禁、网络、电梯等常见的园区设备故障类型。每条记录都有详细的故障描述和报修人信息,模拟了真实的报修工单场景。其中两条处于处理中状态,两条已完成,一条待受理,一条已驳回,状态分布均衡。
6.5 NoticeItem——公告通知模型
@Observed
export class NoticeItem {
title: string; dept: string; time: string; summary: string; unread: boolean;
constructor(title: string, dept: string, time: string, summary: string, unread: boolean) {
this.title = title; this.dept = dept; this.time = time;
this.summary = summary; this.unread = unread;
}
}
NoticeItem是园区公告的数据模型,包含五个属性:
- title:公告标题,简明扼要地传达公告主题。
- dept:发布部门,如安全管理部、能源管理部、人力资源部等,标明公告的来源和权威性。
- time:发布时间,精确到分钟,帮助用户判断公告的时效性。
- summary:内容摘要,公告正文的前若干字,让用户快速了解公告大意。摘要最多显示两行,超出部分用省略号截断。
- unread:未读标记,布尔类型。为true时显示红色圆点,提醒用户该公告尚未阅读。
NOTICE_LIST预置了六条公告,涵盖消防演练、能耗统计、食堂菜单、体检安排、停车场改造、新员工培训等园区常见的通知类型。其中三条标记为未读状态,与标题栏"未读3"的统计数字相对应,数据逻辑自洽。
6.6 EmployeeItem——员工信息模型
@Observed
export class EmployeeItem {
name: string; dept: string; empNo: string; points: number; level: string; building: string;
constructor(name: string, dept: string, empNo: string, points: number, level: string, building: string) {
this.name = name; this.dept = dept; this.empNo = empNo;
this.points = points; this.level = level; this.building = building;
}
}
EmployeeItem是员工个人信息的数据模型,用于"我的"页面展示,包含六个属性:
- name:员工姓名。
- dept:所属部门。
- empNo:员工工号,格式为
EMP加七位数字。 - points:积分值,数字类型,用于员工激励体系的积分展示。
- level:员工等级,如金牌员工、银牌员工、钻石员工等,配合积分体系形成员工成长路径。
- building:办公位置,标明员工所在的楼栋和楼层。
EMPLOYEE_LIST预置了三位员工信息,分别属于技术研发部、产品运营部和市场营销部,积分和等级各不相同,模拟了不同职级和部门的员工画像。第一位员工"石天雄"作为当前登录用户显示在个人中心页面。
数据模型层的设计思考
六个数据模型虽然结构各异,但都遵循了一致的设计规范:
@Observed装饰器:每个模型类都使用@Observed装饰,确保对象属性变化时UI能够自动响应。这是ArkUI状态管理机制的基础,也是声明式UI的核心特性。
构造函数初始化:每个类都提供了完整的构造函数,所有属性通过构造函数参数赋值,确保对象创建时数据完整。
预置模拟数据:每个模型都配套了对应的模拟数据列表,数据内容贴合真实业务场景,便于开发调试和UI演示。这种"模型+模拟数据"的配套模式,在前端开发中非常实用。
语义化字段命名:属性命名清晰直观,见名知意,无需额外注释就能理解每个字段的含义。良好的命名是代码自文档化的关键。
七、组件主体结构
组件主体是整个页面的核心调度中心,它统筹管理页面的状态变量、生命周期、整体布局和各功能模块的组织方式。在ArkUI中,一个页面由@Entry装饰的@Component结构体表示,内部通过build()方法描述UI结构。
7.1 组件声明与状态变量
@Entry
@Component
struct Page804 {
@State currentTab: number = 0;
@State addModal: boolean = false;
@State editModal: boolean = false;
@State delModal: boolean = false;
@State editIdx: number = -1;
@State delIdx: number = -1;
@State breath: boolean = false;
timer: number = -1;
@State workbenchList: WorkbenchItem[] = WORKBENCH_LIST;
@State visitorList: VisitorItem[] = VISITOR_LIST;
@State meetingList: MeetingItem[] = MEETING_LIST;
@State repairList: RepairItem[] = REPAIR_LIST;
@State noticeList: NoticeItem[] = NOTICE_LIST;
@State employeeList: EmployeeItem[] = EMPLOYEE_LIST;
主组件Page804使用@Entry和@Component双重装饰器标记。@Component表示这是一个可复用的UI组件,@Entry表示这是页面的入口组件,应用启动时会首先渲染该组件。
组件内部定义了丰富的状态变量,我们可以将其分为四类:
导航状态:currentTab记录当前选中的Tab索引,初始值为0(即工作台页面)。当用户点击底部Tab栏时,该值更新,触发内容区域的条件渲染切换。
弹窗状态:addModal、editModal、delModal三个布尔变量分别控制新增、编辑、删除三种弹窗的显示与隐藏。editIdx和delIdx记录当前操作的数据项索引,用于在弹窗中展示对应的数据。初始值-1表示未选中任何条目。
动画状态:breath是一个布尔开关,配合timer定时器实现呼吸灯动画效果。breath每秒翻转一次,驱动头部快捷入口、头像背景、柱状图高度等元素产生渐变动画。
业务数据:六个@State装饰的数组变量分别对应六个业务模块的数据源。它们从预置的常量列表初始化,由于使用了@State装饰,当数组内容发生变化时,对应的UI会自动刷新。
这里有一个值得注意的设计细节:timer变量没有使用@State装饰。这是因为timer只是一个定时器句柄,用于在组件销毁时清除定时器,它本身不参与UI渲染。不需要响应式追踪的变量就不要加@State,这是ArkUI性能优化的基本原则之一。
7.2 生命周期钩子
aboutToAppear() {
this.timer = setInterval(() => { this.breath = !this.breath; }, 1000);
}
aboutToDisappear() { clearInterval(this.timer); }
ArkUI提供了丰富的生命周期钩子函数,当前页面使用了其中两个:
aboutToAppear:在组件即将出现时调用,是执行初始化操作的最佳时机。这里启动了一个间隔为1000毫秒的定时器,每秒翻转一次breath变量的值,从而驱动所有绑定了breath状态的UI元素产生呼吸动画效果。
aboutToDisappear:在组件即将消失时调用,用于执行清理操作。这里清除定时器,防止内存泄漏。这是一个非常重要的编程习惯——凡是在aboutToAppear中创建的资源,都应该在aboutToDisappear中对应释放。
呼吸动画的实现思路非常巧妙:通过一个简单的布尔变量翻转,配合三目运算符(this.breath ? A : B),就可以让UI元素在两种状态之间平滑过渡。ArkUI的属性动画机制会自动在状态切换时添加过渡效果,无需开发者手动编写动画代码。
7.3 主体布局结构
build() {
Column() {
this.headerOverview()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) { this.tabWorkbench() }
else if (this.currentTab === 1) { this.tabVisitor() }
else if (this.currentTab === 2) { this.tabMeeting() }
else if (this.currentTab === 3) { this.tabRepair() }
else if (this.currentTab === 4) { this.tabNotice() }
else if (this.currentTab === 5) { this.tabMine() }
this.chartCard()
}
}
.scrollBar(BarState.Off)
.layoutWeight(1)
this.tabBar()
if (this.addModal) { this.panelAdd(() => { this.addModal = false }) }
if (this.editModal) { this.panelEdit(() => { this.editModal = false }) }
if (this.delModal) { this.panelDel(() => { this.delModal = false }) }
}
.backgroundColor(COLORS.bg)
.height('100%')
}
build()方法是组件的核心,它描述了页面的完整UI结构。整体采用垂直布局(Column),从上到下依次为:
头部区域:调用headerOverview()构建函数渲染园区概览头部,包含应用名称、通知图标、园区概览数据卡和快捷功能入口。
分割线:一条1像素的细线,将头部与内容区域视觉分离。使用Divider组件,颜色取自色彩系统中的line变量。
滚动内容区:使用Scroll组件包裹中间内容,确保内容超出屏幕时可以滚动浏览。.scrollBar(BarState.Off)隐藏滚动条,使界面更加简洁。.layoutWeight(1)让滚动区域自动填充剩余空间,这是实现"头部固定-内容滚动-底部固定"经典布局的关键。
滚动内容区内部又包含两部分:通过if-else条件渲染的六个Tab页面(根据currentTab的值显示对应的页面),以及始终显示在底部的能耗统计图表卡片chartCard()。
底部Tab栏:调用tabBar()构建函数渲染底部导航栏,提供六个Tab的切换入口。
弹窗层:三个条件渲染的弹窗组件,分别对应新增、编辑、删除三种操作场景。弹窗使用绝对定位覆盖在页面上方,通过状态变量控制显示与隐藏。每个弹窗都接收一个关闭回调函数,点击确认或取消后关闭弹窗。
整体页面的背景色设置为COLORS.bg(浅灰蓝色),高度占满全屏。这种三段式(头-内容-底)的布局结构是移动应用最经典的布局模式之一,结构清晰、交互直观、用户学习成本低。
八、头部区域详解
头部区域是用户进入应用后首先看到的部分,承担着品牌展示、信息概览、快捷操作等多重功能。当前页面的头部区域设计精巧,内容丰富,由headerOverview()构建函数实现。
8.1 顶部导航栏
Row() {
Column() {
Text('🏢 智园通').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text('A栋 · 12层 · 技术研发部').fontSize(11).fontColor(COLORS.sub).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start)
Blank()
Stack() {
Text('🔔').fontSize(20)
Column() {
Text('5').fontSize(9).fontColor(COLORS.card)
}
.width(14).height(14).borderRadius(7).backgroundColor(COLORS.red)
.alignContent(Alignment.Center)
.position({ x: 14, y: 0 })
}
.width(36).height(36).alignContent(Alignment.Center)
}
.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 8 })
顶部导航栏采用左右布局结构:
左侧是应用品牌信息。主标题"🏢 智园通"使用20号加粗字体,深灰蓝色,醒目而专业。副标题显示用户的位置和部门信息"A栋 · 12层 · 技术研发部",使用11号小号字体和次要文字色,提供上下文信息但不抢夺主标题的视觉焦点。
右侧是通知入口。使用铃铛emoji作为通知图标,右上角叠加一个红色圆形角标,显示未读消息数量"5"。角标的实现使用了Stack堆叠布局配合.position()绝对定位的方式——红色小圆点通过position({ x: 14, y: 0 })定位到图标的右上角,形成了典型的角标效果。角标尺寸为14x14像素,圆角7像素(即圆形),内部数字使用9号白色字体。
整个导航栏宽度占满屏幕,左右各16像素内边距,顶部12像素、底部8像素的垂直内边距,营造出舒适的视觉呼吸感。
8.2 园区概览数据卡
Row() {
Column() {
Row() {
Text('📊').fontSize(18)
Text('园区概览').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.title).margin({ left: 4 })
Blank()
Text('今日').fontSize(11).fontColor(COLORS.green)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(COLORS.chip).borderRadius(8)
}.width('100%')
Row() {
Column() {
Text('1,286').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.blue)
Text('在岗人数').fontSize(10).fontColor(COLORS.sub).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
// ... 其余三项类似
}.width('100%').margin({ top: 10 })
}
.width('100%').padding(14).borderRadius(14)
.backgroundColor(COLORS.card)
}
.width('100%').padding({ left: 16, right: 16 })
园区概览数据卡是头部区域的核心信息展示模块,采用白色卡片式设计,圆角14像素,内边距14像素。
卡片标题栏左侧显示图表图标和"园区概览"标题,右侧有一个"今日"标签,使用绿色文字配淡蓝色背景,表明数据是当日的实时统计。
卡片主体展示四项关键指标,每项占四分之一等宽布局:
- 在岗人数:1,286人,科技蓝显示。这是园区运营的基础数据,反映当前园区内的人员规模。
- 待办事项:6件,智慧绿显示。提醒用户有待处理的工作事项。
- 待审申请:3件,橙色显示。提示有审批等待处理,橙色起到警示作用。
- 设备完好率:98%,智慧绿深色显示。反映园区设备的整体运行状况。
四个数据指标分别使用不同的颜色——蓝色、绿色、橙色、绿色深色,既保持了色彩的丰富性,又通过颜色语义传达了数据的性质。数值使用22号加粗大字,标签使用10号小字,形成鲜明的大小对比,让用户一眼就能抓住关键数字。
这种数据概览卡的设计在B端产品中非常常见,它将最重要的运营指标浓缩在一个卡片中,用户打开应用就能快速了解园区整体运行状态。
8.3 快捷功能入口
Row() {
Column() {
Stack() { Text('✅').fontSize(20) }
.width(40).height(40).borderRadius(20)
.backgroundColor(this.breath ? COLORS.blueD : COLORS.chip)
.alignContent(Alignment.Center)
Text('考勤打卡').fontSize(10).fontColor(COLORS.sub).margin({ top: 4 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
// ... 其余四个类似
}
.width('100%').padding({ left: 12, right: 12, top: 12, bottom: 10 })
快捷功能入口提供了五个高频操作的一键直达入口:考勤打卡、快速审批、会议预约、访客登记、一键报修。
每个入口采用"图标+文字"的上下布局,图标放置在40x40像素的圆形背景中。其中第一个"考勤打卡"入口的背景色与呼吸动画绑定——this.breath ? COLORS.blueD : COLORS.chip,当breath为true时显示深蓝色,为false时显示淡蓝色。配合ArkUI的属性动画机制,背景色会在两种颜色之间平滑过渡,形成呼吸灯般的脉动效果。
这种呼吸动画设计在交互上具有重要意义:它将用户最需要关注的操作(考勤打卡)通过动态效果突出显示,引导用户的视觉注意力。同时,柔和的呼吸节奏不会造成视觉干扰,体现了动效设计的"适度"原则。
五个入口平分屏幕宽度(通过.layoutWeight(1)实现),图标下方是10号灰色功能名称文字。整个快捷入口区域左右内边距12像素,顶部12像素、底部10像素,布局紧凑而不拥挤。
头部区域的设计价值
头部区域虽然只占页面的一小部分,但其信息密度和功能密度都很高。它同时承担了以下功能:
- 品牌展示:通过应用名称和Logo建立品牌认知。
- 位置感知:显示用户当前所在的楼栋和部门信息。
- 消息提醒:通知入口和未读数提醒用户关注新消息。
- 数据概览:四个关键指标让用户快速掌握园区运行状态。
- 快捷操作:五个高频功能入口,减少用户的操作路径。
这种"头部即服务"的设计理念,在B端移动应用中尤为重要。用户打开应用后,无需深入导航,就能在首屏获取最关键的信息并执行最频繁的操作,大幅提升了使用效率。
九、工作台Tab——宫格菜单详解
工作台是用户进入应用后的默认页面,也是功能导航的主入口。该页面采用经典的宫格菜单布局,将十二个常用功能以四行三列的网格形式整齐排列,用户可以一目了然地浏览所有功能入口。
9.1 页面标题区
@Builder tabWorkbench() {
Column() {
Row() {
Text('💼 工作台').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('全部应用 >').fontSize(12).fontColor(COLORS.blue)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('常用功能快捷入口,点击进入对应模块')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 12 })
页面标题区采用左右布局:左侧是"💼 工作台"标题,使用18号加粗字体,庄重醒目;右侧是"全部应用 >“入口,使用科技蓝色,暗示可以点击查看更多应用。标题下方还有一行辅助说明文字"常用功能快捷入口,点击进入对应模块”,使用11号浅灰色,为用户提供操作指引。
这种"主标题+更多入口+副标题说明"的三段式标题设计,在B端产品中非常典型。它既明确了当前页面的功能定位,又为深度操作提供了入口,同时用说明文字降低了新用户的认知门槛。
9.2 宫格菜单渲染机制
十二个功能入口分为四行排列,每行三个。代码中使用了四个独立的Row来渲染每一行,每个Row内部通过ForEach遍历对应的行索引数组。
Row() {
ForEach(WORKBENCH_ROW0, (idx: number) => {
Column() {
Stack() {
Text(this.workbenchList[idx].icon).fontSize(28)
if (this.workbenchList[idx].badge > 0) {
Column() {
Text(this.workbenchList[idx].badge > 99 ? '99+' : this.workbenchList[idx].badge.toString())
.fontSize(9).fontColor(COLORS.card)
}
.width(16).height(16).borderRadius(8).backgroundColor(COLORS.red)
.alignContent(Alignment.Center)
.position({ x: 26, y: 0 })
}
}
.width(52).height(52).borderRadius(14).backgroundColor(COLORS.chip)
.alignContent(Alignment.Center)
Text(this.workbenchList[idx].name).fontSize(11).fontColor(COLORS.title).margin({ top: 6 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 8, bottom: 8 })
.onClick(() => { this.editIdx = idx; this.editModal = true })
}, (idx: number) => 'r0_' + idx.toString())
}
.width('100%').padding({ left: 8, right: 8 })
每个宫格项的结构可以拆解为以下几层:
外层容器:使用Column垂直布局,.layoutWeight(1)确保三个项在一行中平分宽度。上下各8像素内边距,为点击区域留出足够空间。
图标区域:使用Stack堆叠布局,52x52像素的圆角矩形(圆角14像素)作为图标背景,背景色为淡蓝色COLORS.chip。emoji图标大小为28号,居中显示。
角标提示:当badge大于0时,在图标右上角显示红色圆形角标。角标尺寸为16x16像素,通过.position({ x: 26, y: 0 })定位到图标右上角。角标数字有一个巧妙的处理——当数字超过99时显示"99+",避免数字过大撑破角标样式。这是一个非常实用的用户体验细节。
功能名称:图标下方是功能名称文字,11号深灰蓝色,与图标保持6像素的间距。
点击交互:每个宫格项绑定了.onClick()事件,点击时设置editIdx为当前索引并打开编辑弹窗。这里用编辑弹窗来模拟功能入口的点击效果,在实际项目中可以替换为页面跳转或功能展开。
9.3 四行布局与键值生成
四行菜单的渲染逻辑基本相同,区别仅在于遍历的索引数组不同(WORKBENCH_ROW0到ROW3)以及ForEach的键值生成器不同。
键值生成器是ForEach的第三个参数,用于为每个列表项生成唯一标识。ArkUI框架通过这个键值来判断列表项的变化,从而高效地更新UI。代码中分别使用了'r0_' + idx、'r1_' + idx等前缀,确保不同行的列表项不会因为索引相同而产生键值冲突。
虽然四行代码看起来有重复,但这种写法也有其优势:每行都是独立的布局单元,可以单独调整样式和间距,灵活度更高。如果要实现更复杂的布局(如某些行有特殊样式),这种逐行控制的方式会更加方便。
9.4 添加应用按钮
Row() {
Button('+ 添加应用').fontSize(13).fontColor(COLORS.card)
.backgroundColor(COLORS.blue).borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
}
.width('100%').justifyContent(FlexAlign.Center).margin({ top: 8, bottom: 8 })
宫格菜单底部是一个"+ 添加应用"按钮,使用科技蓝背景配白色文字,圆角20像素(胶囊形状),高度36像素。按钮水平居中显示,点击后打开新增弹窗。
这个按钮的设计有两个作用:一是提供功能扩展的入口,用户可以自定义添加更多应用到工作台;二是在视觉上为宫格区域收一个尾,形成完整的区块感。
工作台页面作为应用的首页,其设计体现了"效率优先"的原则——所有功能一目了然,点击路径最短,信息密度适中。宫格菜单作为最经典的导航模式之一,在B端产品中经久不衰,正是因为它在信息展示效率和用户认知成本之间取得了最佳平衡。
十、访客Tab——状态列表与标签筛选
访客管理是智慧园区的重要场景之一,涉及来访预约、身份登记、状态追踪等多个环节。当前页面的访客Tab采用了"标签筛选+状态列表"的布局模式,用户可以通过顶部标签快速筛选不同状态的访客,下方以列表形式展示访客详情。
10.1 页面标题与统计
@Builder tabVisitor() {
Column() {
Row() {
Text('👥 访客管理').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('共6位').fontSize(11).fontColor(COLORS.green)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('访客预约与来访记录,实时同步状态')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 8 })
访客页面的标题区结构与工作台类似,但右上角显示的统计信息有所不同——"共6位"使用绿色文字,直观告知用户当前访客记录的总数量。副标题"访客预约与来访记录,实时同步状态"说明了该模块的核心功能。
10.2 状态标签筛选栏
Scroll() {
Row() {
Text('全部').fontSize(12).fontColor(COLORS.card)
.backgroundColor(COLORS.blue).borderRadius(14)
.padding({ left: 12, right: 12, top: 4, bottom: 4 }).margin({ right: 8 })
Text('待确认').fontSize(12).fontColor(COLORS.sub)
.backgroundColor(COLORS.chip).borderRadius(14)
.padding({ left: 12, right: 12, top: 4, bottom: 4 }).margin({ right: 8 })
// ... 其余标签类似
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.margin({ bottom: 12 })
状态标签筛选栏是访客页面的特色功能。它使用水平滚动的标签列表,提供"全部、待确认、已预约、已离开、已取消"五个筛选选项。
标签采用胶囊形状(圆角14像素),左右内边距12像素,上下4像素。当前选中的"全部"标签使用科技蓝背景配白色文字,未选中的标签使用淡蓝色背景配灰色文字。这种选中态与未选中态的强烈对比,让用户清晰知道当前的筛选条件。
标签栏外层包裹了Scroll组件,设置为水平滚动方向,隐藏滚动条。当标签数量较多超出屏幕宽度时,用户可以左右滑动查看更多标签。这是一种处理多分类筛选的经典方案。
需要说明的是,当前代码中的标签是静态展示的,点击交互尚未完全实现(点击后不会切换选中态)。在实际项目中,可以通过增加一个@State变量来记录当前选中的标签索引,然后根据索引动态设置标签的背景色和文字色。
10.3 访客列表卡片
ForEach(this.visitorList, (item: VisitorItem, idx: number) => {
Row() {
Stack() { Text('👤').fontSize(24) }
.width(44).height(44).borderRadius(22).backgroundColor(COLORS.chip)
.alignContent(Alignment.Center)
Column() {
Row() {
Text(item.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(item.status).fontSize(10).fontColor(COLORS.card)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8).backgroundColor(visitorColor(item.status))
.margin({ left: 8 })
}.alignItems(VerticalAlign.Center)
Text(item.reason).fontSize(12).fontColor(COLORS.sub).margin({ top: 4 })
Row() {
Text('🕐 ' + item.date).fontSize(10).fontColor(COLORS.text3)
Text(' 📱 ' + item.phone).fontSize(10).fontColor(COLORS.text3).margin({ left: 10 })
}.margin({ top: 4 })
}.margin({ left: 12 }).alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('>').fontSize(16).fontColor(COLORS.text3)
}
.width('92%').padding(12).borderRadius(12).backgroundColor(COLORS.card)
.margin({ bottom: 8 })
.onClick(() => { this.editIdx = idx; this.editModal = true })
}, (item: VisitorItem) => item.name + item.date)
访客列表是页面的主体内容,使用ForEach遍历visitorList数组渲染每条访客记录。每条记录是一个白色卡片,采用左右布局结构:
左侧头像:44x44像素的圆形头像区域,使用人物emoji作为占位头像,淡蓝色背景。圆形头像在社交类和人员管理类界面中非常常见,能够快速建立人物识别。
中间信息区:这是卡片的核心内容区域,包含三行信息:
- 第一行:访客姓名(15号加粗)+ 状态标签(10号白色文字,背景色由
visitorColor函数根据状态动态返回)。姓名与状态标签垂直居中对齐。 - 第二行:来访事由,12号灰色文字,简要说明访客来意。
- 第三行:来访时间和联系电话,10号浅灰色文字,分别用时钟和手机emoji作为前缀标识。
右侧箭头:一个">"符号,灰色,暗示该卡片可以点击进入详情页。这是移动界面中常见的"可点击"视觉暗示。
整张卡片宽度为屏幕宽度的92%,内边距12像素,圆角12像素,卡片之间有8像素的间距。这种"92%宽度+居中"的卡片设计在移动端非常流行,既保证了内容区域的宽敞,又通过两侧留白营造了良好的视觉节奏感。
ForEach的键值生成器使用了item.name + item.date的组合,确保每条记录的唯一性。因为可能存在同名访客,但姓名+日期的组合几乎不会重复。
10.4 新增访客按钮
Row() {
Button('+ 新增访客').fontSize(13).fontColor(COLORS.card)
.backgroundColor(COLORS.blue).borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
}
.width('100%').justifyContent(FlexAlign.Center).margin({ top: 4, bottom: 8 })
列表底部是"+ 新增访客"按钮,样式与工作台的添加按钮一致——科技蓝胶囊形按钮,白色文字,点击打开新增弹窗。
访客页面的整体设计体现了"状态驱动"的管理思想:通过色彩和标签将不同状态的访客清晰区分,用户可以一眼识别哪些需要处理、哪些已完成。这种色彩语义化的设计在B端管理系统中尤为重要,它将抽象的状态概念转化为直观的视觉信号,大幅提升了信息获取效率。
十一、会议室Tab——卡片网格布局
会议室预订是智慧园区办公场景中的高频需求。会议室Tab采用了两列卡片网格的布局形式,将所有会议室以卡片形式直观展示,用户可以快速浏览各会议室的状态并进行预约操作。
11.1 页面标题区
@Builder tabMeeting() {
Column() {
Row() {
Text('🏢 会议室预订').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('今日可用 >').fontSize(11).fontColor(COLORS.blue)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('实时查看会议室状态,快速预约使用')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 12 })
会议室页面的标题区延续了统一的设计风格。右上角"今日可用 >"的入口用蓝色文字提示用户可以快速筛选可用会议室,提升预约效率。
11.2 两列卡片网格结构
六个会议室分为三排展示,每排两个,形成两列网格布局。代码中使用三个独立的Row来渲染每一排,每排通过ForEach遍历两个列索引。
Row() {
ForEach([0, 1], (col: number) => {
let item = this.meetingList[col];
Column() {
// 卡片内容...
}
.width('48%').padding(12).borderRadius(12).backgroundColor(COLORS.card)
.alignItems(HorizontalAlign.Start)
.margin({ left: col === 0 ? 0 : '4%' })
.onClick(() => { this.editIdx = col; this.editModal = true })
}, (col: number) => 'm_row1_' + col.toString())
}
.width('92%').margin({ bottom: 10 })
两列网格的宽度计算很有技巧:每张卡片宽度为48%,两张卡片之间的间距为4%(通过左卡片的margin-left实现,第一列左边距为0,第二列左边距为4%)。48% + 4% + 48% = 100%,恰好填满容器宽度。这种百分比布局方式能够自适应不同屏幕尺寸,保证在各种设备上都能呈现规整的两列布局。
每张卡片使用白色背景,圆角12像素,内边距12像素,整体视觉风格与其他页面保持一致。
11.3 会议室卡片内容解析
每张会议室卡片包含丰富的信息,采用垂直布局排列:
Row() {
Text(item.floor).fontSize(10).fontColor(COLORS.sub)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.chip).borderRadius(6)
Blank()
Text(item.status).fontSize(10).fontColor(COLORS.card)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6).backgroundColor(meetingColor(item.status))
}.width('100%')
顶部标签行:左侧是楼层标签(如"3F"),灰色文字配淡蓝色背景,小尺寸圆角6像素。右侧是状态标签,白色文字,背景色由meetingColor函数根据状态动态返回。两个标签一左一右,将位置信息和状态信息同时呈现。
Text(item.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.margin({ top: 8 })
Text('容纳 ' + item.capacity + ' 人').fontSize(11).fontColor(COLORS.sub).margin({ top: 2 })
Text(item.equipment).fontSize(9).fontColor(COLORS.blue).margin({ top: 6 })
会议室基本信息:会议室名称使用14号加粗字体,是卡片的视觉焦点。下方是容纳人数信息,11号灰色文字。再下方是设备配置信息,使用9号科技蓝色文字,设备之间用竖线分隔。用品牌色显示设备信息,既突出了配置亮点,又不会过于张扬。
Divider().strokeWidth(1).color(COLORS.line).margin({ top: 8, bottom: 8 })
if (item.status === '使用中' || item.status === '已预约') {
Text('当前:' + item.current).fontSize(10).fontColor(COLORS.sub)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
} else {
Text(item.status === '维护中' ? '维护原因:' + item.current : '当前无会议').fontSize(10).fontColor(COLORS.text3)
}
分隔线与当前会议信息:一条细分割线将基本信息与当前状态分开。根据会议室状态的不同,显示不同的内容:使用中或已预约时显示当前会议名称,维护中时显示维护原因,空闲时显示"当前无会议"。会议名称设置了最大行数为1行,超出部分用省略号截断,防止长文本撑破布局。
Text('立即预约').fontSize(11).fontColor(COLORS.card)
.margin({ top: 8 }).padding({ top: 5, bottom: 5, left: 14, right: 14 })
.borderRadius(12).backgroundColor(item.status === '空闲' ? COLORS.green : COLORS.text3)
预约按钮:卡片底部是"立即预约"操作按钮。按钮的背景色根据会议室状态动态变化——空闲时显示绿色(可预约),其他状态显示灰色(不可预约)。这种颜色变化直观地传达了按钮的可用性,用户无需阅读文字就能判断是否可以点击预约。
11.4 三排网格的统一性与差异
三排会议室卡片的渲染逻辑基本一致,只是遍历的数据索引和键值前缀不同。第一排遍历索引0和1,第二排遍历2和3,第三排遍历4和5。键值前缀分别为m_row1_、m_row2_、m_row3_,确保每排卡片在ForEach中有唯一的键标识。
这种逐排渲染的方式虽然代码量稍大,但每排都是独立的布局单元,可以灵活控制排间距和排列方式。如果后续需要调整为"上排一个大卡片+下排两个小卡片"的混合布局,也能轻松实现。
11.5 预约会议室按钮
Row() {
Button('+ 预约会议室').fontSize(13).fontColor(COLORS.card)
.backgroundColor(COLORS.green).borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
}
.width('100%').justifyContent(FlexAlign.Center).margin({ top: 4, bottom: 8 })
页面底部的"+ 预约会议室"按钮使用了绿色背景,与其他页面的蓝色按钮形成区别。这种颜色的差异化设计与会议室模块的"智慧绿"主题相呼应,同时绿色也传达了"预约成功、畅通无阻"的正向语义。
会议室页面的两列卡片网格布局,在信息密度和可读性之间取得了良好的平衡。相比于单列列表,两列布局可以在一屏内展示更多的会议室信息;相比于三列或更多列,两列布局又能保证每张卡片有足够的空间展示详细信息。这种"刚刚好"的设计,正是B端产品追求效率与体验平衡的体现。
十二、报修Tab——状态时间轴设计
报修管理是园区物业运营的核心工作之一。报修Tab采用了时间轴布局来展示工单列表,通过左侧时间线配合右侧内容卡片的形式,直观呈现报修工单的处理进度和时间先后关系。
12.1 页面标题区
@Builder tabRepair() {
Column() {
Row() {
Text('🔧 报修中心').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('待处理 2').fontSize(11).fontColor(COLORS.orange)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('设备故障报修与处理进度追踪')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 12 })
报修页面标题区的右上角显示"待处理 2",使用醒目的橙色文字,直接告知用户当前有待处理的工单数量。橙色作为警告色,能够有效吸引用户注意力,提醒及时处理报修事项。
12.2 时间轴布局结构
时间轴是报修页面最具特色的设计元素。每条报修记录由左侧的时间轴线和右侧的内容卡片组成,整体形成一条纵向的时间线。
ForEach(this.repairList, (item: RepairItem, idx: number) => {
Row() {
// 左侧时间轴
Column() {
Column()
.width(12).height(12).borderRadius(6)
.backgroundColor(repairColor(item.status))
.margin({ top: 4 })
Column()
.width(2).layoutWeight(1)
.backgroundColor(idx === this.repairList.length - 1 ? COLORS.transparent : COLORS.line)
.margin({ top: 4 })
}
.width(20).alignItems(HorizontalAlign.Center)
// 右侧内容卡
Column() {
// ... 卡片内容
}
.layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.card)
.margin({ left: 8, bottom: 8 })
.onClick(() => { this.editIdx = idx; this.editModal = true })
}
.width('92%').alignItems(VerticalAlign.Top)
}, (item: RepairItem) => item.title + item.time)
时间轴的实现非常巧妙,我们来逐层解析:
左侧时间轴列:宽度为20像素的垂直列,内部包含两个元素:
- 时间节点圆点:12x12像素的圆形(圆角6像素),背景色由
repairColor函数根据工单状态返回。不同状态的工单显示不同颜色的圆点,用户可以通过圆点颜色快速识别工单状态。圆点顶部有4像素的上边距,使其与右侧卡片的标题文字大致对齐。 - 时间连接线:2像素宽的竖线,
.layoutWeight(1)使其自动填充剩余高度。这里有一个精妙的细节:最后一条记录的连接线使用透明色(COLORS.transparent),其他记录使用灰色分割线颜色。这样时间线在最后一个节点处终止,不会多出一截,形成完整而优雅的时间轴视觉效果。
右侧内容卡片:占据剩余宽度的白色卡片,与左侧时间轴通过8像素的左边距分隔。卡片顶部与时间轴圆点对齐(通过.alignItems(VerticalAlign.Top)实现)。
12.3 报修卡片内容
Column() {
Row() {
Text(item.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text(item.status).fontSize(10).fontColor(COLORS.card)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8).backgroundColor(repairColor(item.status))
}.width('100%').alignItems(VerticalAlign.Center)
Text(item.desc).fontSize(12).fontColor(COLORS.sub).margin({ top: 6 })
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text('🕐 ' + item.time).fontSize(10).fontColor(COLORS.text3)
Text(' 👤 ' + item.reporter).fontSize(10).fontColor(COLORS.text3).margin({ left: 10 })
}.margin({ top: 6 })
}
报修卡片内部采用垂直布局,包含三部分内容:
标题行:左侧是报修标题(15号加粗),右侧是状态标签。状态标签的样式与访客页面保持一致——圆角胶囊形,白色文字,背景色由repairColor函数动态确定。标题与标签垂直居中对齐。
描述内容:报修详细描述,12号灰色文字,最多显示两行,超出部分用省略号截断。这是一个重要的用户体验细节——既展示了足够的故障信息让维修人员判断问题,又避免了过长描述占用过多空间。
底部元信息:报修时间和报修人信息,使用10号浅灰色文字。时间用时钟emoji标识,报修人用人物emoji标识,两者之间有10像素的间距。这些辅助信息为工单提供了完整的上下文。
12.4 底部操作栏
Row() {
Button('+ 发起报修').fontSize(13).fontColor(COLORS.card)
.backgroundColor(COLORS.blue).borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
Blank()
Text('清除已完成').fontSize(13).fontColor(COLORS.red)
.onClick(() => { this.delModal = true })
}.width('92%').margin({ bottom: 8 })
报修页面的底部操作栏与其他页面不同,它包含两个操作入口:左侧是"+ 发起报修"主按钮(科技蓝),右侧是"清除已完成"文字按钮(红色)。
"清除已完成"功能使用纯文字按钮而非实心按钮,因为这是一个次要的清理操作,不需要太强的视觉强调。使用红色则暗示了这是一个删除/清除类的操作,符合用户的色彩认知习惯。点击该按钮会打开删除确认弹窗,防止用户误操作。
时间轴布局是报修页面的灵魂设计。相比于普通的列表布局,时间轴能够更直观地表达事件的时间先后和流程推进关系。在工单管理、任务追踪、审批流程等场景中,时间轴布局能够帮助用户更好地理解任务的处理进度和历史脉络,是B端产品中一种高效的信息组织方式。
十三、公告Tab——信息流卡片设计
园区公告是信息上传下达的重要渠道。公告Tab采用信息流卡片的布局形式,每条公告以独立卡片的形式纵向排列,用户可以像浏览资讯一样快速浏览最新的园区公告。
13.1 页面标题区
@Builder tabNotice() {
Column() {
Row() {
Text('📢 园区公告').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('未读 3').fontSize(11).fontColor(COLORS.red)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('园区最新通知公告,重要信息第一时间掌握')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 12 })
公告页面标题区的右上角显示"未读 3",使用红色文字,与通知类产品的未读提醒惯例保持一致。红色能够有效激发用户的阅读欲望,促使其及时查看未读消息。
13.2 公告卡片结构
每条公告是一张独立的白色卡片,卡片内信息层次分明,从上到下依次为发布来源、公告标题、内容摘要和操作入口。
ForEach(this.noticeList, (item: NoticeItem, idx: number) => {
Column() {
Row() {
Text(item.dept).fontSize(11).fontColor(COLORS.blue)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(COLORS.chip).borderRadius(6)
Blank()
if (item.unread) {
Column().width(8).height(8).borderRadius(4).backgroundColor(COLORS.red)
}
Text(item.time).fontSize(10).fontColor(COLORS.text3).margin({ left: item.unread ? 6 : 0 })
}.width('100%').alignItems(VerticalAlign.Center)
顶部信息栏:左侧是发布部门标签,使用科技蓝色文字配淡蓝色背景,小尺寸圆角。部门标签能够帮助用户快速判断公告的来源和重要性级别。右侧是发布时间和未读标记:如果是未读公告,先显示一个8像素的红色小圆点(未读标记),然后才是时间文字;已读公告则直接显示时间。时间与未读标记之间的间距会根据是否未读动态调整(未读时6像素,已读时0像素),确保布局紧凑。
Text(item.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.margin({ top: 8 }).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
公告标题:15号加粗深灰蓝色文字,是卡片的视觉核心。标题最多显示一行,超出部分用省略号截断,保证卡片高度的一致性。
Text(item.summary).fontSize(12).fontColor(COLORS.sub).margin({ top: 6 })
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
内容摘要:公告正文的摘要部分,12号灰色文字,最多显示两行。摘要让用户在不进入详情页的情况下就能大致了解公告内容,提升了信息浏览效率。两行的限制既展示了足够的信息,又避免了单条公告占用过多屏幕空间。
Row() {
Text('查看详情').fontSize(11).fontColor(COLORS.blue)
Text(' →').fontSize(11).fontColor(COLORS.blue)
}.margin({ top: 8 })
查看详情入口:卡片底部有一个"查看详情 →"的文字链接,使用科技蓝色。文字+箭头的组合是经典的"更多操作"视觉暗示,引导用户点击进入公告详情页。
13.3 卡片整体样式
}
.width('92%').padding(14).borderRadius(12).backgroundColor(COLORS.card)
.margin({ bottom: 10 })
.onClick(() => { this.editIdx = idx; this.editModal = true })
}, (item: NoticeItem) => item.title + item.time)
公告卡片宽度为屏幕的92%,内边距14像素,圆角12像素,底部间距10像素。14像素的内边距比其他页面的卡片略大,因为公告卡片的内容更多,需要更充裕的内部空间来保证阅读舒适度。
整张卡片可点击,点击后打开编辑/详情弹窗,查看公告的完整内容。
13.4 发布公告按钮
Row() {
Button('发布公告').fontSize(13).fontColor(COLORS.card)
.backgroundColor(COLORS.green).borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
}
.width('100%').justifyContent(FlexAlign.Center).margin({ top: 4, bottom: 8 })
页面底部是"发布公告"按钮,使用绿色背景。绿色在这里传达"发布、广播、通知"的正向含义,与会议室页面的预约按钮颜色保持一致,形成了"操作类功能用绿色"的色彩语义。
公告页面的信息流设计借鉴了内容类产品的交互模式——卡片式布局、标题+摘要的信息结构、未读标记的视觉提醒。这种设计既符合用户的阅读习惯,又能高效地传递信息。在B端产品中引入C端产品成熟的交互模式,能够降低用户的学习成本,提升产品的易用性。
十四、我的Tab——中心大卡个人中心
"我的"页面是用户管理个人信息和查看个人相关事务的中心。该页面采用"中心大卡+功能列表"的经典个人中心布局,顶部用一张醒目的大卡片展示用户核心信息,下方以列表形式排列各类个人服务入口。
14.1 页面标题区
@Builder tabMine() {
Column() {
Row() {
Text('👤 个人中心').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('设置 >').fontSize(11).fontColor(COLORS.blue)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('个人信息与园区服务一站式管理')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 12 })
个人中心页面标题区的右上角是"设置 >"入口,用蓝色文字提供系统设置的快速访问通道。
14.2 中心大卡——用户信息展示
中心大卡是个人页面的视觉焦点,它集中展示了用户的头像、姓名、部门、工号、等级、位置、积分等核心信息。
Column() {
Stack() {
Stack() { Text('👨💼').fontSize(42) }
.width(72).height(72).borderRadius(36)
.backgroundColor(this.breath ? COLORS.blueD : COLORS.chip)
.alignContent(Alignment.Center)
}
.width(80).height(80).borderRadius(40)
.border({ width: 3, color: COLORS.blue })
.alignContent(Alignment.Center)
头像区域:头像采用双层圆形设计——内层是72x72像素的圆形头像背景,外层是80x80像素的圆形边框,边框宽度3像素,颜色为科技蓝。双层圆形设计增加了头像的层次感和精致感。
头像背景色与呼吸动画绑定(this.breath ? COLORS.blueD : COLORS.chip),背景色会在深蓝色和淡蓝色之间平滑过渡,形成柔和的呼吸脉动效果。这种动效为静态的头像增添了生命力,也暗示了用户的"在线"状态。
头像使用人物emoji作为占位图,尺寸为42号。在实际项目中可以替换为用户的真实头像图片。
Text(this.employeeList[0].name).fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(COLORS.title).margin({ top: 12 })
Text(this.employeeList[0].dept).fontSize(13).fontColor(COLORS.sub).margin({ top: 2 })
Text('工号:' + this.employeeList[0].empNo).fontSize(11).fontColor(COLORS.text3).margin({ top: 2 })
用户基本信息:头像下方依次是姓名(20号加粗)、部门(13号灰色)、工号(11号浅灰色)。三级信息字号递减、颜色渐浅,形成清晰的信息层级。姓名作为最重要的信息,使用最大号的加粗字体。
Row() {
Column() {
Text('⭐ ' + this.employeeList[0].level).fontSize(12).fontColor(COLORS.orange)
}
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.chip).borderRadius(12)
Column() {
Text('🏢 ' + this.employeeList[0].building).fontSize(12).fontColor(COLORS.greenD)
}
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.chip).borderRadius(12)
.margin({ left: 8 })
}.margin({ top: 10 })
员工标签:姓名下方有两个标签——员工等级和办公位置。员工等级标签使用橙色文字(如"金牌员工"),配合星星emoji,体现荣誉和激励的含义。办公位置标签使用智慧绿深色文字(如"A栋12层"),配合办公楼emoji。两个标签都采用淡蓝色背景、胶囊形状,视觉风格统一。
Divider().strokeWidth(1).color(COLORS.line).margin({ top: 16, bottom: 12 })
Row() {
Column() {
Text(this.employeeList[0].points.toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.blue)
Text('积分').fontSize(10).fontColor(COLORS.sub).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('28').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.green)
Text('打卡天数').fontSize(10).fontColor(COLORS.sub).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('12').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.orange)
Text('本月申请').fontSize(10).fontColor(COLORS.sub).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%')
数据统计区:一条分割线下方是三个核心数据指标,横向等宽排列:
- 积分:2680分,科技蓝色
- 打卡天数:28天,智慧绿色
- 本月申请:12次,橙色
三个数据使用不同的品牌色,既丰富了视觉效果,又通过颜色区分了不同类型的数据。数值使用20号加粗大字,标签使用10号小字,大小对比鲜明。这三个数据分别从激励维度、考勤维度、事务维度反映了员工的园区活动情况。
}
.width('92%').padding(20).borderRadius(16).backgroundColor(COLORS.card)
.alignItems(HorizontalAlign.Center)
中心大卡整体宽度为屏幕的92%,内边距20像素,圆角16像素,是所有页面中内边距最大、圆角最大的卡片,彰显了其视觉重心地位。卡片内容全部居中对齐,营造出庄重而亲和的个人空间氛围。
14.3 功能列表
Column() {
this.mineItem('📋', '我的考勤', '查看打卡记录')
Divider().strokeWidth(1).color(COLORS.line).margin({ left: 52, right: 16 })
this.mineItem('📝', '我的申请', '请假/报销/用车')
Divider().strokeWidth(1).color(COLORS.line).margin({ left: 52, right: 16 })
this.mineItem('📅', '我的会议', '已预约会议列表')
Divider().strokeWidth(1).color(COLORS.line).margin({ left: 52, right: 16 })
this.mineItem('🔧', '我的报修', '报修进度追踪')
Divider().strokeWidth(1).color(COLORS.line).margin({ left: 52, right: 16 })
this.mineItem('👥', '我的访客', '邀请来访记录')
}
.width('92%').borderRadius(12).backgroundColor(COLORS.card)
.margin({ top: 12, bottom: 8 })
中心大卡下方是一个功能列表,包含五个个人服务入口:我的考勤、我的申请、我的会议、我的报修、我的访客。
列表项之间用分割线分隔,分割线左侧缩进52像素(与图标右侧对齐),右侧缩进16像素。这种"缩进式分割线"的设计是移动端列表的经典样式,它既分隔了列表项,又保持了视觉的连贯性——左侧图标区域形成一条隐形的纵向通道,引导用户的视线。
每个列表项都通过mineItem()构建函数生成,实现了代码的复用。
14.4 mineItem 构建函数
@Builder mineItem(icon: string, title: string, desc: string) {
Row() {
Stack() { Text(icon).fontSize(20) }
.width(36).height(36).borderRadius(10).backgroundColor(COLORS.chip)
.alignContent(Alignment.Center)
Column() {
Text(title).fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
Text(desc).fontSize(11).fontColor(COLORS.text3).margin({ top: 2 })
}.margin({ left: 10 }).alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('>').fontSize(14).fontColor(COLORS.text3)
}
.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
.onClick(() => { this.editModal = true })
}
mineItem函数接收三个参数:图标(emoji)、标题和描述。函数内部构建了一个标准的列表项:
- 左侧图标:36x36像素的圆角方形(圆角10像素),淡蓝色背景,emoji图标居中。
- 中间文字区:上下两行文字——主标题(14号,中等字重)和描述文字(11号,浅灰色)。描述文字补充说明该功能的具体内容,帮助用户理解功能范围。
- 右侧箭头:灰色的">"符号,表示可进入详情。
列表项左右内边距16像素,上下12像素,点击区域充裕。点击后打开编辑弹窗,模拟进入详情页的交互。
个人中心页面的设计体现了"以用户为中心"的产品理念——将用户最关心的个人信息和个人相关的事务集中展示在一个页面中,用户可以一站式管理自己的所有园区事务。中心大卡的设计增强了用户的归属感和身份认同感,功能列表的设计则保证了操作的便捷性。
十五、图表卡片——园区能耗柱状图
数据可视化是B端产品的重要能力,它能够将抽象的数字转化为直观的图形,帮助用户快速理解数据趋势和规律。当前页面在所有Tab的下方都放置了一个园区能耗统计图表,以柱状图的形式展示近六个月的园区能耗数据。
15.1 图表卡片整体结构
@Builder chartCard() {
Column() {
Row() {
Text('📊 园区能耗统计').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('近6个月').fontSize(12).fontColor(COLORS.text3)
}.width('100%').margin({ bottom: 12 })
图表卡片的标题栏延续了统一的设计风格:左侧是图表标题"📊 园区能耗统计",右侧是时间范围说明"近6个月"。标题使用15号加粗字体,时间范围使用12号灰色文字,主次分明。
15.2 柱状图主体
Row() {
ForEach(MONTH_IDX, (i: number) => {
Column() {
Text(ENERGY_VAL[i].toString() + 'kWh').fontSize(9).fontColor(COLORS.greenD)
.margin({ bottom: 4 })
Column()
.width(22)
.height(this.breath ? ENERGY_VAL[i] / MAX_VAL * 80 : ENERGY_VAL[i] / MAX_VAL * 64)
.borderRadius({ topLeft: 6, topRight: 6 })
.backgroundColor(i % 2 === 0 ? COLORS.blue : COLORS.green)
Text(MONTH_NAME[i]).fontSize(10).fontColor(COLORS.sub).margin({ top: 4 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}, (i: number) => i.toString())
}
.width('100%').alignItems(VerticalAlign.Bottom)
柱状图使用Row横向排列六根柱子,每根柱子占等宽比例(.layoutWeight(1))。每根柱子由三部分组成:
顶部数值标签:柱子上方显示具体的能耗数值(如"4200kWh"),使用9号智慧绿深色文字。数值标签让用户可以直接读取精确数据,而不仅是看个大概趋势。
柱体:柱体宽度为22像素,高度通过比例计算得出——ENERGY_VAL[i] / MAX_VAL * 基准高度。这里的基准高度与呼吸动画绑定:breath为true时基准高度是80像素,为false时是64像素。这意味着所有柱子的高度会在80%和64%的比例之间同步呼吸变化,形成一种生动的波浪起伏效果。
柱体的顶部左右圆角为6像素,底部为直角,形成"上圆下方"的柱形样式,是现代数据可视化中常见的美化处理。
柱体颜色采用蓝绿交替的配色方案:索引为偶数的柱子用科技蓝,奇数的用智慧绿。这种交替配色既避免了单色的单调感,又不会因为颜色过多而显得杂乱。蓝绿双色也与产品的双主色策略保持一致。
底部月份标签:柱子下方显示月份名称(如"3月"、“4月”),使用10号灰色文字,清晰标识每根柱子对应的时间维度。
整个柱状图区域通过.alignItems(VerticalAlign.Bottom)实现底部对齐,确保所有柱子的底部在同一水平线上,形成标准的柱状图视觉效果。
15.3 呼吸动画的实现机制
图表的呼吸动画是一个值得深入分析的设计亮点。它的实现原理非常简洁:
breath状态变量每秒翻转一次(由aboutToAppear中的setInterval驱动)- 柱体高度通过三目运算符绑定到
breath状态 - ArkUI框架检测到属性变化后,自动在旧值和新值之间添加过渡动画
这种"状态驱动动画"的模式是声明式UI框架的精髓所在。开发者不需要手动编写动画代码,只需要定义状态变化时的目标值,框架就会自动处理中间的过渡效果。这大大简化了动画开发的复杂度,让开发者可以将更多精力放在动画设计本身。
呼吸动画在图表中的作用是多方面的:
- 视觉吸引力:动态的图表比静态图表更能吸引用户注意
- 生命感:柔和的呼吸节奏让冰冷的数据有了温度
- 技术展示:展现了应用的动效能力和精致的交互设计
当然,动效的使用需要把握好"度"。当前页面的呼吸动画节奏平缓(1秒周期),幅度适中(80到64像素),不会对用户阅读数据造成干扰,属于"锦上添花"型的动效设计。
15.4 统计数据汇总
Divider().strokeWidth(1).color(COLORS.line).margin({ top: 12, bottom: 12 })
Row() {
Column() {
Text('总能耗').fontSize(10).fontColor(COLORS.text3)
Text('39,100 kWh').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.blue).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('月均').fontSize(10).fontColor(COLORS.text3)
Text('6,517 kWh').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.green).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('峰值月').fontSize(10).fontColor(COLORS.text3)
Text('8,500 kWh').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.orange).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%')
柱状图下方通过一条分割线分隔出统计汇总区,展示三个关键指标:
- 总能耗:39,100 kWh,科技蓝色。六个月的能耗总和,反映整体能耗规模。
- 月均:6,517 kWh,智慧绿色。月平均能耗,提供能耗水平的基准参考。
- 峰值月:8,500 kWh,橙色。能耗最高的月份数值,用橙色警示能源消耗的高峰期。
三个统计指标分别使用蓝、绿、橙三种颜色,与产品的色彩体系保持一致。每个指标的标签使用10号浅灰色,数值使用14号加粗字体,大小对比清晰。
这些汇总数据为柱状图提供了量化的补充——用户既能通过图形直观感受趋势,又能通过数字获取精确信息,两者相辅相成。
15.5 图表卡片的定位
}
.width('92%').padding(16).backgroundColor(COLORS.card).borderRadius(12)
.margin({ top: 8, bottom: 16 })
}
图表卡片宽度为屏幕的92%,内边距16像素,圆角12像素。它被放置在所有Tab页面的内容区底部,无论切换到哪个Tab,能耗统计图表始终可见。
这种"全局数据看板"的设计有其深意:能耗管理是智慧园区的核心指标之一,将能耗数据放在每个页面都能看到的位置,体现了园区管理对能源效率的重视。同时,统一的图表也为六个不同的业务Tab提供了一个视觉锚点,增强了页面之间的连贯性和整体感。
十六、底部Tab栏
底部Tab栏是移动应用的主导航入口,承载着页面切换的核心功能。当前页面的底部Tab栏设计简洁明了,六个Tab均匀分布,选中态使用品牌色强调,为用户提供清晰的导航指引。
16.1 Tab栏整体结构
@Builder tabBar() {
Row() {
ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
Column() {
Image(tab.icon).width(22).height(22)
.fillColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
Text(tab.name).fontSize(10)
.fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
.fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.padding({ top: 6, bottom: 6 })
.onClick(() => { this.currentTab = idx })
}, (tab: TabMeta) => tab.name)
}
.width('100%').height(56).backgroundColor(COLORS.card)
.border({ width: { top: 1 }, color: COLORS.line })
}
底部Tab栏使用Row水平布局,通过ForEach遍历TAB_LIST数组渲染六个Tab项。整体高度为56像素,这是移动端底部导航的标准高度,符合人体工程学中拇指易于触碰的区域范围。
Tab栏背景为白色,顶部有一条1像素的灰色边线,将Tab栏与内容区域视觉分隔。这种顶部边线的设计在iOS和Android的设计规范中都很常见,它形成了一条清晰的视觉分界线。
16.2 单个Tab项的结构
每个Tab项采用"图标+文字"的上下布局,使用Column垂直排列:
图标:使用Image组件加载资源图标,尺寸为22x22像素。图标颜色通过.fillColor()属性动态设置——选中时为科技蓝(COLORS.tabOn),未选中时为浅灰蓝(COLORS.text3)。使用单色图标配合着色(tint)技术,可以方便地实现图标的状态切换,无需准备两套图标资源。
文字:Tab名称使用10号字体,选中时为科技蓝加粗,未选中时为浅灰蓝常规字重。文字与图标之间有2像素的间距,保持紧凑的视觉关系。
每个Tab项使用.layoutWeight(1)平分Tab栏宽度,确保六个Tab等宽分布。上下各6像素的内边距为点击区域留出了足够空间,提升了点击的容错率。
16.3 交互逻辑
Tab的选中状态由currentTab状态变量控制。当用户点击某个Tab时,.onClick()事件将currentTab更新为对应的索引值。由于currentTab使用了@State装饰,状态变化会触发UI自动刷新:被选中的Tab图标和文字变为蓝色加粗,其他Tab保持灰色。
这种状态驱动的导航切换机制非常简洁——没有复杂的导航控制器,没有页面栈的管理,只是一个简单的状态变量和条件渲染。对于这种同级页面之间的平级切换,这种方式既高效又直观。
16.4 六Tab设计的考量
六个Tab的导航设计在移动端应用中属于较多的情况(通常为3-5个)。之所以设置六个Tab,是因为智慧园区的业务场景比较分散,六个核心功能都有较高的使用频率,不宜将其中某些折叠到二级入口中。
多Tab设计也带来了一些挑战:
- 空间紧张:六个Tab平分底部宽度,每个Tab的可用空间相对较小,因此图标和文字都需要控制尺寸。
- 点击精度:Tab数量多意味着每个Tab的点击区域较小,需要用户更精确地点击。不过56像素的高度和等宽分配基本保证了可点击区域的最低标准。
- 视觉平衡:六个Tab一字排开,需要确保选中态足够醒目,用户才能快速定位当前所在位置。
当前页面的设计通过以下方式应对这些挑战:
- 图标和文字都使用较小的尺寸(22像素图标、10号文字),节省空间
- 选中态使用颜色变化+字重变化的双重强调,确保辨识度
- 图标使用单色矢量风格,在小尺寸下依然清晰可辨
底部Tab栏虽然是一个相对简单的组件,但它是用户使用频率最高的交互元素之一。其设计的优劣直接影响用户对产品的整体体验。一个好的底部导航应该做到:清晰可辨、操作便捷、反馈及时、风格统一。当前页面的Tab栏设计在这些方面都达到了良好的水准。
十七、弹窗系统
弹窗是移动应用中常见的模态交互组件,用于处理新增、编辑、删除、确认等需要用户聚焦的操作。当前页面设计了一套完整的弹窗系统,包括通用遮罩层和三种功能弹窗(新增、编辑、删除),形成了统一的弹窗交互规范。
17.1 通用遮罩层
@Builder modalOverlay(onClose: () => void) {
Stack() {
Column().width('100%').height('100%').backgroundColor(COLORS.mask)
}
.width('100%').height('100%').alignContent(Alignment.Center)
.onClick(() => onClose())
}
modalOverlay是所有弹窗共用的遮罩层组件。它的实现非常简洁:一个占满全屏的半透明黑色层(COLORS.mask即rgba(0,0,0,0.5)),点击遮罩层会触发关闭回调。
遮罩层的作用是多方面的:
- 视觉聚焦:通过暗化背景,将用户的注意力引导到弹窗内容上
- 层级暗示:遮罩层清晰地表明弹窗位于页面内容之上,是更高层级的交互
- 交互阻断:遮罩层拦截了对下方页面的点击,防止用户在弹窗期间误操作页面内容
- 关闭入口:点击遮罩层关闭弹窗是用户普遍熟悉的交互模式,提供了便捷的退出方式
遮罩层接收一个onClose回调函数参数,由调用方传入具体的关闭逻辑。这种设计使得遮罩层可以被多种弹窗复用,每种弹窗只需关注自己的内容和关闭逻辑。
17.2 新增弹窗
@Builder panelAdd(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('➕ 新增申请').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(COLORS.title).margin({ bottom: 16 })
Column() {
Text('标题').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '请输入标题' }).fontSize(14).fontColor(COLORS.title)
.placeholderColor(COLORS.text3).backgroundColor(COLORS.chip)
.borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 12 })
Column() {
Text('内容描述').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '请输入详细描述' }).fontSize(14).fontColor(COLORS.title)
.placeholderColor(COLORS.text3).backgroundColor(COLORS.chip)
.borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 12 })
Column() {
Text('备注').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '添加备注(可选)' }).fontSize(14).fontColor(COLORS.title)
.placeholderColor(COLORS.text3).backgroundColor(COLORS.chip)
.borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 16 })
Row() {
Button('取消').fontSize(14).fontColor(COLORS.sub).backgroundColor(COLORS.chip)
.borderRadius(20).height(40).layoutWeight(1).onClick(() => onClose())
Button('确认提交').fontSize(14).fontColor(COLORS.card).backgroundColor(COLORS.blue)
.borderRadius(20).height(40).layoutWeight(1).margin({ left: 10 }).onClick(() => onClose())
}.width('100%')
}
.width('80%').padding(24).backgroundColor(COLORS.card).borderRadius(20)
}
.width('100%').height('100%').alignContent(Alignment.Center)
}
新增弹窗(panelAdd)是一个居中显示的对话框,宽度为屏幕的80%,白色背景,圆角20像素,内边距24像素。弹窗内容包括:
标题:“➕ 新增申请”,18号加粗字体,明确弹窗的用途。
表单区域:包含三个输入字段:
- 标题:必填项,placeholder为"请输入标题"
- 内容描述:必填项,placeholder为"请输入详细描述"
- 备注:可选项,placeholder为"添加备注(可选)"
每个输入字段都有标签文字(13号灰色)和输入框。输入框使用淡蓝色背景,圆角8像素,高度40像素。placeholder文字使用浅灰色,输入文字使用深灰蓝色。输入框的样式设计统一而整洁。
操作按钮:底部有两个按钮,左右排列,各占一半宽度:
- 取消按钮:灰色文字配淡蓝色背景,次要操作样式
- 确认提交按钮:白色文字配科技蓝背景,主要操作样式
两个按钮都是胶囊形状(圆角20像素),高度40像素,之间有10像素的间距。主操作按钮使用品牌色强调,符合用户的操作预期。
17.3 编辑/详情弹窗
@Builder panelEdit(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('✏️ 详情查看').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(COLORS.title).margin({ bottom: 16 })
Column() {
Text('当前序号').fontSize(13).fontColor(COLORS.sub)
Text(this.editIdx >= 0 ? '第 ' + (this.editIdx + 1) + ' 条记录' : '未选择')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.green).margin({ top: 4 })
}.width('100%').margin({ bottom: 12 })
Column() {
Text('修改内容').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '请输入修改内容' }).fontSize(14).fontColor(COLORS.title)
.placeholderColor(COLORS.text3).backgroundColor(COLORS.chip)
.borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 12 })
Column() {
Text('备注信息').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '添加备注(可选)' }).fontSize(14).fontColor(COLORS.title)
.placeholderColor(COLORS.text3).backgroundColor(COLORS.chip)
.borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 16 })
Row() {
Button('取消').fontSize(14).fontColor(COLORS.sub).backgroundColor(COLORS.chip)
.borderRadius(20).height(40).layoutWeight(1).onClick(() => onClose())
Button('保存修改').fontSize(14).fontColor(COLORS.card).backgroundColor(COLORS.green)
.borderRadius(20).height(40).layoutWeight(1).margin({ left: 10 }).onClick(() => onClose())
}.width('100%')
}
.width('80%').padding(24).backgroundColor(COLORS.card).borderRadius(20)
}
.width('100%').height('100%').alignContent(Alignment.Center)
}
编辑弹窗(panelEdit)的整体结构与新增弹窗类似,但有几个关键区别:
标题不同:使用"✏️ 详情查看"作为标题,暗示这个弹窗兼具查看详情和编辑修改的功能。
当前序号显示:新增了一个"当前序号"信息字段,显示当前操作的是第几条记录。序号使用绿色加粗字体,直观醒目。当editIdx为-1(未选中)时,显示"未选择"的占位文字。
确认按钮颜色不同:确认按钮使用绿色背景(“保存修改”),而非新增弹窗的蓝色。绿色在这里传达"保存、确认、完成"的含义,与新增操作形成区分。这种通过颜色区分操作类型的设计,能够帮助用户快速识别当前的操作性质。
17.4 删除确认弹窗
@Builder panelDel(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('🗑️ 确认删除').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(COLORS.title).margin({ bottom: 12 })
Text(this.delIdx >= 0 ? '确定要删除第 ' + (this.delIdx + 1) + ' 条记录吗?' : '确定要清除已完成记录吗?此操作不可撤销。')
.fontSize(14).fontColor(COLORS.sub).textAlign(TextAlign.Center).margin({ bottom: 20 })
Row() {
Button('取消').fontSize(14).fontColor(COLORS.sub).backgroundColor(COLORS.chip)
.borderRadius(20).height(40).layoutWeight(1).onClick(() => onClose())
Button('确认删除').fontSize(14).fontColor(COLORS.card).backgroundColor(COLORS.red)
.borderRadius(20).height(40).layoutWeight(1).margin({ left: 10 }).onClick(() => onClose())
}.width('100%')
}
.width('80%').padding(24).backgroundColor(COLORS.card).borderRadius(20)
}
.width('100%').height('100%').alignContent(Alignment.Center)
}
删除确认弹窗(panelDel)是三种弹窗中结构最简单的,但也是交互上最需要谨慎的。删除操作具有不可逆性,因此需要明确的二次确认。
删除弹窗的特点包括:
简洁的内容:只包含标题和确认文案,没有输入表单。这是因为删除操作不需要用户输入额外信息,只需要确认意图。
动态确认文案:确认文案会根据delIdx的值动态变化。如果delIdx有效(>=0),显示删除单条记录的确认信息;如果无效(-1),显示清除已完成记录的批量操作确认信息,并特别强调"此操作不可撤销",提醒用户谨慎操作。
红色确认按钮:确认删除按钮使用红色背景,这是UI设计中的通用惯例——红色代表危险、警告、不可逆操作。红色能够唤起用户的警觉,防止误删。
居中对齐的文案:确认文案使用居中对齐(textAlign(TextAlign.Center)),在视觉上更加庄重,也符合确认对话框的常见设计模式。
弹窗系统的设计价值
这套弹窗系统虽然功能相对基础(没有实现真正的数据增删改逻辑),但其设计架构体现了良好的工程实践:
组件复用:通用遮罩层modalOverlay被三个弹窗共享,避免了重复代码。
样式统一:所有弹窗遵循相同的视觉规范——相同的宽度、圆角、内边距、按钮样式,确保了交互体验的一致性。
回调机制:每个弹窗都接收关闭回调函数,由调用方控制弹窗的生命周期,实现了弹窗组件与业务逻辑的解耦。
状态驱动:弹窗的显示与隐藏完全由@State变量控制,符合声明式UI的编程范式。
在实际项目中,这套弹窗系统可以进一步扩展——比如使用@CustomDialog装饰器封装为独立的弹窗组件、通过参数传递动态配置弹窗内容、增加表单验证逻辑等。当前的实现已经为这些扩展打下了良好的基础。
十八、功能模块对比表
为了更清晰地展示六个功能模块的设计特点和技术实现差异,下面从多个维度进行对比分析。
| 对比维度 | 工作台 | 访客 | 会议室 | 报修 | 公告 | 我的 |
|---|---|---|---|---|---|---|
| 布局模式 | 宫格菜单(4行×3列) | 状态列表 + 标签筛选 | 两列卡片网格(3排×2列) | 时间轴列表 | 信息流卡片 | 中心大卡 + 功能列表 |
| 数据模型 | WorkbenchItem | VisitorItem | MeetingItem | RepairItem | NoticeItem | EmployeeItem |
| 模型字段数 | 3个(icon/name/badge) | 5个(name/reason/status/date/phone) | 6个(name/capacity/equipment/status/current/floor) | 5个(title/desc/status/time/reporter) | 5个(title/dept/time/summary/unread) | 6个(name/dept/empNo/points/level/building) |
| 状态类型数 | 1种(角标数字) | 4种(待确认/已预约/已离开/已取消) | 4种(空闲/使用中/已预约/维护中) | 4种(待受理/处理中/已完成/已驳回) | 2种(已读/未读) | 3种(等级/积分/打卡) |
| 颜色映射函数 | 无 | visitorColor | meetingColor | repairColor | 无 | 无 |
| 状态主色 | 红色(角标) | 橙/绿/灰/红 | 绿/蓝/橙/红 | 橙/蓝/绿/红 | 红色(未读点) | 橙/蓝/绿 |
| 列表渲染方式 | 逐行ForEach(4行) | 单列ForEach | 逐排ForEach(3排) | 单列ForEach | 单列ForEach | 构建函数复用 |
| 卡片宽度 | 自适应(三分之一) | 92%屏宽 | 48%屏宽 | 92%屏宽(加时间轴) | 92%屏宽 | 92%屏宽 |
| 卡片圆角 | 14px(图标背景) | 12px | 12px | 12px | 12px | 16px(大卡)/12px(列表) |
| 操作按钮颜色 | 蓝色(添加应用) | 蓝色(新增访客) | 绿色(预约会议室) | 蓝色(发起报修) | 绿色(发布公告) | 无(列表项跳转) |
| 顶部统计信息 | 全部应用入口 | 共6位(绿色) | 今日可用入口 | 待处理2(橙色) | 未读3(红色) | 设置入口 |
| 角标/红点 | 有(数字角标) | 无 | 无 | 无 | 有(未读红点) | 无 |
| 呼吸动画关联 | 无 | 无 | 无 | 无 | 无 | 头像背景呼吸 |
| 点击交互 | 打开编辑弹窗 | 打开编辑弹窗 | 打开编辑弹窗 | 打开编辑弹窗 | 打开编辑弹窗 | 打开编辑弹窗 |
| 信息密度 | 高(12个入口) | 中(6条记录) | 高(6张卡片) | 中(6条记录) | 中(6条公告) | 低(大卡+5项列表) |
对比分析解读
布局多样性:六个Tab采用了六种完全不同的布局方式——宫格、列表+标签、卡片网格、时间轴、信息流、中心大卡。这种多样化的布局设计充分展现了ArkUI框架的UI表达能力,也体现了产品根据不同业务场景选择最合适布局方式的设计思路。
状态复杂度差异:访客、会议室、报修三个模块的状态类型最为丰富(各4种),因此都配备了专门的颜色映射函数。工作台和公告模块虽然状态简单(角标数字、未读标记),但都使用了红色作为提醒色,符合"红色=警示/待处理"的色彩语义。
数据模型字段数:会议室和员工模型字段最多(6个),因为这两个场景需要描述的信息维度最丰富。工作台模型最简单(3个字段),因为它只是功能入口的抽象。
操作按钮的颜色策略:操作按钮的颜色选择并非随意——添加/新增类操作用蓝色,预约/发布类操作用绿色。蓝色代表"创建、新增",绿色代表"预约、发布、确认",形成了微妙的色彩语义区分。
信息密度梯度:从工作台(最高密度)到我的页面(最低密度),信息密度呈现逐渐降低的趋势。这与用户的使用场景相匹配——工作台是功能导航,需要一目了然;个人中心是身份展示,需要舒适大气。
交互一致性:尽管六个页面的布局和内容差异很大,但点击交互保持了高度一致——都是点击后打开编辑/详情弹窗。这种交互一致性降低了用户的学习成本,无论在哪个页面,用户都知道点击卡片会进入详情。
十九、总结与展望
19.1 设计总结
通过对"智园通"智慧园区工作台组件的全面剖析,我们可以总结出以下几个核心设计特点:
1. 组件化架构清晰,分层明确
该应用采用了典型的分层架构设计,从颜色系统、数据模型、状态管理到UI渲染,每一层都有清晰的职责边界。六个@Observed数据模型与六个业务Tab一一对应,@Builder构建函数将UI片段模块化,主组件作为调度中心统筹全局。这种架构使得代码结构清晰、模块独立性强、便于维护和扩展。
2. 六大场景六种布局,针对性强
六个功能Tab没有采用千篇一律的列表布局,而是根据各业务场景的特点选择了最适合的布局方式:工作台用宫格菜单保证功能入口的密度,访客用状态列表便于管理,会议室用卡片网格直观展示空间状态,报修用时间轴体现流程推进,公告用信息流卡片适合阅读,我的用中心大卡突出个人身份。这种"场景驱动布局"的设计理念是B端产品走向成熟的重要标志。
3. 色彩体系完善,语义化程度高
科技蓝与智慧绿的双主色策略,配合三级文字灰度、四种语义功能色,构成了一套完整而专业的色彩体系。颜色在该应用中不仅仅是视觉装饰,更是信息传达的重要工具——不同的状态用不同的颜色标识,不同的操作用不同的颜色区分,用户可以通过颜色快速获取信息。这种色彩语义化的设计在数据密集型的B端产品中尤为重要。
4. 动效精致适度,提升体验
呼吸动画是该应用的一个亮点设计。通过一个简单的breath状态变量和setInterval定时器,就实现了头部快捷入口、个人头像、能耗柱状图等多个元素的联动呼吸效果。动画节奏平缓、幅度适中,既提升了界面的精致感和生命力,又不会对用户造成干扰。这种"适度动效"的设计理念值得借鉴。
5. 弹窗系统统一,交互规范
三种功能弹窗(新增、编辑、删除)共享统一的遮罩层、视觉规范和交互模式,形成了完整的弹窗体系。不同类型的操作通过按钮颜色进行区分(蓝色新增、绿色保存、红色删除),符合用户的色彩认知习惯。弹窗的状态驱动模式也与ArkUI的声明式编程范式完美契合。
19.2 技术亮点
声明式UI的优雅表达:整个应用完全采用声明式UI范式开发,所有UI都是状态的函数。开发者只需要描述"在什么状态下UI应该是什么样子",框架就会自动处理状态变化时的UI更新。这种编程模式大幅降低了UI开发的复杂度,提升了代码的可读性和可维护性。
@Builder的组合威力:@Builder装饰器让UI片段的复用变得异常简单。从mineItem这样的小型列表项,到headerOverview这样的复杂头部区域,再到modalOverlay这样的通用组件,都可以通过构建函数进行封装和复用。这种组合式的UI构建方式,是支撑复杂界面模块化开发的关键技术。
状态管理的恰到好处:该应用的状态管理方案简洁而有效。@State管理组件内状态,@Observed实现对象级响应式,状态变量数量控制在合理范围内,没有过度设计。对于中小型应用来说,这种轻量级的状态管理方案是最佳选择——既满足了需求,又避免了引入复杂状态管理框架的 overhead。
纯CSS式的样式表达:ArkUI的链式调用样式系统(如.fontSize().fontColor().margin())非常类似于CSS的属性设置,但又比CSS更简洁、更类型安全。这种样式表达方式学习成本低,代码可读性好,是声明式UI框架的优秀实践。
19.3 可优化方向
尽管当前实现已经相当完整,但从生产级应用的标准来看,仍有以下几个可以优化的方向:
1. 数据与视图进一步解耦
目前各Tab的数据源直接绑定在主组件中,模拟数据与UI渲染逻辑混在一个文件中。在实际项目中,可以将数据层抽离为独立的service或repository模块,通过ViewModel或状态管理库进行连接。这样数据层可以独立演进(比如从本地模拟数据切换为网络请求),而无需修改UI代码。
2. 列表筛选功能完善
访客页面的状态标签目前只是静态展示,点击不会切换筛选状态。可以增加一个@State selectedVisitorStatus变量来记录当前选中的筛选条件,然后根据条件对visitorList进行过滤。同样的筛选功能也可以扩展到其他列表页面。
3. 表单验证与数据绑定
弹窗中的表单目前只是UI展示,没有实现真正的数据绑定和验证逻辑。在实际项目中,需要将输入框的值绑定到状态变量,并添加必填校验、格式校验等验证逻辑,确保用户输入的数据符合要求。
4. 列表性能优化
当前列表使用ForEach渲染,对于几十条数据完全足够。但如果数据量增长到上百条甚至更多,可以考虑使用LazyForEach实现惰性加载,只渲染可视区域内的列表项,提升长列表的滚动性能。
5. 暗黑模式适配
目前的色彩系统只有浅色模式一套配色。可以扩展ColorPalette接口,增加深色模式的色值定义,然后根据系统的深色模式设置动态切换。这对于提升夜间使用体验非常重要。
6. 国际化支持
当前所有文案都是硬编码的中文文本。对于有国际化需求的产品,可以将所有文案抽离为资源文件,通过$r()的方式引用。这样可以方便地支持多语言切换。
19.4 展望
智慧园区作为数字化转型的重要场景,未来还有广阔的发展空间。站在技术角度,我们可以展望以下几个发展方向:
AI赋能的智慧运营:随着大模型和AI技术的发展,智慧园区应用将不再仅仅是信息展示和流程审批的工具,而是会融入更多的AI能力——智能访客识别、智能故障诊断、智能能耗优化、智能会议调度等。AI将从"辅助工具"升级为"决策伙伴",帮助园区运营者做出更明智的决策。
IoT设备的深度融合:智慧园区的核心是物理空间与数字世界的融合。未来的应用将更多地与IoT设备联动——门禁系统、监控摄像头、环境传感器、智能照明、智能空调等设备的数据将实时同步到应用中,形成真正的"数字孪生"园区。
多端协同的全场景体验:HarmonyOS的分布式能力为智慧园区提供了独特的技术优势。未来的智慧园区应用将不仅仅运行在手机上,还会延伸到平板、智慧屏、可穿戴设备甚至车机上。用户可以在不同设备之间无缝流转任务,获得一致而连贯的使用体验。
低代码/无代码的配置化趋势:随着园区业务的多样化和个性化需求的增加,纯代码开发模式将难以满足快速迭代的需求。低代码/无代码平台将在智慧园区领域得到更多应用,让运营人员可以通过可视化配置的方式自定义页面和流程,降低开发成本、提升响应速度。
数据驱动的持续优化:随着用户数据和运营数据的积累,数据驱动的产品优化将成为常态。通过分析用户的使用行为、功能的使用频率、工单的处理时效等数据,可以持续优化产品的信息架构和交互流程,让产品越来越懂用户。
19.5 结语
"智园通"智慧园区工作台虽然只是一个单页面的演示项目,但它所展现的设计思想和技术实践具有广泛的参考价值。从色彩体系到数据模型,从组件架构到动效设计,从布局策略到交互模式,每一个细节都体现了B端产品设计的方法论。
HarmonyOS ArkUI 作为新一代的声明式UI框架,为开发者提供了强大而灵活的工具。但工具只是手段,真正决定产品品质的,是对用户需求的深刻理解和对细节的极致追求。希望本文的分析能够为从事HarmonyOS开发和B端产品设计的朋友们提供一些启发和参考。
在数字化转型的浪潮中,智慧园区只是千千万万个行业场景中的一个。但正如一滴水可以折射太阳的光芒,一个优秀的园区应用也可以折射出整个行业数字化的方向——以用户为中心,以数据为驱动,以技术为手段,让物理世界变得更加智能、更加高效、更加美好。
科技蓝与智慧绿的B端叙事——HarmonyOS ArkUI 智慧园区六场景工作台组件化设计实践
一、技术前言
在企业数字化转型的浪潮中,智慧园区作为物理空间与数字世界深度融合的典型场景,正经历着从传统物业管理向智能化运营管理的深刻变革。智慧园区管理平台需要整合考勤、访客、会议、报修、公告、员工服务等多维度业务能力,为园区内的企业员工、管理人员、访客等不同角色提供一站式数字化服务入口。
HarmonyOS 作为面向全场景的分布式操作系统,其声明式UI开发框架 ArkUI 为构建跨设备、高性能、体验一致的智慧园区应用提供了强有力的技术支撑。ArkUI 采用声明式UI范式,开发者只需描述UI的状态和结构,框架便会自动处理UI的渲染与更新。其核心特性包括:
声明式UI开发范式:ArkUI 摒弃了传统命令式UI中手动操作DOM节点的繁琐模式,采用类似 SwiftUI、Flutter 的声明式语法。开发者通过 @Component 装饰器标记组件结构体,在 build() 方法中描述UI结构,状态变更通过 @State、@Observed 等装饰器驱动视图自动刷新。这种"状态驱动视图"的编程模型大幅降低了UI开发的心智负担,提升了代码的可读性和可维护性。
组件化与组合机制:ArkUI 提供了 @Builder 装饰器,支持将UI片段抽离为可复用的构建函数,实现UI逻辑的模块化组织。配合 @CustomDialog、@BuilderParam 等高级特性,开发者可以构建层次清晰、职责明确的组件树。在B端复杂场景下,这种组合式架构能够有效支撑多业务模块的并行开发与统一管理。
状态管理体系:ArkUI 的状态管理方案覆盖了从组件内局部状态到跨组件共享状态的完整链路。@State 管理组件内部状态,@Observed 配合 @ObjectLink 实现对象级别的细粒度监听,@Provide/@Consume 支持跨层级的状态透传,AppStorage/LocalStorage 提供全局和页面级的状态存储。这套多层次的状态管理方案为复杂业务场景的数据流转提供了灵活而可靠的保障。
性能优化能力:ArkUI 在框架层面内置了多种性能优化机制。ForEach 的键值生成策略确保列表的高效diff更新,@Memo 支持计算结果缓存,LazyForEach 实现长列表的惰性加载。结合方舟编译器的AOT编译优化,ArkUI 应用能够在各类设备上保持流畅的交互体验。
本文将以"智园通"智慧园区管理平台为载体,深入剖析一个包含六大业务场景的B端工作台组件的完整实现。该应用以科技蓝(#2563EB)与智慧绿(#10B981)为主题色彩,通过浅色背景营造专业、清爽的视觉氛围。六个功能Tab分别采用宫格菜单、状态列表、卡片网格、时间轴、信息流、中心大卡等截然不同的布局方式,充分展现 ArkUI 框架在复杂B端场景下的组件化设计能力与UI表达潜力。
接下来,我们将从整体架构出发,逐层深入色彩体系、数据模型、组件结构、各Tab实现细节、图表系统、底部导航、弹窗机制等核心模块,力求呈现一幅完整而细致的技术全景图。
二、整体架构流程图
在深入代码细节之前,我们先从宏观视角来审视当前页面的整体架构设计。该智慧园区工作台采用了经典的分层架构模式,从底层的数据定义到顶层的UI渲染,形成了一条清晰的单向依赖链路。
架构解读
从架构图中我们可以清晰地看到六层结构的职责划分与协作关系:
第一层:数据定义层——这是整个应用的基础设施,包含颜色系统、Tab元数据和辅助函数三大模块。颜色系统定义了全局统一的色彩规范,确保所有UI组件在视觉上保持一致性。Tab元数据描述了底部导航栏的配置信息,为底部Tab栏提供数据支撑。辅助函数则封装了状态与颜色的映射逻辑,避免在UI代码中散落大量条件判断。
第二层:数据模型层——六个@Observed装饰的类构成了应用的核心业务数据模型。每个模型对应一个业务场景,封装了该场景下的所有数据字段。这些模型通过装饰器获得了响应式能力,当数据发生变化时,绑定的UI会自动刷新。
第三层:状态管理层——主组件内部通过@State装饰器管理着多种类型的状态变量,包括当前Tab索引、呼吸动画开关、弹窗显示状态以及各业务模块的数据列表。这些状态变量是连接数据模型与UI渲染的桥梁。
第四层:组件渲染层——这是用户直接感知的界面层,由九个核心@Builder函数组成。每个Builder函数负责一个独立的UI模块,通过组合基础组件(Column、Row、Text、Image等)构建出丰富的界面效果。六个Tab页面采用了完全不同的布局策略,展现了多样化的UI表达能力。
第五层:弹窗系统——应用设计了通用遮罩层和三种功能弹窗(新增、编辑、删除),形成了一套完整的模态交互体系。弹窗系统通过状态变量控制显示与隐藏,遵循统一的视觉规范。
第六层:主组件——Page804作为顶层组件,统筹协调所有子模块。它在build()方法中按照"头部-内容区-底部导航"的三段式结构组织页面,通过条件渲染实现Tab切换,将各功能模块有机地串联在一起。
这种分层架构的设计思想体现了关注点分离的原则:数据定义与UI渲染分离,业务模型与状态管理分离,通用组件与业务组件分离。每一层都有明确的职责边界,层与层之间通过清晰的接口进行交互,使得代码结构清晰、易于维护、便于扩展。
三、色彩体系设计
色彩是UI设计的灵魂,一套科学合理的色彩体系不仅能塑造产品的视觉辨识度,更能通过色彩语义引导用户的视觉焦点与操作行为。在B端产品中,色彩的功能性远大于其装饰性,需要兼顾专业感、可读性与交互反馈。
当前页面构建了一套完整的色彩调色板系统,通过ColorPalette接口定义色彩规范,再以COLORS常量对象提供全局统一的色值引用。
interface ColorPalette {
bg: string; card: string; chip: string;
title: string; sub: string; text3: string;
blue: string; blueD: string;
green: string; greenD: string;
orange: string; red: string;
line: string; tabOn: string; mask: string;
}
这段接口定义展示了色彩体系的完整构成。我们可以将这十六个颜色变量划分为四大类:
第一类:背景与容器色——包含bg(页面背景)、card(卡片背景)、chip(标签/胶囊背景)三个色值。页面背景采用浅灰蓝色#F0F4F8,营造清爽、专业的视觉基调。卡片背景使用纯白色#FFFFFF,在浅灰背景上形成清晰的层级对比。标签背景使用淡蓝色#E0ECFF,这是科技蓝的浅色版本,既保持了品牌色的关联性,又不会抢夺内容的视觉焦点。
第二类:文字层级色——包含title(标题文字)、sub(次要文字)、text3(辅助文字)三个色值,构成了三级文字灰度体系。标题色#1A2332是深灰蓝色,接近黑色但略带蓝调,在保持高对比度的同时避免了纯黑的生硬感。次要文字色#6B7A8F为中灰蓝色,用于描述性文本和辅助信息。辅助文字色#A0AEC0为浅灰蓝色,用于时间戳、标签等弱化信息。三级文字色的明度梯度合理,确保了信息层级的清晰表达。
第三类:品牌功能色——包含blue/blueD(科技蓝主色与深色)、green/greenD(智慧绿主色与深色)、orange(警示橙)、red(错误红)六组色值。科技蓝#2563EB是主品牌色,用于主要按钮、选中态、重要数据的强调。智慧绿#10B981是辅助品牌色,代表绿色、环保、高效的园区理念,用于正向状态、成功提示。橙色和红色作为语义色,分别对应警告/待处理状态和错误/危险状态。每种主色都配有深色版本(blueD、greenD),用于渐变效果、悬浮态或需要更深色值的场景。
第四类:结构与遮罩色——包含line(分割线)、tabOn(Tab选中色)、mask(遮罩层)三个色值。分割线色#E2E8F0是极浅的灰蓝色,在白色卡片上若隐若现,既起到了区域分隔的作用,又不会破坏界面的整体性。Tab选中色与科技蓝保持一致,强化品牌识别。遮罩层使用半透明黑色rgba(0,0,0,0.5),为弹窗提供合适的背景暗化效果。
const COLORS: ColorPalette = {
bg: '#F0F4F8', card: '#FFFFFF', chip: '#E0ECFF',
title: '#1A2332', sub: '#6B7A8F', text3: '#A0AEC0',
blue: '#2563EB', blueD: '#1D4ED8',
green: '#10B981', greenD: '#059669',
orange: '#F59E0B', red: '#EF4444',
line: '#E2E8F0', tabOn: '#2563EB',
mask: 'rgba(0,0,0,0.5)',
};
这套色彩体系的设计体现了以下几个特点:
蓝绿双主色策略:不同于传统B端产品单一主色的做法,当前页面采用科技蓝+智慧绿的双主色策略。蓝色传达科技感、专业感、信任感,绿色传达智慧、环保、高效的理念。两种颜色在不同场景中交替使用,蓝色多用于操作按钮、数据强调,绿色多用于正向状态、成功反馈,形成了丰富而有序的色彩节奏。
统一色相家族:整套色彩体系在色相上保持高度统一。无论是背景色、文字色还是分割线色,都带有微妙的蓝色倾向,形成了和谐的蓝灰色调家族。这种统一的色相管理确保了界面整体色调的一致性,避免了多种灰阶混用可能造成的视觉杂乱。
语义化色彩命名:色彩变量采用语义化命名方式,而非直接使用颜色名称。例如使用title而非darkGray,使用chip而非lightBlue。这种命名方式的优势在于,当设计风格调整时,只需修改色值而无需改变引用名称,大大降低了维护成本。同时,语义化命名也使得代码更具可读性,开发者可以直观理解每个颜色变量的用途。
色彩与功能深度绑定:色彩在该应用中不仅仅是视觉装饰,更是信息传达的重要载体。不同的状态(待确认、已预约、已完成、已取消等)通过不同的颜色进行标识,用户可以通过颜色快速识别信息类型。这种色彩语义化的设计大幅提升了信息的获取效率,在B端数据密集型场景中尤为重要。
四、Tab元数据与辅助数据
在ArkUI开发中,将UI配置数据与渲染逻辑分离是一种重要的工程实践。Tab元数据和各类辅助数据数组的设计,体现了数据驱动视图的开发理念。
4.1 Tab元数据定义
Tab导航是移动应用最常见的导航模式之一。当前页面定义了TabMeta接口来规范底部Tab的数据结构,并通过TAB_LIST数组统一管理六个Tab的配置信息。
interface TabMeta { name: string; icon: Resource; }
const TAB_LIST: TabMeta[] = [
{ name: '工作台', icon: $r('app.media.ic_public_0') },
{ name: '访客', icon: $r('app.media.ic_public_1') },
{ name: '会议室', icon: $r('app.media.ic_public_2') },
{ name: '报修', icon: $r('app.media.ic_public_3') },
{ name: '公告', icon: $r('app.media.ic_public_4') },
{ name: '我的', icon: $r('app.media.ic_public_5') },
];
TabMeta接口定义了两个核心属性:name表示Tab的显示名称,icon表示Tab的图标资源。图标使用$r()语法引用应用资源文件中的图片,这是HarmonyOS中访问资源的标准方式。通过app.media.ic_public_0到ic_public_5的命名约定,可以看出这是一组统一设计的公共图标资源。
六个Tab分别对应智慧园区的六大核心功能模块:工作台、访客、会议室、报修、公告、我的。这种排列顺序也暗合了用户的使用频率——工作台作为首页入口放在最左侧,个人中心放在最右侧,中间按业务关联性排列。
将Tab配置数据化为数组的好处是显而易见的:当需要调整Tab顺序、新增或删除Tab时,只需修改TAB_LIST数组即可,无需改动底部Tab栏的渲染逻辑。这种数据与视图分离的设计模式,是组件化开发的重要基石。
4.2 图表辅助数据
能耗统计图表是当前页面的一个重要数据可视化模块,它需要三组辅助数据来支撑柱状图的渲染。
const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_NAME: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
const ENERGY_VAL: number[] = [4200, 5100, 6300, 7800, 8500, 7200];
const MAX_VAL: number = 8500;
这四组数据各司其职:MONTH_IDX提供ForEach循环的索引数组,用于驱动六根柱状图的渲染;MONTH_NAME存储每个柱子下方的月份标签文本;ENERGY_VAL存储对应的能耗数值(单位为kWh);MAX_VAL记录能耗的峰值,用于计算每根柱子的相对高度。
从数据中我们可以读出一条完整的能耗趋势线:3月至7月能耗逐月攀升,从4200kWh增长至8500kWh的峰值,8月回落到7200kWh。这大致符合园区能耗的季节性规律——夏季气温升高导致空调用电增加,7月达到顶峰后略有回落。
将图表数据抽离为独立常量的设计方式,使得图表组件更加通用。如果后续需要切换数据维度(比如从月能耗切换到日能耗),只需替换数据源数组即可,图表的渲染逻辑无需改动。
4.3 工作台行索引数据
工作台页面采用四行三列的宫格布局,共十二个功能入口。为了便于ForEach循环渲染,代码中将每行的三个元素索引分别抽离为独立数组。
const WORKBENCH_ROW0: number[] = [0, 1, 2];
const WORKBENCH_ROW1: number[] = [3, 4, 5];
const WORKBENCH_ROW2: number[] = [6, 7, 8];
const WORKBENCH_ROW3: number[] = [9, 10, 11];
这四个数组分别对应四行功能菜单,每个数组元素是WORKBENCH_LIST中的索引值。例如第一行显示索引0、1、2对应的三个功能(考勤打卡、请假申请、报销审批),第二行显示索引3、4、5对应的功能,依此类推。
这种索引数组的设计虽然略显繁琐,但在ArkUI的ForEach机制下有其合理性。每个Row通过ForEach遍历对应行的索引数组,再通过索引从数据源中取出具体条目进行渲染。每行都有独立的键值生成器('r0_' + idx等),确保了列表diff的准确性。
当然,如果追求更简洁的写法,也可以通过二维数组或动态计算索引的方式来实现。但当前这种显式声明的方式胜在直观清晰,便于开发者快速定位和调整每行的内容。
五、工具函数
在UI开发中,经常会遇到"根据状态返回对应颜色"这类逻辑。如果将这些条件判断散落在UI代码的各个角落,不仅会造成代码冗余,还会给后续维护带来困难。将这类逻辑抽离为独立的工具函数,是提升代码质量的有效手段。
当前页面定义了三个颜色映射函数,分别对应访客、报修、会议室三个模块的状态色彩体系。
5.1 访客状态颜色映射
function visitorColor(s: string): string {
if (s === '待确认') return COLORS.orange;
if (s === '已预约') return COLORS.green;
if (s === '已离开') return COLORS.text3;
if (s === '已取消') return COLORS.red;
return COLORS.text3;
}
visitorColor函数接收访客状态字符串作为参数,返回对应的颜色值。四种状态的色彩语义如下:
- 待确认——返回橙色。表示需要用户关注和处理的待办事项,橙色具有较强的视觉警示作用,提醒用户及时确认访客申请。
- 已预约——返回绿色。表示访客预约已确认,流程正常推进,绿色传达安全、正常的信号。
- 已离开——返回辅助灰色。表示访客流程已完成且属于历史记录,使用弱化的灰色避免干扰当前信息。
- 已取消——返回红色。表示预约被取消,属于异常或负面状态,红色直观传达警告意味。
函数末尾设置了默认返回值COLORS.text3,确保在遇到未知状态时不会导致渲染异常,体现了防御式编程的思想。
5.2 报修状态颜色映射
function repairColor(s: string): string {
if (s === '待受理') return COLORS.orange;
if (s === '处理中') return COLORS.blue;
if (s === '已完成') return COLORS.green;
if (s === '已驳回') return COLORS.red;
return COLORS.text3;
}
repairColor函数对应报修工单的四种状态,色彩语义与访客状态略有不同:
- 待受理——橙色。表示报修申请尚未被受理,需要相关人员及时处理。
- 处理中——蓝色。表示工单正在处理过程中,蓝色代表专业的技术支持正在进行。
- 已完成——绿色。表示报修已圆满解决,绿色传达完成和满意的含义。
- 已驳回——红色。表示报修申请被驳回,红色表示需要用户重新提交或关注。
值得注意的是,"处理中"状态使用了科技蓝而非其他颜色,这与访客模块的设计形成了差异。在报修场景中,处理中是一个积极的进行时状态,使用品牌主色蓝色既符合B端产品的专业调性,又能与待受理(橙色)、已完成(绿色)形成清晰的区分。
5.3 会议室状态颜色映射
function meetingColor(s: string): string {
if (s === '空闲') return COLORS.green;
if (s === '使用中') return COLORS.blue;
if (s === '已预约') return COLORS.orange;
if (s === '维护中') return COLORS.red;
return COLORS.text3;
}
meetingColor函数对应会议室的四种状态,色彩分配同样遵循语义化原则:
- 空闲——绿色。表示会议室当前可用,绿色传达"畅通、可使用"的信号。
- 使用中——蓝色。表示会议室正在被使用,蓝色代表正式的会议进行中。
- 已预约——橙色。表示会议室已被预约但尚未开始使用,橙色起到提醒和预告的作用。
- 维护中——红色。表示会议室因设备维护等原因不可用,红色警示用户不可预约。
工具函数的设计价值
这三个工具函数虽然逻辑简单,但在架构层面具有重要价值:
统一色彩语义:将状态与颜色的映射关系集中管理,确保同一种状态在不同位置显示的颜色完全一致,避免了因手动编写条件判断可能导致的色彩不一致问题。
简化UI代码:UI渲染代码只需调用函数即可获得对应颜色,无需编写冗长的if-else链,使得UI代码更加简洁清晰,关注点更加聚焦于布局本身。
便于维护扩展:当需要调整某个状态的颜色或新增状态类型时,只需修改对应函数即可,无需在整个代码库中搜索替换,大幅降低了维护成本。
函数式设计思想:这三个函数都是纯函数——相同的输入始终产生相同的输出,没有副作用。纯函数具有良好的可测试性和可预测性,是函数式编程的重要基石。
六、数据模型层
数据模型是应用的骨架,它定义了业务数据的结构和行为。在ArkUI框架中,通过@Observed装饰器修饰的类可以获得响应式能力——当对象的属性发生变化时,所有依赖该属性的UI会自动更新。
当前页面定义了六个@Observed类,分别对应六个业务场景的数据模型。下面我们逐一进行分析。
6.1 WorkbenchItem——工作台条目模型
@Observed
export class WorkbenchItem {
icon: string; name: string; badge: number;
constructor(icon: string, name: string, badge: number) {
this.icon = icon; this.name = name; this.badge = badge;
}
}
WorkbenchItem是工作台宫格菜单的条目模型,包含三个属性:
- icon:功能图标的emoji字符表示。使用emoji作为图标是一种轻量级的图标方案,无需额外的图片资源,在开发原型或演示场景中非常便捷。
- name:功能名称,显示在图标下方。
- badge:角标数字,表示待处理事项的数量。当badge大于0时,图标右上角会显示红色角标提醒。
WORKBENCH_LIST数组预置了十二个工作台功能,涵盖考勤打卡、请假申请、报销审批、物资领用、用车申请、餐饮预订、工位管理、设备报修、访客登记、会议预约、数据报表、系统设置等园区办公的核心场景。其中部分功能带有角标数字,如报销审批(3条)、设备报修(5条)、访客登记(4条)等,直观提示用户有待处理的事项。
6.2 VisitorItem——访客记录模型
@Observed
export class VisitorItem {
name: string; reason: string; status: string; date: string; phone: string;
constructor(name: string, reason: string, status: string, date: string, phone: string) {
this.name = name; this.reason = reason; this.status = status;
this.date = date; this.phone = phone;
}
}
VisitorItem是访客管理的数据模型,描述了一次访客记录的完整信息,包含五个属性:
- name:访客姓名,用于识别访客身份。
- reason:来访事由,如商务洽谈、面试应聘、项目对接等,帮助接待人员了解来访目的。
- status:访客状态,取值包括待确认、已预约、已离开、已取消四种,配合
visitorColor函数实现状态色彩标识。 - date:来访日期时间,格式为
YYYY-MM-DD HH:mm,精确到分钟级别。 - phone:联系电话,中间四位用星号脱敏处理(如
138****1234),兼顾信息展示与隐私保护。
VISITOR_LIST预置了六条访客记录,涵盖了全部四种状态,数据设计非常完整。从记录中可以看到,当天有三位访客(待确认1位、已预约2位),历史记录有三位(已离开2位、已取消1位),模拟了真实的访客管理场景。
6.3 MeetingItem——会议室信息模型
@Observed
export class MeetingItem {
name: string; capacity: number; equipment: string; status: string; current: string; floor: string;
constructor(name: string, capacity: number, equipment: string, status: string, current: string, floor: string) {
this.name = name; this.capacity = capacity; this.equipment = equipment;
this.status = status; this.current = current; this.floor = floor;
}
}
MeetingItem是会议室预订的数据模型,包含六个属性,信息维度比前两个模型更加丰富:
- name:会议室名称,如"星河会议厅"、"云海会议室"等,赋予每个会议室独特的标识。
- capacity:容纳人数,数字类型,用户可根据参会人数选择合适的会议室。
- equipment:配备设备,用竖线分隔的字符串表示(如"投影|白板|视频会议"),快速展示会议室的硬件配置。
- status:使用状态,包括空闲、使用中、已预约、维护中四种,配合
meetingColor函数显示状态色标。 - current:当前会议内容,当状态为"使用中"或"已预约"时显示会议名称,状态为"维护中"时显示维护原因,空闲时显示占位符。
- floor:所在楼层,如"3F"、"5F"等,方便用户定位会议室位置。
MEETING_LIST预置了六个会议室,规模从4人的洽谈室到50人的培训教室不等,覆盖了不同规模的会议需求。会议室的命名也颇具匠心——星河会议厅、云海会议室、智慧会议室、创客空间等名称赋予了空间以想象力和品牌感。
6.4 RepairItem——报修工单模型
@Observed
export class RepairItem {
title: string; desc: string; status: string; time: string; reporter: string;
constructor(title: string, desc: string, status: string, time: string, reporter: string) {
this.title = title; this.desc = desc; this.status = status;
this.time = time; this.reporter = reporter;
}
}
RepairItem是设备报修的数据模型,包含五个属性:
- title:报修标题,简要描述故障类型,如"空调故障"、"打印机卡纸"等。
- desc:详细描述,说明故障的具体情况和位置信息,为维修人员提供参考。
- status:工单状态,包括待受理、处理中、已完成、已驳回四种,配合
repairColor函数显示状态颜色。 - time:报修时间,格式为
MM-DD HH:mm,记录工单发起的时间点。 - reporter:报修人信息,格式为"部门·姓名",便于维修人员联系对接。
REPAIR_LIST预置了六条报修记录,涵盖了空调、打印机、灯光、门禁、网络、电梯等常见的园区设备故障类型。每条记录都有详细的故障描述和报修人信息,模拟了真实的报修工单场景。其中两条处于处理中状态,两条已完成,一条待受理,一条已驳回,状态分布均衡。
6.5 NoticeItem——公告通知模型
@Observed
export class NoticeItem {
title: string; dept: string; time: string; summary: string; unread: boolean;
constructor(title: string, dept: string, time: string, summary: string, unread: boolean) {
this.title = title; this.dept = dept; this.time = time;
this.summary = summary; this.unread = unread;
}
}
NoticeItem是园区公告的数据模型,包含五个属性:
- title:公告标题,简明扼要地传达公告主题。
- dept:发布部门,如安全管理部、能源管理部、人力资源部等,标明公告的来源和权威性。
- time:发布时间,精确到分钟,帮助用户判断公告的时效性。
- summary:内容摘要,公告正文的前若干字,让用户快速了解公告大意。摘要最多显示两行,超出部分用省略号截断。
- unread:未读标记,布尔类型。为true时显示红色圆点,提醒用户该公告尚未阅读。
NOTICE_LIST预置了六条公告,涵盖消防演练、能耗统计、食堂菜单、体检安排、停车场改造、新员工培训等园区常见的通知类型。其中三条标记为未读状态,与标题栏"未读3"的统计数字相对应,数据逻辑自洽。
6.6 EmployeeItem——员工信息模型
@Observed
export class EmployeeItem {
name: string; dept: string; empNo: string; points: number; level: string; building: string;
constructor(name: string, dept: string, empNo: string, points: number, level: string, building: string) {
this.name = name; this.dept = dept; this.empNo = empNo;
this.points = points; this.level = level; this.building = building;
}
}
EmployeeItem是员工个人信息的数据模型,用于"我的"页面展示,包含六个属性:
- name:员工姓名。
- dept:所属部门。
- empNo:员工工号,格式为
EMP加七位数字。 - points:积分值,数字类型,用于员工激励体系的积分展示。
- level:员工等级,如金牌员工、银牌员工、钻石员工等,配合积分体系形成员工成长路径。
- building:办公位置,标明员工所在的楼栋和楼层。
EMPLOYEE_LIST预置了三位员工信息,分别属于技术研发部、产品运营部和市场营销部,积分和等级各不相同,模拟了不同职级和部门的员工画像。第一位员工"石天雄"作为当前登录用户显示在个人中心页面。
数据模型层的设计思考
六个数据模型虽然结构各异,但都遵循了一致的设计规范:
@Observed装饰器:每个模型类都使用@Observed装饰,确保对象属性变化时UI能够自动响应。这是ArkUI状态管理机制的基础,也是声明式UI的核心特性。
构造函数初始化:每个类都提供了完整的构造函数,所有属性通过构造函数参数赋值,确保对象创建时数据完整。
预置模拟数据:每个模型都配套了对应的模拟数据列表,数据内容贴合真实业务场景,便于开发调试和UI演示。这种"模型+模拟数据"的配套模式,在前端开发中非常实用。
语义化字段命名:属性命名清晰直观,见名知意,无需额外注释就能理解每个字段的含义。良好的命名是代码自文档化的关键。
七、组件主体结构
组件主体是整个页面的核心调度中心,它统筹管理页面的状态变量、生命周期、整体布局和各功能模块的组织方式。在ArkUI中,一个页面由@Entry装饰的@Component结构体表示,内部通过build()方法描述UI结构。
7.1 组件声明与状态变量
@Entry
@Component
struct Page804 {
@State currentTab: number = 0;
@State addModal: boolean = false;
@State editModal: boolean = false;
@State delModal: boolean = false;
@State editIdx: number = -1;
@State delIdx: number = -1;
@State breath: boolean = false;
timer: number = -1;
@State workbenchList: WorkbenchItem[] = WORKBENCH_LIST;
@State visitorList: VisitorItem[] = VISITOR_LIST;
@State meetingList: MeetingItem[] = MEETING_LIST;
@State repairList: RepairItem[] = REPAIR_LIST;
@State noticeList: NoticeItem[] = NOTICE_LIST;
@State employeeList: EmployeeItem[] = EMPLOYEE_LIST;
主组件Page804使用@Entry和@Component双重装饰器标记。@Component表示这是一个可复用的UI组件,@Entry表示这是页面的入口组件,应用启动时会首先渲染该组件。
组件内部定义了丰富的状态变量,我们可以将其分为四类:
导航状态:currentTab记录当前选中的Tab索引,初始值为0(即工作台页面)。当用户点击底部Tab栏时,该值更新,触发内容区域的条件渲染切换。
弹窗状态:addModal、editModal、delModal三个布尔变量分别控制新增、编辑、删除三种弹窗的显示与隐藏。editIdx和delIdx记录当前操作的数据项索引,用于在弹窗中展示对应的数据。初始值-1表示未选中任何条目。
动画状态:breath是一个布尔开关,配合timer定时器实现呼吸灯动画效果。breath每秒翻转一次,驱动头部快捷入口、头像背景、柱状图高度等元素产生渐变动画。
业务数据:六个@State装饰的数组变量分别对应六个业务模块的数据源。它们从预置的常量列表初始化,由于使用了@State装饰,当数组内容发生变化时,对应的UI会自动刷新。
这里有一个值得注意的设计细节:timer变量没有使用@State装饰。这是因为timer只是一个定时器句柄,用于在组件销毁时清除定时器,它本身不参与UI渲染。不需要响应式追踪的变量就不要加@State,这是ArkUI性能优化的基本原则之一。
7.2 生命周期钩子
aboutToAppear() {
this.timer = setInterval(() => { this.breath = !this.breath; }, 1000);
}
aboutToDisappear() { clearInterval(this.timer); }
ArkUI提供了丰富的生命周期钩子函数,当前页面使用了其中两个:
aboutToAppear:在组件即将出现时调用,是执行初始化操作的最佳时机。这里启动了一个间隔为1000毫秒的定时器,每秒翻转一次breath变量的值,从而驱动所有绑定了breath状态的UI元素产生呼吸动画效果。
aboutToDisappear:在组件即将消失时调用,用于执行清理操作。这里清除定时器,防止内存泄漏。这是一个非常重要的编程习惯——凡是在aboutToAppear中创建的资源,都应该在aboutToDisappear中对应释放。
呼吸动画的实现思路非常巧妙:通过一个简单的布尔变量翻转,配合三目运算符(this.breath ? A : B),就可以让UI元素在两种状态之间平滑过渡。ArkUI的属性动画机制会自动在状态切换时添加过渡效果,无需开发者手动编写动画代码。
7.3 主体布局结构
build() {
Column() {
this.headerOverview()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) { this.tabWorkbench() }
else if (this.currentTab === 1) { this.tabVisitor() }
else if (this.currentTab === 2) { this.tabMeeting() }
else if (this.currentTab === 3) { this.tabRepair() }
else if (this.currentTab === 4) { this.tabNotice() }
else if (this.currentTab === 5) { this.tabMine() }
this.chartCard()
}
}
.scrollBar(BarState.Off)
.layoutWeight(1)
this.tabBar()
if (this.addModal) { this.panelAdd(() => { this.addModal = false }) }
if (this.editModal) { this.panelEdit(() => { this.editModal = false }) }
if (this.delModal) { this.panelDel(() => { this.delModal = false }) }
}
.backgroundColor(COLORS.bg)
.height('100%')
}
build()方法是组件的核心,它描述了页面的完整UI结构。整体采用垂直布局(Column),从上到下依次为:
头部区域:调用headerOverview()构建函数渲染园区概览头部,包含应用名称、通知图标、园区概览数据卡和快捷功能入口。
分割线:一条1像素的细线,将头部与内容区域视觉分离。使用Divider组件,颜色取自色彩系统中的line变量。
滚动内容区:使用Scroll组件包裹中间内容,确保内容超出屏幕时可以滚动浏览。.scrollBar(BarState.Off)隐藏滚动条,使界面更加简洁。.layoutWeight(1)让滚动区域自动填充剩余空间,这是实现"头部固定-内容滚动-底部固定"经典布局的关键。
滚动内容区内部又包含两部分:通过if-else条件渲染的六个Tab页面(根据currentTab的值显示对应的页面),以及始终显示在底部的能耗统计图表卡片chartCard()。
底部Tab栏:调用tabBar()构建函数渲染底部导航栏,提供六个Tab的切换入口。
弹窗层:三个条件渲染的弹窗组件,分别对应新增、编辑、删除三种操作场景。弹窗使用绝对定位覆盖在页面上方,通过状态变量控制显示与隐藏。每个弹窗都接收一个关闭回调函数,点击确认或取消后关闭弹窗。
整体页面的背景色设置为COLORS.bg(浅灰蓝色),高度占满全屏。这种三段式(头-内容-底)的布局结构是移动应用最经典的布局模式之一,结构清晰、交互直观、用户学习成本低。
八、头部区域详解
头部区域是用户进入应用后首先看到的部分,承担着品牌展示、信息概览、快捷操作等多重功能。当前页面的头部区域设计精巧,内容丰富,由headerOverview()构建函数实现。
8.1 顶部导航栏
Row() {
Column() {
Text('🏢 智园通').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text('A栋 · 12层 · 技术研发部').fontSize(11).fontColor(COLORS.sub).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start)
Blank()
Stack() {
Text('🔔').fontSize(20)
Column() {
Text('5').fontSize(9).fontColor(COLORS.card)
}
.width(14).height(14).borderRadius(7).backgroundColor(COLORS.red)
.alignContent(Alignment.Center)
.position({ x: 14, y: 0 })
}
.width(36).height(36).alignContent(Alignment.Center)
}
.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 8 })
顶部导航栏采用左右布局结构:
左侧是应用品牌信息。主标题"🏢 智园通"使用20号加粗字体,深灰蓝色,醒目而专业。副标题显示用户的位置和部门信息"A栋 · 12层 · 技术研发部",使用11号小号字体和次要文字色,提供上下文信息但不抢夺主标题的视觉焦点。
右侧是通知入口。使用铃铛emoji作为通知图标,右上角叠加一个红色圆形角标,显示未读消息数量"5"。角标的实现使用了Stack堆叠布局配合.position()绝对定位的方式——红色小圆点通过position({ x: 14, y: 0 })定位到图标的右上角,形成了典型的角标效果。角标尺寸为14x14像素,圆角7像素(即圆形),内部数字使用9号白色字体。
整个导航栏宽度占满屏幕,左右各16像素内边距,顶部12像素、底部8像素的垂直内边距,营造出舒适的视觉呼吸感。
8.2 园区概览数据卡
Row() {
Column() {
Row() {
Text('📊').fontSize(18)
Text('园区概览').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.title).margin({ left: 4 })
Blank()
Text('今日').fontSize(11).fontColor(COLORS.green)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(COLORS.chip).borderRadius(8)
}.width('100%')
Row() {
Column() {
Text('1,286').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.blue)
Text('在岗人数').fontSize(10).fontColor(COLORS.sub).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
// ... 其余三项类似
}.width('100%').margin({ top: 10 })
}
.width('100%').padding(14).borderRadius(14)
.backgroundColor(COLORS.card)
}
.width('100%').padding({ left: 16, right: 16 })
园区概览数据卡是头部区域的核心信息展示模块,采用白色卡片式设计,圆角14像素,内边距14像素。
卡片标题栏左侧显示图表图标和"园区概览"标题,右侧有一个"今日"标签,使用绿色文字配淡蓝色背景,表明数据是当日的实时统计。
卡片主体展示四项关键指标,每项占四分之一等宽布局:
- 在岗人数:1,286人,科技蓝显示。这是园区运营的基础数据,反映当前园区内的人员规模。
- 待办事项:6件,智慧绿显示。提醒用户有待处理的工作事项。
- 待审申请:3件,橙色显示。提示有审批等待处理,橙色起到警示作用。
- 设备完好率:98%,智慧绿深色显示。反映园区设备的整体运行状况。
四个数据指标分别使用不同的颜色——蓝色、绿色、橙色、绿色深色,既保持了色彩的丰富性,又通过颜色语义传达了数据的性质。数值使用22号加粗大字,标签使用10号小字,形成鲜明的大小对比,让用户一眼就能抓住关键数字。
这种数据概览卡的设计在B端产品中非常常见,它将最重要的运营指标浓缩在一个卡片中,用户打开应用就能快速了解园区整体运行状态。
8.3 快捷功能入口
Row() {
Column() {
Stack() { Text('✅').fontSize(20) }
.width(40).height(40).borderRadius(20)
.backgroundColor(this.breath ? COLORS.blueD : COLORS.chip)
.alignContent(Alignment.Center)
Text('考勤打卡').fontSize(10).fontColor(COLORS.sub).margin({ top: 4 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
// ... 其余四个类似
}
.width('100%').padding({ left: 12, right: 12, top: 12, bottom: 10 })
快捷功能入口提供了五个高频操作的一键直达入口:考勤打卡、快速审批、会议预约、访客登记、一键报修。
每个入口采用"图标+文字"的上下布局,图标放置在40x40像素的圆形背景中。其中第一个"考勤打卡"入口的背景色与呼吸动画绑定——this.breath ? COLORS.blueD : COLORS.chip,当breath为true时显示深蓝色,为false时显示淡蓝色。配合ArkUI的属性动画机制,背景色会在两种颜色之间平滑过渡,形成呼吸灯般的脉动效果。
这种呼吸动画设计在交互上具有重要意义:它将用户最需要关注的操作(考勤打卡)通过动态效果突出显示,引导用户的视觉注意力。同时,柔和的呼吸节奏不会造成视觉干扰,体现了动效设计的"适度"原则。
五个入口平分屏幕宽度(通过.layoutWeight(1)实现),图标下方是10号灰色功能名称文字。整个快捷入口区域左右内边距12像素,顶部12像素、底部10像素,布局紧凑而不拥挤。
头部区域的设计价值
头部区域虽然只占页面的一小部分,但其信息密度和功能密度都很高。它同时承担了以下功能:
- 品牌展示:通过应用名称和Logo建立品牌认知。
- 位置感知:显示用户当前所在的楼栋和部门信息。
- 消息提醒:通知入口和未读数提醒用户关注新消息。
- 数据概览:四个关键指标让用户快速掌握园区运行状态。
- 快捷操作:五个高频功能入口,减少用户的操作路径。
这种"头部即服务"的设计理念,在B端移动应用中尤为重要。用户打开应用后,无需深入导航,就能在首屏获取最关键的信息并执行最频繁的操作,大幅提升了使用效率。
九、工作台Tab——宫格菜单详解
工作台是用户进入应用后的默认页面,也是功能导航的主入口。该页面采用经典的宫格菜单布局,将十二个常用功能以四行三列的网格形式整齐排列,用户可以一目了然地浏览所有功能入口。
9.1 页面标题区
@Builder tabWorkbench() {
Column() {
Row() {
Text('💼 工作台').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('全部应用 >').fontSize(12).fontColor(COLORS.blue)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('常用功能快捷入口,点击进入对应模块')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 12 })
页面标题区采用左右布局:左侧是"💼 工作台"标题,使用18号加粗字体,庄重醒目;右侧是"全部应用 >“入口,使用科技蓝色,暗示可以点击查看更多应用。标题下方还有一行辅助说明文字"常用功能快捷入口,点击进入对应模块”,使用11号浅灰色,为用户提供操作指引。
这种"主标题+更多入口+副标题说明"的三段式标题设计,在B端产品中非常典型。它既明确了当前页面的功能定位,又为深度操作提供了入口,同时用说明文字降低了新用户的认知门槛。
9.2 宫格菜单渲染机制
十二个功能入口分为四行排列,每行三个。代码中使用了四个独立的Row来渲染每一行,每个Row内部通过ForEach遍历对应的行索引数组。
Row() {
ForEach(WORKBENCH_ROW0, (idx: number) => {
Column() {
Stack() {
Text(this.workbenchList[idx].icon).fontSize(28)
if (this.workbenchList[idx].badge > 0) {
Column() {
Text(this.workbenchList[idx].badge > 99 ? '99+' : this.workbenchList[idx].badge.toString())
.fontSize(9).fontColor(COLORS.card)
}
.width(16).height(16).borderRadius(8).backgroundColor(COLORS.red)
.alignContent(Alignment.Center)
.position({ x: 26, y: 0 })
}
}
.width(52).height(52).borderRadius(14).backgroundColor(COLORS.chip)
.alignContent(Alignment.Center)
Text(this.workbenchList[idx].name).fontSize(11).fontColor(COLORS.title).margin({ top: 6 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 8, bottom: 8 })
.onClick(() => { this.editIdx = idx; this.editModal = true })
}, (idx: number) => 'r0_' + idx.toString())
}
.width('100%').padding({ left: 8, right: 8 })
每个宫格项的结构可以拆解为以下几层:
外层容器:使用Column垂直布局,.layoutWeight(1)确保三个项在一行中平分宽度。上下各8像素内边距,为点击区域留出足够空间。
图标区域:使用Stack堆叠布局,52x52像素的圆角矩形(圆角14像素)作为图标背景,背景色为淡蓝色COLORS.chip。emoji图标大小为28号,居中显示。
角标提示:当badge大于0时,在图标右上角显示红色圆形角标。角标尺寸为16x16像素,通过.position({ x: 26, y: 0 })定位到图标右上角。角标数字有一个巧妙的处理——当数字超过99时显示"99+",避免数字过大撑破角标样式。这是一个非常实用的用户体验细节。
功能名称:图标下方是功能名称文字,11号深灰蓝色,与图标保持6像素的间距。
点击交互:每个宫格项绑定了.onClick()事件,点击时设置editIdx为当前索引并打开编辑弹窗。这里用编辑弹窗来模拟功能入口的点击效果,在实际项目中可以替换为页面跳转或功能展开。
9.3 四行布局与键值生成
四行菜单的渲染逻辑基本相同,区别仅在于遍历的索引数组不同(WORKBENCH_ROW0到ROW3)以及ForEach的键值生成器不同。
键值生成器是ForEach的第三个参数,用于为每个列表项生成唯一标识。ArkUI框架通过这个键值来判断列表项的变化,从而高效地更新UI。代码中分别使用了'r0_' + idx、'r1_' + idx等前缀,确保不同行的列表项不会因为索引相同而产生键值冲突。
虽然四行代码看起来有重复,但这种写法也有其优势:每行都是独立的布局单元,可以单独调整样式和间距,灵活度更高。如果要实现更复杂的布局(如某些行有特殊样式),这种逐行控制的方式会更加方便。
9.4 添加应用按钮
Row() {
Button('+ 添加应用').fontSize(13).fontColor(COLORS.card)
.backgroundColor(COLORS.blue).borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
}
.width('100%').justifyContent(FlexAlign.Center).margin({ top: 8, bottom: 8 })
宫格菜单底部是一个"+ 添加应用"按钮,使用科技蓝背景配白色文字,圆角20像素(胶囊形状),高度36像素。按钮水平居中显示,点击后打开新增弹窗。
这个按钮的设计有两个作用:一是提供功能扩展的入口,用户可以自定义添加更多应用到工作台;二是在视觉上为宫格区域收一个尾,形成完整的区块感。
工作台页面作为应用的首页,其设计体现了"效率优先"的原则——所有功能一目了然,点击路径最短,信息密度适中。宫格菜单作为最经典的导航模式之一,在B端产品中经久不衰,正是因为它在信息展示效率和用户认知成本之间取得了最佳平衡。
十、访客Tab——状态列表与标签筛选
访客管理是智慧园区的重要场景之一,涉及来访预约、身份登记、状态追踪等多个环节。当前页面的访客Tab采用了"标签筛选+状态列表"的布局模式,用户可以通过顶部标签快速筛选不同状态的访客,下方以列表形式展示访客详情。
10.1 页面标题与统计
@Builder tabVisitor() {
Column() {
Row() {
Text('👥 访客管理').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('共6位').fontSize(11).fontColor(COLORS.green)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('访客预约与来访记录,实时同步状态')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 8 })
访客页面的标题区结构与工作台类似,但右上角显示的统计信息有所不同——"共6位"使用绿色文字,直观告知用户当前访客记录的总数量。副标题"访客预约与来访记录,实时同步状态"说明了该模块的核心功能。
10.2 状态标签筛选栏
Scroll() {
Row() {
Text('全部').fontSize(12).fontColor(COLORS.card)
.backgroundColor(COLORS.blue).borderRadius(14)
.padding({ left: 12, right: 12, top: 4, bottom: 4 }).margin({ right: 8 })
Text('待确认').fontSize(12).fontColor(COLORS.sub)
.backgroundColor(COLORS.chip).borderRadius(14)
.padding({ left: 12, right: 12, top: 4, bottom: 4 }).margin({ right: 8 })
// ... 其余标签类似
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.margin({ bottom: 12 })
状态标签筛选栏是访客页面的特色功能。它使用水平滚动的标签列表,提供"全部、待确认、已预约、已离开、已取消"五个筛选选项。
标签采用胶囊形状(圆角14像素),左右内边距12像素,上下4像素。当前选中的"全部"标签使用科技蓝背景配白色文字,未选中的标签使用淡蓝色背景配灰色文字。这种选中态与未选中态的强烈对比,让用户清晰知道当前的筛选条件。
标签栏外层包裹了Scroll组件,设置为水平滚动方向,隐藏滚动条。当标签数量较多超出屏幕宽度时,用户可以左右滑动查看更多标签。这是一种处理多分类筛选的经典方案。
需要说明的是,当前代码中的标签是静态展示的,点击交互尚未完全实现(点击后不会切换选中态)。在实际项目中,可以通过增加一个@State变量来记录当前选中的标签索引,然后根据索引动态设置标签的背景色和文字色。
10.3 访客列表卡片
ForEach(this.visitorList, (item: VisitorItem, idx: number) => {
Row() {
Stack() { Text('👤').fontSize(24) }
.width(44).height(44).borderRadius(22).backgroundColor(COLORS.chip)
.alignContent(Alignment.Center)
Column() {
Row() {
Text(item.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(item.status).fontSize(10).fontColor(COLORS.card)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8).backgroundColor(visitorColor(item.status))
.margin({ left: 8 })
}.alignItems(VerticalAlign.Center)
Text(item.reason).fontSize(12).fontColor(COLORS.sub).margin({ top: 4 })
Row() {
Text('🕐 ' + item.date).fontSize(10).fontColor(COLORS.text3)
Text(' 📱 ' + item.phone).fontSize(10).fontColor(COLORS.text3).margin({ left: 10 })
}.margin({ top: 4 })
}.margin({ left: 12 }).alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('>').fontSize(16).fontColor(COLORS.text3)
}
.width('92%').padding(12).borderRadius(12).backgroundColor(COLORS.card)
.margin({ bottom: 8 })
.onClick(() => { this.editIdx = idx; this.editModal = true })
}, (item: VisitorItem) => item.name + item.date)
访客列表是页面的主体内容,使用ForEach遍历visitorList数组渲染每条访客记录。每条记录是一个白色卡片,采用左右布局结构:
左侧头像:44x44像素的圆形头像区域,使用人物emoji作为占位头像,淡蓝色背景。圆形头像在社交类和人员管理类界面中非常常见,能够快速建立人物识别。
中间信息区:这是卡片的核心内容区域,包含三行信息:
- 第一行:访客姓名(15号加粗)+ 状态标签(10号白色文字,背景色由
visitorColor函数根据状态动态返回)。姓名与状态标签垂直居中对齐。 - 第二行:来访事由,12号灰色文字,简要说明访客来意。
- 第三行:来访时间和联系电话,10号浅灰色文字,分别用时钟和手机emoji作为前缀标识。
右侧箭头:一个">"符号,灰色,暗示该卡片可以点击进入详情页。这是移动界面中常见的"可点击"视觉暗示。
整张卡片宽度为屏幕宽度的92%,内边距12像素,圆角12像素,卡片之间有8像素的间距。这种"92%宽度+居中"的卡片设计在移动端非常流行,既保证了内容区域的宽敞,又通过两侧留白营造了良好的视觉节奏感。
ForEach的键值生成器使用了item.name + item.date的组合,确保每条记录的唯一性。因为可能存在同名访客,但姓名+日期的组合几乎不会重复。
10.4 新增访客按钮
Row() {
Button('+ 新增访客').fontSize(13).fontColor(COLORS.card)
.backgroundColor(COLORS.blue).borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
}
.width('100%').justifyContent(FlexAlign.Center).margin({ top: 4, bottom: 8 })
列表底部是"+ 新增访客"按钮,样式与工作台的添加按钮一致——科技蓝胶囊形按钮,白色文字,点击打开新增弹窗。
访客页面的整体设计体现了"状态驱动"的管理思想:通过色彩和标签将不同状态的访客清晰区分,用户可以一眼识别哪些需要处理、哪些已完成。这种色彩语义化的设计在B端管理系统中尤为重要,它将抽象的状态概念转化为直观的视觉信号,大幅提升了信息获取效率。
十一、会议室Tab——卡片网格布局
会议室预订是智慧园区办公场景中的高频需求。会议室Tab采用了两列卡片网格的布局形式,将所有会议室以卡片形式直观展示,用户可以快速浏览各会议室的状态并进行预约操作。
11.1 页面标题区
@Builder tabMeeting() {
Column() {
Row() {
Text('🏢 会议室预订').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('今日可用 >').fontSize(11).fontColor(COLORS.blue)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('实时查看会议室状态,快速预约使用')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 12 })
会议室页面的标题区延续了统一的设计风格。右上角"今日可用 >"的入口用蓝色文字提示用户可以快速筛选可用会议室,提升预约效率。
11.2 两列卡片网格结构
六个会议室分为三排展示,每排两个,形成两列网格布局。代码中使用三个独立的Row来渲染每一排,每排通过ForEach遍历两个列索引。
Row() {
ForEach([0, 1], (col: number) => {
let item = this.meetingList[col];
Column() {
// 卡片内容...
}
.width('48%').padding(12).borderRadius(12).backgroundColor(COLORS.card)
.alignItems(HorizontalAlign.Start)
.margin({ left: col === 0 ? 0 : '4%' })
.onClick(() => { this.editIdx = col; this.editModal = true })
}, (col: number) => 'm_row1_' + col.toString())
}
.width('92%').margin({ bottom: 10 })
两列网格的宽度计算很有技巧:每张卡片宽度为48%,两张卡片之间的间距为4%(通过左卡片的margin-left实现,第一列左边距为0,第二列左边距为4%)。48% + 4% + 48% = 100%,恰好填满容器宽度。这种百分比布局方式能够自适应不同屏幕尺寸,保证在各种设备上都能呈现规整的两列布局。
每张卡片使用白色背景,圆角12像素,内边距12像素,整体视觉风格与其他页面保持一致。
11.3 会议室卡片内容解析
每张会议室卡片包含丰富的信息,采用垂直布局排列:
Row() {
Text(item.floor).fontSize(10).fontColor(COLORS.sub)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.chip).borderRadius(6)
Blank()
Text(item.status).fontSize(10).fontColor(COLORS.card)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6).backgroundColor(meetingColor(item.status))
}.width('100%')
顶部标签行:左侧是楼层标签(如"3F"),灰色文字配淡蓝色背景,小尺寸圆角6像素。右侧是状态标签,白色文字,背景色由meetingColor函数根据状态动态返回。两个标签一左一右,将位置信息和状态信息同时呈现。
Text(item.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.margin({ top: 8 })
Text('容纳 ' + item.capacity + ' 人').fontSize(11).fontColor(COLORS.sub).margin({ top: 2 })
Text(item.equipment).fontSize(9).fontColor(COLORS.blue).margin({ top: 6 })
会议室基本信息:会议室名称使用14号加粗字体,是卡片的视觉焦点。下方是容纳人数信息,11号灰色文字。再下方是设备配置信息,使用9号科技蓝色文字,设备之间用竖线分隔。用品牌色显示设备信息,既突出了配置亮点,又不会过于张扬。
Divider().strokeWidth(1).color(COLORS.line).margin({ top: 8, bottom: 8 })
if (item.status === '使用中' || item.status === '已预约') {
Text('当前:' + item.current).fontSize(10).fontColor(COLORS.sub)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
} else {
Text(item.status === '维护中' ? '维护原因:' + item.current : '当前无会议').fontSize(10).fontColor(COLORS.text3)
}
分隔线与当前会议信息:一条细分割线将基本信息与当前状态分开。根据会议室状态的不同,显示不同的内容:使用中或已预约时显示当前会议名称,维护中时显示维护原因,空闲时显示"当前无会议"。会议名称设置了最大行数为1行,超出部分用省略号截断,防止长文本撑破布局。
Text('立即预约').fontSize(11).fontColor(COLORS.card)
.margin({ top: 8 }).padding({ top: 5, bottom: 5, left: 14, right: 14 })
.borderRadius(12).backgroundColor(item.status === '空闲' ? COLORS.green : COLORS.text3)
预约按钮:卡片底部是"立即预约"操作按钮。按钮的背景色根据会议室状态动态变化——空闲时显示绿色(可预约),其他状态显示灰色(不可预约)。这种颜色变化直观地传达了按钮的可用性,用户无需阅读文字就能判断是否可以点击预约。
11.4 三排网格的统一性与差异
三排会议室卡片的渲染逻辑基本一致,只是遍历的数据索引和键值前缀不同。第一排遍历索引0和1,第二排遍历2和3,第三排遍历4和5。键值前缀分别为m_row1_、m_row2_、m_row3_,确保每排卡片在ForEach中有唯一的键标识。
这种逐排渲染的方式虽然代码量稍大,但每排都是独立的布局单元,可以灵活控制排间距和排列方式。如果后续需要调整为"上排一个大卡片+下排两个小卡片"的混合布局,也能轻松实现。
11.5 预约会议室按钮
Row() {
Button('+ 预约会议室').fontSize(13).fontColor(COLORS.card)
.backgroundColor(COLORS.green).borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
}
.width('100%').justifyContent(FlexAlign.Center).margin({ top: 4, bottom: 8 })
页面底部的"+ 预约会议室"按钮使用了绿色背景,与其他页面的蓝色按钮形成区别。这种颜色的差异化设计与会议室模块的"智慧绿"主题相呼应,同时绿色也传达了"预约成功、畅通无阻"的正向语义。
会议室页面的两列卡片网格布局,在信息密度和可读性之间取得了良好的平衡。相比于单列列表,两列布局可以在一屏内展示更多的会议室信息;相比于三列或更多列,两列布局又能保证每张卡片有足够的空间展示详细信息。这种"刚刚好"的设计,正是B端产品追求效率与体验平衡的体现。
十二、报修Tab——状态时间轴设计
报修管理是园区物业运营的核心工作之一。报修Tab采用了时间轴布局来展示工单列表,通过左侧时间线配合右侧内容卡片的形式,直观呈现报修工单的处理进度和时间先后关系。
12.1 页面标题区
@Builder tabRepair() {
Column() {
Row() {
Text('🔧 报修中心').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('待处理 2').fontSize(11).fontColor(COLORS.orange)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('设备故障报修与处理进度追踪')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 12 })
报修页面标题区的右上角显示"待处理 2",使用醒目的橙色文字,直接告知用户当前有待处理的工单数量。橙色作为警告色,能够有效吸引用户注意力,提醒及时处理报修事项。
12.2 时间轴布局结构
时间轴是报修页面最具特色的设计元素。每条报修记录由左侧的时间轴线和右侧的内容卡片组成,整体形成一条纵向的时间线。
ForEach(this.repairList, (item: RepairItem, idx: number) => {
Row() {
// 左侧时间轴
Column() {
Column()
.width(12).height(12).borderRadius(6)
.backgroundColor(repairColor(item.status))
.margin({ top: 4 })
Column()
.width(2).layoutWeight(1)
.backgroundColor(idx === this.repairList.length - 1 ? COLORS.transparent : COLORS.line)
.margin({ top: 4 })
}
.width(20).alignItems(HorizontalAlign.Center)
// 右侧内容卡
Column() {
// ... 卡片内容
}
.layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.card)
.margin({ left: 8, bottom: 8 })
.onClick(() => { this.editIdx = idx; this.editModal = true })
}
.width('92%').alignItems(VerticalAlign.Top)
}, (item: RepairItem) => item.title + item.time)
时间轴的实现非常巧妙,我们来逐层解析:
左侧时间轴列:宽度为20像素的垂直列,内部包含两个元素:
- 时间节点圆点:12x12像素的圆形(圆角6像素),背景色由
repairColor函数根据工单状态返回。不同状态的工单显示不同颜色的圆点,用户可以通过圆点颜色快速识别工单状态。圆点顶部有4像素的上边距,使其与右侧卡片的标题文字大致对齐。 - 时间连接线:2像素宽的竖线,
.layoutWeight(1)使其自动填充剩余高度。这里有一个精妙的细节:最后一条记录的连接线使用透明色(COLORS.transparent),其他记录使用灰色分割线颜色。这样时间线在最后一个节点处终止,不会多出一截,形成完整而优雅的时间轴视觉效果。
右侧内容卡片:占据剩余宽度的白色卡片,与左侧时间轴通过8像素的左边距分隔。卡片顶部与时间轴圆点对齐(通过.alignItems(VerticalAlign.Top)实现)。
12.3 报修卡片内容
Column() {
Row() {
Text(item.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text(item.status).fontSize(10).fontColor(COLORS.card)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8).backgroundColor(repairColor(item.status))
}.width('100%').alignItems(VerticalAlign.Center)
Text(item.desc).fontSize(12).fontColor(COLORS.sub).margin({ top: 6 })
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text('🕐 ' + item.time).fontSize(10).fontColor(COLORS.text3)
Text(' 👤 ' + item.reporter).fontSize(10).fontColor(COLORS.text3).margin({ left: 10 })
}.margin({ top: 6 })
}
报修卡片内部采用垂直布局,包含三部分内容:
标题行:左侧是报修标题(15号加粗),右侧是状态标签。状态标签的样式与访客页面保持一致——圆角胶囊形,白色文字,背景色由repairColor函数动态确定。标题与标签垂直居中对齐。
描述内容:报修详细描述,12号灰色文字,最多显示两行,超出部分用省略号截断。这是一个重要的用户体验细节——既展示了足够的故障信息让维修人员判断问题,又避免了过长描述占用过多空间。
底部元信息:报修时间和报修人信息,使用10号浅灰色文字。时间用时钟emoji标识,报修人用人物emoji标识,两者之间有10像素的间距。这些辅助信息为工单提供了完整的上下文。
12.4 底部操作栏
Row() {
Button('+ 发起报修').fontSize(13).fontColor(COLORS.card)
.backgroundColor(COLORS.blue).borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
Blank()
Text('清除已完成').fontSize(13).fontColor(COLORS.red)
.onClick(() => { this.delModal = true })
}.width('92%').margin({ bottom: 8 })
报修页面的底部操作栏与其他页面不同,它包含两个操作入口:左侧是"+ 发起报修"主按钮(科技蓝),右侧是"清除已完成"文字按钮(红色)。
"清除已完成"功能使用纯文字按钮而非实心按钮,因为这是一个次要的清理操作,不需要太强的视觉强调。使用红色则暗示了这是一个删除/清除类的操作,符合用户的色彩认知习惯。点击该按钮会打开删除确认弹窗,防止用户误操作。
时间轴布局是报修页面的灵魂设计。相比于普通的列表布局,时间轴能够更直观地表达事件的时间先后和流程推进关系。在工单管理、任务追踪、审批流程等场景中,时间轴布局能够帮助用户更好地理解任务的处理进度和历史脉络,是B端产品中一种高效的信息组织方式。
十三、公告Tab——信息流卡片设计
园区公告是信息上传下达的重要渠道。公告Tab采用信息流卡片的布局形式,每条公告以独立卡片的形式纵向排列,用户可以像浏览资讯一样快速浏览最新的园区公告。
13.1 页面标题区
@Builder tabNotice() {
Column() {
Row() {
Text('📢 园区公告').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('未读 3').fontSize(11).fontColor(COLORS.red)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('园区最新通知公告,重要信息第一时间掌握')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 12 })
公告页面标题区的右上角显示"未读 3",使用红色文字,与通知类产品的未读提醒惯例保持一致。红色能够有效激发用户的阅读欲望,促使其及时查看未读消息。
13.2 公告卡片结构
每条公告是一张独立的白色卡片,卡片内信息层次分明,从上到下依次为发布来源、公告标题、内容摘要和操作入口。
ForEach(this.noticeList, (item: NoticeItem, idx: number) => {
Column() {
Row() {
Text(item.dept).fontSize(11).fontColor(COLORS.blue)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(COLORS.chip).borderRadius(6)
Blank()
if (item.unread) {
Column().width(8).height(8).borderRadius(4).backgroundColor(COLORS.red)
}
Text(item.time).fontSize(10).fontColor(COLORS.text3).margin({ left: item.unread ? 6 : 0 })
}.width('100%').alignItems(VerticalAlign.Center)
顶部信息栏:左侧是发布部门标签,使用科技蓝色文字配淡蓝色背景,小尺寸圆角。部门标签能够帮助用户快速判断公告的来源和重要性级别。右侧是发布时间和未读标记:如果是未读公告,先显示一个8像素的红色小圆点(未读标记),然后才是时间文字;已读公告则直接显示时间。时间与未读标记之间的间距会根据是否未读动态调整(未读时6像素,已读时0像素),确保布局紧凑。
Text(item.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.margin({ top: 8 }).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
公告标题:15号加粗深灰蓝色文字,是卡片的视觉核心。标题最多显示一行,超出部分用省略号截断,保证卡片高度的一致性。
Text(item.summary).fontSize(12).fontColor(COLORS.sub).margin({ top: 6 })
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
内容摘要:公告正文的摘要部分,12号灰色文字,最多显示两行。摘要让用户在不进入详情页的情况下就能大致了解公告内容,提升了信息浏览效率。两行的限制既展示了足够的信息,又避免了单条公告占用过多屏幕空间。
Row() {
Text('查看详情').fontSize(11).fontColor(COLORS.blue)
Text(' →').fontSize(11).fontColor(COLORS.blue)
}.margin({ top: 8 })
查看详情入口:卡片底部有一个"查看详情 →"的文字链接,使用科技蓝色。文字+箭头的组合是经典的"更多操作"视觉暗示,引导用户点击进入公告详情页。
13.3 卡片整体样式
}
.width('92%').padding(14).borderRadius(12).backgroundColor(COLORS.card)
.margin({ bottom: 10 })
.onClick(() => { this.editIdx = idx; this.editModal = true })
}, (item: NoticeItem) => item.title + item.time)
公告卡片宽度为屏幕的92%,内边距14像素,圆角12像素,底部间距10像素。14像素的内边距比其他页面的卡片略大,因为公告卡片的内容更多,需要更充裕的内部空间来保证阅读舒适度。
整张卡片可点击,点击后打开编辑/详情弹窗,查看公告的完整内容。
13.4 发布公告按钮
Row() {
Button('发布公告').fontSize(13).fontColor(COLORS.card)
.backgroundColor(COLORS.green).borderRadius(20).height(36)
.onClick(() => { this.addModal = true })
}
.width('100%').justifyContent(FlexAlign.Center).margin({ top: 4, bottom: 8 })
页面底部是"发布公告"按钮,使用绿色背景。绿色在这里传达"发布、广播、通知"的正向含义,与会议室页面的预约按钮颜色保持一致,形成了"操作类功能用绿色"的色彩语义。
公告页面的信息流设计借鉴了内容类产品的交互模式——卡片式布局、标题+摘要的信息结构、未读标记的视觉提醒。这种设计既符合用户的阅读习惯,又能高效地传递信息。在B端产品中引入C端产品成熟的交互模式,能够降低用户的学习成本,提升产品的易用性。
十四、我的Tab——中心大卡个人中心
"我的"页面是用户管理个人信息和查看个人相关事务的中心。该页面采用"中心大卡+功能列表"的经典个人中心布局,顶部用一张醒目的大卡片展示用户核心信息,下方以列表形式排列各类个人服务入口。
14.1 页面标题区
@Builder tabMine() {
Column() {
Row() {
Text('👤 个人中心').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('设置 >').fontSize(11).fontColor(COLORS.blue)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 16, bottom: 4 })
Text('个人信息与园区服务一站式管理')
.fontSize(11).fontColor(COLORS.text3)
.margin({ left: 16, bottom: 12 })
个人中心页面标题区的右上角是"设置 >"入口,用蓝色文字提供系统设置的快速访问通道。
14.2 中心大卡——用户信息展示
中心大卡是个人页面的视觉焦点,它集中展示了用户的头像、姓名、部门、工号、等级、位置、积分等核心信息。
Column() {
Stack() {
Stack() { Text('👨💼').fontSize(42) }
.width(72).height(72).borderRadius(36)
.backgroundColor(this.breath ? COLORS.blueD : COLORS.chip)
.alignContent(Alignment.Center)
}
.width(80).height(80).borderRadius(40)
.border({ width: 3, color: COLORS.blue })
.alignContent(Alignment.Center)
头像区域:头像采用双层圆形设计——内层是72x72像素的圆形头像背景,外层是80x80像素的圆形边框,边框宽度3像素,颜色为科技蓝。双层圆形设计增加了头像的层次感和精致感。
头像背景色与呼吸动画绑定(this.breath ? COLORS.blueD : COLORS.chip),背景色会在深蓝色和淡蓝色之间平滑过渡,形成柔和的呼吸脉动效果。这种动效为静态的头像增添了生命力,也暗示了用户的"在线"状态。
头像使用人物emoji作为占位图,尺寸为42号。在实际项目中可以替换为用户的真实头像图片。
Text(this.employeeList[0].name).fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(COLORS.title).margin({ top: 12 })
Text(this.employeeList[0].dept).fontSize(13).fontColor(COLORS.sub).margin({ top: 2 })
Text('工号:' + this.employeeList[0].empNo).fontSize(11).fontColor(COLORS.text3).margin({ top: 2 })
用户基本信息:头像下方依次是姓名(20号加粗)、部门(13号灰色)、工号(11号浅灰色)。三级信息字号递减、颜色渐浅,形成清晰的信息层级。姓名作为最重要的信息,使用最大号的加粗字体。
Row() {
Column() {
Text('⭐ ' + this.employeeList[0].level).fontSize(12).fontColor(COLORS.orange)
}
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.chip).borderRadius(12)
Column() {
Text('🏢 ' + this.employeeList[0].building).fontSize(12).fontColor(COLORS.greenD)
}
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.chip).borderRadius(12)
.margin({ left: 8 })
}.margin({ top: 10 })
员工标签:姓名下方有两个标签——员工等级和办公位置。员工等级标签使用橙色文字(如"金牌员工"),配合星星emoji,体现荣誉和激励的含义。办公位置标签使用智慧绿深色文字(如"A栋12层"),配合办公楼emoji。两个标签都采用淡蓝色背景、胶囊形状,视觉风格统一。
Divider().strokeWidth(1).color(COLORS.line).margin({ top: 16, bottom: 12 })
Row() {
Column() {
Text(this.employeeList[0].points.toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.blue)
Text('积分').fontSize(10).fontColor(COLORS.sub).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('28').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.green)
Text('打卡天数').fontSize(10).fontColor(COLORS.sub).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('12').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.orange)
Text('本月申请').fontSize(10).fontColor(COLORS.sub).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%')
数据统计区:一条分割线下方是三个核心数据指标,横向等宽排列:
- 积分:2680分,科技蓝色
- 打卡天数:28天,智慧绿色
- 本月申请:12次,橙色
三个数据使用不同的品牌色,既丰富了视觉效果,又通过颜色区分了不同类型的数据。数值使用20号加粗大字,标签使用10号小字,大小对比鲜明。这三个数据分别从激励维度、考勤维度、事务维度反映了员工的园区活动情况。
}
.width('92%').padding(20).borderRadius(16).backgroundColor(COLORS.card)
.alignItems(HorizontalAlign.Center)
中心大卡整体宽度为屏幕的92%,内边距20像素,圆角16像素,是所有页面中内边距最大、圆角最大的卡片,彰显了其视觉重心地位。卡片内容全部居中对齐,营造出庄重而亲和的个人空间氛围。
14.3 功能列表
Column() {
this.mineItem('📋', '我的考勤', '查看打卡记录')
Divider().strokeWidth(1).color(COLORS.line).margin({ left: 52, right: 16 })
this.mineItem('📝', '我的申请', '请假/报销/用车')
Divider().strokeWidth(1).color(COLORS.line).margin({ left: 52, right: 16 })
this.mineItem('📅', '我的会议', '已预约会议列表')
Divider().strokeWidth(1).color(COLORS.line).margin({ left: 52, right: 16 })
this.mineItem('🔧', '我的报修', '报修进度追踪')
Divider().strokeWidth(1).color(COLORS.line).margin({ left: 52, right: 16 })
this.mineItem('👥', '我的访客', '邀请来访记录')
}
.width('92%').borderRadius(12).backgroundColor(COLORS.card)
.margin({ top: 12, bottom: 8 })
中心大卡下方是一个功能列表,包含五个个人服务入口:我的考勤、我的申请、我的会议、我的报修、我的访客。
列表项之间用分割线分隔,分割线左侧缩进52像素(与图标右侧对齐),右侧缩进16像素。这种"缩进式分割线"的设计是移动端列表的经典样式,它既分隔了列表项,又保持了视觉的连贯性——左侧图标区域形成一条隐形的纵向通道,引导用户的视线。
每个列表项都通过mineItem()构建函数生成,实现了代码的复用。
14.4 mineItem 构建函数
@Builder mineItem(icon: string, title: string, desc: string) {
Row() {
Stack() { Text(icon).fontSize(20) }
.width(36).height(36).borderRadius(10).backgroundColor(COLORS.chip)
.alignContent(Alignment.Center)
Column() {
Text(title).fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
Text(desc).fontSize(11).fontColor(COLORS.text3).margin({ top: 2 })
}.margin({ left: 10 }).alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('>').fontSize(14).fontColor(COLORS.text3)
}
.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
.onClick(() => { this.editModal = true })
}
mineItem函数接收三个参数:图标(emoji)、标题和描述。函数内部构建了一个标准的列表项:
- 左侧图标:36x36像素的圆角方形(圆角10像素),淡蓝色背景,emoji图标居中。
- 中间文字区:上下两行文字——主标题(14号,中等字重)和描述文字(11号,浅灰色)。描述文字补充说明该功能的具体内容,帮助用户理解功能范围。
- 右侧箭头:灰色的">"符号,表示可进入详情。
列表项左右内边距16像素,上下12像素,点击区域充裕。点击后打开编辑弹窗,模拟进入详情页的交互。
个人中心页面的设计体现了"以用户为中心"的产品理念——将用户最关心的个人信息和个人相关的事务集中展示在一个页面中,用户可以一站式管理自己的所有园区事务。中心大卡的设计增强了用户的归属感和身份认同感,功能列表的设计则保证了操作的便捷性。
十五、图表卡片——园区能耗柱状图
数据可视化是B端产品的重要能力,它能够将抽象的数字转化为直观的图形,帮助用户快速理解数据趋势和规律。当前页面在所有Tab的下方都放置了一个园区能耗统计图表,以柱状图的形式展示近六个月的园区能耗数据。
15.1 图表卡片整体结构
@Builder chartCard() {
Column() {
Row() {
Text('📊 园区能耗统计').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('近6个月').fontSize(12).fontColor(COLORS.text3)
}.width('100%').margin({ bottom: 12 })
图表卡片的标题栏延续了统一的设计风格:左侧是图表标题"📊 园区能耗统计",右侧是时间范围说明"近6个月"。标题使用15号加粗字体,时间范围使用12号灰色文字,主次分明。
15.2 柱状图主体
Row() {
ForEach(MONTH_IDX, (i: number) => {
Column() {
Text(ENERGY_VAL[i].toString() + 'kWh').fontSize(9).fontColor(COLORS.greenD)
.margin({ bottom: 4 })
Column()
.width(22)
.height(this.breath ? ENERGY_VAL[i] / MAX_VAL * 80 : ENERGY_VAL[i] / MAX_VAL * 64)
.borderRadius({ topLeft: 6, topRight: 6 })
.backgroundColor(i % 2 === 0 ? COLORS.blue : COLORS.green)
Text(MONTH_NAME[i]).fontSize(10).fontColor(COLORS.sub).margin({ top: 4 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}, (i: number) => i.toString())
}
.width('100%').alignItems(VerticalAlign.Bottom)
柱状图使用Row横向排列六根柱子,每根柱子占等宽比例(.layoutWeight(1))。每根柱子由三部分组成:
顶部数值标签:柱子上方显示具体的能耗数值(如"4200kWh"),使用9号智慧绿深色文字。数值标签让用户可以直接读取精确数据,而不仅是看个大概趋势。
柱体:柱体宽度为22像素,高度通过比例计算得出——ENERGY_VAL[i] / MAX_VAL * 基准高度。这里的基准高度与呼吸动画绑定:breath为true时基准高度是80像素,为false时是64像素。这意味着所有柱子的高度会在80%和64%的比例之间同步呼吸变化,形成一种生动的波浪起伏效果。
柱体的顶部左右圆角为6像素,底部为直角,形成"上圆下方"的柱形样式,是现代数据可视化中常见的美化处理。
柱体颜色采用蓝绿交替的配色方案:索引为偶数的柱子用科技蓝,奇数的用智慧绿。这种交替配色既避免了单色的单调感,又不会因为颜色过多而显得杂乱。蓝绿双色也与产品的双主色策略保持一致。
底部月份标签:柱子下方显示月份名称(如"3月"、“4月”),使用10号灰色文字,清晰标识每根柱子对应的时间维度。
整个柱状图区域通过.alignItems(VerticalAlign.Bottom)实现底部对齐,确保所有柱子的底部在同一水平线上,形成标准的柱状图视觉效果。
15.3 呼吸动画的实现机制
图表的呼吸动画是一个值得深入分析的设计亮点。它的实现原理非常简洁:
breath状态变量每秒翻转一次(由aboutToAppear中的setInterval驱动)- 柱体高度通过三目运算符绑定到
breath状态 - ArkUI框架检测到属性变化后,自动在旧值和新值之间添加过渡动画
这种"状态驱动动画"的模式是声明式UI框架的精髓所在。开发者不需要手动编写动画代码,只需要定义状态变化时的目标值,框架就会自动处理中间的过渡效果。这大大简化了动画开发的复杂度,让开发者可以将更多精力放在动画设计本身。
呼吸动画在图表中的作用是多方面的:
- 视觉吸引力:动态的图表比静态图表更能吸引用户注意
- 生命感:柔和的呼吸节奏让冰冷的数据有了温度
- 技术展示:展现了应用的动效能力和精致的交互设计
当然,动效的使用需要把握好"度"。当前页面的呼吸动画节奏平缓(1秒周期),幅度适中(80到64像素),不会对用户阅读数据造成干扰,属于"锦上添花"型的动效设计。
15.4 统计数据汇总
Divider().strokeWidth(1).color(COLORS.line).margin({ top: 12, bottom: 12 })
Row() {
Column() {
Text('总能耗').fontSize(10).fontColor(COLORS.text3)
Text('39,100 kWh').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.blue).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('月均').fontSize(10).fontColor(COLORS.text3)
Text('6,517 kWh').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.green).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('峰值月').fontSize(10).fontColor(COLORS.text3)
Text('8,500 kWh').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.orange).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%')
柱状图下方通过一条分割线分隔出统计汇总区,展示三个关键指标:
- 总能耗:39,100 kWh,科技蓝色。六个月的能耗总和,反映整体能耗规模。
- 月均:6,517 kWh,智慧绿色。月平均能耗,提供能耗水平的基准参考。
- 峰值月:8,500 kWh,橙色。能耗最高的月份数值,用橙色警示能源消耗的高峰期。
三个统计指标分别使用蓝、绿、橙三种颜色,与产品的色彩体系保持一致。每个指标的标签使用10号浅灰色,数值使用14号加粗字体,大小对比清晰。
这些汇总数据为柱状图提供了量化的补充——用户既能通过图形直观感受趋势,又能通过数字获取精确信息,两者相辅相成。
15.5 图表卡片的定位
}
.width('92%').padding(16).backgroundColor(COLORS.card).borderRadius(12)
.margin({ top: 8, bottom: 16 })
}
图表卡片宽度为屏幕的92%,内边距16像素,圆角12像素。它被放置在所有Tab页面的内容区底部,无论切换到哪个Tab,能耗统计图表始终可见。
这种"全局数据看板"的设计有其深意:能耗管理是智慧园区的核心指标之一,将能耗数据放在每个页面都能看到的位置,体现了园区管理对能源效率的重视。同时,统一的图表也为六个不同的业务Tab提供了一个视觉锚点,增强了页面之间的连贯性和整体感。
十六、底部Tab栏
底部Tab栏是移动应用的主导航入口,承载着页面切换的核心功能。当前页面的底部Tab栏设计简洁明了,六个Tab均匀分布,选中态使用品牌色强调,为用户提供清晰的导航指引。
16.1 Tab栏整体结构
@Builder tabBar() {
Row() {
ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
Column() {
Image(tab.icon).width(22).height(22)
.fillColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
Text(tab.name).fontSize(10)
.fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
.fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.padding({ top: 6, bottom: 6 })
.onClick(() => { this.currentTab = idx })
}, (tab: TabMeta) => tab.name)
}
.width('100%').height(56).backgroundColor(COLORS.card)
.border({ width: { top: 1 }, color: COLORS.line })
}
底部Tab栏使用Row水平布局,通过ForEach遍历TAB_LIST数组渲染六个Tab项。整体高度为56像素,这是移动端底部导航的标准高度,符合人体工程学中拇指易于触碰的区域范围。
Tab栏背景为白色,顶部有一条1像素的灰色边线,将Tab栏与内容区域视觉分隔。这种顶部边线的设计在iOS和Android的设计规范中都很常见,它形成了一条清晰的视觉分界线。
16.2 单个Tab项的结构
每个Tab项采用"图标+文字"的上下布局,使用Column垂直排列:
图标:使用Image组件加载资源图标,尺寸为22x22像素。图标颜色通过.fillColor()属性动态设置——选中时为科技蓝(COLORS.tabOn),未选中时为浅灰蓝(COLORS.text3)。使用单色图标配合着色(tint)技术,可以方便地实现图标的状态切换,无需准备两套图标资源。
文字:Tab名称使用10号字体,选中时为科技蓝加粗,未选中时为浅灰蓝常规字重。文字与图标之间有2像素的间距,保持紧凑的视觉关系。
每个Tab项使用.layoutWeight(1)平分Tab栏宽度,确保六个Tab等宽分布。上下各6像素的内边距为点击区域留出了足够空间,提升了点击的容错率。
16.3 交互逻辑
Tab的选中状态由currentTab状态变量控制。当用户点击某个Tab时,.onClick()事件将currentTab更新为对应的索引值。由于currentTab使用了@State装饰,状态变化会触发UI自动刷新:被选中的Tab图标和文字变为蓝色加粗,其他Tab保持灰色。
这种状态驱动的导航切换机制非常简洁——没有复杂的导航控制器,没有页面栈的管理,只是一个简单的状态变量和条件渲染。对于这种同级页面之间的平级切换,这种方式既高效又直观。
16.4 六Tab设计的考量
六个Tab的导航设计在移动端应用中属于较多的情况(通常为3-5个)。之所以设置六个Tab,是因为智慧园区的业务场景比较分散,六个核心功能都有较高的使用频率,不宜将其中某些折叠到二级入口中。
多Tab设计也带来了一些挑战:
- 空间紧张:六个Tab平分底部宽度,每个Tab的可用空间相对较小,因此图标和文字都需要控制尺寸。
- 点击精度:Tab数量多意味着每个Tab的点击区域较小,需要用户更精确地点击。不过56像素的高度和等宽分配基本保证了可点击区域的最低标准。
- 视觉平衡:六个Tab一字排开,需要确保选中态足够醒目,用户才能快速定位当前所在位置。
当前页面的设计通过以下方式应对这些挑战:
- 图标和文字都使用较小的尺寸(22像素图标、10号文字),节省空间
- 选中态使用颜色变化+字重变化的双重强调,确保辨识度
- 图标使用单色矢量风格,在小尺寸下依然清晰可辨
底部Tab栏虽然是一个相对简单的组件,但它是用户使用频率最高的交互元素之一。其设计的优劣直接影响用户对产品的整体体验。一个好的底部导航应该做到:清晰可辨、操作便捷、反馈及时、风格统一。当前页面的Tab栏设计在这些方面都达到了良好的水准。
十七、弹窗系统
弹窗是移动应用中常见的模态交互组件,用于处理新增、编辑、删除、确认等需要用户聚焦的操作。当前页面设计了一套完整的弹窗系统,包括通用遮罩层和三种功能弹窗(新增、编辑、删除),形成了统一的弹窗交互规范。
17.1 通用遮罩层
@Builder modalOverlay(onClose: () => void) {
Stack() {
Column().width('100%').height('100%').backgroundColor(COLORS.mask)
}
.width('100%').height('100%').alignContent(Alignment.Center)
.onClick(() => onClose())
}
modalOverlay是所有弹窗共用的遮罩层组件。它的实现非常简洁:一个占满全屏的半透明黑色层(COLORS.mask即rgba(0,0,0,0.5)),点击遮罩层会触发关闭回调。
遮罩层的作用是多方面的:
- 视觉聚焦:通过暗化背景,将用户的注意力引导到弹窗内容上
- 层级暗示:遮罩层清晰地表明弹窗位于页面内容之上,是更高层级的交互
- 交互阻断:遮罩层拦截了对下方页面的点击,防止用户在弹窗期间误操作页面内容
- 关闭入口:点击遮罩层关闭弹窗是用户普遍熟悉的交互模式,提供了便捷的退出方式
遮罩层接收一个onClose回调函数参数,由调用方传入具体的关闭逻辑。这种设计使得遮罩层可以被多种弹窗复用,每种弹窗只需关注自己的内容和关闭逻辑。
17.2 新增弹窗
@Builder panelAdd(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('➕ 新增申请').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(COLORS.title).margin({ bottom: 16 })
Column() {
Text('标题').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '请输入标题' }).fontSize(14).fontColor(COLORS.title)
.placeholderColor(COLORS.text3).backgroundColor(COLORS.chip)
.borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 12 })
Column() {
Text('内容描述').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '请输入详细描述' }).fontSize(14).fontColor(COLORS.title)
.placeholderColor(COLORS.text3).backgroundColor(COLORS.chip)
.borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 12 })
Column() {
Text('备注').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '添加备注(可选)' }).fontSize(14).fontColor(COLORS.title)
.placeholderColor(COLORS.text3).backgroundColor(COLORS.chip)
.borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 16 })
Row() {
Button('取消').fontSize(14).fontColor(COLORS.sub).backgroundColor(COLORS.chip)
.borderRadius(20).height(40).layoutWeight(1).onClick(() => onClose())
Button('确认提交').fontSize(14).fontColor(COLORS.card).backgroundColor(COLORS.blue)
.borderRadius(20).height(40).layoutWeight(1).margin({ left: 10 }).onClick(() => onClose())
}.width('100%')
}
.width('80%').padding(24).backgroundColor(COLORS.card).borderRadius(20)
}
.width('100%').height('100%').alignContent(Alignment.Center)
}
新增弹窗(panelAdd)是一个居中显示的对话框,宽度为屏幕的80%,白色背景,圆角20像素,内边距24像素。弹窗内容包括:
标题:“➕ 新增申请”,18号加粗字体,明确弹窗的用途。
表单区域:包含三个输入字段:
- 标题:必填项,placeholder为"请输入标题"
- 内容描述:必填项,placeholder为"请输入详细描述"
- 备注:可选项,placeholder为"添加备注(可选)"
每个输入字段都有标签文字(13号灰色)和输入框。输入框使用淡蓝色背景,圆角8像素,高度40像素。placeholder文字使用浅灰色,输入文字使用深灰蓝色。输入框的样式设计统一而整洁。
操作按钮:底部有两个按钮,左右排列,各占一半宽度:
- 取消按钮:灰色文字配淡蓝色背景,次要操作样式
- 确认提交按钮:白色文字配科技蓝背景,主要操作样式
两个按钮都是胶囊形状(圆角20像素),高度40像素,之间有10像素的间距。主操作按钮使用品牌色强调,符合用户的操作预期。
17.3 编辑/详情弹窗
@Builder panelEdit(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('✏️ 详情查看').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(COLORS.title).margin({ bottom: 16 })
Column() {
Text('当前序号').fontSize(13).fontColor(COLORS.sub)
Text(this.editIdx >= 0 ? '第 ' + (this.editIdx + 1) + ' 条记录' : '未选择')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.green).margin({ top: 4 })
}.width('100%').margin({ bottom: 12 })
Column() {
Text('修改内容').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '请输入修改内容' }).fontSize(14).fontColor(COLORS.title)
.placeholderColor(COLORS.text3).backgroundColor(COLORS.chip)
.borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 12 })
Column() {
Text('备注信息').fontSize(13).fontColor(COLORS.sub)
TextInput({ placeholder: '添加备注(可选)' }).fontSize(14).fontColor(COLORS.title)
.placeholderColor(COLORS.text3).backgroundColor(COLORS.chip)
.borderRadius(8).height(40).margin({ top: 6 })
}.width('100%').margin({ bottom: 16 })
Row() {
Button('取消').fontSize(14).fontColor(COLORS.sub).backgroundColor(COLORS.chip)
.borderRadius(20).height(40).layoutWeight(1).onClick(() => onClose())
Button('保存修改').fontSize(14).fontColor(COLORS.card).backgroundColor(COLORS.green)
.borderRadius(20).height(40).layoutWeight(1).margin({ left: 10 }).onClick(() => onClose())
}.width('100%')
}
.width('80%').padding(24).backgroundColor(COLORS.card).borderRadius(20)
}
.width('100%').height('100%').alignContent(Alignment.Center)
}
编辑弹窗(panelEdit)的整体结构与新增弹窗类似,但有几个关键区别:
标题不同:使用"✏️ 详情查看"作为标题,暗示这个弹窗兼具查看详情和编辑修改的功能。
当前序号显示:新增了一个"当前序号"信息字段,显示当前操作的是第几条记录。序号使用绿色加粗字体,直观醒目。当editIdx为-1(未选中)时,显示"未选择"的占位文字。
确认按钮颜色不同:确认按钮使用绿色背景(“保存修改”),而非新增弹窗的蓝色。绿色在这里传达"保存、确认、完成"的含义,与新增操作形成区分。这种通过颜色区分操作类型的设计,能够帮助用户快速识别当前的操作性质。
17.4 删除确认弹窗
@Builder panelDel(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('🗑️ 确认删除').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(COLORS.title).margin({ bottom: 12 })
Text(this.delIdx >= 0 ? '确定要删除第 ' + (this.delIdx + 1) + ' 条记录吗?' : '确定要清除已完成记录吗?此操作不可撤销。')
.fontSize(14).fontColor(COLORS.sub).textAlign(TextAlign.Center).margin({ bottom: 20 })
Row() {
Button('取消').fontSize(14).fontColor(COLORS.sub).backgroundColor(COLORS.chip)
.borderRadius(20).height(40).layoutWeight(1).onClick(() => onClose())
Button('确认删除').fontSize(14).fontColor(COLORS.card).backgroundColor(COLORS.red)
.borderRadius(20).height(40).layoutWeight(1).margin({ left: 10 }).onClick(() => onClose())
}.width('100%')
}
.width('80%').padding(24).backgroundColor(COLORS.card).borderRadius(20)
}
.width('100%').height('100%').alignContent(Alignment.Center)
}
删除确认弹窗(panelDel)是三种弹窗中结构最简单的,但也是交互上最需要谨慎的。删除操作具有不可逆性,因此需要明确的二次确认。
删除弹窗的特点包括:
简洁的内容:只包含标题和确认文案,没有输入表单。这是因为删除操作不需要用户输入额外信息,只需要确认意图。
动态确认文案:确认文案会根据delIdx的值动态变化。如果delIdx有效(>=0),显示删除单条记录的确认信息;如果无效(-1),显示清除已完成记录的批量操作确认信息,并特别强调"此操作不可撤销",提醒用户谨慎操作。
红色确认按钮:确认删除按钮使用红色背景,这是UI设计中的通用惯例——红色代表危险、警告、不可逆操作。红色能够唤起用户的警觉,防止误删。
居中对齐的文案:确认文案使用居中对齐(textAlign(TextAlign.Center)),在视觉上更加庄重,也符合确认对话框的常见设计模式。
弹窗系统的设计价值
这套弹窗系统虽然功能相对基础(没有实现真正的数据增删改逻辑),但其设计架构体现了良好的工程实践:
组件复用:通用遮罩层modalOverlay被三个弹窗共享,避免了重复代码。
样式统一:所有弹窗遵循相同的视觉规范——相同的宽度、圆角、内边距、按钮样式,确保了交互体验的一致性。
回调机制:每个弹窗都接收关闭回调函数,由调用方控制弹窗的生命周期,实现了弹窗组件与业务逻辑的解耦。
状态驱动:弹窗的显示与隐藏完全由@State变量控制,符合声明式UI的编程范式。
在实际项目中,这套弹窗系统可以进一步扩展——比如使用@CustomDialog装饰器封装为独立的弹窗组件、通过参数传递动态配置弹窗内容、增加表单验证逻辑等。当前的实现已经为这些扩展打下了良好的基础。
十八、功能模块对比表
为了更清晰地展示六个功能模块的设计特点和技术实现差异,下面从多个维度进行对比分析。
| 对比维度 | 工作台 | 访客 | 会议室 | 报修 | 公告 | 我的 |
|---|---|---|---|---|---|---|
| 布局模式 | 宫格菜单(4行×3列) | 状态列表 + 标签筛选 | 两列卡片网格(3排×2列) | 时间轴列表 | 信息流卡片 | 中心大卡 + 功能列表 |
| 数据模型 | WorkbenchItem | VisitorItem | MeetingItem | RepairItem | NoticeItem | EmployeeItem |
| 模型字段数 | 3个(icon/name/badge) | 5个(name/reason/status/date/phone) | 6个(name/capacity/equipment/status/current/floor) | 5个(title/desc/status/time/reporter) | 5个(title/dept/time/summary/unread) | 6个(name/dept/empNo/points/level/building) |
| 状态类型数 | 1种(角标数字) | 4种(待确认/已预约/已离开/已取消) | 4种(空闲/使用中/已预约/维护中) | 4种(待受理/处理中/已完成/已驳回) | 2种(已读/未读) | 3种(等级/积分/打卡) |
| 颜色映射函数 | 无 | visitorColor | meetingColor | repairColor | 无 | 无 |
| 状态主色 | 红色(角标) | 橙/绿/灰/红 | 绿/蓝/橙/红 | 橙/蓝/绿/红 | 红色(未读点) | 橙/蓝/绿 |
| 列表渲染方式 | 逐行ForEach(4行) | 单列ForEach | 逐排ForEach(3排) | 单列ForEach | 单列ForEach | 构建函数复用 |
| 卡片宽度 | 自适应(三分之一) | 92%屏宽 | 48%屏宽 | 92%屏宽(加时间轴) | 92%屏宽 | 92%屏宽 |
| 卡片圆角 | 14px(图标背景) | 12px | 12px | 12px | 12px | 16px(大卡)/12px(列表) |
| 操作按钮颜色 | 蓝色(添加应用) | 蓝色(新增访客) | 绿色(预约会议室) | 蓝色(发起报修) | 绿色(发布公告) | 无(列表项跳转) |
| 顶部统计信息 | 全部应用入口 | 共6位(绿色) | 今日可用入口 | 待处理2(橙色) | 未读3(红色) | 设置入口 |
| 角标/红点 | 有(数字角标) | 无 | 无 | 无 | 有(未读红点) | 无 |
| 呼吸动画关联 | 无 | 无 | 无 | 无 | 无 | 头像背景呼吸 |
| 点击交互 | 打开编辑弹窗 | 打开编辑弹窗 | 打开编辑弹窗 | 打开编辑弹窗 | 打开编辑弹窗 | 打开编辑弹窗 |
| 信息密度 | 高(12个入口) | 中(6条记录) | 高(6张卡片) | 中(6条记录) | 中(6条公告) | 低(大卡+5项列表) |
对比分析解读
布局多样性:六个Tab采用了六种完全不同的布局方式——宫格、列表+标签、卡片网格、时间轴、信息流、中心大卡。这种多样化的布局设计充分展现了ArkUI框架的UI表达能力,也体现了产品根据不同业务场景选择最合适布局方式的设计思路。
状态复杂度差异:访客、会议室、报修三个模块的状态类型最为丰富(各4种),因此都配备了专门的颜色映射函数。工作台和公告模块虽然状态简单(角标数字、未读标记),但都使用了红色作为提醒色,符合"红色=警示/待处理"的色彩语义。
数据模型字段数:会议室和员工模型字段最多(6个),因为这两个场景需要描述的信息维度最丰富。工作台模型最简单(3个字段),因为它只是功能入口的抽象。
操作按钮的颜色策略:操作按钮的颜色选择并非随意——添加/新增类操作用蓝色,预约/发布类操作用绿色。蓝色代表"创建、新增",绿色代表"预约、发布、确认",形成了微妙的色彩语义区分。
信息密度梯度:从工作台(最高密度)到我的页面(最低密度),信息密度呈现逐渐降低的趋势。这与用户的使用场景相匹配——工作台是功能导航,需要一目了然;个人中心是身份展示,需要舒适大气。
交互一致性:尽管六个页面的布局和内容差异很大,但点击交互保持了高度一致——都是点击后打开编辑/详情弹窗。这种交互一致性降低了用户的学习成本,无论在哪个页面,用户都知道点击卡片会进入详情。
十九、总结与展望
19.1 设计总结
通过对"智园通"智慧园区工作台组件的全面剖析,我们可以总结出以下几个核心设计特点:
1. 组件化架构清晰,分层明确
该应用采用了典型的分层架构设计,从颜色系统、数据模型、状态管理到UI渲染,每一层都有清晰的职责边界。六个@Observed数据模型与六个业务Tab一一对应,@Builder构建函数将UI片段模块化,主组件作为调度中心统筹全局。这种架构使得代码结构清晰、模块独立性强、便于维护和扩展。
2. 六大场景六种布局,针对性强
六个功能Tab没有采用千篇一律的列表布局,而是根据各业务场景的特点选择了最适合的布局方式:工作台用宫格菜单保证功能入口的密度,访客用状态列表便于管理,会议室用卡片网格直观展示空间状态,报修用时间轴体现流程推进,公告用信息流卡片适合阅读,我的用中心大卡突出个人身份。这种"场景驱动布局"的设计理念是B端产品走向成熟的重要标志。
3. 色彩体系完善,语义化程度高
科技蓝与智慧绿的双主色策略,配合三级文字灰度、四种语义功能色,构成了一套完整而专业的色彩体系。颜色在该应用中不仅仅是视觉装饰,更是信息传达的重要工具——不同的状态用不同的颜色标识,不同的操作用不同的颜色区分,用户可以通过颜色快速获取信息。这种色彩语义化的设计在数据密集型的B端产品中尤为重要。
4. 动效精致适度,提升体验
呼吸动画是该应用的一个亮点设计。通过一个简单的breath状态变量和setInterval定时器,就实现了头部快捷入口、个人头像、能耗柱状图等多个元素的联动呼吸效果。动画节奏平缓、幅度适中,既提升了界面的精致感和生命力,又不会对用户造成干扰。这种"适度动效"的设计理念值得借鉴。
5. 弹窗系统统一,交互规范
三种功能弹窗(新增、编辑、删除)共享统一的遮罩层、视觉规范和交互模式,形成了完整的弹窗体系。不同类型的操作通过按钮颜色进行区分(蓝色新增、绿色保存、红色删除),符合用户的色彩认知习惯。弹窗的状态驱动模式也与ArkUI的声明式编程范式完美契合。
19.2 技术亮点
声明式UI的优雅表达:整个应用完全采用声明式UI范式开发,所有UI都是状态的函数。开发者只需要描述"在什么状态下UI应该是什么样子",框架就会自动处理状态变化时的UI更新。这种编程模式大幅降低了UI开发的复杂度,提升了代码的可读性和可维护性。
@Builder的组合威力:@Builder装饰器让UI片段的复用变得异常简单。从mineItem这样的小型列表项,到headerOverview这样的复杂头部区域,再到modalOverlay这样的通用组件,都可以通过构建函数进行封装和复用。这种组合式的UI构建方式,是支撑复杂界面模块化开发的关键技术。
状态管理的恰到好处:该应用的状态管理方案简洁而有效。@State管理组件内状态,@Observed实现对象级响应式,状态变量数量控制在合理范围内,没有过度设计。对于中小型应用来说,这种轻量级的状态管理方案是最佳选择——既满足了需求,又避免了引入复杂状态管理框架的 overhead。
纯CSS式的样式表达:ArkUI的链式调用样式系统(如.fontSize().fontColor().margin())非常类似于CSS的属性设置,但又比CSS更简洁、更类型安全。这种样式表达方式学习成本低,代码可读性好,是声明式UI框架的优秀实践。
19.3 可优化方向
尽管当前实现已经相当完整,但从生产级应用的标准来看,仍有以下几个可以优化的方向:
1. 数据与视图进一步解耦
目前各Tab的数据源直接绑定在主组件中,模拟数据与UI渲染逻辑混在一个文件中。在实际项目中,可以将数据层抽离为独立的service或repository模块,通过ViewModel或状态管理库进行连接。这样数据层可以独立演进(比如从本地模拟数据切换为网络请求),而无需修改UI代码。
2. 列表筛选功能完善
访客页面的状态标签目前只是静态展示,点击不会切换筛选状态。可以增加一个@State selectedVisitorStatus变量来记录当前选中的筛选条件,然后根据条件对visitorList进行过滤。同样的筛选功能也可以扩展到其他列表页面。
3. 表单验证与数据绑定
弹窗中的表单目前只是UI展示,没有实现真正的数据绑定和验证逻辑。在实际项目中,需要将输入框的值绑定到状态变量,并添加必填校验、格式校验等验证逻辑,确保用户输入的数据符合要求。
4. 列表性能优化
当前列表使用ForEach渲染,对于几十条数据完全足够。但如果数据量增长到上百条甚至更多,可以考虑使用LazyForEach实现惰性加载,只渲染可视区域内的列表项,提升长列表的滚动性能。
5. 暗黑模式适配
目前的色彩系统只有浅色模式一套配色。可以扩展ColorPalette接口,增加深色模式的色值定义,然后根据系统的深色模式设置动态切换。这对于提升夜间使用体验非常重要。
6. 国际化支持
当前所有文案都是硬编码的中文文本。对于有国际化需求的产品,可以将所有文案抽离为资源文件,通过$r()的方式引用。这样可以方便地支持多语言切换。
19.4 展望
智慧园区作为数字化转型的重要场景,未来还有广阔的发展空间。站在技术角度,我们可以展望以下几个发展方向:
AI赋能的智慧运营:随着大模型和AI技术的发展,智慧园区应用将不再仅仅是信息展示和流程审批的工具,而是会融入更多的AI能力——智能访客识别、智能故障诊断、智能能耗优化、智能会议调度等。AI将从"辅助工具"升级为"决策伙伴",帮助园区运营者做出更明智的决策。
IoT设备的深度融合:智慧园区的核心是物理空间与数字世界的融合。未来的应用将更多地与IoT设备联动——门禁系统、监控摄像头、环境传感器、智能照明、智能空调等设备的数据将实时同步到应用中,形成真正的"数字孪生"园区。
多端协同的全场景体验:HarmonyOS的分布式能力为智慧园区提供了独特的技术优势。未来的智慧园区应用将不仅仅运行在手机上,还会延伸到平板、智慧屏、可穿戴设备甚至车机上。用户可以在不同设备之间无缝流转任务,获得一致而连贯的使用体验。
低代码/无代码的配置化趋势:随着园区业务的多样化和个性化需求的增加,纯代码开发模式将难以满足快速迭代的需求。低代码/无代码平台将在智慧园区领域得到更多应用,让运营人员可以通过可视化配置的方式自定义页面和流程,降低开发成本、提升响应速度。
数据驱动的持续优化:随着用户数据和运营数据的积累,数据驱动的产品优化将成为常态。通过分析用户的使用行为、功能的使用频率、工单的处理时效等数据,可以持续优化产品的信息架构和交互流程,让产品越来越懂用户。
19.5 结语
"智园通"智慧园区工作台虽然只是一个单页面的演示项目,但它所展现的设计思想和技术实践具有广泛的参考价值。从色彩体系到数据模型,从组件架构到动效设计,从布局策略到交互模式,每一个细节都体现了B端产品设计的方法论。
HarmonyOS ArkUI 作为新一代的声明式UI框架,为开发者提供了强大而灵活的工具。但工具只是手段,真正决定产品品质的,是对用户需求的深刻理解和对细节的极致追求。希望本文的分析能够为从事HarmonyOS开发和B端产品设计的朋友们提供一些启发和参考。
在数字化转型的浪潮中,智慧园区只是千千万万个行业场景中的一个。但正如一滴水可以折射太阳的光芒,一个优秀的园区应用也可以折射出整个行业数字化的方向——以用户为中心,以数据为驱动,以技术为手段,让物理世界变得更加智能、更加高效、更加美好。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)