在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、项目背景与需求分析

1.1 智能家居市场现状

随着物联网技术的快速发展,智能家居已成为家庭生活的新趋势。据市场研究机构预测,2026年全球智能家居市场规模将突破1500亿美元,中国市场占比超过30%。然而,智能家居方案的规划和实施面临诸多挑战:

  • 信息碎片化:市场上智能设备种类繁多,品牌林立,消费者难以全面了解
  • 方案选型困难:不同空间、不同面积需要不同的设备配置方案
  • 预算难以控制:缺少直观的价格测算工具,容易超支
  • 方案管理混乱:配置方案缺乏统一的记录和管理手段

1.2 应用定位

基于以上痛点,我们开发了智能家居方案管控应用,旨在为用户提供一站式的智能家居方案规划服务:

核心功能说明
场景选择根据空间类型和面积选择适合的智能场景方案
设备配置从8大类16种智能设备中选择所需设备
价格测算实时计算方案总价,支持自定义设备价格
历史记录保存和管理已规划的方案,便于对比和复用

1.3 技术选型

技术选型理由
开发框架HarmonyOS ArkTS原生性能,声明式UI,类型安全
UI组件库ArkUI提供丰富的原生组件和动画能力
数据持久化Preferences轻量级本地存储,适合小数据量场景
状态管理单例模式 + @State简单高效,适合小型应用

二、架构设计与模块划分

2.1 分层架构设计

应用采用经典的三层架构模式:

┌─────────────────────────────────────────────┐
│                  UI层 (Pages)               │
│  Index.ets │ SceneSelectPage.ets           │
│  DeviceSelectPage.ets │ PriceCalculatePage │
│  RecordHistoryPage.ets                     │
├─────────────────────────────────────────────┤
│               业务逻辑层 (Managers)          │
│  SceneManager │ DeviceManager              │
│  RecordManager                             │
├─────────────────────────────────────────────┤
│                   数据层 (Models/Services)   │
│  Scene.ets │ Device.ets │ Record.ets       │
│  Selection.ets │ StorageService.ets        │
└─────────────────────────────────────────────┘

2.2 模块职责说明

模块职责文件
UI层用户交互界面pages/ 目录下所有文件
业务逻辑层数据管理和业务处理managers/ 目录下所有文件
数据模型数据结构定义models/ 目录下所有文件
数据持久化本地存储操作services/StorageService.ets
复用组件可复用UI组件components/ 目录下所有文件

2.3 设计原则

  1. 单一职责原则:每个类只负责一个功能
  2. 依赖倒置原则:高层模块不依赖底层模块,都依赖抽象
  3. 状态管理:使用单例模式管理全局状态,@State管理组件状态
  4. 组件复用:提取通用组件,提高代码复用率
  5. 数据持久化:使用Preferences实现数据本地存储

三、数据模型设计

3.1 Scene(场景模型)

场景模型定义了智能家居方案的基础信息:

export class Scene {
  id: string = '';
  name: string = '';
  area: number = 0;
  type: string = '';
  description: string = '';
  image: Resource = $r('app.media.background');
  width: number = 0;
  height: number = 0;

  constructor(id: string, name: string, area: number, type: string,
              description: string, image: Resource, width: number, height: number) {
    this.id = id;
    this.name = name;
    this.area = area;
    this.type = type;
    this.description = description;
    this.image = image;
    this.width = width;
    this.height = height;
  }

  get sceneArea(): number {
    return this.area;
  }
}

字段说明

字段类型说明
idstring场景唯一标识
namestring场景名称(如"客厅智能方案")
areanumber场景面积(㎡)
typestring场景类型(如"简约现代")
descriptionstring场景描述
imageResource场景示意图资源
widthnumber场景宽度(米)
heightnumber场景高度(米)

3.2 Device(设备模型)

设备模型定义了智能设备的详细信息:

export class Device {
  id: string = '';
  name: string = '';
  category: string = '';
  specification: string = '';
  unit: string = '';
  marketPrice: number = 0;
  customPrice: number = 0;
  selected: boolean = false;
  quantity: number = 1;

  constructor(id: string, name: string, category: string,
              specification: string, unit: string, marketPrice: number) {
    this.id = id;
    this.name = name;
    this.category = category;
    this.specification = specification;
    this.unit = unit;
    this.marketPrice = marketPrice;
    this.customPrice = marketPrice;
    this.selected = false;
    this.quantity = 1;
  }
}

字段说明

字段类型说明
idstring设备唯一标识
namestring设备名称
categorystring设备分类
specificationstring设备规格
unitstring计量单位
marketPricenumber市场价格
customPricenumber用户自定义价格
selectedboolean是否选中
quantitynumber数量

3.3 Record(记录模型)

记录模型保存了用户已规划的方案信息:

import { Device } from './Device';

export class Record {
  id: string = '';
  name: string = '';
  sceneId: string = '';
  sceneName: string = '';
  sceneArea: number = 0;
  sceneType: string = '';
  devices: Array<Device> = [];
  totalPrice: number = 0;
  createTime: string = '';

  constructor(id: string, name: string, sceneId: string, sceneName: string,
              sceneArea: number, sceneType: string, devices: Array<Device>,
              totalPrice: number, createTime: string) {
    this.id = id;
    this.name = name;
    this.sceneId = sceneId;
    this.sceneName = sceneName;
    this.sceneArea = sceneArea;
    this.sceneType = sceneType;
    this.devices = devices;
    this.totalPrice = totalPrice;
    this.createTime = createTime;
  }
}

字段说明

字段类型说明
idstring记录唯一标识(时间戳)
namestring方案名称
sceneIdstring关联场景ID
sceneNamestring关联场景名称
sceneAreanumber场景面积
sceneTypestring场景类型
devicesArray<Device>选中的设备列表
totalPricenumber方案总价
createTimestring创建时间

