HarmonyOS 录房源表单设计与实现实践

在房地产行业应用中,录房源功能是核心业务流程之一,涉及复杂的表单交互、数据校验、组件复用等技术挑战。本文基于真实的鸿蒙房产经纪平台项目,深入解析如何构建高效、易用、可维护的录房源表单系统。

一、业务场景与技术挑战

业务背景

房产经纪人需要快速录入房源信息,包括基础信息、物业地址、业主信息、客户跟进等多个维度,字段多达50+个,且存在复杂的业务逻辑和数据关联。在实际业务中,录房源不仅仅是简单的表单填写,还涉及以下复杂场景:

  1. 多角色权限控制:不同级别的经纪人有不同的录入权限
  2. 数据关联性强:楼盘选择后需自动填充区域、商圈等信息
  3. 业务规则复杂:出售和出租模式下的字段差异
  4. 数据校验严格:价格合理性、面积逻辑性等业务校验

核心挑战

技术层面挑战:

  1. 复杂表单管理:多模块、多字段的统一管理和状态同步
  2. 组件复用:减少重复代码,提高开发效率
  3. 用户体验:流畅的交互、清晰的视觉层次、智能的输入提示
  4. 数据校验:实时校验、错误提示、必填项管理
  5. 业务逻辑:出售/出租切换、权限控制、数据联动

业务层面挑战:

  1. 录入效率:经纪人希望快速录入,减少重复操作
  2. 数据准确性:避免录入错误导致的业务问题
  3. 跨平台一致性:与Web端、小程序端保持数据结构一致
  4. 离线支持:网络不稳定时的数据暂存

二、整体架构设计

1. 模块化组件架构

在设计录房源表单时,我们采用模块化的组件架构,将复杂的表单拆分为多个独立且可复用的功能模块。这种设计不仅提高了代码的可维护性,还使得各模块可以独立开发和测试。

┌─────────────────────────────────────────┐
│            AddHousePage (主页面)         │
├─────────────────────────────────────────┤
│  ├─ 交易类型选择 (TradeType)             │
│  ├─ 房源用途模块 (HouseUseModule)        │
│  ├─ 物业地址模块 (PropertyAddress)       │
│  ├─ 基础信息模块 (EHBaseInfo)           │
│  ├─ 客户跟进模块 (EHCustomFollow)       │
│  ├─ 业主信息模块 (EHOwnerInfo)          │
│  └─ 维护人模块 (DefenderModule)         │
├─────────────────────────────────────────┤
│          通用组件 (Common Components)    │
│  ├─ 编辑项 (EditItem)                   │
│  ├─ 选择项 (SelectItem)                 │
│  ├─ 单选项 (RadioItem)                  │
│  ├─ 标签列表 (TagList)                  │
│  └─ 标题栏 (TitleBar)                   │
└─────────────────────────────────────────┘

架构设计原则:

  • 单一职责:每个模块只负责特定的业务功能
  • 松耦合:模块间通过标准化的数据接口通信
  • 高内聚:相关的UI和逻辑封装在同一模块内
  • 可复用:通用组件可在其他页面复用

2. 数据模型设计

数据模型是整个表单系统的核心,我们设计了完整的房源数据模型来支撑复杂的业务需求。这个模型不仅要满足当前的录入需求,还要考虑未来的扩展性和与后端API的兼容性。

HouseBean.ts - 房源数据模型

这个数据模型经过精心设计,包含了房源的所有核心信息。每个字段都有明确的业务含义,并且支持可选和必填的灵活配置。

export class HouseBean {
  // 基础信息 - 房源的核心标识信息
  PropertyID?: string          // 房源ID,系统唯一标识
  Title?: string              // 房源标题,用于搜索和展示
  BuildingName?: string       // 楼盘名称,关联楼盘基础信息
  Price?: string             // 价格,支持出售价和租金
  MJ?: string               // 面积,建筑面积,单位平方米
  
  // 位置信息 - 房源的地理位置相关
  District?: number          // 区域ID,关联区域字典
  DistrictName?: string     // 区域名称,用于显示
  ShangQuanID?: number      // 商圈ID,关联商圈字典
  ShangQuanName?: string    // 商圈名称,影响房源价值
  Address?: string          // 详细地址,精确到门牌号
  
  // 房屋信息 - 房源的物理属性
  Floor?: number            // 楼层,当前房源所在楼层
  SumFloor?: number         // 总楼层,建筑物总层数
  Orientation?: number      // 朝向ID,关联朝向字典
  OrientationName?: string  // 朝向名称,如南北、东西等
  Decorate?: number         // 装修ID,关联装修字典
  DecorateName?: string     // 装修名称,如精装、简装等
  
  // 房型信息 - 房源的结构布局
  CountF?: number = 0       // 室,卧室数量
  CountT?: number = 0       // 厅,客厅数量
  CountW?: number = 0       // 卫,卫生间数量
  CountY?: number = 0       // 阳台,阳台数量
  
