鸿蒙原生应用 ArkUI 实战:虚线照片卡 + 五件套档案表单 —— 商品建档页的录入架构

App 53「保修期管理」商品页(Func1Tab),主题色 #00A862 生态绿,4 个 Tab 分别为首页(🔧)、商品(📦)、保修(⏰)、我的(👤)。本篇基于 53-warranty-mgr/entry/src/main/ets/pages/Func1Tab.ets(共 106 行)逐段拆解,附 4 张实机截图。

商品页整体布局

一、整体结构:三区"Header + 上传卡 + 表单"布局

商品页是"照片采集 + 档案表单"的组合布局——3 个 @Builder 块:

build() {
  Column() {
    this.Header()
    Scroll() {
      Column({ space: 16 }) {
        this.PhotoZone()
        this.FormCard()
        Button('保存商品').fontSize(15).fontColor('#FFFFFF').backgroundColor(C.primary)
          .width('100%').height(48).borderRadius(D.rMd)
          .onClick(() => { promptAction.showToast({ message: this.name.length > 0 ? '商品已保存' : '请填写商品名称' }); })
      }
      .width('100%')
      .padding({ left: D.pad, right: D.pad, top: 16, bottom: D.pad + this.safeBottom + 20 })
    }
    .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
  }
  .width('100%').height('100%').backgroundColor(C.bg)
}

3 块结构:PhotoZone(全宽虚线照片上传卡)→ FormCard(五件套档案表单)→ 保存按钮。商品页是"建档页"——先上传凭证(照片/发票),再填五件套(名称/型号/日期/分类/保修期限),最后保存。凭证在上、档案在下,模拟真实购买登记流程:买了新设备 → 拍下发票 → 记录型号 → 设保修期限 → 存档。

二、五状态声明 + 数据模型

@State name: string = '';
@State model: string = '';
@State buyDate: string = '';
@State activeCat: number = 0;
@State activeWarranty: number = 1;
private cats: string[] = ['数码', '家电', '家具', '汽车', '其他'];
private warranties: string[] = ['1年', '2年', '3年', '自定义'];

五个 @State:name(商品名称)、model(型号)、buyDate(购买日期)、activeCat(分类索引,默认 0 = 数码)、activeWarranty(期限索引,默认 1 = 2年)。cats 5 分类 + warranties 4 期限——两个独立选择维度。期限默认 2 年是中位数默认值(多数数码产品保修两年)。

三、Header:单行标题

Header() {
  Row() { Text('添加商品').fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text) }
  .width('100%').height(this.safeTop + 56).padding({ top: this.safeTop, left: D.pad, right: D.pad })
  .backgroundColor(C.card).alignItems(VerticalAlign.Bottom)
}

20 号加粗、safeTop + 56 固定高、文字贴底——系列功能页 Header 统一范式。标题"添加商品"是动词短语(动作型标题),暗示"这是一个待完成的操作",比名词标题("商品")更有引导性。

四、PhotoZone:全宽虚线照片上传卡

PhotoZone() {
  Column({ space: 8 }) {
    Text('📷').fontSize(40)
    Text('上传商品照片/发票').fontSize(13).fontColor(C.primary)
  }
  .width('100%').padding({ top: 24, bottom: 24 }).backgroundColor(C.card).borderRadius(D.rLg)
  .border({ width: 1, color: C.primary, style: BorderStyle.Dashed })
  .onClick(() => { promptAction.showToast({ message: '上传照片' }); })
}

逐点拆解:

  1. 全宽竖向卡:width('100%') + padding({ top: 24, bottom: 24 })——整行一个上传区(与 App 52 的双卡横向排布不同,本页是单卡全宽),竖向居中布局(40 号图标 + 13 号文字上下排列)。
  2. 绿色虚线边框:BorderStyle.Dashed + C.primary 绿——虚线 = "可点击上传区"的视觉语言(与 App 52 的蓝色虚线同手法、异色系)。
  3. 40 号大图标:📷 是全卡视觉主体——比 App 52 的 36 号更大,单卡设计让图标有更大展示空间。
  4. 主色文字:13 号 C.primary 绿字"上传商品照片/发票"——照片 + 发票双凭证(保修凭证通常是发票,照片是商品实拍)。
  5. 大圆角:borderRadius(D.rLg = 20)——上传卡用最大圆角(比列表卡 14 更大),让"功能卡"从表单中突出。

点击照片上传虚线卡

五、FormCard:五件套档案表单

