HarmonyOS ArkTS 实战:实现一个校园洗衣房排队与洗衣状态记录应用
项目效果
本文实现一个基于 HarmonyOS 和 ArkTS 的校园洗衣房排队与洗衣状态记录应用。应用可以记录宿舍洗衣房不同洗衣机的排队时间、洗衣模式和完成状态,并支持洗衣状态切换、洗衣模式选择、长队筛选、平均等待时间统计和已完成数量统计。
最终运行效果如下:

页面功能包括:
- 记录洗衣机名称;
- 记录洗衣房位置;
- 记录等待分钟数;
- 选择快洗、标准洗、脱水、烘干模式;
- 标记洗衣中或已完成;
- 按全部、洗衣中、已完成、长队等待筛选;
- 统计已完成数量、平均等待时间、长队等待数量;
- 删除单条洗衣记录。
前言
校园宿舍洗衣房经常会出现排队情况。尤其是晚上、周末或者下雨天,洗衣机和烘干机可能会被连续使用。如果只是靠记忆,很容易忘记自己正在等哪台机器、已经等了多久,或者某次洗衣是否已经完成。
本文实现的校园洗衣房排队与洗衣状态记录应用,就是围绕这个场景设计的。用户可以添加洗衣记录,填写洗衣机名称、洗衣房位置和等待时间,选择洗衣模式。洗衣完成后,可以点击按钮切换状态,页面会自动统计已完成数量、平均等待时间和长队等待数量。
这个项目不依赖后端接口,也不需要数据库,但包含表单输入、数组新增、状态切换、条件筛选、列表渲染和动态统计等典型 ArkTS 练习内容。
一、核心数据结构
每一条洗衣记录使用 LaundryRecord 表示:
interface LaundryRecord {
id: number;
machineName: string;
place: string;
waitMinutes: number;
mode: string;
finished: boolean;
}
其中:
machineName表示洗衣机名称;place表示洗衣房位置;waitMinutes表示等待时间;mode表示快洗、标准洗、脱水或烘干;finished表示是否已经完成。
二、页面状态设计
页面使用 @State 保存输入内容、筛选条件和记录列表:
@State private machineText: string = '';
@State private placeText: string = '';
@State private waitText: string = '';
@State private washMode: string = '标准洗';
@State private filterType: string = '全部';
@State private nextId: number = 5;
@State private records: LaundryRecord[] = [
{ id: 1, machineName: '1号洗衣机', place: '三号楼一层洗衣房', waitMinutes: 8, mode: '快洗', finished: true },
{ id: 2, machineName: '2号洗衣机', place: '三号楼一层洗衣房', waitMinutes: 18, mode: '标准洗', finished: false },
{ id: 3, machineName: '烘干机A', place: '五号楼二层洗衣房', waitMinutes: 12, mode: '烘干', finished: true },
{ id: 4, machineName: '4号洗衣机', place: '五号楼二层洗衣房', waitMinutes: 25, mode: '脱水', finished: false }
];
这些状态变化后,ArkUI 会自动刷新页面。
三、添加洗衣记录
添加记录时,需要读取洗衣机名称、洗衣房位置和等待时间,并把等待时间从字符串转换成数字:
private addRecord(): void {
const machineName: string = this.machineText.trim();
const place: string = this.placeText.trim();
const waitMinutes: number = Number(this.waitText);
if (machineName.length === 0 || place.length === 0 || Number.isNaN(waitMinutes) || waitMinutes <= 0) {
return;
}
const record: LaundryRecord = {
id: this.nextId,
machineName,
place,
waitMinutes,
mode: this.washMode,
finished: false
};
this.records = [record, ...this.records];
this.nextId += 1;
this.machineText = '';
this.placeText = '';
this.waitText = '';
}
这里把新记录放在数组最前面,方便用户优先看到最新添加的洗衣任务。
四、状态切换和删除
点击“标记完成”时,只需要切换当前记录的 finished 状态:
private toggleFinished(id: number): void {
this.records = this.records.map((item: LaundryRecord) => {
if (item.id === id) {
return {
id: item.id,
machineName: item.machineName,
place: item.place,
waitMinutes: item.waitMinutes,
mode: item.mode,
finished: !item.finished
};
}
return item;
});
}
删除记录使用 filter():
private deleteRecord(id: number): void {
this.records = this.records.filter((item: LaundryRecord) => item.id !== id);
}
五、筛选和统计
筛选逻辑如下:
private getFilteredRecords(): LaundryRecord[] {
if (this.filterType === '洗衣中') {
return this.records.filter((item: LaundryRecord) => !item.finished);
}
if (this.filterType === '已完成') {
return this.records.filter((item: LaundryRecord) => item.finished);
}
if (this.filterType === '长队等待') {
return this.records.filter((item: LaundryRecord) => item.waitMinutes >= 15);
}
return this.records;
}
平均等待时间和长队等待数量也直接从数组计算:
private getAverageWait(): number {
if (this.records.length === 0) {
return 0;
}
let total: number = 0;
this.records.forEach((item: LaundryRecord) => {
total += item.waitMinutes;
});
return Math.round(total / this.records.length);
}
private getLongWaitCount(): number {
return this.records.filter((item: LaundryRecord) => item.waitMinutes >= 15).length;
}
六、页面布局说明
页面主要由四部分组成:
- 顶部标题卡片:展示应用名称和说明;
- 统计卡片:展示已完成、平均等待、长队等待;
- 添加表单:输入洗衣机名称、洗衣房位置和等待时间;
- 记录列表:展示每条洗衣记录,并支持完成状态切换和删除。
列表使用 List 和 ForEach 渲染:
List() {
ForEach(this.getFilteredRecords(), (item: LaundryRecord) => {
ListItem() {
this.LaundryCard(item);
}
}, (item: LaundryRecord) => item.id.toString());
}
当筛选结果为空时,页面显示空状态提示,避免列表区域空白。
七、Index.ets 完整代码
打开文件:
entry/src/main/ets/pages/Index.ets
将其中内容替换为下面代码:
interface LaundryRecord {
id: number;
machineName: string;
place: string;
waitMinutes: number;
mode: string;
finished: boolean;
}
@Entry
@Component
struct Index {
@State private machineText: string = '';
@State private placeText: string = '';
@State private waitText: string = '';
@State private washMode: string = '标准洗';
@State private filterType: string = '全部';
@State private nextId: number = 5;
@State private records: LaundryRecord[] = [
{ id: 1, machineName: '1号洗衣机', place: '三号楼一层洗衣房', waitMinutes: 8, mode: '快洗', finished: true },
{ id: 2, machineName: '2号洗衣机', place: '三号楼一层洗衣房', waitMinutes: 18, mode: '标准洗', finished: false },
{ id: 3, machineName: '烘干机A', place: '五号楼二层洗衣房', waitMinutes: 12, mode: '烘干', finished: true },
{ id: 4, machineName: '4号洗衣机', place: '五号楼二层洗衣房', waitMinutes: 25, mode: '脱水', finished: false }
];
private addRecord(): void {
const machineName: string = this.machineText.trim();
const place: string = this.placeText.trim();
const waitMinutes: number = Number(this.waitText);
if (machineName.length === 0 || place.length === 0 || Number.isNaN(waitMinutes) || waitMinutes <= 0) {
return;
}
const record: LaundryRecord = {
id: this.nextId,
machineName,
place,
waitMinutes,
mode: this.washMode,
finished: false
};
this.records = [record, ...this.records];
this.nextId += 1;
this.machineText = '';
this.placeText = '';
this.waitText = '';
}
private toggleFinished(id: number): void {
this.records = this.records.map((item: LaundryRecord) => {
if (item.id === id) {
return {
id: item.id,
machineName: item.machineName,
place: item.place,
waitMinutes: item.waitMinutes,
mode: item.mode,
finished: !item.finished
};
}
return item;
});
}
private deleteRecord(id: number): void {
this.records = this.records.filter((item: LaundryRecord) => item.id !== id);
}
private getFilteredRecords(): LaundryRecord[] {
if (this.filterType === '洗衣中') {
return this.records.filter((item: LaundryRecord) => !item.finished);
}
if (this.filterType === '已完成') {
return this.records.filter((item: LaundryRecord) => item.finished);
}
if (this.filterType === '长队等待') {
return this.records.filter((item: LaundryRecord) => item.waitMinutes >= 15);
}
return this.records;
}
private getFinishedCount(): number {
return this.records.filter((item: LaundryRecord) => item.finished).length;
}
private getAverageWait(): number {
if (this.records.length === 0) {
return 0;
}
let total: number = 0;
this.records.forEach((item: LaundryRecord) => {
total += item.waitMinutes;
});
return Math.round(total / this.records.length);
}
private getLongWaitCount(): number {
return this.records.filter((item: LaundryRecord) => item.waitMinutes >= 15).length;
}
private getModeColor(mode: string): ResourceColor {
if (mode === '快洗') {
return '#059669';
}
if (mode === '标准洗') {
return '#2563EB';
}
if (mode === '脱水') {
return '#D97706';
}
return '#7C3AED';
}
@Builder
HeaderCard() {
Column({ space: 8 }) {
Text('校园洗衣房记录')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.width('100%');
Text('记录洗衣排队时间和当前完成状态')
.fontSize(14)
.fontColor('#DCFCE7')
.width('100%');
}
.width('100%')
.padding(20)
.backgroundColor('#059669')
.borderRadius(8);
}
@Builder
StatCard(title: string, value: string, color: ResourceColor, bgColor: ResourceColor) {
Column({ space: 4 }) {
Text(value)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(color);
Text(title)
.fontSize(12)
.fontColor('#475467');
}
.width('31%')
.height(76)
.justifyContent(FlexAlign.Center)
.backgroundColor(bgColor)
.borderRadius(8);
}
@Builder
ModeButton(text: string) {
Button(text)
.height(34)
.layoutWeight(1)
.fontSize(13)
.fontColor(this.washMode === text ? Color.White : '#344054')
.backgroundColor(this.washMode === text ? this.getModeColor(text) : '#E8EEF7')
.borderRadius(8)
.onClick(() => {
this.washMode = text;
});
}
@Builder
FilterButton(text: string) {
Button(text)
.height(34)
.layoutWeight(1)
.fontSize(13)
.fontColor(this.filterType === text ? Color.White : '#344054')
.backgroundColor(this.filterType === text ? '#059669' : '#E8EEF7')
.borderRadius(8)
.onClick(() => {
this.filterType = text;
});
}
@Builder
LaundryCard(item: LaundryRecord) {
Column({ space: 10 }) {
Row() {
Text(item.machineName)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#182431')
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis });
Text(item.finished ? '已完成' : '洗衣中')
.fontSize(12)
.fontColor(Color.White)
.backgroundColor(item.finished ? '#059669' : '#D97706')
.borderRadius(6)
.padding({ left: 8, right: 8, top: 4, bottom: 4 });
}
.width('100%');
Text(item.place)
.fontSize(13)
.fontColor('#667085')
.width('100%');
Row({ space: 8 }) {
Text(item.mode)
.fontSize(12)
.fontColor(Color.White)
.backgroundColor(this.getModeColor(item.mode))
.borderRadius(6)
.padding({ left: 8, right: 8, top: 4, bottom: 4 });
Text(`${item.waitMinutes} 分钟`)
.fontSize(12)
.fontColor(item.waitMinutes >= 15 ? '#D92D20' : '#059669')
.backgroundColor(item.waitMinutes >= 15 ? '#FEE4E2' : '#ECFDF3')
.borderRadius(6)
.padding({ left: 8, right: 8, top: 4, bottom: 4 });
}
.width('100%');
Row({ space: 10 }) {
Button(item.finished ? '改为洗衣中' : '标记完成')
.height(34)
.fontSize(13)
.fontColor('#059669')
.backgroundColor('#ECFDF3')
.borderRadius(8)
.padding({ left: 12, right: 12 })
.onClick(() => {
this.toggleFinished(item.id);
});
Blank();
Button('删除')
.height(34)
.fontSize(13)
.fontColor('#D92D20')
.backgroundColor('#FEE4E2')
.borderRadius(8)
.padding({ left: 12, right: 12 })
.onClick(() => {
this.deleteRecord(item.id);
});
}
.width('100%');
}
.width('100%')
.padding(16)
.margin({ bottom: 12 })
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({ radius: 10, color: '#10000000', offsetX: 0, offsetY: 4 });
}
@Builder
EmptyState() {
Column({ space: 8 }) {
Text('暂无洗衣记录')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#344054');
Text('添加洗衣任务后,这里会显示洗衣状态列表。')
.fontSize(13)
.fontColor('#667085')
.textAlign(TextAlign.Center);
}
.width('100%')
.padding(26)
.backgroundColor(Color.White)
.borderRadius(8);
}
build() {
Column() {
this.HeaderCard();
Row() {
this.StatCard('已完成', `${this.getFinishedCount()}`, '#059669', '#ECFDF3');
this.StatCard('平均等待', `${this.getAverageWait()}`, '#2563EB', '#EFF6FF');
this.StatCard('长队等待', `${this.getLongWaitCount()}`, '#D97706', '#FFFBEB');
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 14 });
Column() {
Text('添加洗衣任务')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#182431')
.width('100%')
.margin({ bottom: 12 });
TextInput({ placeholder: '洗衣机名称,例如 1号洗衣机', text: this.machineText })
.height(42)
.fontSize(14)
.backgroundColor('#F5F7FA')
.borderRadius(8)
.padding({ left: 12, right: 12 })
.onChange((value: string) => {
this.machineText = value;
});
TextInput({ placeholder: '洗衣房位置,例如 三号楼一层洗衣房', text: this.placeText })
.height(42)
.fontSize(14)
.backgroundColor('#F5F7FA')
.borderRadius(8)
.padding({ left: 12, right: 12 })
.margin({ top: 10 })
.onChange((value: string) => {
this.placeText = value;
});
TextInput({ placeholder: '等待分钟数', text: this.waitText })
.height(42)
.fontSize(14)
.backgroundColor('#F5F7FA')
.borderRadius(8)
.type(InputType.Number)
.padding({ left: 12, right: 12 })
.margin({ top: 10 })
.onChange((value: string) => {
this.waitText = value;
});
Text('洗衣模式')
.fontSize(14)
.fontColor('#667085')
.width('100%')
.margin({ top: 14, bottom: 10 });
Row({ space: 8 }) {
this.ModeButton('快洗');
this.ModeButton('标准洗');
this.ModeButton('脱水');
this.ModeButton('烘干');
}
.width('100%');
Button('添加记录')
.height(44)
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#059669')
.borderRadius(8)
.width('100%')
.margin({ top: 16 })
.onClick(() => {
this.addRecord();
});
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ top: 16 })
.shadow({ radius: 10, color: '#10000000', offsetX: 0, offsetY: 4 });
Text('洗衣记录列表')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#182431')
.width('100%')
.margin({ top: 20, bottom: 12 });
Row({ space: 8 }) {
this.FilterButton('全部');
this.FilterButton('洗衣中');
this.FilterButton('已完成');
this.FilterButton('长队等待');
}
.width('100%')
.margin({ bottom: 12 });
if (this.getFilteredRecords().length === 0) {
this.EmptyState();
} else {
List() {
ForEach(this.getFilteredRecords(), (item: LaundryRecord) => {
ListItem() {
this.LaundryCard(item);
}
}, (item: LaundryRecord) => item.id.toString());
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off);
}
}
.width('100%')
.height('100%')
.padding({ left: 18, right: 18, top: 16 })
.backgroundColor('#F3F6FB');
}
}
八、运行测试
在 DevEco Studio 中打开项目,进入:
entry/src/main/ets/pages/Index.ets
使用 Preview、模拟器或真机运行页面。测试步骤如下:
- 输入洗衣机名称;
- 输入洗衣房位置;
- 输入等待分钟数;
- 选择快洗、标准洗、脱水或烘干;
- 点击“添加记录”;
- 点击“标记完成”切换状态;
- 使用筛选按钮查看不同记录;
- 删除单条记录;
- 检查统计卡片是否同步变化。
经过测试,添加记录、状态切换、筛选、删除和统计功能均可正常运行。
九、总结
本文基于 HarmonyOS 和 ArkTS 实现了一个校园洗衣房排队与洗衣状态记录应用。项目通过 @State 管理页面数据,使用 ArkUI 完成页面布局,并实现了洗衣记录添加、完成状态切换、洗衣模式选择、筛选展示和动态统计等功能。
这个项目虽然是单页面应用,但业务场景具体,交互逻辑完整,适合用来练习 ArkTS 中的表单输入、数组操作、条件渲染和列表渲染。后续可以继续扩展本地存储、洗衣倒计时、洗衣房筛选、机器故障记录和使用高峰统计等功能。
更多推荐

所有评论(0)