  // 业务信息 - 房源的业务状态
  Status?: number           // 状态,如待售、已售等
  WHEmpName?: string        // 维护人,负责此房源的经纪人
  ListedTime?: string       // 录入时间,首次录入的时间戳
  
  constructor() {
    // 初始化默认值,确保基本字段有合理的初始状态
    this.Floor = 1
    this.SumFloor = 1
    this.CountF = 1
    this.CountT = 1
    this.CountW = 1
  }
}

数据模型的设计考虑:

  1. 业务完整性:覆盖房源的所有关键信息
  2. 扩展性:预留扩展字段,支持未来业务需求
  3. 类型安全:使用TypeScript确保类型安全
  4. 默认值处理:合理的默认值减少用户录入负担

三、核心页面实现

1. 主页面架构

主页面是整个录房源功能的入口和容器,负责协调各个子模块的交互,管理全局状态,处理数据提交等核心逻辑。在设计时,我们特别注重用户体验和性能优化。

AddHousePage.ets - 录房源主页面

主页面采用滚动布局,将不同的功能模块垂直排列。这种布局方式符合用户的操作习惯,同时便于在小屏设备上使用。页面还集成了智能校验、自动保存、错误提示等增强功能。

@Entry
@Component
export struct AddHousePage {
  // 核心数据状态
  @State houseData: HouseBean = new HouseBean()  // 房源数据主体
  @State trade: TradeType = TradeType.SALE       // 交易类型:出售/出租
  @State isSubmitting: boolean = false           // 提交状态,防止重复提交
  @State validationErrors: Map<string, string> = new Map()  // 校验错误信息

  build() {
    Column() {
      // 顶部标题栏:提供导航和主要操作按钮
      TitleBar({
        title: "录房源",
        backShow: true,
        backText: "取消",
        rightText: "保存",
        rightCallBack: () => this.submitHouseData()
      })

      // 主体内容:滚动容器承载所有表单模块
      Scroll() {
        Column({ space: 12 }) {
          // 交易类型选择:影响后续字段的显示和校验
          this.buildTradeTypeSelector()
          
          // 各功能模块:按业务逻辑顺序排列
          HouseUseModule({ houseData: $houseData })
          PropertyAddress({ houseData: $houseData })
          EHBaseInfo({ 
            houseData: $houseData,
            tradeType: this.trade,
            validationErrors: this.validationErrors
          })
          EHCustomFollow({ houseData: $houseData })
          EHOwnerInfo({ houseData: $houseData })
          this.buildDefenderModule()
        }
        .padding({ left: 12, right: 12, top: 12 })
      }
      .scrollBar(BarState.Off)        // 隐藏滚动条,保持界面简洁
      .edgeEffect(EdgeEffect.Spring)   // 边缘弹性效果,提升用户体验
    }
    .backgroundColor($r("app.color.bg_color"))
  }

  /**
   * 交易类型选择器
   * 
   * 这是录房源的第一步,用户需要选择是出售还是出租。
   * 不同的交易类型会影响后续字段的显示、校验规则和业务逻辑。
   * 采用卡片式设计,视觉效果清晰,用户操作简单。
   */
  @Builder
  buildTradeTypeSelector() {
    Row({ space: 9 }) {
      this.buildTradeTypeItem(TradeType.SALE, "出售")
      this.buildTradeTypeItem(TradeType.RENT, "出租")
    }
    .width("100%")
    .justifyContent(FlexAlign.SpaceBetween)
  }

