基于HarmonyOS API 24翠湖花园智慧物业管理系统:社区服务应用UI 组件层(@Entry 入口 + 各业务 @Component),设计令牌与配置字典层(const 常量)
引言:智慧社区的时代呼唤
在数字化浪潮席卷全球的今天,传统物业管理行业正经历着前所未有的深刻变革。曾几何时,业主想要报修一个漏水的水龙头,需要亲自跑到物业服务中心排队登记;想要缴纳每月的物业费,需要在工作日请假前往收费窗口;想要了解小区的最新通知,只能依赖楼道里张贴的纸质公告。这种低效、被动、信息不对称的服务模式,已经远远无法满足现代都市居民对美好居住生活的向往。

随着移动互联网、物联网、人工智能等技术的快速普及,"智慧社区"这一概念应运而生。智慧社区的核心目标,是通过数字化手段将物业管理方与业主之间的服务链路彻底打通,让报修、缴费、公告、投诉等高频日常事务能够在指尖轻松完成。一个设计优良的物业管理应用,不仅能够大幅提升物业方的运营效率,降低人力沟通成本,更能够让业主享受到透明、即时、有温度的服务体验。
本文将要深入剖析的,正是一套面向中高端住宅小区的智慧物业管理应用。该应用以虚构的"翠湖花园小区"为服务对象,完整覆盖了业主日常接触最频繁的五大业务场景:首页服务聚合、报修工单管理、缴费账单记录、公告通知中心、个人中心档案。从交互设计到数据建模,从视觉规范到状态管理,这套应用都展现出了相当高的工程水准。
特别值得一提的是,该应用采用了鸿蒙操作系统(HarmonyOS)的 ArkUI 声明式开发范式。ArkUI 使用 TypeScript 超集语言,通过装饰器语法(如 @Component、@State、@Builder、@Observed)来描述 UI 结构与状态绑定关系,兼具声明式开发的简洁性和强类型语言的安全性。这种技术选型使得整个应用在保持代码可读性的同时,具备了良好的可维护性和扩展性。
接下来,我们将逐模块、逐函数、逐行地深入拆解这套系统的实现细节,带你完整理解一个生产级物业应用是如何从零构建起来的。
一、整体架构概览
1.1 五大功能模块划分
整个应用在业务层面被清晰地划分为五个一级功能模块,每个模块对应底部导航栏中的一个 Tab 入口:
- 首页服务:作为应用的首屏,聚合展示物业概况、服务入口网格、快捷服务和社区动态,是业主获取信息的总览中枢。
- 报修管理:承载报修工单的完整生命周期管理,包括新增报修、查看详情、编辑工单、删除记录、状态筛选等能力。
- 缴费记录:集中展示各类费用账单,提供月度缴费趋势可视化图表和在线支付入口。
- 公告通知:物业方向业主发布通知的渠道,支持按类型筛选、查看详情和内容分享。
- 我的:业主个人档案中心,展示房屋信息、物业统计数据、年度缴费分布及快捷设置入口。
1.2 技术架构特点
从技术实现角度看,该应用具备以下几个显著特征:
第一,采用集中式设计令牌(Design Token) 管理色彩、图标等视觉资源,所有颜色值统一定义在一个常量对象中,确保了全应用视觉风格的高度一致性。
第二,使用配置字典驱动渲染的方式管理业务元数据。报修类型、工单状态、账单类型、公告级别等业务枚举值,均以 Record<string, Meta> 的形式预置配置,UI 层只需根据 key 查表即可获取对应的图标、颜色、背景色等展示属性,极大地降低了硬编码。
第三,通过 @Observed 装饰器将数据模型类标记为可观察对象,配合 @State 状态变量实现 UI 与数据的自动同步刷新,这是 ArkUI 响应式编程的核心机制。
第四,每个功能模块独立封装为 @Component 组件,组件内部通过 @Builder 方法定义可复用的 UI 片段(如列表项、弹窗、卡片),实现了良好的组件化拆分。
1.3 代码组织结构
整个源文件遵循"自顶向下、先定义后使用"的组织原则,依次包含以下层次:
- 类型接口定义层(interface 声明)
- 可观察数据模型层(@Observed class)
- 设计令牌与配置字典层(const 常量)
- 模拟数据层(mock 数组)
- 统计工具函数层
- 枚举定义层
- UI 组件层(@Entry 入口 + 各业务 @Component)
这种分层方式让阅读者能够按照"数据如何定义 → 数据如何配置 → 数据如何展示"的逻辑顺序顺畅地理解整个应用。
二、类型系统与数据建模
2.1 元数据接口定义
应用首先定义了一系列用于描述业务元数据结构的 TypeScript 接口。这些接口本身不承载业务数据,而是规定了各类配置项的"形状",为后续的配置字典提供类型约束。
interface ServiceMeta {
label: string
icon: string
color: string
bg: string
}
interface RepairStatusMeta {
label: string
color: string
bg: string
step: number
}
interface RepairTypeMeta {
label: string
icon: string
color: string
}

上述代码定义了三个核心元数据接口:
ServiceMeta:描述首页服务网格中每一项的展示属性,包含label(文字标签)、icon(Emoji 图标)、color(主色)和bg(背景色)四个字段。这种将视觉属性与业务标识分离的设计,使得换肤或调整视觉风格时只需修改配置而无需改动 UI 代码。RepairStatusMeta:在服务元数据的基础上增加了step字段,用于表示该状态在工单流转流程中的序号(0 表示初始态,数值越大表示越靠后)。这个step字段在后续渲染横向时间线进度条时起到了关键作用——通过比较当前工单的step与目标节点的step,即可判断该节点是否已完成。RepairTypeMeta:描述报修分类的展示信息,相对精简,只包含标签、图标和主色。
类似的,还有 BillTypeMeta(账单类型)、NoticeLevelMeta(公告级别)、NoticeTypeMeta(公告类型)、QuickServiceMeta(快捷服务)等接口,它们的结构思路一致,均围绕"如何把一个业务枚举值映射为一组视觉属性"这一目标展开。
其中 QuickServiceMeta 多了一个 desc 字段用于展示描述文案:
interface QuickServiceMeta {
label: string
icon: string
desc: string
color: string
bg: string
}

desc 字段在首页快捷服务列表中以副标题形式呈现,例如"一键提交维修申请""生成访客通行码"等,帮助用户快速理解每项服务的具体用途。
2.2 柱状图数据与色彩方案接口
为了让缴费模块的图表渲染具备类型安全,应用专门定义了柱状图数据接口:
interface BarDataMeta {
label: string
value: number
color: string
}

BarDataMeta 包含三个字段:label 是横轴标签(如"1月"“物业费”),value 是数值,color 是该柱条的颜色。由于每个月/每个类目可以指定不同的颜色,图表能够通过颜色编码传递额外的语义信息,例如用深浅区分普通月份和异常月份。
色彩方案接口则集中定义了整个应用的调色板契约:
interface PropertyColorScheme {
primary: string
primaryDark: string
primaryLight: string
accent: string
accentLight: string
bg: string
cardBg: string
textPrimary: string
textSecondary: string
textHint: string
border: string
success: string
warning: string
danger: string
white: string
}

这个接口的字段非常丰富,覆盖了主色系(primary/primaryDark/primaryLight)、强调色(accent/accentLight)、背景色(bg/cardBg)、文本色阶(textPrimary/textSecondary/textHint 三级灰度)、语义色(success/warning/danger)以及边框色和纯白色。这种细粒度的色彩分层,使得开发者可以精确表达"这是一个主要操作按钮"还是"这是一个辅助提示文字",避免了颜色使用的随意性。
2.3 物业基础信息接口
interface PropertyInfoMeta {
community: string
address: string
units: number
residents: number
area: string
builtYear: string
}

PropertyInfoMeta 封装了小区的静态档案信息,包括小区名称、地址、栋数、住户数、占地面积和建成年份。这些信息在首页信息卡片和个人中心房屋信息中多处复用,集中定义避免了数据散落各处导致的不一致风险。
2.4 可观察数据模型
应用的核心业务数据模型有三个:报修单 RepairItem、缴费账单 BillItem、公告 NoticeItem。它们都使用了 @Observed 装饰器:
@Observed
export class RepairItem {
id: number = 0
type: string = ''
description: string = ''
status: string = ''
date: string = ''
handler: string = ''
location: string = ''
priority: string = '普通'
contact: string = ''
constructor(id: number, type: string, description: string, status: string,
date: string, handler: string, location: string,
priority: string, contact: string) {
this.id = id; this.type = type; this.description = description
this.status = status; this.date = date; this.handler = handler
this.location = location; this.priority = priority; this.contact = contact
}
}

逐行分析这段代码:
@Observed装饰器是 ArkUI 框架提供的,它告诉框架这个类的实例属性是可被观察的。当该实例被@State或@ObjectLink变量引用时,其属性的变化会自动触发依赖该属性的 UI 组件重新渲染。这是实现深度响应式更新的关键。export关键字使得该类可以被其他模块导入复用。- 每个字段在声明时都赋予了默认值(如
id: number = 0、type: string = ''),这是 TypeScript 的严格实践,确保即使通过new RepairItem()不传参也能得到一个结构合法的对象,避免 undefined 导致的运行时错误。 priority的默认值设为'普通'而非空字符串,体现了业务语义——大多数报修的优先级就是普通,预设默认值简化了调用方代码。- 构造函数接收全部 9 个参数并逐一赋值。注意赋值语句采用分号分隔的紧凑写法(
this.id = id; this.type = type; ...),这在字段较多时能够减少代码行数,是 ArkTS 代码中常见的风格。
BillItem 和 NoticeItem 的结构与之类似:
@Observed
export class BillItem {
id: number = 0
type: string = ''
amount: number = 0
date: string = ''
status: string = ''
period: string = ''
paidDate: string = ''
method: string = ''
constructor(id: number, type: string, amount: number, date: string,
status: string, period: string, paidDate: string, method: string) {
this.id = id; this.type = type; this.amount = amount
this.date = date; this.status = status; this.period = period
this.paidDate = paidDate; this.method = method
}
}

BillItem 的字段涵盖了账单的全部维度:amount 是金额(number 类型,便于做金额计算和格式化)、period 是账期(如"2026年7月")、paidDate 是实付日期(未缴费时为空字符串)、method 是支付方式(如"微信支付"“支付宝”)。status 字段使用字符串而非布尔值,因为账单有"已缴费"“未缴费”"逾期"三种状态,布尔值无法表达。
NoticeItem 则额外包含 views(浏览次数,number 类型)和 level(紧急程度)字段,views 数值会在公告列表中以"👁356"的形式展示,帮助用户判断公告的热度。
三、设计令牌与配置字典
3.1 全局色彩方案
设计令牌(Design Token)是现代前端工程中管理视觉规范的最佳实践。本应用将全部色彩集中在一个常量对象中:
const PROPERTY_COLORS: PropertyColorScheme = {
primary: '#455A64',
primaryDark: '#263238',
primaryLight: '#607D8B',
accent: '#546E7A',
accentLight: '#CFD8DC',
bg: '#ECEFF1',
cardBg: '#FFFFFF',
textPrimary: '#263238',
textSecondary: '#607D8B',
textHint: '#B0BEC5',
border: '#ECEFF1',
success: '#2E7D32',
warning: '#F57F17',
danger: '#C62828',
white: '#FFFFFF'
}