3.4 Selection(选择状态模型)

选择状态模型用于保存用户的选择状态:

class QuantityPair {
  deviceId: string = '';
  quantity: number = 0;
}

export class Selection {
  sceneId: string = '';
  sceneName: string = '';
  deviceIds: Array<string> = [];
  deviceQuantityPairs: Array<QuantityPair> = [];

  constructor() {
    this.sceneId = '';
    this.sceneName = '';
    this.deviceIds = [];
    this.deviceQuantityPairs = [];
  }

  setQuantity(deviceId: string, quantity: number): void {
    for (let i = 0; i < this.deviceQuantityPairs.length; i++) {
      if (this.deviceQuantityPairs[i].deviceId === deviceId) {
        this.deviceQuantityPairs[i].quantity = quantity;
        return;
      }
    }
    let pair = new QuantityPair();
    pair.deviceId = deviceId;
    pair.quantity = quantity;
    this.deviceQuantityPairs.push(pair);
  }

  getQuantity(deviceId: string): number {
    for (let i = 0; i < this.deviceQuantityPairs.length; i++) {
      if (this.deviceQuantityPairs[i].deviceId === deviceId) {
        return this.deviceQuantityPairs[i].quantity;
      }
    }
    return 1;
  }
}

四、核心管理类实现

4.1 SceneManager(场景管理类)

场景管理类负责管理所有场景数据和选择状态:

import { Scene } from '../models/Scene';

export class SceneManager {
  private scenes: Array<Scene> = [];
  private selectedScene: Scene | null = null;

  constructor() {
    this.initScenes();
  }

  private initScenes(): void {
    this.scenes = [
      new Scene('s1', '客厅智能方案', 30, '简约现代', '适合小户型,基础智能设备配置',
                 $r('app.media.background'), 5.0, 6.0),
      new Scene('s2', '客厅智能方案', 40, '豪华智能', '适合中大户型,全套智能设备',
                 $r('app.media.background'), 6.0, 7.0),
      new Scene('s3', '卧室智能方案', 15, '舒适睡眠', '专注睡眠环境,智能调光控温',
                 $r('app.media.background'), 3.5, 4.5),
      new Scene('s4', '卧室智能方案', 20, '轻奢智能', '舒适与科技结合,智能窗帘照明',
                 $r('app.media.background'), 4.0, 5.0),
      new Scene('s5', '书房智能方案', 10, '高效办公', '专注工作环境,智能降噪照明',
                 $r('app.media.background'), 3.0, 3.5),
      new Scene('s6', '书房智能方案', 15, '多功能室', '办公休闲一体,智能切换场景',
                 $r('app.media.background'), 3.5, 4.5),
      new Scene('s7', '阳台智能方案', 8, '阳光房', '智能遮阳灌溉,绿植养护',
                 $r('app.media.background'), 2.5, 3.2),
      new Scene('s8', '阳台智能方案', 12, '休闲区', '智能照明音响,户外休闲体验',
                 $r('app.media.background'), 3.0, 4.0),
      new Scene('s9', '全屋智能方案', 80, '标准版', '基础全屋智能,覆盖主要空间',
                 $r('app.media.background'), 8.0, 10.0),
      new Scene('s10', '全屋智能方案', 120, '旗舰版', '顶级全屋智能,AI深度融合',
                 $r('app.media.background'), 10.0, 12.0),
    ];
  }

  getScenesByArea(area: number): Array<Scene> {
    let result: Array<Scene> = [];
    for (let i = 0; i < this.scenes.length; i++) {
      if (this.scenes[i].area === area) {
        result.push(this.scenes[i]);
      }
    }
    return result;
  }

  getAllAreas(): Array<number> {
    return [8, 10, 15, 20, 30, 40, 80, 120];
  }

  selectScene(scene: Scene): void {
    this.selectedScene = scene;
  }

  getSelectedScene(): Scene | null {
    return this.selectedScene;
  }
}

export const sceneManager: SceneManager = new SceneManager();

核心方法说明

方法说明
initScenes()初始化10种预设场景方案
getScenesByArea(area)根据面积筛选场景
getAllAreas()获取所有可选面积
selectScene(scene)设置选中场景
getSelectedScene()获取选中场景

4.2 DeviceManager(设备管理类)

设备管理类负责管理所有智能设备数据:

import { Device } from '../models/Device';

export class DeviceManager {
  private devices: Array<Device> = [];

  constructor() {
    this.initDevices();
  }

  private initDevices(): void {
    this.devices = [
      new Device('d1', '智能开关', '控制类', 'WiFi远程控制', '个', 129),
      new Device('d2', '智能插座', '控制类', '定时开关电量统计', '个', 79),
      new Device('d3', '智能灯带', '照明类', 'RGB调光', '米', 99),
      new Device('d4', '智能吸顶灯', '照明类', '色温调节', '盏', 299),
      new Device('d5', '智能窗帘电机', '环境类', '语音控制', '套', 599),
      new Device('d6', '智能空调伴侣', '环境类', '远程控温', '个', 149),
      new Device('d7', '智能门锁', '安防类', '指纹密码NFC', '套', 1299),
      new Device('d8', '智能摄像头', '安防类', '2K夜视', '台', 399),
      new Device('d9', '智能音箱', '交互类', '语音助手', '台', 299),
      new Device('d10', '智能网关', '交互类', '蓝牙Mesh', '个', 199),
      new Device('d11', '智能传感器', '传感类', '温湿度检测', '个', 59),
      new Device('d12', '智能烟雾报警器', '传感类', '远程报警', '个', 129),
      new Device('d13', '智能扫地机器人', '清洁类', '自动清扫', '台', 1599),
      new Device('d14', '智能空气净化器', '清洁类', 'PM2.5监测', '台', 899),
      new Device('d15', '智能投影仪', '娱乐类', '4K高清', '台', 2999),
      new Device('d16', '智能电视盒子', '娱乐类', '语音点播', '台', 399),
    ];
  }
  // ... 其他方法
}