  @Builder
  buildTradeTypeItem(tradeType: TradeType, title: string) {
    Stack() {
      // 背景图片:使用不同的背景图区分选中和未选中状态
      Image(this.getTradeTypeImage(tradeType))
        .height(88)
        .objectFit(ImageFit.Fill)
      
      // 内容层:显示交易类型名称和统计信息
      Column() {
        Text(title)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.trade === tradeType ? 
            $r("app.color.main_color") : 
            $r("app.color.text_secondary"))
        
        // 显示当前类型下的房源数量统计
        Text(`${this.getHouseCount(tradeType)}/100`)
          .fontSize(14)
          .fontColor($r("app.color.text_secondary"))
          .margin({ top: 4 })
      }
      .padding({ left: 12, top: 12 })
    }
    .alignContent(Alignment.TopStart)
    .layoutWeight(1)
    .onClick(() => {
      this.trade = tradeType
      this.onTradeTypeChanged()
    })
  }

  /**
   * 交易类型切换处理
   * 
   * 当用户切换交易类型时,需要清理相关的字段数据,
   * 重置校验状态,并通知相关组件更新显示。
   * 这确保了数据的一致性和用户体验的连贯性。
   */
  private onTradeTypeChanged() {
    // 清除特定于交易类型的字段
    this.houseData.RentType = undefined
    this.validationErrors.clear()
    
    // 触发相关组件更新
    ToastUtil.showToast(`已切换到${this.trade === TradeType.SALE ? '出售' : '出租'}模式`)
  }

  /**
   * 提交房源数据
   * 
   * 这是整个录房源流程的最后一步,需要进行完整的数据校验,
   * 构造符合API要求的数据格式,处理网络请求和错误情况。
   * 特别注意防重复提交和用户体验优化。
   */
  private async submitHouseData() {
    if (this.isSubmitting) return
    
    try {
      // 数据校验:确保所有必填项都已填写且格式正确
      if (!this.validateHouseData()) {
        ToastUtil.showToast("请检查必填项")
        return
      }

      this.isSubmitting = true
      
      // 构造提交数据:转换为API期望的格式
      const submitData = this.buildSubmitData()
      
      // 调用API:发送数据到服务器
      const response = await HouseApiService.addHouse(submitData)
      
      if (response.success) {
        ToastUtil.showToast("房源录入成功")
        router.back()
      } else {
        ToastUtil.showToast(response.message || "录入失败")
      }
      
    } catch (error) {
      LogUtil.error("录入房源失败", error)
      ToastUtil.showToast("网络异常,请重试")
    } finally {
      this.isSubmitting = false
    }
  }

  /**
   * 数据校验
   * 
   * 校验是确保数据质量的关键环节。我们实现了多层次的校验:
   * 1. 必填项校验:确保关键信息不为空
   * 2. 格式校验:确保数据格式正确(如数字、日期等)
   * 3. 业务校验:确保数据符合业务逻辑(如楼层不能超过总楼层)
   * 4. 关联校验:确保相关字段的数据一致性
   */
  private validateHouseData(): boolean {
    this.validationErrors.clear()
    
    const requiredFields = [
      { field: 'BuildingName', message: '请选择楼盘' },
      { field: 'Price', message: '请输入价格' },
      { field: 'MJ', message: '请输入面积' },
      { field: 'Floor', message: '请输入楼层' },
      { field: 'SumFloor', message: '请输入总楼层' }
    ]
    
    let isValid = true
    requiredFields.forEach(({ field, message }) => {
      const value = this.houseData[field as keyof HouseBean]
      if (!value || (typeof value === 'string' && value.trim() === '')) {
        this.validationErrors.set(field, message)
        isValid = false
      }
    })
    
    return isValid
  }
}

主页面设计要点:

  1. 状态管理:使用@State统一管理页面状态
  2. 模块协调:通过数据绑定实现模块间通信
  3. 用户体验:提交防重、错误提示、状态反馈
  4. 性能优化:合理的组件更新策略

四、通用组件设计

1. 可编辑表单项组件

可编辑表单项是录房源页面中使用频率最高的组件,用于处理各种文本和数字输入。这个组件需要支持多种输入类型、实时校验、错误提示等功能,同时保持一致的视觉风格。

EditItem.ets - 通用编辑组件

这个组件的设计考虑了房地产业务的特殊需求,比如价格输入需要支持"万"作为单位,面积输入需要支持小数点,楼层输入只能是整数等。组件内部集成了完整的校验逻辑和错误处理机制。

@Component
export struct EditItem {
  // 基础属性:定义组件的基本行为和外观
  @Prop keyText: string = ""              // 字段标签文本
  @Prop unit: string = ""                 // 单位文本,如"万"、"㎡"
  @Prop placeholder: string = "请输入"     // 占位符文本
  @Prop inputType: InputType = InputType.Normal  // 输入类型:文本、数字等
  @Prop isRequired: boolean = false       // 是否必填
  @Prop isShowLine: boolean = true        // 是否显示分割线
  @Prop maxLength: number = 100           // 最大输入长度
  
  // 数据绑定:与父组件的数据双向绑定
  @Link value: string                     // 输入值,支持双向绑定
  
  // 校验相关:支持实时校验和错误提示
  @Prop errorMessage: string = ""         // 错误提示信息
  @Prop onChanged?: (value: string) => void  // 值变化回调