这套配色方案属于 Material Design 的 Blue Grey 色系,主色调 #455A64 是一种沉稳的蓝灰色,给人专业、可靠、不浮夸的视觉感受,非常契合物业管理这类需要传递信任感的服务型应用。
逐项解读:
primary(#455A64)是应用的主品牌色,用于顶部标题、主操作按钮、激活态 Tab、关键数据数字等高优先级视觉元素。primaryDark(#263238)是主色的加深版,用于渐变背景的末端颜色,营造层次感。primaryLight(#607D8B)是主色的提亮版,用于次级文本和一些次要图标。accent(#546E7A)是强调色,用于"编辑""保存修改"等次要操作按钮,与主操作按钮形成区分。accentLight(#CFD8DC)是非常浅的蓝灰,用于未选中状态的标签背景、时间线连接线等弱视觉元素。bg(#ECEFF1)是页面整体背景色,比纯白略带灰调,长时间阅读不刺眼。textPrimary/textSecondary/textHint构成了三级文字色阶,分别对应标题正文、辅助说明、占位提示,层级分明。success/warning/danger是语义色,绿色表示成功已完成,橙色表示待处理或警告,红色表示危险或逾期。
3.2 服务类型配置字典
const SERVICE_CONFIG: Record<string, ServiceMeta> = {
'报修': { label: '报修', icon: '🔧', color: '#455A64', bg: '#CFD8DC' },
'缴费': { label: '缴费', icon: '💳', color: '#546E7A', bg: '#ECEFF1' },
'投诉': { label: '投诉', icon: '📋', color: '#78909C', bg: '#E1F5FE' },
'访客': { label: '访客', icon: '👥', color: '#455A64', bg: '#E0F2F1' },
'车位': { label: '车位', icon: '🅿️', color: '#607D8B', bg: '#CFD8DC' },
'快递': { label: '快递', icon: '📦', color: '#546E7A', bg: '#ECEFF1' },
'保洁': { label: '保洁', icon: '🧹', color: '#455A64', bg: '#E0F7FA' },
'公告': { label: '公告', icon: '📢', color: '#37474F', bg: '#CFD8DC' }
}

这是一个以中文字符串为 key 的字典,涵盖了首页服务网格的 8 个入口。使用 Record<string, ServiceMeta> 类型声明确保了每个 value 都必须符合 ServiceMeta 接口的结构,TypeScript 编译器会在编译期校验。
选择 Emoji 作为图标是一个务实的决策——Emoji 是跨平台的标准字符集,无需引入图标字体库或 SVG 资源即可在所有设备上正常显示,大幅降低了资源管理成本。每个服务项的 bg 背景色都经过精心搭配,虽然整体保持蓝灰色调,但通过细微的色相偏移(如投诉项偏青蓝、访客项偏薄荷绿、保洁项偏青色)让网格不至于过于单调。
3.3 报修状态与类型配置
报修状态配置是整个应用中最具设计巧思的字典之一:
const REPAIR_STATUS_CONFIG: Record<string, RepairStatusMeta> = {
'待受理': { label: '待受理', color: '#F57F17', bg: '#FFF8E1', step: 0 },
'处理中': { label: '处理中', color: '#1565C0', bg: '#E3F2FD', step: 1 },
'已完成': { label: '已完成', color: '#2E7D32', bg: '#E8F5E9', step: 2 },
'已关闭': { label: '已关闭', color: '#607D8B', bg: '#ECEFF1', step: 3 },
'已评价': { label: '已评价', color: '#6A1B9A', bg: '#F3E5F5', step: 4 }
}

每个状态都有一组协调的 color(文字色)和 bg(背景色)配色,这些配色遵循了 Material Design 的色板规范——前景色使用较深的色调保证可读性,背景色使用同色系的极浅色调作为底色。例如"待受理"用橙色系(#F57F17 文字 + #FFF8E1 浅黄背景),传达"需要关注但尚未处理"的语义;"已完成"用绿色系,传达"顺利结束"的正面信号。
step 字段是这套配置的灵魂。它将 5 种状态映射为 0 到 4 的有序数值,使得 UI 层可以通过简单的数值比较来判断工单流程推进到了哪一步。在报修详情弹窗的纵向时间线和列表项的横向迷你进度条中,都会用到这个 step 来决定哪些节点应该高亮、哪些应该置灰。
报修类型配置则相对简单:
const REPAIR_TYPE_CONFIG: Record<string, RepairTypeMeta> = {
'水电维修': { label: '水电维修', icon: '💧', color: '#1565C0' },
'电梯故障': { label: '电梯故障', icon: '🛗', color: '#F57F17' },
'门窗维修': { label: '门窗维修', icon: '🚪', color: '#546E7A' },
'公共设施': { label: '公共设施', icon: '🏗️', color: '#455A64' },
'绿化养护': { label: '绿化养护', icon: '🌿', color: '#2E7D32' },
'安全设备': { label: '安全设备', icon: '🔒', color: '#C62828' },
'外墙渗水': { label: '外墙渗水', icon: '🌧️', color: '#0277BD' },
'管道疏通': { label: '管道疏通', icon: '🔧', color: '#6A1B9A' }
}
8 种报修类型覆盖了住宅小区最常见的维修场景。每种类型的图标选择都力求直观——水滴代表水电、电梯图标代表电梯、门代表门窗、树叶代表绿化、锁代表安全设备。color 字段虽然定义了,但在实际 UI 中使用频率不高,主要作为扩展预留。
3.4 账单类型与公告配置
const BILL_TYPE_CONFIG: Record<string, BillTypeMeta> = {
'物业费': { label: '物业费', icon: '🏠', color: '#455A64', bg: '#CFD8DC' },
'水费': { label: '水费', icon: '💧', color: '#1565C0', bg: '#E3F2FD' },
'电费': { label: '电费', icon: '⚡', color: '#F57F17', bg: '#FFF8E1' },
'燃气费': { label: '燃气费', icon: '🔥', color: '#C62828', bg: '#FFEBEE' },
'停车费': { label: '停车费', icon: '🅿️', color: '#546E7A', bg: '#ECEFF1' },
'垃圾处理费': { label: '垃圾处理费', icon: '🗑️', color: '#2E7D32', bg: '#E8F5E9' }
}
6 种账单类型的配色同样遵循语义化原则:水费用蓝色、电费用黄色、燃气费用红色(火焰的危险感)、垃圾处理费用绿色(环保意象),这种"颜色即语义"的设计让用户扫一眼就能区分账单类型。
公告相关配置分为级别和类型两个维度:
const NOTICE_LEVEL_CONFIG: Record<string, NoticeLevelMeta> = {
'紧急': { label: '紧急', color: '#C62828', bg: '#FFEBEE', icon: '🔴' },
'重要': { label: '重要', color: '#F57F17', bg: '#FFF8E1', icon: '🟠' },
'普通': { label: '普通', color: '#455A64', bg: '#ECEFF1', icon: '🔵' },
'一般': { label: '一般', color: '#607D8B', bg: '#CFD8DC', icon: '⚪' }
}
公告级别用红橙蓝灰四色圆形图标直观区分紧急程度,红色圆形(🔴)代表紧急、橙色(🟠)代表重要、蓝色(🔵)代表普通、白色(⚪)代表一般。这种"红绿灯式"的视觉编码符合人类对颜色的本能认知,用户无需阅读文字就能快速判断公告的重要性。
3.5 图表数据与快捷服务
const QUICK_SERVICES: QuickServiceMeta[] = [
{ label: '在线报修', icon: '🔧', desc: '一键提交维修申请', color: '#455A64', bg: '#CFD8DC' },
{ label: '物业缴费', icon: '💳', desc: '在线支付各类费用', color: '#1565C0', bg: '#E3F2FD' },
{ label: '访客邀请', icon: '👥', desc: '生成访客通行码', color: '#2E7D32', bg: '#E8F5E9' },
{ label: '投诉建议', icon: '💬', desc: '反馈问题与建议', color: '#F57F17', bg: '#FFF8E1' }
]
快捷服务数组定义了首页"快捷服务"区块的 4 个入口项。与 SERVICE_CONFIG 字典不同,这里使用数组而非对象,因为快捷服务有固定的展示顺序且每项带有 desc 描述文案。desc 字段让用户在点击前就能了解功能用途,提升了操作的可预期性。
月度和年度缴费数据则是为图表渲染准备的数据源:
const MONTHLY_PAYMENT_DATA: BarDataMeta[] = [
{ label: '1月', value: 580, color: '#455A64' },
{ label: '2月', value: 580, color: '#455A64' },
{ label: '3月', value: 620, color: '#546E7A' },
{ label: '4月', value: 580, color: '#455A64' },
// ... 共12个月
{ label: '12月', value: 650, color: '#546E7A' }
]
12 个月的缴费金额数据,大部分月份是 580 元的固定支出,但 3 月、6 月、9 月、12 月这四个季末月份金额略高(620 或 650 元),暗示了季度性费用的存在(如季度物业费调整或季节性水电费波动)。通过 color 字段将略高的月份用稍亮的 #546E7A 标注,让用户在柱状图中能够一眼识别异常月份。
const ANNUAL_PAYMENT_DATA: BarDataMeta[] = [
{ label: '物业费', value: 3600, color: '#455A64' },
{ label: '水费', value: 960, color: '#1565C0' },
{ label: '电费', value: 1440, color: '#F57F17' },
{ label: '燃气费', value: 720, color: '#C62828' },
{ label: '停车费', value: 2400, color: '#546E7A' },
{ label: '垃圾费', value: 240, color: '#2E7D32' }
]
年度缴费分布数据展示了全年各类费用的总额。物业费(3600 元)和停车费(2400 元)是最大的两块支出,电费(1440 元)次之,垃圾处理费(240 元)最少。每项的颜色与 BILL_TYPE_CONFIG 中的配色保持一致,确保了全应用色彩语义的统一。
3.6 物业基础信息
const PROPERTY_INFO: PropertyInfoMeta = {
community: '翠湖花园小区',
address: '上海市浦东新区张杨路2888号',
units: 12,
residents: 856,
area: '6.8万平方米',
builtYear: '2018年'
}
这段配置定义了应用服务的主体——翠湖花园小区的基础档案。12 栋楼、856 户住户、6.8 万平方米的规模,属于中型住宅社区。2018 年建成意味着这是一个相对较新的小区,配套智能化物业系统具有现实合理性。这些数据在首页信息卡片和个人中心多处展示。
四、模拟数据与统计函数
4.1 报修工单模拟数据
应用预置了 18 条报修记录作为模拟数据,覆盖了全部 8 种报修类型和 5 种工单状态:
const mockRepairs: RepairItem[] = [
new RepairItem(1, '水电维修', '厨房水龙头漏水严重,已经持续3天,需要尽快维修',
'已完成', '2026-08-01', '王师傅', '3栋2单元501室', '紧急', '13800138001'),
new RepairItem(2, '电梯故障', '2栋1单元电梯运行异响,间常卡顿',
'处理中', '2026-08-03', '李工程师', '2栋1单元', '重要', '13800138002'),
new RepairItem(3, '门窗维修', '客厅窗户密封条老化,漏风严重',
'待受理', '2026-08-05', '', '5栋3单元802室', '普通', '13800138003'),
// ... 共18条
]
以第一条为例逐字段分析:
id为 1,是工单的唯一标识。type为'水电维修',对应REPAIR_TYPE_CONFIG中的配置。description为详细的故障描述——“厨房水龙头漏水严重,已经持续3天,需要尽快维修”,描述中包含了故障部位(厨房水龙头)、严重程度(漏水严重)、持续时间(3天)和紧急诉求(需要尽快维修),信息非常完整。status为'已完成',表示该工单已经走完全流程。date为'2026-08-01',是报修提交日期。handler为'王师傅',是物业指派的维修人员姓名。location为'3栋2单元501室',是故障发生的具体位置,精确到门牌号。priority为'紧急',表示优先级最高。contact为联系电话。
注意第三条记录的 handler 字段为空字符串 '',这是因为该工单状态为"待受理",物业尚未指派处理人员。UI 层在渲染时会对空 handler 做特殊处理——显示"待分配"而非空白。这种"用空字符串表示未分配"的设计简单直接,但要求所有消费该字段的代码都做空值判断。
18 条数据中,待受理 5 条、处理中 4 条、已完成 5 条、已关闭 2 条、已评价 2 条,状态分布合理,能够充分展示各种状态下的 UI 表现。
4.2 缴费账单模拟数据
const mockBills: BillItem[] = [
new BillItem(1, '物业费', 300.00, '2026-08-01', '已缴费', '2026年7月', '2026-07-28', '微信支付'),
new BillItem(2, '水费', 48.50, '2026-08-01', '已缴费', '2026年7月', '2026-07-28', '支付宝'),
// ... 已缴费记录
new BillItem(11, '物业费', 300.00, '2026-08-05', '未缴费', '2026年8月', '', ''),
new BillItem(12, '水费', 0, '2026-08-05', '未缴费', '2026年8月', '', ''),
// ... 未缴费记录
new BillItem(15, '停车费', 200.00, '2026-07-15', '逾期', '2026年7月', '', '')
]
15 条账单数据精心设计了三种状态:已缴费(10 条,含完整支付信息)、未缴费(4 条,金额为 0 表示账单尚未出账)、逾期(1 条,金额 200 元的停车费)。逾期记录的 paidDate 和 method 均为空,UI 层会以红色标注金额并显示"⚠️ 逾期"警示标签。
值得注意的是,未缴费的水费、电费、燃气费金额为 0,这符合现实场景——这些费用的实际金额需要等月末抄表后才出账,在账单周期开始时金额尚未确定。而物业费(300 元)作为固定费用则可以提前显示。
4.3 公告模拟数据
const mockNotices: NoticeItem[] = [
new NoticeItem(1, '8月10日小区停水通知', '停水停电', '2026-08-08', '紧急',
'因市政供水管道检修,8月10日8:00-18:00小区将停水,请提前储水。',
'物业服务中心', 356),
new NoticeItem(2, '电梯年检维护安排', '维修通知', '2026-08-06', '重要',
'2栋1单元电梯将于8月12日9:00-12:00进行年度检测维护,届时暂停使用。',
'工程维护部', 234),
// ... 共12条
]
12 条公告覆盖了全部 6 种公告类型(停水停电、维修通知、社区活动、安全提醒、物业公告、缴费提醒)和全部 4 种紧急级别。第一条停水通知被标记为"紧急"级别,内容包含了停水原因(市政供水管道检修)、停水时间(8月10日8:00-18:00)和应对建议(请提前储水),信息完整且具有实操性。views 为 356 表示该公告已被浏览 356 次,反映了紧急公告的高关注度。
4.4 统计工具函数
function getRepairTotalCount(): number { return 18 }
function getRepairPendingCount(): number { return 5 }
function getRepairDoneCount(): number { return 8 }
function getBillTotalCount(): number { return 15 }
function getBillPaidCount(): number { return 10 }
function getBillUnpaidCount(): number { return 5 }
function getNoticeTotalCount(): number { return 12 }
function getAnnualPaymentTotal(): number { return 9360 }
function getMonthlyAverage(): number { return 780 }
这组统计函数目前直接返回硬编码的常量值。这种设计虽然在当前阶段看似"多余"——直接写数字不就行了?——但从工程角度考量是有远见的。将统计逻辑封装为函数后,未来如果数据源从静态 mock 切换为动态接口,只需修改函数内部实现(如改为从 API 获取并计算),而所有调用方代码无需任何改动。这是一种典型的"依赖倒置"实践,用函数调用隔离了数据获取的细节。
getAnnualPaymentTotal() 返回 9360 元,这与 ANNUAL_PAYMENT_DATA 中各项之和(3600+960+1440+720+2400+240=9360)完全吻合,保证了数据的一致性。
五、入口组件与底部导航
5.1 Tab 枚举定义
enum PropertyTab {
HOME = 0,
REPAIR = 1,
PAYMENT = 2,
NOTICE = 3,
PROFILE = 4
}
使用枚举而非魔法数字来定义 Tab 索引,是提升代码可读性的基础实践。PropertyTab.HOME 比 0 更能表达语义,且在重构 Tab 顺序时只需修改枚举定义而无需全文搜索替换。
5.2 入口组件结构
@Entry
@Component
struct PropertyManagerApp {
@State activeTab: PropertyTab = PropertyTab.HOME
@Builder contentArea() {
Column() {
if (this.activeTab === PropertyTab.HOME) {
HomeServiceContent()
} else if (this.activeTab === PropertyTab.REPAIR) {
RepairManageContent()
} else if (this.activeTab === PropertyTab.PAYMENT) {
PaymentRecordContent()
} else if (this.activeTab === PropertyTab.NOTICE) {
NoticeContent()
} else {
ProfileContent()
}
}
.layoutWeight(1)
}
逐行分析:
@Entry装饰器标记这是应用的入口组件,整个页面渲染的起点。@Component装饰器声明这是一个自定义组件,可以被其他组件引用。@State activeTab: PropertyTab = PropertyTab.HOME定义了一个响应式状态变量,初始值为首页。当activeTab的值发生变化时,ArkUI 框架会自动重新执行contentAreaBuilder 中的条件判断,切换显示对应的子组件。@Builder contentArea()是一个构建器方法,用于封装可复用的 UI 片段。这里通过if-else链根据activeTab的值决定渲染哪个子组件。.layoutWeight(1)让内容区域占据除底部导航栏之外的全部剩余空间。
这种"条件渲染 + 状态驱动"的模式是 ArkUI 实现页面切换的标准做法。与传统命令式框架手动 show/hide 或 replace 不同,声明式框架让开发者只需描述"当前状态应该显示什么",框架自动负责 DOM 的创建和销毁。
5.3 底部 Tab 项构建器
@Builder bottomTabItem(icon: string, label: string, tab: PropertyTab) {
Column() {
Text(icon).fontSize(22).opacity(this.activeTab === tab ? 1.0 : 0.4)
Text(label).fontSize(10)
.fontColor(this.activeTab === tab ? PROPERTY_COLORS.primary : PROPERTY_COLORS.textHint)
.fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
if (this.activeTab === tab) {
Column().width(20).height(3)
.backgroundColor(PROPERTY_COLORS.primary).borderRadius(2).margin({ top: 3 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 6, bottom: 6 })
.onClick(() => { this.activeTab = tab })
}
这个 Builder 接收三个参数:图标、标签文字和对应的 Tab 枚举值,生成一个完整的底部导航项。其设计亮点在于通过三元运算符实现了"选中态"与"未选中态"的视觉差异:
- 图标透明度:选中时为 1.0(完全不透明),未选中时降至 0.4(半透明),通过透明度变化引导视觉焦点。
- 标签文字颜色:选中时用主色
primary,未选中时用浅灰色textHint。 - 标签字重:选中时加粗
FontWeight.Bold,未选中时常规FontWeight.Normal。 - 选中指示器:仅当
this.activeTab === tab时才渲染一个 20×3 的圆角短横线作为底部高亮条,通过if条件渲染实现"按需出现"。
.onClick(() => { this.activeTab = tab }) 是整个交互的核心——点击 Tab 项时修改 activeTab 状态,触发框架重新渲染 contentArea,完成页面切换。整个过程无需开发者手动操作 DOM,完全由框架的响应式系统自动完成。
5.4 主布局组装
build() {
Column() {
this.contentArea()
Row() {
this.bottomTabItem('🏠', '首页服务', PropertyTab.HOME)
this.bottomTabItem('🔧', '报修管理', PropertyTab.REPAIR)
this.bottomTabItem('💳', '缴费记录', PropertyTab.PAYMENT)
this.bottomTabItem('📢', '公告通知', PropertyTab.NOTICE)
this.bottomTabItem('👤', '我的', PropertyTab.PROFILE)
}
.width('100%')
.backgroundColor(PROPERTY_COLORS.white)
.padding({ top: 4, bottom: 8 })
.shadow({ radius: 12, color: '#1A000000', offsetY: -3 })
}
.width('100%').height('100%')
.backgroundColor(PROPERTY_COLORS.bg)
}
build() 方法是每个组件的 UI 入口。这里用 Column 纵向排列内容区域和底部导航栏,底部导航栏用 Row 横向排列 5 个 Tab 项。
底部导航栏的视觉处理值得注意:
.backgroundColor(PROPERTY_COLORS.white)白色背景与页面灰底形成对比。.shadow({ radius: 12, color: '#1A000000', offsetY: -3 })添加了一个向上偏移的阴影,#1A000000中的1A是十六进制的 26,对应约 10% 的透明度,营造了导航栏"浮"在内容上方的层次感。offsetY: -3让阴影向上投射,符合导航栏在底部的位置特征。
六、首页服务模块
6.1 服务网格项构建器
@Builder serviceGridItem(serviceKey: string) {
Column() {
Column() {
Text(SERVICE_CONFIG[serviceKey]?.icon ?? '').fontSize(26)
}
.width(50).height(50)
.borderRadius(16)
.backgroundColor(SERVICE_CONFIG[serviceKey]?.bg ?? '#ECEFF1')
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Text(SERVICE_CONFIG[serviceKey]?.label ?? '').fontSize(11)
.fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 14, bottom: 14 })
}
这个 Builder 接收一个 serviceKey 字符串(如 '报修'),从 SERVICE_CONFIG 字典中查询对应的图标和背景色来渲染网格项。
代码中使用了可选链操作符 ?. 和空值合并操作符 ?? 的组合:SERVICE_CONFIG[serviceKey]?.icon ?? ''。这是一种防御性编程的写法——如果 serviceKey 在字典中不存在(返回 undefined),可选链会安全地返回 undefined 而非抛出异常,空值合并则将 undefined 兜底为空字符串。虽然当前所有 key 都是合法的,但这种写法保证了即使传入未知 key 也不会导致渲染崩溃。
图标容器是一个 50×50 的圆角方块(borderRadius(16)),背景色取自配置。alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center) 确保图标在容器中水平垂直居中。
6.2 快捷服务列表项构建器
@Builder quickServiceItem(qs: QuickServiceMeta) {
Row() {
Column() {
Text(qs.icon).fontSize(24)
}
.width(44).height(44).borderRadius(12)
.backgroundColor(qs.bg)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(qs.label).fontSize(14).fontWeight(FontWeight.Medium)
.fontColor(PROPERTY_COLORS.textPrimary)
Text(qs.desc).fontSize(11).fontColor(PROPERTY_COLORS.textSecondary)
.margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
Text('›').fontSize(20).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding(14)
.backgroundColor(PROPERTY_COLORS.white)
.borderRadius(12)
.margin({ left: 14, right: 14, top: 6 })
}
快捷服务项采用"左图标 + 中文案 + 右箭头"的经典列表布局:
- 左侧是 44×44 的圆角图标容器,背景色取自配置。
- 中间是文字区域,包含主标题(
label,14号字中等字重)和副标题(desc,11号字次要文字色),通过.layoutWeight(1)占据剩余空间。 - 右侧是一个
›字符作为"进入"指示符,浅灰色暗示可点击。
.margin({ left: 14, right: 14, top: 6 }) 让每个列表项左右留出 14 的边距,顶部留 6 的间距,形成卡片间的呼吸感。白色背景配合 12 的圆角,让每个快捷服务项看起来像一张独立的卡片。
6.3 首页主布局
首页的 build() 方法较为复杂,由顶部标题栏、可滚动内容区两部分组成。内容区内部又包含了物业信息卡片、服务网格、快捷服务列表、物业动态四个区块。
首先是顶部标题栏:
Row() {
Column() {
Text('翠湖花园').fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
Text(PROPERTY_INFO.address).fontSize(11)
.fontColor(PROPERTY_COLORS.textSecondary).margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text('🔔').fontSize(22)
}
.width(40).height(40).borderRadius(20)
.backgroundColor(PROPERTY_COLORS.white)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
}
.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 12 })
标题栏左侧是小区名称(20号粗体)和地址(11号次要色),右侧是一个 40×40 的圆形通知铃铛按钮。这种"左标题右操作"的布局是移动端页面的标准范式。
接下来是整个首页的视觉焦点——物业信息卡片:
Column() {
Row() {
Text('🏢').fontSize(28)
Column() {
Text(PROPERTY_INFO.community).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.white)
Text(PROPERTY_INFO.builtYear + '建成 · ' + PROPERTY_INFO.area)
.fontSize(11).fontColor('#B0BEC5').margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
Column() {
Text('优秀').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('物业评级').fontSize(9).fontColor('#B0BEC5')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row() {
Column() {
Text(PROPERTY_INFO.units.toString()).fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('栋数').fontSize(10).fontColor('#B0BEC5').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
// ... 住户、满意度、服务时长
}
.width('100%').margin({ top: 16 })
}
.width('100%').padding(16)
.linearGradient({
angle: 135,
colors: [['#455A64', 0.0], ['#37474F', 1.0]]
})
.borderRadius(16)
.margin({ left: 14, right: 14, top: 4 })
.shadow({ radius: 10, color: '#33455A64', offsetY: 4 })
这张卡片是首页的"门面",设计非常精致:
- 使用
linearGradient添加了 135 度角的线性渐变背景,从#455A64(主色)渐变到#37474F(深主色),营造出沉稳大气的深色卡片效果。135 度角意味着渐变方向从左上到右下,这是移动端卡片渐变的常用角度。 - 卡片内文字全部使用白色(
#FFFFFF)和浅蓝灰(#B0BEC5),在深色背景上保证了良好的对比度。 - 第一行展示小区名称、建成年份和面积,右侧以大字号显示"优秀"评级。
- 第二行用四等分布局展示栋数(12)、住户(856)、满意度(98.5%)、服务时长(24h)四项关键指标,每项都是大数字+小标签的组合。
.shadow({ radius: 10, color: '#33455A64', offsetY: 4 })添加了带主色调的阴影(#33455A64中的33是约 20% 透明度),让卡片产生"浮"在页面上的视觉效果,且阴影颜色与卡片主色协调而非生硬的黑色。
服务网格部分通过两行四列的方式排布 8 个服务入口:
Row() {
this.serviceGridItem('报修')
this.serviceGridItem('缴费')
this.serviceGridItem('投诉')
this.serviceGridItem('访客')
}
.width('100%')
Row() {
this.serviceGridItem('车位')
this.serviceGridItem('快递')
this.serviceGridItem('保洁')
this.serviceGridItem('公告')
}
.width('100%')
由于每个 serviceGridItem 内部使用了 .layoutWeight(1),8 个项目会自动均分两行的宽度,形成标准的 4×2 网格。
物业动态部分是一个时间线式的信息流:
Row() {
Column() {
Text('🟢').fontSize(10)
}.width(8).alignItems(HorizontalAlign.Center)
Text('小区南门道闸维修完成,已恢复正常使用').fontSize(12)
.fontColor(PROPERTY_COLORS.textSecondary).layoutWeight(1).margin({ left: 6 })
Text('2小时前').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding({ top: 8, bottom: 8 })
Divider().color(PROPERTY_COLORS.border)
每条动态由状态圆点(绿/蓝/橙表示不同状态)、动态文案和相对时间组成,条目之间用 Divider 分隔线隔开。状态圆点的颜色暗示了动态的紧迫程度——绿色表示已完成、蓝色表示进行中、橙色表示需关注。
整个内容区被包裹在 Scroll() 组件中,.layoutWeight(1).scrollBar(BarState.Off) 让滚动区域占满剩余空间并隐藏滚动条,保证视觉简洁。
七、报修管理模块
报修管理是整个应用中功能最复杂、代码量最大的模块,涵盖了完整的工单 CRUD(增删改查)流程和丰富的弹窗交互。
7.1 状态变量定义
@State selectedStatus: string = '全部'
@State showAddModal: boolean = false
@State showDetailModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteModal: boolean = false
@State selectedRepair: RepairItem | null = null
@State formType: string = '水电维修'
@State formPriority: string = '普通'
@State formLocation: string = ''
@State formDescription: string = ''
@State formContact: string = ''
这些状态变量分为三类:
- 筛选状态:
selectedStatus记录当前选中的状态筛选条件,初始为"全部"。 - 弹窗显隐控制:
showAddModal、showDetailModal、showEditModal、showDeleteModal四个布尔值分别控制新增、详情、编辑、删除四个弹窗的显示与隐藏。使用独立的布尔值而非单一变量,是因为某些场景下需要同时显示多个弹窗(如在详情弹窗中点击"编辑"会叠加编辑弹窗)。 - 表单数据:
formType、formPriority、formLocation、formDescription、formContact存储新增报修表单的输入值。 - 选中对象:
selectedRepair使用联合类型RepairItem | null,null表示未选中任何工单,非null时指向当前操作的工单对象。
7.2 遮罩层构建器
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(38,50,56,0.55)')
.onClick(onClose)
}
这是一个通用的弹窗遮罩层构建器,接收一个 onClose 回调函数。遮罩层是一个铺满全屏的 Column,背景色 rgba(38,50,56,0.55) 是主色 #263238 的 55% 透明度版本,营造出"底层内容被半透明蒙层覆盖"的视觉效果。点击遮罩层会触发 onClose 回调关闭弹窗,这是移动端弹窗的标准交互——点击蒙层关闭比强制点击关闭按钮更符合用户习惯。
7.3 新增报修弹窗
新增报修弹窗是表单交互的核心,包含了类型选择、优先级选择、位置输入、问题描述、联系电话五个表单字段。
弹窗的整体结构采用"遮罩层 + 内容卡片"的层叠布局:
@Builder addRepairModal() {
Column() {
this.modalOverlay(() => { this.showAddModal = false })
Column() {
Row() {
Text('新增报修单').fontSize(17).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(PROPERTY_COLORS.textHint)
.onClick(() => { this.showAddModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color(PROPERTY_COLORS.border)
// ... 表单内容
}
.width('90%').constraintSize({ maxHeight: '82%' })
.backgroundColor('#FFFFFF').borderRadius(18)
.alignItems(HorizontalAlign.Center)
.position({ x: '5%', y: '9%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
分析这个弹窗的布局策略:
- 最外层
Column铺满全屏(width('100%').height('100%')),使用position({ x: 0, y: 0 })和zIndex(999)确保弹窗层级最高,覆盖在页面其他内容之上。 - 内部先渲染
modalOverlay(遮罩层),再渲染内容卡片。由于Column默认从上到下排列,遮罩层和卡片会重叠——但实际上这里利用了position绝对定位的特性,遮罩层铺满全屏,卡片通过position({ x: '5%', y: '9%' })定位在屏幕上方 9% 处。 - 卡片宽度为 90%(
width('90%')),配合position({ x: '5%' })实现水平居中。 constraintSize({ maxHeight: '82%' })限制卡片最大高度不超过屏幕的 82%,防止表单内容过多时撑满全屏。borderRadius(18)较大的圆角让弹窗看起来更柔和现代。
报修类型选择器采用了分两行展示的标签组设计:
Row() {
ForEach(REPAIR_TYPES.slice(0, 4), (t: string) => {
if (this.formType === t) {
Text(REPAIR_TYPE_CONFIG[t]?.icon + ' ' + t)
.fontSize(11).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary)
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(14).margin({ left: 3, right: 3 })
} else {
Text(REPAIR_TYPE_CONFIG[t]?.icon + ' ' + t)
.fontSize(11).fontColor(PROPERTY_COLORS.primary)
.backgroundColor(PROPERTY_COLORS.accentLight)
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(14).margin({ left: 3, right: 3 })
.onClick(() => { this.formType = t })
}
})
}
REPAIR_TYPES.slice(0, 4) 取前 4 种类型显示在第一行,slice(4, 8) 取后 4 种显示在第二行。每个标签通过 if-else 判断是否为当前选中项:
- 选中态:白色文字 + 主色背景,不可点击(没有
onClick)。 - 未选中态:主色文字 + 浅色背景,可点击切换。
标签文本由图标和类型名拼接而成(REPAIR_TYPE_CONFIG[t]?.icon + ' ' + t),例如"💧 水电维修"。borderRadius(14) 配合 padding 让标签呈胶囊形。
优先级选择器、位置输入框、问题描述文本域、联系电话输入框的结构类似,均采用"标题标签 + 输入控件"的组合。其中 TextArea 用于多行文本输入(问题描述),TextInput 用于单行输入(位置、电话),区分使用保证了输入体验的合理性。
7.4 报修详情弹窗与时间线
详情弹窗是报修管理中信息密度最高的界面,展示了工单的完整信息和处理进度时间线。
弹窗头部用图标、类型、日期和状态标签组合展示工单概要:
Row() {
Text(REPAIR_TYPE_CONFIG[this.selectedRepair?.type ?? '水电维修']?.icon ?? '🔧')
.fontSize(28)
Column() {
Text(this.selectedRepair?.type ?? '').fontSize(15).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
Text(this.selectedRepair?.date ?? '').fontSize(11)
.fontColor(PROPERTY_COLORS.textSecondary).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
Column() {
Text(REPAIR_STATUS_CONFIG[this.selectedRepair?.status ?? '待受理']?.label ?? '待受理')
.fontSize(11).fontColor(REPAIR_STATUS_CONFIG[this.selectedRepair?.status ?? '待受理']?.color ?? '#888888')
.backgroundColor(REPAIR_STATUS_CONFIG[this.selectedRepair?.status ?? '待受理']?.bg ?? '#ECEFF1')
.padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(10)
}
Text('✕').fontSize(18).fontColor(PROPERTY_COLORS.textHint)
.margin({ left: 8 })
.onClick(() => { this.showDetailModal = false })
}
这段代码大量使用了 this.selectedRepair?.xxx ?? '默认值' 的模式。由于 selectedRepair 可能是 null(虽然进入详情弹窗时一定非空,但 TypeScript 无法静态保证),每次访问其属性都需要空值兜底。这是类型安全的代价,但也保证了代码的健壮性。
状态标签的颜色和背景色动态取自 REPAIR_STATUS_CONFIG,根据工单当前状态自动适配配色——待受理显示橙色、处理中显示蓝色、已完成显示绿色,无需写任何条件判断。
时间线是详情弹窗最精彩的部分:
// 时间线节点1
Row() {
Column() {
Column().width(12).height(12).borderRadius(6)
.backgroundColor(PROPERTY_COLORS.primary)
Column().width(2).height(28)
.backgroundColor(PROPERTY_COLORS.accentLight)
}
.width(12)
Column() {
Text('已提交报修').fontSize(12).fontWeight(FontWeight.Medium)
.fontColor(PROPERTY_COLORS.textPrimary)
Text(this.selectedRepair?.date ?? '').fontSize(10)
.fontColor(PROPERTY_COLORS.textHint).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).padding({ left: 10, bottom: 8 })
}
每个时间线节点由两部分组成:左侧的"圆点+连接线"和右侧的文字说明。圆点是一个 12×12 的圆形(borderRadius(6)),连接线是 2×28 的竖线。第一个节点(已提交)始终高亮为 primary 主色,因为任何工单都已提交。
后续节点通过 step 比较来决定高亮状态:
Column().width(12).height(12).borderRadius(6)
.backgroundColor((REPAIR_STATUS_CONFIG[this.selectedRepair?.status ?? '']?.step ?? 0) >= 1
? PROPERTY_COLORS.primary : PROPERTY_COLORS.accentLight)
这段代码的逻辑是:获取当前工单状态的 step 值,如果大于等于目标节点的 step(这里是 1),则圆点显示主色(已完成),否则显示浅色(未到达)。文字颜色和字重也同步变化——已完成节点的文字用 textPrimary 深色且加粗,未到达节点用 textHint 浅色且常规。
最后一个节点(维修完成)使用 success 绿色而非 primary 主色,在视觉上传达"最终完成"的特殊语义:
Column().width(12).height(12).borderRadius(6)
.backgroundColor((REPAIR_STATUS_CONFIG[this.selectedRepair?.status ?? '']?.step ?? 0) >= 2
? PROPERTY_COLORS.success : PROPERTY_COLORS.accentLight)
7.5 编辑与删除弹窗
编辑弹窗的结构与新增弹窗类似,但表单字段会预填当前工单的数据。它还增加了一个状态选择器,允许物业人员修改工单状态:
Row() {
ForEach(REPAIR_STATUSES, (s: string) => {
if ((this.selectedRepair?.status ?? '') === s) {
Text(s).fontSize(11).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12).margin({ left: 3, right: 3 })
} else {
Text(s).fontSize(11).fontColor(PROPERTY_COLORS.primary)
.backgroundColor(PROPERTY_COLORS.bg)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12).margin({ left: 3, right: 3 })
}
})
}
注意这里的状态选择器没有 onClick 回调——这意味着编辑弹窗目前是展示性的,状态切换功能尚未完全实现。这是演示型应用的常见做法,UI 完整但交互逻辑留待接入真实后端时补全。
删除确认弹窗采用了"警示图标 + 确认信息 + 双按钮"的标准模式:
Column() {
Column() {
Text('⚠️').fontSize(44).margin({ top: 24 })
}
.width(72).height(72).borderRadius(36)
.backgroundColor('#FFEBEE')
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Text('确认删除此报修记录?').fontSize(17).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary).margin({ top: 16 })
Text('删除后记录将不可恢复').fontSize(12).fontColor(PROPERTY_COLORS.danger)
.margin({ top: 4 })
// ... 工单信息预览
Row() {
Text('取消').fontSize(14).fontColor(PROPERTY_COLORS.textSecondary)
.backgroundColor(PROPERTY_COLORS.bg).borderRadius(22)
.padding({ left: 32, right: 32, top: 11, bottom: 11 })
.onClick(() => { this.showDeleteModal = false })
Text('确认删除').fontSize(14).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.danger).borderRadius(22)
.padding({ left: 32, right: 32, top: 11, bottom: 11 })
.margin({ left: 12 })
.onClick(() => { this.showDeleteModal = false })
}
}
删除弹窗的设计要点:
- 顶部 72×72 的圆形红色背景容器内放置⚠️警示图标,红色背景(
#FFEBEE)与 danger 色系呼应,第一时间传达"危险操作"的信号。 - 主标题"确认删除此报修记录?"明确告知操作性质。
- 副标题"删除后记录将不可恢复"用
danger红色强调后果的不可逆性。 - 底部双按钮采用"取消(灰色)+ 确认删除(红色)"的布局,确认按钮用红色进一步强化危险操作的视觉警示。两个按钮的
padding较大(left: 32, right: 32),保证触摸区域足够大。
7.6 报修列表项构建器
列表项是报修管理页的核心展示单元,包含了工单概要和横向迷你进度条:
@Builder repairItemBuilder(r: RepairItem) {
Column() {
Row() {
Column() {
Text(REPAIR_TYPE_CONFIG[r.type]?.icon ?? '🔧').fontSize(22)
}
.width(44).height(44).borderRadius(14)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Row() {
Text(REPAIR_TYPE_CONFIG[r.type]?.label ?? r.type).fontSize(14)
.fontWeight(FontWeight.Medium).fontColor(PROPERTY_COLORS.textPrimary)
Text('#' + r.id).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
.margin({ left: 6 })
}
Text(r.description).fontSize(11).fontColor(PROPERTY_COLORS.textSecondary)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 3 })
Row() {
Text('📍' + r.location).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('📅' + r.date).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
.margin({ left: 10 })
}
.margin({ top: 4 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
Column() {
Text(REPAIR_STATUS_CONFIG[r.status]?.label ?? r.status)
.fontSize(10).fontColor(REPAIR_STATUS_CONFIG[r.status]?.color ?? '#888888')
.backgroundColor(REPAIR_STATUS_CONFIG[r.status]?.bg ?? '#ECEFF1')
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
if (r.handler !== '') {
Text('👤' + r.handler).fontSize(9).fontColor(PROPERTY_COLORS.textHint)
.margin({ top: 4 })
}
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12)
// 状态时间线(横向迷你版)
Row() {
ForEach([0, 1, 2, 3], (step: number) => {
Column() {
Column().width(8).height(8).borderRadius(4)
.backgroundColor((REPAIR_STATUS_CONFIG[r.status]?.step ?? 0) >= step
? PROPERTY_COLORS.primary : PROPERTY_COLORS.accentLight)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
if (step < 3) {
Column().height(2).layoutWeight(1)
.backgroundColor((REPAIR_STATUS_CONFIG[r.status]?.step ?? 0) > step
? PROPERTY_COLORS.primary : PROPERTY_COLORS.accentLight)
}
})
}
.width('100%').padding({ left: 12, right: 12, bottom: 10 })
}
.width('100%').backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).margin({ left: 14, right: 14, top: 6 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
.onClick(() => { this.selectedRepair = r; this.showDetailModal = true })
}
这是整个应用中信息密度最高的列表项设计。逐部分分析:
上半部分(Row 布局):
- 左侧 44×44 圆角图标容器,显示报修类型的 Emoji 图标。
- 中间区域(
layoutWeight(1))包含三行信息:第一行是类型名称+工单编号(#1),第二行是问题描述(单行显示,超出部分用省略号截断TextOverflow.Ellipsis),第三行是位置和日期。 - 右侧是状态标签(动态配色)和处理人信息。注意
if (r.handler !== '')的条件渲染——只有已分配处理人的工单才显示处理人,避免显示空信息。
下半部分(横向迷你时间线):
这是列表项最具创新性的设计。通过 ForEach([0, 1, 2, 3]) 遍历 4 个步骤节点,每个节点是一个 8×8 的圆点,节点之间用高度为 2 的横线连接。圆点和连接线的颜色根据当前工单的 step 值动态决定:
>= step用主色高亮(已完成节点)> step用主色高亮(已跨越的连接线)- 否则用浅色(未到达)
这种设计让用户无需打开详情就能在列表层面快速感知每个工单的进度,是一种高效的信息压缩展示。
整体样式:
- 白色背景 + 14 圆角 + 轻微阴影(
radius: 4, color: '#0D000000', offsetY: 2),形成浮起的卡片效果。 .onClick将选中的工单赋值给selectedRepair并打开详情弹窗。
7.7 报修管理主布局
build() {
Stack() {
Column() {
// 顶部标题
Row() {
Text('报修管理').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
Column().layoutWeight(1)
Text('+ 新增').fontSize(13).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary).borderRadius(16)
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.onClick(() => { this.showAddModal = true })
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
// 统计卡片
Row() {
Column() {
Text(getRepairTotalCount().toString()).fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.primary)
Text('总报修').fontSize(10).fontColor(PROPERTY_COLORS.textSecondary)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(30).backgroundColor(PROPERTY_COLORS.accentLight)
Column() {
Text(getRepairPendingCount().toString()).fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.warning)
Text('处理中').fontSize(10).fontColor(PROPERTY_COLORS.textSecondary)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(30).backgroundColor(PROPERTY_COLORS.accentLight)
Column() {
Text(getRepairDoneCount().toString()).fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.success)
Text('已完成').fontSize(10).fontColor(PROPERTY_COLORS.textSecondary)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
// ... 状态筛选 + 列表
}
.width('100%').height('100%')
if (this.showAddModal) { this.addRepairModal() }
if (this.showDetailModal) { this.detailRepairModal() }
if (this.showEditModal) { this.editRepairModal() }
if (this.showDeleteModal) { this.deleteRepairModal() }
}
.width('100%').height('100%')
}
报修管理页的主布局使用了 Stack(层叠布局)而非 Column。Stack 的特性是子元素会层叠堆放,后声明的子元素覆盖在先声明的之上。这里的主内容 Column 在底层,四个弹窗根据各自的布尔状态条件性地渲染在上层。
统计卡片用三等分布局展示总报修数(18,主色)、处理中数(5,橙色)、已完成数(8,绿色),三组数字用不同语义色区分,中间用 1×30 的竖线分隔。这种"数字+标签+分隔线"的设计是数据概览卡片的标准模式。
状态筛选条使用横向滚动的 Scroll + Row:
Scroll() {
Row() {
ForEach(['全部', '待受理', '处理中', '已完成', '已关闭', '已评价'], (s: string) => {
if (this.selectedStatus === s) {
Text(s).fontSize(11).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14).margin({ left: 4, right: 4 })
} else {
Text(s).fontSize(11).fontColor(PROPERTY_COLORS.primary)
.backgroundColor(PROPERTY_COLORS.white)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14).margin({ left: 4, right: 4 })
.onClick(() => { this.selectedStatus = s })
}
})
}
.padding({ left: 10, right: 10 })
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(36)
.scrollable(ScrollDirection.Horizontal) 启用横向滚动,.scrollBar(BarState.Off) 隐藏滚动条,.height(36) 固定高度。当筛选标签较多时可以横向滑动查看,避免了在小屏幕上标签拥挤的问题。
报修列表区域通过 18 次 this.repairItemBuilder(mockRepairs[n]) 调用渲染全部工单。虽然这种逐条手动调用的方式略显冗长(而非使用 ForEach),但在 ArkUI 中这种写法能够确保每条数据的渲染都是确定性的,便于调试。
八、缴费记录模块
8.1 缴费弹窗
缴费弹窗模拟了在线支付的确认流程:
@Builder payBillModal() {
Column() {
this.modalOverlay(() => { this.showPayModal = false })
Column() {
Row() {
Text('确认缴费').fontSize(17).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(PROPERTY_COLORS.textHint)
.onClick(() => { this.showPayModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color(PROPERTY_COLORS.border)
Column() {
Text(BILL_TYPE_CONFIG[this.selectedBill?.type ?? '物业费']?.icon + ' ' + (this.selectedBill?.type ?? ''))
.fontSize(16).fontWeight(FontWeight.Bold).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 20 })
Text('¥' + (this.selectedBill?.amount ?? 0).toFixed(2)).fontSize(36)
.fontWeight(FontWeight.Bold).fontColor(PROPERTY_COLORS.primary)
.margin({ top: 8 })
Text('账期:' + (this.selectedBill?.period ?? '')).fontSize(12)
.fontColor(PROPERTY_COLORS.textSecondary).margin({ top: 6 })
// ... 账单编号、缴费方式、优惠、实付金额
}
// ... 取消和立即支付按钮
}
.width('85%').backgroundColor('#FFFFFF').borderRadius(18)
.alignItems(HorizontalAlign.Center)
.position({ x: '7.5%', y: '22%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
缴费弹窗的视觉焦点是中央的大号金额显示:
.fontSize(36)的特大字号让金额成为视觉重心。.toFixed(2)确保金额始终显示两位小数(如300.00),符合财务显示规范。¥符号前缀明确了货币单位。
弹窗下半部分用多组 Row + Divider 构成明细列表,展示账单编号、缴费方式、优惠和实付金额。每组都是"左侧标签 + 右侧数值"的对照布局,Divider 分隔线让各项明细清晰可辨。
8.2 账单列表项构建器
@Builder billItemBuilder(b: BillItem) {
Row() {
Column() {
Text(BILL_TYPE_CONFIG[b.type]?.icon ?? '💳').fontSize(22)
}
.width(44).height(44).borderRadius(14)
.backgroundColor(BILL_TYPE_CONFIG[b.type]?.bg ?? PROPERTY_COLORS.accentLight)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(BILL_TYPE_CONFIG[b.type]?.label ?? b.type).fontSize(14)
.fontWeight(FontWeight.Medium).fontColor(PROPERTY_COLORS.textPrimary)
Text(b.period).fontSize(11).fontColor(PROPERTY_COLORS.textSecondary)
.margin({ top: 2 })
Row() {
Text(b.date).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
if (b.paidDate !== '') {
Text('· 已付:' + b.paidDate).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
.margin({ left: 6 })
}
}
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
Column() {
Text('¥' + b.amount.toFixed(2)).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(b.status === '逾期' ? PROPERTY_COLORS.danger : PROPERTY_COLORS.textPrimary)
if (b.status === '已缴费') {
Text('✅ 已缴费').fontSize(10).fontColor(PROPERTY_COLORS.success)
.margin({ top: 4 })
} else if (b.status === '逾期') {
Text('⚠️ 逾期').fontSize(10).fontColor(PROPERTY_COLORS.danger)
.margin({ top: 4 })
} else {
Text('待缴费').fontSize(10).fontColor(PROPERTY_COLORS.warning)
.margin({ top: 4 })
}
if (b.status !== '已缴费' && b.status !== '逾期') {
Text('去缴费').fontSize(10).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary).borderRadius(10)
.padding({ left: 10, right: 10, top: 3, bottom: 3 }).margin({ top: 6 })
}
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12)
.backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).margin({ left: 14, right: 14, top: 6 })
.shadow({ radius: 3, color: '#0D000000', offsetY: 1 })
.onClick(() => {
if (b.status !== '已缴费') {
this.selectedBill = b; this.showPayModal = true
}
})
}
账单列表项的右侧区域根据状态展示不同的内容,这是通过三层 if-else if-else 实现的:
- 已缴费:显示绿色"✅ 已缴费"标签,金额用正常文字色。
- 逾期:显示红色"⚠️ 逾期"标签,金额用
danger红色强调。 - 待缴费:显示橙色"待缴费"标签 + 主色"去缴费"按钮。
.onClick 中有条件判断 if (b.status !== '已缴费'),意味着只有未缴费和逾期的账单点击后才会弹出缴费弹窗,已缴费的账单点击无反应。这种设计避免了用户误操作已完成的账单。
8.3 缴费汇总卡片与柱状图
缴费记录页顶部有一张深色渐变汇总卡片:
Column() {
Row() {
Column() {
Text('年度已缴').fontSize(11).fontColor('#B0BEC5')
Text('¥7,560.00').fontSize(22).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF').margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text('待缴').fontSize(11).fontColor('#B0BEC5')
Text('¥1,800.00').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FFCDD2').margin({ top: 4 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Column() {
Row() {
Column().width('81%').height(6)
.backgroundColor('#FFFFFF').borderRadius(3)
Column().layoutWeight(1)
}
.width('100%').height(6)
.backgroundColor('#44606B').borderRadius(3)
}
.width('100%').margin({ top: 14 })
Text('年度缴费进度 81%').fontSize(10).fontColor('#B0BEC5')
.margin({ top: 6 })
}
.width('100%').padding(16)
.linearGradient({ angle: 135, colors: [['#455A64', 0.0], ['#37474F', 1.0]] })
.borderRadius(16)
.margin({ left: 14, right: 14, bottom: 10 })
.shadow({ radius: 8, color: '#33455A64', offsetY: 3 })
汇总卡片的设计亮点在于进度条的实现:
- 外层是一个
#44606B(主色的加深版)背景的容器,高度 6,圆角 3。 - 内层是一个宽度为
'81%'的白色条,高度 6,圆角 3,嵌套在外层容器中。 - 通过内外两层颜色的对比,形成"已完成 81%"的进度条视觉效果。
- 待缴金额
¥1,800.00使用#FFCDD2(浅红色)而非白色,暗示待缴项需要关注。
月度缴费柱状图是缴费页的数据可视化亮点:
Column() {
Text('📊 月度缴费趋势').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 16, top: 14, bottom: 10 })
Row() {
ForEach(MONTHLY_PAYMENT_DATA, (d: BarDataMeta) => {
Column() {
Text(d.value.toString()).fontSize(8)
.fontColor(PROPERTY_COLORS.textSecondary).margin({ bottom: 2 })
Column()
.width(16)
.height((d.value / this.maxMonthly * 70).toFixed(0) + 'vp')
.backgroundColor(d.color)
.borderRadius({ topLeft: 3, topRight: 3 })
Text(d.label).fontSize(8).fontColor(PROPERTY_COLORS.textHint)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
})
}
.padding({ left: 10, right: 10, bottom: 14 })
}
这个柱状图完全用 ArkUI 的基础组件手工绘制,没有引入任何图表库:
ForEach遍历MONTHLY_PAYMENT_DATA数组(12 个月),每个月渲染一个Column。- 每个
Column从上到下包含:数值标签(8号字)、柱条(Column组件)、月份标签(8号字)。 - 柱条高度通过
(d.value / this.maxMonthly * 70).toFixed(0) + 'vp'动态计算:当前值除以最大值(650)得到比例,乘以 70(最大高度)得到实际高度,.toFixed(0)取整,最后拼接'vp'单位。 - 柱条宽度固定为 16,顶部圆角(
topLeft: 3, topRight: 3)让柱条顶部柔和。 layoutWeight(1)让 12 个月份均分 Row 的宽度。
这种"纯组件绘图"的方式虽然不如专业图表库功能丰富,但对于简单的柱状图来说足够实用,且无需引入额外依赖,保持了应用的轻量性。
九、公告通知模块
9.1 公告详情弹窗
公告详情弹窗展示了公告的完整内容,头部用级别标签传达紧急程度:
Row() {
Text(NOTICE_LEVEL_CONFIG[this.selectedNotice?.level ?? '普通']?.icon + ' ' + (NOTICE_LEVEL_CONFIG[this.selectedNotice?.level ?? '普通']?.label ?? '普通'))
.fontSize(11).fontColor(NOTICE_LEVEL_CONFIG[this.selectedNotice?.level ?? '普通']?.color ?? '#888888')
.backgroundColor(NOTICE_LEVEL_CONFIG[this.selectedNotice?.level ?? '普通']?.bg ?? '#ECEFF1')
.padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(10)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(PROPERTY_COLORS.textHint)
.onClick(() => { this.showDetailModal = false })
}
级别标签从 NOTICE_LEVEL_CONFIG 中动态获取图标、文字、颜色和背景色。例如"紧急"级别会显示为"🔴 紧急",红色文字配浅红背景。这种配置驱动的渲染方式确保了所有级别的视觉表现一致且可维护。
公告正文区域展示了标题、类型标签、日期、浏览量和正文内容:
Text(this.selectedNotice?.title ?? '').fontSize(17).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 20, right: 20, top: 8 })
Row() {
Text(NOTICE_TYPE_CONFIG[this.selectedNotice?.type ?? '']?.icon + ' ' + (NOTICE_TYPE_CONFIG[this.selectedNotice?.type ?? '']?.label ?? ''))
.fontSize(11).fontColor(PROPERTY_COLORS.primary)
.backgroundColor(PROPERTY_COLORS.accentLight)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
Text('📅' + (this.selectedNotice?.date ?? '')).fontSize(10)
.fontColor(PROPERTY_COLORS.textHint).margin({ left: 10 })
Text('👁' + (this.selectedNotice?.views ?? 0)).fontSize(10)
.fontColor(PROPERTY_COLORS.textHint).margin({ left: 10 })
}
类型标签用主色文字配浅色背景,与级别标签形成视觉区分。日期和浏览量用 Emoji 前缀(📅 和 👁)增加辨识度。正文使用 lineHeight(24) 设置行高,保证多段文本的阅读舒适度。
底部操作栏提供"删除公告"和"分享"两个操作:
Row() {
Text('🗑️ 删除公告').fontSize(13).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.danger).borderRadius(18)
.padding({ left: 22, right: 22, top: 9, bottom: 9 })
.onClick(() => { this.showDeleteModal = true })
Text('📢 分享').fontSize(13).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary).borderRadius(18)
.padding({ left: 22, right: 22, top: 9, bottom: 9 })
.margin({ left: 10 })
}
删除按钮用 danger 红色,分享按钮用 primary 主色,两个按钮的视觉权重区分了操作的性质——删除是破坏性操作需要警示,分享是常规操作。
9.2 公告列表项构建器
@Builder noticeItemBuilder(n: NoticeItem) {
Column() {
Row() {
Column() {
Text(NOTICE_TYPE_CONFIG[n.type]?.icon ?? '📢').fontSize(20)
}
.width(40).height(40).borderRadius(12)
.backgroundColor(PROPERTY_COLORS.bg)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Row() {
Text(NOTICE_LEVEL_CONFIG[n.level]?.icon ?? '🔵').fontSize(9)
Text(NOTICE_LEVEL_CONFIG[n.level]?.label ?? n.level).fontSize(9)
.fontColor(NOTICE_LEVEL_CONFIG[n.level]?.color ?? '#888888')
.backgroundColor(NOTICE_LEVEL_CONFIG[n.level]?.bg ?? '#ECEFF1')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6)
.margin({ left: 4 })
Text(NOTICE_TYPE_CONFIG[n.type]?.label ?? n.type).fontSize(9)
.fontColor(PROPERTY_COLORS.primary).margin({ left: 6 })
}
Text(n.title).fontSize(14).fontWeight(FontWeight.Medium)
.fontColor(PROPERTY_COLORS.textPrimary).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 5 })
Row() {
Text(n.date).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('· ' + n.author).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
.margin({ left: 6 })
Text('· 👁' + n.views).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
.margin({ left: 6 })
}
.margin({ top: 4 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
}
.width('100%').padding(12)
}
.width('100%').backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).margin({ left: 14, right: 14, top: 6 })
.shadow({ radius: 3, color: '#0D000000', offsetY: 1 })
.onClick(() => { this.selectedNotice = n; this.showDetailModal = true })
}
公告列表项的信息层级非常丰富:
- 第一行:级别圆点图标 + 级别标签(彩色背景胶囊)+ 类型标签(文字形式)。
- 第二行:公告标题(14号字中等字重,单行省略截断)。
- 第三行:发布日期 + 发布部门 + 浏览次数,用
·分隔。
这种"级别+类型+标题+元信息"的四层信息架构,让用户在列表层面就能快速判断公告的重要性和类型,决定是否需要点开查看详情。
9.3 公告类型筛选
公告页的类型筛选与报修页的状态筛选结构一致,但筛选维度不同:
Scroll() {
Row() {
ForEach(['全部', '停水停电', '维修通知', '社区活动', '安全提醒', '物业公告', '缴费提醒'], (t: string) => {
if (this.selectedType === t) {
Text(t).fontSize(11).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14).margin({ left: 4, right: 4 })
} else {
Text(t).fontSize(11).fontColor(PROPERTY_COLORS.primary)
.backgroundColor(PROPERTY_COLORS.white)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14).margin({ left: 4, right: 4 })
.onClick(() => { this.selectedType = t })
}
})
}
.padding({ left: 10, right: 10 })
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(36)
7 个筛选标签(含"全部")横向排列,选中态与未选中态的视觉差异通过背景色反转实现——选中为白字深底,未选中为深字白底。
十、个人中心模块
10.1 用户信息卡片
个人中心顶部是一张深色渐变的用户信息卡片:
Column() {
Row() {
Column() {
Text('👤').fontSize(36)
}
.width(64).height(64).borderRadius(32)
.backgroundColor(PROPERTY_COLORS.accentLight)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text('刘建国').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('翠湖花园 · 3栋2单元501室').fontSize(12)
.fontColor('#B0BEC5').margin({ top: 3 })
Row() {
Text('业主认证').fontSize(9).fontColor('#FFFFFF')
.backgroundColor('#2E7D32').borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text('VIP会员').fontSize(9).fontColor('#FFFFFF')
.backgroundColor('#F57F17').borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).margin({ left: 6 })
}
.margin({ top: 6 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
}
.width('100%').padding(16)
}
.width('100%')
.linearGradient({ angle: 135, colors: [['#455A64', 0.0], ['#263238', 1.0]] })
.borderRadius(16)
.margin({ left: 14, right: 14, top: 14 })
.shadow({ radius: 10, color: '#33455A64', offsetY: 4 })
用户卡片的设计要点:
- 左侧 64×64 的圆形头像占位符(
borderRadius(32)恰好是宽度的一半,形成正圆),使用 Emoji👤作为默认头像。 - 用户名"刘建国"用 18 号粗体白色显示,下方是小区和门牌信息。
- 两个身份标签——“业主认证”(绿色背景)和"VIP会员"(橙色背景)——传达用户的身份和会员等级。绿色暗示认证已通过,橙色暗示 VIP 的尊贵感。
- 渐变从
#455A64到#263238(比首页卡片更深的渐变末端),让个人中心的视觉权重略高于首页,强调"这是属于用户自己的空间"。
10.2 房屋信息卡片
Column() {
Text('🏠 房屋信息').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 16, top: 14, bottom: 10 })
Row() {
Column() {
Text('小区').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text(PROPERTY_INFO.community).fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('面积').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('128.5㎡').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('户型').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('三室两厅').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
}
.width('100%').padding({ left: 16, right: 16, bottom: 14 })
Row() {
Column() {
Text('车位').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('B2-088').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('入住时间').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('2019年5月').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('物业费标准').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('2.5元/㎡/月').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
}
.width('100%').padding({ left: 16, right: 16, bottom: 14 })
}
房屋信息卡片用两行三列的网格展示了 6 项房屋属性:小区、面积、户型、车位、入住时间、物业费标准。每项都是"小标签(10号字浅灰)+ 大数值(13号字深色)"的上下结构。layoutWeight(1) 确保三列均分宽度。
10.3 统计卡片构建器
@Builder statCard(icon: string, label: string, value: string, color: string) {
Column() {
Text(icon).fontSize(20).margin({ bottom: 4 })
Text(value).fontSize(18).fontWeight(FontWeight.Bold).fontColor(color)
Text(label).fontSize(10).fontColor(PROPERTY_COLORS.textSecondary).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).padding({ top: 16, bottom: 16 })
}
这是一个参数化的统计卡片构建器,接收图标、标签、数值和颜色四个参数。通过传入不同的颜色值,可以让每张卡片传达不同的语义——报修记录用主色、已完成用绿色、缴费次数用强调色、已读公告用橙色。
四张卡片以 2×2 网格排布:
Row() {
this.statCard('🔧', '报修记录', '18', PROPERTY_COLORS.primary)
this.statCard('✅', '已完成', '8', PROPERTY_COLORS.success)
}
.width('100%').padding({ left: 14, right: 14 })
Row() {
this.statCard('💳', '缴费次数', '15', PROPERTY_COLORS.accent)
this.statCard('📢', '已读公告', '10', PROPERTY_COLORS.warning)
}
.width('100%').padding({ left: 14, right: 14, top: 8 })
10.4 年度缴费分布柱状图
个人中心的柱状图与缴费页的月度趋势图实现方式一致,但数据源和维度不同:
Row() {
ForEach(ANNUAL_PAYMENT_DATA, (d: BarDataMeta) => {
Column() {
Text('¥' + d.value.toString()).fontSize(8)
.fontColor(PROPERTY_COLORS.textSecondary).margin({ bottom: 3 })
Column()
.width(20)
.height((d.value / this.maxAnnual * 90).toFixed(0) + 'vp')
.backgroundColor(d.color)
.borderRadius({ topLeft: 4, topRight: 4 })
Text(d.label).fontSize(8).fontColor(PROPERTY_COLORS.textHint)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
})
}
年度缴费分布图展示 6 类费用的全年总额,maxAnnual 为 3600(物业费,最大值)。柱条宽度为 20(比月度图的 16 略宽,因为只有 6 根柱子空间更充裕),最大高度 90。数值标签前加了 ¥ 符号强调金额属性。
柱状图下方还有一个年度总计行:
Row() {
Text('年度总计').fontSize(12).fontColor(PROPERTY_COLORS.textSecondary)
Column().layoutWeight(1)
Text('¥' + getAnnualPaymentTotal().toString()).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.primary)
}
Column().layoutWeight(1) 作为弹性占位符将左右两侧的内容推开,实现"左侧标签 + 右侧数值"的两端对齐布局。
10.5 快捷操作列表
个人中心底部是设置类快捷操作列表:
Column() {
Row() {
Text('🔑').fontSize(18)
Text('修改密码').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.layoutWeight(1).margin({ left: 10 })
Text('›').fontSize(18).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
Divider().color(PROPERTY_COLORS.border)
// ... 消息通知、联系物业、关于我们
}
.width('100%').backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).margin({ left: 14, right: 14 })
.padding({ left: 16, right: 16 })
每行操作采用"图标 + 标题 + 右箭头"的标准设置项布局,行间用 Divider 分隔。4 个操作项分别是修改密码、消息通知、联系物业和关于我们,覆盖了用户最常用的设置需求。
页面最底部是版本信息:
Text('v1.0 · 翠湖花园智慧物业 · 2026').fontSize(10)
.fontColor(PROPERTY_COLORS.textHint)
.alignSelf(ItemAlign.Center).margin({ top: 20, bottom: 20 })
.alignSelf(ItemAlign.Center) 让版本号文字在 Column 中水平居中,10 号字浅灰色处理得非常低调,不干扰主内容。
十一、关键特性对比总结
下表对应用中各模块的关键特性进行了横向对比:
| 模块 | 核心功能 | 数据模型 | 弹窗数量 | 特色交互 | 可视化组件 |
|---|---|---|---|---|---|
| 首页服务 | 服务聚合展示 | PropertyInfoMeta | 无 | 渐变信息卡片、服务网格、动态时间流 | 无 |
| 报修管理 | 工单全生命周期管理 | RepairItem(@Observed) | 4个(新增/详情/编辑/删除) | 横向迷你进度条、纵向时间线、状态筛选 | 统计卡片 |
| 缴费记录 | 账单查看与支付 | BillItem(@Observed) | 1个(缴费确认) | 深色渐变汇总卡、进度条、状态差异化展示 | 月度柱状图 |
| 公告通知 | 通知发布与查看 | NoticeItem(@Observed) | 2个(详情/删除确认) | 级别色彩编码、类型筛选、浏览量展示 | 无 |
| 个人中心 | 用户档案与统计 | 复用全局配置 | 无 | 身份标签、房屋信息网格、年度分布图 | 年度柱状图、统计卡片网格 |
下表总结了应用中使用的主要技术特性及其作用:
| 技术特性 | 使用方式 | 作用与价值 |
|---|---|---|
| @Entry + @Component | 标记入口组件和业务组件 | 声明式组件化开发,组件可独立维护和复用 |
| @State | 管理组件内部响应式状态 | 状态变化自动触发 UI 刷新,无需手动操作 DOM |
| @Observed | 标记 RepairItem/BillItem/NoticeItem | 实现深度对象属性变化的响应式监听 |
| @Builder | 封装可复用 UI 片段 | 减少代码重复,提升可读性和可维护性 |
| Record 字典配置 | SERVICE_CONFIG 等配置对象 | 业务元数据与 UI 渲染解耦,便于扩展和换肤 |
| Design Token | PROPERTY_COLORS 集中色彩管理 | 全应用视觉一致性保障,修改一处全局生效 |
| 三元运算符 | 选中态/未选中态切换 | 简洁的条件渲染,避免冗长的 if-else |
| 可选链 ?. + 空值合并 ?? | selectedRepair?.xxx ?? 默认值 | 空安全防御性编程,避免 undefined 导致崩溃 |
| Stack 层叠布局 | 弹窗与主内容层叠 | 弹窗浮于内容之上,多弹窗可叠加 |
| linearGradient | 深色卡片渐变背景 | 营造视觉层次和质感,提升设计档次 |
| 手工柱状图 | ForEach + 动态高度 Column | 零依赖数据可视化,轻量且灵活 |
| ForEach | 列表和标签组渲染 | 声明式遍历数组生成 UI,数据驱动视图 |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
// ============ 类型定义 ============
interface ServiceMeta {
label: string
icon: string
color: string
bg: string
}
interface RepairStatusMeta {
label: string
color: string
bg: string
step: number
}
interface RepairTypeMeta {
label: string
icon: string
color: string
}
interface BillTypeMeta {
label: string
icon: string
color: string
bg: string
}
interface NoticeLevelMeta {
label: string
color: string
bg: string
icon: string
}
interface NoticeTypeMeta {
label: string
icon: string
color: string
}
interface QuickServiceMeta {
label: string
icon: string
desc: string
color: string
bg: string
}
interface BarDataMeta {
label: string
value: number
color: string
}
interface PropertyColorScheme {
primary: string
primaryDark: string
primaryLight: string
accent: string
accentLight: string
bg: string
cardBg: string
textPrimary: string
textSecondary: string
textHint: string
border: string
success: string
warning: string
danger: string
white: string
}
interface PropertyInfoMeta {
community: string
address: string
units: number
residents: number
area: string
builtYear: string
}
// ============ 报修数据模型 ============
@Observed
export class RepairItem {
id: number = 0
type: string = ''
description: string = ''
status: string = ''
date: string = ''
handler: string = ''
location: string = ''
priority: string = '普通'
contact: string = ''
constructor(id: number, type: string, description: string, status: string, date: string, handler: string, location: string, priority: string, contact: string) {
this.id = id; this.type = type; this.description = description
this.status = status; this.date = date; this.handler = handler
this.location = location; this.priority = priority; this.contact = contact
}
}
// ============ 缴费账单数据模型 ============
@Observed
export class BillItem {
id: number = 0
type: string = ''
amount: number = 0
date: string = ''
status: string = ''
period: string = ''
paidDate: string = ''
method: string = ''
constructor(id: number, type: string, amount: number, date: string, status: string, period: string, paidDate: string, method: string) {
this.id = id; this.type = type; this.amount = amount
this.date = date; this.status = status; this.period = period
this.paidDate = paidDate; this.method = method
}
}
// ============ 公告数据模型 ============
@Observed
export class NoticeItem {
id: number = 0
title: string = ''
type: string = ''
date: string = ''
level: string = ''
content: string = ''
author: string = ''
views: number = 0
constructor(id: number, title: string, type: string, date: string, level: string, content: string, author: string, views: number) {
this.id = id; this.title = title; this.type = type
this.date = date; this.level = level; this.content = content
this.author = author; this.views = views
}
}
// ============ 设计令牌 ============
const PROPERTY_COLORS: PropertyColorScheme = {
primary: '#455A64',
primaryDark: '#263238',
primaryLight: '#607D8B',
accent: '#546E7A',
accentLight: '#CFD8DC',
bg: '#ECEFF1',
cardBg: '#FFFFFF',
textPrimary: '#263238',
textSecondary: '#607D8B',
textHint: '#B0BEC5',
border: '#ECEFF1',
success: '#2E7D32',
warning: '#F57F17',
danger: '#C62828',
white: '#FFFFFF'
}
const SERVICE_CONFIG: Record<string, ServiceMeta> = {
'报修': { label: '报修', icon: '🔧', color: '#455A64', bg: '#CFD8DC' },
'缴费': { label: '缴费', icon: '💳', color: '#546E7A', bg: '#ECEFF1' },
'投诉': { label: '投诉', icon: '📋', color: '#78909C', bg: '#E1F5FE' },
'访客': { label: '访客', icon: '👥', color: '#455A64', bg: '#E0F2F1' },
'车位': { label: '车位', icon: '🅿️', color: '#607D8B', bg: '#CFD8DC' },
'快递': { label: '快递', icon: '📦', color: '#546E7A', bg: '#ECEFF1' },
'保洁': { label: '保洁', icon: '🧹', color: '#455A64', bg: '#E0F7FA' },
'公告': { label: '公告', icon: '📢', color: '#37474F', bg: '#CFD8DC' }
}
const REPAIR_STATUS_CONFIG: Record<string, RepairStatusMeta> = {
'待受理': { label: '待受理', color: '#F57F17', bg: '#FFF8E1', step: 0 },
'处理中': { label: '处理中', color: '#1565C0', bg: '#E3F2FD', step: 1 },
'已完成': { label: '已完成', color: '#2E7D32', bg: '#E8F5E9', step: 2 },
'已关闭': { label: '已关闭', color: '#607D8B', bg: '#ECEFF1', step: 3 },
'已评价': { label: '已评价', color: '#6A1B9A', bg: '#F3E5F5', step: 4 }
}
const REPAIR_TYPE_CONFIG: Record<string, RepairTypeMeta> = {
'水电维修': { label: '水电维修', icon: '💧', color: '#1565C0' },
'电梯故障': { label: '电梯故障', icon: '🛗', color: '#F57F17' },
'门窗维修': { label: '门窗维修', icon: '🚪', color: '#546E7A' },
'公共设施': { label: '公共设施', icon: '🏗️', color: '#455A64' },
'绿化养护': { label: '绿化养护', icon: '🌿', color: '#2E7D32' },
'安全设备': { label: '安全设备', icon: '🔒', color: '#C62828' },
'外墙渗水': { label: '外墙渗水', icon: '🌧️', color: '#0277BD' },
'管道疏通': { label: '管道疏通', icon: '🔧', color: '#6A1B9A' }
}
const BILL_TYPE_CONFIG: Record<string, BillTypeMeta> = {
'物业费': { label: '物业费', icon: '🏠', color: '#455A64', bg: '#CFD8DC' },
'水费': { label: '水费', icon: '💧', color: '#1565C0', bg: '#E3F2FD' },
'电费': { label: '电费', icon: '⚡', color: '#F57F17', bg: '#FFF8E1' },
'燃气费': { label: '燃气费', icon: '🔥', color: '#C62828', bg: '#FFEBEE' },
'停车费': { label: '停车费', icon: '🅿️', color: '#546E7A', bg: '#ECEFF1' },
'垃圾处理费': { label: '垃圾处理费', icon: '🗑️', color: '#2E7D32', bg: '#E8F5E9' }
}
const NOTICE_LEVEL_CONFIG: Record<string, NoticeLevelMeta> = {
'紧急': { label: '紧急', color: '#C62828', bg: '#FFEBEE', icon: '🔴' },
'重要': { label: '重要', color: '#F57F17', bg: '#FFF8E1', icon: '🟠' },
'普通': { label: '普通', color: '#455A64', bg: '#ECEFF1', icon: '🔵' },
'一般': { label: '一般', color: '#607D8B', bg: '#CFD8DC', icon: '⚪' }
}
const NOTICE_TYPE_CONFIG: Record<string, NoticeTypeMeta> = {
'停水停电': { label: '停水停电', icon: '⚡', color: '#C62828' },
'维修通知': { label: '维修通知', icon: '🔧', color: '#F57F17' },
'社区活动': { label: '社区活动', icon: '🎉', color: '#2E7D32' },
'安全提醒': { label: '安全提醒', icon: '🛡️', color: '#1565C0' },
'物业公告': { label: '物业公告', icon: '📢', color: '#455A64' },
'缴费提醒': { label: '缴费提醒', icon: '💳', color: '#6A1B9A' }
}
const QUICK_SERVICES: QuickServiceMeta[] = [
{ label: '在线报修', icon: '🔧', desc: '一键提交维修申请', color: '#455A64', bg: '#CFD8DC' },
{ label: '物业缴费', icon: '💳', desc: '在线支付各类费用', color: '#1565C0', bg: '#E3F2FD' },
{ label: '访客邀请', icon: '👥', desc: '生成访客通行码', color: '#2E7D32', bg: '#E8F5E9' },
{ label: '投诉建议', icon: '💬', desc: '反馈问题与建议', color: '#F57F17', bg: '#FFF8E1' }
]
const MONTHLY_PAYMENT_DATA: BarDataMeta[] = [
{ label: '1月', value: 580, color: '#455A64' },
{ label: '2月', value: 580, color: '#455A64' },
{ label: '3月', value: 620, color: '#546E7A' },
{ label: '4月', value: 580, color: '#455A64' },
{ label: '5月', value: 580, color: '#455A64' },
{ label: '6月', value: 650, color: '#546E7A' },
{ label: '7月', value: 580, color: '#455A64' },
{ label: '8月', value: 580, color: '#455A64' },
{ label: '9月', value: 620, color: '#546E7A' },
{ label: '10月', value: 580, color: '#455A64' },
{ label: '11月', value: 580, color: '#455A64' },
{ label: '12月', value: 650, color: '#546E7A' }
]
const ANNUAL_PAYMENT_DATA: BarDataMeta[] = [
{ label: '物业费', value: 3600, color: '#455A64' },
{ label: '水费', value: 960, color: '#1565C0' },
{ label: '电费', value: 1440, color: '#F57F17' },
{ label: '燃气费', value: 720, color: '#C62828' },
{ label: '停车费', value: 2400, color: '#546E7A' },
{ label: '垃圾费', value: 240, color: '#2E7D32' }
]
const PROPERTY_INFO: PropertyInfoMeta = {
community: '翠湖花园小区',
address: '上海市浦东新区张杨路2888号',
units: 12,
residents: 856,
area: '6.8万平方米',
builtYear: '2018年'
}
const REPAIR_TYPES: string[] = ['水电维修', '电梯故障', '门窗维修', '公共设施', '绿化养护', '安全设备', '外墙渗水', '管道疏通']
const REPAIR_STATUSES: string[] = ['待受理', '处理中', '已完成', '已关闭', '已评价']
const REPAIR_PRIORITIES: string[] = ['紧急', '重要', '普通', '低']
// ============ 全局写死数据:18条报修记录 ============
const mockRepairs: RepairItem[] = [
new RepairItem(1, '水电维修', '厨房水龙头漏水严重,已经持续3天,需要尽快维修', '已完成', '2026-08-01', '王师傅', '3栋2单元501室', '紧急', '13800138001'),
new RepairItem(2, '电梯故障', '2栋1单元电梯运行异响,间常卡顿', '处理中', '2026-08-03', '李工程师', '2栋1单元', '重要', '13800138002'),
new RepairItem(3, '门窗维修', '客厅窗户密封条老化,漏风严重', '待受理', '2026-08-05', '', '5栋3单元802室', '普通', '13800138003'),
new RepairItem(4, '公共设施', '小区东门门禁系统故障,无法识别人脸', '处理中', '2026-08-06', '赵技术员', '东门入口', '重要', '13800138004'),
new RepairItem(5, '绿化养护', '中心花园树木需要修剪,遮挡路灯', '已完成', '2026-07-28', '陈园艺师', '中心花园', '普通', '13800138005'),
new RepairItem(6, '安全设备', '地下车库消防栓漏水,需紧急处理', '已关闭', '2026-07-25', '孙消防员', 'B2层车库', '紧急', '13800138006'),
new RepairItem(7, '外墙渗水', '主卧外墙下雨渗水,墙皮脱落', '处理中', '2026-08-02', '周维修工', '8栋1单元1203室', '重要', '13800138007'),
new RepairItem(8, '管道疏通', '卫生间下水道堵塞,反水严重', '已完成', '2026-07-30', '吴师傅', '6栋2单元301室', '紧急', '13800138008'),
new RepairItem(9, '水电维修', '客厅吊灯线路短路,频繁跳闸', '已评价', '2026-07-20', '王师傅', '1栋3单元1002室', '重要', '13800138009'),
new RepairItem(10, '电梯故障', '3栋2单元电梯门无法完全关闭', '待受理', '2026-08-07', '', '3栋2单元', '重要', '13800138010'),
new RepairItem(11, '门窗维修', '入户门锁芯卡顿,钥匙难以转动', '已完成', '2026-07-22', '郑锁匠', '4栋1单元605室', '普通', '13800138011'),
new RepairItem(12, '公共设施', '小区南门道闸升降缓慢', '处理中', '2026-08-04', '刘维修工', '南门入口', '普通', '13800138012'),
new RepairItem(13, '绿化养护', 'B区草坪大面积枯黄需要补种', '待受理', '2026-08-08', '', 'B区花园', '低', '13800138013'),
new RepairItem(14, '安全设备', '走廊烟感报警器误报频繁', '已完成', '2026-07-18', '赵技术员', '7栋3单元走廊', '重要', '13800138014'),
new RepairItem(15, '外墙渗水', '阳台外墙瓷砖脱落风险', '已关闭', '2026-07-15', '周维修工', '2栋2单元701室', '紧急', '13800138015'),
new RepairItem(16, '管道疏通', '厨房排水管异味严重', '已评价', '2026-07-10', '吴师傅', '5栋1单元402室', '普通', '13800138016'),
new RepairItem(17, '水电维修', '卫生间地漏返味需安装防臭芯', '待受理', '2026-08-09', '', '9栋2单元305室', '低', '13800138017'),
new RepairItem(18, '电梯故障', '5栋3单元电梯内照明灯闪烁', '处理中', '2026-08-06', '李工程师', '5栋3单元', '普通', '13800138018')
]
// ============ 全局写死数据:15条缴费记录 ============
const mockBills: BillItem[] = [
new BillItem(1, '物业费', 300.00, '2026-08-01', '已缴费', '2026年7月', '2026-07-28', '微信支付'),
new BillItem(2, '水费', 48.50, '2026-08-01', '已缴费', '2026年7月', '2026-07-28', '支付宝'),
new BillItem(3, '电费', 126.80, '2026-08-01', '已缴费', '2026年7月', '2026-07-28', '银行卡'),
new BillItem(4, '燃气费', 62.00, '2026-08-01', '已缴费', '2026年7月', '2026-07-28', '微信支付'),
new BillItem(5, '停车费', 200.00, '2026-08-01', '已缴费', '2026年7月', '2026-07-25', '自动扣款'),
new BillItem(6, '垃圾处理费', 20.00, '2026-08-01', '已缴费', '2026年7月', '2026-07-25', '微信支付'),
new BillItem(7, '物业费', 300.00, '2026-07-01', '已缴费', '2026年6月', '2026-06-26', '微信支付'),
new BillItem(8, '水费', 52.30, '2026-07-01', '已缴费', '2026年6月', '2026-06-26', '支付宝'),
new BillItem(9, '电费', 118.50, '2026-07-01', '已缴费', '2026年6月', '2026-06-26', '银行卡'),
new BillItem(10, '燃气费', 58.00, '2026-07-01', '已缴费', '2026年6月', '2026-06-26', '微信支付'),
new BillItem(11, '物业费', 300.00, '2026-08-05', '未缴费', '2026年8月', '', ''),
new BillItem(12, '水费', 0, '2026-08-05', '未缴费', '2026年8月', '', ''),
new BillItem(13, '电费', 0, '2026-08-05', '未缴费', '2026年8月', '', ''),
new BillItem(14, '燃气费', 0, '2026-08-05', '未缴费', '2026年8月', '', ''),
new BillItem(15, '停车费', 200.00, '2026-07-15', '逾期', '2026年7月', '', '')
]
// ============ 全局写死数据:12条公告 ============
const mockNotices: NoticeItem[] = [
new NoticeItem(1, '8月10日小区停水通知', '停水停电', '2026-08-08', '紧急', '因市政供水管道检修,8月10日8:00-18:00小区将停水,请提前储水。', '物业服务中心', 356),
new NoticeItem(2, '电梯年检维护安排', '维修通知', '2026-08-06', '重要', '2栋1单元电梯将于8月12日9:00-12:00进行年度检测维护,届时暂停使用。', '工程维护部', 234),
new NoticeItem(3, '中秋社区联欢活动报名', '社区活动', '2026-08-05', '普通', '中秋佳节将至,物业将举办社区联欢活动,欢迎业主积极报名参加。活动时间:9月15日14:00。', '社区文化部', 512),
new NoticeItem(4, '夏季防火安全温馨提示', '安全提醒', '2026-08-03', '重要', '夏季高温干燥,请勿在楼道堆放杂物,注意用电安全,离开家中请关闭电源。', '安全保卫部', 428),
new NoticeItem(5, '7月物业服务月报发布', '物业公告', '2026-08-01', '普通', '7月物业服务月报已发布,包含维修工单统计、费用收支明细、巡检记录等内容。', '物业服务中心', 189),
new NoticeItem(6, '8月物业费缴纳提醒', '缴费提醒', '2026-07-28', '重要', '8月物业费缴纳周期已开始,请于8月15日前完成缴纳,支持微信、支付宝、银行转账。', '财务部', 367),
new NoticeItem(7, '地下车库消防演练通知', '安全提醒', '2026-07-25', '紧急', '7月30日10:00将在地下车库进行消防演练,届时将有烟雾模拟,请勿恐慌。', '安全保卫部', 298),
new NoticeItem(8, '小区绿化升级改造公告', '物业公告', '2026-07-22', '普通', '小区中心花园将进行绿化升级改造,新增休闲长椅和儿童游乐设施,工期约30天。', '工程维护部', 445),
new NoticeItem(9, '智能门禁系统升级通知', '维修通知', '2026-07-20', '重要', '小区门禁系统将于7月25日升级,升级期间需使用临时通行码,请提前在App申领。', '技术部', 312),
new NoticeItem(10, '高温天气防暑降温提醒', '安全提醒', '2026-07-18', '普通', '近期持续高温,请业主注意防暑降温,减少户外活动,物业服务中心备有防暑药品。', '物业服务中心', 276),
new NoticeItem(11, '社区书法班招生通知', '社区活动', '2026-07-15', '一般', '社区书法兴趣班开始招生,每周六下午14:00-16:00上课,名额有限先到先得。', '社区文化部', 156),
new NoticeItem(12, '垃圾分类新规实施提醒', '物业公告', '2026-07-10', '重要', '根据最新规定,7月15日起小区将严格执行垃圾分类新标准,请各位业主积极配合。', '物业服务中心', 389)
]
// ============ 统计函数 ============
function getRepairTotalCount(): number { return 18 }
function getRepairPendingCount(): number { return 5 }
function getRepairDoneCount(): number { return 8 }
function getBillTotalCount(): number { return 15 }
function getBillPaidCount(): number { return 10 }
function getBillUnpaidCount(): number { return 5 }
function getNoticeTotalCount(): number { return 12 }
function getAnnualPaymentTotal(): number { return 9360 }
function getMonthlyAverage(): number { return 780 }
// ============ 底部 Tab 枚举 ============
enum PropertyTab {
HOME = 0,
REPAIR = 1,
PAYMENT = 2,
NOTICE = 3,
PROFILE = 4
}
// ============ 入口页面 ============
@Entry
@Component
struct PropertyManagerApp {
@State activeTab: PropertyTab = PropertyTab.HOME
@Builder contentArea() {
Column() {
if (this.activeTab === PropertyTab.HOME) {
HomeServiceContent()
} else if (this.activeTab === PropertyTab.REPAIR) {
RepairManageContent()
} else if (this.activeTab === PropertyTab.PAYMENT) {
PaymentRecordContent()
} else if (this.activeTab === PropertyTab.NOTICE) {
NoticeContent()
} else {
ProfileContent()
}
}
.layoutWeight(1)
}
@Builder bottomTabItem(icon: string, label: string, tab: PropertyTab) {
Column() {
Text(icon).fontSize(22).opacity(this.activeTab === tab ? 1.0 : 0.4)
Text(label).fontSize(10)
.fontColor(this.activeTab === tab ? PROPERTY_COLORS.primary : PROPERTY_COLORS.textHint)
.fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
if (this.activeTab === tab) {
Column().width(20).height(3)
.backgroundColor(PROPERTY_COLORS.primary).borderRadius(2).margin({ top: 3 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 6, bottom: 6 })
.onClick(() => { this.activeTab = tab })
}
build() {
Column() {
this.contentArea()
Row() {
this.bottomTabItem('🏠', '首页服务', PropertyTab.HOME)
this.bottomTabItem('🔧', '报修管理', PropertyTab.REPAIR)
this.bottomTabItem('💳', '缴费记录', PropertyTab.PAYMENT)
this.bottomTabItem('📢', '公告通知', PropertyTab.NOTICE)
this.bottomTabItem('👤', '我的', PropertyTab.PROFILE)
}
.width('100%')
.backgroundColor(PROPERTY_COLORS.white)
.padding({ top: 4, bottom: 8 })
.shadow({ radius: 12, color: '#1A000000', offsetY: -3 })
}
.width('100%').height('100%')
.backgroundColor(PROPERTY_COLORS.bg)
}
}
// ============ 首页服务页 ============
@Component
struct HomeServiceContent {
@Builder serviceGridItem(serviceKey: string) {
Column() {
Column() {
Text(SERVICE_CONFIG[serviceKey]?.icon ?? '').fontSize(26)
}
.width(50).height(50)
.borderRadius(16)
.backgroundColor(SERVICE_CONFIG[serviceKey]?.bg ?? '#ECEFF1')
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Text(SERVICE_CONFIG[serviceKey]?.label ?? '').fontSize(11)
.fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 14, bottom: 14 })
}
@Builder quickServiceItem(qs: QuickServiceMeta) {
Row() {
Column() {
Text(qs.icon).fontSize(24)
}
.width(44).height(44).borderRadius(12)
.backgroundColor(qs.bg)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(qs.label).fontSize(14).fontWeight(FontWeight.Medium)
.fontColor(PROPERTY_COLORS.textPrimary)
Text(qs.desc).fontSize(11).fontColor(PROPERTY_COLORS.textSecondary)
.margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
Text('›').fontSize(20).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding(14)
.backgroundColor(PROPERTY_COLORS.white)
.borderRadius(12)
.margin({ left: 14, right: 14, top: 6 })
}
build() {
Column() {
// 顶部标题栏
Row() {
Column() {
Text('翠湖花园').fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
Text(PROPERTY_INFO.address).fontSize(11)
.fontColor(PROPERTY_COLORS.textSecondary).margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text('🔔').fontSize(22)
}
.width(40).height(40).borderRadius(20)
.backgroundColor(PROPERTY_COLORS.white)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
}
.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 12 })
Scroll() {
Column() {
// 物业信息卡片
Column() {
Row() {
Text('🏢').fontSize(28)
Column() {
Text(PROPERTY_INFO.community).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.white)
Text(PROPERTY_INFO.builtYear + '建成 · ' + PROPERTY_INFO.area)
.fontSize(11).fontColor('#B0BEC5').margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
Column() {
Text('优秀').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('物业评级').fontSize(9).fontColor('#B0BEC5')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row() {
Column() {
Text(PROPERTY_INFO.units.toString()).fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('栋数').fontSize(10).fontColor('#B0BEC5').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text(PROPERTY_INFO.residents.toString()).fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('住户').fontSize(10).fontColor('#B0BEC5').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('98.5%').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('满意度').fontSize(10).fontColor('#B0BEC5').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('24h').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('服务').fontSize(10).fontColor('#B0BEC5').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').margin({ top: 16 })
}
.width('100%').padding(16)
.linearGradient({
angle: 135,
colors: [['#455A64', 0.0], ['#37474F', 1.0]]
})
.borderRadius(16)
.margin({ left: 14, right: 14, top: 4 })
.shadow({ radius: 10, color: '#33455A64', offsetY: 4 })
// 服务网格
Column() {
Text('物业服务').fontSize(15).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 4, top: 16, bottom: 10 })
Row() {
this.serviceGridItem('报修')
this.serviceGridItem('缴费')
this.serviceGridItem('投诉')
this.serviceGridItem('访客')
}
.width('100%')
Row() {
this.serviceGridItem('车位')
this.serviceGridItem('快递')
this.serviceGridItem('保洁')
this.serviceGridItem('公告')
}
.width('100%')
}
.width('100%').padding({ left: 14, right: 14 })
// 快捷服务列表
Text('快捷服务').fontSize(15).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 18, top: 16, bottom: 4 })
this.quickServiceItem(QUICK_SERVICES[0])
this.quickServiceItem(QUICK_SERVICES[1])
this.quickServiceItem(QUICK_SERVICES[2])
this.quickServiceItem(QUICK_SERVICES[3])
// 物业动态
Text('物业动态').fontSize(15).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 18, top: 16, bottom: 4 })
Column() {
Row() {
Column() {
Text('🟢').fontSize(10)
}.width(8).alignItems(HorizontalAlign.Center)
Text('小区南门道闸维修完成,已恢复正常使用').fontSize(12)
.fontColor(PROPERTY_COLORS.textSecondary).layoutWeight(1).margin({ left: 6 })
Text('2小时前').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding({ top: 8, bottom: 8 })
Divider().color(PROPERTY_COLORS.border)
Row() {
Column() {
Text('🔵').fontSize(10)
}.width(8).alignItems(HorizontalAlign.Center)
Text('8月物业费缴纳周期已开启,请及时缴费').fontSize(12)
.fontColor(PROPERTY_COLORS.textSecondary).layoutWeight(1).margin({ left: 6 })
Text('5小时前').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding({ top: 8, bottom: 8 })
Divider().color(PROPERTY_COLORS.border)
Row() {
Column() {
Text('🟠').fontSize(10)
}.width(8).alignItems(HorizontalAlign.Center)
Text('2栋1单元电梯检修中,预计今日完成').fontSize(12)
.fontColor(PROPERTY_COLORS.textSecondary).layoutWeight(1).margin({ left: 6 })
Text('昨天').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding({ top: 8, bottom: 8 })
Divider().color(PROPERTY_COLORS.border)
Row() {
Column() {
Text('🟢').fontSize(10)
}.width(8).alignItems(HorizontalAlign.Center)
Text('中心花园绿化升级工程正式启动').fontSize(12)
.fontColor(PROPERTY_COLORS.textSecondary).layoutWeight(1).margin({ left: 6 })
Text('3天前').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding({ top: 8, bottom: 8 })
}
.width('100%').padding({ left: 16, right: 16 })
.backgroundColor(PROPERTY_COLORS.white)
.borderRadius(12)
.margin({ left: 14, right: 14, top: 6, bottom: 20 })
}
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
}
}
// ============ 报修管理页 ============
@Component
struct RepairManageContent {
@State selectedStatus: string = '全部'
@State showAddModal: boolean = false
@State showDetailModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteModal: boolean = false
@State selectedRepair: RepairItem | null = null
@State formType: string = '水电维修'
@State formPriority: string = '普通'
@State formLocation: string = ''
@State formDescription: string = ''
@State formContact: string = ''
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(38,50,56,0.55)')
.onClick(onClose)
}
.margin({ top: 3 })
Row() {
Text('📍' + r.location).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('📅' + r.date).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
.margin({ left: 10 })
}
.margin({ top: 4 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
Column() {
Text(REPAIR_STATUS_CONFIG[r.status]?.label ?? r.status)
.fontSize(10).fontColor(REPAIR_STATUS_CONFIG[r.status]?.color ?? '#888888')
.backgroundColor(REPAIR_STATUS_CONFIG[r.status]?.bg ?? '#ECEFF1')
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
if (r.handler !== '') {
Text('👤' + r.handler).fontSize(9).fontColor(PROPERTY_COLORS.textHint)
.margin({ top: 4 })
}
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12)
// 状态时间线(横向迷你版)
Row() {
ForEach([0, 1, 2, 3], (step: number) => {
Column() {
Column().width(8).height(8).borderRadius(4)
.backgroundColor((REPAIR_STATUS_CONFIG[r.status]?.step ?? 0) >= step ? PROPERTY_COLORS.primary : PROPERTY_COLORS.accentLight)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
if (step < 3) {
Column().height(2).layoutWeight(1)
.backgroundColor((REPAIR_STATUS_CONFIG[r.status]?.step ?? 0) > step ? PROPERTY_COLORS.primary : PROPERTY_COLORS.accentLight)
}
})
}
.width('100%').padding({ left: 12, right: 12, bottom: 10 })
}
.width('100%').backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).margin({ left: 14, right: 14, top: 6 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
.onClick(() => { this.selectedRepair = r; this.showDetailModal = true })
}
build() {
Stack() {
Column() {
// 顶部标题
Row() {
Text('报修管理').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
Column().layoutWeight(1)
Text('+ 新增').fontSize(13).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary).borderRadius(16)
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.onClick(() => { this.showAddModal = true })
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
// 统计卡片
Row() {
Column() {
Text(getRepairTotalCount().toString()).fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.primary)
Text('总报修').fontSize(10).fontColor(PROPERTY_COLORS.textSecondary)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(30).backgroundColor(PROPERTY_COLORS.accentLight)
Column() {
Text(getRepairPendingCount().toString()).fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.warning)
Text('处理中').fontSize(10).fontColor(PROPERTY_COLORS.textSecondary)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(30).backgroundColor(PROPERTY_COLORS.accentLight)
Column() {
Text(getRepairDoneCount().toString()).fontSize(20).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.success)
Text('已完成').fontSize(10).fontColor(PROPERTY_COLORS.textSecondary)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').padding({ top: 14, bottom: 14 })
.backgroundColor(PROPERTY_COLORS.white)
.margin({ left: 14, right: 14, bottom: 8 })
.borderRadius(14)
// 状态筛选
Scroll() {
Row() {
ForEach(['全部', '待受理', '处理中', '已完成', '已关闭', '已评价'], (s: string) => {
if (this.selectedStatus === s) {
Text(s).fontSize(11).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14).margin({ left: 4, right: 4 })
} else {
Text(s).fontSize(11).fontColor(PROPERTY_COLORS.primary)
.backgroundColor(PROPERTY_COLORS.white)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14).margin({ left: 4, right: 4 })
.onClick(() => { this.selectedStatus = s })
}
})
}
.padding({ left: 10, right: 10 })
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(36)
// 报修列表
Scroll() {
Column() {
this.repairItemBuilder(mockRepairs[0])
this.repairItemBuilder(mockRepairs[1])
this.repairItemBuilder(mockRepairs[2])
this.repairItemBuilder(mockRepairs[3])
this.repairItemBuilder(mockRepairs[4])
this.repairItemBuilder(mockRepairs[5])
this.repairItemBuilder(mockRepairs[6])
this.repairItemBuilder(mockRepairs[7])
this.repairItemBuilder(mockRepairs[8])
this.repairItemBuilder(mockRepairs[9])
this.repairItemBuilder(mockRepairs[10])
this.repairItemBuilder(mockRepairs[11])
this.repairItemBuilder(mockRepairs[12])
this.repairItemBuilder(mockRepairs[13])
this.repairItemBuilder(mockRepairs[14])
this.repairItemBuilder(mockRepairs[15])
this.repairItemBuilder(mockRepairs[16])
this.repairItemBuilder(mockRepairs[17])
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
if (this.showAddModal) { this.addRepairModal() }
if (this.showDetailModal) { this.detailRepairModal() }
if (this.showEditModal) { this.editRepairModal() }
if (this.showDeleteModal) { this.deleteRepairModal() }
}
.width('100%').height('100%')
}
}
// ============ 缴费记录页 ============
@Component
struct PaymentRecordContent {
@State showPayModal: boolean = false
@State selectedBill: BillItem | null = null
maxMonthly: number = 650
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(38,50,56,0.55)')
.onClick(onClose)
}
// ========== 缴费弹框 ==========
@Builder payBillModal() {
Column() {
this.modalOverlay(() => { this.showPayModal = false })
Column() {
Row() {
Text('确认缴费').fontSize(17).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(PROPERTY_COLORS.textHint)
.onClick(() => { this.showPayModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color(PROPERTY_COLORS.border)
Column() {
Text(BILL_TYPE_CONFIG[this.selectedBill?.type ?? '物业费']?.icon + ' ' + (this.selectedBill?.type ?? ''))
.fontSize(16).fontWeight(FontWeight.Bold).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 20 })
Text('¥' + (this.selectedBill?.amount ?? 0).toFixed(2)).fontSize(36)
.fontWeight(FontWeight.Bold).fontColor(PROPERTY_COLORS.primary)
.margin({ top: 8 })
Text('账期:' + (this.selectedBill?.period ?? '')).fontSize(12)
.fontColor(PROPERTY_COLORS.textSecondary).margin({ top: 6 })
Row() {
Text('账单编号').fontSize(12).fontColor(PROPERTY_COLORS.textSecondary)
Column().layoutWeight(1)
Text('#B' + (this.selectedBill?.id ?? 0)).fontSize(12)
.fontColor(PROPERTY_COLORS.textPrimary)
}
.width('100%').padding({ top: 16, bottom: 12 })
Divider().color(PROPERTY_COLORS.border)
Row() {
Text('缴费方式').fontSize(12).fontColor(PROPERTY_COLORS.textSecondary)
Column().layoutWeight(1)
Text('微信支付').fontSize(12).fontColor(PROPERTY_COLORS.primary)
}
.width('100%').padding({ top: 12, bottom: 12 })
Divider().color(PROPERTY_COLORS.border)
Row() {
Text('优惠').fontSize(12).fontColor(PROPERTY_COLORS.textSecondary)
Column().layoutWeight(1)
Text('-¥0.00').fontSize(12).fontColor(PROPERTY_COLORS.success)
}
.width('100%').padding({ top: 12, bottom: 12 })
Divider().color(PROPERTY_COLORS.border)
Row() {
Text('实付金额').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
Column().layoutWeight(1)
Text('¥' + (this.selectedBill?.amount ?? 0).toFixed(2)).fontSize(18)
.fontWeight(FontWeight.Bold).fontColor(PROPERTY_COLORS.primary)
}
.width('100%').padding({ top: 14, bottom: 14 })
}
.width('100%').padding({ left: 20, right: 20 })
Row() {
Text('取消').fontSize(14).fontColor(PROPERTY_COLORS.textSecondary)
.backgroundColor(PROPERTY_COLORS.bg).borderRadius(22)
.padding({ left: 32, right: 32, top: 12, bottom: 12 })
.onClick(() => { this.showPayModal = false })
Text('立即支付').fontSize(14).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary).borderRadius(22)
.padding({ left: 32, right: 32, top: 12, bottom: 12 })
.margin({ left: 12 })
.onClick(() => { this.showPayModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 8, bottom: 20 })
}
.width('85%').backgroundColor('#FFFFFF').borderRadius(18)
.alignItems(HorizontalAlign.Center)
.position({ x: '7.5%', y: '22%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
// ========== 账单列表项 Builder ==========
@Builder billItemBuilder(b: BillItem) {
Row() {
Column() {
Text(BILL_TYPE_CONFIG[b.type]?.icon ?? '💳').fontSize(22)
}
.width(44).height(44).borderRadius(14)
.backgroundColor(BILL_TYPE_CONFIG[b.type]?.bg ?? PROPERTY_COLORS.accentLight)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(BILL_TYPE_CONFIG[b.type]?.label ?? b.type).fontSize(14)
.fontWeight(FontWeight.Medium).fontColor(PROPERTY_COLORS.textPrimary)
Text(b.period).fontSize(11).fontColor(PROPERTY_COLORS.textSecondary)
.margin({ top: 2 })
Row() {
Text(b.date).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
if (b.paidDate !== '') {
Text('· 已付:' + b.paidDate).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
.margin({ left: 6 })
}
}
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
Column() {
Text('¥' + b.amount.toFixed(2)).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(b.status === '逾期' ? PROPERTY_COLORS.danger : PROPERTY_COLORS.textPrimary)
if (b.status === '已缴费') {
Text('✅ 已缴费').fontSize(10).fontColor(PROPERTY_COLORS.success)
.margin({ top: 4 })
} else if (b.status === '逾期') {
Text('⚠️ 逾期').fontSize(10).fontColor(PROPERTY_COLORS.danger)
.margin({ top: 4 })
} else {
Text('待缴费').fontSize(10).fontColor(PROPERTY_COLORS.warning)
.margin({ top: 4 })
}
if (b.status !== '已缴费' && b.status !== '逾期') {
Text('去缴费').fontSize(10).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary).borderRadius(10)
.padding({ left: 10, right: 10, top: 3, bottom: 3 }).margin({ top: 6 })
}
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12)
.backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).margin({ left: 14, right: 14, top: 6 })
.shadow({ radius: 3, color: '#0D000000', offsetY: 1 })
.onClick(() => {
if (b.status !== '已缴费') {
this.selectedBill = b; this.showPayModal = true
}
})
}
build() {
Stack() {
Column() {
// 顶部标题
Row() {
Text('缴费记录').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
Column().layoutWeight(1)
Text('¥' + getAnnualPaymentTotal().toString()).fontSize(14)
.fontColor(PROPERTY_COLORS.primary).fontWeight(FontWeight.Bold)
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
// 缴费汇总卡片
Column() {
Row() {
Column() {
Text('年度已缴').fontSize(11).fontColor('#B0BEC5')
Text('¥7,560.00').fontSize(22).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF').margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text('待缴').fontSize(11).fontColor('#B0BEC5')
Text('¥1,800.00').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FFCDD2').margin({ top: 4 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
// 缴费进度条
Column() {
Row() {
Column().width('81%').height(6)
.backgroundColor('#FFFFFF').borderRadius(3)
Column().layoutWeight(1)
}
.width('100%').height(6)
.backgroundColor('#44606B').borderRadius(3)
}
.width('100%').margin({ top: 14 })
Text('年度缴费进度 81%').fontSize(10).fontColor('#B0BEC5')
.margin({ top: 6 })
}
.width('100%').padding(16)
.linearGradient({ angle: 135, colors: [['#455A64', 0.0], ['#37474F', 1.0]] })
.borderRadius(16)
.margin({ left: 14, right: 14, bottom: 10 })
.shadow({ radius: 8, color: '#33455A64', offsetY: 3 })
// 月度缴费柱状图
Column() {
Text('📊 月度缴费趋势').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 16, top: 14, bottom: 10 })
Row() {
ForEach(MONTHLY_PAYMENT_DATA, (d: BarDataMeta) => {
Column() {
Text(d.value.toString()).fontSize(8)
.fontColor(PROPERTY_COLORS.textSecondary).margin({ bottom: 2 })
Column()
.width(16)
.height((d.value / this.maxMonthly * 70).toFixed(0) + 'vp')
.backgroundColor(d.color)
.borderRadius({ topLeft: 3, topRight: 3 })
Text(d.label).fontSize(8).fontColor(PROPERTY_COLORS.textHint)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
})
}
.padding({ left: 10, right: 10, bottom: 14 })
}
.width('100%').backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).margin({ left: 14, right: 14, bottom: 8 })
// 账单列表
Scroll() {
Column() {
this.billItemBuilder(mockBills[0])
this.billItemBuilder(mockBills[1])
this.billItemBuilder(mockBills[2])
this.billItemBuilder(mockBills[3])
this.billItemBuilder(mockBills[4])
this.billItemBuilder(mockBills[5])
this.billItemBuilder(mockBills[6])
this.billItemBuilder(mockBills[7])
this.billItemBuilder(mockBills[8])
this.billItemBuilder(mockBills[9])
this.billItemBuilder(mockBills[10])
this.billItemBuilder(mockBills[11])
this.billItemBuilder(mockBills[12])
this.billItemBuilder(mockBills[13])
this.billItemBuilder(mockBills[14])
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
if (this.showPayModal) { this.payBillModal() }
}
.width('100%').height('100%')
}
}
// ============ 公告通知页 ============
@Component
struct NoticeContent {
@State selectedType: string = '全部'
@State showDetailModal: boolean = false
@State showDeleteModal: boolean = false
@State selectedNotice: NoticeItem | null = null
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(38,50,56,0.55)')
.onClick(onClose)
}
// ========== 公告详情弹框 ==========
@Builder noticeDetailModal() {
Column() {
this.modalOverlay(() => { this.showDetailModal = false })
Column() {
Row() {
Text(NOTICE_LEVEL_CONFIG[this.selectedNotice?.level ?? '普通']?.icon + ' ' + (NOTICE_LEVEL_CONFIG[this.selectedNotice?.level ?? '普通']?.label ?? '普通'))
.fontSize(11).fontColor(NOTICE_LEVEL_CONFIG[this.selectedNotice?.level ?? '普通']?.color ?? '#888888')
.backgroundColor(NOTICE_LEVEL_CONFIG[this.selectedNotice?.level ?? '普通']?.bg ?? '#ECEFF1')
.padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(10)
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor(PROPERTY_COLORS.textHint)
.onClick(() => { this.showDetailModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 8 })
Scroll() {
Column() {
Text(this.selectedNotice?.title ?? '').fontSize(17).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 20, right: 20, top: 8 })
Row() {
Text(NOTICE_TYPE_CONFIG[this.selectedNotice?.type ?? '']?.icon + ' ' + (NOTICE_TYPE_CONFIG[this.selectedNotice?.type ?? '']?.label ?? ''))
.fontSize(11).fontColor(PROPERTY_COLORS.primary)
.backgroundColor(PROPERTY_COLORS.accentLight)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
Text('📅' + (this.selectedNotice?.date ?? '')).fontSize(10)
.fontColor(PROPERTY_COLORS.textHint).margin({ left: 10 })
Text('👁' + (this.selectedNotice?.views ?? 0)).fontSize(10)
.fontColor(PROPERTY_COLORS.textHint).margin({ left: 10 })
}
.width('100%').padding({ left: 20, right: 20, top: 10 })
Divider().color(PROPERTY_COLORS.border).margin({ top: 14, left: 20, right: 20 })
Text(this.selectedNotice?.content ?? '').fontSize(14)
.fontColor(PROPERTY_COLORS.textPrimary)
.lineHeight(24)
.width('100%').padding({ left: 20, right: 20, top: 14, bottom: 14 })
Row() {
Text('发布人:' + (this.selectedNotice?.author ?? '')).fontSize(11)
.fontColor(PROPERTY_COLORS.textSecondary)
}
.width('100%').padding({ left: 20, right: 20, bottom: 16 })
}
}
.layoutWeight(1)
Row() {
Text('🗑️ 删除公告').fontSize(13).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.danger).borderRadius(18)
.padding({ left: 22, right: 22, top: 9, bottom: 9 })
.onClick(() => { this.showDeleteModal = true })
Text('📢 分享').fontSize(13).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary).borderRadius(18)
.padding({ left: 22, right: 22, top: 9, bottom: 9 })
.margin({ left: 10 })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 10, bottom: 16 })
}
.width('90%').constraintSize({ maxHeight: '80%' })
.backgroundColor('#FFFFFF').borderRadius(18)
.alignItems(HorizontalAlign.Center)
.position({ x: '5%', y: '10%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
// ========== 删除确认弹框 ==========
@Builder deleteNoticeModal() {
Column() {
this.modalOverlay(() => { this.showDeleteModal = false })
Column() {
Column() {
Text('🗑️').fontSize(40)
}
.width(68).height(68).borderRadius(34)
.backgroundColor(PROPERTY_COLORS.bg)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Text('确认删除此公告?').fontSize(17).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary).margin({ top: 16 })
Text('删除后公告将从列表中移除').fontSize(12).fontColor(PROPERTY_COLORS.textSecondary)
.margin({ top: 4 })
Row() {
Text(NOTICE_LEVEL_CONFIG[this.selectedNotice?.level ?? '普通']?.icon ?? '📢')
.fontSize(14)
Text(this.selectedNotice?.title ?? '').fontSize(12)
.fontColor(PROPERTY_COLORS.textPrimary).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1).margin({ left: 6 })
}
.backgroundColor(PROPERTY_COLORS.bg).borderRadius(10)
.padding({ left: 14, right: 14, top: 10, bottom: 10 }).margin({ top: 16, left: 20, right: 20 })
Row() {
Text('取消').fontSize(14).fontColor(PROPERTY_COLORS.textSecondary)
.backgroundColor(PROPERTY_COLORS.bg).borderRadius(22)
.padding({ left: 30, right: 30, top: 11, bottom: 11 })
.onClick(() => { this.showDeleteModal = false })
Text('确认删除').fontSize(14).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.danger).borderRadius(22)
.padding({ left: 30, right: 30, top: 11, bottom: 11 })
.margin({ left: 12 })
.onClick(() => { this.showDeleteModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 22, bottom: 22 })
}
.width('82%').backgroundColor('#FFFFFF').borderRadius(18)
.alignItems(HorizontalAlign.Center)
.position({ x: '9%', y: '30%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
// ========== 公告列表项 Builder ==========
@Builder noticeItemBuilder(n: NoticeItem) {
Column() {
Row() {
Column() {
Text(NOTICE_TYPE_CONFIG[n.type]?.icon ?? '📢').fontSize(20)
}
.width(40).height(40).borderRadius(12)
.backgroundColor(PROPERTY_COLORS.bg)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Row() {
Text(NOTICE_LEVEL_CONFIG[n.level]?.icon ?? '🔵').fontSize(9)
Text(NOTICE_LEVEL_CONFIG[n.level]?.label ?? n.level).fontSize(9)
.fontColor(NOTICE_LEVEL_CONFIG[n.level]?.color ?? '#888888')
.backgroundColor(NOTICE_LEVEL_CONFIG[n.level]?.bg ?? '#ECEFF1')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6)
.margin({ left: 4 })
Text(NOTICE_TYPE_CONFIG[n.type]?.label ?? n.type).fontSize(9)
.fontColor(PROPERTY_COLORS.primary).margin({ left: 6 })
}
Text(n.title).fontSize(14).fontWeight(FontWeight.Medium)
.fontColor(PROPERTY_COLORS.textPrimary).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 5 })
Row() {
Text(n.date).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('· ' + n.author).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
.margin({ left: 6 })
Text('· 👁' + n.views).fontSize(10).fontColor(PROPERTY_COLORS.textHint)
.margin({ left: 6 })
}
.margin({ top: 4 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
}
.width('100%').padding(12)
}
.width('100%').backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).margin({ left: 14, right: 14, top: 6 })
.shadow({ radius: 3, color: '#0D000000', offsetY: 1 })
.onClick(() => { this.selectedNotice = n; this.showDetailModal = true })
}
build() {
Stack() {
Column() {
// 顶部标题
Row() {
Text('公告通知').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
Column().layoutWeight(1)
Text(getNoticeTotalCount().toString() + '条').fontSize(13)
.fontColor(PROPERTY_COLORS.textSecondary)
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
// 类型筛选
Scroll() {
Row() {
ForEach(['全部', '停水停电', '维修通知', '社区活动', '安全提醒', '物业公告', '缴费提醒'], (t: string) => {
if (this.selectedType === t) {
Text(t).fontSize(11).fontColor('#FFFFFF')
.backgroundColor(PROPERTY_COLORS.primary)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14).margin({ left: 4, right: 4 })
} else {
Text(t).fontSize(11).fontColor(PROPERTY_COLORS.primary)
.backgroundColor(PROPERTY_COLORS.white)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14).margin({ left: 4, right: 4 })
.onClick(() => { this.selectedType = t })
}
})
}
.padding({ left: 10, right: 10 })
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(36)
// 公告列表
Scroll() {
Column() {
this.noticeItemBuilder(mockNotices[0])
this.noticeItemBuilder(mockNotices[1])
this.noticeItemBuilder(mockNotices[2])
this.noticeItemBuilder(mockNotices[3])
this.noticeItemBuilder(mockNotices[4])
this.noticeItemBuilder(mockNotices[5])
this.noticeItemBuilder(mockNotices[6])
this.noticeItemBuilder(mockNotices[7])
this.noticeItemBuilder(mockNotices[8])
this.noticeItemBuilder(mockNotices[9])
this.noticeItemBuilder(mockNotices[10])
this.noticeItemBuilder(mockNotices[11])
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
if (this.showDetailModal) { this.noticeDetailModal() }
if (this.showDeleteModal) { this.deleteNoticeModal() }
}
.width('100%').height('100%')
}
}
// ============ 我的页面 ============
@Component
struct ProfileContent {
maxAnnual: number = 3600
@Builder statCard(icon: string, label: string, value: string, color: string) {
Column() {
Text(icon).fontSize(20).margin({ bottom: 4 })
Text(value).fontSize(18).fontWeight(FontWeight.Bold).fontColor(color)
Text(label).fontSize(10).fontColor(PROPERTY_COLORS.textSecondary).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).padding({ top: 16, bottom: 16 })
}
build() {
Scroll() {
Column() {
// 用户信息卡片
Column() {
Row() {
Column() {
Text('👤').fontSize(36)
}
.width(64).height(64).borderRadius(32)
.backgroundColor(PROPERTY_COLORS.accentLight)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text('刘建国').fontSize(18).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('翠湖花园 · 3栋2单元501室').fontSize(12)
.fontColor('#B0BEC5').margin({ top: 3 })
Row() {
Text('业主认证').fontSize(9).fontColor('#FFFFFF')
.backgroundColor('#2E7D32').borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text('VIP会员').fontSize(9).fontColor('#FFFFFF')
.backgroundColor('#F57F17').borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).margin({ left: 6 })
}
.margin({ top: 6 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
}
.width('100%').padding(16)
}
.width('100%')
.linearGradient({ angle: 135, colors: [['#455A64', 0.0], ['#263238', 1.0]] })
.borderRadius(16)
.margin({ left: 14, right: 14, top: 14 })
.shadow({ radius: 10, color: '#33455A64', offsetY: 4 })
// 房屋信息卡片
Column() {
Text('🏠 房屋信息').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 16, top: 14, bottom: 10 })
Row() {
Column() {
Text('小区').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text(PROPERTY_INFO.community).fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('面积').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('128.5㎡').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('户型').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('三室两厅').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
}
.width('100%').padding({ left: 16, right: 16, bottom: 14 })
Row() {
Column() {
Text('车位').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('B2-088').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('入住时间').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('2019年5月').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('物业费标准').fontSize(10).fontColor(PROPERTY_COLORS.textHint)
Text('2.5元/㎡/月').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
}
.width('100%').padding({ left: 16, right: 16, bottom: 14 })
}
.width('100%').backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).margin({ left: 14, right: 14, top: 10 })
// 物业统计
Text('📊 物业统计').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 18, top: 16, bottom: 8 })
Row() {
this.statCard('🔧', '报修记录', '18', PROPERTY_COLORS.primary)
this.statCard('✅', '已完成', '8', PROPERTY_COLORS.success)
}
.width('100%').padding({ left: 14, right: 14 })
Row() {
this.statCard('💳', '缴费次数', '15', PROPERTY_COLORS.accent)
this.statCard('📢', '已读公告', '10', PROPERTY_COLORS.warning)
}
.width('100%').padding({ left: 14, right: 14, top: 8 })
// 年度缴费柱状图
Column() {
Text('💰 年度缴费分布').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 16, top: 14, bottom: 10 })
Row() {
ForEach(ANNUAL_PAYMENT_DATA, (d: BarDataMeta) => {
Column() {
Text('¥' + d.value.toString()).fontSize(8)
.fontColor(PROPERTY_COLORS.textSecondary).margin({ bottom: 3 })
Column()
.width(20)
.height((d.value / this.maxAnnual * 90).toFixed(0) + 'vp')
.backgroundColor(d.color)
.borderRadius({ topLeft: 4, topRight: 4 })
Text(d.label).fontSize(8).fontColor(PROPERTY_COLORS.textHint)
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
})
}
.padding({ left: 12, right: 12, bottom: 14 })
Row() {
Text('年度总计').fontSize(12).fontColor(PROPERTY_COLORS.textSecondary)
Column().layoutWeight(1)
Text('¥' + getAnnualPaymentTotal().toString()).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.primary)
}
.width('100%').padding({ left: 16, right: 16, bottom: 14 })
}
.width('100%').backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).margin({ left: 14, right: 14, top: 10 })
// 快捷操作
Text('⚙️ 快捷操作').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(PROPERTY_COLORS.textPrimary)
.width('100%').padding({ left: 18, top: 16, bottom: 8 })
Column() {
Row() {
Text('🔑').fontSize(18)
Text('修改密码').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.layoutWeight(1).margin({ left: 10 })
Text('›').fontSize(18).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
Divider().color(PROPERTY_COLORS.border)
Row() {
Text('🔔').fontSize(18)
Text('消息通知').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.layoutWeight(1).margin({ left: 10 })
Text('›').fontSize(18).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
Divider().color(PROPERTY_COLORS.border)
Row() {
Text('📞').fontSize(18)
Text('联系物业').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.layoutWeight(1).margin({ left: 10 })
Text('›').fontSize(18).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
Divider().color(PROPERTY_COLORS.border)
Row() {
Text('ℹ️').fontSize(18)
Text('关于我们').fontSize(13).fontColor(PROPERTY_COLORS.textPrimary)
.layoutWeight(1).margin({ left: 10 })
Text('›').fontSize(18).fontColor(PROPERTY_COLORS.textHint)
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
}
.width('100%').backgroundColor(PROPERTY_COLORS.white)
.borderRadius(14).margin({ left: 14, right: 14 })
.padding({ left: 16, right: 16 })
Text('v1.0 · 翠湖花园智慧物业 · 2026').fontSize(10)
.fontColor(PROPERTY_COLORS.textHint)
.alignSelf(ItemAlign.Center).margin({ top: 20, bottom: 20 })
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
.width('100%')
}
}
总结
通过对这套智慧物业管理系统源代码的逐行深度剖析,我们可以清晰地看到一个生产级移动端应用在架构设计、数据建模、视觉规范和交互细节等各个层面所展现的工程素养。

在架构设计层面,应用采用了清晰的分层组织——从类型定义到数据模型,从配置字典到 UI 组件,每一层职责单一、边界清晰。五大业务模块各自封装为独立的 @Component,通过入口组件的 activeTab 状态统一调度,实现了模块间的松耦合。Stack 层叠布局让弹窗系统能够灵活叠加而不干扰主内容渲染。
在数据建模层面,三个 @Observed 数据模型(RepairItem、BillItem、NoticeItem)完整覆盖了物业业务的三大核心实体。每个模型的字段设计贴合实际业务需求——报修单包含完整的工单流转信息(类型、状态、处理人、优先级),账单包含财务所需的全部维度(金额、账期、支付方式、实付日期),公告包含传播所需的元数据(级别、类型、浏览量、发布人)。
在视觉规范层面,PROPERTY_COLORS 设计令牌定义了 15 个语义化色彩变量,从主色到文本色阶到语义色,构建了一套完整的色彩体系。多个配置字典(SERVICE_CONFIG、REPAIR_STATUS_CONFIG、BILL_TYPE_CONFIG 等)将业务枚举值与视觉属性一一映射,实现了"配置驱动渲染"的高效模式。所有配色遵循 Material Design 色板规范,前景色与背景色搭配保证了对对比度和可读性。
在交互细节层面,应用展现了大量精心设计的交互模式:报修列表项的横向迷你进度条让用户无需进入详情即可感知工单进度;详情弹窗的纵向时间线通过 step 数值比较动态控制节点高亮;缴费汇总卡片的进度条通过内外两层颜色对比直观展示完成比例;删除确认弹窗用红色警示图标和"不可恢复"提示强化危险操作的防误触设计;底部导航栏的选中指示器通过 if 条件渲染实现按需出现。
在数据可视化层面,应用没有引入任何第三方图表库,而是通过 ForEach 遍历数据数组,配合动态计算高度的 Column 组件手工绘制柱状图。这种零依赖方案虽然功能有限,但对于简单的数据展示场景已经足够,且保持了应用的轻量性和可维护性。
在防御性编程层面,代码大量使用了可选链操作符 ?. 和空值合并操作符 ?? 的组合,对可能为 null 的 selectedRepair、selectedBill、selectedNotice 对象的属性访问做了全面的空值兜底。这种写法虽然增加了代码长度,但从根本上消除了因空值导致的运行时崩溃风险。
当然,作为一套演示型应用,它也存在一些可改进之处。例如列表渲染采用了手动逐条调用 builder 的方式而非 ForEach,在数据量增大时不够优雅;表单提交和状态切换目前仅切换了弹窗显隐,尚未实现真实的数据持久化;筛选功能修改了 selectedStatus/selectedType 状态但列表渲染未与之联动过滤。这些都是从演示原型走向生产应用时需要补全的环节。
更多推荐

所有评论(0)