FormCard() {
  Column({ space: 14 }) {
    Column({ space: 6 }) {
      Text('商品名称').fontSize(13).fontColor(C.textSub).width('100%')
      TextInput({ placeholder: '例如:iPhone 15 Pro' })
        .backgroundColor(C.cardSoft).borderRadius(D.rSm).height(44)
        .onChange((v: string) => { this.name = v; })
    }
    Column({ space: 6 }) {
      Text('型号规格').fontSize(13).fontColor(C.textSub).width('100%')
      TextInput({ placeholder: '输入型号' })
        .backgroundColor(C.cardSoft).borderRadius(D.rSm).height(44)
        .onChange((v: string) => { this.model = v; })
    }
    Column({ space: 6 }) {
      Text('购买日期').fontSize(13).fontColor(C.textSub).width('100%')
      TextInput({ placeholder: '2026-08-14' })
        .backgroundColor(C.cardSoft).borderRadius(D.rSm).height(44)
        .onChange((v: string) => { this.buyDate = v; })
    }

前三件套:

  1. 商品名称:TextInput 44 高淡绿灰底圆角 10,onChange 绑定 this.name——保存按钮校验的锚点字段。
  2. 型号规格:记录商品具体型号(如 A2896),保修时需要凭型号查询售后信息。
  3. 购买日期:日期格式占位符(年-月-日),保修期限从购买日算起,是"剩余天数"的计算起点。
    Column({ space: 6 }) {
      Text('商品分类').fontSize(13).fontColor(C.textSub).width('100%')
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(this.cats, (c: string, idx: number) => {
          Text(c).fontSize(12)
            .fontColor(this.activeCat === idx ? '#FFFFFF' : C.textSub)
            .padding({ left: 14, right: 14, top: 7, bottom: 7 })
            .backgroundColor(this.activeCat === idx ? C.primary : C.cardSoft).borderRadius(14)
            .margin({ right: 10, bottom: 10 })
            .onClick(() => { this.activeCat = idx; })
        }, (c: string) => c)
      }.width('100%')
    }

第四件套——商品分类 5 胶囊:Flex({ wrap: FlexWrap.Wrap }) 换行胶囊——数码/家电/家具/汽车/其他,选中态绿底白字、未选灰底灰字,圆角 14、margin({ right: 10, bottom: 10 }) 间距;默认选中第一项"数码"。

    Column({ space: 6 }) {
      Text('保修期限').fontSize(13).fontColor(C.textSub).width('100%')
      Row({ space: 10 }) {
        ForEach(this.warranties, (w: string, idx: number) => {
          Text(w).fontSize(12)
            .fontColor(this.activeWarranty === idx ? '#FFFFFF' : C.textSub)
            .textAlign(TextAlign.Center).layoutWeight(1).padding({ top: 9, bottom: 9 })
            .backgroundColor(this.activeWarranty === idx ? C.primary : C.cardSoft).borderRadius(D.rSm)
            .onClick(() => { this.activeWarranty = idx; })
        }, (w: string) => w)
      }.width('100%')
    }

第五件套——保修期限 4 等分胶囊:Row + layoutWeight(1) 四等分 + textAlign(TextAlign.Center) 居中——1年/2年/3年/自定义一行排开。与分类胶囊的换行布局不同,期限选项少且短,一行等分排开更高效。同一页两种胶囊布局,按选项数量与长度分别适配。

选择商品分类胶囊

六、保存按钮:三元校验

Button('保存商品').fontSize(15).fontColor('#FFFFFF').backgroundColor(C.primary)
  .width('100%').height(48).borderRadius(D.rMd)
  .onClick(() => { promptAction.showToast({ message: this.name.length > 0 ? '商品已保存' : '请填写商品名称' }); })

逐点拆解:

  1. 全宽绿色 CTA:width('100%').height(48) + C.primary——表单页标准 CTA 形态。
  2. 三元校验:this.name.length > 0 ? '商品已保存' : '请填写商品名称'——以名称为校验锚点(商品名称是建档的核心字段,没有名字的档案没有意义),非空 → 保存成功、为空 → 提示填写。
  3. 与 App 52 的差异:52 校验金额(票据核心字段是钱),53 校验名称(商品核心字段是名字)——两个应用的校验锚点都指向各自最关键的字段。

滚动查看保修期限与保存按钮

七、系列横向对比:录入页的两种表单形态

录入页代表 App录入方式核心特色主题色
全宽照片卡 + 五件套App 53 商品凭证 + 档案双胶囊布局#00A862 生态绿
双虚线卡 + 四件套App 52 上传双来源 + 财务金额校验#0066CC 科技蓝
动态选项表单App 49 投票纯表单动态增删选项#6C5CE7 紫

差异点:App 53 的"双胶囊布局"是系列中表单设计的精细案例——分类 5 项较长用 Flex 换行、期限 4 项较短用 Row 等分,按选项数量与长度分别适配布局;而 App 52 的两组胶囊都用 Flex 换行(类型 4 项 + 分类 6 项都较多)。

八、开发者视角:调试与验证技巧

  1. 日期输入校验:购买日期用 TextInput 自由输入——产品化必须加格式校验(正则匹配年-月-日),否则用户输错格式会导致保修期限计算错误;更优方案是换 DatePicker 日期选择器。
  2. 期限与到期日联动:当前期限胶囊只存选中索引,没有联动计算到期日——产品化时应 到期日 = 购买日期 + 期限,并在表单中实时预览剩余天数。
  3. 默认值语义:分类默认"数码"、期限默认"2年"——默认值要符合多数场景,但"自定义"期限选中时需弹出输入框(1 到 99 年),当前版本未实现自定义输入逻辑。
  4. 胶囊选中态公式:两组胶囊选中 = 白字绿底、未选 = 灰字灰底——与 App 52 完全一致,系列表单的统一交互范式。
  5. Scroll 与按钮可达性:表单较长时保存按钮可能在屏幕外——当前 Scroll 包裹保证按钮可滚动到达,safeBottom 内边距已处理底部安全区。

九、演进方向

方向当前状态产品化演进
照片上传Toast"上传照片"PhotoViewPicker 选图 + 多图上传
日期输入TextInput 自由输入DatePicker 日期选择器
期限联动仅存选中索引到期日 = 购买日 + 期限 实时预览
自定义期限未实现弹出输入框(1-99 年)
保存反馈Toast"商品已保存"返回首页刷新 + 新商品置顶

十、一句话总结

商品页以"全宽虚线照片卡 + 五件套档案表单 + 校验保存"完成了商品的建档闭环。BorderStyle.Dashed 绿色虚线边框暗示可点击上传区、Flex 换行胶囊(5 分类)+ Row 等分胶囊(4 期限)双布局按选项适配、三元校验 name.length > 0 ? '商品已保存' : '请填写商品名称' 以名称为锚点——106 行代码,一个"拍凭证 → 填档案 → 设期限 → 校验保存"的商品建档页,也是"凭证 + 档案"双录型表单的标准范式。

项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