请添加图片描述

一、状态变量设计:极简但精准

@State transformMode: FlipTransformMode = FlipTransformMode.ROTATE3D

整个页面只有一个 @State 变量,但它控制着整个页面的核心交互——卡片的翻转方式。

FlipTransformMode 枚举的设计意图
FlipTransformMode.ROTATE3D   // CSS-like 的 rotate3d 属性
FlipTransformMode.MATRIX4    // 4x4 变换矩阵

这两种模式代表了鸿蒙 3D 变换的两种实现路径

维度 rotate3d Matrix4
API 层级 高层封装,声明式 底层矩阵,命令式
灵活性 只能做旋转 可以做旋转、缩放、平移、透视的任意组合
可读性 rotate3d(1, 0, 0, 180) 语义清晰 Matrix4().rotate(...).translate(...) 链式调用
性能 框架内部优化 直接操作 GPU 矩阵,更精细
适用场景 简单翻转、旋转动画 复杂 3D 变换、多轴组合

页面提供切换按钮,让用户可以实时对比两种模式的视觉效果差异——这是一个技术演示的设计思路。


二、页面结构:五层信息架构

┌─────────────────────────────────┐
│  ProfileHeader                  │  ← 用户身份
│  [头像] 个人中心 / 昵称 / 等级   │
├─────────────────────────────────┤
│  ModeSwitch                     │  ← 模式切换
│  [rotate3d]  [Matrix4]          │
├─────────────────────────────────┤
│                                 │
│  FlipMemberCard                 │  ← 核心交互区
│  (3D 翻转会员卡片)              │
│                                 │
├──────┬──────┬──────┬────────────┤
│ 订单  │ 优惠券│ 收藏  │ 设置      │  ← 快捷入口
├─────────────────────────────────┤
│  TipsPanel                      │  ← 知识补充
│  1. xxx                         │
│  2. xxx                         │
└─────────────────────────────────┘

每一层都是一个独立的 @Builder,职责单一、互不干扰。


三、@Builder 拆分:单文件内的组件化

@Builder
ProfileHeader() { ... }

@Builder
ModeSwitch() { ... }

@Builder
QuickMenu() { ... }

@Builder
TipsPanel() { ... }
为什么用 @Builder 而不是独立组件?
维度 @Builder(当前方案) 独立 @Component
文件数量 单文件,所有逻辑集中 每个区块一个文件
状态访问 直接访问父组件的 @State 需要通过参数传入
复用性 仅限当前文件 可在任意页面复用
适用场景 页面内部的结构拆分 跨页面复用的功能模块

这里选择 @Builder 是合理的——ProfileHeaderModeSwitch 等区块只在这个页面使用,且需要直接访问 this.transformMode。如果拆成独立组件,反而需要额外传参:

// 如果拆成独立组件,需要这样:
ModeSwitch({
  transformMode: this.transformMode,
  onModeChange: (mode) => { this.transformMode = mode }
})

@Builder 省去了这层"参数透传",代码更简洁。

build() 中的调用方式
build() {
  Scroll() {
    Column({ space: 20 }) {
      this.ProfileHeader()
      this.ModeSwitch()
      FlipMemberCard({
        member: DEMO_MEMBER,
        transformMode: this.transformMode
      })
      this.QuickMenu()
      this.TipsPanel()
    }
  }
}

注意 FlipMemberCard 不是 @Builder,而是一个独立的 @Component——因为它需要跨页面复用(其他页面也可能展示会员卡片),且接收明确的参数(membertransformMode)。


四、ModeSwitch:枚举驱动的按钮组

@Builder
ModeSwitch() {
  Row({ space: 8 }) {
    Button('rotate3d')
      .layoutWeight(1)
      .height(36)
      .fontSize(12)
      .backgroundColor(this.transformMode === FlipTransformMode.ROTATE3D ? '#4338CA' : '#CBD5E1')
      .fontColor(this.transformMode === FlipTransformMode.ROTATE3D ? Color.White : '#334155')
      .onClick(() => {
        this.transformMode = FlipTransformMode.ROTATE3D
      })
    Button('Matrix4')
      .layoutWeight(1)
      .height(36)
      .fontSize(12)
      .backgroundColor(this.transformMode === FlipTransformMode.MATRIX4 ? '#4338CA' : '#CBD5E1')
      .fontColor(this.transformMode === FlipTransformMode.MATRIX4 ? Color.White : '#334155')
      .onClick(() => {
        this.transformMode = FlipTransformMode.MATRIX4
      })
  }
}
选中态的视觉表达
状态 背景色 文字色 语义
选中 #4338CA(深靛蓝) Color.White 当前激活的模式
未选中 #CBD5E1(浅灰) #334155(深灰) 可切换的备选模式
数据流
用户点击按钮
  → onClick 更新 this.transformMode
  → @State 触发 UI 刷新
  → 按钮背景色/文字色变化(选中态切换)
  → FlipMemberCard 接收新的 transformMode 属性
  → 卡片翻转动画切换实现方式