export const deviceManager: DeviceManager = new DeviceManager();

设备分类说明

分类设备价格范围
控制类智能开关、智能插座¥79-¥129
照明类智能灯带、智能吸顶灯¥99-¥299
环境类智能窗帘电机、智能空调伴侣¥149-¥599
安防类智能门锁、智能摄像头¥399-¥1299
交互类智能音箱、智能网关¥199-¥299
传感类智能传感器、智能烟雾报警器¥59-¥129
清洁类智能扫地机器人、智能空气净化器¥899-¥1599
娱乐类智能投影仪、智能电视盒子¥399-¥2999

核心方法说明

方法说明
getDevicesByCategory(category)根据分类筛选设备
getAllCategories()获取所有设备分类
toggleDeviceSelection(id)切换设备选中状态
updateDevicePrice(id, price)更新设备自定义价格
updateDeviceQuantity(id, quantity)更新设备数量
getSelectedDevices()获取所有选中设备
getTotalPrice()计算选中设备总价

4.3 RecordManager(记录管理类)

记录管理类负责保存和管理用户的方案记录:

import { Record } from '../models/Record';
import { Scene } from '../models/Scene';
import { Device } from '../models/Device';
import { storageService } from '../services/StorageService';
import { sceneManager } from './SceneManager';
import { deviceManager } from './DeviceManager';

export class RecordManager {
  private records: Array<Record> = [];

  constructor() {
    this.loadRecords();
  }

  saveRecord(name: string): boolean {
    let scene = sceneManager.getSelectedScene();
    if (scene === null) {
      return false;
    }

    let devices = deviceManager.getSelectedDevices();
    let deviceCopy: Array<Device> = [];
    for (let i = 0; i < devices.length; i++) {
      let dev = devices[i];
      let newDev = new Device(dev.id, dev.name, dev.category, dev.specification, dev.unit, dev.marketPrice);
      newDev.customPrice = dev.customPrice;
      newDev.selected = dev.selected;
      newDev.quantity = dev.quantity;
      deviceCopy.push(newDev);
    }

    let totalPrice: number = 0;
    for (let i = 0; i < deviceCopy.length; i++) {
      totalPrice += deviceCopy[i].customPrice * deviceCopy[i].quantity;
    }

    let timestamp: string = new Date().toISOString();
    let record = new Record(
      timestamp,
      name,
      scene.id,
      scene.name,
      scene.area,
      scene.type,
      deviceCopy,
      totalPrice,
      timestamp
    );

    this.records.unshift(record);
    this.saveRecordsAsync();
    return true;
  }
  // ... 其他方法
}

export const recordManager: RecordManager = new RecordManager();

核心方法说明

方法说明
saveRecord(name)保存方案记录(深拷贝设备数据)
getAllRecords()获取所有记录
getRecordById(id)根据ID获取记录
deleteRecord(id)删除记录
loadRecords()从本地存储加载记录

五、核心UI组件实现

5.1 SmartHomeDrawer(智能家居场景绘制组件)

这是应用中最具特色的组件之一,使用纯代码绘制智能家居场景示意图:

@Component
export struct SmartHomeDrawer {
  private layoutType: string = '';
  private color: string = '#4A90D9';
  private bgColor: string = '#E8F0FE';

