装修管家应用:基于HarmonyOS API 24全流程装修管理平台外层包裹了水平 Scroll 容器,并隐藏滚动条(scrollBar(BarState.Off)),用户可以左右滑动浏览所有选项
一、行业背景与应用意义
1.1 装修行业的管理困境
家庭装修是一项涉及面广、周期长、参与方多的复杂工程。一套普通住宅的精装修往往需要经历拆除、水电、泥瓦、木工、油漆、安装、保洁等多个施工阶段,耗时动辄两到三个月,甚至半年以上。在这个过程中,业主需要同时对接设计师、施工队长、各工种师傅、材料供应商等多方角色。

传统的装修管理方式主要依赖纸质记录、微信沟通和口头确认,存在大量痛点。方案图纸散落在不同聊天记录里,材料采购清单靠脑记或手写笔记,施工进度只能靠现场巡查或师傅口头汇报,预算花费更是一笔糊涂账,往往等到装修结束才发现严重超支。这种粗放的管理模式不仅效率低下,而且极易产生纠纷和返工。
1.2 数字化管理的必要性
随着移动互联网的深入普及和智能终端的广泛覆盖,将装修管理流程数字化、结构化已经成为行业的迫切需求。一个优秀的装修管理应用应当具备以下核心能力:
- 方案全生命周期管理:从设计初稿到施工确认再到竣工验收,每个阶段的方案状态都需要清晰可追踪。
- 材料采购精细化管控:材料品类繁多、规格各异,需要分类管理采购进度,避免漏购或重复采购。
- 施工进度可视化呈现:业主不可能天天跑工地,需要通过甘特图、时间线等可视化方式远程掌握施工进展。
- 预算执行实时监控:每一笔花费都应归类记录,与预算进行对比,及时预警超支风险。
- 多维度数据汇总:将分散在各模块的数据聚合到个人中心,形成全局视角的装修仪表盘。
1.3 本应用的设计定位
本文要深入解析的"装修管家"应用,正是围绕上述需求打造的一款全流程装修管理工具。它采用华为 HarmonyOS 的 ArkUI 声明式开发框架构建,以暖橙色为主视觉基调,营造出温馨的家居氛围。应用涵盖装修方案管理、材料清单跟踪、施工进度监控、预算统计分析和个人中心五大核心模块,形成了一个完整的装修管理闭环。
这款应用不仅仅是一个简单的待办清单,而是一个结构化的数据管理平台。它将装修过程中产生的方案、材料、工序、费用等异构数据统一建模,通过配置化的元数据体系驱动界面渲染,实现了高度的可扩展性和可维护性。接下来,我们将逐层深入剖析它的技术实现。
二、整体架构概览
2.1 技术栈选型
本应用基于 HarmonyOS 的 ArkTS 语言和 ArkUI 声明式 UI 框架开发。ArkTS 是 TypeScript 的超集,在保留类型系统优势的同时,针对声明式 UI 编程范式进行了语言层面的扩展。ArkUI 则提供了一套以 build() 方法为核心的组件化构建体系,开发者通过链式调用的方式声明界面结构和样式。
应用采用了以下几个关键的 ArkUI 特性:
@Entry和@Component装饰器用于定义入口组件和自定义组件。@State状态管理装饰器实现组件内部的响应式数据流。@Observed装饰器标记可观察的数据模型类,配合 UI 框架实现数据变化驱动的界面更新。@Builder装饰器定义可复用的 UI 构建片段,类似于其他框架中的渲染函数或模板片段。- 声明式布局容器
Column、Row、Stack、Scroll等,通过链式属性设置实现 Flexbox 风格的布局。
2.2 模块划分
从代码组织来看,整个应用可以分为以下几个清晰的层次:
| 层次 | 职责 | 主要内容 |
|---|---|---|
| 类型定义层 | 定义接口契约 | PlanStatusMeta、PlanStyleMeta、ColorPalette 等接口 |
| 数据模型层 | 定义业务实体 | RenoPlan、MaterialItem、Phase 三个可观察类 |
| 设计令牌层 | 统一视觉规范 | COLORS 色板、各类配置映射表 |
| 数据准备层 | 提供模拟数据 | mockPlans、mockMaterials、mockPhases 等 |
| 统计函数层 | 聚合计算指标 | getPlanCount、getOverallProgress 等 |
| 导航框架层 | 管理页面切换 | RenoTab 枚举与 RenoManagerApp 入口 |
| 业务页面层 | 五大功能模块 | 方案页、材料页、进度页、预算页、个人页 |
这种分层设计使得各层职责单一、耦合度低,便于独立演进和维护。
2.3 数据流向
应用的数据流遵循单向数据流原则。底层的 Mock 数据和配置常量作为数据源,通过统计函数聚合后传入各业务页面组件。页面组件内部通过 @State 维护交互状态(如弹窗显隐、选中项、表单输入值等),当状态变化时,ArkUI 框架自动触发对应组件的重新渲染。用户操作(如点击、输入)通过事件回调修改状态,形成闭环。
三、类型定义体系
应用在文件开头集中定义了一组 TypeScript 接口,这些接口充当了整个应用的类型契约,为配置化驱动界面渲染奠定了基础。
3.1 状态元数据接口
interface PlanStatusMeta {
label: string
color: string
bg: string
icon: string
level: number
}

这段代码定义了装修方案状态的元数据结构。label 是状态的显示文本,如"施工中";color 是状态对应的主题色,用于文字和进度条着色;bg 是状态标签的背景色;icon 是 Emoji 图标,用于直观的视觉标识;level 是状态层级编号,用于排序和流程判断。
这种设计的好处在于,状态的视觉表现不是硬编码在各个组件中的,而是集中配置在一处。当需要新增一个状态或调整配色时,只需修改配置表,所有引用该状态的组件都会自动更新。
3.2 风格与材料分类元数据
interface PlanStyleMeta {
label: string
icon: string
color: string
}
interface MaterialCategoryMeta {
label: string
icon: string
color: string
bg: string
}
interface PhaseMeta {
label: string
icon: string
color: string
}

这三个接口分别定义了装修风格、材料分类和施工阶段的元数据结构。它们结构相似,都包含 label、icon 和 color 字段。MaterialCategoryMeta 额外增加了 bg 字段,因为材料分类在列表中需要更丰富的背景色区分。
值得注意的是,这些接口虽然结构简单,但它们的存在让配置数据有了类型约束。在 ArkTS 的编译时检查下,如果配置表中某个字段拼写错误或类型不符,编译器会立即报错,大大降低了运行时出错的风险。
3.3 预算分类与色彩系统
interface BudgetCategoryMeta {
label: string
icon: string
color: string
percent: number
}
interface ColorPalette {
primary: string
primaryLight: string
secondary: string
bg: string
card: string
text: string
subtext: string
border: string
white: string
dark: string
warning: string
success: string
}

BudgetCategoryMeta 在基础元数据上增加了 percent 字段,表示该预算类别在总预算中的占比。ColorPalette 则定义了应用的全局色彩系统,包含了主色、浅主色、辅助色、背景色、卡片色、正文色、副文本色、边框色等 12 个语义化色彩槽位。
这种语义化的色彩命名方式比直接使用十六进制颜色值更加清晰。当我们在代码中看到 COLORS.primary 时,立刻能理解这是主色调;而如果看到 '#BF360C',则需要查阅设计稿才能理解其用途。语义化命名还使得主题切换成为可能——只需替换 ColorPalette 对象的值,整个应用的配色就会随之改变。
四、数据模型设计
4.1 可观察的装修方案模型
@Observed
export class RenoPlan {
id: number = 0
name: string = ''
style: string = ''
area: number = 0
budget: number = 0
duration: number = 0
startDate: string = ''
endDate: string = ''
status: string = '设计中'
progress: number = 0
designer: string = ''
description: string = ''
constructor(id: number, name: string, style: string, area: number,
budget: number, duration: number, startDate: string,
endDate: string, status: string, progress: number,
designer: string, description: string) {
this.id = id; this.name = name; this.style = style; this.area = area
this.budget = budget; this.duration = duration; this.startDate = startDate
this.endDate = endDate; this.status = status; this.progress = progress
this.designer = designer; this.description = description
}
}

RenoPlan 类是装修方案的核心数据模型,使用了 @Observed 装饰器标记。这个装饰器的意义在于:当该类的实例属性发生变化时,ArkUI 框架能够感知到变化并自动刷新依赖这些数据的 UI 组件。这是实现响应式编程的关键机制。
该模型包含了 12 个字段,覆盖了方案的标识(id)、基本信息(name、style、area)、财务信息(budget)、时间信息(duration、startDate、endDate)、状态信息(status、progress)和协作信息(designer、description)。
每个字段都有默认初始值,这是 ArkTS 的良好实践——它确保了即使构造函数未被调用,对象也处于一个合法的状态。status 的默认值为 '设计中',progress 默认为 0,这些都是符合业务逻辑的初始状态。
构造函数采用了参数列表的形式,虽然参数较多,但通过分号分隔的紧凑写法保持了代码的简洁。这种写法在数据模型类中很常见,因为这些类的职责就是承载数据,不需要复杂的方法逻辑。
4.2 材料项模型
@Observed
export class MaterialItem {
id: number = 0
category: string = ''
name: string = ''
spec: string = ''
quantity: number = 0
unit: string = ''
price: number = 0
purchased: boolean = false
brand: string = ''
supplier: string = ''
constructor(id: number, category: string, name: string, spec: string,
quantity: number, unit: string, price: number,
purchased: boolean, brand: string, supplier: string) {
this.id = id; this.category = category; this.name = name; this.spec = spec
this.quantity = quantity; this.unit = unit; this.price = price
this.purchased = purchased; this.brand = brand; this.supplier = supplier
}
}

MaterialItem 模型描述了装修材料的完整信息。其中 spec 字段记录材料规格(如"800x800mm 哑光"),unit 记录计量单位(如"片"、“㎡”、“桶”),purchased 是布尔值标记是否已采购,brand 和 supplier 分别记录品牌和供应商。
purchased 字段的设计尤为关键。它是一个布尔标志位,驱动了材料列表中多项视觉变化:已购材料会显示删除线、灰色文字和绿色"已购"标签;未购材料则正常显示并带有橙色"待购"标签。这种通过单一布尔值驱动多维度视觉差异的方式,体现了状态驱动 UI 的设计思想。
4.3 施工阶段模型
@Observed
export class Phase {
id: number = 0
name: string = ''
progress: number = 0
startDate: string = ''
endDate: string = ''
owner: string = ''
status: string = '待开始'
workers: number = 0
notes: string = ''
constructor(id: number, name: string, progress: number, startDate: string,
endDate: string, owner: string, status: string,
workers: number, notes: string) {
this.id = id; this.name = name; this.progress = progress
this.startDate = startDate; this.endDate = endDate; this.owner = owner
this.status = status; this.workers = workers; this.notes = notes
}
}

Phase 模型描述了装修施工的各个阶段。name 字段对应 PHASE_CONFIG 中的键(如"拆除工程"、“水电改造”),owner 记录负责人姓名,workers 记录施工人数,notes 是施工说明备注。
三个模型类都使用了 @Observed 装饰器,这保证了当任何属性变化时,与之绑定的 UI 都会自动更新。这种数据驱动的模式消除了手动操作 DOM 的需要,是声明式 UI 框架的核心优势。
五、设计令牌与配置系统
5.1 全局色彩系统
const COLORS: ColorPalette = {
primary: '#BF360C',
primaryLight: '#D84315',
secondary: '#FFCCBC',
bg: '#FFF3E0',
card: '#FFFFFF',
text: '#3E2723',
subtext: '#8D6E63',
border: '#FFCCBC',
white: '#FFFFFF',
dark: '#3E2723',
warning: '#FF6F00',
success: '#2E7D32'
}

COLORS 常量定义了应用的完整色彩系统。主色 #BF360C 是一种深橙红色(Material Design 中的 Deep Orange 900),传递出温暖、活力的家居感。背景色 #FFF3E0 是极浅的橙色,营造出柔和的视觉氛围。正文色 #3E2723 是深棕色,比纯黑色更加柔和,长时间阅读不易疲劳。
值得注意的是,虽然定义了这个全局色彩常量,但在实际组件代码中,很多地方直接使用了十六进制颜色值而非引用 COLORS 对象。这在快速原型开发中是常见的做法,但在生产级项目中,建议统一通过色彩令牌引用,以便于主题管理和色彩规范的一致性。
5.2 方案状态配置
const PLAN_STATUS_CONFIG: Record<string, PlanStatusMeta> = {
'设计中': { label: '设计中', color: '#1976D2', bg: '#E3F2FD', icon: '📐', level: 1 },
'已确认': { label: '已确认', color: '#00695C', bg: '#E0F2F1', icon: '✅', level: 2 },
'施工中': { label: '施工中', color: '#BF360C', bg: '#FFCCBC', icon: '🔨', level: 3 },
'已完工': { label: '已完工', color: '#2E7D32', bg: '#E8F5E9', icon: '🎉', level: 4 },
'已暂停': { label: '已暂停', color: '#FF6F00', bg: '#FFF3E0', icon: '⏸️', level: 0 }
}

这是一张方案状态的配置映射表,以状态名称为键,以 PlanStatusMeta 元数据为值。每个状态都精心配置了语义化的色彩搭配:
- 设计中使用蓝色系,传达专业、理性的设计阶段氛围。
- 已确认使用青绿色系,表示方案已定、可以推进。
- 施工中使用与应用主色一致的橙红色,强调当前正在进行的重点状态。
- 已完工使用绿色系,传达完成和成功的正面信号。
- 已暂停使用警告橙色,提示需要关注的异常状态。
level 字段定义了状态的流程顺序:已暂停为 0(脱离正常流程),设计中为 1,已确认为 2,施工中为 3,已完工为 4。这个层级关系可用于后续的状态流转校验和流程控制。
5.3 装修风格配置
const PLAN_STYLE_CONFIG: Record<string, PlanStyleMeta> = {
'现代简约': { label: '现代简约', icon: '🏠', color: '#455A64' },
'北欧风情': { label: '北欧风情', icon: '🌲', color: '#2E7D32' },
'新中式': { label: '新中式', icon: '🏯', color: '#8D6E63' },
'美式经典': { label: '美式经典', icon: '🏛️', color: '#5D4037' },
'工业风': { label: '工业风', icon: '🏭', color: '#37474F' },
'日式侘寂': { label: '日式侘寂', icon: '🏯', color: '#6D4C41' },
'地中海': { label: '地中海', icon: '🌊', color: '#0277BD' }
}