  build() {
    Column() {
      Row() {
        // 标签区域:显示字段名称和必填标识
        Row() {
          Text(this.keyText)
            .fontSize(14)
            .fontColor($r("app.color.text_secondary"))
          
          // 必填标识:红色星号提醒用户
          if (this.isRequired) {
            Text("*")
              .fontSize(14)
              .fontColor($r("app.color.error"))
              .margin({ left: 3 })
          }
        }

        // 输入区域:文本输入框和单位显示
        Row() {
          TextInput({ 
            text: this.value, 
            placeholder: this.placeholder 
          })
            .layoutWeight(1)
            .fontSize(14)
            .padding({ top: 0, bottom: 0, right: 8 })
            .placeholderFont({ size: 14 })
            .placeholderColor($r("app.color.text_hint"))
            .fontColor($r("app.color.text_primary"))
            .type(this.inputType)
            .textAlign(TextAlign.End)
            .backgroundColor(Color.Transparent)
            .maxLength(this.maxLength)
            .onChange((value: string) => {
              this.value = value
              this.onChanged?.(value)
            })
            .border({
              width: this.errorMessage ? 1 : 0,
              color: $r("app.color.error")
            })
          
          // 单位显示:如"万"、"㎡"等
          if (this.unit) {
            Text(this.unit)
              .fontSize(14)
              .fontColor($r("app.color.text_primary"))
          }
        }
        .layoutWeight(1)
      }
      .width("100%")
      .padding({ top: 12, bottom: 12 })
      .justifyContent(FlexAlign.SpaceBetween)

      // 错误提示:校验失败时显示具体的错误信息
      if (this.errorMessage) {
        Text(this.errorMessage)
          .fontSize(12)
          .fontColor($r("app.color.error"))
          .width("100%")
          .textAlign(TextAlign.End)
          .margin({ top: 4 })
      }

      // 分割线:视觉上分隔不同的输入项
      if (this.isShowLine) {
        Divider()
          .height(0.5)
          .color($r("app.color.divider"))
      }
    }
  }
}

组件特性详解:

  1. 输入类型支持:文本、数字、电话等多种输入类型
  2. 实时校验:输入过程中的即时反馈
  3. 视觉反馈:错误状态的边框和提示文字
  4. 用户体验:右对齐输入、合理的占位符、清晰的必填标识

2. 选择器组件

选择器组件用于处理枚举类型的数据选择,如区域、朝向、装修等。这类数据通常来自后端字典接口,需要支持动态加载和搜索功能。

SelectItem.ets - 通用选择组件

选择器组件的设计重点是处理各种选择场景:单选、多选、级联选择等。组件内部集成了弹窗管理、数据加载、搜索过滤等功能,为用户提供便捷的选择体验。

@Component
export struct SelectItem {
  // 显示属性:控制组件的外观和行为
  @Prop keyText: string = ""              // 字段标签
  @Prop valueText: string = ""            // 当前选中的值文本
  @Prop placeholder: string = "请选择"     // 未选择时的提示文本
  @Prop isRequired: boolean = false       // 是否必填
  @Prop isShowArrow: boolean = true       // 是否显示右侧箭头
  @Prop isEnabled: boolean = true         // 是否可用(禁用状态)
  
  // 校验属性:错误状态处理
  @Prop errorMessage: string = ""         // 错误提示信息
  
  // 交互属性:点击事件处理
  @Prop onClicked?: () => void            // 点击回调函数

  build() {
    Column() {
      Row() {
        // 标签区域:字段名称和必填标识
        Row() {
          Text(this.keyText)
            .fontSize(14)
            .fontColor($r("app.color.text_secondary"))
          
          if (this.isRequired) {
            Text("*")
              .fontSize(14)
              .fontColor($r("app.color.error"))
              .margin({ left: 3 })
          }
        }

        // 选择区域:显示当前值或占位符,以及操作提示
        Row() {
          Text(this.valueText || this.placeholder)
            .fontSize(14)
            .fontColor(this.valueText ? 
              $r("app.color.text_primary") : 
              $r("app.color.text_hint"))
            .layoutWeight(1)
            .textAlign(TextAlign.End)
          
          // 右侧箭头:提示用户可以点击选择
          if (this.isShowArrow && this.isEnabled) {
            Image($r("app.media.icon_arrow_right"))
              .width(14)
              .height(14)
              .margin({ left: 8 })
          }
        }
        .layoutWeight(1)
      }
      .width("100%")
      .padding({ top: 12, bottom: 12 })
      .justifyContent(FlexAlign.SpaceBetween)
      .onClick(() => {
        if (this.isEnabled) {
          this.onClicked?.()
        }
      })
      .opacity(this.isEnabled ? 1 : 0.5)

      // 错误提示:校验失败时的提示信息
      if (this.errorMessage) {
        Text(this.errorMessage)
          .fontSize(12)
          .fontColor($r("app.color.error"))
          .width("100%")
          .textAlign(TextAlign.End)
          .margin({ top: 4 })
      }

      Divider()
        .height(0.5)
        .color($r("app.color.divider"))
    }
  }
}

选择器组件的核心功能:

  1. 状态显示:清晰区分已选择和未选择状态
  2. 禁用支持:支持禁用状态,防止不当操作
  3. 视觉提示:通过箭头和颜色提示用户可操作性
  4. 扩展性:支持各种选择器类型的扩展

3. 标签选择组件

标签选择组件主要用于房源特色、标签等多选场景。房地产业务中,房源往往有多个特色标签,如"学区房"、"地铁房"、"精装修"等,用户需要能够方便地进行多选操作。

TagList.ets - 多选标签组件

标签组件采用流式布局,能够自适应不同屏幕尺寸。组件支持最大选择数量限制、选择状态管理、动态添加删除等功能,为用户提供直观的多选体验。