  build() {
    Column() {
      if (this.layoutType === '简约现代') {
        this.drawModernLayout();
      } else if (this.layoutType === '豪华智能') {
        this.drawLuxuryLayout();
      } else if (this.layoutType === '舒适睡眠') {
        this.drawSleepLayout();
      }
      // ... 其他布局类型
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.bgColor)
    .borderRadius({ topLeft: 8, topRight: 8 })
    .padding(4)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  drawLight() {
    Column() {}
      .width(10)
      .height(14)
      .backgroundColor('#FFD700')
      .borderRadius(1)
      .borderWidth(1)
      .borderColor('#FFA500')
  }

  @Builder
  drawSwitch() {
    Column() {}
      .width(8)
      .height(8)
      .backgroundColor('#666666')
      .borderRadius(1)
  }

  @Builder
  drawCamera() {
    Column() {}
      .width(10)
      .height(10)
      .backgroundColor('#9E9E9E')
      .borderRadius(5)
      .borderWidth(1)
      .borderColor('#666666')
  }

  // ... 其他绘制方法
}

绘制元素说明

元素颜色尺寸说明
drawLight()#FFD700 金色10×14智能照明
drawSwitch()#666666 灰色8×8智能开关
drawCamera()#9E9E9E 银色10×10安防摄像头
drawSpeaker()#333333 黑色12×10智能音箱
drawCurtain()#C0C0C0 银色8×16智能窗帘
drawAC()#42A5F5 蓝色14×8智能空调
drawDoor()#8B4513 棕色6×18智能门锁
drawSensor()#FF5252 红色6×6传感器
drawRobot()#4DB6AC 青色10×8扫地机器人
drawTV()#1A1A1A 黑色18×10智能电视

场景绘制示例

以"豪华智能"场景为例,绘制了一个完整的客厅智能布局:

@Builder
drawLuxuryLayout() {
  Column({ space: 2 }) {
    Row({ space: 2 }) {
      this.drawLight();
      this.drawLight();
      this.drawTV();
      this.drawLight();
      this.drawLight();
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)

    Row({ space: 2 }) {
      this.drawCamera();
      this.drawSpeaker();
      this.drawAC();
      this.drawSpeaker();
      this.drawCamera();
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)

    Row({ space: 2 }) {
      this.drawCurtain();
      this.drawSwitch();
      this.drawRobot();
      this.drawSwitch();
      this.drawCurtain();
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}

5.2 DeviceItem(设备列表项组件)

设备列表项组件展示单个设备的详细信息和操作按钮:

import { Device } from '../models/Device';

@Component
export struct DeviceItem {
  @State device: Device = new Device('', '', '', '', '', 0);
  private onToggleFunc: (id: string) => void = () => {};
  private onPriceEditFunc: (device: Device) => void = () => {};
  private onQuantityChangeFunc: (id: string, quantity: number) => void = () => {};

  build() {
    Row({ space: 0 }) {
      Column() {}
        .width(this.device.selected ? 3 : 0)
        .height('100%')
        .backgroundColor('#FF6B35')
        .borderRadius({ topLeft: 6, bottomLeft: 6 })

      Row({ space: 10 }) {
        Checkbox()
          .select(this.device.selected)
          .onChange((value: boolean) => {
            this.onToggleFunc(this.device.id);
          })

        Column({ space: 3 }) {
          Text(this.device.name)
            .fontSize(14)
            .fontWeight(FontWeight.Medium)
          Text(`${this.device.specification} / ${this.device.unit}`)
            .fontSize(12)
            .fontColor('#999999')
        }
        .flexGrow(1)

        Text(`¥${this.device.customPrice}`)
          .fontSize(14)
          .fontColor('#E53935')
          .fontWeight(FontWeight.Medium)
          .onClick(() => {
            this.onPriceEditFunc(this.device);
          })

        Row({ space: 6 }) {
          Button('-')
            .width(28)
            .height(28)
            .fontSize(14)
            .backgroundColor('#F5F5F5')
            .fontColor('#333333')
            .borderRadius(14)
            .onClick(() => {
              if (this.device.quantity > 1) {
                this.onQuantityChangeFunc(this.device.id, this.device.quantity - 1);
              }
            })

          Text(`${this.device.quantity}`)
            .fontSize(14)
            .width(26)
            .textAlign(TextAlign.Center)

          Button('+')
            .width(28)
            .height(28)
            .fontSize(14)
            .backgroundColor('#FF6B35')
            .fontColor('#FFFFFF')
            .borderRadius(14)
            .onClick(() => {
              this.onQuantityChangeFunc(this.device.id, this.device.quantity + 1);
            })
        }
      }
      .padding({ left: 12, right: 12, top: 10, bottom: 10 })
    }
    .backgroundColor(this.device.selected ? '#FFF8F5' : '#FFFFFF')
    .borderRadius(6)
    .borderWidth(1)
    .borderColor('#F0F0F0')
  }
}

组件特性

  1. 选中状态指示:左侧红色竖条表示选中状态
  2. 复选框交互:点击复选框切换选中状态
  3. 价格可编辑:点击价格文字弹出编辑对话框
  4. 数量增减:支持增减按钮调整设备数量

5.3 DevicePriceEditDialog(设备价格编辑对话框)

自定义对话框组件,用于修改设备价格:

import { Device } from '../models/Device';

@CustomDialog
export struct DevicePriceEditDialog {
  controller: CustomDialogController;
  device: Device = new Device('', '', '', '', '', 0);
  onConfirm: (id: string, price: number) => void = () => {};
  onCancel: () => void = () => {};

  @State inputPrice: string = '';

  build() {
    Column({ space: 12 }) {
      Text('修改价格')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')

      Text(`当前价格:¥${this.device.customPrice}`)
        .fontSize(14)
        .fontColor('#666666')

      TextInput({ placeholder: '请输入新价格' })
        .width('100%')
        .height(44)
        .fontSize(14)
        .backgroundColor('#F5F5F5')
        .borderRadius(6)
        .type(InputType.Number)
        .onChange((value: string) => {
          this.inputPrice = value;
        })
        .onFocus(() => {
          this.inputPrice = `${this.device.customPrice}`;
        })

      Row({ space: 12 }) {
        Button('取消')
          .flexGrow(1)
          .height(44)
          .fontSize(14)
          .backgroundColor('#F5F5F5')
          .fontColor('#666666')
          .borderRadius(22)
          .onClick(() => {
            this.onCancel();
            this.controller.close();
          })

        Button('确认')
          .flexGrow(1)
          .height(44)
          .fontSize(14)
          .backgroundColor('#FF6B35')
          .fontColor('#FFFFFF')
          .borderRadius(22)
          .onClick(() => {
            let price = parseFloat(this.inputPrice);
            if (!isNaN(price) && price >= 0) {
              this.onConfirm(this.device.id, price);
              this.controller.close();
            }
          })
      }
    }
    .padding(20)
    .width(260)
    .backgroundColor('#FFFFFF')
    .borderRadius(10)
  }
}

对话框特性

  1. 输入聚焦时自动填充当前价格
  2. 仅接受数字输入InputType.Number
  3. 价格验证:确保输入有效且非负

5.4 PriceCategory(价格分类组件)

价格分类组件按分类展示选中设备的价格明细:

import { Device } from '../models/Device';

@Component
export struct PriceCategory {
  @State categoryName: string = '';
  @State devices: Array<Device> = [];
  @State subtotal: number = 0;

  aboutToAppear(): void {
    this.calculateSubtotal();
  }

  aboutToReappear(): void {
    this.calculateSubtotal();
  }

  private calculateSubtotal(): void {
    this.subtotal = 0;
    for (let i = 0; i < this.devices.length; i++) {
      this.subtotal += this.devices[i].customPrice * this.devices[i].quantity;
    }
  }

  build() {
    Column({ space: 6 }) {
      if (this.devices.length === 0) {
        Column() {}
          .height(0)
      } else {
        Text(`${this.categoryName}`)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .padding({ left: 12, top: 6 })

        Column() {
          ForEach(this.devices, (device: Device) => {
            Row({ space: 10 }) {
              Column({ space: 3 }) {
                Text(device.name)
                  .fontSize(14)
                  .fontColor('#333333')
                Text(`${device.specification} / ${device.unit}`)
                  .fontSize(12)
                  .fontColor('#999999')
              }
              .flexGrow(1)

              Text(`¥${device.customPrice}`)
                .fontSize(14)
                .fontColor('#666666')

              Text(`×${device.quantity}`)
                .fontSize(14)
                .fontColor('#666666')

              Text(`¥${(device.customPrice * device.quantity).toFixed(2)}`)
                .fontSize(14)
                .fontColor('#E53935')
                .fontWeight(FontWeight.Medium)
            }
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          })
        }
        .backgroundColor('#FFFFFF')
        .borderRadius(6)

        Row() {
          Text('小计:')
            .fontSize(14)
            .fontColor('#666666')
          Text(`¥${this.subtotal.toFixed(2)}`)
            .fontSize(14)
            .fontColor('#E53935')
            .fontWeight(FontWeight.Medium)
        }
        .justifyContent(FlexAlign.End)
        .padding({ right: 12, bottom: 6 })
      }
    }
  }
}

组件特性

  1. 动态显示:无选中设备时隐藏该分类
  2. 小计计算:自动计算该分类下设备的总价
  3. 格式化显示:价格保留两位小数

六、页面实现

6.1 Index(主页面)

主页面是应用的入口,使用Tabs组件实现四个功能模块的导航:

import { SceneSelectPage } from './SceneSelectPage';
import { DeviceSelectPage } from './DeviceSelectPage';
import { PriceCalculatePage } from './PriceCalculatePage';
import { RecordHistoryPage } from './RecordHistoryPage';

@Entry
@Component
struct Index {
  @State currentIndex: number = 0;
  @State tabTitles: Array<string> = ['场景', '设备', '测算', '记录'];
  @State tabIcons: Array<string> = ['🏠', '🔧', '💰', '📋'];

  build() {
    Column() {
      Text('智能家居方案管控')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .padding({ top: 28, bottom: 8 })
        .width('100%')
        .textAlign(TextAlign.Center)

      Tabs({ barPosition: BarPosition.End }) {
        TabContent() {
          SceneSelectPage()
        }
        .tabBar(this.buildTabBar(0))

        TabContent() {
          DeviceSelectPage()
        }
        .tabBar(this.buildTabBar(1))

        TabContent() {
          PriceCalculatePage()
        }
        .tabBar(this.buildTabBar(2))

        TabContent() {
          RecordHistoryPage()
        }
        .tabBar(this.buildTabBar(3))
      }
      .flexGrow(1)
      .backgroundColor('#F5F5F5')
      .onChange((index: number) => {
        this.currentIndex = index;
      })
    }
    .height('100%')
    .backgroundColor('#F5F5F5')
    .padding({ bottom: 54 })
  }

  @Builder
  buildTabBar(index: number) {
    Column({ space: 4 }) {
      Text(this.tabIcons[index])
        .fontSize(20)
        .fontColor(this.currentIndex === index ? '#FF6B35' : '#999999')

      Text(this.tabTitles[index])
        .fontSize(12)
        .fontColor(this.currentIndex === index ? '#FF6B35' : '#999999')

      if (this.currentIndex === index) {
        Column() {}
          .width(20)
          .height(3)
          .backgroundColor('#FF6B35')
          .borderRadius(2)
      }
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
  }
}

Tab导航设计

Tab图标页面功能
场景🏠SceneSelectPage选择智能场景方案
设备🔧DeviceSelectPage选择智能设备
测算💰PriceCalculatePage价格测算与方案保存
记录📋RecordHistoryPage查看历史方案记录

6.2 SceneSelectPage(场景选择页面)

场景选择页面允许用户按面积筛选并选择场景:

import { SceneCard } from '../components/SceneCard';
import { Scene } from '../models/Scene';
import { sceneManager } from '../managers/SceneManager';

@Component
export struct SceneSelectPage {
  @State selectedArea: number = 0;
  @State selectedScene: Scene | null = null;
  @State scenes: Array<Scene> = [];
  @State areas: Array<number> = [];

  build() {
    Column({ space: 16 }) {
      Column({ space: 8 }) {
        Text('智能家居场景选择')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .width('100%')

        Row({ space: 8 }) {
          Row() {
            Column() {}.width(4).height(16).backgroundColor('#FF6B35').borderRadius(2)
            Text('面积选择')
              .fontSize(14)
              .fontWeight(FontWeight.Medium)
              .fontColor('#333333')
          }
          .width('100%')
        }

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
          ForEach(this.areas, (area: number) => {
            Button(`${area}`)
              .width('22%')
              .height(40)
              .fontSize(12)
              .fontWeight(this.selectedArea === area ? FontWeight.Bold : FontWeight.Normal)
              .backgroundColor(this.selectedArea === area ? '#FF6B35' : '#FFFFFF')
              .fontColor(this.selectedArea === area ? '#FFFFFF' : '#666666')
              .borderRadius(8)
              .borderWidth(1)
              .borderColor('#E0E0E0')
              .onClick(() => {
                this.selectedArea = area;
                this.scenes = sceneManager.getScenesByArea(area);
                this.selectedScene = null;
              })
          })
        }
        .padding({ right: 4 })
      }

      Column({ space: 8 }) {
        Row() {
          Column() {}.width(4).height(16).backgroundColor('#4A90D9').borderRadius(2)
          Text('场景选择')
            .fontSize(14)
            .fontWeight(FontWeight.Medium)
            .fontColor('#333333')
        }

        Scroll() {
          Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
            ForEach(this.scenes, (scene: Scene) => {
              SceneCard({
                scene: scene,
                isSelected: this.selectedScene !== null && this.selectedScene.id === scene.id,
                onClickFunc: (s: Scene) => {
                  this.selectedScene = s;
                  sceneManager.selectScene(s);
                }
              })
            })
          }
          .padding({ left: 12, right: 12, bottom: 8 })
        }
        .flexGrow(1)
      }
    }
    .width('100%')
    .height('100%')
    .padding({ left: 16, right: 16, top: 8 })
    .backgroundColor('#F5F5F5')
    .justifyContent(FlexAlign.Start)
    .onAppear(() => {
      this.areas = sceneManager.getAllAreas();
      if (this.areas.length > 0) {
        this.selectedArea = this.areas[0];
        this.scenes = sceneManager.getScenesByArea(this.selectedArea);
      }
    })
  }
}

页面特性

  1. 面积筛选:点击面积按钮筛选对应面积的场景
  2. 场景卡片:双列布局展示场景卡片
  3. 选中状态:选中场景后卡片显示橙色边框和勾选标记

6.3 DeviceSelectPage(设备选择页面)

设备选择页面允许用户按分类浏览和选择设备:

import { DeviceItem } from '../components/DeviceItem';
import { DevicePriceEditDialog } from '../components/DevicePriceEditDialog';
import { Device } from '../models/Device';
import { deviceManager } from '../managers/DeviceManager';

@Component
export struct DeviceSelectPage {
  @State selectedCategory: string = '';
  @State categories: Array<string> = [];
  @State devices: Array<Device> = [];

  build() {
    Column({ space: 12 }) {
      Column({ space: 8 }) {
        Text('智能设备选择')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .width('100%')

        Row() {
          Column() {}.width(4).height(16).backgroundColor('#FF6B35').borderRadius(2)
          Text('设备分类')
            .fontSize(14)
            .fontWeight(FontWeight.Medium)
            .fontColor('#333333')
        }

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
          ForEach(this.categories, (category: string) => {
            Button(category)
              .width('22%')
              .height(36)
              .fontSize(11)
              .fontWeight(this.selectedCategory === category ? FontWeight.Bold : FontWeight.Normal)
              .backgroundColor(this.selectedCategory === category ? '#FF6B35' : '#FFFFFF')
              .fontColor(this.selectedCategory === category ? '#FFFFFF' : '#666666')
              .borderRadius(6)
              .borderWidth(1)
              .borderColor('#E0E0E0')
              .onClick(() => {
                this.selectedCategory = category;
                this.devices = deviceManager.getDevicesByCategory(category);
              })
          })
        }
      }

      Column({ space: 8 }) {
        Row() {
          Column() {}.width(4).height(16).backgroundColor('#4A90D9').borderRadius(2)
          Text('设备列表')
            .fontSize(14)
            .fontWeight(FontWeight.Medium)
            .fontColor('#333333')
        }

        Scroll() {
          Column({ space: 8 }) {
            ForEach(this.devices, (device: Device) => {
              DeviceItem({
                device: device,
                onToggleFunc: (id: string) => {
                  deviceManager.toggleDeviceSelection(id);
                },
                onQuantityChangeFunc: (id: string, quantity: number) => {
                  deviceManager.updateDeviceQuantity(id, quantity);
                },
                onPriceEditFunc: (d: Device) => {
                  this.openEditDialog(d);
                }
              })
            })
          }
          .padding({ bottom: 8 })
        }
        .flexGrow(1)
      }
    }
    .width('100%')
    .height('100%')
    .padding({ left: 16, right: 16, top: 8 })
    .backgroundColor('#F5F5F5')
    .justifyContent(FlexAlign.Start)
    .onAppear(() => {
      this.categories = deviceManager.getAllCategories();
      if (this.categories.length > 0) {
        this.selectedCategory = this.categories[0];
        this.devices = deviceManager.getDevicesByCategory(this.selectedCategory);
      }
    })
  }

  openEditDialog(device: Device): void {
    let onConfirm = (id: string, price: number) => {
      deviceManager.updateDevicePrice(id, price);
      this.devices = deviceManager.getDevicesByCategory(this.selectedCategory);
    };
    let onCancel = () => {};

    let controller = new CustomDialogController({
      builder: DevicePriceEditDialog({
        device: device,
        onConfirm: onConfirm,
        onCancel: onCancel
      }),
      alignment: DialogAlignment.Center,
      autoCancel: true
    });
    controller.open();
  }
}

页面特性