风格配置表涵盖了七种主流装修风格,每种风格都配有 Emoji 图标和主题色。色彩选择与风格名称高度契合:北欧风情用森林绿、地中海用海洋蓝、工业风用蓝灰色、美式经典用深棕色。这种视觉与语义的关联设计,使得用户在浏览方案列表时能够通过颜色快速识别风格类型。
5.4 材料分类与施工阶段配置
const MATERIAL_CATEGORY_CONFIG: Record<string, MaterialCategoryMeta> = {
'瓷砖': { label: '瓷砖', icon: '🧱', color: '#BF360C', bg: '#FFCCBC' },
'地板': { label: '地板', icon: '🪵', color: '#8D6E63', bg: '#EFEBE9' },
'涂料': { label: '涂料', icon: '🎨', color: '#1976D2', bg: '#E3F2FD' },
'灯具': { label: '灯具', icon: '💡', color: '#FF6F00', bg: '#FFF3E0' },
'卫浴': { label: '卫浴', icon: '🚿', color: '#0277BD', bg: '#E1F5FE' },
'门窗': { label: '门窗', icon: '🚪', color: '#5D4037', bg: '#EFEBE9' },
'五金': { label: '五金', icon: '🔧', color: '#37474F', bg: '#ECEFF1' },
'家具': { label: '家具', icon: '🛋️', color: '#6D4C41', bg: '#EFEBE9' }
}
const PHASE_CONFIG: Record<string, PhaseMeta> = {
'拆除工程': { label: '拆除工程', icon: '⛏️', color: '#F44336' },
'水电改造': { label: '水电改造', icon: '⚡', color: '#1976D2' },
'泥瓦工程': { label: '泥瓦工程', icon: '🧱', color: '#BF360C' },
'木工工程': { label: '木工工程', icon: '🪚', color: '#8D6E63' },
'油漆工程': { label: '油漆工程', icon: '🖌️', color: '#FF6F00' },
'安装工程': { label: '安装工程', icon: '🔩', color: '#37474F' },
'保洁验收': { label: '保洁验收', icon: '🧹', color: '#2E7D32' },
'软装入住': { label: '软装入住', icon: '🪴', color: '#00695C' }
}

材料分类配置了八大品类,施工阶段配置了八道工序。施工阶段的色彩设计遵循了工程进度的视觉逻辑:拆除阶段用红色(警示性)、水电用蓝色(管道色彩)、泥瓦用橙红(砖瓦泥土色)、木工用棕色(木材色)、油漆用橙色(涂料色)、安装用蓝灰(金属质感)、保洁用绿色(清洁完成)、软装用青绿(生机勃勃)。
5.5 预算分类与枚举常量
const BUDGET_CATEGORIES: BudgetCategoryMeta[] = [
{ label: '人工费', icon: '👷', color: '#BF360C', percent: 30 },
{ label: '材料费', icon: '🧱', color: '#D84315', percent: 35 },
{ label: '设计费', icon: '📐', color: '#FF6F00', percent: 10 },
{ label: '家电', icon: '📺', color: '#1976D2', percent: 15 },
{ label: '软装', icon: '🪴', color: '#2E7D32', percent: 7 },
{ label: '其他', icon: '📋', color: '#8D6E63', percent: 3 }
]
const MATERIAL_CATEGORIES: string[] = ['瓷砖', '地板', '涂料', '灯具', '卫浴', '门窗', '五金', '家具']
const PLAN_STATUSES: string[] = ['设计中', '已确认', '施工中', '已完工', '已暂停']
const PLAN_STYLES: string[] = ['现代简约', '北欧风情', '新中式', '美式经典', '工业风', '日式侘寂', '地中海']