@Component
export struct TagList {
  // 数据属性:标签数据和选择状态
  @Prop tags: TagItem[] = []                      // 可选标签列表
  @State selectedTags: Set<number> = new Set()    // 已选择的标签ID集合
  
  // 配置属性:行为控制
  @Prop maxSelection: number = 999                // 最大选择数量
  @Prop onSelectionChanged?: (selectedIds: number[]) => void  // 选择变化回调

  build() {
    // 流式布局:自动换行,适应不同的标签数量和长度
    Flex({ wrap: FlexWrap.Wrap, space: { main: 8, cross: 8 } }) {
      ForEach(this.tags, (tag: TagItem, index: number) => {
        this.buildTagItem(tag)
      })
    }
    .width("100%")
  }

  /**
   * 构建单个标签项
   * 
   * 每个标签都是一个可点击的胶囊状按钮,通过颜色和边框
   * 来区分选中和未选中状态。设计上遵循Material Design
   * 的标签设计规范,提供良好的视觉反馈。
   */
  @Builder
  buildTagItem(tag: TagItem) {
    Text(tag.text)
      .fontSize(14)
      .fontColor(this.selectedTags.has(tag.value) ? 
        Color.White : 
        $r("app.color.text_primary"))
      .backgroundColor(this.selectedTags.has(tag.value) ? 
        $r("app.color.main_color") : 
        $r("app.color.tag_background"))
      .padding({ left: 12, right: 12, top: 6, bottom: 6 })
      .borderRadius(16)
      .border({
        width: 1,
        color: this.selectedTags.has(tag.value) ? 
          $r("app.color.main_color") : 
          $r("app.color.border")
      })
      .onClick(() => {
        this.toggleTag(tag)
      })
  }

  /**
   * 切换标签选择状态
   * 
   * 处理标签的选择和取消选择逻辑,包括最大选择数量的限制。
   * 当达到最大选择数量时,会提示用户并阻止继续选择。
   */
  private toggleTag(tag: TagItem) {
    const newSelection = new Set(this.selectedTags)
    
    if (newSelection.has(tag.value)) {
      // 取消选择
      newSelection.delete(tag.value)
    } else {
      // 选择新标签
      if (newSelection.size >= this.maxSelection) {
        ToastUtil.showToast(`最多选择${this.maxSelection}个标签`)
        return
      }
      newSelection.add(tag.value)
    }
    
    this.selectedTags = newSelection
    this.onSelectionChanged?.(Array.from(newSelection))
  }
}

标签组件的设计亮点:

  1. 流式布局:自适应屏幕宽度,标签自动换行
  2. 状态管理:清晰的选中/未选中视觉区分
  3. 数量限制:防止用户选择过多标签
  4. 即时反馈:选择后立即更新状态和外观

五、高级功能实现

1. 智能表单填充

在房地产业务中,很多数据是相互关联的。比如选择了楼盘后,区域、商圈、均价等信息都可以自动填充。这种智能填充功能大大提高了录入效率,减少了用户的重复劳动。

智能填充的实现原理:

  • 数据关联分析:分析字段间的依赖关系
  • API集成:调用相关的数据查询接口
  • 用户体验优化:平滑的数据更新,避免突兀的变化
  • 错误处理:网络异常时的降级处理
export class FormAutoFill {
  /**
   * 根据楼盘信息自动填充相关字段
   * 
   * 当用户选择楼盘后,系统会自动查询该楼盘的基础信息,
   * 包括所在区域、商圈、均价等,并自动填充到相应字段。
   * 这个功能大大减少了用户的输入工作量,提高了数据准确性。
   */
  static async autoFillByBuilding(buildingId: string, houseData: HouseBean) {
    try {
      // 显示加载状态,提升用户体验
      ToastUtil.showToast("正在获取楼盘信息...")
      
      const buildingInfo = await HouseApiService.getBuildingInfo(buildingId)
      
      if (buildingInfo) {
        // 自动填充区域信息:减少用户选择步骤
        houseData.District = buildingInfo.districtId
        houseData.DistrictName = buildingInfo.districtName
        houseData.ShangQuanID = buildingInfo.shangQuanId
        houseData.ShangQuanName = buildingInfo.shangQuanName
        
        // 自动填充均价:为用户提供价格参考
        if (buildingInfo.averagePrice) {
          houseData.PriceAverage = buildingInfo.averagePrice.toString()
        }
        
        // 填充其他相关信息
        if (buildingInfo.propertyType) {
          houseData.PropertyType = buildingInfo.propertyType
        }
        
        ToastUtil.showToast("已自动填充相关信息")
      }
    } catch (error) {
      LogUtil.error("自动填充失败", error)
      // 失败时不影响用户继续操作
      ToastUtil.showToast("获取楼盘信息失败,请手动填写")
    }
  }