  1. 分类筛选:8个设备分类按钮,点击切换显示
  2. 设备列表:展示当前分类下的所有设备
  3. 交互操作:支持选中、修改价格、调整数量

6.4 PriceCalculatePage(价格测算页面)

价格测算页面展示已选场景和设备的价格明细:

import { Scene } from '../models/Scene';
import { sceneManager } from '../managers/SceneManager';
import { deviceManager } from '../managers/DeviceManager';
import { recordManager } from '../managers/RecordManager';
import { PriceCategory } from '../components/PriceCategory';

@Component
export struct PriceCalculatePage {
  @State selectedScene: Scene | null = null;
  @State categories: Array<string> = deviceManager.getAllCategories();
  @State totalPrice: number = 0;
  @State recordName: string = '';

  aboutToAppear(): void {
    this.selectedScene = sceneManager.getSelectedScene();
    this.totalPrice = deviceManager.getTotalPrice();
  }

  aboutToReappear(): void {
    this.selectedScene = sceneManager.getSelectedScene();
    this.totalPrice = deviceManager.getTotalPrice();
  }

  build() {
    Column({ space: 12 }) {
      Text('价格测算')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .padding({ left: 12, top: 12 })

      if (this.selectedScene !== null) {
        Column({ space: 6 }) {
          Text(`已选场景:${this.selectedScene.name}`)
            .fontSize(14)
            .fontColor('#333333')
            .fontWeight(FontWeight.Medium)
          Text(`${this.selectedScene.type} | ${this.selectedScene.area}`)
            .fontSize(12)
            .fontColor('#666666')
        }
        .backgroundColor('#FFFFFF')
        .padding({ left: 12, right: 12, top: 10, bottom: 10 })
        .borderRadius(6)
        .margin({ left: 12, right: 12 })
      } else {
        Text('请先选择智能场景')
          .fontSize(14)
          .fontColor('#999999')
          .backgroundColor('#FFFFFF')
          .padding({ left: 12, right: 12, top: 10, bottom: 10 })
          .borderRadius(6)
          .margin({ left: 12, right: 12 })
      }

      Row({ space: 6 }) {
        Column() {}
          .width(3)
          .height(16)
          .backgroundColor('#FF6B35')
          .borderRadius(2)
        Text('设备明细')
          .fontSize(14)
          .fontColor('#666666')
      }
      .padding({ left: 12 })

      Scroll() {
        Column({ space: 12 }) {
          ForEach(this.categories, (category: string) => {
            PriceCategory({
              categoryName: category,
              devices: deviceManager.getSelectedDevices()
            })
          })
        }
        .padding({ left: 12, right: 12, bottom: 12 })
      }
      .flexGrow(1)

      Column({ space: 12 }) {
        Column({ space: 6 }) {
          Text('方案总价')
            .fontSize(14)
            .fontColor('#666666')
          Text(`¥${this.totalPrice.toFixed(2)}`)
            .fontSize(28)
            .fontColor('#E53935')
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 12 })

        TextInput({ placeholder: '请输入方案名称' })
          .width('90%')
          .height(40)
          .fontSize(14)
          .backgroundColor('#F5F5F5')
          .borderRadius(6)
          .onChange((value: string) => {
            this.recordName = value;
          })

        Button('保存方案')
          .width('90%')
          .height(44)
          .fontSize(16)
          .backgroundColor('#FF6B35')
          .fontColor('#FFFFFF')
          .borderRadius(22)
          .onClick(() => {
            this.handleSaveRecord();
          })
      }
      .padding({ bottom: 12 })
      .backgroundColor('#FFFFFF')
      .shadow({ radius: 8, color: '#00000015', offsetY: -4 })
    }
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  private handleSaveRecord(): void {
    if (this.recordName.trim() === '') {
      this.showToast('请输入方案名称');
      return;
    }
    if (this.selectedScene === null) {
      this.showToast('请先选择智能场景');
      return;
    }
    recordManager.saveRecord(this.recordName);
    this.showToast('方案保存成功');
    this.recordName = '';
  }

