项目效果

本文实现一个基于 HarmonyOS 和 ArkTS 的校园教室设备报修与处理记录应用。应用可以记录教室设备故障名称、所在教室、紧急程度和设备类型,并支持处理状态切换、高优先级筛选、平均紧急度统计和已处理数量统计。

最终运行效果如下:

【在这里插入项目运行截图】

页面功能包括:

  • 记录故障设备名称;
  • 记录设备所在教室;
  • 记录紧急程度;
  • 选择投影、空调、灯光、网络设备类型;
  • 标记待处理或已处理;
  • 按全部、待处理、已处理、高优先级筛选;
  • 统计已处理数量、平均紧急度、高优先级数量;
  • 删除单条报修记录。

前言

校园教室中经常会遇到设备故障问题,比如投影仪无法显示、空调无法启动、灯管损坏、网络连接异常等。如果这些问题只是口头反馈,后续很容易出现遗漏,也不方便统计哪些故障还没处理、哪些问题比较紧急。

本文实现的校园教室设备报修与处理记录应用,就是围绕这个场景设计的。用户可以添加报修记录,填写设备名称、所在教室和紧急程度,并选择设备类型。故障处理完成后,可以点击按钮切换状态,页面会自动统计已处理数量、平均紧急度和高优先级报修数量。

这个项目不依赖后端接口,也不需要数据库,但包含表单输入、数组新增、状态切换、条件筛选、列表渲染和动态统计等典型 ArkTS 练习内容。


一、核心数据结构

每一条设备报修记录使用 RepairRecord 表示:

interface RepairRecord {
  id: number;
  deviceName: string;
  roomName: string;
  urgentLevel: number;
  deviceType: string;
  handled: boolean;
}

其中:

  • deviceName 表示故障设备名称;
  • roomName 表示设备所在教室;
  • urgentLevel 表示紧急程度;
  • deviceType 表示投影、空调、灯光或网络;
  • handled 表示是否已经处理。

二、页面状态设计

页面使用 @State 保存输入内容、筛选条件和记录列表:

@State private deviceText: string = '';
@State private roomText: string = '';
@State private urgentText: string = '';
@State private deviceType: string = '投影';
@State private filterType: string = '全部';
@State private nextId: number = 5;

@State private records: RepairRecord[] = [
  { id: 1, deviceName: '投影仪无法显示', roomName: '教学楼 A203', urgentLevel: 5, deviceType: '投影', handled: false },
  { id: 2, deviceName: '空调无法启动', roomName: '教学楼 B105', urgentLevel: 4, deviceType: '空调', handled: true },
  { id: 3, deviceName: '灯管闪烁', roomName: '实验楼 C302', urgentLevel: 3, deviceType: '灯光', handled: false },
  { id: 4, deviceName: '教室网络不稳定', roomName: '教学楼 D401', urgentLevel: 4, deviceType: '网络', handled: false }
];

这些状态变化后,ArkUI 会自动刷新页面。


三、添加报修记录

添加记录时,需要读取设备名称、所在教室和紧急程度,并把紧急程度从字符串转换成数字:

private addRecord(): void {
  const deviceName: string = this.deviceText.trim();
  const roomName: string = this.roomText.trim();
  const urgentLevel: number = Number(this.urgentText);

  if (deviceName.length === 0 || roomName.length === 0 || Number.isNaN(urgentLevel) || urgentLevel < 1 || urgentLevel > 5) {
    return;
  }

  const record: RepairRecord = {
    id: this.nextId,
    deviceName,
    roomName,
    urgentLevel,
    deviceType: this.deviceType,
    handled: false
  };

  this.records = [record, ...this.records];
  this.nextId += 1;
  this.deviceText = '';
  this.roomText = '';
  this.urgentText = '';
}

这里把新记录放在数组最前面,方便用户优先看到最新添加的报修信息。


四、状态切换和删除

点击“标记处理”时,只需要切换当前记录的 handled 状态:

private toggleHandled(id: number): void {
  this.records = this.records.map((item: RepairRecord) => {
    if (item.id === id) {
      return {
        id: item.id,
        deviceName: item.deviceName,
        roomName: item.roomName,
        urgentLevel: item.urgentLevel,
        deviceType: item.deviceType,
        handled: !item.handled
      };
    }
    return item;
  });
}

删除记录使用 filter()

private deleteRecord(id: number): void {
  this.records = this.records.filter((item: RepairRecord) => item.id !== id);
}

五、筛选和统计

筛选逻辑如下:

private getFilteredRecords(): RepairRecord[] {
  if (this.filterType === '待处理') {
    return this.records.filter((item: RepairRecord) => !item.handled);
  }

  if (this.filterType === '已处理') {
    return this.records.filter((item: RepairRecord) => item.handled);
  }

  if (this.filterType === '高优先级') {
    return this.records.filter((item: RepairRecord) => item.urgentLevel >= 4);
  }

  return this.records;
}

平均紧急度和高优先级数量也直接从数组计算:

private getAverageUrgent(): number {
  if (this.records.length === 0) {
    return 0;
  }

  let total: number = 0;
  this.records.forEach((item: RepairRecord) => {
    total += item.urgentLevel;
  });
  return Math.round(total / this.records.length);
}