  /**
   * 根据户型智能推荐价格
   * 
   * 基于房源的面积、均价、户型等信息,智能计算推荐价格。
   * 这个功能帮助经纪人快速定价,提供市场参考。
   */
  static calculateSuggestedPrice(houseData: HouseBean): number {
    const { MJ, PriceAverage, CountF, CountT } = houseData
    
    if (!MJ || !PriceAverage) return 0
    
    const area = parseFloat(MJ)
    const avgPrice = parseFloat(PriceAverage)
    
    // 基础价格计算:面积 × 均价
    let basePrice = area * avgPrice / 10000 // 转换为万元
    
    // 户型加成:更多的房间通常价格更高
    const roomBonus = (CountF || 0) * 0.1 + (CountT || 0) * 0.05
    basePrice *= (1 + roomBonus)
    
    // 楼层加成:根据楼层调整价格
    const floorBonus = this.calculateFloorBonus(houseData.Floor, houseData.SumFloor)
    basePrice *= (1 + floorBonus)
    
    return Math.round(basePrice * 100) / 100 // 保留两位小数
  }

  /**
   * 计算楼层加成
   * 
   * 根据楼层位置计算价格加成,一般来说:
   * - 1楼:略微减少(潮湿、采光等问题)
   * - 中层:正常价格
   * - 高层:略微增加(视野好、安静)
   * - 顶楼:减少(夏热冬冷、漏水风险)
   */
  private static calculateFloorBonus(floor?: number, sumFloor?: number): number {
    if (!floor || !sumFloor) return 0
    
    const floorRatio = floor / sumFloor
    
    if (floor === 1) return -0.05        // 1楼减少5%
    if (floor === sumFloor) return -0.03  // 顶楼减少3%
    if (floorRatio > 0.7) return 0.02     // 高层增加2%
    if (floorRatio < 0.3) return -0.02    // 低层减少2%
    
    return 0 // 中层正常价格
  }

  /**
   * 智能补全地址信息
   * 
   * 根据用户输入的部分地址信息,调用地图API进行智能补全,
   * 提供地址建议,提高地址录入的准确性。
   */
  static async smartCompleteAddress(partialAddress: string): Promise<string[]> {
    try {
      if (partialAddress.length < 2) return []
      
      const suggestions = await MapApiService.searchAddress(partialAddress)
      return suggestions.map(item => item.address)
    } catch (error) {
      LogUtil.error("地址补全失败", error)
      return []
    }
  }
}

智能填充的业务价值:

  1. 提高效率:减少重复输入,提升录入速度
  2. 减少错误:自动填充的数据准确性更高
  3. 用户体验:降低操作复杂度,提升满意度
  4. 数据一致性:确保相关字段的数据关联正确

2. 表单数据缓存

在移动应用中,用户可能因为各种原因(电话、网络中断等)暂时离开应用。为了避免用户辛苦录入的数据丢失,我们实现了智能的表单数据缓存机制。

缓存机制的设计考虑:

  • 数据安全:敏感信息的加密存储
  • 存储策略:按用户和表单类型分别缓存
  • 过期管理:避免过期数据的混淆
  • 恢复提示:用户重新进入时的友好提示
export class FormCache {
  private static readonly CACHE_KEY = "house_form_cache"
  private static readonly CACHE_DURATION = 30 * 60 * 1000 // 30分钟缓存有效期

  /**
   * 保存表单数据到缓存
   * 
   * 在用户输入过程中定期保存数据,以及在关键节点
   * (如页面跳转、应用后台等)主动保存数据。
   * 缓存数据包含时间戳,用于判断数据的有效性。
   */
  static saveFormData(houseData: HouseBean) {
    try {
      const cacheData = {
        data: houseData,
        timestamp: Date.now(),
        version: "1.0",  // 版本号,用于兼容性处理
        userId: AgentUtil.userInfo().id  // 用户ID,避免数据混淆
      }
      
      // 敏感信息过滤:移除或加密敏感字段
      const filteredData = this.filterSensitiveData(cacheData)
      
      PreferencesUtil.putSync(this.CACHE_KEY, JSON.stringify(filteredData))
      
      LogUtil.info("表单数据已缓存", { timestamp: cacheData.timestamp })
    } catch (error) {
      LogUtil.error("缓存表单数据失败", error)
    }
  }

  /**
   * 从缓存恢复表单数据
   * 
   * 应用启动或用户重新进入表单时,检查是否有可恢复的缓存数据。
   * 需要验证数据的有效性、完整性和归属性。
   */
  static restoreFormData(): HouseBean | null {
    try {
      const cacheStr = PreferencesUtil.getStringSync(this.CACHE_KEY)
      if (!cacheStr) return null
      
      const cacheData = JSON.parse(cacheStr)
      
      // 验证数据归属:确保是当前用户的数据
      if (cacheData.userId !== AgentUtil.userInfo().id) {
        this.clearFormData()
        return null
      }
      
      // 验证数据时效:超过有效期的数据不恢复
      const isExpired = Date.now() - cacheData.timestamp > this.CACHE_DURATION
      if (isExpired) {
        this.clearFormData()
        LogUtil.info("缓存数据已过期")
        return null
      }
      
      LogUtil.info("成功恢复表单数据", { 
        cacheAge: Date.now() - cacheData.timestamp 
      })
      
      return cacheData.data as HouseBean
    } catch (error) {
      LogUtil.error("恢复表单数据失败", error)
      this.clearFormData() // 清除损坏的缓存
      return null
    }
  }

