在房地产行业应用中,录房源功能是核心业务流程之一,涉及复杂的表单交互、数据校验、组件复用等技术挑战。本文基于真实的鸿蒙房产经纪平台项目,深入解析如何构建高效、易用、可维护的录房源表单系统。
一、业务场景与技术挑战
业务背景
房产经纪人需要快速录入房源信息,包括基础信息、物业地址、业主信息、客户跟进等多个维度,字段多达50+个,且存在复杂的业务逻辑和数据关联。在实际业务中,录房源不仅仅是简单的表单填写,还涉及以下复杂场景:
- 多角色权限控制:不同级别的经纪人有不同的录入权限
- 数据关联性强:楼盘选择后需自动填充区域、商圈等信息
- 业务规则复杂:出售和出租模式下的字段差异
- 数据校验严格:价格合理性、面积逻辑性等业务校验
核心挑战
技术层面挑战:
- 复杂表单管理:多模块、多字段的统一管理和状态同步
- 组件复用:减少重复代码,提高开发效率
- 用户体验:流畅的交互、清晰的视觉层次、智能的输入提示
- 数据校验:实时校验、错误提示、必填项管理
- 业务逻辑:出售/出租切换、权限控制、数据联动
业务层面挑战:
- 录入效率:经纪人希望快速录入,减少重复操作
- 数据准确性:避免录入错误导致的业务问题
- 跨平台一致性:与Web端、小程序端保持数据结构一致
- 离线支持:网络不稳定时的数据暂存
二、整体架构设计
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
}
}
数据模型的设计考虑:
- 业务完整性:覆盖房源的所有关键信息
- 扩展性:预留扩展字段,支持未来业务需求
- 类型安全:使用TypeScript确保类型安全
- 默认值处理:合理的默认值减少用户录入负担
三、核心页面实现
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
}
}
主页面设计要点:
- 状态管理:使用@State统一管理页面状态
- 模块协调:通过数据绑定实现模块间通信
- 用户体验:提交防重、错误提示、状态反馈
- 性能优化:合理的组件更新策略
四、通用组件设计
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"))
}
}
}
}
组件特性详解:
- 输入类型支持:文本、数字、电话等多种输入类型
- 实时校验:输入过程中的即时反馈
- 视觉反馈:错误状态的边框和提示文字
- 用户体验:右对齐输入、合理的占位符、清晰的必填标识
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"))
}
}
}
选择器组件的核心功能:
- 状态显示:清晰区分已选择和未选择状态
- 禁用支持:支持禁用状态,防止不当操作
- 视觉提示:通过箭头和颜色提示用户可操作性
- 扩展性:支持各种选择器类型的扩展
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. 智能表单填充
在房地产业务中,很多数据是相互关联的。比如选择了楼盘后,区域、商圈、均价等信息都可以自动填充。这种智能填充功能大大提高了录入效率,减少了用户的重复劳动。
智能填充的实现原理:
- 数据关联分析:分析字段间的依赖关系
- 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 []
}
}
}
智能填充的业务价值:
- 提高效率:减少重复输入,提升录入速度
- 减少错误:自动填充的数据准确性更高
- 用户体验:降低操作复杂度,提升满意度
- 数据一致性:确保相关字段的数据关联正确
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)
}
}
缓存机制的用户体验优化:
- 无感知保存:用户无需手动保存,系统自动处理
- 智能恢复:重新进入时友好提示用户恢复数据
- 数据安全:确保缓存数据不会泄露给其他用户
- 性能优化:合理的缓存策略,避免影响应用性能
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
}
校验框架的技术优势:
- 可扩展性:新增校验规则无需修改核心代码
- 性能优化:智能的校验策略,避免不必要的计算
- 用户体验:清晰的错误提示,帮助用户快速修正
- 业务适配:支持复杂的房地产业务校验需求
结语
本文详细介绍了在鸿蒙ArkTS平台上构建企业级录房源表单的完整解决方案。从架构设计到具体实现,从性能优化到用户体验,每个环节都经过深入思考和实践验证。
这套解决方案不仅适用于房地产行业,也可以作为其他行业复杂表单应用的参考。通过合理的组件设计、完善的数据管理、智能的用户交互和持续的性能优化,我们可以在鸿蒙平台上构建出高质量的企业级应用。
随着鸿蒙生态的不断完善和ArkTS技术的持续发展,相信会有更多优秀的表单解决方案涌现,为移动应用开发带来更多可能性。