private getHighUrgentCount(): number {
  return this.records.filter((item: RepairRecord) => item.urgentLevel >= 4).length;
}

六、页面布局说明

页面主要由四部分组成:

  1. 顶部标题卡片:展示应用名称和说明;
  2. 统计卡片:展示已处理、平均紧急度、高优先级;
  3. 添加表单:输入设备名称、所在教室和紧急程度;
  4. 报修列表:展示每条设备报修记录,并支持处理状态切换和删除。

列表使用 ListForEach 渲染:

List() {
  ForEach(this.getFilteredRecords(), (item: RepairRecord) => {
    ListItem() {
      this.RepairCard(item);
    }
  }, (item: RepairRecord) => item.id.toString());
}

当筛选结果为空时,页面显示空状态提示,避免列表区域空白。


七、Index.ets 完整代码

打开文件:

entry/src/main/ets/pages/Index.ets

将其中内容替换为下面代码:

interface RepairRecord {
  id: number;
  deviceName: string;
  roomName: string;
  urgentLevel: number;
  deviceType: string;
  handled: boolean;
}

@Entry
@Component
struct Index {
  @State private deviceText: string = '';
  @State private roomText: string = '';
  @State private urgentText: string = '';
  @State private deviceType: string = '投影';
  @State private filterType: string = '全部';
  @State private nextId: number = 5;

  @State private records: RepairRecord[] = [
    { id: 1, deviceName: '投影仪无法显示', roomName: '教学楼 A203', urgentLevel: 5, deviceType: '投影', handled: false },
    { id: 2, deviceName: '空调无法启动', roomName: '教学楼 B105', urgentLevel: 4, deviceType: '空调', handled: true },
    { id: 3, deviceName: '灯管闪烁', roomName: '实验楼 C302', urgentLevel: 3, deviceType: '灯光', handled: false },
    { id: 4, deviceName: '教室网络不稳定', roomName: '教学楼 D401', urgentLevel: 4, deviceType: '网络', handled: false }
  ];

  private addRecord(): void {
    const deviceName: string = this.deviceText.trim();
    const roomName: string = this.roomText.trim();
    const urgentLevel: number = Number(this.urgentText);

    if (deviceName.length === 0 || roomName.length === 0 || Number.isNaN(urgentLevel) || urgentLevel < 1 || urgentLevel > 5) {
      return;
    }

    const record: RepairRecord = {
      id: this.nextId,
      deviceName,
      roomName,
      urgentLevel,
      deviceType: this.deviceType,
      handled: false
    };

    this.records = [record, ...this.records];
    this.nextId += 1;
    this.deviceText = '';
    this.roomText = '';
    this.urgentText = '';
  }

  private toggleHandled(id: number): void {
    this.records = this.records.map((item: RepairRecord) => {
      if (item.id === id) {
        return {
          id: item.id,
          deviceName: item.deviceName,
          roomName: item.roomName,
          urgentLevel: item.urgentLevel,
          deviceType: item.deviceType,
          handled: !item.handled
        };
      }
      return item;
    });
  }

  private deleteRecord(id: number): void {
    this.records = this.records.filter((item: RepairRecord) => item.id !== id);
  }

  private getFilteredRecords(): RepairRecord[] {
    if (this.filterType === '待处理') {
      return this.records.filter((item: RepairRecord) => !item.handled);
    }

    if (this.filterType === '已处理') {
      return this.records.filter((item: RepairRecord) => item.handled);
    }

    if (this.filterType === '高优先级') {
      return this.records.filter((item: RepairRecord) => item.urgentLevel >= 4);
    }

    return this.records;
  }

  private getHandledCount(): number {
    return this.records.filter((item: RepairRecord) => item.handled).length;
  }

  private getAverageUrgent(): number {
    if (this.records.length === 0) {
      return 0;
    }

    let total: number = 0;
    this.records.forEach((item: RepairRecord) => {
      total += item.urgentLevel;
    });
    return Math.round(total / this.records.length);
  }

  private getHighUrgentCount(): number {
    return this.records.filter((item: RepairRecord) => item.urgentLevel >= 4).length;
  }

  private getTypeColor(type: string): ResourceColor {
    if (type === '投影') {
      return '#2563EB';
    }

    if (type === '空调') {
      return '#059669';
    }

    if (type === '灯光') {
      return '#D97706';
    }

    return '#7C3AED';
  }