  /**
   * 清除表单缓存
   * 
   * 在表单成功提交后、用户主动取消、或者数据无效时清除缓存。
   */
  static clearFormData() {
    PreferencesUtil.deleteSync(this.CACHE_KEY)
    LogUtil.info("表单缓存已清除")
  }

  /**
   * 检查是否有可恢复的缓存
   * 
   * 用于在用户进入表单时提示是否有未完成的表单可以恢复。
   */
  static hasRecoverableData(): boolean {
    const data = this.restoreFormData()
    return data !== null
  }

  /**
   * 过滤敏感数据
   * 
   * 在缓存前移除或加密敏感信息,如业主联系方式等。
   */
  private static filterSensitiveData(cacheData: any): any {
    const filtered = { ...cacheData }
    
    // 移除敏感字段
    if (filtered.data) {
      delete filtered.data.ownerPhone
      delete filtered.data.ownerIdCard
    }
    
    return filtered
  }

  /**
   * 自动保存机制
   * 
   * 在用户输入过程中定期自动保存,减少数据丢失风险。
   */
  static startAutoSave(houseData: HouseBean, interval: number = 30000) {
    return setInterval(() => {
      this.saveFormData(houseData)
    }, interval)
  }
}

缓存机制的用户体验优化:

  1. 无感知保存:用户无需手动保存,系统自动处理
  2. 智能恢复:重新进入时友好提示用户恢复数据
  3. 数据安全:确保缓存数据不会泄露给其他用户
  4. 性能优化:合理的缓存策略,避免影响应用性能

3. 表单校验增强

表单校验是确保数据质量的重要环节。我们设计了多层次、可扩展的校验框架,不仅支持基础的格式校验,还支持复杂的业务逻辑校验。

校验框架的设计特点:

  • 规则驱动:通过配置规则而非硬编码实现校验
  • 实时校验:输入过程中的即时反馈
  • 批量校验:提交时的完整性校验
  • 国际化支持:错误消息的多语言支持
export class FormValidator {
  // 校验规则配置:采用声明式的规则定义,便于维护和扩展
  private static rules: Map<string, ValidationRule[]> = new Map([
    ['Price', [
      { type: 'required', message: '请输入价格' },
      { type: 'number', message: '价格必须为数字' },
      { type: 'min', value: 0.1, message: '价格不能小于0.1万' },
      { type: 'max', value: 99999, message: '价格不能超过99999万' },
      { type: 'decimal', maxDecimal: 2, message: '价格最多保留2位小数' }
    ]],
    ['MJ', [
      { type: 'required', message: '请输入面积' },
      { type: 'number', message: '面积必须为数字' },
      { type: 'min', value: 1, message: '面积不能小于1㎡' },
      { type: 'max', value: 9999, message: '面积不能超过9999㎡' },
      { type: 'decimal', maxDecimal: 2, message: '面积最多保留2位小数' }
    ]],
    ['Floor', [
      { type: 'required', message: '请输入楼层' },
      { type: 'integer', message: '楼层必须为整数' },
      { type: 'min', value: 1, message: '楼层不能小于1层' }
    ]],
    ['SumFloor', [
      { type: 'required', message: '请输入总楼层' },
      { type: 'integer', message: '总楼层必须为整数' },
      { type: 'min', value: 1, message: '总楼层不能小于1层' },
      { type: 'max', value: 200, message: '总楼层不能超过200层' }
    ]],
    ['Title', [
      { type: 'required', message: '请输入房源标题' },
      { type: 'minLength', value: 5, message: '标题至少5个字符' },
      { type: 'maxLength', value: 50, message: '标题不能超过50个字符' },
      { type: 'pattern', pattern: /^[^<>]*$/, message: '标题不能包含特殊字符' }
    ]]
  ])

  /**
   * 校验单个字段
   * 
   * 用于实时校验,在用户输入过程中提供即时反馈。
   * 支持多种校验规则的组合,按顺序执行,遇到错误即停止。
   */
  static validateField(fieldName: string, value: any): string {
    const rules = this.rules.get(fieldName)
    if (!rules) return ""

    for (const rule of rules) {
      const error = this.checkRule(value, rule)
      if (error) return error
    }

    return ""
  }