预算分类以数组形式定义,六个类别的百分比加总恰好为 100%,符合预算分配的完整性要求。材料费占比最高(35%),人工费次之(30%),这反映了装修成本的真实分布规律。
后三行为纯字符串数组,分别列举了材料分类、方案状态和装修风格的可用值。这些数组主要用于表单选择器中 ForEach 的遍历渲染,为用户提供可选项列表。将可选值提取为常量数组,既避免了在组件中硬编码,也方便了后续的扩展和维护。
六、Mock 数据与统计函数
6.1 装修方案模拟数据
const mockPlans: RenoPlan[] = [
new RenoPlan(1, '阳光城三居室全屋精装', '现代简约', 128, 285000, 90,
'2026-03-01', '2026-05-30', '施工中', 45, '王工',
'三室两厅,南北通透,全屋收纳系统+智能家居预留'),
new RenoPlan(2, '江景大平层轻奢方案', '美式经典', 180, 520000, 120,
'2026-02-15', '2026-06-15', '施工中', 60, '李工',
'270度江景视野,客厅挑高3.6米,全屋石材+定制护墙板'),
// ... 其余方案省略展示
]
模拟数据精心构造了七套不同类型的装修方案,涵盖了从 72 平米学区房到 320 平米别墅的各种户型场景,预算从 9.8 万到 88 万跨度极大,风格涵盖现代简约、美式经典、新中式、工业风、日式侘寂、地中海等多种类型。每条数据的描述字段都包含了具体的装修亮点,使得演示效果更加真实可信。
6.2 材料与施工阶段模拟数据
const mockMaterials: MaterialItem[] = [
new MaterialItem(1, '瓷砖', '客厅通体大理石瓷砖', '800x800mm 哑光', 45, '片', 128, true, '马可波罗', '建材城A区'),
new MaterialItem(2, '瓷砖', '厨卫防滑墙砖', '300x600mm 亮面', 80, '片', 18, true, '东鹏', '建材城A区'),
// ... 共24条材料数据,覆盖8大品类
]
const mockPhases: Phase[] = [
new Phase(1, '拆除工程', 100, '2026-03-01', '2026-03-05', '陈师傅', '已完工', 4, '原墙地砖全拆+垃圾清运3车'),
new Phase(2, '水电改造', 100, '2026-03-06', '2026-03-16', '张电工', '已完工', 2, '全屋水电重做+网络6类线+预留投影电源'),
new Phase(3, '泥瓦工程', 80, '2026-03-17', '2026-04-10', '王泥工', '施工中', 3, '厨卫贴砖+客厅找平+阳台防水完成'),
// ... 共8个施工阶段
]
材料数据共 24 条,覆盖了八大品类的典型材料,每条都包含真实的品牌名(马可波罗、东鹏、圣象、多乐士、欧普、TOTO 等)和供应商信息。施工阶段数据共 8 条,严格按照装修工序的时间顺序排列,前两个阶段已完工,中间两个施工中,后面四个待开始,完整呈现了一个进行中的装修工程的时间线。
6.3 预算与月度支出数据
const monthlyExpenses: number[] = [35000, 58000, 72000, 45000, 38000, 28000]
const monthlyLabels: string[] = ['1月', '2月', '3月', '4月', '5月', '6月']
const maxExpense: number = 80000
const budgetList: BudgetCategoryMeta[] = [
{ label: '人工费', icon: '👷', color: '#BF360C', percent: 30 },
{ label: '材料费', icon: '🧱', color: '#D84315', percent: 35 },
{ label: '设计费', icon: '📐', color: '#FF6F00', percent: 10 },
{ label: '家电', icon: '📺', color: '#1976D2', percent: 15 },
{ label: '软装', icon: '🪴', color: '#2E7D32', percent: 7 },
{ label: '其他', icon: '📋', color: '#8D6E63', percent: 3 }
]
月度支出数据反映了装修费用的典型分布规律:3 月份支出最高(72000 元),因为泥瓦工程和水电改造通常集中在开工初期。maxExpense 设为 80000,作为柱状图纵轴的最大刻度基准,确保所有柱子都能在可视区域内显示。
6.4 统计聚合函数
function getPlanCount(): number { return 7 }
function getActivePlanCount(): number { return 2 }
function getCompletedPlanCount(): number { return 1 }
function getMaterialTotal(): number { return 24 }
function getMaterialPurchased(): number { return 13 }
function getOverallProgress(): number { return 45 }
这组统计函数为页面顶部的数据概览栏提供聚合指标。在当前版本中,它们返回的是硬编码的固定值,但在真实项目中,这些函数应当从数据源动态计算。例如 getActivePlanCount 应当过滤 status === '施工中' 的方案数量,getMaterialPurchased 应当统计 purchased === true 的材料数量。
这种将统计逻辑封装为函数的做法是良好的架构习惯。即使当前返回固定值,未来替换为动态计算时,调用方代码无需任何修改,只需更新函数内部实现即可。这体现了依赖倒置的原则——页面组件依赖抽象的统计接口,而非具体的数据来源。
七、入口与导航架构
7.1 Tab 枚举定义
enum RenoTab {
PLANS = 0,
MATERIALS = 1,
PROGRESS = 2,
BUDGET = 3,
PROFILE = 4
}
应用使用枚举类型定义了五个底部导航标签的索引值。使用枚举而非魔法数字(如直接用 0、1、2、3、4)使得代码更加自文档化——在条件判断中看到 RenoTab.PLANS 比看到 0 要清晰得多。枚举还提供了编译时的类型检查,防止传入无效的标签索引。
7.2 入口组件与内容区域
@Entry
@Component
struct RenoManagerApp {
@State activeTab: RenoTab = RenoTab.PLANS
@Builder contentArea() {
Column() {
if (this.activeTab === RenoTab.PLANS) {
PlanListContent()
} else if (this.activeTab === RenoTab.MATERIALS) {
MaterialListContent()
} else if (this.activeTab === RenoTab.PROGRESS) {
ProgressContent()
} else if (this.activeTab === RenoTab.BUDGET) {
BudgetContent()
} else {
ProfileContent()
}
}
.layoutWeight(1)
}
RenoManagerApp 是应用的入口组件,使用 @Entry 装饰器标记。它维护了一个核心状态 activeTab,记录当前激活的标签页,初始值为 PLANS(装修方案页)。
contentArea 是一个 @Builder 方法,根据 activeTab 的值条件渲染对应的页面组件。这种通过条件分支切换内容区域的方式简洁直观。layoutWeight(1) 使得内容区域占据除底部导航栏以外的所有剩余空间。
值得注意的设计细节是,这里使用 if-else if-else 链而非 switch 语句。在 ArkUI 的声明式语法中,if 条件渲染是框架原生支持的特性,框架能够精确追踪条件分支的变化,只重新渲染发生变化的分支,而 switch 在 ArkUI 中并非一等公民。
7.3 底部导航栏构建
@Builder bottomTabItem(icon: string, label: string, tab: RenoTab) {
Column() {
Text(icon).fontSize(20).opacity(this.activeTab === tab ? 1.0 : 0.4)
Text(label).fontSize(9)
.fontColor(this.activeTab === tab ? '#BF360C' : '#8D6E63')
.fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 1 })
if (this.activeTab === tab) {
Column().width(20).height(3)
.backgroundColor('#BF360C').borderRadius(2).margin({ top: 2 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 5, bottom: 5 })
.onClick(() => { this.activeTab = tab })
}
bottomTabItem 是底部导航栏单个标签项的构建器,接收图标、标签文本和目标 Tab 三个参数。这个构建器巧妙地利用了三元运算符实现选中态与未选中态的视觉差异:
- 图标透明度:选中时为 1.0(完全不透明),未选中时为 0.4(半透明),通过透明度变化突出当前选中项。
- 标签文字颜色:选中时为主色
#BF360C,未选中时为灰色#8D6E63。 - 标签字重:选中时加粗,未选中时常规。
- 选中指示条:仅当
activeTab === tab时渲染一个 20x3 的橙色圆角短条,作为选中态的视觉锚点。
onClick 回调将 activeTab 设置为当前点击的 tab 值,触发状态更新和界面重渲染。由于每个标签项的 layoutWeight(1) 设置,五个标签在 Row 容器中均分宽度。
7.4 根布局组装
build() {
Column() {
this.contentArea()
Row() {
this.bottomTabItem('📋', '装修方案', RenoTab.PLANS)
this.bottomTabItem('🧱', '材料清单', RenoTab.MATERIALS)
this.bottomTabItem('📊', '施工进度', RenoTab.PROGRESS)
this.bottomTabItem('💰', '预算统计', RenoTab.BUDGET)
this.bottomTabItem('👤', '我的', RenoTab.PROFILE)
}
.width('100%')
.backgroundColor('#FFFFFF')
.padding({ top: 4, bottom: 6 })
.border({ width: 1, color: '#FFCCBC' })
}
.width('100%').height('100%')
.backgroundColor('#FFF3E0')
}
}
build() 方法组装了应用的根布局:一个纵向 Column 容器,上方是内容区域(占据剩余空间),下方是底部导航栏 Row。导航栏使用白色背景,顶部有一条 #FFCCBC 色的细边框作为分隔线。整个根容器的背景色为 #FFF3E0(浅橙色),与全局色彩系统保持一致。
五个标签分别使用 Emoji 图标:📋(方案)、🧱(材料)、📊(进度)、💰(预算)、👤(我的)。使用 Emoji 作为图标既免去了引入图标字体库的成本,又保证了跨平台的视觉一致性。
八、装修方案管理模块
装修方案管理是应用的核心模块,承担着方案的全生命周期管理职责。这个模块的实现代码量最大,包含了列表展示、详情查看、新增、编辑和删除确认等多个交互场景。
8.1 组件状态声明
@Component
struct PlanListContent {
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteModal: boolean = false
@State showDetailModal: boolean = false
@State selectedPlan: RenoPlan | null = null
@State editingPlan: RenoPlan | null = null
@State formName: string = ''
@State formStyle: string = '现代简约'
@State formStatus: string = '设计中'
@State formArea: string = ''
@State formBudget: string = ''
@State formDuration: string = ''
@State formDesigner: string = ''
@State formDescription: string = ''
组件声明了丰富的状态变量,可以分为三类:
弹窗显隐控制:showAddModal、showEditModal、showDeleteModal、showDetailModal 四个布尔值分别控制四种弹窗的显示与隐藏。这种多弹窗独立控制的设计使得用户可以在不同操作之间灵活切换。
选中与编辑上下文:selectedPlan 和 editingPlan 用于记录当前被选中查看或正在编辑的方案对象,类型为 RenoPlan | null,初始为 null 表示未选中任何方案。
表单字段:formName 到 formDescription 共八个字符串变量,对应新增方案表单的各个输入字段。formStyle 和 formStatus 有默认值,因为它们通过选择器交互,需要预选一个默认项。
8.2 模态遮罩构建器
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(62,39,35,0.5)')
.onClick(onClose)
}
modalOverlay 是一个通用的模态遮罩构建器,接收一个 onClose 回调函数。它渲染一个全屏的半透明遮罩层,背景色为 rgba(62,39,35,0.5)(半透明深棕色),点击遮罩区域时触发 onClose 回调关闭弹窗。
这个构建器被所有弹窗复用,体现了 DRY(Don’t Repeat Yourself)原则。将遮罩逻辑抽取为独立构建器后,每个弹窗只需在顶部调用 this.modalOverlay(...) 即可获得一致的遮罩效果和点击关闭行为。
8.3 方案卡片构建器
@Builder planCardBuilder(p: RenoPlan) {
Row() {
Column().width(4).height('100%').backgroundColor(PLAN_STATUS_CONFIG[p.status]?.color ?? '#BF360C')
Column() {
Row() {
Column() {
Text(PLAN_STYLE_CONFIG[p.style]?.icon ?? '🏠').fontSize(20)
}.width(40).height(40)
.backgroundColor(PLAN_STYLE_CONFIG[p.style]?.color ?? '#BF360C')
.borderRadius(6).alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(p.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Row() {
Text(PLAN_STYLE_CONFIG[p.style]?.label ?? p.style).fontSize(10)
.fontColor(PLAN_STYLE_CONFIG[p.style]?.color ?? '#BF360C')
Text('·').fontSize(10).fontColor('#8D6E63').margin({ left: 4, right: 4 })
Text(p.area + '㎡').fontSize(10).fontColor('#8D6E63')
Text('·').fontSize(10).fontColor('#8D6E63').margin({ left: 4, right: 4 })
Text(p.duration + '天').fontSize(10).fontColor('#8D6E63')
}.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 8 })
Column() {
Text(PLAN_STATUS_CONFIG[p.status]?.icon ?? '📋').fontSize(14)
Text(PLAN_STATUS_CONFIG[p.status]?.label ?? p.status).fontSize(9)
.fontColor(PLAN_STATUS_CONFIG[p.status]?.color ?? '#BF360C')
.margin({ top: 1 })
}.alignItems(HorizontalAlign.End)
}
.width('100%')
方案卡片是列表中的核心展示单元,其构建逻辑较为复杂,我们逐段分析:
左侧状态色条:Column().width(4).height('100%') 创建了一个 4 像素宽的竖条,背景色取自方案状态配置。这个色条是卡片的视觉锚点,用户扫一眼就能通过颜色判断方案状态——蓝色是设计中、橙红是施工中、绿色是已完工。
风格图标方块:一个 40x40 的圆角方块,背景色为风格主题色,内嵌风格对应的 Emoji 图标。例如现代简约显示 🏠 配蓝灰色背景,北欧风情显示 🌲 配绿色背景。
方案名称与摘要行:名称使用 14 号粗体深棕色字体。下方的摘要行使用 10 号字体,以分隔符"·"连接风格名、面积和工期,所有摘要文字使用灰色 #8D6E63。
状态标识:右上角显示状态图标和状态文字,颜色与状态色条一致。
卡片配置中大量使用了 PLAN_STATUS_CONFIG[p.status]?.color ?? '#BF360C' 这种空值合并模式。?. 可选链确保当状态值不在配置表中时不会抛出异常,?? 空值合并运算符提供兜底默认值。这种防御性编程模式增强了代码的健壮性。
Row() {
Column() {
Text('预算').fontSize(9).fontColor('#8D6E63')
Text('¥' + (p.budget / 10000).toFixed(1) + '万').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor('#BF360C').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('工期').fontSize(9).fontColor('#8D6E63')
Text(p.startDate + ' ~').fontSize(10).fontColor('#3E2723').margin({ top: 1 })
Text(p.endDate).fontSize(10).fontColor('#3E2723')
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('设计师').fontSize(9).fontColor('#8D6E63')
Text(p.designer).fontSize(11).fontColor('#3E2723').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}
.width('100%').margin({ top: 8 })
卡片的第二行区域分为三列,均分宽度展示预算、工期和设计师信息。预算金额通过 (p.budget / 10000).toFixed(1) 将元转换为万元并保留一位小数,例如 285000 元显示为"¥28.5万"。这种金额格式化提升了可读性,避免了长串数字的视觉负担。预算金额使用主色 #BF360C 粗体显示,是卡片中最醒目的数字。
工期信息分两行显示开始日期和结束日期,开始日期后跟"~"符号暗示日期范围。
Row() {
Column() {
Text('施工进度').fontSize(9).fontColor('#8D6E63')
Row() {
Column().width(p.progress + '%').height(6)
.backgroundColor('#BF360C').borderRadius({ topLeft: 3, bottomLeft: 3 })
Column().layoutWeight(1).height(6)
.backgroundColor('#FFCCBC').borderRadius({ topRight: 3, bottomRight: 3 })
}
.width('100%').height(6).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Text(p.progress + '%').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor('#BF360C').margin({ left: 8 })
}
.width('100%').margin({ top: 6 })
}
.layoutWeight(1).padding({ left: 8 })
}
.width('100%').backgroundColor('#FFFFFF')
.borderRadius(8).margin({ left: 12, right: 12, top: 6 })
.padding({ top: 12, bottom: 12, left: 0, right: 12 })
.onClick(() => { this.selectedPlan = p; this.showDetailModal = true })
}
卡片最后一行是施工进度条,这是纯 CSS 式的进度条实现:一个 Row 容器内放两个 Column,第一个宽度为 p.progress + '%',背景色为主色,表示已完成的进度;第二个 layoutWeight(1) 占据剩余空间,背景色为浅色,表示未完成部分。两个 Column 分别设置了左圆角和右圆角,组合成两端圆角的进度条效果。
卡片的 onClick 回调将点击的方案赋值给 selectedPlan 并打开详情弹窗。整个卡片白色背景、8 像素圆角、左右 12 像素外边距,形成了卡片式的视觉风格。
8.4 详情弹窗
@Builder detailModal() {
Column() {
this.modalOverlay(() => { this.showDetailModal = false })
Column() {
Row() {
Text(PLAN_STATUS_CONFIG[this.selectedPlan?.status ?? '设计中']?.icon ?? '📋')
.fontSize(28)
Column() {
Text(this.selectedPlan?.name ?? '').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Row() {
Text(PLAN_STYLE_CONFIG[this.selectedPlan?.style ?? '现代简约']?.label ?? '')
.fontSize(10).fontColor(PLAN_STYLE_CONFIG[this.selectedPlan?.style ?? '现代简约']?.color ?? '#BF360C')
Text('·').fontSize(10).fontColor('#8D6E63').margin({ left: 4, right: 4 })
Text((this.selectedPlan?.area ?? 0) + '㎡').fontSize(10).fontColor('#8D6E63')
Text('·').fontSize(10).fontColor('#8D6E63').margin({ left: 4, right: 4 })
Text((this.selectedPlan?.duration ?? 0) + '天').fontSize(10).fontColor('#8D6E63')
}.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
Text('✕').fontSize(18).fontColor('#8D6E63')
.onClick(() => { this.showDetailModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color('#FFCCBC')
详情弹窗展示了方案的完整信息。弹窗顶部是遮罩层,随后是一个白色圆角容器,宽度 92%、高度 80%,通过 position({ x: '4%', y: '10%' }) 定位在屏幕中央偏上位置。
弹窗头部左侧显示状态大图标(28 号字体),中间是方案名称和摘要信息,右侧是关闭按钮"✕"。由于 selectedPlan 可能为 null,所有访问都使用了 ?? 运算符提供默认值,例如 this.selectedPlan?.name ?? '' 确保即使没有选中方案也不会显示 undefined。
Scroll() {
Column() {
Row() {
Column() {
Text('📐 面积').fontSize(10).fontColor('#8D6E63')
Text((this.selectedPlan?.area ?? 0) + ' ㎡').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('💰 预算').fontSize(10).fontColor('#8D6E63')
Text('¥' + ((this.selectedPlan?.budget ?? 0) / 10000).toFixed(1) + '万').fontSize(14)
.fontWeight(FontWeight.Bold).fontColor('#BF360C').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%').margin({ top: 12 })
详情内容区域使用 Scroll 包裹,确保内容超出弹窗高度时可以滚动浏览。面积和预算并排展示,每个字段都有 Emoji 标签和加粗数值,预算值同样进行万元格式化并用主色突出。
Row() {
Text('施工进度').fontSize(12).fontColor('#3E2723').fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text((this.selectedPlan?.progress ?? 0) + '%').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor('#BF360C')
}.width('100%').margin({ top: 14, left: 4 })
Row() {
Column().width((this.selectedPlan?.progress ?? 0) + '%').height(10)
.backgroundColor('#BF360C').borderRadius({ topLeft: 5, bottomLeft: 5 })
Column().layoutWeight(1).height(10)
.backgroundColor('#FFCCBC').borderRadius({ topRight: 5, bottomRight: 5 })
}
.width('100%').height(10).margin({ top: 6, left: 4, right: 4 })
详情弹窗中的进度条比卡片中的更粗(10 像素高度 vs 6 像素),圆角也更大(5 像素 vs 3 像素),因为在详情视图中进度信息是重点展示对象,需要更强的视觉权重。
Row() {
Text('✏️ 编辑').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#FF6F00').borderRadius(6)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.onClick(() => { this.showDetailModal = false; this.editingPlan = this.selectedPlan; this.showEditModal = true })
Text('🗑️ 删除').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#BF360C').borderRadius(6)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.margin({ left: 8 })
.onClick(() => { this.showDetailModal = false; this.showDeleteModal = true })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 12, bottom: 14 })
弹窗底部是操作按钮区域,居中排列"编辑"和"删除"两个按钮。编辑按钮使用警告橙色 #FF6F00,删除按钮使用主色 #BF360C。两个按钮的点击回调都先关闭详情弹窗,再打开对应的操作弹窗。这种弹窗串联的设计实现了流畅的操作流程转换。
8.5 新增方案弹窗
新增方案弹窗是一个完整的表单界面,包含了方案的所有可输入字段。
@Builder addPlanModal() {
Column() {
this.modalOverlay(() => { this.showAddModal = false })
Column() {
Row() {
Text('➕ 新增装修方案').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor('#8D6E63')
.onClick(() => { this.showAddModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 16, bottom: 10 })
Divider().color('#FFCCBC')
弹窗头部与详情弹窗风格一致:标题加粗 17 号字体,右侧关闭按钮,底部用 Divider 分隔线隔开标题区和内容区。
Scroll() {
Column() {
Text('方案名称').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: '如:阳光城三居室精装' })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
.onChange((v: string) => { this.formName = v })
每个表单字段都遵循统一的模式:先是一个 11 号灰色字段标签,然后是输入控件。TextInput 的 placeholder 提供输入提示,placeholderColor 设为浅色 #FFCCBC,输入区域使用浅橙色背景和 6 像素圆角。onChange 回调将输入值实时同步到对应的 @State 变量。
Text('装修风格').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
Scroll() {
Row() {
ForEach(PLAN_STYLES, (s: string) => {
if (this.formStyle === s) {
Text(PLAN_STYLE_CONFIG[s]?.icon + ' ' + s)
.fontSize(10).fontColor('#FFFFFF').backgroundColor('#BF360C')
.padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(6)
.margin({ left: 3, right: 3 })
} else {
Text(PLAN_STYLE_CONFIG[s]?.icon + ' ' + s)
.fontSize(10).fontColor('#BF360C').backgroundColor('#FFCCBC')
.padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(6)
.margin({ left: 3, right: 3 })
.onClick(() => { this.formStyle = s })
}
})
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(36).margin({ top: 4 })
装修风格选择器是一个水平滚动的标签组。通过 ForEach 遍历 PLAN_STYLES 数组,为每种风格生成一个标签按钮。选中状态的标签为白字橙底,未选中状态为橙字浅橙底。由于风格数量较多(7 种),外层包裹了水平 Scroll 容器,并隐藏滚动条(scrollBar(BarState.Off)),用户可以左右滑动浏览所有选项。
这种标签选择器比下拉菜单更加直观,用户可以一眼看到所有可选项,且每个选项都带有风格图标,增强了辨识度。
Text('状态').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
Row() {
ForEach(PLAN_STATUSES, (st: string) => {
if (this.formStatus === st) {
Text(PLAN_STATUS_CONFIG[st]?.icon + ' ' + st)
.fontSize(10).fontColor('#FFFFFF')
.backgroundColor(PLAN_STATUS_CONFIG[st]?.color ?? '#BF360C')
.padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(6)
.margin({ left: 3, right: 3 })
} else {
Text(PLAN_STATUS_CONFIG[st]?.icon + ' ' + st)
.fontSize(10).fontColor(PLAN_STATUS_CONFIG[st]?.color ?? '#BF360C')
.backgroundColor('#FFF3E0')
.padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(6)
.margin({ left: 3, right: 3 })
.onClick(() => { this.formStatus = st })
}
})
}
.margin({ left: 16, top: 4 })
状态选择器的实现方式与风格选择器类似,但有一个重要区别:选中状态的背景色不是统一的主色,而是取自 PLAN_STATUS_CONFIG[st]?.color,即每个状态的标签在选中时会显示该状态对应的主题色。这使得状态选择器不仅表达了选中态,还通过颜色传达了状态的语义信息。
Text('面积/预算/工期').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
Row() {
TextInput({ placeholder: '面积㎡' })
.placeholderColor('#FFCCBC').fontSize(12).layoutWeight(1)
.backgroundColor('#FFF3E0').borderRadius(6)
.onChange((v: string) => { this.formArea = v })
TextInput({ placeholder: '预算¥' })
.placeholderColor('#FFCCBC').fontSize(12).layoutWeight(1)
.backgroundColor('#FFF3E0').borderRadius(6).margin({ left: 6 })
.onChange((v: string) => { this.formBudget = v })
TextInput({ placeholder: '工期天' })
.placeholderColor('#FFCCBC').fontSize(12).layoutWeight(1)
.backgroundColor('#FFF3E0').borderRadius(6).margin({ left: 6 })
.onChange((v: string) => { this.formDuration = v })
}
.width('90%').margin({ top: 4 })
面积、预算和工期三个数值字段合并为一行展示,每个 TextInput 使用 layoutWeight(1) 均分宽度,中间用 6 像素间距隔开。这种紧凑布局节省了垂直空间,同时这三个字段在语义上是相关的(都是方案的量化指标),放在一起也符合用户的认知逻辑。
Row() {
Text('取消').fontSize(13).fontColor('#8D6E63')
.backgroundColor('#FFF3E0').borderRadius(6)
.padding({ left: 24, right: 24, top: 8, bottom: 8 })
.onClick(() => { this.showAddModal = false })
Text('保存方案').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#BF360C').borderRadius(6)
.padding({ left: 24, right: 24, top: 8, bottom: 8 })
.margin({ left: 10 })
.onClick(() => { this.showAddModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 14, bottom: 14 })
弹窗底部是"取消"和"保存方案"两个按钮。取消按钮为浅橙底灰字,保存按钮为主色底白字,通过颜色对比引导用户优先选择保存操作。当前保存按钮的回调仅关闭弹窗,在真实项目中应当在此处收集所有 form* 状态变量的值,构造新的 RenoPlan 对象并添加到数据列表中。
8.6 编辑与删除弹窗
编辑弹窗的结构与新增弹窗类似,但输入框的 placeholder 使用的是当前编辑方案的字段值:
@Builder editPlanModal() {
Column() {
this.modalOverlay(() => { this.showEditModal = false })
Column() {
Row() {
Text('✏️ 编辑方案').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor('#8D6E63')
.onClick(() => { this.showEditModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 16, bottom: 10 })
Divider().color('#FFCCBC')
Column() {
Text('方案名称').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: this.editingPlan?.name ?? '' })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
Text('面积(m²)').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: (this.editingPlan?.area ?? 0).toString() })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
// ... 预算和描述字段
}
.layoutWeight(1)
Row() {
Text('取消').fontSize(13).fontColor('#8D6E63')
.backgroundColor('#FFF3E0').borderRadius(6)
.padding({ left: 24, right: 24, top: 8, bottom: 8 })
.onClick(() => { this.showEditModal = false })
Text('保存修改').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#FF6F00').borderRadius(6)
.padding({ left: 24, right: 24, top: 8, bottom: 8 })
.margin({ left: 10 })
.onClick(() => { this.showEditModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 14, bottom: 14 })
}
.width('92%').height('65%').backgroundColor('#FFFFFF').borderRadius(12)
.alignItems(HorizontalAlign.Center)
.position({ x: '4%', y: '17%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
编辑弹窗的高度为 65%(比新增弹窗的 80% 矮),因为编辑弹窗只包含名称、面积、预算和描述四个字段,不需要风格和状态选择器。TextInput 的 placeholder 直接显示当前编辑方案的值,如 this.editingPlan?.name ?? '',这样用户打开编辑弹窗就能看到原有数据。保存修改按钮使用警告橙色 #FF6F00,与新增弹窗的主色保存按钮形成区分。
删除确认弹窗采用了居中对话框的样式:
@Builder deletePlanModal() {
Column() {
this.modalOverlay(() => { this.showDeleteModal = false })
Column() {
Text('⚠️').fontSize(40).margin({ top: 22 })
Text('确认删除此方案?').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Text('删除后方案数据不可恢复').fontSize(12).fontColor('#BF360C').margin({ top: 4 })
Row() {
Text(PLAN_STATUS_CONFIG[this.selectedPlan?.status ?? '设计中']?.icon ?? '📋')
.fontSize(18)
Text(this.selectedPlan?.name ?? '').fontSize(13).fontColor('#3E2723')
.fontWeight(FontWeight.Bold).margin({ left: 8 })
}
.backgroundColor('#FFF3E0').borderRadius(8)
.padding({ left: 14, right: 14, top: 10, bottom: 10 }).margin({ top: 14 })
Row() {
Text('取消').fontSize(13).fontColor('#8D6E63')
.backgroundColor('#FFF3E0').borderRadius(6)
.padding({ left: 28, right: 28, top: 8, bottom: 8 })
.onClick(() => { this.showDeleteModal = false })
Text('确认删除').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#BF360C').borderRadius(6)
.padding({ left: 28, right: 28, top: 8, bottom: 8 })
.margin({ left: 10 })
.onClick(() => { this.showDeleteModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 18, bottom: 18 })
}
.width('80%').backgroundColor('#FFFFFF').borderRadius(12)
.alignItems(HorizontalAlign.Center)
.position({ x: '10%', y: '36%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
删除弹窗的设计遵循了危险操作确认的最佳实践:顶部用大号警告图标"⚠️"(40 号字体)引起注意,标题明确说明操作性质,副标题用主色文字强调"删除后不可恢复"的后果。中间展示待删除方案的图标和名称,让用户确认删除对象。底部按钮的"确认删除"使用主色 #BF360C(红色调),在视觉上传达危险操作的警示。
弹窗宽度为 80%(比其他弹窗的 92% 窄),通过 position({ x: '10%', y: '36%' }) 居中偏下定位,形成了居中对话框的视觉效果。所有弹窗都设置了 zIndex(999),确保它们浮于页面内容之上。
8.7 页面主体构建
build() {
Stack() {
Column() {
Row() {
Column() {
Text('装修方案管理').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Text('共' + getPlanCount() + '套方案 · ' + getActivePlanCount() + '套施工中')
.fontSize(10).fontColor('#8D6E63').margin({ top: 3 })
}.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Text('+').fontSize(22).fontColor('#FFFFFF')
.backgroundColor('#BF360C').width(30).height(30).borderRadius(6)
.textAlign(TextAlign.Center)
.onClick(() => { this.showAddModal = true })
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
.backgroundColor('#FFFFFF')
页面使用 Stack 作为根容器,这样弹窗可以叠加在页面内容之上。页面顶部是一个白色标题栏,左侧显示页面标题和统计摘要(“共7套方案 · 2套施工中”),右侧是一个 30x30 的橙色"+"号按钮,点击打开新增弹窗。
Row() {
Column() {
Text(getPlanCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#BF360C')
Text('总方案').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text(getActivePlanCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FF6F00')
Text('施工中').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text(getCompletedPlanCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
Text('已完工').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text(getOverallProgress().toString() + '%').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1976D2')
Text('总进度').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').padding({ top: 10, bottom: 10 })
.backgroundColor('#FFFFFF').margin({ top: 1 })
标题栏下方是四列统计概览栏,分别展示总方案数(7)、施工中数量(2)、已完工数量(1)和总进度(45%)。每个统计项的数字使用 18 号粗体字体,并根据语义选择颜色:总数用主色、施工中用警告橙、已完工用成功绿、总进度用蓝色。下方是 9 号灰色的标签文字。
Scroll() {
Column() {
this.planCardBuilder(mockPlans[0])
this.planCardBuilder(mockPlans[1])
this.planCardBuilder(mockPlans[2])
this.planCardBuilder(mockPlans[3])
this.planCardBuilder(mockPlans[4])
this.planCardBuilder(mockPlans[5])
this.planCardBuilder(mockPlans[6])
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
if (this.showDetailModal) { this.detailModal() }
if (this.showAddModal) { this.addPlanModal() }
if (this.showEditModal) { this.editPlanModal() }
if (this.showDeleteModal) { this.deletePlanModal() }
}
.width('100%').height('100%')
}
列表区域使用 Scroll 包裹,依次调用 planCardBuilder 渲染七条方案数据。滚动条设为隐藏(BarState.Off),保持界面整洁。Stack 的末尾通过条件判断叠加显示各弹窗——当对应的状态变量为 true 时,弹窗构建器被调用并渲染在 Stack 的上层。
九、材料清单管理模块
材料清单模块负责管理装修过程中所有材料的采购信息,支持按品类分类浏览、新增、编辑和删除材料。
9.1 组件状态与材料项构建器
@Component
struct MaterialListContent {
@State selectedCategory: string = '全部'
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteModal: boolean = false
@State selectedMaterial: MaterialItem | null = null
@State editingMaterial: MaterialItem | null = null
@State formName: string = ''
@State formSpec: string = ''
@State formCategory: string = '瓷砖'
@State formQuantity: string = ''
@State formPrice: string = ''
@State formBrand: string = ''
@State formSupplier: string = ''
材料模块的状态结构与方案模块类似,但表单字段换成了材料相关的属性。formCategory 默认值为 '瓷砖',因为瓷砖通常是装修中最早采购的材料品类。
@Builder materialItemBuilder(m: MaterialItem) {
Row() {
Column() {
Text(m.purchased ? '☑' : '☐').fontSize(16)
.fontColor(m.purchased ? '#2E7D32' : '#BF360C')
}.width(30).alignItems(HorizontalAlign.Center)
Column() {
Row() {
Text(m.name).fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(m.purchased ? '#8D6E63' : '#3E2723')
.decoration({
type: m.purchased ? TextDecorationType.LineThrough : TextDecorationType.None
})
.layoutWeight(1)
Text('¥' + m.price).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#BF360C')
}
Row() {
Text(m.spec).fontSize(10).fontColor('#8D6E63')
Column().width(4)
Text(m.brand).fontSize(10).fontColor('#8D6E63')
Column().width(4)
Text(m.quantity + m.unit).fontSize(10).fontColor('#5D4037')
Column().layoutWeight(1)
if (m.purchased) {
Text('已购').fontSize(9).fontColor('#2E7D32')
.backgroundColor('#E8F5E9').padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(4)
} else {
Text('待购').fontSize(9).fontColor('#BF360C')
.backgroundColor('#FFCCBC').padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(4)
}
}.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 6 })
Text('>').fontSize(12).fontColor('#FFCCBC').margin({ left: 4 })
}
.width('100%').backgroundColor('#FFFFFF')
.borderRadius(6).margin({ left: 12, right: 12, top: 4 })
.padding({ top: 10, bottom: 10, left: 10, right: 10 })
.onClick(() => { this.selectedMaterial = m; this.showEditModal = true })
}
材料项构建器的左侧是采购状态勾选框:已购材料显示绿色"☑",未购材料显示橙色"☐"。材料名称在已购状态下会添加删除线(TextDecorationType.LineThrough)并变为灰色,这种视觉处理与购物清单的交互习惯一致——已完成的项目视觉降级,突出未完成项。
右侧价格使用主色粗体显示。第二行是规格、品牌和数量的摘要信息,最右侧是采购状态标签。已购标签为绿底绿字,待购标签为橙底橙字,通过颜色快速区分采购状态。整行点击进入编辑弹窗。
9.2 分类分区展示
@Builder categorySection(cat: string) {
Column() {
Row() {
Text(MATERIAL_CATEGORY_CONFIG[cat]?.icon ?? '📦').fontSize(16)
Text(cat).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
.margin({ left: 6 })
Column().layoutWeight(1)
Text('查看全部').fontSize(10).fontColor('#BF360C')
}
.width('100%').padding({ left: 16, right: 16, top: 10, bottom: 6 })
Column() {
ForEach(mockMaterials.filter((m: MaterialItem) => m.category === cat), (m: MaterialItem) => {
this.materialItemBuilder(m)
})
}
}
.width('100%')
}
categorySection 构建器按材料分类组织列表。每个分类区块的顶部是分类标题行,包含分类图标、分类名称和"查看全部"链接。下方使用 ForEach 配合 filter 方法筛选出该分类下的所有材料并逐条渲染。
mockMaterials.filter((m: MaterialItem) => m.category === cat) 这行代码是数据筛选的核心逻辑。它遍历全部 24 条材料数据,只保留 category 字段等于当前分类名称的项。这种声明式的数据筛选方式简洁高效,是函数式编程在 UI 渲染中的典型应用。
9.3 材料页面主体
build() {
Stack() {
Column() {
Row() {
Text('材料清单').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text(getMaterialPurchased() + '/' + getMaterialTotal() + ' 已购')
.fontSize(10).fontColor('#8D6E63')
Text('+').fontSize(22).fontColor('#FFFFFF')
.backgroundColor('#BF360C').width(30).height(30).borderRadius(6)
.textAlign(TextAlign.Center).margin({ left: 10 })
.onClick(() => { this.showAddModal = true })
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
.backgroundColor('#FFFFFF')
材料页标题栏右侧显示采购进度摘要"13/24 已购",格式为"已购数/总数"。新增按钮的样式与方案页一致。
Row() {
Column() {
Text(((getMaterialPurchased() / getMaterialTotal() * 100).toFixed(0) + '%'))
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#BF360C')
Text('采购进度').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text(getMaterialTotal().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FF6F00')
Text('材料总数').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text(getMaterialPurchased().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
Text('已采购').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text((getMaterialTotal() - getMaterialPurchased()).toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1976D2')
Text('待采购').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').padding({ top: 10, bottom: 10 })
.backgroundColor('#FFFFFF').margin({ top: 1 })
四列统计栏展示了采购进度(54%,由已购数除以总数计算得出)、材料总数(24)、已采购数(13)和待采购数(11)。采购进度百分比通过 (getMaterialPurchased() / getMaterialTotal() * 100).toFixed(0) 动态计算,toFixed(0) 保留整数。
Scroll() {
Column() {
this.categorySection('瓷砖')
this.categorySection('地板')
this.categorySection('涂料')
this.categorySection('灯具')
this.categorySection('卫浴')
this.categorySection('门窗')
this.categorySection('五金')
this.categorySection('家具')
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
if (this.showAddModal) { this.addMaterialModal() }
if (this.showEditModal) { this.editMaterialModal() }
if (this.showDeleteModal) { this.deleteMaterialModal() }
}
.width('100%').height('100%')
}
列表区域依次调用八个分类的 categorySection,形成按品类分区的长列表。每个分类区块内部通过 filter 筛选对应品类的材料,实现了数据的分组展示。
9.4 材料新增弹窗
材料新增弹窗与方案新增弹窗结构相似,但字段针对材料特点进行了定制:
@Builder addMaterialModal() {
Column() {
this.modalOverlay(() => { this.showAddModal = false })
Column() {
Row() {
Text('➕ 新增材料').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor('#8D6E63')
.onClick(() => { this.showAddModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 16, bottom: 10 })
Divider().color('#FFCCBC')
Scroll() {
Column() {
Text('材料分类').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
Row() {
ForEach(MATERIAL_CATEGORIES, (c: string) => {
if (this.formCategory === c) {
Text(MATERIAL_CATEGORY_CONFIG[c]?.icon + ' ' + c)
.fontSize(10).fontColor('#FFFFFF').backgroundColor('#BF360C')
.padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(6)
.margin({ left: 3, right: 3 })
} else {
Text(MATERIAL_CATEGORY_CONFIG[c]?.icon + ' ' + c)
.fontSize(10).fontColor('#BF360C').backgroundColor('#FFCCBC')
.padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(6)
.margin({ left: 3, right: 3 })
.onClick(() => { this.formCategory = c })
}
})
}
.margin({ left: 16, top: 4 })
材料分类选择器遍历 MATERIAL_CATEGORIES 数组(八个品类),每个标签显示品类图标和名称。与方案风格选择器不同的是,材料分类选择器没有使用水平滚动,而是直接排成一行——虽然八个标签可能在一行内放不下,但这种设计在窄屏设备上会让用户感知到标签的溢出,从而理解需要通过其他方式(如换行或滚动)来查看完整列表。
数量和单价字段同样并排展示:
Row() {
Column() {
Text('数量').fontSize(11).fontColor('#8D6E63')
TextInput({ placeholder: '数量' })
.placeholderColor('#FFCCBC').fontSize(12).width('100%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
.onChange((v: string) => { this.formQuantity = v })
}.layoutWeight(1)
Column() {
Text('单价').fontSize(11).fontColor('#8D6E63')
TextInput({ placeholder: '单价¥' })
.placeholderColor('#FFCCBC').fontSize(12).width('100%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
.onChange((v: string) => { this.formPrice = v })
}.layoutWeight(1).margin({ left: 8 })
}
.width('90%').margin({ top: 10 })
数量和单价各有独立的标签,放在两个 Column 中并排排列。这种布局比方案页的面积/预算/工期三列布局更加宽松,因为材料表单的字段标签和输入框需要更多的垂直空间。
十、施工进度模块
施工进度模块通过时间线和甘特图两种可视化方式呈现装修工程各阶段的进展。
10.1 时间线构建器
@Builder phaseTimelineBuilder(ph: Phase) {
Row() {
Column() {
Text(PHASE_CONFIG[ph.name]?.icon ?? '🔨').fontSize(20)
if (ph.status !== '已完工') {
Column().width(2).layoutWeight(1)
.backgroundColor('#FFCCBC').margin({ top: 4 })
}
}
.width(40).alignItems(HorizontalAlign.Center)
时间线的左侧是一个 40 像素宽的纵向列,顶部是阶段图标(20 号字体),下方是一条连接线。连接线仅在阶段未完工时渲染——已完工的阶段是流程的终点或已过的节点,不需要向下的连接线。连接线宽度 2 像素,使用浅橙色,layoutWeight(1) 使其占据图标下方的所有剩余空间,与下一个阶段的图标相连。
这种通过条件渲染连接线的方式,巧妙地实现了时间线的"进行中"和"已完成"视觉区分。
Column() {
Row() {
Column() {
Text(ph.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Row() {
Text(ph.startDate).fontSize(10).fontColor('#8D6E63')
Text('→').fontSize(10).fontColor('#8D6E63').margin({ left: 4, right: 4 })
Text(ph.endDate).fontSize(10).fontColor('#8D6E63')
Column().layoutWeight(1)
if (ph.status === '已完工') {
Text('已完工').fontSize(9).fontColor('#2E7D32')
.backgroundColor('#E8F5E9').padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
} else if (ph.status === '施工中') {
Text('施工中').fontSize(9).fontColor('#BF360C')
.backgroundColor('#FFCCBC').padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
} else {
Text('待开始').fontSize(9).fontColor('#FF6F00')
.backgroundColor('#FFF3E0').padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
}
}
.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Text(ph.progress + '%').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(ph.status === '已完工' ? '#2E7D32' : (ph.status === '施工中' ? '#BF360C' : '#FF6F00'))
.margin({ left: 8 })
}
.width('100%')
时间线卡片的右侧是阶段详情区域。顶部行包含阶段名称(粗体)、工期范围(开始日期 → 结束日期)和状态标签。状态标签的颜色根据三种状态分别使用绿色(已完工)、橙红(施工中)和橙色(待开始),与全局状态配色体系一致。
进度百分比显示在右上角,16 号粗体字体,颜色同样根据状态变化。这种多层嵌套的三元运算符 ph.status === '已完工' ? '#2E7D32' : (ph.status === '施工中' ? '#BF360C' : '#FF6F00') 虽然略显复杂,但在 ArkUI 的属性链式调用中是常见的模式。
Row() {
Column().width(ph.progress + '%').height(8)
.backgroundColor(ph.status === '已完工' ? '#2E7D32' : (ph.status === '施工中' ? '#BF360C' : '#FF6F00'))
.borderRadius({ topLeft: 4, bottomLeft: 4 })
Column().layoutWeight(1).height(8)
.backgroundColor('#FFCCBC').borderRadius({ topRight: 4, bottomRight: 4 })
}
.width('100%').height(8).margin({ top: 6 })
Row() {
Text('👷 ' + ph.owner).fontSize(10).fontColor('#5D4037')
Column().width(8)
Text(ph.workers + '人施工').fontSize(10).fontColor('#8D6E63')
Column().layoutWeight(1)
Text('查看详情 >').fontSize(10).fontColor('#BF360C')
}
.width('100%').margin({ top: 6 })
}
.layoutWeight(1).backgroundColor('#FFFFFF').borderRadius(8)
.padding({ top: 10, bottom: 10, left: 12, right: 12 })
.margin({ left: 8, right: 12 })
.onClick(() => { this.selectedPhase = ph; this.showDetailModal = true })
}
.width('100%').margin({ top: 4 })
}
进度条的颜色也会根据状态变化,使得已完工阶段的进度条为绿色、施工中为橙红、待开始为橙色。卡片底部显示负责人(带👷图标)和施工人数,右侧是"查看详情 >"链接,点击打开阶段详情弹窗。
10.2 甘特图总览
@Builder ganttOverviewBuilder() {
Column() {
Text('📊 甘特图总览').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').width('100%').padding({ left: 16, top: 12, bottom: 8 })
Row() {
Column().width('12.5%').height(16).backgroundColor('#2E7D32')
Column().width('12.5%').height(16).backgroundColor('#2E7D32')
Column().width('10%').height(16).backgroundColor('#BF360C')
Column().width('12.5%').height(16).backgroundColor('#FF6F00')
Column().layoutWeight(1).height(16).backgroundColor('#FFCCBC')
}
.width('100%').height(16).borderRadius(4).margin({ left: 16, right: 16 })
Row() {
Text('拆除').fontSize(8).fontColor('#2E7D32').layoutWeight(1).textAlign(TextAlign.Center)
Text('水电').fontSize(8).fontColor('#2E7D32').layoutWeight(1).textAlign(TextAlign.Center)
Text('泥瓦').fontSize(8).fontColor('#BF360C').layoutWeight(1).textAlign(TextAlign.Center)
Text('木工').fontSize(8).fontColor('#FF6F00').layoutWeight(1).textAlign(TextAlign.Center)
Text('油漆→入住').fontSize(8).fontColor('#FFCCBC').layoutWeight(1).textAlign(TextAlign.Center)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 4 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 6 }).padding({ bottom: 12 })
}
甘特图总览是一个简化的水平条形图,用一个 Row 内的多个 Column 拼接而成。每个 Column 的宽度比例对应了该施工阶段在总工期中的时间占比:
- 拆除工程和水电改造各占 12.5%,已完成(绿色)。
- 泥瓦工程占 10%,正在施工(橙红色)。
- 木工工程占 12.5%,正在施工(橙色)。
- 油漆到入住阶段占剩余空间(约 50%),尚未开始(浅橙色)。
下方的标签行用 8 号字体标注每个色段对应的阶段简称。最后一个标签"油漆→入住"将四个未开始阶段合并标注,因为它们在视觉上共享同一段浅色区域。
这种用纯布局组件拼接出甘特图的方式,虽然不如专业图表库精确,但对于移动端的概览展示已经足够,且实现简单、性能优异。
10.3 阶段详情弹窗
@Builder phaseDetailModal() {
Column() {
this.modalOverlay(() => { this.showDetailModal = false })
Column() {
Row() {
Text(PHASE_CONFIG[this.selectedPhase?.name ?? '']?.icon ?? '🔨')
.fontSize(28)
Column() {
Text(this.selectedPhase?.name ?? '').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Row() {
Text('工期:' + (this.selectedPhase?.startDate ?? '') + ' → ' + (this.selectedPhase?.endDate ?? ''))
.fontSize(10).fontColor('#8D6E63')
}.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
Text('✕').fontSize(18).fontColor('#8D6E63')
.onClick(() => { this.showDetailModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color('#FFCCBC')
Scroll() {
Column() {
Row() {
Text('施工进度').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text((this.selectedPhase?.progress ?? 0) + '%').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#BF360C')
}.width('100%').margin({ top: 14 })
Row() {
Column().width((this.selectedPhase?.progress ?? 0) + '%').height(12)
.backgroundColor('#BF360C').borderRadius({ topLeft: 6, bottomLeft: 6 })
Column().layoutWeight(1).height(12)
.backgroundColor('#FFCCBC').borderRadius({ topRight: 6, bottomRight: 6 })
}
.width('100%').height(12).margin({ top: 6 })
Row() {
Column() {
Text('👷 负责人').fontSize(10).fontColor('#8D6E63')
Text(this.selectedPhase?.owner ?? '').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('👥 施工人数').fontSize(10).fontColor('#8D6E63')
Text((this.selectedPhase?.workers ?? 0) + '人').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%').margin({ top: 14 })
Text('📋 施工说明').fontSize(12).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').margin({ top: 14 })
Text(this.selectedPhase?.notes ?? '').fontSize(12).fontColor('#5D4037')
.width('100%').margin({ top: 4 })
Row() {
Text('当前状态:' + (this.selectedPhase?.status ?? ''))
.fontSize(11)
.fontColor(this.selectedPhase?.status === '已完工' ? '#2E7D32' :
(this.selectedPhase?.status === '施工中' ? '#BF360C' : '#FF6F00'))
.backgroundColor(this.selectedPhase?.status === '已完工' ? '#E8F5E9' :
(this.selectedPhase?.status === '施工中' ? '#FFCCBC' : '#FFF3E0'))
.padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8)
}.width('100%').margin({ top: 12 })
}
.padding({ left: 20, right: 20, bottom: 16 })
}
.layoutWeight(1)
Row() {
Text('✏️ 编辑阶段').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#FF6F00').borderRadius(6)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.onClick(() => { this.showDetailModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 12, bottom: 14 })
}
.width('92%').height('70%').backgroundColor('#FFFFFF').borderRadius(12)
.alignItems(HorizontalAlign.Center)
.position({ x: '4%', y: '15%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
阶段详情弹窗展示了施工阶段的完整信息,包括进度条(12 像素高度,比列表中的 8 像素更粗)、负责人、施工人数、施工说明和当前状态标签。
状态标签的颜色和背景色通过嵌套三元运算符根据三种状态动态选择。施工说明使用 Text 组件展示多行文本,width('100%') 确保文本自动换行。弹窗底部只有一个"编辑阶段"按钮,目前点击后仅关闭弹窗。
10.4 进度页面主体
build() {
Stack() {
Column() {
Row() {
Text('施工进度').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text('总进度 ' + getOverallProgress() + '%').fontSize(11).fontColor('#BF360C')
.fontWeight(FontWeight.Bold)
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
.backgroundColor('#FFFFFF')
this.ganttOverviewBuilder()
Scroll() {
Column() {
this.phaseTimelineBuilder(mockPhases[0])
this.phaseTimelineBuilder(mockPhases[1])
this.phaseTimelineBuilder(mockPhases[2])
this.phaseTimelineBuilder(mockPhases[3])
this.phaseTimelineBuilder(mockPhases[4])
this.phaseTimelineBuilder(mockPhases[5])
this.phaseTimelineBuilder(mockPhases[6])
this.phaseTimelineBuilder(mockPhases[7])
}
.padding({ left: 12, right: 12, top: 6, bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
if (this.showDetailModal) { this.phaseDetailModal() }
}
.width('100%').height('100%')
}
进度页的布局从上到下依次为:标题栏(含总进度百分比)、甘特图总览、可滚动的时间线列表。标题栏右侧的"总进度 45%"使用主色粗体显示,是页面最核心的数据指标。时间线列表包含全部 8 个施工阶段,通过滚动浏览完整工期。
十一、预算统计模块
预算统计模块通过多种图表可视化呈现装修预算的分配和使用情况,是应用中数据可视化最丰富的模块。
11.1 组件属性与预算项构建器
@Component
struct BudgetContent {
totalBudget: number = 285000
totalSpent: number = 276000
totalRemaining: number = 9000
预算组件没有使用 @State 装饰器,而是使用普通属性存储预算数据。这是因为当前版本中预算数据是静态的,不需要响应式更新。totalBudget 为总预算 285000 元(28.5 万),totalSpent 为已花费 276000 元(27.6 万),totalRemaining 为剩余 9000 元(0.9 万)。
@Builder budgetItemBuilder(cat: BudgetCategoryMeta) {
Column() {
Row() {
Text(cat.icon).fontSize(16)
Text(cat.label).fontSize(13).fontWeight(FontWeight.Medium)
.fontColor('#3E2723').margin({ left: 8 })
Column().layoutWeight(1)
Text('¥' + (this.totalBudget * cat.percent / 100 / 10000).toFixed(1) + '万')
.fontSize(12).fontColor('#BF360C').fontWeight(FontWeight.Bold)
}
.width('100%')
Row() {
Column().width(cat.percent + '%').height(8)
.backgroundColor(cat.color).borderRadius({ topLeft: 4, bottomLeft: 4 })
Column().layoutWeight(1).height(8)
.backgroundColor('#FFCCBC').borderRadius({ topRight: 4, bottomRight: 4 })
}
.width('100%').height(8).margin({ top: 6 })
Row() {
Text('占比 ' + cat.percent + '%').fontSize(10).fontColor('#8D6E63')
Column().layoutWeight(1)
Text('已花 ¥' + (this.totalSpent * cat.percent / 100 / 10000).toFixed(1) + '万')
.fontSize(10).fontColor('#FF6F00')
Column().width(8)
Text('余 ¥' + (this.totalRemaining * cat.percent / 100).toFixed(0))
.fontSize(10).fontColor('#2E7D32')
}
.width('100%').margin({ top: 4 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.padding({ top: 10, bottom: 10, left: 12, right: 12 })
.margin({ left: 12, right: 12, top: 4 })
}
budgetItemBuilder 渲染单个预算分类的详情卡片。金额计算公式为 this.totalBudget * cat.percent / 100 / 10000,即总预算乘以该分类的百分比再除以 10000 转为万元。例如材料费:285000 * 35 / 100 / 10000 = 9.975,toFixed(1) 保留一位小数为"10.0万"。
每张卡片包含三行信息:第一行是分类图标、名称和预算金额;第二行是占比进度条(宽度等于百分比,颜色取自分类配置);第三行是占比百分比、已花金额和剩余金额。已花金额使用警告橙色,剩余金额使用成功绿色,通过颜色传达财务健康度。
11.2 饼图(条形模拟)
@Builder pieChartBuilder() {
Column() {
Text('💰 预算分布').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').width('100%').padding({ left: 16, top: 12, bottom: 8 })
Row() {
Column().width('30%').height(20).backgroundColor('#BF360C')
.borderRadius({ topLeft: 10, bottomLeft: 10 })
Column().width('35%').height(20).backgroundColor('#D84315')
Column().width('10%').height(20).backgroundColor('#FF6F00')
Column().width('15%').height(20).backgroundColor('#1976D2')
Column().width('7%').height(20).backgroundColor('#2E7D32')
Column().layoutWeight(1).height(20).backgroundColor('#8D6E63')
.borderRadius({ topRight: 10, bottomRight: 10 })
}
.width('100%').height(20).padding({ left: 16, right: 16 })
Row() {
ForEach(budgetList, (cat: BudgetCategoryMeta) => {
Row() {
Column().width(8).height(8).backgroundColor(cat.color).borderRadius(2)
Text(cat.label).fontSize(9).fontColor('#5D4037').margin({ left: 3 })
}
.margin({ left: 2, right: 2 })
})
}
.width('100%').padding({ left: 16, right: 16, top: 8 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 6 }).padding({ bottom: 12 })
}
由于 ArkUI 内置组件不包含饼图,这里用水平条形图模拟了饼图的效果。一个 Row 内放置六个 Column,每个的宽度百分比对应预算分类的占比(30%、35%、10%、15%、7%、3%),颜色分别取自各分类的配置色。第一个和最后一个 Column 设置了左圆角和右圆角,使整个色条两端圆润。
色条下方是图例区域,通过 ForEach 遍历 budgetList,为每个分类生成一个色块加文字的图例项。8x8 的小色块配合 9 号文字,紧凑地排列在一行中。
11.3 月度柱状图
@Builder monthlyBarChartBuilder() {
Column() {
Text('📊 月度支出趋势').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').width('100%').padding({ left: 16, top: 12, bottom: 8 })
Row() {
ForEach([0, 1, 2, 3, 4, 5], (i: number) => {
Column() {
Text((monthlyExpenses[i] / 10000).toFixed(1) + '万')
.fontSize(9).fontColor('#BF360C').margin({ bottom: 3 })
Column().width(24)
.height((monthlyExpenses[i] / maxExpense * 80).toFixed(0) + 'vp')
.backgroundColor(i === 2 ? '#BF360C' : '#FF7043')
.borderRadius({ topLeft: 4, topRight: 4 })
Text(monthlyLabels[i]).fontSize(9).fontColor('#8D6E63').margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
})
}
.padding({ left: 12, right: 12, bottom: 12 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 8 })
}
月度柱状图通过 ForEach 遍历索引数组 [0, 1, 2, 3, 4, 5],为六个月各渲染一个柱子。每个柱子的结构是:顶部金额标签(万元)、中间彩色柱体、底部月份标签。
柱体高度通过公式 monthlyExpenses[i] / maxExpense * 80 计算,即当月支出除以最大支出基准再乘以 80(最大柱高 80vp)。例如 3 月支出 72000 元,高度为 72000 / 80000 * 80 = 72vp。柱体宽度固定为 24 像素,仅 3 月(索引 2)的柱子使用主色 #BF360C,其余月份使用浅橙色 #FF7043,突出显示支出最高的月份。
柱体顶部设置了 topLeft 和 topRight 圆角,形成圆角顶部的柱状图效果。vp 是 HarmonyOS 的虚拟像素单位,能够根据屏幕密度自动缩放,保证在不同设备上的视觉一致性。
11.4 预算页面主体
build() {
Column() {
Row() {
Text('预算统计').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text('2026年度').fontSize(10).fontColor('#8D6E63')
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
.backgroundColor('#FFFFFF')
Scroll() {
Column() {
Column() {
Row() {
Text('💰 总预算').fontSize(12).fontColor('#8D6E63')
Column().layoutWeight(1)
Text('¥' + (this.totalBudget / 10000).toFixed(1) + '万')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#BF360C')
}.width('100%')
Row() {
Text('已花费:¥' + (this.totalSpent / 10000).toFixed(1) + '万')
.fontSize(11).fontColor('#FF6F00')
Column().layoutWeight(1)
Text('剩余:¥' + (this.totalRemaining / 10000).toFixed(1) + '万')
.fontSize(11).fontColor('#2E7D32')
}.width('100%').margin({ top: 6 })
Row() {
Column().width((this.totalSpent / this.totalBudget * 100).toFixed(0) + '%')
.height(10).backgroundColor('#BF360C')
.borderRadius({ topLeft: 5, bottomLeft: 5 })
Column().layoutWeight(1).height(10)
.backgroundColor('#FFCCBC').borderRadius({ topRight: 5, bottomRight: 5 })
}
.width('100%').height(10).margin({ top: 8 })
Text('使用率 ' + (this.totalSpent / this.totalBudget * 100).toFixed(0) + '%')
.fontSize(10).fontColor('#8D6E63').margin({ top: 4 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.padding({ top: 14, bottom: 14, left: 16, right: 16 })
.margin({ left: 12, right: 12, top: 8 })
this.pieChartBuilder()
this.monthlyBarChartBuilder()
Text('📋 预算分类明细').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').width('100%').padding({ left: 16, top: 14, bottom: 6 })
this.budgetItemBuilder(budgetList[0])
this.budgetItemBuilder(budgetList[1])
this.budgetItemBuilder(budgetList[2])
this.budgetItemBuilder(budgetList[3])
this.budgetItemBuilder(budgetList[4])
this.budgetItemBuilder(budgetList[5])
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
}
预算页的标题栏右侧显示"2026年度"标签,标明预算的统计周期。页面内容从上到下依次为:
- 总预算概览卡片:显示总预算(20 号大字体主色)、已花费和剩余金额、使用率进度条。使用率计算为
(276000 / 285000 * 100).toFixed(0)= 97%,表示预算已使用 97%,接近耗尽。 - 预算分布条形图(饼图模拟)。
- 月度支出趋势柱状图。
- 预算分类明细:六个分类的详情卡片,每张卡片展示该分类的预算金额、占比进度条和已花/剩余金额。
整个页面通过 Scroll 包裹,用户可以滚动浏览所有图表和明细。这种从总览到明细的信息架构,符合用户从宏观到微观的认知习惯。
十二、个人中心模块
个人中心模块汇总了用户信息、房屋信息、装修统计和预算对比等全局数据,并提供了若干功能入口。
12.1 用户信息卡片
@Component
struct ProfileContent {
build() {
Scroll() {
Column() {
Column() {
Row() {
Column() {
Text('👨🔧').fontSize(40)
}.width(64).height(64).backgroundColor('#FFCCBC').borderRadius(8)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text('陈业明').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Text('业主 · 装修总负责').fontSize(11).fontColor('#8D6E63').margin({ top: 3 })
Text('📞 138****6688').fontSize(10).fontColor('#BF360C').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
}
.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 16 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 12 })
用户信息卡片左侧是一个 64x64 的圆角头像区域,背景色为浅橙色,内嵌工人 Emoji 图标。右侧是用户姓名(17 号粗体)、角色标签(“业主 · 装修总负责”)和脱敏手机号(“138****6688”)。手机号使用主色显示,是联系方式的视觉强调。
12.2 房屋信息卡片
Column() {
Text('🏠 房屋信息').fontSize(13).fontWeight(FontWeight.Bold)
.width('100%').padding({ left: 16, top: 12, bottom: 8 })
Row() {
Column() {
Text('小区').fontSize(10).fontColor('#8D6E63')
Text('阳光城三期').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('户型').fontSize(10).fontColor('#8D6E63')
Text('三室两厅两卫').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%').padding({ left: 16, right: 16 })
Row() {
Column() {
Text('面积').fontSize(10).fontColor('#8D6E63')
Text('128㎡').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('楼层').fontSize(10).fontColor('#8D6E63')
Text('18/32层').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%').padding({ left: 16, right: 16, top: 8 })
Row() {
Column() {
Text('朝向').fontSize(10).fontColor('#8D6E63')
Text('南北通透').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('交房日').fontSize(10).fontColor('#8D6E63')
Text('2026-02-01').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%').padding({ left: 16, right: 16, top: 8, bottom: 12 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 8 })
房屋信息卡片以两列网格布局展示了六项房屋属性:小区、户型、面积、楼层、朝向和交房日。每项使用 10 号灰色标签加 12 号深色值的组合,标签在上、值在下。三行两列的布局紧凑而清晰,覆盖了房屋的基本信息维度。
12.3 装修统计仪表盘
Column() {
Text('📊 装修统计').fontSize(13).fontWeight(FontWeight.Bold)
.width('100%').padding({ left: 16, top: 12, bottom: 8 })
Row() {
Column() {
Text('7').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#BF360C')
Text('方案数').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('24').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF6F00')
Text('材料数').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('8').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1976D2')
Text('施工阶段').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('90').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
Text('总工期天').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%').padding({ top: 8, bottom: 8 })
Row() {
Column() {
Text('45%').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#BF360C')
Text('施工进度').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('54%').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
Text('采购进度').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('3').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF6F00')
Text('施工人员').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('97%').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1976D2')
Text('预算使用').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%').padding({ top: 8, bottom: 12 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 8 })
装修统计仪表盘是个人中心的核心区域,以 4x2 的网格布局展示了八项关键指标。上排是方案数(7)、材料数(24)、施工阶段数(8)和总工期天数(90);下排是施工进度(45%)、采购进度(54%)、施工人员数(3)和预算使用率(97%)。
每个指标使用 20 号粗体数字配合语义化颜色:方案数和施工进度用主色、材料数和施工人员用警告橙、施工阶段和预算使用用蓝色、总工期和采购进度用成功绿。这种统一的配色规范使得仪表盘既 colorful 又有序。
12.4 预算对比与功能入口
Column() {
Text('💰 预算对比').fontSize(13).fontWeight(FontWeight.Bold)
.width('100%').padding({ left: 16, top: 12, bottom: 8 })
Row() {
Column() {
Text('总预算').fontSize(10).fontColor('#8D6E63')
Text('¥28.5万').fontSize(15).fontWeight(FontWeight.Bold)
.fontColor('#BF360C').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('已花').fontSize(10).fontColor('#8D6E63')
Text('¥27.6万').fontSize(15).fontWeight(FontWeight.Bold)
.fontColor('#FF6F00').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('剩余').fontSize(10).fontColor('#8D6E63')
Text('¥0.9万').fontSize(15).fontWeight(FontWeight.Bold)
.fontColor('#2E7D32').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%').padding({ top: 8, bottom: 12 })
Row() {
Column().width('97%').height(8)
.backgroundColor('#BF360C').borderRadius({ topLeft: 4, bottomLeft: 4 })
Column().layoutWeight(1).height(8)
.backgroundColor('#FFCCBC').borderRadius({ topRight: 4, bottomRight: 4 })
}
.width('90%').height(8).margin({ bottom: 12 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 8 })
预算对比卡片以三列均分布局展示总预算、已花费和剩余金额,下方是 97% 的使用率进度条。这里的金额直接使用字符串硬编码(“¥28.5万”),而非从变量计算,这是因为这些数据在当前版本中是静态展示。
Column() {
Row() { Text('⚙️').fontSize(16); Text('施工日志').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('>').fontColor('#FFCCBC') }
.width('100%').padding({ top: 10, bottom: 10, left: 4 })
Divider().color('#FFF3E0')
Row() { Text('📷').fontSize(16); Text('现场照片').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('>').fontColor('#FFCCBC') }
.width('100%').padding({ top: 10, bottom: 10, left: 4 })
Divider().color('#FFF3E0')
Row() { Text('📋').fontSize(16); Text('合同管理').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('>').fontColor('#FFCCBC') }
.width('100%').padding({ top: 10, bottom: 10, left: 4 })
Divider().color('#FFF3E0')
Row() { Text('📤').fontSize(16); Text('导出报告').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('>').fontColor('#FFCCBC') }
.width('100%').padding({ top: 10, bottom: 10, left: 4 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 8 })
.padding({ left: 16, right: 16 })
Text('v1.0 · 装修管家 · 2026').fontSize(10).fontColor('#FFCCBC')
.alignSelf(ItemAlign.Center).margin({ top: 16, bottom: 16 })
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
.width('100%').height('100%')
}
}
功能入口列表采用经典的设置页样式:每行一个图标、标题和右箭头">“,行间用浅色 Divider 分隔。四个入口分别是施工日志、现场照片、合同管理和导出报告,覆盖了装修管理的延伸功能。页面底部居中显示版本号"v1.0 · 装修管家 · 2026”,使用浅色字体作为页脚信息。
十三、关键特性对比总结
下面通过一张表格系统对比应用五大模块的关键特性:
| 维度 | 装修方案管理 | 材料清单管理 | 施工进度监控 | 预算统计分析 | 个人中心 |
|---|---|---|---|---|---|
| 核心职责 | 方案全生命周期管理 | 材料采购进度跟踪 | 施工阶段可视化 | 预算分配与消耗监控 | 全局数据汇总展示 |
| 数据模型 | RenoPlan (12字段) | MaterialItem (10字段) | Phase (9字段) | BudgetCategoryMeta | 静态硬编码数据 |
| 列表展示 | 卡片式列表 | 按品类分区列表 | 时间线 + 甘特图 | 条形图 + 柱状图 + 明细卡片 | 信息卡片网格 |
| 交互弹窗 | 详情/新增/编辑/删除 | 新增/编辑/删除 | 阶段详情 | 无弹窗 | 无弹窗 |
| 统计指标 | 4项(总数/施工中/完工/进度) | 4项(进度/总数/已购/待购) | 1项(总进度) | 3项(预算/已花/剩余) | 8项(方案/材料/阶段/工期/进度/采购/人员/预算) |
| 可视化图表 | 进度条 | 进度条 + 采购标签 | 甘特图 + 时间线进度条 | 模拟饼图 + 月度柱状图 + 占比条 | 预算使用率进度条 |
| 表单交互 | 文本输入 + 标签选择器 + 多字段并行输入 | 文本输入 + 分类标签选择器 | 无表单 | 无表单 | 无表单 |
| 状态驱动 | 5种状态(设计/确认/施工/完工/暂停) | 2种状态(已购/待购) | 3种状态(完工/施工中/待开始) | 固定数值 | 固定数值 |
| 配置化程度 | 高(状态/风格均配置化) | 高(分类配置化) | 高(阶段配置化) | 中(分类配置化,金额硬编码) | 低(全部硬编码) |
| 数据格式化 | 金额转万元、日期范围 | 数量+单位拼接 | 日期范围、百分比 | 金额转万元、百分比、vp高度 | 金额万元、百分比 |
十四、详细总结
14.1 架构设计亮点
本文深入解析的"装修管家"应用,在架构设计上展现了多个值得称道的亮点。
配置化驱动的设计理念是贯穿全局的核心思想。从状态元数据到风格配置,从材料分类到施工阶段,所有可枚举的业务概念都被抽象为配置映射表(Record<string, XXXMeta>)。这使得界面渲染逻辑与业务数据彻底解耦——组件代码只负责"如何展示",配置表负责"展示什么"。当业务需求变化(如新增一种装修风格或一种材料品类)时,只需在配置表中添加一条记录,所有相关界面自动获得新选项的渲染能力,无需修改任何组件代码。
类型安全的契约体系通过 TypeScript 接口为整个应用建立了严格的类型约束。每个配置元数据结构都有对应的接口定义,每个数据模型类都有明确的字段类型声明。ArkTS 编译器在编译阶段就能捕获类型不匹配、字段缺失等错误,大幅降低了运行时异常的风险。@Observed 装饰器的使用确保了数据模型变化能够自动驱动 UI 更新,实现了真正的响应式编程。
Builder 模式的组件复用通过 @Builder 装饰器将可复用的 UI 片段封装为独立方法。modalOverlay 遮罩构建器被所有弹窗复用,planCardBuilder 和 materialItemBuilder 分别被列表循环调用。这种模式减少了代码重复,保证了视觉一致性,也使得修改一处即可全局生效。
14.2 视觉设计特点
应用采用了以深橙红色(#BF360C)为主色、浅橙色(#FFF3E0)为背景色的暖色系配色方案,营造出温馨的家居氛围。这种色彩选择与"装修"这一业务场景高度契合——家是温暖的,装修是创造家的过程。
状态色彩体系的设计尤为精细。五种方案状态分别使用蓝、青绿、橙红、绿、橙五种色系,既保证了视觉区分度,又通过色彩心理学传达了状态语义。施工阶段的八种色彩同样经过精心搭配,从拆除的警示红到软装的生机绿,形成了一条视觉化的工程进度色谱。
Emoji 图标的广泛使用是另一个视觉特色。从状态图标到分类图标,从功能入口到统计标签,应用全程使用 Emoji 作为视觉标识。这不仅免去了引入图标资源库的成本,还保证了跨平台的一致性显示,同时为界面增添了亲和力。
14.3 数据可视化的实现策略
在缺乏专业图表库的约束下,应用展现了出色的"纯组件拼图"能力。
进度条通过两个 Column 的宽度百分比拼接实现,配合不同的圆角设置形成两端圆润的效果。这种实现方式极其轻量,不依赖任何第三方库,且性能优异。
甘特图通过一个 Row 内多个不同宽度的 Column 拼接而成,每个色段的宽度比例对应工期占比,颜色对应完成状态。虽然精度不如专业甘特图组件,但作为移动端的概览展示已经足够。
模拟饼图同样采用水平条形拼接的方式,六个色段的宽度等于各分类的百分比。下方配合 ForEach 生成的图例,形成了完整的"饼图"信息表达。
月度柱状图通过 ForEach 遍历数据数组,每个柱子的高度通过支出金额与最大值的比例计算得出,使用 vp 单位确保跨设备的一致性。最高月份的柱子使用主色突出显示,其余月份使用浅色,实现了数据重点的视觉强调。
14.4 交互设计的考量
应用在交互设计上体现了移动端应用的多个最佳实践。
弹窗的层级管理通过 Stack 容器配合 zIndex(999) 实现,弹窗叠加在页面内容之上,点击遮罩或关闭按钮可 dismiss。四种弹窗(详情、新增、编辑、删除)通过独立的状态变量控制,互不干扰,用户可以在不同操作之间灵活切换。
删除操作的二次确认是安全设计的体现。删除弹窗使用大号警告图标、红色确认按钮和"不可恢复"的提示文案,确保用户在执行不可逆操作前有充分的认知准备。
表单的标签选择器比传统的下拉菜单更加直观和高效。用户可以一眼看到所有可选项,通过点击切换选中状态,选中态和未选中态通过颜色反转明确区分。这种交互方式在选项数量适中(5-8 个)时体验最佳。
滚动区域的隐藏滚动条(scrollBar(BarState.Off))是移动端界面的常见做法。在触摸屏设备上,用户通过手势滚动,滚动条的存在反而会干扰视觉,隐藏后界面更加整洁。
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

设置API为24的模板项目:

初始化项目,自动下载相关依赖:

完整代码:
// ============ 类型定义 ============
interface PlanStatusMeta {
label: string
color: string
bg: string
icon: string
level: number
}
Text('✏️ 编辑方案').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor('#8D6E63')
.onClick(() => { this.showEditModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 16, bottom: 10 })
Divider().color('#FFCCBC')
Column() {
Text('方案名称').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: this.editingPlan?.name ?? '' })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
Text('面积(m²)').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: (this.editingPlan?.area ?? 0).toString() })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
Text('预算(元)').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: (this.editingPlan?.budget ?? 0).toString() })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
Text('方案描述').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextArea({ placeholder: this.editingPlan?.description ?? '' })
.placeholderColor('#FFCCBC').fontSize(12).width('90%').height(60)
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
}
.layoutWeight(1)
Row() {
Text('取消').fontSize(13).fontColor('#8D6E63')
.backgroundColor('#FFF3E0').borderRadius(6)
.padding({ left: 24, right: 24, top: 8, bottom: 8 })
.onClick(() => { this.showEditModal = false })
Text('保存修改').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#FF6F00').borderRadius(6)
.padding({ left: 24, right: 24, top: 8, bottom: 8 })
.margin({ left: 10 })
.onClick(() => { this.showEditModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 14, bottom: 14 })
}
.width('92%').height('65%').backgroundColor('#FFFFFF').borderRadius(12)
.alignItems(HorizontalAlign.Center)
.position({ x: '4%', y: '17%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
@Builder deletePlanModal() {
Column() {
this.modalOverlay(() => { this.showDeleteModal = false })
Column() {
Text('⚠️').fontSize(40).margin({ top: 22 })
Text('确认删除此方案?').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Text('删除后方案数据不可恢复').fontSize(12).fontColor('#BF360C').margin({ top: 4 })
Row() {
Text(PLAN_STATUS_CONFIG[this.selectedPlan?.status ?? '设计中']?.icon ?? '📋')
.fontSize(18)
Text(this.selectedPlan?.name ?? '').fontSize(13).fontColor('#3E2723')
.fontWeight(FontWeight.Bold).margin({ left: 8 })
}
.backgroundColor('#FFF3E0').borderRadius(8)
.padding({ left: 14, right: 14, top: 10, bottom: 10 }).margin({ top: 14 })
Row() {
Text('取消').fontSize(13).fontColor('#8D6E63')
.backgroundColor('#FFF3E0').borderRadius(6)
.padding({ left: 28, right: 28, top: 8, bottom: 8 })
.onClick(() => { this.showDeleteModal = false })
Text('确认删除').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#BF360C').borderRadius(6)
.padding({ left: 28, right: 28, top: 8, bottom: 8 })
.margin({ left: 10 })
.onClick(() => { this.showDeleteModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 18, bottom: 18 })
}
.width('80%').backgroundColor('#FFFFFF').borderRadius(12)
.alignItems(HorizontalAlign.Center)
.position({ x: '10%', y: '36%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
build() {
Stack() {
Column() {
Row() {
Column() {
Text('装修方案管理').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Text('共' + getPlanCount() + '套方案 · ' + getActivePlanCount() + '套施工中')
.fontSize(10).fontColor('#8D6E63').margin({ top: 3 })
}.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Text('+').fontSize(22).fontColor('#FFFFFF')
.backgroundColor('#BF360C').width(30).height(30).borderRadius(6)
.textAlign(TextAlign.Center)
.onClick(() => { this.showAddModal = true })
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
.backgroundColor('#FFFFFF')
Row() {
Column() {
Text(getPlanCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#BF360C')
Text('总方案').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text(getActivePlanCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FF6F00')
Text('施工中').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text(getCompletedPlanCount().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
Text('已完工').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text(getOverallProgress().toString() + '%').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1976D2')
Text('总进度').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').padding({ top: 10, bottom: 10 })
.backgroundColor('#FFFFFF').margin({ top: 1 })
Scroll() {
Column() {
this.planCardBuilder(mockPlans[0])
this.planCardBuilder(mockPlans[1])
this.planCardBuilder(mockPlans[2])
this.planCardBuilder(mockPlans[3])
this.planCardBuilder(mockPlans[4])
this.planCardBuilder(mockPlans[5])
this.planCardBuilder(mockPlans[6])
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
if (this.showDetailModal) { this.detailModal() }
if (this.showAddModal) { this.addPlanModal() }
if (this.showEditModal) { this.editPlanModal() }
if (this.showDeleteModal) { this.deletePlanModal() }
}
.width('100%').height('100%')
}
}
// ============ 材料清单页 ============
@Component
struct MaterialListContent {
@State selectedCategory: string = '全部'
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteModal: boolean = false
@State selectedMaterial: MaterialItem | null = null
@State editingMaterial: MaterialItem | null = null
@State formName: string = ''
@State formSpec: string = ''
@State formCategory: string = '瓷砖'
@State formQuantity: string = ''
@State formPrice: string = ''
@State formBrand: string = ''
@State formSupplier: string = ''
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(62,39,35,0.5)')
.onClick(onClose)
}
@Builder materialItemBuilder(m: MaterialItem) {
Row() {
Column() {
Text(m.purchased ? '☑' : '☐').fontSize(16)
.fontColor(m.purchased ? '#2E7D32' : '#BF360C')
}.width(30).alignItems(HorizontalAlign.Center)
Column() {
Row() {
Text(m.name).fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(m.purchased ? '#8D6E63' : '#3E2723')
.decoration({
type: m.purchased ? TextDecorationType.LineThrough : TextDecorationType.None
})
.layoutWeight(1)
Text('¥' + m.price).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#BF360C')
}
Row() {
Text(m.spec).fontSize(10).fontColor('#8D6E63')
Column().width(4)
Text(m.brand).fontSize(10).fontColor('#8D6E63')
Column().width(4)
Text(m.quantity + m.unit).fontSize(10).fontColor('#5D4037')
Column().layoutWeight(1)
if (m.purchased) {
Text('已购').fontSize(9).fontColor('#2E7D32')
.backgroundColor('#E8F5E9').padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(4)
} else {
Text('待购').fontSize(9).fontColor('#BF360C')
.backgroundColor('#FFCCBC').padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(4)
}
}.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 6 })
Text('>').fontSize(12).fontColor('#FFCCBC').margin({ left: 4 })
}
.width('100%').backgroundColor('#FFFFFF')
.borderRadius(6).margin({ left: 12, right: 12, top: 4 })
.padding({ top: 10, bottom: 10, left: 10, right: 10 })
.onClick(() => { this.selectedMaterial = m; this.showEditModal = true })
}
@Builder categorySection(cat: string) {
Column() {
Row() {
Text(MATERIAL_CATEGORY_CONFIG[cat]?.icon ?? '📦').fontSize(16)
Text(cat).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#3E2723')
.margin({ left: 6 })
Column().layoutWeight(1)
Text('查看全部').fontSize(10).fontColor('#BF360C')
}
.width('100%').padding({ left: 16, right: 16, top: 10, bottom: 6 })
Column() {
ForEach(mockMaterials.filter((m: MaterialItem) => m.category === cat), (m: MaterialItem) => {
this.materialItemBuilder(m)
})
}
}
.width('100%')
}
@Builder addMaterialModal() {
Column() {
this.modalOverlay(() => { this.showAddModal = false })
Column() {
Row() {
Text('➕ 新增材料').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor('#8D6E63')
.onClick(() => { this.showAddModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 16, bottom: 10 })
Divider().color('#FFCCBC')
Scroll() {
Column() {
Text('材料分类').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
Row() {
ForEach(MATERIAL_CATEGORIES, (c: string) => {
if (this.formCategory === c) {
Text(MATERIAL_CATEGORY_CONFIG[c]?.icon + ' ' + c)
.fontSize(10).fontColor('#FFFFFF').backgroundColor('#BF360C')
.padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(6)
.margin({ left: 3, right: 3 })
} else {
Text(MATERIAL_CATEGORY_CONFIG[c]?.icon + ' ' + c)
.fontSize(10).fontColor('#BF360C').backgroundColor('#FFCCBC')
.padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(6)
.margin({ left: 3, right: 3 })
.onClick(() => { this.formCategory = c })
}
})
}
.margin({ left: 16, top: 4 })
Text('材料名称').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: '如:客厅通体大理石瓷砖' })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
.onChange((v: string) => { this.formName = v })
Text('规格').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: '如:800x800mm 哑光' })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
.onChange((v: string) => { this.formSpec = v })
Row() {
Column() {
Text('数量').fontSize(11).fontColor('#8D6E63')
TextInput({ placeholder: '数量' })
.placeholderColor('#FFCCBC').fontSize(12).width('100%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
.onChange((v: string) => { this.formQuantity = v })
}.layoutWeight(1)
Column() {
Text('单价').fontSize(11).fontColor('#8D6E63')
TextInput({ placeholder: '单价¥' })
.placeholderColor('#FFCCBC').fontSize(12).width('100%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
.onChange((v: string) => { this.formPrice = v })
}.layoutWeight(1).margin({ left: 8 })
}
.width('90%').margin({ top: 10 })
Text('品牌').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: '品牌名称' })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
.onChange((v: string) => { this.formBrand = v })
Text('供应商').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: '供应渠道' })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6)
.margin({ top: 4 })
.onChange((v: string) => { this.formSupplier = v })
}
}
.layoutWeight(1)
Row() {
Text('取消').fontSize(13).fontColor('#8D6E63')
.backgroundColor('#FFF3E0').borderRadius(6)
.padding({ left: 24, right: 24, top: 8, bottom: 8 })
.onClick(() => { this.showAddModal = false })
Text('保存').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#BF360C').borderRadius(6)
.padding({ left: 24, right: 24, top: 8, bottom: 8 })
.margin({ left: 10 })
.onClick(() => { this.showAddModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 14, bottom: 14 })
}
.width('92%').height('80%').backgroundColor('#FFFFFF').borderRadius(12)
.alignItems(HorizontalAlign.Center)
.position({ x: '4%', y: '10%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
@Builder editMaterialModal() {
Column() {
this.modalOverlay(() => { this.showEditModal = false })
Column() {
Row() {
Text('✏️ 编辑材料').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text('✕').fontSize(18).fontColor('#8D6E63')
.onClick(() => { this.showEditModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 16, bottom: 10 })
Divider().color('#FFCCBC')
Column() {
Text('材料名称').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: this.editingMaterial?.name ?? '' })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6).margin({ top: 4 })
Text('规格').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: this.editingMaterial?.spec ?? '' })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6).margin({ top: 4 })
Text('数量').fontSize(11).fontColor('#8D6E63').margin({ top: 10, left: 20 })
TextInput({ placeholder: (this.editingMaterial?.quantity ?? 0).toString() })
.placeholderColor('#FFCCBC').fontSize(13).width('90%')
.backgroundColor('#FFF3E0').borderRadius(6).margin({ top: 4 })
}
.layoutWeight(1)
Row() {
Text('删除').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#BF360C').borderRadius(6)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.onClick(() => { this.showEditModal = false; this.showDeleteModal = true })
Text('保存修改').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#FF6F00').borderRadius(6)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.margin({ left: 10 })
.onClick(() => { this.showEditModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 14, bottom: 14 })
}
.width('92%').height('65%').backgroundColor('#FFFFFF').borderRadius(12)
.alignItems(HorizontalAlign.Center)
.position({ x: '4%', y: '17%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
@Builder deleteMaterialModal() {
Column() {
this.modalOverlay(() => { this.showDeleteModal = false })
Column() {
Text('⚠️').fontSize(40).margin({ top: 22 })
Text('确认删除此材料?').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Text('删除后不可恢复').fontSize(12).fontColor('#BF360C').margin({ top: 4 })
Row() {
Text(this.selectedMaterial?.name ?? '').fontSize(13).fontColor('#3E2723')
.fontWeight(FontWeight.Bold)
}
.backgroundColor('#FFF3E0').borderRadius(8)
.padding({ left: 14, right: 14, top: 10, bottom: 10 }).margin({ top: 14 })
Row() {
Text('取消').fontSize(13).fontColor('#8D6E63')
.backgroundColor('#FFF3E0').borderRadius(6)
.padding({ left: 28, right: 28, top: 8, bottom: 8 })
.onClick(() => { this.showDeleteModal = false })
Text('确认删除').fontSize(13).fontColor('#FFFFFF')
.backgroundColor('#BF360C').borderRadius(6)
.padding({ left: 28, right: 28, top: 8, bottom: 8 })
.margin({ left: 10 })
.onClick(() => { this.showDeleteModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ left: 20, right: 20, top: 18, bottom: 18 })
}
.width('80%').backgroundColor('#FFFFFF').borderRadius(12)
.alignItems(HorizontalAlign.Center)
.position({ x: '10%', y: '36%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
build() {
Stack() {
Column() {
Row() {
Text('材料清单').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text(getMaterialPurchased() + '/' + getMaterialTotal() + ' 已购')
.fontSize(10).fontColor('#8D6E63')
Text('+').fontSize(22).fontColor('#FFFFFF')
.backgroundColor('#BF360C').width(30).height(30).borderRadius(6)
.textAlign(TextAlign.Center).margin({ left: 10 })
.onClick(() => { this.showAddModal = true })
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
.backgroundColor('#FFFFFF')
Row() {
Column() {
Text(((getMaterialPurchased() / getMaterialTotal() * 100).toFixed(0) + '%'))
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#BF360C')
Text('采购进度').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text(getMaterialTotal().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FF6F00')
Text('材料总数').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text(getMaterialPurchased().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
Text('已采购').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text((getMaterialTotal() - getMaterialPurchased()).toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1976D2')
Text('待采购').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').padding({ top: 10, bottom: 10 })
.backgroundColor('#FFFFFF').margin({ top: 1 })
Scroll() {
Column() {
this.categorySection('瓷砖')
this.categorySection('地板')
this.categorySection('涂料')
this.categorySection('灯具')
this.categorySection('卫浴')
this.categorySection('门窗')
this.categorySection('五金')
this.categorySection('家具')
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
if (this.showAddModal) { this.addMaterialModal() }
if (this.showEditModal) { this.editMaterialModal() }
if (this.showDeleteModal) { this.deleteMaterialModal() }
}
.width('100%').height('100%')
}
}
// ============ 施工进度页 ============
@Component
struct ProgressContent {
@State showDetailModal: boolean = false
@State selectedPhase: Phase | null = null
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(62,39,35,0.5)')
.onClick(onClose)
}
@Builder phaseTimelineBuilder(ph: Phase) {
Row() {
Column() {
Text(PHASE_CONFIG[ph.name]?.icon ?? '🔨').fontSize(20)
if (ph.status !== '已完工') {
Column().width(2).layoutWeight(1)
.backgroundColor('#FFCCBC').margin({ top: 4 })
}
}
.width(40).alignItems(HorizontalAlign.Center)
Column() {
Row() {
Column() {
Text(ph.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Row() {
Text(ph.startDate).fontSize(10).fontColor('#8D6E63')
Text('→').fontSize(10).fontColor('#8D6E63').margin({ left: 4, right: 4 })
Text(ph.endDate).fontSize(10).fontColor('#8D6E63')
Column().layoutWeight(1)
if (ph.status === '已完工') {
Text('已完工').fontSize(9).fontColor('#2E7D32')
.backgroundColor('#E8F5E9').padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
} else if (ph.status === '施工中') {
Text('施工中').fontSize(9).fontColor('#BF360C')
.backgroundColor('#FFCCBC').padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
} else {
Text('待开始').fontSize(9).fontColor('#FF6F00')
.backgroundColor('#FFF3E0').padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
}
}
.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Text(ph.progress + '%').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(ph.status === '已完工' ? '#2E7D32' : (ph.status === '施工中' ? '#BF360C' : '#FF6F00'))
.margin({ left: 8 })
}
.width('100%')
Row() {
Column().width(ph.progress + '%').height(8)
.backgroundColor(ph.status === '已完工' ? '#2E7D32' : (ph.status === '施工中' ? '#BF360C' : '#FF6F00'))
.borderRadius({ topLeft: 4, bottomLeft: 4 })
Column().layoutWeight(1).height(8)
.backgroundColor('#FFCCBC').borderRadius({ topRight: 4, bottomRight: 4 })
}
.width('100%').height(8).margin({ top: 6 })
Row() {
Text('👷 ' + ph.owner).fontSize(10).fontColor('#5D4037')
Column().width(8)
Text(ph.workers + '人施工').fontSize(10).fontColor('#8D6E63')
Column().layoutWeight(1)
Text('查看详情 >').fontSize(10).fontColor('#BF360C')
}
.width('100%').margin({ top: 6 })
}
.layoutWeight(1).backgroundColor('#FFFFFF').borderRadius(8)
.padding({ top: 10, bottom: 10, left: 12, right: 12 })
.margin({ left: 8, right: 12 })
.onClick(() => { this.selectedPhase = ph; this.showDetailModal = true })
}
.width('100%').margin({ top: 4 })
}
@Builder ganttOverviewBuilder() {
Column() {
Text('📊 甘特图总览').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').width('100%').padding({ left: 16, top: 12, bottom: 8 })
Row() {
Column().width('12.5%').height(16).backgroundColor('#2E7D32')
Column().width('12.5%').height(16).backgroundColor('#2E7D32')
Column().width('10%').height(16).backgroundColor('#BF360C')
Column().width('12.5%').height(16).backgroundColor('#FF6F00')
Column().layoutWeight(1).height(16).backgroundColor('#FFCCBC')
}
.width('100%').height(16).borderRadius(4).margin({ left: 16, right: 16 })
Row() {
Text('拆除').fontSize(8).fontColor('#2E7D32').layoutWeight(1).textAlign(TextAlign.Center)
Text('水电').fontSize(8).fontColor('#2E7D32').layoutWeight(1).textAlign(TextAlign.Center)
Text('泥瓦').fontSize(8).fontColor('#BF360C').layoutWeight(1).textAlign(TextAlign.Center)
Text('木工').fontSize(8).fontColor('#FF6F00').layoutWeight(1).textAlign(TextAlign.Center)
Text('油漆→入住').fontSize(8).fontColor('#FFCCBC').layoutWeight(1).textAlign(TextAlign.Center)
}
.width('100%').padding({ left: 16, right: 16 }).margin({ top: 4 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 6 }).padding({ bottom: 12 })
}
@Builder phaseDetailModal() {
Column() {
this.modalOverlay(() => { this.showDetailModal = false })
Column() {
Row() {
Text(PHASE_CONFIG[this.selectedPhase?.name ?? '']?.icon ?? '🔨')
.fontSize(28)
Column() {
Text(this.selectedPhase?.name ?? '').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Row() {
Text('工期:' + (this.selectedPhase?.startDate ?? '') + ' → ' + (this.selectedPhase?.endDate ?? ''))
.fontSize(10).fontColor('#8D6E63')
}.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
Text('✕').fontSize(18).fontColor('#8D6E63')
.onClick(() => { this.showDetailModal = false })
}
.width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
Divider().color('#FFCCBC')
Scroll() {
Column() {
Row() {
Text('施工进度').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text((this.selectedPhase?.progress ?? 0) + '%').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#BF360C')
}.width('100%').margin({ top: 14 })
Row() {
Column().width((this.selectedPhase?.progress ?? 0) + '%').height(12)
.backgroundColor('#BF360C').borderRadius({ topLeft: 6, bottomLeft: 6 })
Column().layoutWeight(1).height(12)
.backgroundColor('#FFCCBC').borderRadius({ topRight: 6, bottomRight: 6 })
}
}
.padding({ left: 12, right: 12, bottom: 12 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 8 })
}
build() {
Column() {
Row() {
Text('预算统计').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Column().layoutWeight(1)
Text('2026年度').fontSize(10).fontColor('#8D6E63')
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
.backgroundColor('#FFFFFF')
Scroll() {
Column() {
Column() {
Row() {
Text('💰 总预算').fontSize(12).fontColor('#8D6E63')
Column().layoutWeight(1)
Text('¥' + (this.totalBudget / 10000).toFixed(1) + '万')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#BF360C')
}.width('100%')
Row() {
Text('已花费:¥' + (this.totalSpent / 10000).toFixed(1) + '万')
.fontSize(11).fontColor('#FF6F00')
Column().layoutWeight(1)
Text('剩余:¥' + (this.totalRemaining / 10000).toFixed(1) + '万')
.fontSize(11).fontColor('#2E7D32')
}.width('100%').margin({ top: 6 })
Row() {
Column().width((this.totalSpent / this.totalBudget * 100).toFixed(0) + '%')
.height(10).backgroundColor('#BF360C')
.borderRadius({ topLeft: 5, bottomLeft: 5 })
Column().layoutWeight(1).height(10)
.backgroundColor('#FFCCBC').borderRadius({ topRight: 5, bottomRight: 5 })
}
.width('100%').height(10).margin({ top: 8 })
Text('使用率 ' + (this.totalSpent / this.totalBudget * 100).toFixed(0) + '%')
.fontSize(10).fontColor('#8D6E63').margin({ top: 4 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.padding({ top: 14, bottom: 14, left: 16, right: 16 })
.margin({ left: 12, right: 12, top: 8 })
this.pieChartBuilder()
this.monthlyBarChartBuilder()
Text('📋 预算分类明细').fontSize(13).fontWeight(FontWeight.Bold)
.fontColor('#3E2723').width('100%').padding({ left: 16, top: 14, bottom: 6 })
this.budgetItemBuilder(budgetList[0])
this.budgetItemBuilder(budgetList[1])
this.budgetItemBuilder(budgetList[2])
this.budgetItemBuilder(budgetList[3])
this.budgetItemBuilder(budgetList[4])
this.budgetItemBuilder(budgetList[5])
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
}
}
// ============ 我的页面 ============
@Component
struct ProfileContent {
build() {
Scroll() {
Column() {
Column() {
Row() {
Column() {
Text('👨🔧').fontSize(40)
}.width(64).height(64).backgroundColor('#FFCCBC').borderRadius(8)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text('陈业明').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#3E2723')
Text('业主 · 装修总负责').fontSize(11).fontColor('#8D6E63').margin({ top: 3 })
Text('📞 138****6688').fontSize(10).fontColor('#BF360C').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
}
.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 16 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 12 })
Column() {
Text('🏠 房屋信息').fontSize(13).fontWeight(FontWeight.Bold)
.width('100%').padding({ left: 16, top: 12, bottom: 8 })
Row() {
Column() {
Text('小区').fontSize(10).fontColor('#8D6E63')
Text('阳光城三期').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('户型').fontSize(10).fontColor('#8D6E63')
Text('三室两厅两卫').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%').padding({ left: 16, right: 16 })
Row() {
Column() {
Text('面积').fontSize(10).fontColor('#8D6E63')
Text('128㎡').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('楼层').fontSize(10).fontColor('#8D6E63')
Text('18/32层').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%').padding({ left: 16, right: 16, top: 8 })
Row() {
Column() {
Text('朝向').fontSize(10).fontColor('#8D6E63')
Text('南北通透').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('交房日').fontSize(10).fontColor('#8D6E63')
Text('2026-02-01').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%').padding({ left: 16, right: 16, top: 8, bottom: 12 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 8 })
Column() {
Text('📊 装修统计').fontSize(13).fontWeight(FontWeight.Bold)
.width('100%').padding({ left: 16, top: 12, bottom: 8 })
Row() {
Column() {
Text('7').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#BF360C')
Text('方案数').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('24').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF6F00')
Text('材料数').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('8').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1976D2')
Text('施工阶段').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('90').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
Text('总工期天').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%').padding({ top: 8, bottom: 8 })
Row() {
Column() {
Text('45%').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#BF360C')
Text('施工进度').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('54%').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#2E7D32')
Text('采购进度').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('3').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF6F00')
Text('施工人员').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('97%').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1976D2')
Text('预算使用').fontSize(9).fontColor('#8D6E63').margin({ top: 1 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%').padding({ top: 8, bottom: 12 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 8 })
Column() {
Text('💰 预算对比').fontSize(13).fontWeight(FontWeight.Bold)
.width('100%').padding({ left: 16, top: 12, bottom: 8 })
Row() {
Column() {
Text('总预算').fontSize(10).fontColor('#8D6E63')
Text('¥28.5万').fontSize(15).fontWeight(FontWeight.Bold)
.fontColor('#BF360C').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('已花').fontSize(10).fontColor('#8D6E63')
Text('¥27.6万').fontSize(15).fontWeight(FontWeight.Bold)
.fontColor('#FF6F00').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('剩余').fontSize(10).fontColor('#8D6E63')
Text('¥0.9万').fontSize(15).fontWeight(FontWeight.Bold)
.fontColor('#2E7D32').margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%').padding({ top: 8, bottom: 12 })
Row() {
Column().width('97%').height(8)
.backgroundColor('#BF360C').borderRadius({ topLeft: 4, bottomLeft: 4 })
Column().layoutWeight(1).height(8)
.backgroundColor('#FFCCBC').borderRadius({ topRight: 4, bottomRight: 4 })
}
.width('90%').height(8).margin({ bottom: 12 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 8 })
Column() {
Row() { Text('⚙️').fontSize(16); Text('施工日志').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('>').fontColor('#FFCCBC') }
.width('100%').padding({ top: 10, bottom: 10, left: 4 })
Divider().color('#FFF3E0')
Row() { Text('📷').fontSize(16); Text('现场照片').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('>').fontColor('#FFCCBC') }
.width('100%').padding({ top: 10, bottom: 10, left: 4 })
Divider().color('#FFF3E0')
Row() { Text('📋').fontSize(16); Text('合同管理').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('>').fontColor('#FFCCBC') }
.width('100%').padding({ top: 10, bottom: 10, left: 4 })
Divider().color('#FFF3E0')
Row() { Text('📤').fontSize(16); Text('导出报告').fontSize(13).layoutWeight(1).margin({ left: 10 }); Text('>').fontColor('#FFCCBC') }
.width('100%').padding({ top: 10, bottom: 10, left: 4 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(8)
.margin({ left: 12, right: 12, top: 8 })
.padding({ left: 16, right: 16 })
Text('v1.0 · 装修管家 · 2026').fontSize(10).fontColor('#FFCCBC')
.alignSelf(ItemAlign.Center).margin({ top: 16, bottom: 16 })
}
.padding({ bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
.width('100%').height('100%')
}
}
14.5 可改进的方向
尽管应用在当前版本中已经展现了良好的架构和视觉设计,但仍有一些可以改进的方向。
数据的动态化是最优先的改进方向。当前所有数据都是硬编码的 Mock 数据,统计函数返回固定值。在真实项目中,应当将数据存储迁移到持久化层(如关系型数据库或 Preferences),统计函数改为从数据源动态聚合计算,表单提交时真正将数据写入存储。
全局色彩令牌的统一引用。虽然定义了 COLORS 色彩常量,但组件代码中大量直接使用了十六进制颜色值。建议统一通过 COLORS.xxx 引用,使得主题切换和色彩调整更加集中可控。
表单验证的完善。当前表单提交按钮仅关闭弹窗,没有进行任何输入校验。在真实场景中,应当对必填字段进行非空校验,对数值字段进行格式和范围校验(如面积不能为负、预算应当为正整数等),并在校验失败时给出明确的错误提示。
无障碍支持的增强。当前界面的文字字号普遍偏小(8-13 号字体为主),对于视力不佳的用户可能存在阅读困难。建议增加无障碍标签(accessibilityText)和更大的触摸区域,提升应用的可用性。
14.6 结语
综上所述,这款"装修管家"应用通过五个功能模块的精心设计,构建了一个覆盖装修全流程的数字化管理平台。从方案的创建到材料的采购,从施工的进度跟踪到预算的实时监控,再到个人中心的全局汇总,应用形成了一个完整的信息闭环。

在技术实现上,配置化驱动、类型安全契约、Builder 模式复用、纯组件数据可视化等设计策略,使得代码在保持丰富功能的同时具备了良好的可维护性和可扩展性。暖色系的视觉设计、精细的状态色彩体系、Emoji 图标的亲和力表达,共同营造了符合家居场景的产品气质。
这款应用不仅是一个实用的装修管理工具,更是一个优秀的 ArkUI 声明式开发实践范例。它展示了如何在不依赖大量第三方库的情况下,利用框架原生组件构建出功能完整、视觉精致、交互流畅的移动端应用。对于希望深入学习 HarmonyOS 应用开发的工程师而言,这份代码值得逐行研读和借鉴。
更多推荐

所有评论(0)