  @Builder
  HeaderCard() {
    Column({ space: 8 }) {
      Text('教室设备报修')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.White)
        .width('100%');

      Text('记录设备故障、紧急程度和处理状态')
        .fontSize(14)
        .fontColor('#E0F2FE')
        .width('100%');
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#2563EB')
    .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
  TypeButton(text: string) {
    Button(text)
      .height(34)
      .layoutWeight(1)
      .fontSize(13)
      .fontColor(this.deviceType === text ? Color.White : '#344054')
      .backgroundColor(this.deviceType === text ? this.getTypeColor(text) : '#E8EEF7')
      .borderRadius(8)
      .onClick(() => {
        this.deviceType = text;
      });
  }

  @Builder
  FilterButton(text: string) {
    Button(text)
      .height(34)
      .layoutWeight(1)
      .fontSize(13)
      .fontColor(this.filterType === text ? Color.White : '#344054')
      .backgroundColor(this.filterType === text ? '#2563EB' : '#E8EEF7')
      .borderRadius(8)
      .onClick(() => {
        this.filterType = text;
      });
  }

  @Builder
  RepairCard(item: RepairRecord) {
    Column({ space: 10 }) {
      Row() {
        Text(item.deviceName)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#182431')
          .layoutWeight(1)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis });

        Text(item.handled ? '已处理' : '待处理')
          .fontSize(12)
          .fontColor(Color.White)
          .backgroundColor(item.handled ? '#059669' : '#D97706')
          .borderRadius(6)
          .padding({ left: 8, right: 8, top: 4, bottom: 4 });
      }
      .width('100%');

      Text(`所在教室:${item.roomName}`)
        .fontSize(13)
        .fontColor('#667085')
        .width('100%');

      Row({ space: 8 }) {
        Text(item.deviceType)
          .fontSize(12)
          .fontColor(Color.White)
          .backgroundColor(this.getTypeColor(item.deviceType))
          .borderRadius(6)
          .padding({ left: 8, right: 8, top: 4, bottom: 4 });

        Text(`紧急程度 ${item.urgentLevel}/5`)
          .fontSize(12)
          .fontColor(item.urgentLevel >= 4 ? '#D92D20' : '#2563EB')
          .backgroundColor(item.urgentLevel >= 4 ? '#FEE4E2' : '#EFF6FF')
          .borderRadius(6)
          .padding({ left: 8, right: 8, top: 4, bottom: 4 });
      }
      .width('100%');

      Row({ space: 10 }) {
        Button(item.handled ? '改为待处理' : '标记处理')
          .height(34)
          .fontSize(13)
          .fontColor('#2563EB')
          .backgroundColor('#EFF6FF')
          .borderRadius(8)
          .padding({ left: 12, right: 12 })
          .onClick(() => {
            this.toggleHandled(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.getHandledCount()}`, '#059669', '#ECFDF3');
        this.StatCard('平均紧急度', `${this.getAverageUrgent()}`, '#2563EB', '#EFF6FF');
        this.StatCard('高优先级', `${this.getHighUrgentCount()}`, '#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: '故障设备,例如 投影仪无法显示', text: this.deviceText })
          .height(42)
          .fontSize(14)
          .backgroundColor('#F5F7FA')
          .borderRadius(8)
          .padding({ left: 12, right: 12 })
          .onChange((value: string) => {
            this.deviceText = value;
          });

        TextInput({ placeholder: '所在教室,例如 教学楼 A203', text: this.roomText })
          .height(42)
          .fontSize(14)
          .backgroundColor('#F5F7FA')
          .borderRadius(8)
          .padding({ left: 12, right: 12 })
          .margin({ top: 10 })
          .onChange((value: string) => {
            this.roomText = value;
          });

        TextInput({ placeholder: '紧急程度 1-5', text: this.urgentText })
          .height(42)
          .fontSize(14)
          .backgroundColor('#F5F7FA')
          .borderRadius(8)
          .type(InputType.Number)
          .padding({ left: 12, right: 12 })
          .margin({ top: 10 })
          .onChange((value: string) => {
            this.urgentText = value;
          });

        Text('设备类型')
          .fontSize(14)
          .fontColor('#667085')
          .width('100%')
          .margin({ top: 14, bottom: 10 });

        Row({ space: 8 }) {
          this.TypeButton('投影');
          this.TypeButton('空调');
          this.TypeButton('灯光');
          this.TypeButton('网络');
        }
        .width('100%');

        Button('添加记录')
          .height(44)
          .fontSize(16)
          .fontColor(Color.White)
          .backgroundColor('#2563EB')
          .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: RepairRecord) => {
            ListItem() {
              this.RepairCard(item);
            }
          }, (item: RepairRecord) => 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、模拟器或真机运行页面。测试步骤如下:

  1. 输入故障设备名称;
  2. 输入所在教室;
  3. 输入 1 到 5 的紧急程度;
  4. 选择投影、空调、灯光或网络类型;
  5. 点击“添加记录”;
  6. 点击“标记处理”切换状态;
  7. 使用筛选按钮查看不同记录;
  8. 删除单条记录;
  9. 检查统计卡片是否同步变化。

经过测试,添加记录、状态切换、筛选、删除和统计功能均可正常运行。


九、总结

本文基于 HarmonyOS 和 ArkTS 实现了一个校园教室设备报修与处理记录应用。项目通过 @State 管理页面数据,使用 ArkUI 完成页面布局,并实现了报修记录添加、处理状态切换、设备类型选择、筛选展示和动态统计等功能。

这个项目虽然是单页面应用,但业务场景具体,交互逻辑完整,适合用来练习 ArkTS 中的表单输入、数组操作、条件渲染和列表渲染。后续可以继续扩展本地存储、报修人信息、维修备注、处理时间、楼栋筛选和故障类型统计等功能。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