  private showToast(message: string): void {
    AlertDialog.show({
      message: message,
      autoCancel: true
    });
  }
}

页面特性

  1. 场景信息展示:显示已选场景的名称、类型和面积
  2. 设备明细:按分类展示选中设备的价格明细
  3. 总价计算:实时计算并显示方案总价
  4. 方案保存:输入名称后保存方案到本地

6.5 RecordHistoryPage(历史记录页面)

历史记录页面展示用户保存的所有方案:

import { Record } from '../models/Record';
import { recordManager } from '../managers/RecordManager';
import { RecordItem } from '../components/RecordItem';

@Component
export struct RecordHistoryPage {
  @State records: Array<Record> = recordManager.loadRecords();

  aboutToAppear(): void {
    this.records = recordManager.loadRecords();
  }

  build() {
    Column({ space: 12 }) {
      Text('历史记录')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .padding({ left: 12, top: 12 })

      if (this.records.length === 0) {
        Column({ space: 12 }) {
          Image($r('app.media.background'))
            .width(120)
            .height(120)
            .opacity(0.5)
          Text('暂无历史记录')
            .fontSize(16)
            .fontColor('#999999')
          Text('完成价格测算后可保存方案')
            .fontSize(14)
            .fontColor('#CCCCCC')
        }
        .flexGrow(1)
        .width('100%')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
      } else {
        Scroll() {
          Column() {
            ForEach(this.records, (record: Record) => {
              this.buildRecordItem(record)
            })
          }
          .padding({ left: 12, right: 12, bottom: 12 })
        }
        .flexGrow(1)
      }
    }
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  buildRecordItem(record: Record) {
    RecordItem({
      record: record,
      onViewDetailFunc: (rec: Record) => {
        this.showDetailDialog(rec);
      },
      onDeleteFunc: (id: string) => {
        recordManager.deleteRecord(id);
        this.records = recordManager.loadRecords();
        this.showToast('记录已删除');
      }
    })
  }

  private showDetailDialog(record: Record): void {
    let detailText: string = `名称:${record.name}\n场景:${record.sceneName}\n类型:${record.sceneType}\n面积:${record.sceneArea}㎡\n总价:¥${record.totalPrice.toFixed(2)}\n时间:${record.createTime}`;
    AlertDialog.show({
      title: '方案详情',
      message: detailText,
      autoCancel: true
    });
  }

  private showToast(message: string): void {
    AlertDialog.show({
      message: message,
      autoCancel: true
    });
  }
}

页面特性