  /**
   * 校验整个表单
   * 
   * 用于提交前的完整性校验,返回所有字段的校验结果。
   * 支持字段间的关联校验,如楼层不能超过总楼层等。
   */
  static validateForm(houseData: HouseBean): Map<string, string> {
    const errors = new Map<string, string>()

    // 基础字段校验
    this.rules.forEach((rules, fieldName) => {
      const value = houseData[fieldName as keyof HouseBean]
      const error = this.validateField(fieldName, value)
      if (error) {
        errors.set(fieldName, error)
      }
    })

    // 关联字段校验:处理字段间的逻辑关系
    this.validateRelatedFields(houseData, errors)

    return errors
  }

  /**
   * 关联字段校验
   * 
   * 处理字段间的业务逻辑校验,如:
   * - 楼层不能超过总楼层
   * - 出租房源必须填写租赁方式
   * - 价格应该在合理范围内
   */
  private static validateRelatedFields(houseData: HouseBean, errors: Map<string, string>) {
    // 楼层关联校验
    if (houseData.Floor && houseData.SumFloor) {
      if (houseData.Floor > houseData.SumFloor) {
        errors.set('Floor', '楼层不能超过总楼层')
      }
    }

    // 价格合理性校验
    if (houseData.Price && houseData.MJ && houseData.PriceAverage) {
      const unitPrice = parseFloat(houseData.Price) * 10000 / parseFloat(houseData.MJ)
      const avgPrice = parseFloat(houseData.PriceAverage)
      
      // 单价偏离均价超过50%时提示
      if (Math.abs(unitPrice - avgPrice) / avgPrice > 0.5) {
        errors.set('Price', '价格可能不合理,请检查')
      }
    }

    // 户型合理性校验
    if (houseData.CountF && houseData.CountT) {
      if (houseData.CountF > 10 || houseData.CountT > 5) {
        errors.set('CountF', '户型配置可能不合理')
      }
    }
  }

  /**
   * 单个规则校验
   * 
   * 执行具体的校验规则,支持多种校验类型。
   * 这个方法是校验框架的核心,负责实际的校验逻辑。
   */
  private static checkRule(value: any, rule: ValidationRule): string {
    // 空值处理:required规则单独处理
    if (!value || value.toString().trim() === '') {
      return rule.type === 'required' ? rule.message : ''
    }

    switch (rule.type) {
      case 'number':
        return isNaN(Number(value)) ? rule.message : ''
      
      case 'integer':
        return !Number.isInteger(Number(value)) ? rule.message : ''
      
      case 'min':
        return Number(value) < rule.value! ? rule.message : ''
      
      case 'max':
        return Number(value) > rule.value! ? rule.message : ''
      
      case 'minLength':
        return value.toString().length < rule.value! ? rule.message : ''
      
      case 'maxLength':
        return value.toString().length > rule.value! ? rule.message : ''
      
      case 'decimal':
        const decimalPlaces = (value.toString().split('.')[1] || '').length
        return decimalPlaces > rule.maxDecimal! ? rule.message : ''
      
      case 'pattern':
        return !rule.pattern!.test(value.toString()) ? rule.message : ''
      
      default:
        return ''
    }
  }

  /**
   * 动态添加校验规则
   * 
   * 支持运行时动态添加校验规则,用于处理特殊业务需求。
   */
  static addValidationRule(fieldName: string, rule: ValidationRule) {
    const existingRules = this.rules.get(fieldName) || []
    existingRules.push(rule)
    this.rules.set(fieldName, existingRules)
  }

  /**
   * 批量校验结果处理
   * 
   * 将校验结果转换为用户友好的提示信息。
   */
  static formatValidationErrors(errors: Map<string, string>): string {
    if (errors.size === 0) return ""
    
    const errorMessages = Array.from(errors.values())
    return errorMessages.slice(0, 3).join(';') + 
           (errorMessages.length > 3 ? '...' : '')
  }
}

// 校验规则接口定义
interface ValidationRule {
  type: 'required' | 'number' | 'integer' | 'min' | 'max' | 
        'minLength' | 'maxLength' | 'decimal' | 'pattern'
  message: string
  value?: number
  maxDecimal?: number
  pattern?: RegExp
}

校验框架的技术优势:

  1. 可扩展性:新增校验规则无需修改核心代码
  2. 性能优化:智能的校验策略,避免不必要的计算
  3. 用户体验:清晰的错误提示,帮助用户快速修正
  4. 业务适配:支持复杂的房地产业务校验需求

结语

本文详细介绍了在鸿蒙ArkTS平台上构建企业级录房源表单的完整解决方案。从架构设计到具体实现,从性能优化到用户体验,每个环节都经过深入思考和实践验证。

这套解决方案不仅适用于房地产行业,也可以作为其他行业复杂表单应用的参考。通过合理的组件设计、完善的数据管理、智能的用户交互和持续的性能优化,我们可以在鸿蒙平台上构建出高质量的企业级应用。

随着鸿蒙生态的不断完善和ArkTS技术的持续发展,相信会有更多优秀的表单解决方案涌现,为移动应用开发带来更多可能性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值