这是一个**“状态提升”**的经典模式——transformMode 定义在父组件,通过属性传递给子组件 FlipMemberCard,子组件根据模式选择不同的 3D 变换实现。


五、ProfileHeader:渐变头像 + 文本信息

@Builder
ProfileHeader() {
  Row({ space: 14 }) {
    Column() {
      Text(DEMO_MEMBER.avatarLabel)
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
    }
    .width(56)
    .height(56)
    .justifyContent(FlexAlign.Center)
    .borderRadius(28)
    .linearGradient({
      angle: 135,
      colors: [['#6366F1', 0], ['#8B5CF6', 1]]
    })
    // ...
  }
}
linearGradient 渐变头像
.linearGradient({
  angle: 135,
  colors: [['#6366F1', 0], ['#8B5CF6', 1]]
})
  • angle: 135:从左上到右下的对角线方向
  • colors:从靛蓝色(#6366F1)渐变到紫色(#8B5CF6

这是一个无图片的头像方案——用用户名首字 + 渐变背景代替真实头像。在没有网络图片或默认头像时,这种方案既美观又不依赖外部资源。

borderRadius(28) = 圆形裁剪

头像容器 56×56borderRadius(28) 恰好是宽高的一半,实现完美圆形。这是鸿蒙中实现圆形头像的标准做法。


六、QuickMenu:配置驱动的快捷入口

@Builder
QuickMenu() {
  Row({ space: 10 }) {
    ForEach(['订单', '优惠券', '收藏', '设置'], (item: string) => {
      Column({ space: 6 }) {
        Text('◆')
          .fontSize(16)
          .fontColor('#6366F1')
        Text(item)
          .fontSize(12)
          .fontColor('#475569')
      }
      .layoutWeight(1)
      .padding({ top: 12, bottom: 12 })
      .backgroundColor('#FFFFFF')
      .borderRadius(10)
    }, (item: string) => item)
  }
}
代替图标

这里用 Unicode 字符 作为图标的占位符。在实际项目中,应该替换为 Image 组件或 SymbolGlyph 图标。用字符占位的好处是:

  • 不依赖图片资源
  • 开发阶段快速预览布局效果
  • 颜色可通过 fontColor 统一控制
layoutWeight(1) 四等分

四个菜单项使用 layoutWeight(1) 实现等宽分布,无论屏幕多宽,每个菜单项始终占 1/4 宽度。


七、与其他页面的架构对比

维度 验证码页面 视频播放器页面 会员卡片页面
核心问题 输入状态控制 手势交互 + 跨组件通信 3D 变换 + 模式切换
状态数量 多个 @State 3 个 @StorageLink 1 个 @State
代码组织 build() 平铺 build() 平铺 @Builder 拆分为 5 个区块
子组件通信 双向绑定 $ AppStorage 全局 属性传递(单向数据流)
枚举使用 FlipTransformMode 驱动模式切换
视觉重点 输入框 + 倒计时 视频区域 3D 翻转卡片

八、设计亮点与改进建议

亮点
  • @Builder 拆分:在保持单文件简洁的同时,实现了逻辑分块,可读性远优于平铺式 build()
  • 枚举驱动模式切换FlipTransformMode 让两种 3D 实现方式可以无缝切换,便于技术对比
  • 单向数据流transformMode 从父组件流向子组件,数据流清晰可追踪
  • 无图片头像方案linearGradient + 首字,零资源依赖
  • 配置驱动菜单QuickMenu 的菜单项是字符串数组,新增入口只需加一个字符串
可改进之处
  • QuickMenu 缺少点击事件:只有展示,没有 onClick,实际项目中需要绑定路由跳转
  • QuickMenu 图标是占位符 应该替换为语义化图标(SymbolGlyphImage
  • ModeSwitch 可抽取为通用组件:这种"枚举按钮组"在很多场景都适用(如 Tab 切换),可以封装为 EnumButtonGroup<T> 泛型组件
  • 缺少翻转手势:当前只通过按钮切换模式,用户可能期望直接点击/滑动卡片触发翻转
  • 缺少卡片数据动态化DEMO_MEMBER 是硬编码的演示数据,实际项目中应从网络或本地数据库获取
  • Scroll 包裹整个页面:当内容超出屏幕时自动滚动,但缺少回弹效果配置(.edgeEffect(EdgeEffect.Spring)

完整代码

import { FlipMemberCard } from '../flipcard/FlipMemberCard'
import {
  DEMO_MEMBER,
  FlipTransformMode,
  FLIP_TIPS
} from '../flipcard/MemberCardTypes'



struct Index {
   transformMode: FlipTransformMode = FlipTransformMode.ROTATE3D

  build() {
    Scroll() {
      Column({ space: 20 }) {
        this.ProfileHeader()
        this.ModeSwitch()
        FlipMemberCard({
          member: DEMO_MEMBER,
          transformMode: this.transformMode
        })
        this.QuickMenu()
        this.TipsPanel()
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16, bottom: 24 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F5F9')
  }

  
  ProfileHeader() {
    Row({ space: 14 }) {
      Column() {
        Text(DEMO_MEMBER.avatarLabel)
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
      }
      .width(56)
      .height(56)
      .justifyContent(FlexAlign.Center)
      .borderRadius(28)
      .linearGradient({
        angle: 135,
        colors: [['#6366F1', 0], ['#8B5CF6', 1]]
      })

      Column({ space: 4 }) {
        Text('个人中心')
          .fontSize(12)
          .fontColor('#64748B')
        Text(DEMO_MEMBER.nickname)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#0F172A')
        Text(`${DEMO_MEMBER.tier} · 会员卡片 3D 翻转`)
          .fontSize(12)
          .fontColor('#6366F1')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
    }
    .width('100%')
  }

  
  ModeSwitch() {
    Row({ space: 8 }) {
      Button('rotate3d')
        .layoutWeight(1)
        .height(36)
        .fontSize(12)
        .backgroundColor(this.transformMode === FlipTransformMode.ROTATE3D ? '#4338CA' : '#CBD5E1')
        .fontColor(this.transformMode === FlipTransformMode.ROTATE3D ? Color.White : '#334155')
        .onClick(() => {
          this.transformMode = FlipTransformMode.ROTATE3D
        })
      Button('Matrix4')
        .layoutWeight(1)
        .height(36)
        .fontSize(12)
        .backgroundColor(this.transformMode === FlipTransformMode.MATRIX4 ? '#4338CA' : '#CBD5E1')
        .fontColor(this.transformMode === FlipTransformMode.MATRIX4 ? Color.White : '#334155')
        .onClick(() => {
          this.transformMode = FlipTransformMode.MATRIX4
        })
    }
    .width('100%')
  }

  
  QuickMenu() {
    Row({ space: 10 }) {
      ForEach(['订单', '优惠券', '收藏', '设置'], (item: string) => {
        Column({ space: 6 }) {
          Text('◆')
            .fontSize(16)
            .fontColor('#6366F1')
          Text(item)
            .fontSize(12)
            .fontColor('#475569')
        }
        .layoutWeight(1)
        .padding({ top: 12, bottom: 12 })
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
      }, (item: string) => item)
    }
    .width('100%')
  }

  
  TipsPanel() {
    Column({ space: 8 }) {
      Text('实战要点')
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .fontColor('#334155')
      ForEach(FLIP_TIPS, (tip: string, index: number) => {
        Text(`${index + 1}. ${tip}`)
          .fontSize(12)
          .fontColor('#64748B')
      }, (tip: string, index: number) => `flip_tip_${index}`)
    }
    .alignItems(HorizontalAlign.Start)
    .width('100%')
    .padding(14)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
  }
}

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

九、@Builder vs @Component 的决策树

需要拆分的 UI 区块
  │
  ├─ 是否跨页面复用?
  │   ├─ 是 → 使用 @Component(独立文件)
  │   └─ 否 ↓
  │
  ├─ 是否需要直接访问父组件的 @State?
  │   ├─ 是 → 使用 @Builder(当前方案)
  │   └─ 否 ↓
  │
  └─ 是否逻辑复杂、状态独立?
      ├─ 是 → 使用 @Component(同文件或独立文件)
      └─ 否 → 使用 @Builder(当前方案)

这个页面中的 FlipMemberCard 是独立 @Component(跨页面复用 + 参数传入),而 ProfileHeaderModeSwitch 等是 @Builder(仅当前页面使用 + 直接访问 this.transformMode)——两者的选择都是合理的。

Logo

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

更多推荐