  1. 空状态处理:无记录时显示提示信息
  2. 记录列表:展示所有保存的方案记录
  3. 详情查看:点击查看方案详细信息
  4. 记录删除:支持删除不需要的方案

七、数据持久化实现

7.1 StorageService(存储服务)

存储服务使用Preferences实现数据的本地持久化:

import preferences from '@ohos.data.preferences';
import { Record } from '../models/Record';
import { Selection } from '../models/Selection';
import { Device } from '../models/Device';

class ParseRecord {
  id: string = '';
  name: string = '';
  sceneId: string = '';
  sceneName: string = '';
  sceneArea: number = 0;
  sceneType: string = '';
  devices: Array<ParseDevice> = [];
  totalPrice: number = 0;
  createTime: string = '';
}

class ParseDevice {
  id: string = '';
  name: string = '';
  category: string = '';
  specification: string = '';
  unit: string = '';
  marketPrice: number = 0;
  customPrice: number = 0;
  selected: boolean = false;
  quantity: number = 0;
}

class ParseSelection {
  sceneId: string = '';
  sceneName: string = '';
  deviceIds: Array<string> = [];
  deviceQuantityPairs: Array<ParseQuantityPair> = [];
}

class ParseQuantityPair {
  deviceId: string = '';
  quantity: number = 0;
}

export class StorageService {
  private static RECORDS_KEY: string = 'smarthome_records';
  private static SELECTION_KEY: string = 'smarthome_selection';

