车辆维修保养管理系统:HarmonyOS ArkTS API 24移动端应用的从主入口组件到各业务Tab组件,层层嵌套,通过 @State、@Link、@Prop 等装饰器实现组件间的状态传递与响应
一、行业背景与应用意义
1.1 汽车后市场的数字化转型浪潮
随着中国汽车保有量突破三亿辆大关,汽车后市场规模持续扩大,汽车维修保养行业正迎来前所未有的发展机遇。然而,大量中小型汽修门店仍然依赖纸质工单、手工记账和电话沟通的传统管理模式,这不仅导致工作效率低下,更容易出现工单遗漏、配件库存混乱、客户信息丢失等问题。

在这样的行业背景下,一套轻量化、移动化的车辆维修保养管理系统显得尤为重要。门店管理者可以通过手机或平板随时查看维修工单状态、车辆档案、配件库存以及经营统计数据,技师可以快速录入维修项目并跟踪进度,从而实现从"接车-维修-交付"的全流程数字化管理。
1.2 移动端管理系统的核心价值
传统的汽修管理系统多部署在PC端,技师在车间工作时难以实时操作。而移动端管理系统具有天然的优势:技师可以在车间内用一部设备完成工单创建、状态更新、配件查询等操作,大幅减少了在工位和办公区之间往返的时间成本。
本系统正是面向这一需求设计的一套移动端汽修管理应用。它涵盖了维修工单管理、车辆档案管理、配件库存管理、经营统计分析以及门店信息管理五大核心模块,形成了一个完整的业务闭环。
1.3 技术选型与框架特性
本系统基于声明式UI框架开发,采用了组件化、状态驱动的架构思想。整个应用由多个独立组件构成,每个组件负责管理自己的内部状态和UI渲染。通过响应式的状态绑定机制,当数据发生变化时,UI会自动更新,无需手动操作DOM节点。
系统还引入了可观察对象(Observed Class)的数据模型设计,使得业务数据与UI视图之间的同步变得自然而高效。配合纯函数式的数据过滤和统计逻辑,代码的可测试性和可维护性得到了显著提升。
二、系统总体架构概览
2.1 架构分层设计
整个系统在代码组织上采用了清晰的分层架构,自底向上可以划分为以下几个层次:
- 接口定义层:使用 interface 定义类型契约,约束颜色配色、应用配置、工单状态、配件分类等数据结构。
- 数据模型层:使用 @Observed 修饰的可观察类定义核心业务实体,包括工单、车辆、配件和统计实体。
- 静态配置层:以 const 常量形式集中管理配色方案、门店信息、状态映射和分类映射,确保配置的单一数据源。
- Mock数据层:提供模拟的业务数据,覆盖工单、车辆、配件、月度统计、分类库存和技师信息等多个维度。
- 工具函数层:一组纯函数,负责状态查找、数据格式化、数据过滤和统计计算,无副作用,便于复用和测试。
- UI组件层:从主入口组件到各业务Tab组件,层层嵌套,通过 @State、@Link、@Prop 等装饰器实现组件间的状态传递与响应。
2.2 功能模块划分
系统共划分为五个主要功能模块,通过底部导航栏进行切换:
| 模块 | 标识 | 核心功能 |
|---|---|---|
| 维修工单 | WorkOrder | 工单的创建、编辑、删除、搜索与状态筛选 |
| 车辆档案 | Vehicle | 车辆信息的登记、编辑、删除与搜索 |
| 配件库存 | Parts | 配件的录入、编辑、删除、分类筛选与库存预警 |
| 经营统计 | Stats | 工单量趋势、营收、库存占比、技师工时等多维统计 |
| 我的门店 | Profile | 门店信息展示、营收概览、技师团队与快捷服务入口 |
三、接口定义层:类型契约的建立
3.1 颜色调色板接口
系统首先定义了一个统一的颜色调色板接口,用于规范整个应用的视觉风格。
interface ColorPalette {
primary: string
accent: string
bg: string
warning: string
success: string
textPrimary: string
textSecondary: string
textHint: string
border: string
cardBg: string
white: string
danger: string
dark: string
}

逐行解析:
primary:主色调,用于导航栏、按钮等关键交互元素的背景色。accent:强调色,用于高亮选中状态、链接文字和可交互元素的强调展示。bg:全局背景色,整个应用页面的底色。warning:警告色,用于低库存提醒、待处理事项等需要引起注意的场景。success:成功色,用于已完成状态标识和库存充足提示。textPrimary:主要文字颜色,用于标题、重要数据展示。textSecondary:次要文字颜色,用于描述性文字、辅助信息。textHint:提示文字颜色,用于占位符、最次要的说明文字。border:边框颜色,用于卡片边框、分割线。cardBg:卡片背景色,通常为白色,与全局背景形成层次。white:纯白色,用于特定场景的白色背景。danger:危险色,用于删除按钮、错误提示。dark:深色调,用于渐变效果的深色端。
通过将所有颜色集中定义在一个接口中,后续修改配色方案时只需调整一处,所有引用该常量的组件都会自动生效,这体现了配置集中管理的最佳实践。
3.2 应用配置接口
interface AppConfig {
appName: string
shopName: string
shopAddress: string
shopPhone: string
serviceTime: string
techLeader: string
}

逐行解析:
appName:应用名称,在标题栏或品牌展示处使用。shopName:门店全称,包含品牌和门店类型信息。shopAddress:门店详细地址,用于客户导航和服务范围确认。shopPhone:门店联系电话,方便客户预约和咨询。serviceTime:营业时间,告知客户服务时段。techLeader:技术负责人,体现门店的技术管理水平。
这个接口将门店的基础信息结构化,避免了在代码中散落硬编码的字符串。
3.3 工单状态配置接口
工单状态是整个系统最核心的业务概念之一。系统通过两个接口来定义状态的配置结构。
interface WorkStatusCfg {
label: string
color: string
bg: string
}
interface WorkStatusMap {
pending: WorkStatusCfg
working: WorkStatusCfg
waitingParts: WorkStatusCfg
finished: WorkStatusCfg
delivered: WorkStatusCfg
}

逐行解析:
WorkStatusCfg 接口定义了单个状态的三要素:
label:状态的中文显示文本,如"待检修"、“维修中”。color:状态对应的文字颜色,用于状态标签的文字着色。bg:状态对应的背景颜色,用于状态标签的背景着色。
WorkStatusMap 接口则将五种工单状态组织为一个映射结构:
pending:待检修状态,工单已创建但尚未开始维修。working:维修中状态,技师正在执行维修作业。waitingParts:待配件状态,维修因缺少配件而暂停。finished:已完成状态,维修作业全部完成。delivered:已交付状态,车辆已交还车主。
这种设计将状态语义与视觉表现绑定在一起,使用时只需通过状态名称查找即可获取完整的展示配置。
3.4 配件分类配置接口
interface CategoryCfg {
label: string
color: string
bg: string
}
interface CategoryMap {
oil: CategoryCfg
filter: CategoryCfg
brake: CategoryCfg
tire: CategoryCfg
battery: CategoryCfg
spark: CategoryCfg
wiper: CategoryCfg
other: CategoryCfg
}

逐行解析:
CategoryCfg 与 WorkStatusCfg 结构完全一致,同样包含标签、文字颜色和背景颜色三个字段。
CategoryMap 定义了八种配件分类:
oil:机油类配件,如嘉实多、美孚等品牌润滑油。filter:滤芯类配件,包括机油滤芯、空气滤芯、空调滤芯。brake:刹车片类配件,涉及制动系统的摩擦材料。tire:轮胎类配件,涵盖各规格型号的轮胎。battery:电瓶类配件,汽车蓄电池。spark:火花塞类配件,点火系统的关键部件。wiper:雨刮类配件,雨刮片和雨刮器总成。other:其他类配件,用于无法归入以上分类的配件。
3.5 技师信息接口
interface TechInfo {
name: string
title: string
years: string
phone: string
skill: string
}

逐行解析:
name:技师姓名或工号简称,如"李工"。title:技师职称,如"高级技师"、“中级技师”。years:从业年限,以字符串形式存储,如"12年"。phone:联系电话,已做脱敏处理,如"139****1101"。skill:擅长领域,如"发动机/底盘"、“制动/电气”。
这个接口为技师团队管理提供了标准化的数据结构。
四、数据模型层:可观察的业务实体
4.1 可观察类的核心思想
系统中所有核心业务实体都使用了 @Observed 装饰器进行修饰。这是一个关键的架构决策。被 @Observed 修饰的类,其实例的属性变化可以被框架自动追踪。当这些属性被绑定到UI组件的状态上时,属性的变化会自动触发对应UI的刷新,无需开发者手动调用更新方法。
这种响应式编程模式大幅简化了状态管理的复杂度。例如,当技师修改了某个工单的状态后,工单卡片上的状态标签颜色会自动更新;当新增一辆车辆后,车辆列表会自动增加一条记录——这一切都是框架自动完成的。
4.2 工单数据模型
@Observed class WorkOrder {
id: number = 0
orderNo: string = ''
plateNo: string = ''
model: string = ''
project: string = ''
mechanic: string = ''
status: string = ''
inTime: string = ''
expectTime: string = ''
cost: number = 0
constructor(id: number, orderNo: string, plateNo: string, model: string, project: string,
mechanic: string, status: string, inTime: string, expectTime: string, cost: number) {
this.id = id
this.orderNo = orderNo
this.plateNo = plateNo
this.model = model
this.project = project
this.mechanic = mechanic
this.status = status
this.inTime = inTime
this.expectTime = expectTime
this.cost = cost
}
}

逐行解析:
工单模型包含了维修作业的全生命周期信息:
id:工单唯一标识符,自增整数,用于内部数据管理。orderNo:工单编号,格式为"WX-2025-0812-001",包含日期和序号信息,便于人工识别。plateNo:车牌号码,如"京A8W5J6",是车辆识别的关键字段。model:车型信息,如"丰田卡罗拉",便于技师了解车辆基本情况。project:维修项目描述,如"更换刹车片+机油保养",记录本次维修的具体内容。mechanic:负责维修的技师姓名,如"李工"。status:当前工单状态,取值为"待检修"、“维修中”、“待配件”、"已完成"或"已交付"之一。inTime:车辆进厂时间,格式为"MM/DD HH:mm"。expectTime:预计完成时间,格式同上。cost:维修费用,以元为单位的数值。
构造函数接收全部十个参数并进行赋值初始化。每个属性都提供了默认值,确保对象在未完全初始化时不会出现 undefined 问题。
4.3 车辆数据模型
@Observed class Vehicle {
id: number = 0
plateNo: string = ''
brand: string = ''
model: string = ''
year: number = 0
color: string = ''
owner: string = ''
phone: string = ''
mileage: number = 0
lastMaintenance: string = ''
constructor(id: number, plateNo: string, brand: string, model: string, year: number,
color: string, owner: string, phone: string, mileage: number, lastMaintenance: string) {
this.id = id
this.plateNo = plateNo
this.brand = brand
this.model = model
this.year = year
this.color = color
this.owner = owner
this.phone = phone
this.mileage = mileage
this.lastMaintenance = lastMaintenance
}
}

逐行解析:
车辆模型记录了车辆的完整档案信息:
id:车辆档案唯一标识。plateNo:车牌号,作为车辆的主键索引。brand:品牌,如"丰田"、“大众”。model:具体型号,如"卡罗拉2023款"。year:年份,整型数字,如2023。color:车身颜色,如"珍珠白"、“曜石黑”。owner:车主姓名。phone:车主联系电话,已脱敏处理。mileage:行驶里程,以公里为单位的整型数值。lastMaintenance:上次保养日期,格式为"YYYY-MM-DD"。
车辆档案是汽修门店最重要的客户资产之一。通过完整的车辆档案,门店可以主动提醒客户进行定期保养,提升客户粘性和复购率。
4.4 配件数据模型
@Observed class PartsItem {
id: number = 0
name: string = ''
code: string = ''
category: string = ''
stock: number = 0
price: number = 0
supplier: string = ''
minStock: number = 0
constructor(id: number, name: string, code: string, category: string, stock: number,
price: number, supplier: string, minStock: number) {
this.id = id
this.name = name
this.code = code
this.category = category
this.stock = stock
this.price = price
this.supplier = supplier
this.minStock = minStock
}
}

逐行解析:
配件模型是库存管理的核心数据结构:
id:配件记录唯一标识。name:配件名称,如"博世刹车片前轮",包含品牌和规格信息。code:配件编号,如"P-001",用于快速检索和盘点。category:配件分类,取值为中文分类名,如"机油"、“刹车片”。stock:当前库存数量。price:单价,以元为单位。supplier:供应商名称,如"博世授权店"。minStock:最低库存预警阈值,当实际库存低于此值时触发预警。
特别值得注意的设计是 minStock 字段。它与 stock 配合使用,在UI层实现了动态的库存预警功能:当库存低于最低库存时,卡片会显示橙色"低库存"警示标签;否则显示绿色"库存充足"标签。
4.5 统计实体模型
系统定义了三个用于统计展示的数据模型,分别对应月度工单量、分类库存量和技师工时统计。
@Observed class MonthStat {
month: string = ''
count: number = 0
constructor(month: string, count: number) {
this.month = month
this.count = count
}
}
@Observed class CategoryStockStat {
category: string = ''
stock: number = 0
constructor(category: string, stock: number) {
this.category = category
this.stock = stock
}
}
@Observed class MechanicStat {
name: string = ''
hours: number = 0
orders: number = 0
cost: number = 0
constructor(name: string, hours: number, orders: number, cost: number) {
this.name = name
this.hours = hours
this.orders = orders
this.cost = cost
}
}

逐行解析:
MonthStat 用于柱状图展示,month 存储月份标签(如"2月"),count 存储该月的工单数量。
CategoryStockStat 用于横向进度条展示,category 存储分类名称,stock 存储该分类的库存总量。
MechanicStat 用于技师绩效展示,包含四个维度:
name:技师姓名。hours:本月工时数。orders:完成工单数。cost:创造产值(元)。
这三个统计实体虽然结构简单,但它们是统计页面的数据基石,支撑了柱状图、进度条和列表三种可视化形式。
五、静态配置层:配色与业务映射的单一数据源
5.1 全局配色常量
const COLORS: ColorPalette = {
primary: '#37474F',
accent: '#29B6F6',
bg: '#ECEFF1',
warning: '#FF7043',
success: '#66BB6A',
textPrimary: '#37474F',
textSecondary: '#607D8B',
textHint: '#90A4AE',
border: '#DDDDDD',
cardBg: '#FFFFFF',
white: '#FFFFFF',
danger: '#EF5350',
dark: '#263238'
}