  async saveRecords(records: Array<Record>): Promise<void> {
    let recordsJson: string = JSON.stringify(records);
    let context = getContext(this) as Context;
    try {
      let pref = await preferences.getPreferences(context, 'smarthome_storage');
      await pref.put(StorageService.RECORDS_KEY, recordsJson);
      await pref.flush();
    } catch (err) {
      console.error('saveRecords error:', err);
    }
  }

  async loadRecords(): Promise<Array<Record>> {
    let context = getContext(this) as Context;
    try {
      let pref = await preferences.getPreferences(context, 'smarthome_storage');
      let recordsJson: string = await pref.get(StorageService.RECORDS_KEY, '') as string;

      if (recordsJson !== '') {
        return this.parseRecords(recordsJson);
      }
    } catch (err) {
      console.error('loadRecords error:', err);
    }
    return [];
  }

  private parseRecords(jsonStr: string): Array<Record> {
    let records: Array<Record> = [];
    let data: Array<ParseRecord> = JSON.parse(jsonStr) as Array<ParseRecord>;
    for (let i = 0; i < data.length; i++) {
      let recordData = data[i];
      let deviceArray: Array<Device> = [];
      for (let j = 0; j < recordData.devices.length; j++) {
        let devData = recordData.devices[j];
        let device = new Device(
          devData.id,
          devData.name,
          devData.category,
          devData.specification,
          devData.unit,
          devData.marketPrice
        );
        device.customPrice = devData.customPrice;
        device.selected = devData.selected;
        device.quantity = devData.quantity;
        deviceArray.push(device);
      }
      records.push(new Record(
        recordData.id,
        recordData.name,
        recordData.sceneId,
        recordData.sceneName,
        recordData.sceneArea,
        recordData.sceneType,
        deviceArray,
        recordData.totalPrice,
        recordData.createTime
      ));
    }
    return records;
  }
}

export const storageService: StorageService = new StorageService();

存储设计要点

  1. Parse类设计:创建ParseRecord、ParseDevice等类用于JSON反序列化,避免直接使用any类型
  2. 异步操作:保存和加载操作使用async/await异步执行
  3. 错误处理:所有异步操作都有try-catch错误处理
  4. 数据恢复:加载时重新创建Device对象,确保类型正确

八、开发过程中的挑战与解决方案

8.1 Tab切换闪退问题

问题:使用TabsController时出现TypeError

解决方案:移除TabsController,使用Tabs的onChange回调同步currentIndex

// 修复前
Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) { ... }
.onClick(() => { this.tabsController.changeIndex(index); })

// 修复后
Tabs({ barPosition: BarPosition.End }) { ... }
.onChange((index: number) => { this.currentIndex = index; })

8.2 自定义对话框初始化错误

问题:@CustomDialog组件使用new初始化导致崩溃

解决方案:使用@CustomDialog装饰器,通过CustomDialogController的builder属性初始化

// 修复前
@Component
export struct PriceEditDialog {
  private controller: CustomDialogController = new CustomDialogController({ ... })
}

// 修复后
@CustomDialog
export struct DevicePriceEditDialog {
  controller: CustomDialogController;
  device: Device = new Device('', '', '', '', '', 0);
}

8.3 布局适配问题

问题:Tab导航栏被系统底部手势导航栏遮挡

解决方案:在根Column添加底部padding适配安全区域

Column() {
  // ...
}
.height('100%')
.padding({ bottom: 54 })  // 适配系统底部导航栏

8.4 类型转换问题

问题:JSON反序列化后类型丢失

解决方案:创建Parse类用于中间转换,手动重建对象实例

private parseRecords(jsonStr: string): Array<Record> {
  let records: Array<Record> = [];
  let data: Array<ParseRecord> = JSON.parse(jsonStr) as Array<ParseRecord>;
  // 手动重建Record和Device对象
  for (let i = 0; i < data.length; i++) {
    // ... 创建新对象并赋值
  }
  return records;
}

九、项目总结与展望

9.1 项目成果

本应用成功实现了智能家居方案管控的核心功能:

  1. 场景选择:10种预设场景方案,支持按面积筛选
  2. 设备配置:8大类16种智能设备,支持选中、价格修改、数量调整
  3. 价格测算:实时计算方案总价,支持自定义价格
  4. 历史记录:本地保存方案记录,支持查看和删除

9.2 技术收获

通过开发本应用,深入掌握了以下技术:

  1. ArkTS语法:类型安全、装饰器使用、@State状态管理
  2. HarmonyOS组件:Tabs、Scroll、Flex、ForEach、CustomDialog等
  3. 数据持久化:Preferences的使用和JSON序列化
  4. 组件化开发:可复用组件的设计和封装

9.3 改进方向

未来可以从以下方面进行改进:

  1. 性能优化:使用@Observed/@ObjectLink优化状态管理,减少不必要的渲染
  2. 功能扩展:添加设备对比、方案分享、智能推荐等功能
  3. 用户体验:添加动画效果、手势操作、深色模式支持
  4. 数据安全:添加数据加密、备份恢复功能
Logo

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

更多推荐