逐行解析:
系统采用了"科技蓝灰金属色系"的视觉设计语言:
primary设为#37474F,这是一种深蓝灰色(Blue Grey 800),给人以沉稳、专业的工业感,非常适合汽修行业的科技化定位。accent设为#29B6F6,亮蓝色(Light Blue 400),用于强调交互元素和选中状态,与深蓝灰形成明暗对比。bg设为#ECEFF1,极浅的蓝灰色(Blue Grey 50),作为全局背景,不喧宾夺主。warning设为#FF7043,深橙色(Deep Orange 400),用于警示信息,视觉冲击力强。success设为#66BB6A,绿色(Green 400),传达积极、完成的语义。danger设为#EF5350,红色(Red 400),用于危险操作如删除按钮。dark设为#263238,更深的蓝灰色(Blue Grey 900),用于渐变效果的暗端和底部导航栏背景。
这套配色方案遵循了 Material Design 的色彩规范,通过蓝灰色系的主色调营造专业感,配合橙色和绿色作为功能性的点缀色,整体视觉统一而富有层次。
5.2 门店信息配置
const APP_CONFIG: AppConfig = {
appName: '车管家',
shopName: '速达汽修连锁 · 中关村旗舰店',
shopAddress: '北京市海淀区中关村大街128号',
shopPhone: '010-6288-5566',
serviceTime: '08:30 - 20:30',
techLeader: '王师傅'
}
逐行解析:
- 应用名为"车管家",简洁易记,传达管家式服务的理念。
- 门店全称为"速达汽修连锁 · 中关村旗舰店",体现了连锁品牌和旗舰店定位。
- 地址位于北京海淀区中关村,科技氛围浓厚。
- 营业时间为早八点半到晚八点半,覆盖大部分工作时间。
- 技术负责人为"王师傅",增强了门店的信任感。
5.3 工单状态映射
const WORK_STATUS: WorkStatusMap = {
pending: { label: '待检修', color: '#FF7043', bg: '#FFF3E0' },
working: { label: '维修中', color: '#29B6F6', bg: '#E1F5FE' },
waitingParts: { label: '待配件', color: '#FFA726', bg: '#FFF8E1' },
finished: { label: '已完成', color: '#66BB6A', bg: '#E8F5E9' },
delivered: { label: '已交付', color: '#37474F', bg: '#ECEFF1' }
}
逐行解析:
每种状态都有精心设计的颜色搭配:
- 待检修(橙色系):表示工单刚创建,尚未开始,橙色传达"需要注意"的语义。
- 维修中(蓝色系):表示正在作业中,蓝色传达"进行中"的冷静感。
- 待配件(琥珀色系):表示因配件短缺而等待,琥珀色传达"暂停等待"的含义。
- 已完成(绿色系):表示维修完成,绿色传达"成功"的积极信号。
- 已交付(蓝灰色系):表示流程终结,使用主色调表示归档状态。
每种状态的 color 和 bg 都是同色系的深浅搭配,确保文字在背景上有足够的对比度,同时保持视觉的和谐统一。
5.4 配件分类映射
const PART_CATEGORY: CategoryMap = {
oil: { label: '机油', color: '#FF7043', bg: '#FFF3E0' },
filter: { label: '滤芯', color: '#29B6F6', bg: '#E1F5FE' },
brake: { label: '刹车片', color: '#EF5350', bg: '#FFEBEE' },
tire: { label: '轮胎', color: '#5C6BC0', bg: '#E8EAF6' },
battery: { label: '电瓶', color: '#66BB6A', bg: '#E8F5E9' },
spark: { label: '火花塞', color: '#FFA726', bg: '#FFF8E1' },
wiper: { label: '雨刮', color: '#26A69A', bg: '#E0F2F1' },
other: { label: '其他', color: '#78909C', bg: '#ECEFF1' }
}
逐行解析:
八种配件分类各自拥有独特的颜色标识,便于用户在浏览配件列表时快速区分:
- 机油用橙色,呼应润滑油的色泽。
- 滤芯用蓝色,传达清洁过滤的概念。
- 刹车片用红色,暗示制动安全的重要性。
- 轮胎用靛蓝色,区别于其他分类。
- 电瓶用绿色,暗示电力和能量。
- 火花塞用琥珀色,呼应点火的火花。
- 雨刮用青色,暗示雨水。
- 其他用蓝灰色,中性低调。
5.5 筛选器配置
const WORK_STATUS_FILTERS: string[] = ['全部', '待检修', '维修中', '待配件', '已完成', '已交付']
const CATEGORY_FILTERS: string[] = ['全部', '机油', '滤芯', '刹车片', '轮胎', '电瓶', '火花塞', '雨刮', '其他']
逐行解析:
两个筛选器数组分别用于工单页和配件页的横向滚动筛选标签栏。它们都以"全部"开头,允许用户查看所有数据。这种设计简单直接:选中"全部"时显示所有记录,选中具体状态/分类时仅显示匹配的记录。筛选标签的渲染逻辑会根据当前选中项改变背景色和文字颜色,提供清晰的视觉反馈。
六、Mock数据层:贴近真实业务的模拟数据
6.1 工单模拟数据
系统预置了20条工单模拟数据,覆盖了五种状态和多种维修项目类型。
const mockWorkOrders: WorkOrder[] = [
new WorkOrder(1, 'WX-2025-0812-001', '京A8W5J6', '丰田卡罗拉', '更换刹车片+机油保养', '李工', '维修中', '08/12 09:30', '08/12 16:00', 680),
new WorkOrder(2, 'WX-2025-0812-002', '京N6K3M2', '大众朗逸', '更换轮胎四条+四轮定位', '张工', '待配件', '08/12 08:50', '08/13 11:00', 2360),
// ... 更多工单
new WorkOrder(20, 'WX-2025-0812-020', '京Z7H2N5', '雷克萨斯ES', '常规保养+空调滤芯', '李工', '已交付', '08/11 13:30', '08/11 15:20', 560)
]
逐行解析(以第一条为例):
- 工单ID为1,编号"WX-2025-0812-001"表示2025年8月12日的第1号工单。
- 车牌"京A8W5J6",车型"丰田卡罗拉"。
- 维修项目"更换刹车片+机油保养",这是一项组合维修任务。
- 指派技师"李工",当前状态"维修中"。
- 进厂时间"08/12 09:30",预计完成时间"08/12 16:00"。
- 维修费用680元。
20条工单数据涵盖了从待检修到已交付的全状态流转,维修项目包括刹车系统、机油保养、轮胎更换、电瓶更换、空调检修、变速箱保养、火花塞更换等多种常见业务场景,费用从260元到2360元不等,充分模拟了真实门店的工单分布。
6.2 车辆模拟数据
const mockVehicles: Vehicle[] = [
new Vehicle(1, '京A8W5J6', '丰田', '卡罗拉2023款', 2023, '珍珠白', '赵磊', '139****2233', 45000, '2025-07-01'),
// ... 更多车辆
new Vehicle(16, '京V8R6C3', '五菱', '宏光S 2019款', 2019, '晴空银', '何军', '182****8856', 136000, '2025-01-20')
]
逐行解析(以第一条为例):
- 车辆ID为1,车牌"京A8W5J6"。
- 品牌"丰田",型号"卡罗拉2023款",年份2023。
- 颜色"珍珠白",车主"赵磊",电话已脱敏。
- 行驶里程45000公里,上次保养日期2025年7月1日。
16条车辆数据覆盖了合资品牌(丰田、大众、本田、别克、日产)、豪华品牌(奔驰、宝马、奥迪、雷克萨斯)、国产自主品牌(比亚迪、哈弗、吉利、五菱)以及新能源品牌(特斯拉),里程从1.5万到13.6万公里,年份从2019到2023,充分模拟了真实门店的客户车辆构成。
6.3 配件模拟数据
const mockParts: PartsItem[] = [
new PartsItem(1, '博世刹车片前轮', 'P-001', '刹车片', 32, 180, '博世授权店', 10),
new PartsItem(2, '博世刹车片后轮', 'P-002', '刹车片', 8, 168, '博世授权店', 10),
// ... 更多配件
new PartsItem(18, '壳牌刹车油DOT4', 'P-018', '其他', 22, 68, '壳牌授权', 10)
]
逐行解析(以第一条为例):
- 配件ID为1,名称"博世刹车片前轮",编号"P-001"。
- 分类"刹车片",当前库存32件,单价180元。
- 供应商"博世授权店",最低库存预警值10件。
18条配件数据中,部分配件的库存低于最低库存阈值(如"博世刹车片后轮"库存8件,低于最低库存10件),这些配件在UI上会自动显示"低库存"预警标签。配件品牌涵盖博世、嘉实多、美孚、曼牌、马勒、米其林、普利司通、瓦尔塔、风帆、NGK、3M、长城、菲罗多、壳牌等主流汽配品牌,真实还原了汽修门店的配件库。
6.4 统计模拟数据
const mockMonthStats: MonthStat[] = [
new MonthStat('2月', 112),
new MonthStat('3月', 138),
new MonthStat('4月', 126),
new MonthStat('5月', 152),
new MonthStat('6月', 145),
new MonthStat('7月', 168)
]
const mockCategoryStocks: CategoryStockStat[] = [
new CategoryStockStat('机油', 69),
new CategoryStockStat('滤芯', 113),
new CategoryStockStat('刹车片', 51),
new CategoryStockStat('轮胎', 19),
new CategoryStockStat('电瓶', 15),
new CategoryStockStat('火花塞', 60),
new CategoryStockStat('雨刮', 33),
new CategoryStockStat('其他', 22)
]
const mockMechanicStats: MechanicStat[] = [
new MechanicStat('李工', 186, 42, 28600),
new MechanicStat('张工', 172, 38, 34200),
new MechanicStat('王工', 168, 35, 29800),
new MechanicStat('赵工', 145, 30, 21500)
]
const mockTechnicians: TechInfo[] = [
{ name: '李工', title: '高级技师', years: '12年', phone: '139****1101', skill: '发动机/底盘' },
{ name: '张工', title: '高级技师', years: '10年', phone: '139****1102', skill: '制动/电气' },
{ name: '王工', title: '中级技师', years: '7年', phone: '139****1103', skill: '变速箱/空调' },
{ name: '赵工', title: '中级技师', years: '5年', phone: '139****1104', skill: '轮胎/钣金' }
]
逐行解析:
月度工单统计展示了2月至7月共6个月的数据,工单量从112到168不等,呈整体上升趋势,体现了门店业务的增长态势。
分类库存统计展示了8个配件分类的库存总量,滤芯库存最多(113件),电瓶库存最少(15件)。
技师统计展示了4位技师的工时、完成工单数和产值。张工虽然工单数不是最多,但产值最高(34200元),说明他接手的可能是高价值维修项目。
技师信息列表补充了职称、年限和擅长领域,李工和张工为高级技师,王工和赵工为中级技师,形成了合理的技术梯队。
七、工具函数层:纯函数式的业务逻辑
7.1 状态与分类查找函数
function getStatusCfg(status: string): WorkStatusCfg {
if (status === '待检修') {
return WORK_STATUS.pending
}
if (status === '维修中') {
return WORK_STATUS.working
}
if (status === '待配件') {
return WORK_STATUS.waitingParts
}
if (status === '已完成') {
return WORK_STATUS.finished
}
return WORK_STATUS.delivered
}
function getCategoryCfg(category: string): CategoryCfg {
if (category === '机油') {
return PART_CATEGORY.oil
}
if (category === '滤芯') {
return PART_CATEGORY.filter
}
if (category === '刹车片') {
return PART_CATEGORY.brake
}
if (category === '轮胎') {
return PART_CATEGORY.tire
}
if (category === '电瓶') {
return PART_CATEGORY.battery
}
if (category === '火花塞') {
return PART_CATEGORY.spark
}
if (category === '雨刮') {
return PART_CATEGORY.wiper
}
return PART_CATEGORY.other
}
逐行解析:
这两个函数是连接业务字符串与视觉配置的桥梁。getStatusCfg 接收中文状态字符串,返回对应的 WorkStatusCfg 配置对象;getCategoryCfg 接收中文分类字符串,返回对应的 CategoryCfg 配置对象。
函数采用 if 判断的逐级匹配方式,最后一个分支作为默认返回值。这种写法虽然不如 switch 或 Map 查找简洁,但在不支持某些高级语法的环境中具有良好的兼容性,且逻辑清晰易读。
这两个函数在UI渲染时被频繁调用——每张工单卡片的状态标签颜色、每张配件卡片的分类标签颜色,都通过它们动态获取。
7.2 编号生成函数
function genOrderNo(len: number): string {
const n = len + 1
if (n < 100) {
return 'WX-2025-0812-0' + n.toString()
}
return 'WX-2025-0812-' + n.toString()
}
function genPartCode(len: number): string {
const n = len + 1
if (n < 100) {
return 'P-0' + n.toString()
}
return 'P-' + n.toString()
}
逐行解析:
genOrderNo 根据当前工单列表长度生成下一个工单编号。参数 len 是当前数组的长度,加1后得到新工单的序号。当序号小于100时,前面补零(如"001"),否则直接拼接。生成的编号格式为"WX-2025-0812-021"。
genPartCode 的逻辑类似,生成格式为"P-019"的配件编号。
这两个函数在新增工单/配件时被调用,自动填充编号字段,减轻用户的手工输入负担。
7.3 格式化函数
function formatMoney(cost: number): string {
return '¥' + cost.toFixed(0)
}
function formatMileage(mileage: number): string {
return (mileage / 10000).toFixed(1) + '万km'
}
function todayStr(): string {
return '08/12'
}
function nowTimeStr(): string {
return '08/12 09:00'
}
逐行解析:
formatMoney:将数值格式化为带人民币符号的字符串,如"¥680"。使用toFixed(0)确保不显示小数位。formatMileage:将公里数转换为"万km"单位的字符串,如45000变为"4.5万km"。除以10000后保留一位小数。todayStr:返回当前日期字符串,这里简化为固定值"08/12"。nowTimeStr:返回当前时间字符串,简化为"08/12 09:00"。
7.4 数据过滤函数
function filterOrders(orders: WorkOrder[], statusFilter: string, keyword: string): WorkOrder[] {
const result: WorkOrder[] = []
for (let i = 0; i < orders.length; i++) {
const o = orders[i]
const matchStatus = statusFilter === '全部' || o.status === statusFilter
const matchKw = keyword === '' || o.plateNo.indexOf(keyword) >= 0 ||
o.orderNo.indexOf(keyword) >= 0 || o.model.indexOf(keyword) >= 0
if (matchStatus && matchKw) {
result.push(o)
}
}
return result
}
function filterVehicles(vehicles: Vehicle[], keyword: string): Vehicle[] {
const result: Vehicle[] = []
for (let i = 0; i < vehicles.length; i++) {
const v = vehicles[i]
const matchKw = keyword === '' || v.plateNo.indexOf(keyword) >= 0 ||
v.brand.indexOf(keyword) >= 0 || v.owner.indexOf(keyword) >= 0
if (matchKw) {
result.push(v)
}
}
return result
}
function filterParts(parts: PartsItem[], categoryFilter: string, keyword: string): PartsItem[] {
const result: PartsItem[] = []
for (let i = 0; i < parts.length; i++) {
const p = parts[i]
const matchCat = categoryFilter === '全部' || p.category === categoryFilter
const matchKw = keyword === '' || p.name.indexOf(keyword) >= 0 || p.code.indexOf(keyword) >= 0
if (matchCat && matchKw) {
result.push(p)
}
}
return result
}
逐行解析:
三个过滤函数分别处理工单、车辆和配件的筛选逻辑。它们的核心设计模式一致:
- 创建空结果数组。
- 遍历原始数组,对每个元素进行条件判断。
- 条件判断采用"短路或"模式:当筛选条件为"全部"或关键字为空时,自动匹配通过;否则进行精确匹配或包含匹配。
- 同时满足所有条件的元素才被加入结果数组。
filterOrders 支持状态筛选和关键字搜索(车牌、工单号、车型三个字段)。filterVehicles 仅支持关键字搜索(车牌、品牌、车主三个字段)。filterParts 支持分类筛选和关键字搜索(配件名称、编号两个字段)。
使用 indexOf 进行模糊匹配,当关键字是目标字段的子串时即视为匹配,用户体验友好。
7.5 统计计算函数
function getMaxCount(stats: MonthStat[]): number {
let max = 1
for (let i = 0; i < stats.length; i++) {
if (stats[i].count > max) {
max = stats[i].count
}
}
return max
}
function getMaxStock(stats: CategoryStockStat[]): number {
let max = 1
for (let i = 0; i < stats.length; i++) {
if (stats[i].stock > max) {
max = stats[i].stock
}
}
return max
}
function getStockPct(stock: number, stats: CategoryStockStat[]): number {
const max = getMaxStock(stats)
let pct = Math.floor(stock / max * 100)
if (pct < 8) {
pct = 8
}
return pct
}
function getBarHeight(count: number, stats: MonthStat[]): number {
const max = getMaxCount(stats)
const h = Math.floor(count / max * 120)
return 24 + h
}
逐行解析:
getMaxCount 和 getMaxStock 是辅助函数,分别找出月度工单量和分类库存量的最大值。初始值设为1而非0,是为了避免后续除法运算中出现除以零的情况。
getStockPct 计算某分类库存占最大库存的百分比,用于横向进度条的宽度。关键设计是当百分比低于8%时,强制设为8%,确保即使库存量最小的分类,其进度条也有最小可见宽度,避免视觉上的"消失"。
getBarHeight 计算柱状图中某月柱子的高度。以最大值对应120像素为基准进行等比缩放,再加上24像素的基础高度,确保最矮的柱子也有可见高度。
7.6 汇总统计函数
function getTotalCost(orders: WorkOrder[]): number {
let total = 0
for (let i = 0; i < orders.length; i++) {
if (orders[i].status === '已完成' || orders[i].status === '已交付') {
total = total + orders[i].cost
}
}
return total
}
function countFinishedOrders(orders: WorkOrder[]): number {
let c = 0
for (let i = 0; i < orders.length; i++) {
if (orders[i].status === '已完成' || orders[i].status === '已交付') {
c = c + 1
}
}
return c
}
function countWaitingOrders(orders: WorkOrder[]): number {
let c = 0
for (let i = 0; i < orders.length; i++) {
if (orders[i].status === '待配件') {
c = c + 1
}
}
return c
}
逐行解析:
getTotalCost:累加所有"已完成"和"已交付"状态工单的费用,得到今日营收总额。只统计已完成工单的收入是合理的业务逻辑——未完成的工单收入尚未确认。countFinishedOrders:统计"已完成"和"已交付"状态的工单数量。countWaitingOrders:统计"待配件"状态的工单数量,帮助管理者快速了解因配件短缺而阻塞的工单数量。
7.7 配件图标函数
function getCategoryIcon(category: string): string {
if (category === '机油') {
return '🛢️'
}
if (category === '滤芯') {
return '🌀'
}
if (category === '刹车片') {
return '🛑'
}
if (category === '轮胎') {
return '⭕'
}
if (category === '电瓶') {
return '🔋'
}
if (category === '火花塞') {
return '⚡'
}
if (category === '雨刮') {
return '💧'
}
return '📦'
}
逐行解析:
这个函数为每种配件分类返回一个对应的 Emoji 图标。机油用油桶、滤芯用旋涡、刹车片用停止标志、轮胎用圆圈、电瓶用电池、火花塞用闪电、雨刮用水滴、其他用包裹。这些图标在配件卡片的左侧以圆形背景展示,增强了视觉辨识度,让用户一眼就能区分不同分类的配件。
八、主入口组件:应用的骨架与导航
8.1 Tab索引枚举
enum TabIndex {
WorkOrder = 0,
Vehicle = 1,
Parts = 2,
Stats = 3,
Profile = 4
}
逐行解析:
枚举类型定义了五个Tab页的索引值。使用枚举而非魔法数字(如直接写0、1、2…)提升了代码的可读性和可维护性。在组件中通过 TabIndex.WorkOrder 而非 0 来引用,含义一目了然。
8.2 主入口组件状态声明
@Entry
@Component
struct GarageApp {
@State currentTab: number = TabIndex.WorkOrder
@State workOrders: WorkOrder[] = mockWorkOrders
@State vehicles: Vehicle[] = mockVehicles
@State parts: PartsItem[] = mockParts
逐行解析:
@Entry装饰器标记此组件为应用的入口组件,框架会从这里开始渲染。@Component装饰器声明这是一个自定义组件。@State currentTab:当前选中的Tab索引,初始值为工单页。这个状态的变化会驱动整个页面内容的切换。@State workOrders:工单数据数组,初始化为Mock数据。@State vehicles:车辆数据数组,初始化为Mock数据。@State parts:配件数据数组,初始化为Mock数据。
三个数据数组作为应用级状态在主组件中管理,并通过 @Link 向下传递给各业务Tab组件,实现了数据的单向流动。
8.3 遮罩层构建器
@Builder modalOverlay(onClose: () => void) {
Column() {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => { onClose() })
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
}
逐行解析:
@Builder 是一个UI构建器装饰器,用于定义可复用的UI片段。modalOverlay 构建器接收一个 onClose 回调函数参数,生成一个半透明黑色遮罩层。
- 外层
Column占满全屏,使用绝对定位(position)覆盖在页面之上。 - 内层
Column同样占满全屏,背景色为半透明黑色(rgba(0,0,0,0.5))。 - 点击遮罩层时触发
onClose回调,关闭弹窗。
这个构建器在主组件和各业务Tab组件中都有定义,用于新增、编辑、删除确认弹窗的遮罩效果。
8.4 头部导航栏构建器
@Builder headerBar(title: string) {
Row() {
Text(title)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Column().layoutWeight(1)
Text('🛠️')
.fontSize(24)
}
.width('100%')
.padding({ left: 20, right: 20, top: 50, bottom: 16 })
.alignItems(VerticalAlign.Center)
.linearGradient({
direction: GradientDirection.RightBottom,
colors: [[0x37474F, 0.0], [0x263238, 1.0]]
})
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
}
逐行解析:
headerBar 构建器接收一个标题字符串参数,生成应用顶部的导航栏:
- 使用
Row水平布局,左侧显示标题文字(22号粗体白色),右侧显示工具图标。 - 中间的
Column().layoutWeight(1)作为弹性占位符,将标题推向左侧,图标推向右侧。 padding设置了较大的顶部内边距(50),为状态栏留出空间。linearGradient设置从左上到右下的渐变背景,从#37474F(主色)渐变到#263238(深色),营造金属质感。borderRadius仅设置左下和右下圆角,使导航栏底部呈现弧形过渡。
8.5 底部导航栏构建器
@Builder bottomTabBar() {
Row() {
Column() {
Text('🔧')
.fontSize(22)
.margin({ bottom: 2 })
Text('工单')
.fontSize(11)
.fontColor(this.currentTab === TabIndex.WorkOrder ? '#29B6F6' : '#90A4AE')
if (this.currentTab === TabIndex.WorkOrder) {
Column()
.width(20)
.height(3)
.borderRadius(2)
.backgroundColor('#29B6F6')
.margin({ top: 2 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.currentTab = TabIndex.WorkOrder })
// ... 其他四个Tab项结构相同
}
.width('100%')
.height(64)
.backgroundColor('#263238')
.border({ width: 1, color: '#37474F' })
}
逐行解析:
底部导航栏包含五个Tab项,每个Tab项的结构完全一致:
- 上方是 Emoji 图标(22号字体),下方是中文标签(11号字体)。
- 标签颜色根据当前选中状态动态变化:选中时为亮蓝色
#29B6F6,未选中时为灰色#90A4AE。 - 当Tab被选中时,标签下方显示一个小的亮蓝色指示条(20x3像素的圆角矩形),提供额外的视觉反馈。
- 每个
Column使用layoutWeight(1)均分导航栏宽度。 onClick事件将currentTab切换到对应的Tab索引。
整个导航栏背景为深色 #263238,高度64像素,顶部有一像素的边框线与内容区分隔。
8.6 主构建方法
build() {
Column() {
Stack() {
Column() {
if (this.currentTab === TabIndex.WorkOrder) {
this.headerBar('维修工单')
WorkOrderTab({ orders: this.workOrders })
}
if (this.currentTab === TabIndex.Vehicle) {
this.headerBar('车辆档案')
VehicleTab({ vehicles: this.vehicles })
}
if (this.currentTab === TabIndex.Parts) {
this.headerBar('配件库存')
PartsTab({ parts: this.parts })
}
if (this.currentTab === TabIndex.Stats) {
this.headerBar('经营统计')
StatsTab()
}
if (this.currentTab === TabIndex.Profile) {
this.headerBar('我的门店')
ProfileTab({ orders: this.workOrders })
}
this.bottomTabBar()
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
逐行解析:
build 方法是组件的核心渲染方法。通过一系列 if 条件判断,根据 currentTab 的值决定渲染哪个业务Tab组件:
- 每个分支都先调用
headerBar渲染标题栏,然后渲染对应的业务组件。 - 业务组件通过参数接收数据:
WorkOrderTab接收工单数组,VehicleTab接收车辆数组,PartsTab接收配件数组,ProfileTab接收工单数组(用于统计展示),StatsTab不需要参数(直接引用Mock数据)。 - 最后调用
bottomTabBar渲染底部导航栏。 - 最外层设置全局背景色为
COLORS.bg。
使用 Stack 作为容器,为后续可能叠加的遮罩层和弹窗提供了层级基础。
九、维修工单Tab:全流程CRUD管理
9.1 组件状态声明
@Component
struct WorkOrderTab {
@Link orders: WorkOrder[]
@State statusFilter: string = '全部'
@State keyword: string = ''
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteConfirm: boolean = false
@State editItem: WorkOrder | null = null
@State deleteItem: WorkOrder | null = null
@State formOrderNo: string = ''
@State formPlateNo: string = ''
@State formModel: string = ''
@State formProject: string = ''
@State formMechanic: string = ''
@State formStatus: string = '待检修'
@State formInTime: string = ''
@State formExpectTime: string = ''
@State formCost: string = ''
逐行解析:
@Link orders:与父组件建立双向绑定,工单数据的增删改会同步到父组件状态。statusFilter:当前选中的状态筛选条件。keyword:搜索关键字。showAddModal、showEditModal、showDeleteConfirm:三个布尔值控制三种弹窗的显隐。editItem、deleteItem:记录当前正在编辑或删除的工单对象,类型为可空(| null)。form*系列字段:表单输入的临时存储,所有值以字符串形式保存(包括费用等数值字段),在提交时再进行类型转换。
9.2 新增工单逻辑
openAdd() {
this.formOrderNo = genOrderNo(this.orders.length)
this.formPlateNo = ''
this.formModel = ''
this.formProject = ''
this.formMechanic = ''
this.formStatus = '待检修'
this.formInTime = nowTimeStr()
this.formExpectTime = '08/12 18:00'
this.formCost = ''
this.showAddModal = true
}
confirmAdd() {
const plate = this.formPlateNo === '' ? '京A8W5J6' : this.formPlateNo
const model = this.formModel === '' ? '未知车型' : this.formModel
const project = this.formProject === '' ? '常规保养' : this.formProject
const mech = this.formMechanic === '' ? '李工' : this.formMechanic
const cost = this.formCost === '' ? 0 : Number(this.formCost)
this.orders.push(new WorkOrder(this.orders.length + 1, this.formOrderNo, plate, model,
project, mech, this.formStatus, this.formInTime, this.formExpectTime, cost))
this.showAddModal = false
}
逐行解析:
openAdd 方法在打开新增弹窗前进行表单初始化:
- 工单编号通过
genOrderNo自动生成。 - 进厂时间设为当前时间,预计完成时间设为当天18:00。
- 状态默认为"待检修"。
- 其他字段留空,等待用户输入。
confirmAdd 方法在用户确认新增时执行:
- 对每个字段进行空值兜底处理:车牌为空则使用默认值"京A8W5J6",车型为空则用"未知车型",项目为空则用"常规保养",技师为空则默认"李工",费用为空则为0。
- 使用
Number()将费用字符串转换为数值。 - 创建新的
WorkOrder实例并 push 到数组中。 - 关闭弹窗。
这种"空值兜底"的设计确保即使用户不填写某些字段,系统也能生成有效数据,不会因空值导致渲染异常。
9.3 编辑与删除逻辑
openEdit(item: WorkOrder) {
this.editItem = item
this.formOrderNo = item.orderNo
this.formPlateNo = item.plateNo
this.formModel = item.model
this.formProject = item.project
this.formMechanic = item.mechanic
this.formStatus = item.status
this.formInTime = item.inTime
this.formExpectTime = item.expectTime
this.formCost = item.cost.toString()
this.showEditModal = true
}
confirmEdit() {
const item = this.editItem
if (item !== null) {
item.plateNo = this.formPlateNo
item.model = this.formModel
item.project = this.formProject
item.mechanic = this.formMechanic
item.status = this.formStatus
item.cost = this.formCost === '' ? 0 : Number(this.formCost)
}
this.editItem = null
this.showEditModal = false
}
confirmDelete() {
const item = this.deleteItem
if (item !== null) {
const idx = this.orders.indexOf(item)
if (idx >= 0) {
this.orders.splice(idx, 1)
}
}
this.deleteItem = null
this.showDeleteConfirm = false
}
逐行解析:
openEdit 将目标工单的数据回填到表单字段中,然后显示编辑弹窗。注意费用字段通过 toString() 转换为字符串以适配表单输入框。
confirmEdit 直接修改 editItem 对象的属性。由于 WorkOrder 是 @Observed 类,属性修改会自动触发UI更新。编辑时不允许修改工单编号和进厂时间,这是合理的业务约束——编号是唯一标识不应变更,进厂时间是历史记录不应篡改。
confirmDelete 通过 indexOf 找到目标工单在数组中的索引,然后使用 splice 删除。删除前先进行 null 检查和索引有效性检查,确保操作安全。
9.4 工单卡片构建器
@Builder workOrderCard(item: WorkOrder) {
Row() {
Column()
.width(4)
.borderRadius(2)
.backgroundColor(getStatusCfg(item.status).color)
.alignSelf(ItemAlign.Stretch)
Column() {
Row() {
Text(item.orderNo)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Column().layoutWeight(1)
Text(item.status)
.fontSize(11)
.fontColor(getStatusCfg(item.status).color)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor(getStatusCfg(item.status).bg)
}
.width('100%')
.alignItems(VerticalAlign.Center)
逐行解析:
工单卡片是信息密度最高的UI元素之一:
- 最左侧是一个4像素宽的彩色竖条,颜色由工单状态决定(通过
getStatusCfg获取),使用alignSelf(ItemAlign.Stretch)使其高度撑满整个卡片。这个设计让用户在浏览列表时能通过颜色快速识别工单状态。 - 卡片主体分为多个
Row层级:- 第一行:左侧是工单编号(加粗),右侧是状态标签(带背景色的圆角小标签)。
- 第二行:车牌号(加粗大字号)、车型(灰色小字号)、技师姓名(蓝色)。
- 第三行:维修项目描述(灰色)、费用金额(橙色加粗大字号)。
- 第四行:进厂/预计时间(灰色小字)、编辑按钮、删除按钮。
Row() {
Text('🚗 ' + item.plateNo)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Column().width(10)
Text(item.model)
.fontSize(13)
.fontColor('#888888')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Column().layoutWeight(1)
Text(item.mechanic)
.fontSize(12)
.fontColor('#29B6F6')
}
逐行解析:
车牌号前加了汽车 Emoji 图标增强视觉辨识度。车型文字设置了 maxLines(1) 和 textOverflow 省略号,确保长文本不会破坏布局。技师姓名使用蓝色 #29B6F6 突出显示。
Row() {
Text('项目: ' + item.project)
.fontSize(12)
.fontColor('#607D8B')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Column().layoutWeight(1)
Text(formatMoney(item.cost))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
}
逐行解析:
维修项目同样设置了单行省略号。费用金额通过 formatMoney 格式化为"¥680"的形式,使用橙色 #FF7043 加粗16号字体,是卡片上视觉权重最高的元素——费用是用户最关心的信息。
Row() {
Text('进厂 ' + item.inTime + ' 预计 ' + item.expectTime)
.fontSize(11)
.fontColor('#90A4AE')
Column().layoutWeight(1)
Button('编辑')
.height(26)
.borderRadius(13)
.fontSize(11)
.backgroundColor('#E1F5FE')
.fontColor('#0288D1')
.onClick(() => { this.openEdit(item) })
Column().width(8)
Button('删除')
.height(26)
.borderRadius(13)
.fontSize(11)
.backgroundColor('#FFEBEE')
.fontColor('#EF5350')
.onClick(() => { this.openDelete(item) })
}
逐行解析:
卡片底部一行展示了时间信息和操作按钮:
- 时间文字使用最小字号(11号)和最浅颜色(
#90A4AE),作为辅助信息。 - 编辑按钮使用浅蓝色背景和深蓝色文字,视觉上温和。
- 删除按钮使用浅红色背景和红色文字,视觉上警示。
- 两个按钮都采用胶囊形状(高度26,圆角13),紧凑而不拥挤。
9.5 状态筛选标签与表单状态选择器
@Builder statusTag(label: string) {
Text(label)
.fontSize(13)
.fontColor(this.statusFilter === label ? '#FFFFFF' : '#607D8B')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.statusFilter === label ? '#37474F' : '#FFFFFF')
.border({ width: 1, color: this.statusFilter === label ? '#37474F' : '#DDDDDD' })
.margin({ right: 8 })
.onClick(() => { this.statusFilter = label })
}
@Builder formStatusChip(s: string) {
Text(s)
.fontSize(12)
.fontColor(this.formStatus === s ? '#FFFFFF' : '#607D8B')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.formStatus === s ? '#29B6F6' : '#F5F5F5')
.border({ width: 1, color: this.formStatus === s ? '#29B6F6' : '#E0E0E0' })
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.formStatus = s })
}
逐行解析:
statusTag 是列表页的状态筛选标签,formStatusChip 是表单弹窗中的状态选择器。两者结构相似但配色不同:
- 筛选标签选中时使用深蓝灰色(主色调)背景白色文字,未选中时白底灰字带灰边框。
- 表单选择器选中时使用亮蓝色(强调色)背景白色文字,未选中时浅灰底灰字带浅灰边框。
这种配色差异帮助用户区分"筛选"和"选择"两种不同的交互场景。点击事件分别修改 statusFilter 和 formStatus 状态。
9.6 表单字段构建器
@Builder modalFormFields() {
Column() {
Column() {
Text('工单号')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formOrderNo, placeholder: '系统自动生成' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formOrderNo = value })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ bottom: 12 })
// ... 更多表单字段
}
}
逐行解析:
表单字段构建器将所有输入项封装在一个 Column 中。每个字段遵循统一的结构:
- 外层
Column占满宽度,左对齐,底部留12像素间距。 - 字段标签使用13号灰色字体,底部留6像素间距。
TextInput输入框高度40像素,圆角10,浅灰背景,左右内边距12像素。onChange回调将输入值同步到对应的form*状态字段。
工单号字段的 placeholder 为"系统自动生成",因为该值在打开弹窗时已由 genOrderNo 自动填充,用户通常无需手动修改。
9.7 弹窗内容构建器
系统为新增、编辑、删除三种操作分别设计了弹窗内容构建器。
@Builder addModalContent() {
Column() {
Text('新增维修工单')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
.margin({ bottom: 4 })
Text('填写车辆维修信息')
.fontSize(13)
.fontColor('#90A4AE')
.margin({ bottom: 16 })
Scroll() {
Column() {
this.modalFormFields()
}
.width('100%')
}
.layoutWeight(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
Row() {
Button('取消')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#F5F5F5')
.fontColor('#666666')
.fontSize(14)
.onClick(() => { this.showAddModal = false })
Column().width(12)
Button('确认新增')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#37474F')
.fontColor('#FFFFFF')
.fontSize(14)
.onClick(() => { this.confirmAdd() })
}
.width('100%')
.margin({ top: 12 })
}
.width('88%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.constraintSize({ maxHeight: '80%' })
}
逐行解析:
新增弹窗的结构自上而下分为三个部分:
- 标题区:主标题"新增维修工单"(20号粗体),副标题"填写车辆维修信息"(13号灰色)。
- 表单滚动区:使用
Scroll包裹表单字段,支持垂直滚动。scrollBar(BarState.Off)隐藏滚动条保持视觉整洁。layoutWeight(1)让滚动区占据弹窗的弹性空间。constraintSize({ maxHeight: '80%' })限制弹窗最大高度为屏幕的80%,防止表单过长时溢出屏幕。 - 操作按钮区:取消按钮(灰色背景)和确认新增按钮(深蓝灰背景),各占一半宽度(
layoutWeight(1)),中间留12像素间距。按钮高度42像素,圆角21,呈胶囊形。
编辑弹窗 editModalContent 结构几乎相同,区别在于标题改为"编辑维修工单",确认按钮文字改为"保存修改"且背景色改为亮蓝色 #29B6F6。
删除确认弹窗 deleteModalContent 则更加简洁,没有表单字段,只显示警告图标、确认文字和工单编号,底部是取消和红色"确认删除"按钮。
9.8 工单Tab主构建方法
build() {
Stack() {
Column() {
Row() {
Text('🔍')
.fontSize(14)
.margin({ right: 8 })
TextInput({ placeholder: '搜索工单号 / 车牌 / 车型' })
.height(40)
.layoutWeight(1)
.borderRadius(20)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#E0E0E0' })
.padding({ left: 14, right: 14 })
.onChange((value: string) => { this.keyword = value })
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
.alignItems(VerticalAlign.Center)
Scroll() {
Row() {
ForEach(WORK_STATUS_FILTERS, (label: string) => {
this.statusTag(label)
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 12, bottom: 4 })
Scroll() {
Column() {
ForEach(filterOrders(this.orders, this.statusFilter, this.keyword), (item: WorkOrder) => {
this.workOrderCard(item)
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 20 })
}
.layoutWeight(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
Row() {
Button('+ 新增工单')
.height(44)
.width('100%')
.borderRadius(22)
.backgroundColor('#37474F')
.fontColor('#FFFFFF')
.fontSize(15)
.onClick(() => { this.openAdd() })
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 12 })
}
.layoutWeight(1)
.width('100%')
逐行解析:
工单Tab页面自上而下由四个部分组成:
-
搜索栏:放大镜图标加输入框,圆角20像素的白色搜索框,placeholder 提示可搜索的字段。输入变化实时更新
keyword状态。 -
状态筛选栏:横向滚动的
Scroll容器,使用ForEach遍历WORK_STATUS_FILTERS数组生成筛选标签。横向滚动确保所有筛选标签都能展示,不被屏幕宽度限制。 -
工单列表:垂直滚动的
Scroll容器,使用ForEach遍历filterOrders的返回结果生成工单卡片。注意这里每次渲染都会调用filterOrders进行实时过滤,当statusFilter或keyword变化时,列表内容自动更新。layoutWeight(1)让列表占据剩余空间。 -
新增按钮:底部固定的全宽按钮,深蓝灰背景白色文字,点击调用
openAdd打开新增弹窗。
if (this.showAddModal) {
this.modalOverlay(() => { this.showAddModal = false })
Column() {
this.addModalContent()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
// ... 编辑和删除弹窗结构相同
}
.layoutWeight(1)
.width('100%')
}
逐行解析:
在 Stack 容器中,弹窗层叠加在内容层之上。当 showAddModal 为 true 时,先渲染遮罩层 modalOverlay,再渲染弹窗内容 addModalContent。弹窗外层 Column 占满全屏,通过 justifyContent(FlexAlign.Center) 和 alignItems(HorizontalAlign.Center) 实现居中布局。
编辑和删除弹窗的渲染逻辑完全相同,只是条件和内容构建器不同。
十、车辆档案Tab:客户资产的全息管理
10.1 组件结构与CRUD逻辑
车辆Tab的组件结构与工单Tab高度一致,同样包含 @Link 数据绑定、三个弹窗控制状态、表单字段状态以及完整的CRUD方法。
@Component
struct VehicleTab {
@Link vehicles: Vehicle[]
@State keyword: string = ''
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteConfirm: boolean = false
@State editItem: Vehicle | null = null
@State deleteItem: Vehicle | null = null
@State formPlateNo: string = ''
@State formBrand: string = ''
@State formVModel: string = ''
@State formYear: string = ''
@State formColor: string = ''
@State formOwner: string = ''
@State formPhone: string = ''
@State formMileage: string = ''
@State formLastMaint: string = ''
逐行解析:
车辆表单字段比工单更多,包含车牌、品牌、型号、年份、颜色、车主、电话、里程和上次保养日期共九个字段。注意型号字段命名为 formVModel 而非 formModel,这是为了避免与组件内部可能的命名冲突。
confirmAdd() {
const plate = this.formPlateNo === '' ? '京A8W5J6' : this.formPlateNo
const brand = this.formBrand === '' ? '丰田' : this.formBrand
const model = this.formVModel === '' ? '卡罗拉' : this.formVModel
const year = this.formYear === '' ? 2024 : Number(this.formYear)
const color = this.formColor === '' ? '珍珠白' : this.formColor
const owner = this.formOwner === '' ? '未知车主' : this.formOwner
const phone = this.formPhone === '' ? '139****0000' : this.formPhone
const mileage = this.formMileage === '' ? 0 : Number(this.formMileage)
const lastMaint = this.formLastMaint === '' ? todayStr() : this.formLastMaint
this.vehicles.push(new Vehicle(this.vehicles.length + 1, plate, brand, model, year,
color, owner, phone, mileage, lastMaint))
this.showAddModal = false
}
逐行解析:
新增车辆时的空值兜底逻辑同样完善:每个字段都有合理的默认值。年份和里程通过 Number() 转换为数值类型。上次保养日期为空时使用 todayStr() 填充当前日期。
10.2 车辆卡片设计
@Builder vehicleCard(item: Vehicle) {
Row() {
Column() {
Text('🚗')
.fontSize(30)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#E1F5FE')
.justifyContent(FlexAlign.Center)
Column() {
Row() {
Text(item.plateNo)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Column().layoutWeight(1)
Text(item.owner)
.fontSize(12)
.fontColor('#29B6F6')
}
.width('100%')
.alignItems(VerticalAlign.Center)
逐行解析:
车辆卡片采用了与工单卡片不同的布局风格:
- 左侧是一个64x64的圆形头像区域,浅蓝色背景配汽车Emoji图标,视觉上更加醒目。
- 第一行:车牌号(加粗16号)和车主姓名(蓝色12号)。
- 后续行采用三列网格布局,每列包含标签(10号灰色)和值(12号深色),展示品牌型号、年份、颜色、电话、里程、上次保养等信息。
Row() {
Column() {
Text('品牌型号')
.fontSize(10)
.fontColor('#90A4AE')
Text(item.brand + ' ' + item.model)
.fontSize(12)
.fontColor('#37474F')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('年份')
.fontSize(10)
.fontColor('#90A4AE')
Text(item.year + '款')
.fontSize(12)
.fontColor('#37474F')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('颜色')
.fontSize(10)
.fontColor('#90A4AE')
Text(item.color)
.fontSize(12)
.fontColor('#37474F')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.margin({ top: 8 })
逐行解析:
三列等宽的信息展示行使用 layoutWeight(1) 实现均分。每列上方是10号灰色标签,下方是12号深色值。这种"标签+值"的布局方式信息密度高,层次清晰。品牌型号字段设置了单行省略号,防止长型号名称破坏布局。
Row() {
Column() {
Text('行驶里程')
.fontSize(10)
.fontColor('#90A4AE')
Text(formatMileage(item.mileage))
.fontSize(12)
.fontColor('#FF7043')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
逐行解析:
行驶里程使用橙色 #FF7043 突出显示,并通过 formatMileage 函数将原始公里数转换为"4.5万km"的可读格式。里程是判断车辆保养需求的重要指标,因此用醒目的颜色标识。
10.3 车辆表单的双列布局
Row() {
Column() {
Text('品牌')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formBrand, placeholder: '如 丰田' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formBrand = value })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column().width(10)
Column() {
Text('型号')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formVModel, placeholder: '如 卡罗拉2023款' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formVModel = value })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.margin({ bottom: 12 })
.alignItems(VerticalAlign.Top)
逐行解析:
由于车辆表单字段较多,系统采用了双列布局来优化空间利用。品牌和型号、年份和颜色、车主和电话、里程和上次保养分别成对排列。每对字段使用 Row 容器,两个 Column 各占 layoutWeight(1) 宽度,中间用10像素宽的间隔 Column 分隔。这种设计使表单更加紧凑,减少了弹窗的垂直高度。
10.4 车辆Tab主构建方法
车辆Tab的主构建方法与工单Tab结构相似,包含搜索栏、车辆列表和新增按钮。区别在于没有状态筛选栏(车辆档案不需要状态筛选),搜索框的 placeholder 改为"搜索车牌 / 品牌 / 车主",列表数据通过 filterVehicles 函数过滤。
十一、配件库存Tab:精细化的库存预警管理
11.1 组件状态与CRUD逻辑
@Component
struct PartsTab {
@Link parts: PartsItem[]
@State categoryFilter: string = '全部'
@State keyword: string = ''
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteConfirm: boolean = false
@State editItem: PartsItem | null = null
@State deleteItem: PartsItem | null = null
@State formName: string = ''
@State formCode: string = ''
@State formCategory: string = '机油'
@State formStock: string = ''
@State formPrice: string = ''
@State formSupplier: string = ''
@State formMinStock: string = ''
逐行解析:
配件Tab使用 categoryFilter 替代了工单Tab的 statusFilter,用于配件分类筛选。表单默认分类设为"机油"。其他状态字段与工单Tab和车辆Tab保持一致的设计模式。
confirmAdd() {
const name = this.formName === '' ? '通用配件' : this.formName
const code = this.formCode === '' ? genPartCode(this.parts.length) : this.formCode
const stock = this.formStock === '' ? 0 : Number(this.formStock)
const price = this.formPrice === '' ? 0 : Number(this.formPrice)
const supplier = this.formSupplier === '' ? '未登记供应商' : this.formSupplier
const minStock = this.formMinStock === '' ? 10 : Number(this.formMinStock)
this.parts.push(new PartsItem(this.parts.length + 1, name, code, this.formCategory,
stock, price, supplier, minStock))
this.showAddModal = false
}
逐行解析:
新增配件时空值兜底逻辑:名称默认"通用配件",编号自动生成,供应商默认"未登记供应商",最低库存默认10件。库存和价格转为数值类型。
11.2 分类筛选标签
@Builder categoryTag(label: string) {
Text(label)
.fontSize(13)
.fontColor(this.categoryFilter === label ? '#FFFFFF' : '#607D8B')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.categoryFilter === label ? '#37474F' : '#FFFFFF')
.border({ width: 1, color: this.categoryFilter === label ? '#37474F' : '#DDDDDD' })
.margin({ right: 8 })
.onClick(() => { this.categoryFilter = label })
}
逐行解析:
分类筛选标签与工单状态筛选标签结构完全一致,但绑定的状态是 categoryFilter。由于配件分类有8种(加上"全部"共9个标签),横向滚动的设计尤为重要。
11.3 配件卡片与库存预警
@Builder partsCard(item: PartsItem) {
Row() {
Column() {
Text(getCategoryIcon(item.category))
.fontSize(22)
}
.width(48)
.height(48)
.borderRadius(12)
.backgroundColor(getCategoryCfg(item.category).bg)
.justifyContent(FlexAlign.Center)
逐行解析:
配件卡片左侧是一个48x48的圆角方形图标区域,背景色由配件分类决定(通过 getCategoryCfg 获取),图标由分类对应的 Emoji 决定(通过 getCategoryIcon 获取)。这种设计让每张配件卡片在视觉上都有独特的分类标识。
Row() {
Text('库存')
.fontSize(12)
.fontColor('#607D8B')
Column().width(6)
Text(item.stock + '件')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(item.stock < item.minStock ? '#FF7043' : '#37474F')
Column().width(8)
if (item.stock < item.minStock) {
Text('⚠ 低库存')
.fontSize(10)
.fontColor('#FF7043')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('#FFF3E0')
} else {
Text('库存充足')
.fontSize(10)
.fontColor('#66BB6A')
.padding({ left: 6, right: 2, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('#E8F5E9')
}
Column().layoutWeight(1)
Text('¥' + item.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
}
逐行解析:
这是配件卡片最核心的设计——库存预警机制:
- 库存数量文字的颜色是动态的:当库存低于最低库存阈值时(
item.stock < item.minStock),文字变为橙色#FF7043;否则为正常的深色#37474F。 - 通过
if...else条件渲染,低库存时显示橙色"低库存"警示标签,库存充足时显示绿色"库存充足"标签。 - 右侧始终显示单价(橙色加粗)。
这种动态的视觉反馈让管理者一眼就能识别哪些配件需要补货,是库存管理中最实用的功能之一。
Row() {
Text('最低库存 ' + item.minStock + '件')
.fontSize(10)
.fontColor('#B0BEC5')
Column().layoutWeight(1)
Button('编辑')
.height(26)
.borderRadius(13)
.fontSize(11)
.backgroundColor('#E1F5FE')
.fontColor('#0288D1')
.onClick(() => { this.openEdit(item) })
Column().width(8)
Button('删除')
.height(26)
.borderRadius(13)
.fontSize(11)
.backgroundColor('#FFEBEE')
.fontColor('#EF5350')
.onClick(() => { this.openDelete(item) })
}
逐行解析:
卡片底部显示最低库存阈值(浅灰色小字)和操作按钮。最低库存信息的展示让用户了解预警的判断依据,增加了系统的透明度。
11.4 配件表单与分类选择器
@Builder formCategoryChip(s: string) {
Text(s)
.fontSize(12)
.fontColor(this.formCategory === s ? '#FFFFFF' : '#607D8B')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.formCategory === s ? '#29B6F6' : '#F5F5F5')
.border({ width: 1, color: this.formCategory === s ? '#29B6F6' : '#E0E0E0' })
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.formCategory = s })
}
逐行解析:
配件表单中的分类选择器使用 ForEach(CATEGORY_FILTERS.slice(1), ...) 遍历分类数组(去掉"全部"选项),生成8个可选的分类标签。slice(1) 跳过数组的第一个元素"全部",因为"全部"是筛选用的,不应出现在表单的分类选择中。
11.5 配件Tab主构建方法
配件Tab的主构建方法与工单Tab结构高度一致,包含搜索栏、分类筛选栏(横向滚动)、配件列表(垂直滚动)和新增按钮。列表数据通过 filterParts 函数进行分类和关键字双重过滤。
十二、经营统计Tab:数据可视化的实践
12.1 区块标题构建器
@Builder sectionTitle(title: string, sub: string) {
Row() {
Column() {
Text(title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Text(sub)
.fontSize(11)
.fontColor('#90A4AE')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Column()
.width(36)
.height(4)
.borderRadius(2)
.linearGradient({
direction: GradientDirection.Right,
colors: [[0x37474F, 0.0], [0x29B6F6, 1.0]]
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
逐行解析:
统计页面的每个数据区块都使用统一的标题样式:左侧是主标题(16号粗体)和副标题(11号灰色),右侧是一个36x4像素的渐变装饰条,从深蓝灰渐变到亮蓝色,作为视觉点缀。
12.2 概览数据卡片
Row() {
Column() {
Text(mockWorkOrders.length + '')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Text('今日工单')
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text(countFinishedOrders(mockWorkOrders) + '')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#66BB6A')
Text('已完成')
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
// ... 待配件、在修车辆
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.border({ width: 1, color: '#E8E8E8' })
.shadow({ radius: 6, color: '#14000000', offsetX: 0, offsetY: 2 })
逐行解析:
页面顶部的概览卡片采用四列等宽布局,分别展示今日工单总数(深色)、已完成数(绿色)、待配件数(橙色)和在修车辆数(蓝色)。每个数字使用20号粗体,下方是10号灰色标签。四个数字使用不同颜色,帮助用户快速区分各项指标的健康状况。
12.3 营收卡片
Column() {
Row() {
Column() {
Text('今日营收')
.fontSize(12)
.fontColor('#90A4AE')
Text('¥' + getTotalCost(mockWorkOrders).toFixed(0))
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
.margin({ top: 6 })
Text('已完成/已交付 工单收入合计')
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('📈')
.fontSize(36)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#E1F5FE')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.border({ width: 1, color: '#E8E8E8' })
.shadow({ radius: 6, color: '#14000000', offsetX: 0, offsetY: 2 })
.margin({ top: 12 })
.linearGradient({
direction: GradientDirection.Right,
colors: [[0xFFFFFF, 0.0], [0xE1F5FE, 1.0]]
})
逐行解析:
营收卡片是统计页面视觉权重最高的元素:
- 营收金额使用30号粗体深色字体,是整个页面最大的文字,突出了营收数据的重要性。
- 金额通过
getTotalCost(mockWorkOrders).toFixed(0)计算并格式化,只统计已完成和已交付工单的收入。 - 右侧是圆形浅蓝色背景配趋势图标的装饰元素。
- 卡片背景使用从白色到浅蓝色的渐变,增强了视觉层次感。
12.4 月度工单量柱状图
Column() {
this.sectionTitle('月维修工单量', '近6个月维修业务走势')
Row() {
ForEach(mockMonthStats, (item: MonthStat) => {
Column() {
Column()
.width(24)
.height(getBarHeight(item.count, mockMonthStats))
.borderRadius({ topLeft: 8, topRight: 8 })
.linearGradient({
direction: GradientDirection.Bottom,
colors: [[0x37474F, 0.0], [0x29B6F6, 1.0]]
})
Text(item.count.toString())
.fontSize(10)
.fontColor('#607D8B')
.margin({ top: 4 })
Text(item.month)
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.End)
})
}
.width('100%')
.height(190)
.alignItems(VerticalAlign.Bottom)
.margin({ top: 14 })
}
逐行解析:
柱状图是统计页面最具技术含量的可视化组件:
- 使用
ForEach遍历mockMonthStats数组,每个月份生成一个柱子。 - 每个柱子由三部分组成:渐变色柱体(24像素宽,高度由
getBarHeight动态计算)、数值文字(10号灰色)、月份标签(10号浅灰色)。 - 柱体使用从底部到顶部的渐变色:底部为深蓝灰
#37474F,顶部为亮蓝色#29B6F6,形成金属光泽效果。 - 柱体顶部设置了8像素的圆角,视觉更加柔和。
- 外层
Row高度190像素,alignItems(VerticalAlign.Bottom)确保所有柱子底部对齐。 - 每个月份的
Column使用justifyContent(FlexAlign.End)确保内容从底部开始排列。
这是一个纯代码绘制的柱状图,没有依赖任何第三方图表库,充分展示了声明式UI的灵活性。
12.5 分类库存进度条
Column() {
this.sectionTitle('配件分类库存占比', '各类配件当前库存量')
Column() {
ForEach(mockCategoryStocks, (item: CategoryStockStat) => {
Row() {
Text(item.category)
.fontSize(12)
.fontColor('#607D8B')
.width(48)
Row() {
Row()
.width(getStockPct(item.stock, mockCategoryStocks) + '%')
.height(12)
.borderRadius(6)
.backgroundColor(getCategoryCfg(item.category).color)
}
.layoutWeight(1)
.height(12)
.borderRadius(6)
.backgroundColor('#ECEFF1')
.justifyContent(FlexAlign.Start)
Text(item.stock + '件')
.fontSize(11)
.fontColor('#90A4AE')
.width(48)
.textAlign(TextAlign.End)
}
.width('100%')
.margin({ bottom: 10 })
.alignItems(VerticalAlign.Center)
})
}
.width('100%')
.margin({ top: 16 })
}
逐行解析:
横向进度条展示了各配件分类的库存占比:
- 每行三列布局:左侧48像素宽的分类名称、中间弹性的进度条、右侧48像素宽的库存数量。
- 进度条采用嵌套
Row设计:外层Row是灰色背景的轨道(#ECEFF1),内层Row是彩色填充条,宽度由getStockPct计算的百分比决定。 - 填充条颜色由分类配置决定(通过
getCategoryCfg获取),每种分类使用不同颜色。 - 进度条高度12像素,圆角6像素,视觉精致。
12.6 技师工时统计
Column() {
this.sectionTitle('维修师傅工时统计', '本月工时 · 完成工单 · 产值')
Column() {
ForEach(mockMechanicStats, (item: MechanicStat, index: number) => {
Column() {
Row() {
Column() {
Text(item.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Text('完成 ' + item.orders + ' 单')
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Column() {
Text(item.hours + 'h')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#29B6F6')
Text(formatMoney(item.cost))
.fontSize(10)
.fontColor('#FF7043')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.alignItems(VerticalAlign.Center)
if (index < mockMechanicStats.length - 1) {
Column()
.width('100%')
.height(1)
.backgroundColor('#F0F0F0')
.margin({ top: 10, bottom: 10 })
}
}
.width('100%')
})
}
.width('100%')
.margin({ top: 12 })
}
逐行解析:
技师工时统计采用列表形式展示,每位技师一行:
- 左侧:技师姓名(15号粗体)和完成工单数(10号灰色)。
- 右侧:工时数(14号粗体蓝色,带"h"单位)和产值(10号橙色,带"¥"符号)。
- 技师之间用1像素的浅灰色分割线分隔,最后一个技师下方不显示分割线(通过
index < mockMechanicStats.length - 1判断)。
这种布局简洁明了,管理者可以快速对比各技师的工时和产值表现。
十三、我的门店Tab:品牌形象与综合信息
13.1 门店信息卡片
@Builder shopCard() {
Column() {
Row() {
Column() {
Text('🔧')
.fontSize(28)
}
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('#22FFFFFF')
.justifyContent(FlexAlign.Center)
Column() {
Text(APP_CONFIG.shopName)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('📞 ' + APP_CONFIG.shopPhone + ' ' + APP_CONFIG.serviceTime)
.fontSize(11)
.fontColor('#B0BEC5')
.margin({ top: 4 })
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.alignItems(VerticalAlign.Center)
Column()
.width('100%')
.height(1)
.backgroundColor('#22FFFFFF')
.margin({ top: 14, bottom: 14 })
Row() {
Text('📍 ' + APP_CONFIG.shopAddress)
.fontSize(11)
.fontColor('#CFD8DC')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
}
.width('100%')
.padding(16)
.borderRadius(12)
.linearGradient({
direction: GradientDirection.RightBottom,
colors: [[0x37474F, 0.0], [0x263238, 1.0]]
})
.shadow({ radius: 8, color: '#33000000', offsetX: 0, offsetY: 4 })
}
逐行解析:
门店信息卡片是整个应用中视觉最独特的卡片,采用了深色渐变背景:
- 顶部行:左侧是半透明白色圆形背景的工具图标,右侧是门店名称(白色粗体)和电话+营业时间(浅灰色)。
- 中间是半透明白色的分割线(
#22FFFFFF表示22%透明度的白色)。 - 底部是门店地址,使用定位图标前缀,浅灰色文字。
- 卡片背景使用与导航栏相同的深色渐变(从
#37474F到#263238),配合较大的阴影(radius 8,offsetY 4),营造出悬浮卡片的效果。
13.2 概览网格与营收卡片
@Builder overviewGrid() {
Column() {
Row() {
Column() {
Text(this.orders.length + '')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Text('今日工单')
.fontSize(11)
.fontColor('#90A4AE')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
// ... 已完成、待配件、在修车辆
}
.width('100%')
.padding({ top: 14, bottom: 14 })
}
.width('100%')
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.border({ width: 1, color: '#E8E8E8' })
.shadow({ radius: 6, color: '#14000000', offsetX: 0, offsetY: 2 })
}
逐行解析:
概览网格与统计页面的概览卡片类似,但数字字号更大(22号),并且数据来源于通过 @Prop 传入的 orders 数组而非直接引用Mock数据。
@Builder revenueCard() {
Row() {
Column() {
Text('今日营收')
.fontSize(12)
.fontColor('#90A4AE')
Text('¥' + getTotalCost(this.orders).toFixed(0))
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
.margin({ top: 6 })
Text('本月累计 ' + formatMoney(getTotalCost(this.orders) * 9))
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('💰')
.fontSize(32)
}
.width(60)
.height(60)
.borderRadius(30)
.backgroundColor('#FFF3E0')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.border({ width: 1, color: '#E8E8E8' })
.shadow({ radius: 6, color: '#14000000', offsetX: 0, offsetY: 2 })
.alignItems(VerticalAlign.Center)
}
逐行解析:
营收卡片与统计页面的营收卡片设计思路相似,但配色不同:营收金额使用橙色 #FF7043(而非深色),右上角是橙色圆形背景的钱袋图标。底部增加了"本月累计"的估算值(今日营收乘以9作为月度估算),为管理者提供更长周期的营收参考。
13.3 技师团队卡片
@Builder techCard() {
Column() {
Row() {
Column() {
Text('维修师傅')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Text('团队共 ' + mockTechnicians.length + ' 人')
.fontSize(11)
.fontColor('#90A4AE')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Text('技术负责人: ' + APP_CONFIG.techLeader)
.fontSize(10)
.fontColor('#29B6F6')
}
.width('100%')
.alignItems(VerticalAlign.Center)
Column() {
ForEach(mockTechnicians, (item: TechInfo, index: number) => {
Column() {
Row() {
Column() {
Text(item.name.substring(0, 1))
.fontSize(15)
.fontColor('#FFFFFF')
}
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor('#37474F')
.justifyContent(FlexAlign.Center)
Column() {
Row() {
Text(item.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Column().width(8)
Text(item.title)
.fontSize(10)
.fontColor('#0288D1')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('#E1F5FE')
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text(item.skill + ' · ' + item.years + '经验')
.fontSize(11)
.fontColor('#90A4AE')
.margin({ top: 4 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
Text(item.phone)
.fontSize(10)
.fontColor('#B0BEC5')
}
.width('100%')
.alignItems(VerticalAlign.Center)
if (index < mockTechnicians.length - 1) {
Column()
.width('100%')
.height(1)
.backgroundColor('#F0F0F0')
.margin({ top: 10, bottom: 10 })
}
}
.width('100%')
})
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.border({ width: 1, color: '#E8E8E8' })
.shadow({ radius: 6, color: '#14000000', offsetX: 0, offsetY: 2 })
}
逐行解析:
技师团队卡片展示了门店的技术力量:
- 卡片标题行:左侧是"维修师傅"标题和团队人数,右侧是技术负责人信息(蓝色)。
- 技师列表:每位技师一行,左侧是40x40的圆形头像(深蓝灰背景,白色姓氏首字),中间是姓名+职称标签+擅长领域和经验,右侧是电话号码。
- 职称标签使用浅蓝色背景深蓝色文字的圆角小标签,视觉上类似徽章效果。
- 技师之间用分割线分隔。
item.name.substring(0, 1) 提取技师姓名的第一个字作为头像文字,这是一种简单但有效的头像生成方式。
13.4 快捷服务入口
@Builder serviceRow(icon: string, label: string, value: string) {
Row() {
Text(icon)
.fontSize(16)
.width(32)
Text(label)
.fontSize(13)
.fontColor('#37474F')
Column().layoutWeight(1)
Text(value)
.fontSize(12)
.fontColor('#90A4AE')
Text('›')
.fontSize(14)
.fontColor('#B0BEC5')
.margin({ left: 6 })
}
.width('100%')
.padding({ top: 12, bottom: 12 })
.alignItems(VerticalAlign.Center)
}
逐行解析:
serviceRow 是一个通用的列表行构建器,用于生成快捷服务入口:
- 左侧是图标(16号,32像素宽)和标签(13号深色)。
- 右侧是值(12号灰色)和右箭头"›"(14号浅灰色)。
- 整行上下各12像素内边距。
在主构建方法中,通过四次调用生成了四个服务入口:
this.serviceRow('📋', '门店经营报表', '月报 / 年报')
this.serviceRow('🛠️', '维修预约管理', '待处理 3 条')
this.serviceRow('⭐', '客户评价管理', '4.9 分')
this.serviceRow('⚙️', '系统设置', 'v2.4.0')
每个入口右侧显示了对应的状态或版本信息,如"待处理 3 条"暗示有预约等待处理,"4.9 分"展示了客户满意度,"v2.4.0"显示系统版本号。这些入口为未来功能扩展预留了空间。
13.5 我的门店Tab主构建方法
build() {
Scroll() {
Column() {
this.shopCard()
this.overviewGrid()
this.revenueCard()
this.techCard()
Column() {
this.serviceRow('📋', '门店经营报表', '月报 / 年报')
this.serviceRow('🛠️', '维修预约管理', '待处理 3 条')
this.serviceRow('⭐', '客户评价管理', '4.9 分')
this.serviceRow('⚙️', '系统设置', 'v2.4.0')
}
.width('100%')
.padding({ left: 16, right: 16 })
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.border({ width: 1, color: '#E8E8E8' })
.shadow({ radius: 6, color: '#14000000', offsetX: 0, offsetY: 2 })
.margin({ top: 12, bottom: 20 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
}
.layoutWeight(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
}
逐行解析:
我的门店Tab将所有信息卡片垂直排列在一个可滚动容器中:门店信息卡片、概览网格、营收卡片、技师团队卡片和快捷服务入口。整体布局从品牌形象到经营数据再到团队介绍,最后是功能入口,形成了一个完整的"门店主页"。
十四、五大功能模块关键特性对比
| 特性维度 | 维修工单 | 车辆档案 | 配件库存 | 经营统计 | 我的门店 |
|---|---|---|---|---|---|
| 核心功能 | 工单全生命周期管理 | 车辆信息登记与维护 | 配件录入与库存预警 | 多维数据可视化 | 门店品牌与综合信息 |
| 数据模型 | WorkOrder | Vehicle | PartsItem | MonthStat/CategoryStockStat/MechanicStat | 复用WorkOrder+TechInfo |
| CRUD操作 | 支持新增/编辑/删除 | 支持新增/编辑/删除 | 支持新增/编辑/删除 | 只读展示 | 只读展示 |
| 搜索功能 | 工单号/车牌/车型 | 车牌/品牌/车主 | 配件名称/编号 | 无 | 无 |
| 筛选功能 | 5种状态筛选 | 无 | 8种分类筛选 | 无 | 无 |
| 弹窗类型 | 新增/编辑/删除确认 | 新增/编辑/删除确认 | 新增/编辑/删除确认 | 无 | 无 |
| 数据传递 | @Link双向绑定 | @Link双向绑定 | @Link双向绑定 | 直接引用Mock | @Prop单向只读 |
| 可视化形式 | 卡片列表 | 卡片列表 | 卡片列表+预警标签 | 柱状图+进度条+列表 | 深色渐变卡片+列表 |
| 动态视觉 | 状态色条+状态标签 | 圆形头像+三列网格 | 分类图标+库存预警色 | 渐变柱体+动态进度条 | 渐变背景+职称徽章 |
| 特殊设计 | 编号自动生成 | 里程格式化显示 | 低库存智能预警 | 纯代码绘制图表 | 品牌形象展示 |
| 表单布局 | 单列纵向 | 双列网格 | 双列网格 | 无表单 | 无表单 |
| 业务价值 | 维修流程管控 | 客户资产管理 | 库存成本控制 | 经营决策支持 | 品牌与团队展示 |
十五、技术架构总结
15.1 架构模式评析
本系统采用了典型的"状态驱动UI"架构模式。整个应用的数据流可以概括为:状态定义 -> 状态变更 -> UI自动刷新。每个组件通过 @State 管理自身内部状态,通过 @Link 与父组件建立双向绑定,通过 @Prop 接收只读数据。这种单向数据流的设计使得状态变更的来源清晰可追溯,降低了调试难度。
@Observed 装饰器的使用是另一个架构亮点。通过将业务实体类标记为可观察,对象属性的修改可以自动触发依赖该属性的UI组件刷新。这使得编辑操作变得极其简单——直接修改对象属性即可,无需手动触发更新或操作DOM。
15.2 代码组织特点
系统的代码组织遵循了"自底向上"的构建顺序:先定义接口类型契约,再实现数据模型,然后提供静态配置和Mock数据,接着编写工具函数,最后构建UI组件。每一层都只依赖其下方的层,不存在循环依赖。这种分层设计使得代码的可读性和可维护性都得到了保障。
工具函数全部采用纯函数实现,没有副作用,不依赖外部状态,仅通过参数接收输入并返回输出。这使得这些函数可以被任意复用和独立测试,也方便后续替换为真实的后端API调用。
15.3 UI设计亮点
系统在UI设计上有多个值得称道的细节:
色彩体系的统一性。整套配色方案以蓝灰色系为主色调,橙色作为警示色,绿色作为成功色,蓝色作为强调色。所有组件严格遵循这一色彩体系,视觉风格高度统一。
信息层级的清晰性。通过字号大小(22/20/16/15/13/12/11/10号)、字重粗细、颜色深浅三个维度的组合,建立了明确的信息层级。重要数据用大字号粗体,次要信息用小字号浅色,辅助说明用最小字号最浅色。
条件渲染的巧妙运用。从底部导航栏的选中指示条、配件卡片的低库存预警标签,到技师列表的分割线,系统大量使用条件渲染来实现动态UI效果,代码简洁而表达力强。
纯代码绘制的图表。统计页面的柱状图和进度条完全使用基础UI组件(Column、Row)和属性设置(渐变、圆角、动态宽高)实现,无需引入任何第三方图表库,展现了声明式UI的强大表现力。
15.4 业务逻辑完整性
从业务角度看,系统覆盖了汽修门店运营的核心环节:
- 接车环节:通过工单新增功能记录车辆信息和维修需求。
- 维修环节:通过工单状态流转(待检修->维修中->待配件->已完成)跟踪维修进度。
- 配件环节:通过配件库存管理和低库存预警确保维修所需配件的供应。
- 交付环节:通过"已交付"状态标记完成交付的工单。
- 复盘环节:通过经营统计页面分析工单量趋势、营收情况和技师绩效。
五个功能模块形成了一个完整的业务闭环,从接车到交付再到经营分析,覆盖了汽修门店日常运营的全流程。
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
// ============================================================
// 464.ets - 车辆维修保养管理系统
// 科技蓝灰金属色系:钢蓝#37474F / 亮蓝#29B6F6 / 浅灰蓝#ECEFF1
// 橙色#FF7043警示 / 绿色#66BB6A完成
// ============================================================
// ---- 接口定义 ----
interface ColorPalette {
primary: string
accent: string
bg: string
warning: string
success: string
textPrimary: string
textSecondary: string
textHint: string
border: string
cardBg: string
white: string
danger: string
dark: string
}
interface AppConfig {
appName: string
shopName: string
shopAddress: string
shopPhone: string
serviceTime: string
techLeader: string
}
interface WorkStatusCfg {
label: string
color: string
bg: string
}
interface WorkStatusMap {
pending: WorkStatusCfg
working: WorkStatusCfg
waitingParts: WorkStatusCfg
finished: WorkStatusCfg
delivered: WorkStatusCfg
}
interface CategoryCfg {
label: string
color: string
bg: string
}
interface CategoryMap {
oil: CategoryCfg
filter: CategoryCfg
brake: CategoryCfg
tire: CategoryCfg
battery: CategoryCfg
spark: CategoryCfg
wiper: CategoryCfg
other: CategoryCfg
}
interface TechInfo {
name: string
title: string
years: string
phone: string
skill: string
}
// ---- 数据模型 ----
@Observed class WorkOrder {
id: number = 0
orderNo: string = ''
plateNo: string = ''
model: string = ''
project: string = ''
mechanic: string = ''
status: string = ''
inTime: string = ''
expectTime: string = ''
cost: number = 0
constructor(id: number, orderNo: string, plateNo: string, model: string, project: string,
mechanic: string, status: string, inTime: string, expectTime: string, cost: number) {
this.id = id
this.orderNo = orderNo
this.plateNo = plateNo
this.model = model
this.project = project
this.mechanic = mechanic
this.status = status
this.inTime = inTime
this.expectTime = expectTime
this.cost = cost
}
}
@Observed class Vehicle {
id: number = 0
plateNo: string = ''
brand: string = ''
model: string = ''
year: number = 0
color: string = ''
owner: string = ''
phone: string = ''
mileage: number = 0
lastMaintenance: string = ''
constructor(id: number, plateNo: string, brand: string, model: string, year: number,
color: string, owner: string, phone: string, mileage: number, lastMaintenance: string) {
this.id = id
this.plateNo = plateNo
this.brand = brand
this.model = model
this.year = year
this.color = color
this.owner = owner
this.phone = phone
this.mileage = mileage
this.lastMaintenance = lastMaintenance
}
}
@Observed class PartsItem {
id: number = 0
name: string = ''
code: string = ''
category: string = ''
stock: number = 0
price: number = 0
supplier: string = ''
minStock: number = 0
constructor(id: number, name: string, code: string, category: string, stock: number,
price: number, supplier: string, minStock: number) {
this.id = id
this.name = name
this.code = code
this.category = category
this.stock = stock
this.price = price
this.supplier = supplier
this.minStock = minStock
}
}
@Observed class MonthStat {
month: string = ''
count: number = 0
constructor(month: string, count: number) {
this.month = month
this.count = count
}
}
@Observed class CategoryStockStat {
category: string = ''
stock: number = 0
constructor(category: string, stock: number) {
this.category = category
this.stock = stock
}
}
@Observed class MechanicStat {
name: string = ''
hours: number = 0
orders: number = 0
cost: number = 0
constructor(name: string, hours: number, orders: number, cost: number) {
this.name = name
this.hours = hours
this.orders = orders
this.cost = cost
}
}
// ---- 静态配置 ----
const COLORS: ColorPalette = {
primary: '#37474F',
accent: '#29B6F6',
bg: '#ECEFF1',
warning: '#FF7043',
success: '#66BB6A',
textPrimary: '#37474F',
textSecondary: '#607D8B',
textHint: '#90A4AE',
border: '#DDDDDD',
cardBg: '#FFFFFF',
white: '#FFFFFF',
danger: '#EF5350',
dark: '#263238'
}
const APP_CONFIG: AppConfig = {
appName: '车管家',
shopName: '速达汽修连锁 · 中关村旗舰店',
shopAddress: '北京市海淀区中关村大街128号',
shopPhone: '010-6288-5566',
serviceTime: '08:30 - 20:30',
techLeader: '王师傅'
}
const WORK_STATUS: WorkStatusMap = {
pending: { label: '待检修', color: '#FF7043', bg: '#FFF3E0' },
working: { label: '维修中', color: '#29B6F6', bg: '#E1F5FE' },
waitingParts: { label: '待配件', color: '#FFA726', bg: '#FFF8E1' },
finished: { label: '已完成', color: '#66BB6A', bg: '#E8F5E9' },
delivered: { label: '已交付', color: '#37474F', bg: '#ECEFF1' }
}
const PART_CATEGORY: CategoryMap = {
oil: { label: '机油', color: '#FF7043', bg: '#FFF3E0' },
filter: { label: '滤芯', color: '#29B6F6', bg: '#E1F5FE' },
brake: { label: '刹车片', color: '#EF5350', bg: '#FFEBEE' },
tire: { label: '轮胎', color: '#5C6BC0', bg: '#E8EAF6' },
battery: { label: '电瓶', color: '#66BB6A', bg: '#E8F5E9' },
spark: { label: '火花塞', color: '#FFA726', bg: '#FFF8E1' },
wiper: { label: '雨刮', color: '#26A69A', bg: '#E0F2F1' },
other: { label: '其他', color: '#78909C', bg: '#ECEFF1' }
}
const WORK_STATUS_FILTERS: string[] = ['全部', '待检修', '维修中', '待配件', '已完成', '已交付']
const CATEGORY_FILTERS: string[] = ['全部', '机油', '滤芯', '刹车片', '轮胎', '电瓶', '火花塞', '雨刮', '其他']
// ---- Mock数据 ----
const mockWorkOrders: WorkOrder[] = [
new WorkOrder(1, 'WX-2025-0812-001', '京A8W5J6', '丰田卡罗拉', '更换刹车片+机油保养', '李工', '维修中', '08/12 09:30', '08/12 16:00', 680),
new WorkOrder(2, 'WX-2025-0812-002', '京N6K3M2', '大众朗逸', '更换轮胎四条+四轮定位', '张工', '待配件', '08/12 08:50', '08/13 11:00', 2360),
new WorkOrder(3, 'WX-2025-0812-003', '京Q7A8P5', '本田雅阁', '发动机大保养', '王工', '维修中', '08/12 09:10', '08/12 18:00', 1250),
new WorkOrder(4, 'WX-2025-0812-004', '京B3C9T1', '比亚迪汉EV', '更换电瓶', '李工', '已完成', '08/11 14:20', '08/11 15:40', 850),
new WorkOrder(5, 'WX-2025-0812-005', '京H5F2V8', '别克GL8', '空调系统检修+加氟', '赵工', '待检修', '08/12 10:00', '08/12 17:30', 520),
new WorkOrder(6, 'WX-2025-0812-006', '京M1D6R3', '奔驰C200', '更换火花塞+清洗节气门', '张工', '维修中', '08/12 09:40', '08/12 15:00', 980),
new WorkOrder(7, 'WX-2025-0812-007', '京C2E7S9', '奥迪A6L', '变速箱油更换', '王工', '待配件', '08/11 16:30', '08/13 10:00', 1480),
new WorkOrder(8, 'WX-2025-0812-008', '京K8G4N6', '哈弗H6', '更换雨刮+检查刹车', '赵工', '已完成', '08/11 11:00', '08/11 12:30', 260),
new WorkOrder(9, 'WX-2025-0812-009', '京F4H9J7', '日产轩逸', '机油保养+机滤空滤', '李工', '待检修', '08/12 11:00', '08/12 18:30', 450),
new WorkOrder(10, 'WX-2025-0812-010', '京J1M5W2', '宝马X5', '刹车盘打磨+刹车油更换', '张工', '维修中', '08/12 08:30', '08/12 19:00', 1650),
new WorkOrder(11, 'WX-2025-0812-011', '京P9N3Q8', '特斯拉Model3', '轮胎换位+动平衡', '赵工', '已完成', '08/10 15:00', '08/10 16:20', 380),
new WorkOrder(12, 'WX-2025-0812-012', '京R2T7Y4', '大众迈腾', '底盘异响检查', '王工', '维修中', '08/12 10:30', '08/12 16:30', 420),
new WorkOrder(13, 'WX-2025-0812-013', '京S5K8U1', '丰田RAV4', '更换前轮刹车片', '李工', '待配件', '08/12 09:00', '08/13 09:00', 720),
new WorkOrder(14, 'WX-2025-0812-014', '京T6L9E2', '本田CR-V', '全车漆面抛光打蜡', '赵工', '待检修', '08/12 13:00', '08/13 12:00', 880),
new WorkOrder(15, 'WX-2025-0812-015', '京U3P4F6', '奥迪Q5', '更换电瓶+全车检测', '张工', '已完成', '08/11 09:20', '08/11 11:40', 1050),
new WorkOrder(16, 'WX-2025-0812-016', '京V8R6C3', '五菱宏光', '机油保养', '李工', '已交付', '08/11 10:10', '08/11 12:00', 320),
new WorkOrder(17, 'WX-2025-0812-017', '京W4N7J9', '宝马3系', '更换后轮刹车片+刹车油', '王工', '维修中', '08/12 11:30', '08/12 17:00', 960),
new WorkOrder(18, 'WX-2025-0812-018', '京X1M2K8', '吉利星越L', '更换火花塞+点火线圈', '赵工', '已完成', '08/10 14:00', '08/10 17:30', 760),
new WorkOrder(19, 'WX-2025-0812-019', '京Y5J6L4', '别克君威', '变速箱油+滤网更换', '张工', '待配件', '08/12 08:00', '08/13 15:00', 1560),
new WorkOrder(20, 'WX-2025-0812-020', '京Z7H2N5', '雷克萨斯ES', '常规保养+空调滤芯', '李工', '已交付', '08/11 13:30', '08/11 15:20', 560)
]
const mockVehicles: Vehicle[] = [
new Vehicle(1, '京A8W5J6', '丰田', '卡罗拉2023款', 2023, '珍珠白', '赵磊', '139****2233', 45000, '2025-07-01'),
new Vehicle(2, '京N6K3M2', '大众', '朗逸2022款', 2022, '曜石黑', '孙丽', '138****7765', 62000, '2025-06-15'),
new Vehicle(3, '京Q7A8P5', '本田', '雅阁2021款', 2021, '极光蓝', '周强', '137****9981', 83000, '2025-05-20'),
new Vehicle(4, '京B3C9T1', '比亚迪', '汉EV 2023款', 2023, '赤帝红', '吴敏', '136****4521', 28000, '2025-07-22'),
new Vehicle(5, '京H5F2V8', '别克', 'GL8 2022款', 2022, '星夜灰', '郑浩', '135****6634', 96000, '2025-04-18'),
new Vehicle(6, '京M1D6R3', '奔驰', 'C200L 2023款', 2023, '曜岩黑', '王芳', '134****8812', 31000, '2025-07-10'),
new Vehicle(7, '京C2E7S9', '奥迪', 'A6L 2021款', 2021, '深海蓝', '李娜', '133****5567', 89000, '2025-03-30'),
new Vehicle(8, '京K8G4N6', '哈弗', 'H6 2020款', 2020, '炫晶白', '陈勇', '132****4478', 118000, '2025-02-14'),
new Vehicle(9, '京F4H9J7', '日产', '轩逸2023款', 2023, '珠光白', '刘洋', '131****6639', 15000, '2025-07-28'),
new Vehicle(10, '京J1M5W2', '宝马', 'X5 2022款', 2022, '碳黑', '徐鹏', '130****2245', 52000, '2025-06-08'),
new Vehicle(11, '京P9N3Q8', '特斯拉', 'Model3 2022款', 2022, '珍珠白', '杨雪', '188****7741', 47000, '2025-06-25'),
new Vehicle(12, '京R2T7Y4', '大众', '迈腾2021款', 2021, '幻影黑', '高翔', '186****8890', 74000, '2025-04-02'),
new Vehicle(13, '京S5K8U1', '丰田', 'RAV4 2023款', 2023, '铂金白', '马超', '185****3309', 26000, '2025-07-18'),
new Vehicle(14, '京T6L9E2', '本田', 'CR-V 2022款', 2022, '炫雅银', '朱琳', '184****6672', 68000, '2025-05-12'),
new Vehicle(15, '京U3P4F6', '奥迪', 'Q5L 2023款', 2023, '天云灰', '罗刚', '183****2298', 33000, '2025-07-05'),
new Vehicle(16, '京V8R6C3', '五菱', '宏光S 2019款', 2019, '晴空银', '何军', '182****8856', 136000, '2025-01-20')
]
const mockParts: PartsItem[] = [
new PartsItem(1, '博世刹车片前轮', 'P-001', '刹车片', 32, 180, '博世授权店', 10),
new PartsItem(2, '博世刹车片后轮', 'P-002', '刹车片', 8, 168, '博世授权店', 10),
new PartsItem(3, '嘉实多极护5W-30', 'P-003', '机油', 56, 320, '嘉实多华北总代', 20),
new PartsItem(4, '美孚速霸1000', 'P-004', '机油', 9, 268, '美孚直营', 15),
new PartsItem(5, '曼牌机油滤芯', 'P-005', '滤芯', 48, 45, '曼牌经销', 20),
new PartsItem(6, '马勒空气滤芯', 'P-006', '滤芯', 36, 52, '马勒代理', 15),
new PartsItem(7, '马勒空调滤芯', 'P-007', '滤芯', 29, 68, '马勒代理', 15),
new PartsItem(8, '米其林轮胎205/55R16', 'P-008', '轮胎', 14, 620, '米其林授权店', 6),
new PartsItem(9, '普利司通轮胎225/45R18', 'P-009', '轮胎', 5, 780, '普利司通总代', 6),
new PartsItem(10, '瓦尔塔AGM电瓶70Ah', 'P-010', '电瓶', 12, 1280, '瓦尔塔授权', 5),
new PartsItem(11, '风帆电瓶60Ah', 'P-011', '电瓶', 3, 560, '风帆代理', 6),
new PartsItem(12, 'NGK铱金火花塞', 'P-012', '火花塞', 42, 88, 'NGK经销', 16),
new PartsItem(13, '博世双铱金火花塞', 'P-013', '火花塞', 18, 120, '博世授权店', 12),
new PartsItem(14, '3M无骨雨刮24寸', 'P-014', '雨刮', 26, 75, '3M经销商', 12),
new PartsItem(15, '博世雨刮器总成', 'P-015', '雨刮', 7, 96, '博世授权店', 10),
new PartsItem(16, '长城润滑油变速箱油', 'P-016', '机油', 4, 260, '长城直营', 10),
new PartsItem(17, '菲罗多刹车盘', 'P-017', '刹车片', 11, 320, '菲罗多代理', 5),
new PartsItem(18, '壳牌刹车油DOT4', 'P-018', '其他', 22, 68, '壳牌授权', 10)
]
const mockMonthStats: MonthStat[] = [
new MonthStat('2月', 112),
new MonthStat('3月', 138),
new MonthStat('4月', 126),
new MonthStat('5月', 152),
new MonthStat('6月', 145),
new MonthStat('7月', 168)
]
const mockCategoryStocks: CategoryStockStat[] = [
new CategoryStockStat('机油', 69),
new CategoryStockStat('滤芯', 113),
new CategoryStockStat('刹车片', 51),
new CategoryStockStat('轮胎', 19),
new CategoryStockStat('电瓶', 15),
new CategoryStockStat('火花塞', 60),
new CategoryStockStat('雨刮', 33),
new CategoryStockStat('其他', 22)
]
const mockMechanicStats: MechanicStat[] = [
new MechanicStat('李工', 186, 42, 28600),
new MechanicStat('张工', 172, 38, 34200),
new MechanicStat('王工', 168, 35, 29800),
new MechanicStat('赵工', 145, 30, 21500)
]
const mockTechnicians: TechInfo[] = [
{ name: '李工', title: '高级技师', years: '12年', phone: '139****1101', skill: '发动机/底盘' },
{ name: '张工', title: '高级技师', years: '10年', phone: '139****1102', skill: '制动/电气' },
{ name: '王工', title: '中级技师', years: '7年', phone: '139****1103', skill: '变速箱/空调' },
{ name: '赵工', title: '中级技师', years: '5年', phone: '139****1104', skill: '轮胎/钣金' }
]
// ---- 全局纯函数 ----
function getStatusCfg(status: string): WorkStatusCfg {
if (status === '待检修') {
return WORK_STATUS.pending
}
if (status === '维修中') {
return WORK_STATUS.working
}
if (status === '待配件') {
return WORK_STATUS.waitingParts
}
if (status === '已完成') {
return WORK_STATUS.finished
}
return WORK_STATUS.delivered
}
function getCategoryCfg(category: string): CategoryCfg {
if (category === '机油') {
return PART_CATEGORY.oil
}
if (category === '滤芯') {
return PART_CATEGORY.filter
}
if (category === '刹车片') {
return PART_CATEGORY.brake
}
if (category === '轮胎') {
return PART_CATEGORY.tire
}
if (category === '电瓶') {
return PART_CATEGORY.battery
}
if (category === '火花塞') {
return PART_CATEGORY.spark
}
if (category === '雨刮') {
return PART_CATEGORY.wiper
}
return PART_CATEGORY.other
}
function genOrderNo(len: number): string {
const n = len + 1
if (n < 100) {
return 'WX-2025-0812-0' + n.toString()
}
return 'WX-2025-0812-' + n.toString()
}
function genPartCode(len: number): string {
const n = len + 1
if (n < 100) {
return 'P-0' + n.toString()
}
return 'P-' + n.toString()
}
function formatMoney(cost: number): string {
return '¥' + cost.toFixed(0)
}
function formatMileage(mileage: number): string {
return (mileage / 10000).toFixed(1) + '万km'
}
function todayStr(): string {
return '08/12'
}
function nowTimeStr(): string {
return '08/12 09:00'
}
function filterOrders(orders: WorkOrder[], statusFilter: string, keyword: string): WorkOrder[] {
const result: WorkOrder[] = []
for (let i = 0; i < orders.length; i++) {
const o = orders[i]
const matchStatus = statusFilter === '全部' || o.status === statusFilter
const matchKw = keyword === '' || o.plateNo.indexOf(keyword) >= 0 ||
o.orderNo.indexOf(keyword) >= 0 || o.model.indexOf(keyword) >= 0
if (matchStatus && matchKw) {
result.push(o)
}
}
return result
}
function filterVehicles(vehicles: Vehicle[], keyword: string): Vehicle[] {
const result: Vehicle[] = []
for (let i = 0; i < vehicles.length; i++) {
const v = vehicles[i]
const matchKw = keyword === '' || v.plateNo.indexOf(keyword) >= 0 ||
v.brand.indexOf(keyword) >= 0 || v.owner.indexOf(keyword) >= 0
if (matchKw) {
result.push(v)
}
}
return result
}
function filterParts(parts: PartsItem[], categoryFilter: string, keyword: string): PartsItem[] {
const result: PartsItem[] = []
for (let i = 0; i < parts.length; i++) {
const p = parts[i]
const matchCat = categoryFilter === '全部' || p.category === categoryFilter
const matchKw = keyword === '' || p.name.indexOf(keyword) >= 0 || p.code.indexOf(keyword) >= 0
if (matchCat && matchKw) {
result.push(p)
}
}
return result
}
function getMaxCount(stats: MonthStat[]): number {
let max = 1
for (let i = 0; i < stats.length; i++) {
if (stats[i].count > max) {
max = stats[i].count
}
}
return max
}
function getMaxStock(stats: CategoryStockStat[]): number {
let max = 1
for (let i = 0; i < stats.length; i++) {
if (stats[i].stock > max) {
max = stats[i].stock
}
}
return max
}
function getStockPct(stock: number, stats: CategoryStockStat[]): number {
const max = getMaxStock(stats)
let pct = Math.floor(stock / max * 100)
if (pct < 8) {
pct = 8
}
return pct
}
function getBarHeight(count: number, stats: MonthStat[]): number {
const max = getMaxCount(stats)
const h = Math.floor(count / max * 120)
return 24 + h
}
function getTotalCost(orders: WorkOrder[]): number {
let total = 0
for (let i = 0; i < orders.length; i++) {
if (orders[i].status === '已完成' || orders[i].status === '已交付') {
total = total + orders[i].cost
}
}
return total
}
function countFinishedOrders(orders: WorkOrder[]): number {
let c = 0
for (let i = 0; i < orders.length; i++) {
if (orders[i].status === '已完成' || orders[i].status === '已交付') {
c = c + 1
}
}
return c
}
function countWaitingOrders(orders: WorkOrder[]): number {
let c = 0
for (let i = 0; i < orders.length; i++) {
if (orders[i].status === '待配件') {
c = c + 1
}
}
return c
}
// ---- Tab索引枚举 ----
enum TabIndex {
WorkOrder = 0,
Vehicle = 1,
Parts = 2,
Stats = 3,
Profile = 4
}
// ---- 主入口 ----
@Entry
@Component
struct GarageApp {
@State currentTab: number = TabIndex.WorkOrder
@State workOrders: WorkOrder[] = mockWorkOrders
@State vehicles: Vehicle[] = mockVehicles
@State parts: PartsItem[] = mockParts
@Builder modalOverlay(onClose: () => void) {
Column() {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => { onClose() })
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
}
@Builder headerBar(title: string) {
Row() {
Text(title)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Column().layoutWeight(1)
Text('🛠️')
.fontSize(24)
}
.width('100%')
.padding({ left: 20, right: 20, top: 50, bottom: 16 })
.alignItems(VerticalAlign.Center)
.linearGradient({
direction: GradientDirection.RightBottom,
colors: [[0x37474F, 0.0], [0x263238, 1.0]]
})
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
}
@Builder bottomTabBar() {
Row() {
Column() {
Text('🔧')
.fontSize(22)
.margin({ bottom: 2 })
Text('工单')
.fontSize(11)
.fontColor(this.currentTab === TabIndex.WorkOrder ? '#29B6F6' : '#90A4AE')
if (this.currentTab === TabIndex.WorkOrder) {
Column()
.width(20)
.height(3)
.borderRadius(2)
.backgroundColor('#29B6F6')
.margin({ top: 2 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.currentTab = TabIndex.WorkOrder })
Column() {
Text('🚗')
.fontSize(22)
.margin({ bottom: 2 })
Text('车辆')
.fontSize(11)
.fontColor(this.currentTab === TabIndex.Vehicle ? '#29B6F6' : '#90A4AE')
if (this.currentTab === TabIndex.Vehicle) {
Column()
.width(20)
.height(3)
.borderRadius(2)
.backgroundColor('#29B6F6')
.margin({ top: 2 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.currentTab = TabIndex.Vehicle })
Column() {
Text('📦')
.fontSize(22)
.margin({ bottom: 2 })
Text('配件')
.fontSize(11)
.fontColor(this.currentTab === TabIndex.Parts ? '#29B6F6' : '#90A4AE')
if (this.currentTab === TabIndex.Parts) {
Column()
.width(20)
.height(3)
.borderRadius(2)
.backgroundColor('#29B6F6')
.margin({ top: 2 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.currentTab = TabIndex.Parts })
Column() {
Text('📊')
.fontSize(22)
.margin({ bottom: 2 })
Text('统计')
.fontSize(11)
.fontColor(this.currentTab === TabIndex.Stats ? '#29B6F6' : '#90A4AE')
if (this.currentTab === TabIndex.Stats) {
Column()
.width(20)
.height(3)
.borderRadius(2)
.backgroundColor('#29B6F6')
.margin({ top: 2 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.currentTab = TabIndex.Stats })
Column() {
Text('👤')
.fontSize(22)
.margin({ bottom: 2 })
Text('我的')
.fontSize(11)
.fontColor(this.currentTab === TabIndex.Profile ? '#29B6F6' : '#90A4AE')
if (this.currentTab === TabIndex.Profile) {
Column()
.width(20)
.height(3)
.borderRadius(2)
.backgroundColor('#29B6F6')
.margin({ top: 2 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.currentTab = TabIndex.Profile })
}
.width('100%')
.height(64)
.backgroundColor('#263238')
.border({ width: 1, color: '#37474F' })
}
build() {
Column() {
Stack() {
Column() {
if (this.currentTab === TabIndex.WorkOrder) {
this.headerBar('维修工单')
WorkOrderTab({ orders: this.workOrders })
}
if (this.currentTab === TabIndex.Vehicle) {
this.headerBar('车辆档案')
VehicleTab({ vehicles: this.vehicles })
}
if (this.currentTab === TabIndex.Parts) {
this.headerBar('配件库存')
PartsTab({ parts: this.parts })
}
if (this.currentTab === TabIndex.Stats) {
this.headerBar('经营统计')
StatsTab()
}
if (this.currentTab === TabIndex.Profile) {
this.headerBar('我的门店')
ProfileTab({ orders: this.workOrders })
}
this.bottomTabBar()
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
}
// ---- 工单Tab ----
@Component
struct WorkOrderTab {
@Link orders: WorkOrder[]
@State statusFilter: string = '全部'
@State keyword: string = ''
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteConfirm: boolean = false
@State editItem: WorkOrder | null = null
@State deleteItem: WorkOrder | null = null
@State formOrderNo: string = ''
@State formPlateNo: string = ''
@State formModel: string = ''
@State formProject: string = ''
@State formMechanic: string = ''
@State formStatus: string = '待检修'
@State formInTime: string = ''
@State formExpectTime: string = ''
@State formCost: string = ''
openAdd() {
this.formOrderNo = genOrderNo(this.orders.length)
this.formPlateNo = ''
this.formModel = ''
this.formProject = ''
this.formMechanic = ''
this.formStatus = '待检修'
this.formInTime = nowTimeStr()
this.formExpectTime = '08/12 18:00'
this.formCost = ''
this.showAddModal = true
}
openEdit(item: WorkOrder) {
this.editItem = item
this.formOrderNo = item.orderNo
this.formPlateNo = item.plateNo
this.formModel = item.model
this.formProject = item.project
this.formMechanic = item.mechanic
this.formStatus = item.status
this.formInTime = item.inTime
this.formExpectTime = item.expectTime
this.formCost = item.cost.toString()
this.showEditModal = true
}
openDelete(item: WorkOrder) {
this.deleteItem = item
this.showDeleteConfirm = true
}
confirmAdd() {
const plate = this.formPlateNo === '' ? '京A8W5J6' : this.formPlateNo
const model = this.formModel === '' ? '未知车型' : this.formModel
const project = this.formProject === '' ? '常规保养' : this.formProject
const mech = this.formMechanic === '' ? '李工' : this.formMechanic
const cost = this.formCost === '' ? 0 : Number(this.formCost)
this.orders.push(new WorkOrder(this.orders.length + 1, this.formOrderNo, plate, model,
project, mech, this.formStatus, this.formInTime, this.formExpectTime, cost))
this.showAddModal = false
}
confirmEdit() {
const item = this.editItem
if (item !== null) {
item.plateNo = this.formPlateNo
item.model = this.formModel
item.project = this.formProject
item.mechanic = this.formMechanic
item.status = this.formStatus
item.cost = this.formCost === '' ? 0 : Number(this.formCost)
}
this.editItem = null
this.showEditModal = false
}
confirmDelete() {
const item = this.deleteItem
if (item !== null) {
const idx = this.orders.indexOf(item)
if (idx >= 0) {
this.orders.splice(idx, 1)
}
}
this.deleteItem = null
this.showDeleteConfirm = false
}
@Builder modalOverlay(onClose: () => void) {
Column() {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => { onClose() })
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
}
@Builder statusTag(label: string) {
Text(label)
.fontSize(13)
.fontColor(this.statusFilter === label ? '#FFFFFF' : '#607D8B')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.statusFilter === label ? '#37474F' : '#FFFFFF')
.border({ width: 1, color: this.statusFilter === label ? '#37474F' : '#DDDDDD' })
.margin({ right: 8 })
.onClick(() => { this.statusFilter = label })
}
@Builder formStatusChip(s: string) {
Text(s)
.fontSize(12)
.fontColor(this.formStatus === s ? '#FFFFFF' : '#607D8B')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.formStatus === s ? '#29B6F6' : '#F5F5F5')
.border({ width: 1, color: this.formStatus === s ? '#29B6F6' : '#E0E0E0' })
.margin({ right: 8, bottom: 8 })
.onClick(() => { this.formStatus = s })
}
@Builder workOrderCard(item: WorkOrder) {
Row() {
Column()
.width(4)
.borderRadius(2)
.backgroundColor(getStatusCfg(item.status).color)
.alignSelf(ItemAlign.Stretch)
Column() {
Row() {
Text(item.orderNo)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Column().layoutWeight(1)
Text(item.status)
.fontSize(11)
.fontColor(getStatusCfg(item.status).color)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor(getStatusCfg(item.status).bg)
}
.width('100%')
.alignItems(VerticalAlign.Center)
Row() {
Text('🚗 ' + item.plateNo)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Column().width(10)
Text(item.model)
.fontSize(13)
.fontColor('#888888')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Column().layoutWeight(1)
Text(item.mechanic)
.fontSize(12)
.fontColor('#29B6F6')
}
.width('100%')
.margin({ top: 10 })
.alignItems(VerticalAlign.Center)
Row() {
Text('项目: ' + item.project)
.fontSize(12)
.fontColor('#607D8B')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Column().layoutWeight(1)
Text(formatMoney(item.cost))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
}
.width('100%')
.margin({ top: 10 })
.alignItems(VerticalAlign.Center)
Row() {
Text('进厂 ' + item.inTime + ' 预计 ' + item.expectTime)
.fontSize(11)
.fontColor('#90A4AE')
Column().layoutWeight(1)
Button('编辑')
.height(26)
.borderRadius(13)
.fontSize(11)
.backgroundColor('#E1F5FE')
.fontColor('#0288D1')
.onClick(() => { this.openEdit(item) })
Column().width(8)
Button('删除')
.height(26)
.borderRadius(13)
.fontSize(11)
.backgroundColor('#FFEBEE')
.fontColor('#EF5350')
.onClick(() => { this.openDelete(item) })
}
.width('100%')
.margin({ top: 10 })
.alignItems(VerticalAlign.Center)
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.border({ width: 1, color: '#E8E8E8' })
.shadow({ radius: 6, color: '#14000000', offsetX: 0, offsetY: 2 })
.margin({ bottom: 12 })
}
@Builder modalFormFields() {
Column() {
Column() {
Text('工单号')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formOrderNo, placeholder: '系统自动生成' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formOrderNo = value })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ bottom: 12 })
Column() {
Text('车牌号')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formPlateNo, placeholder: '如 京A8W5J6' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formPlateNo = value })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ bottom: 12 })
Column() {
Text('车型')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formModel, placeholder: '如 丰田卡罗拉' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formModel = value })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ bottom: 12 })
Column() {
Text('维修项目')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formProject, placeholder: '如 更换刹车片' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formProject = value })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ bottom: 12 })
Column() {
Text('维修师傅')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formMechanic, placeholder: '如 李工' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formMechanic = value })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ bottom: 12 })
Column() {
Text('费用(元)')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formCost, placeholder: '如 680' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formCost = value })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ bottom: 12 })
Column() {
Text('维修状态')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 8 })
Row() {
ForEach(WORK_STATUS_FILTERS.slice(1), (s: string) => {
this.formStatusChip(s)
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
.width('100%')
}
@Builder addModalContent() {
Column() {
Text('新增维修工单')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
.margin({ bottom: 4 })
Text('填写车辆维修信息')
.fontSize(13)
.fontColor('#90A4AE')
.margin({ bottom: 16 })
Scroll() {
Column() {
this.modalFormFields()
}
.width('100%')
}
.layoutWeight(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
Row() {
Button('取消')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#F5F5F5')
.fontColor('#666666')
.fontSize(14)
.onClick(() => { this.showAddModal = false })
Column().width(12)
Button('确认新增')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#37474F')
.fontColor('#FFFFFF')
.fontSize(14)
.onClick(() => { this.confirmAdd() })
}
.width('100%')
.margin({ top: 12 })
}
.width('88%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.constraintSize({ maxHeight: '80%' })
}
@Builder editModalContent() {
Column() {
Text('编辑维修工单')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
.margin({ bottom: 4 })
Text('修改工单 ' + this.formOrderNo)
.fontSize(13)
.fontColor('#90A4AE')
.margin({ bottom: 16 })
Scroll() {
Column() {
this.modalFormFields()
}
.width('100%')
}
.layoutWeight(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
Row() {
Button('取消')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#F5F5F5')
.fontColor('#666666')
.fontSize(14)
.onClick(() => { this.showEditModal = false })
Column().width(12)
Button('保存修改')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#29B6F6')
.fontColor('#FFFFFF')
.fontSize(14)
.onClick(() => { this.confirmEdit() })
}
.width('100%')
.margin({ top: 12 })
}
.width('88%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.constraintSize({ maxHeight: '80%' })
}
@Builder deleteModalContent() {
Column() {
Text('⚠️')
.fontSize(40)
.margin({ bottom: 10 })
Text('确认删除该工单?')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
.margin({ bottom: 8 })
Text((this.deleteItem !== null ? this.deleteItem.orderNo : '') + ' 删除后不可恢复')
.fontSize(13)
.fontColor('#90A4AE')
.margin({ bottom: 20 })
Row() {
Button('取消')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#F5F5F5')
.fontColor('#666666')
.fontSize(14)
.onClick(() => { this.showDeleteConfirm = false })
Column().width(12)
Button('确认删除')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#EF5350')
.fontColor('#FFFFFF')
.fontSize(14)
.onClick(() => { this.confirmDelete() })
}
.width('100%')
}
.width('82%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(24)
}
build() {
Stack() {
Column() {
Row() {
Text('🔍')
.fontSize(14)
.margin({ right: 8 })
TextInput({ placeholder: '搜索工单号 / 车牌 / 车型' })
.height(40)
.layoutWeight(1)
.borderRadius(20)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#E0E0E0' })
.padding({ left: 14, right: 14 })
.onChange((value: string) => { this.keyword = value })
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
.alignItems(VerticalAlign.Center)
Scroll() {
Row() {
ForEach(WORK_STATUS_FILTERS, (label: string) => {
this.statusTag(label)
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 12, bottom: 4 })
Scroll() {
Column() {
ForEach(filterOrders(this.orders, this.statusFilter, this.keyword), (item: WorkOrder) => {
this.workOrderCard(item)
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 20 })
}
.layoutWeight(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
Row() {
Button('+ 新增工单')
.height(44)
.width('100%')
.borderRadius(22)
.backgroundColor('#37474F')
.fontColor('#FFFFFF')
.fontSize(15)
.onClick(() => { this.openAdd() })
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 12 })
}
.layoutWeight(1)
.width('100%')
if (this.showAddModal) {
this.modalOverlay(() => { this.showAddModal = false })
Column() {
this.addModalContent()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
if (this.showEditModal) {
this.modalOverlay(() => { this.showEditModal = false })
Column() {
this.editModalContent()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
if (this.showDeleteConfirm) {
this.modalOverlay(() => { this.showDeleteConfirm = false })
Column() {
this.deleteModalContent()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
.layoutWeight(1)
.width('100%')
}
}
// ---- 车辆Tab ----
@Component
struct VehicleTab {
@Link vehicles: Vehicle[]
@State keyword: string = ''
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteConfirm: boolean = false
@State editItem: Vehicle | null = null
@State deleteItem: Vehicle | null = null
@State formPlateNo: string = ''
@State formBrand: string = ''
@State formVModel: string = ''
@State formYear: string = ''
@State formColor: string = ''
@State formOwner: string = ''
@State formPhone: string = ''
@State formMileage: string = ''
@State formLastMaint: string = ''
openAdd() {
this.formPlateNo = ''
this.formBrand = ''
this.formVModel = ''
this.formYear = '2024'
this.formColor = ''
this.formOwner = ''
this.formPhone = ''
this.formMileage = ''
this.formLastMaint = todayStr()
this.showAddModal = true
}
openEdit(item: Vehicle) {
this.editItem = item
this.formPlateNo = item.plateNo
this.formBrand = item.brand
this.formVModel = item.model
this.formYear = item.year.toString()
this.formColor = item.color
this.formOwner = item.owner
this.formPhone = item.phone
this.formMileage = item.mileage.toString()
this.formLastMaint = item.lastMaintenance
this.showEditModal = true
}
openDelete(item: Vehicle) {
this.deleteItem = item
this.showDeleteConfirm = true
}
confirmAdd() {
const plate = this.formPlateNo === '' ? '京A8W5J6' : this.formPlateNo
const brand = this.formBrand === '' ? '丰田' : this.formBrand
const model = this.formVModel === '' ? '卡罗拉' : this.formVModel
const year = this.formYear === '' ? 2024 : Number(this.formYear)
const color = this.formColor === '' ? '珍珠白' : this.formColor
const owner = this.formOwner === '' ? '未知车主' : this.formOwner
const phone = this.formPhone === '' ? '139****0000' : this.formPhone
const mileage = this.formMileage === '' ? 0 : Number(this.formMileage)
const lastMaint = this.formLastMaint === '' ? todayStr() : this.formLastMaint
this.vehicles.push(new Vehicle(this.vehicles.length + 1, plate, brand, model, year,
color, owner, phone, mileage, lastMaint))
this.showAddModal = false
}
confirmEdit() {
const item = this.editItem
if (item !== null) {
item.plateNo = this.formPlateNo
item.brand = this.formBrand
item.model = this.formVModel
item.year = this.formYear === '' ? 2024 : Number(this.formYear)
item.color = this.formColor
item.owner = this.formOwner
item.phone = this.formPhone
item.mileage = this.formMileage === '' ? 0 : Number(this.formMileage)
item.lastMaintenance = this.formLastMaint
}
this.editItem = null
this.showEditModal = false
}
confirmDelete() {
const item = this.deleteItem
if (item !== null) {
const idx = this.vehicles.indexOf(item)
if (idx >= 0) {
this.vehicles.splice(idx, 1)
}
}
this.deleteItem = null
this.showDeleteConfirm = false
}
@Builder modalOverlay(onClose: () => void) {
Column() {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => { onClose() })
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
}
@Builder vehicleCard(item: Vehicle) {
Row() {
Column() {
Text('🚗')
.fontSize(30)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#E1F5FE')
.justifyContent(FlexAlign.Center)
Column() {
Row() {
Text(item.plateNo)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Column().layoutWeight(1)
Text(item.owner)
.fontSize(12)
.fontColor('#29B6F6')
}
.width('100%')
.alignItems(VerticalAlign.Center)
Row() {
Column() {
Text('品牌型号')
.fontSize(10)
.fontColor('#90A4AE')
Text(item.brand + ' ' + item.model)
.fontSize(12)
.fontColor('#37474F')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('年份')
.fontSize(10)
.fontColor('#90A4AE')
Text(item.year + '款')
.fontSize(12)
.fontColor('#37474F')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('颜色')
.fontSize(10)
.fontColor('#90A4AE')
Text(item.color)
.fontSize(12)
.fontColor('#37474F')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.margin({ top: 8 })
Row() {
Column() {
Text('车主电话')
.fontSize(10)
.fontColor('#90A4AE')
Text(item.phone)
.fontSize(12)
.fontColor('#37474F')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('行驶里程')
.fontSize(10)
.fontColor('#90A4AE')
Text(formatMileage(item.mileage))
.fontSize(12)
.fontColor('#FF7043')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('上次保养')
.fontSize(10)
.fontColor('#90A4AE')
Text(item.lastMaintenance)
.fontSize(12)
.fontColor('#37474F')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.margin({ top: 8 })
Row() {
Column().layoutWeight(1)
Button('编辑')
.height(26)
.borderRadius(13)
.fontSize(11)
.backgroundColor('#E1F5FE')
.fontColor('#0288D1')
.onClick(() => { this.openEdit(item) })
Column().width(8)
Button('删除')
.height(26)
.borderRadius(13)
.fontSize(11)
.backgroundColor('#FFEBEE')
.fontColor('#EF5350')
.onClick(() => { this.openDelete(item) })
}
.width('100%')
.margin({ top: 10 })
.alignItems(VerticalAlign.Center)
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.border({ width: 1, color: '#E8E8E8' })
.shadow({ radius: 6, color: '#14000000', offsetX: 0, offsetY: 2 })
.margin({ bottom: 12 })
}
@Builder modalFormFields() {
Column() {
Column() {
Text('车牌号')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formPlateNo, placeholder: '如 京A8W5J6' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formPlateNo = value })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ bottom: 12 })
Row() {
Column() {
Text('品牌')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formBrand, placeholder: '如 丰田' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formBrand = value })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column().width(10)
Column() {
Text('型号')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formVModel, placeholder: '如 卡罗拉2023款' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formVModel = value })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.margin({ bottom: 12 })
.alignItems(VerticalAlign.Top)
Row() {
Column() {
Text('年份')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formYear, placeholder: '如 2023' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formYear = value })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column().width(10)
Column() {
Text('颜色')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formColor, placeholder: '如 珍珠白' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formColor = value })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.margin({ bottom: 12 })
.alignItems(VerticalAlign.Top)
Row() {
Column() {
Text('车主')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formOwner, placeholder: '如 赵磊' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formOwner = value })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column().width(10)
Column() {
Text('电话')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formPhone, placeholder: '如 139****2233' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formPhone = value })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.margin({ bottom: 12 })
.alignItems(VerticalAlign.Top)
Row() {
Column() {
Text('里程(km)')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formMileage, placeholder: '如 45000' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formMileage = value })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column().width(10)
Column() {
Text('上次保养')
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 6 })
TextInput({ text: this.formLastMaint, placeholder: '如 2025-07-01' })
.height(40)
.borderRadius(10)
.backgroundColor('#F5F5F5')
.padding({ left: 12, right: 12 })
.onChange((value: string) => { this.formLastMaint = value })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.alignItems(VerticalAlign.Top)
}
.width('100%')
}
@Builder addModalContent() {
Column() {
Text('新增车辆档案')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
.margin({ bottom: 4 })
Text('登记车辆基本信息')
.fontSize(13)
.fontColor('#90A4AE')
.margin({ bottom: 16 })
Scroll() {
Column() {
this.modalFormFields()
}
.width('100%')
}
.layoutWeight(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
Row() {
Button('取消')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#F5F5F5')
.fontColor('#666666')
.fontSize(14)
.onClick(() => { this.showAddModal = false })
Column().width(12)
Button('确认新增')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#37474F')
.fontColor('#FFFFFF')
.fontSize(14)
.onClick(() => { this.confirmAdd() })
}
.width('100%')
.margin({ top: 12 })
}
.width('88%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.constraintSize({ maxHeight: '80%' })
}
@Builder editModalContent() {
Column() {
Text('编辑车辆档案')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
.margin({ bottom: 4 })
Text('修改车辆 ' + this.formPlateNo)
.fontSize(13)
.fontColor('#90A4AE')
.margin({ bottom: 16 })
Scroll() {
Column() {
this.modalFormFields()
}
.width('100%')
}
.layoutWeight(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
Row() {
Button('取消')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#F5F5F5')
.fontColor('#666666')
.fontSize(14)
.onClick(() => { this.showEditModal = false })
Column().width(12)
Button('保存修改')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#29B6F6')
.fontColor('#FFFFFF')
.fontSize(14)
.onClick(() => { this.confirmEdit() })
}
.width('100%')
.margin({ top: 12 })
}
.width('88%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.constraintSize({ maxHeight: '80%' })
}
@Builder deleteModalContent() {
Column() {
Text('⚠️')
.fontSize(40)
.margin({ bottom: 10 })
Text('确认删除该车辆?')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
.margin({ bottom: 8 })
Text((this.deleteItem !== null ? this.deleteItem.plateNo : '') + ' 删除后不可恢复')
.fontSize(13)
.fontColor('#90A4AE')
.margin({ bottom: 20 })
Row() {
Button('取消')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#F5F5F5')
.fontColor('#666666')
.fontSize(14)
.onClick(() => { this.showDeleteConfirm = false })
Column().width(12)
Button('确认删除')
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('#EF5350')
.fontColor('#FFFFFF')
.fontSize(14)
.onClick(() => { this.confirmDelete() })
}
.width('100%')
}
.width('82%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(24)
}
build() {
Stack() {
Column() {
Row() {
Text('🔍')
.fontSize(14)
.margin({ right: 8 })
TextInput({ placeholder: '搜索车牌 / 品牌 / 车主' })
.height(40)
.layoutWeight(1)
.borderRadius(20)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#E0E0E0' })
.padding({ left: 14, right: 14 })
.onChange((value: string) => { this.keyword = value })
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
.alignItems(VerticalAlign.Center)
Scroll() {
Column() {
ForEach(filterVehicles(this.vehicles, this.keyword), (item: Vehicle) => {
this.vehicleCard(item)
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 20 })
}
.layoutWeight(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
Row() {
Button('+ 新增车辆')
.height(44)
.width('100%')
.borderRadius(22)
.backgroundColor('#37474F')
.fontColor('#FFFFFF')
.fontSize(15)
.onClick(() => { this.openAdd() })
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 12 })
}
.layoutWeight(1)
.width('100%')
if (this.showAddModal) {
this.modalOverlay(() => { this.showAddModal = false })
Column() {
this.addModalContent()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
if (this.showEditModal) {
this.modalOverlay(() => { this.showEditModal = false })
Column() {
this.editModalContent()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
if (this.showDeleteConfirm) {
this.modalOverlay(() => { this.showDeleteConfirm = false })
Column() {
this.deleteModalContent()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
.layoutWeight(1)
.width('100%')
}
}
function getCategoryIcon(category: string): string {
if (category === '机油') {
return '🛢️'
}
if (category === '滤芯') {
return '🌀'
}
if (category === '刹车片') {
return '🛑'
}
if (category === '轮胎') {
return '⭕'
}
if (category === '电瓶') {
return '🔋'
}
if (category === '火花塞') {
return '⚡'
}
if (category === '雨刮') {
return '💧'
}
return '📦'
}
this.serviceRow('⭐', '客户评价管理', '4.9 分')
this.serviceRow('⚙️', '系统设置', 'v2.4.0')
}
.width('100%')
.padding({ left: 16, right: 16 })
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.border({ width: 1, color: '#E8E8E8' })
.shadow({ radius: 6, color: '#14000000', offsetX: 0, offsetY: 2 })
.margin({ top: 12, bottom: 20 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
}
.layoutWeight(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
}
}
15.5 可扩展性分析
系统在多个方面预留了扩展空间:
- 数据层扩展:当前使用Mock数据,后续可替换为后端API调用,工具函数的纯函数设计使这一替换几乎无成本。
- 功能扩展:我的门店Tab中的快捷服务入口(报表、预约、评价、设置)为未来功能迭代提供了入口。
- 状态扩展:工单状态映射和配件分类映射采用配置化设计,新增状态或分类只需修改配置常量,不影响业务逻辑。
- UI主题扩展:颜色配置集中管理,未来可实现主题切换功能(如深色模式)只需替换COLORS常量。
15.6 总结
本系统是一个结构完整、设计精细的车辆维修保养管理移动端应用。它以五大功能模块构建了汽修门店的数字化管理闭环,通过响应式状态驱动和可观察数据模型实现了高效的状态管理,通过统一的色彩体系和精细的信息层级营造了专业的视觉体验,通过纯代码绘制的图表展现了声明式UI的表现力。

从代码层面看,系统展现了良好的工程实践:接口先行、配置集中、函数纯净、组件自治。这些设计原则使得系统具备了较强的可读性、可测试性和可维护性,为后续的功能迭代和规模化开发奠定了坚实的基础。
对于开发者而言,本系统的代码是一份优秀的声明式UI开发学习材料——它涵盖了接口定义、可观察类、状态装饰器、构建器复用、条件渲染、列表渲染、弹窗管理、表单处理、数据过滤、数据可视化等声明式UI开发的核心知识点,每个知识点都有完整的代码示例和真实的业务场景支撑。
更多推荐


所有评论(0)