HarmonyOS之自定义组件开发:打造可复用的UI模块

自定义组件是HarmonyOS开发中提高代码复用性和维护性的关键。本文将深入讲解如何创建和使用自定义组件,涵盖组件设计原则、参数传递、事件处理等核心概念。

一、自定义组件基础

1.1 创建第一个自定义组件

// 基础按钮组件
@Component
struct CustomButton {
  // 组件参数 - 使用@Prop装饰器接收外部数据
  @Prop buttonText: string = '按钮'
  @Prop backgroundColor: string = '#007DFF'
  @Prop textColor: string = '#FFFFFF'
  @Prop disabled: boolean = false

  // 组件内部状态 - 使用@State装饰器
  @State isPressed: boolean = false

  build() {
    Button(this.buttonText)
      .width(120)
      .height(40)
      .backgroundColor(this.disabled ? '#CCCCCC' : this.backgroundColor)
      .fontColor(this.disabled ? '#666666' : this.textColor)
      .fontSize(16)
      .borderRadius(8)
      .opacity(this.isPressed ? 0.8 : 1.0)
      .enabled(!this.disabled)
      .onClick(() => {
        console.log('按钮被点击:', this.buttonText)
      })
      .onTouch((event: TouchEvent) => {
        if (event.type === TouchType.Down) {
          this.isPressed = true
        } else if (event.type === TouchType.Up) {
          this.isPressed = false
        }
      })
  }
}

// 使用自定义组件
@Component
struct CustomComponentDemo {
  build() {
    Column({ space: 20 }) {
      Text('自定义按钮组件演示')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      // 使用自定义按钮组件
      CustomButton({ buttonText: '主要按钮' })
      
      CustomButton({
        buttonText: '成功按钮',
        backgroundColor: '#34C759',
        textColor: '#FFFFFF'
      })
      
      CustomButton({
        buttonText: '警告按钮',
        backgroundColor: '#FF9500',
        textColor: '#FFFFFF'
      })
      
      CustomButton({
        buttonText: '禁用按钮',
        disabled: true
      })
    }
    .width('100%')
    .padding(20)
    .alignItems(HorizontalAlign.Center)
  }
}

1.2 组件参数类型定义

// 定义组件参数接口
interface ButtonConfig {
  text: string
  type?: 'primary' | 'success' | 'warning' | 'danger'
  size?: 'small' | 'medium' | 'large'
  disabled?: boolean
  loading?: boolean
}

// 增强型按钮组件
@Component
struct EnhancedButton {
  // 使用接口定义参数
  @Prop config: ButtonConfig

  // 计算属性 - 根据配置生成样式
  private get buttonStyle(): ButtonStyle {
    const baseStyle = {
      width: this.getButtonWidth(),
      height: this.getButtonHeight(),
      backgroundColor: this.getBackgroundColor(),
      fontSize: this.getFontSize(),
      borderRadius: 6
    }
    return baseStyle
  }

  build() {
    Button(this.config.text)
      .width(this.buttonStyle.width)
      .height(this.buttonStyle.height)
      .backgroundColor(this.buttonStyle.backgroundColor)
      .fontColor('#FFFFFF')
      .fontSize(this.buttonStyle.fontSize)
      .borderRadius(this.buttonStyle.borderRadius)
      .enabled(!this.config.disabled && !this.config.loading)
      .overlay(
        this.config.loading ? 
          Progress({ value: 50, total: 100 })
            .width(20)
            .height(20)
            .color(Color.White) 
          : null
      )
  }

  private getButtonWidth(): string | number {
    switch (this.config.size) {
      case 'small': return 80
      case 'large': return 160
      default: return 120
    }
  }

  private getButtonHeight(): number {
    switch (this.config.size) {
      case 'small': return 32
      case 'large': return 48
      default: return 40
    }
  }

  private getBackgroundColor(): string {
    switch (this.config.type) {
      case 'success': return '#34C759'
      case 'warning': return '#FF9500'
      case 'danger': return '#FF3B30'
      default: return '#007DFF'
    }
  }

  private getFontSize(): number {
    switch (this.config.size) {
      case 'small': return 12
      case 'large': return 16
      default: return 14
    }
  }
}

// 使用增强型按钮
@Component
struct EnhancedButtonDemo {
  build() {
    Column({ space: 15 }) {
      EnhancedButton({
        config: {
          text: '主要按钮',
          type: 'primary',
          size: 'medium'
        }
      })
      
      EnhancedButton({
        config: {
          text: '加载中...',
          type: 'success',
          loading: true
        }
      })
      
      EnhancedButton({
        config: {
          text: '危险操作',
          type: 'danger',
          size: 'large'
        }
      })
    }
    .width('100%')
    .padding(20)
  }
}

二、组件事件通信

2.1 自定义事件处理

// 定义事件回调接口
interface ButtonEvents {
  onClick?: () => void
  onLongPress?: () => void
  onDisabledClick?: () => void
}

// 支持事件处理的按钮组件
@Component
struct EventButton {
  @Prop buttonText: string = '按钮'
  @Prop disabled: boolean = false
  @Prop events: ButtonEvents = {}

  @State pressStartTime: number = 0
  private readonly LONG_PRESS_DURATION: number = 1000 // 1秒

  build() {
    Button(this.buttonText)
      .width(120)
      .height(40)
      .backgroundColor(this.disabled ? '#CCCCCC' : '#007DFF')
      .fontColor('#FFFFFF')
      .enabled(!this.disabled)
      .onClick(() => {
        if (this.disabled) {
          this.events.onDisabledClick?.()
        } else {
          this.events.onClick?.()
        }
      })
      .onTouch((event: TouchEvent) => {
        if (event.type === TouchType.Down) {
          this.pressStartTime = new Date().getTime()
        } else if (event.type === TouchType.Up) {
          const pressDuration = new Date().getTime() - this.pressStartTime
          if (pressDuration >= this.LONG_PRESS_DURATION) {
            this.events.onLongPress?.()
          }
        }
      })
  }
}

// 使用事件按钮组件
@Component
struct EventButtonDemo {
  @State clickCount: number = 0
  @State longPressCount: number = 0
  @State disabledClickCount: number = 0

  build() {
    Column({ space: 20 }) {
      Text('组件事件通信演示')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      // 事件统计显示
      Text(`点击次数: ${this.clickCount}`).fontSize(14)
      Text(`长按次数: ${this.longPressCount}`).fontSize(14)
      Text(`禁用点击次数: ${this.disabledClickCount}`).fontSize(14)
      
      // 普通按钮
      EventButton({
        buttonText: '点击我',
        events: {
          onClick: () => {
            this.clickCount++
            console.log('按钮被点击')
          },
          onLongPress: () => {
            this.longPressCount++
            console.log('按钮被长按')
          }
        }
      })
      
      // 禁用按钮
      EventButton({
        buttonText: '禁用按钮',
        disabled: true,
        events: {
          onDisabledClick: () => {
            this.disabledClickCount++
            console.log('禁用的按钮被点击')
          }
        }
      })
      
      Button('重置计数')
        .onClick(() => {
          this.clickCount = 0
          this.longPressCount = 0
          this.disabledClickCount = 0
        })
    }
    .width('100%')
    .padding(20)
    .alignItems(HorizontalAlign.Center)
  }
}

2.2 使用@Link进行双向通信

// 计数器组件
@Component
struct CounterComponent {
  @Link count: number
  @Prop minValue: number = 0
  @Prop maxValue: number = 100
  @Prop step: number = 1

  private updateCount(delta: number): void {
    const newValue = this.count + delta
    if (newValue >= this.minValue && newValue <= this.maxValue) {
      this.count = newValue
    }
  }

  build() {
    Row({ space: 15 }) {
      Button('-')
        .width(40)
        .height(40)
        .backgroundColor(this.count > this.minValue ? '#FF3B30' : '#CCCCCC')
        .enabled(this.count > this.minValue)
        .onClick(() => this.updateCount(-this.step))
      
      Text(this.count.toString())
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .width(60)
        .textAlign(TextAlign.Center)
      
      Button('+')
        .width(40)
        .height(40)
        .backgroundColor(this.count < this.maxValue ? '#34C759' : '#CCCCCC')
        .enabled(this.count < this.maxValue)
        .onClick(() => this.updateCount(this.step))
    }
  }
}

// 购物车商品组件
@Component
struct CartItemComponent {
  @Link item: CartItem
  @Prop onQuantityChange?: (item: CartItem, newQuantity: number) => void

  build() {
    Row({ space: 15 }) {
      // 商品信息
      Column({ space: 5 }) {
        Text(this.item.name)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
        Text(`¥${this.item.price.toFixed(2)}`)
          .fontSize(14)
          .fontColor('#FF6B6B')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)

      // 数量控制器
      CounterComponent({
        count: $item.quantity,
        minValue: 0,
        maxValue: 10,
        step: 1
      })

      // 删除按钮
      Button('删除')
        .width(60)
        .height(30)
        .backgroundColor('#FF3B30')
        .fontColor(Color.White)
        .fontSize(12)
        .onClick(() => {
          this.item.quantity = 0
          this.onQuantityChange?.(this.item, 0)
        })
    }
    .width('100%')
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(8)
    .shadow({ radius: 2, color: '#10000000', offsetX: 0, offsetY: 1 })
  }
}

// 购物车页面
@Component
struct ShoppingCartDemo {
  @State cartItems: CartItem[] = [
    { id: 1, name: 'HarmonyOS编程指南', price: 68.00, quantity: 2 },
    { id: 2, name: 'TypeScript实战', price: 49.00, quantity: 1 },
    { id: 3, name: '前端架构设计', price: 79.00, quantity: 0 }
  ]

  build() {
    Column({ space: 20 }) {
      Text('购物车演示')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      // 商品列表
      ForEach(this.cartItems, (item: CartItem) => {
        if (item.quantity > 0) {
          CartItemComponent({
            item: $item,
            onQuantityChange: (changedItem, newQuantity) => {
              console.log(`商品 ${changedItem.name} 数量改为: ${newQuantity}`)
              if (newQuantity === 0) {
                this.removeItem(changedItem.id)
              }
            }
          })
        }
      })
      
      // 购物车统计
      this.buildCartSummary()
    }
    .width('100%')
    .padding(20)
  }

  @Builder
  buildCartSummary() {
    const totalQuantity = this.cartItems.reduce((sum, item) => sum + item.quantity, 0)
    const totalAmount = this.cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)

    Column({ space: 10 }) {
      Text('购物车统计')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
      
      Text(`商品种类: ${this.cartItems.filter(item => item.quantity > 0).length}`)
        .fontSize(14)
      Text(`总数量: ${totalQuantity}`)
        .fontSize(14)
      Text(`总金额: ¥${totalAmount.toFixed(2)}`)
        .fontSize(16)
        .fontColor('#FF6B6B')
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#F8F9FA')
    .borderRadius(8)
  }

  private removeItem(itemId: number): void {
    this.cartItems = this.cartItems.filter(item => item.id !== itemId)
  }
}

class CartItem {
  id: number = 0
  name: string = ''
  price: number = 0
  quantity: number = 0
}

三、复杂组件开发

3.1 卡片组件开发

// 卡片组件参数接口
interface CardConfig {
  title: string
  subtitle?: string
  content: string
  imageUrl?: string
  actions?: CardAction[]
  elevation?: number
  borderRadius?: number
}

interface CardAction {
  text: string
  type: 'primary' | 'default' | 'danger'
  onPress: () => void
}

// 通用卡片组件
@Component
struct UniversalCard {
  @Prop config: CardConfig
  @State isExpanded: boolean = false

  build() {
    Column({ space: 0 }) {
      // 卡片头部
      this.buildCardHeader()
      
      // 卡片内容
      this.buildCardContent()
      
      // 卡片操作区
      if (this.config.actions && this.config.actions.length > 0) {
        this.buildCardActions()
      }
    }
    .width('100%')
    .backgroundColor(Color.White)
    .borderRadius(this.config.borderRadius || 12)
    .shadow({
      radius: this.config.elevation || 2,
      color: '#20000000',
      offsetX: 0,
      offsetY: this.config.elevation ? this.config.elevation / 2 : 1
    })
  }

  @Builder
  buildCardHeader() {
    Row({ space: 12 }) {
      // 图片
      if (this.config.imageUrl) {
        Image(this.config.imageUrl)
          .width(40)
          .height(40)
          .borderRadius(20)
          .objectFit(ImageFit.Cover)
      }
      
      // 标题区域
      Column({ space: 2 }) {
        Text(this.config.title)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Start)
        
        if (this.config.subtitle) {
          Text(this.config.subtitle)
            .fontSize(12)
            .fontColor('#666666')
            .textAlign(TextAlign.Start)
        }
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      
      // 展开/收起按钮
      if (this.config.content.length > 100) {
        Button(this.isExpanded ? '收起' : '展开')
          .width(60)
          .height(28)
          .fontSize(12)
          .backgroundColor(Color.Transparent)
          .fontColor('#007DFF')
          .onClick(() => {
            this.isExpanded = !this.isExpanded
          })
      }
    }
    .width('100%')
    .padding(16)
  }

  @Builder
  buildCardContent() {
    Column({ space: 8 }) {
      Text(this.isExpanded ? 
        this.config.content : 
        this.config.content.substring(0, 100) + '...'
      )
        .fontSize(14)
        .fontColor('#333333')
        .textAlign(TextAlign.Start)
        .lineHeight(20)
    }
    .width('100%')
    .padding({ left: 16, right: 16, bottom: 16 })
  }

  @Builder
  buildCardActions() {
    Divider()
    
    Row({ space: 12 }) {
      ForEach(this.config.actions, (action: CardAction) => {
        Button(action.text)
          .layoutWeight(1)
          .height(36)
          .fontSize(14)
          .backgroundColor(this.getActionColor(action.type))
          .fontColor(Color.White)
          .onClick(() => {
            action.onPress()
          })
      })
    }
    .width('100%')
    .padding(16)
  }

  private getActionColor(type: string): string {
    switch (type) {
      case 'primary': return '#007DFF'
      case 'danger': return '#FF3B30'
      default: return '#666666'
    }
  }
}

// 卡片组件使用示例
@Component
struct CardComponentDemo {
  @State likes: number = 42
  @State shares: number = 8

  build() {
    Column({ space: 20 }) {
      UniversalCard({
        config: {
          title: 'HarmonyOS开发实战',
          subtitle: '发布于 2小时前',
          content: 'HarmonyOS是华为推出的分布式操作系统,为不同设备的智能化、互联与协同提供统一的语言。本文将深入讲解ArkUI框架的使用技巧和最佳实践,帮助开发者快速构建高质量的跨设备应用。',
          imageUrl: $r('app.media.author_avatar'),
          elevation: 4,
          borderRadius: 16,
          actions: [
            {
              text: '点赞',
              type: 'primary',
              onPress: () => {
                this.likes++
                console.log('点赞成功')
              }
            },
            {
              text: '分享',
              type: 'default',
              onPress: () => {
                this.shares++
                console.log('分享成功')
              }
            }
          ]
        }
      })
      
      // 统计信息
      Row({ space: 20 }) {
        Text(`点赞: ${this.likes}`).fontSize(14)
        Text(`分享: ${this.shares}`).fontSize(14)
      }
    }
    .width('100%')
    .padding(20)
  }
}

3.2 表单组件开发

// 表单字段接口
interface FormField {
  id: string
  label: string
  type: 'text' | 'email' | 'password' | 'number' | 'textarea'
  required?: boolean
  placeholder?: string
  value: string
  error?: string
}

// 表单组件
@Component
struct FormComponent {
  @Prop fields: FormField[] = []
  @Prop onSubmit?: (formData: Record<string, string>) => void
  @Prop submitText: string = '提交'

  @State formData: Record<string, string> = {}

  aboutToAppear() {
    // 初始化表单数据
    this.fields.forEach(field => {
      this.formData[field.id] = field.value
    })
  }

  build() {
    Column({ space: 20 }) {
      Text('表单组件')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      // 表单字段
      ForEach(this.fields, (field: FormField) => {
        this.buildFormField(field)
      })
      
      // 提交按钮
      Button(this.submitText)
        .width('100%')
        .height(44)
        .backgroundColor('#007DFF')
        .fontColor(Color.White)
        .onClick(() => {
          if (this.validateForm()) {
            this.onSubmit?.(this.formData)
          }
        })
    }
    .width('100%')
    .padding(20)
  }

  @Builder
  buildFormField(field: FormField) {
    Column({ space: 5 }) {
      // 标签
      Row() {
        Text(field.label)
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
        if (field.required) {
          Text('*')
            .fontSize(14)
            .fontColor('#FF3B30')
        }
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)
      
      // 输入框
      if (field.type === 'textarea') {
        TextArea({
          placeholder: field.placeholder,
          text: this.formData[field.id]
        })
          .onChange((value: string) => {
            this.formData[field.id] = value
            this.clearError(field.id)
          })
          .height(100)
      } else {
        TextInput({
          placeholder: field.placeholder,
          text: this.formData[field.id]
        })
          .type(this.getInputType(field.type))
          .onChange((value: string) => {
            this.formData[field.id] = value
            this.clearError(field.id)
          })
          .height(40)
      }
      
      // 错误信息
      if (field.error) {
        Text(field.error)
          .fontSize(12)
          .fontColor('#FF3B30')
          .width('100%')
          .textAlign(TextAlign.Start)
      }
    }
    .width('100%')
  }

  private getInputType(fieldType: string): InputType {
    switch (fieldType) {
      case 'email': return InputType.Email
      case 'password': return InputType.Password
      case 'number': return InputType.Number
      default: return InputType.Normal
    }
  }

  private validateForm(): boolean {
    let isValid = true
    
    this.fields.forEach(field => {
      if (field.required && !this.formData[field.id]?.trim()) {
        field.error = `${field.label}不能为空`
        isValid = false
      } else if (field.type === 'email' && !this.isValidEmail(this.formData[field.id])) {
        field.error = '请输入有效的邮箱地址'
        isValid = false
      } else {
        field.error = undefined
      }
    })
    
    return isValid
  }

  private isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return emailRegex.test(email)
  }

  private clearError(fieldId: string): void {
    const field = this.fields.find(f => f.id === fieldId)
    if (field) {
      field.error = undefined
    }
  }
}

// 表单使用示例
@Component
struct FormDemo {
  @State formFields: FormField[] = [
    {
      id: 'username',
      label: '用户名',
      type: 'text',
      required: true,
      placeholder: '请输入用户名',
      value: ''
    },
    {
      id: 'email',
      label: '邮箱',
      type: 'email',
      required: true,
      placeholder: '请输入邮箱地址',
      value: ''
    },
    {
      id: 'password',
      label: '密码',
      type: 'password',
      required: true,
      placeholder: '请输入密码',
      value: ''
    },
    {
      id: 'bio',
      label: '个人简介',
      type: 'textarea',
      placeholder: '请输入个人简介',
      value: ''
    }
  ]

  build() {
    Column({ space: 20 }) {
      FormComponent({
        fields: this.formFields,
        submitText: '注册',
        onSubmit: (formData) => {
          console.log('表单提交数据:', formData)
          // 处理表单提交
          this.handleFormSubmit(formData)
        }
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  private handleFormSubmit(formData: Record<string, string>): void {
    // 模拟API调用
    console.log('提交的用户数据:', formData)
    
    // 显示成功消息
    // 可以在这里添加成功处理逻辑
  }
}

四、组件最佳实践

4.1 组件设计原则

// 符合单一职责原则的组件
@Component
struct UserAvatar {
  @Prop imageUrl: string
  @Prop size: number = 40
  @Prop online: boolean = false

  build() {
    Stack({ alignContent: Alignment.BottomEnd }) {
      Image(this.imageUrl)
        .width(this.size)
        .height(this.size)
        .borderRadius(this.size / 2)
        .objectFit(ImageFit.Cover)
      
      if (this.online) {
        Circle({ width: this.size / 4, height: this.size / 4 })
          .fill('#34C759')
          .stroke({ width: 2, color: Color.White })
      }
    }
    .width(this.size)
    .height(this.size)
  }
}

// 组合使用示例
@Component
struct UserProfile {
  @Prop user: User = new User()

  build() {
    Row({ space: 12 }) {
      UserAvatar({
        imageUrl: this.user.avatar,
        size: 60,
        online: this.user.isOnline
      })
      
      Column({ space: 4 }) {
        Text(this.user.name)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
        Text(this.user.title)
          .fontSize(14)
          .fontColor('#666666')
        Text(`最后活跃: ${this.user.lastActive}`)
          .fontSize(12)
          .fontColor('#999999')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding(16)
  }
}

class User {
  name: string = '匿名用户'
  avatar: string = ''
  title: string = ''
  isOnline: boolean = false
  lastActive: string = '刚刚'
}

4.2 性能优化组件

@Component
struct OptimizedListComponent {
  @Prop items: ListItem[] = []
  @State expandedItemId: string = ''

  build() {
    List({ space: 10 }) {
      ForEach(this.items, (item: ListItem) => {
        ListItem() {
          this.ListItemContent(item)
        }
      }, (item: ListItem) => item.id)
    }
  }

  @Builder
  ListItemContent(item: ListItem) {
    Column({ space: 8 }) {
      // 标题行
      this.buildItemHeader(item)
      
      // 内容区域(条件渲染)
      if (this.expandedItemId === item.id) {
        this.buildItemDetails(item)
      }
    }
    .width('100%')
    .padding(12)
    .backgroundColor(Color.White)
    .borderRadius(8)
    .onClick(() => {
      this.expandedItemId = this.expandedItemId === item.id ? '' : item.id
    })
  }

  @Builder
  buildItemHeader(item: ListItem) {
    Row({ space: 12 }) {
      Image(item.icon)
        .width(24)
        .height(24)
      
      Text(item.title)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .layoutWeight(1)
      
      Image(this.expandedItemId === item.id ? 
        $r('app.media.arrow_up') : $r('app.media.arrow_down'))
        .width(16)
        .height(16)
    }
    .width('100%')
  }

  @Builder
  buildItemDetails(item: ListItem) {
    Column({ space: 8 }) {
      Text(item.description)
        .fontSize(14)
        .fontColor('#666666')
        .textAlign(TextAlign.Start)
      
      Row({ space: 8 }) {
        ForEach(item.tags, (tag: string) => {
          Text(tag)
            .fontSize(12)
            .fontColor('#007DFF')
            .padding({ left: 8, right: 8, top: 4, bottom: 4 })
            .backgroundColor('#F0F8FF')
            .borderRadius(12)
        })
      }
    }
    .width('100%')
    .margin({ top: 8 })
  }
}

// 使用示例
@Component
struct OptimizedListDemo {
  @State listItems: ListItem[] = [
    {
      id: '1',
      title: 'HarmonyOS开发',
      icon: $r('app.media.harmony_icon'),
      description: '学习HarmonyOS应用开发的基础知识和高级特性',
      tags: ['移动开发', '鸿蒙', 'ArkTS']
    },
    {
      id: '2',
      title: 'TypeScript进阶',
      icon: $r('app.media.ts_icon'),
      description: '深入理解TypeScript的类型系统和高级特性',
      tags: ['编程语言', '类型安全']
    }
  ]

  build() {
    Column() {
      OptimizedListComponent({
        items: this.listItems
      })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

class ListItem {
  id: string = ''
  title: string = ''
  icon: Resource = $r('app.media.default_icon')
  description: string = ''
  tags: string[] = []
}

4.3 组件组合与嵌套

// 基础布局组件
@Component
struct CardLayout {
  @Prop title: string = ''
  @Prop subtitle: string = ''
  @Prop content: any

  build() {
    Column({ space: 0 }) {
      // 头部
      if (this.title || this.subtitle) {
        this.buildHeader()
      }
      
      // 内容区域
      this.buildContent()
    }
    .width('100%')
    .backgroundColor(Color.White)
    .borderRadius(12)
    .shadow({ radius: 2, color: '#10000000', offsetX: 0, offsetY: 1 })
  }

  @Builder
  buildHeader() {
    Column({ space: 4 }) {
      if (this.title) {
        Text(this.title)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
      }
      
      if (this.subtitle) {
        Text(this.subtitle)
          .fontSize(12)
          .fontColor('#666666')
      }
    }
    .width('100%')
    .padding(16)
    .border({ width: 0.5, color: '#EEEEEE' })
  }

  @Builder
  buildContent() {
    Column() {
      this.content()
    }
    .width('100%')
    .padding(16)
  }
}

// 统计卡片组件
@Component
struct StatsCard {
  @Prop title: string = ''
  @Prop value: number = 0
  @Prop unit: string = ''
  @Prop trend: 'up' | 'down' | 'neutral' = 'neutral'
  @Prop trendValue: number = 0

  build() {
    CardLayout({
      title: this.title,
      content: () => {
        this.buildStatsContent()
      }
    })
  }

  @Builder
  buildStatsContent() {
    Column({ space: 8 }) {
      Row({ space: 8 }) {
        Text(this.value.toString())
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        
        if (this.unit) {
          Text(this.unit)
            .fontSize(14)
            .fontColor('#666666')
            .align(Alignment.Bottom)
        }
      }
      
      if (this.trend !== 'neutral') {
        Row({ space: 4 }) {
          Image(this.trend === 'up' ? 
            $r('app.media.trend_up') : $r('app.media.trend_down'))
            .width(12)
            .height(12)
          
          Text(`${this.trendValue}%`)
            .fontSize(12)
            .fontColor(this.trend === 'up' ? '#34C759' : '#FF3B30')
        }
      }
    }
    .width('100%')
  }
}

// 图表卡片组件
@Component
struct ChartCard {
  @Prop title: string = ''
  @Prop data: number[] = []
  @Prop maxValue: number = 100

  build() {
    CardLayout({
      title: this.title,
      content: () => {
        this.buildChartContent()
      }
    })
  }

  @Builder
  buildChartContent() {
    Column({ space: 12 }) {
      // 简易柱状图
      Row({ space: 4 }) {
        ForEach(this.data, (value: number, index: number) => {
          Column({ space: 0 }) {
            // 柱状条
            Column()
              .width(20)
              .height((value / this.maxValue) * 60)
              .backgroundColor('#007DFF')
              .borderRadius(4)
            
            // 标签
            Text((index + 1).toString())
              .fontSize(10)
              .fontColor('#666666')
          }
          .height(80)
          .justifyContent(FlexAlign.End)
        })
      }
      .height(80)
      
      // 数据标签
      Row({ space: 8 }) {
        ForEach(this.data, (value: number, index: number) => {
          Text(value.toString())
            .fontSize(10)
            .fontColor('#666666')
            .width(20)
            .textAlign(TextAlign.Center)
        })
      }
    }
    .width('100%')
  }
}

// 仪表板组件 - 组合使用多个卡片组件
@Component
struct DashboardComponent {
  @State statsData = {
    users: 1245,
    revenue: 89456,
    conversion: 12.3
  }

  @State chartData = [45, 78, 56, 89, 67, 92, 75]

  build() {
    Column({ space: 16 }) {
      Text('数据仪表板')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      // 统计卡片行
      Row({ space: 16 }) {
        StatsCard({
          title: '用户数量',
          value: this.statsData.users,
          trend: 'up',
          trendValue: 12
        })
        .layoutWeight(1)
        
        StatsCard({
          title: '收入',
          value: this.statsData.revenue,
          unit: '元',
          trend: 'up',
          trendValue: 8
        })
        .layoutWeight(1)
        
        StatsCard({
          title: '转化率',
          value: this.statsData.conversion,
          unit: '%',
          trend: 'down',
          trendValue: 2
        })
        .layoutWeight(1)
      }
      .width('100%')
      .height(120)
      
      // 图表卡片
      ChartCard({
        title: '每周数据趋势',
        data: this.chartData,
        maxValue: 100
      })
      
      // 操作按钮
      Button('刷新数据')
        .width('100%')
        .height(44)
        .backgroundColor('#007DFF')
        .fontColor(Color.White)
        .onClick(() => {
          this.refreshData()
        })
    }
    .width('100%')
    .padding(16)
  }

  refreshData() {
    // 模拟数据刷新
    this.statsData = {
      users: Math.floor(Math.random() * 2000),
      revenue: Math.floor(Math.random() * 100000),
      conversion: Math.random() * 20
    }
    
    this.chartData = Array(7).fill(0).map(() => 
      Math.floor(Math.random() * 100)
    )
  }
}

五、主题化与样式管理

5.1 主题感知组件

// 主题配置接口
interface ThemeConfig {
  primaryColor: string
  secondaryColor: string
  backgroundColor: string
  textColor: string
  borderRadius: number
  spacing: number
}

// 默认主题
const defaultTheme: ThemeConfig = {
  primaryColor: '#007DFF',
  secondaryColor: '#34C759',
  backgroundColor: '#FFFFFF',
  textColor: '#333333',
  borderRadius: 8,
  spacing: 16
}

// 暗色主题
const darkTheme: ThemeConfig = {
  primaryColor: '#0A84FF',
  secondaryColor: '#30D158',
  backgroundColor: '#1C1C1E',
  textColor: '#FFFFFF',
  borderRadius: 8,
  spacing: 16
}

// 主题上下文组件
@Component
struct ThemeProvider {
  @Prop theme: ThemeConfig = defaultTheme
  @Prop content: any

  build() {
    Column() {
      this.content()
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.theme.backgroundColor)
  }
}

// 主题感知按钮组件
@Component
struct ThemedButton {
  @Prop text: string = ''
  @Prop type: 'primary' | 'secondary' | 'outline' = 'primary'
  @Prop disabled: boolean = false
  @Prop onPress: () => void = () => {}

  @Consume theme: ThemeConfig

  build() {
    Button(this.text)
      .width('100%')
      .height(44)
      .backgroundColor(this.getBackgroundColor())
      .fontColor(this.getTextColor())
      .fontSize(16)
      .borderRadius(this.theme.borderRadius)
      .border({
        width: this.type === 'outline' ? 1 : 0,
        color: this.getBorderColor()
      })
      .enabled(!this.disabled)
      .opacity(this.disabled ? 0.6 : 1.0)
      .onClick(() => {
        this.onPress()
      })
  }

  private getBackgroundColor(): string {
    if (this.disabled) return '#CCCCCC'
    switch (this.type) {
      case 'primary': return this.theme.primaryColor
      case 'secondary': return this.theme.secondaryColor
      case 'outline': return 'transparent'
      default: return this.theme.primaryColor
    }
  }

  private getTextColor(): string {
    if (this.disabled) return '#666666'
    switch (this.type) {
      case 'outline': return this.theme.primaryColor
      default: return '#FFFFFF'
    }
  }

  private getBorderColor(): string {
    if (this.disabled) return '#CCCCCC'
    return this.theme.primaryColor
  }
}

// 主题切换演示
@Component
struct ThemeDemo {
  @State currentTheme: ThemeConfig = defaultTheme
  @State isDarkMode: boolean = false

  build() {
    ThemeProvider({
      theme: this.currentTheme,
      content: () => {
        this.buildContent()
      }
    })
  }

  @Builder
  buildContent() {
    Column({ space: this.currentTheme.spacing }) {
      Text('主题化组件演示')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.currentTheme.textColor)
      
      // 按钮示例
      ThemedButton({
        text: '主要按钮',
        type: 'primary',
        onPress: () => {
          console.log('主要按钮被点击')
        }
      })
      
      ThemedButton({
        text: '次要按钮',
        type: 'secondary',
        onPress: () => {
          console.log('次要按钮被点击')
        }
      })
      
      ThemedButton({
        text: '轮廓按钮',
        type: 'outline',
        onPress: () => {
          console.log('轮廓按钮被点击')
        }
      })
      
      // 主题切换按钮
      ThemedButton({
        text: this.isDarkMode ? '切换至亮色主题' : '切换至暗色主题',
        type: 'outline',
        onPress: () => {
          this.toggleTheme()
        }
      })
      
      // 主题信息
      Text(`当前主题: ${this.isDarkMode ? '暗色' : '亮色'}`)
        .fontSize(14)
        .fontColor(this.currentTheme.textColor)
    }
    .width('100%')
    .padding(this.currentTheme.spacing)
  }

  toggleTheme() {
    this.isDarkMode = !this.isDarkMode
    this.currentTheme = this.isDarkMode ? darkTheme : defaultTheme
  }
}

5.2 样式工具函数

// 样式工具类
class StyleUtils {
  // 间距工具
  static spacing = {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 32
  }

  // 颜色工具
  static colors = {
    primary: '#007DFF',
    success: '#34C759',
    warning: '#FF9500',
    danger: '#FF3B30',
    info: '#5856D6',
    light: '#F2F2F7',
    dark: '#1C1C1E'
  }

  // 文字样式
  static textStyles = {
    h1: { fontSize: 24, fontWeight: FontWeight.Bold },
    h2: { fontSize: 20, fontWeight: FontWeight.Bold },
    h3: { fontSize: 18, fontWeight: FontWeight.Medium },
    body: { fontSize: 16 },
    caption: { fontSize: 14, fontColor: '#666666' }
  }

  // 阴影工具
  static shadow = (elevation: number) => {
    return {
      radius: elevation * 0.5,
      color: `#${Math.floor(elevation * 10).toString(16).padStart(2, '0')}000000`,
      offsetX: 0,
      offsetY: elevation * 0.25
    }
  }

  // 边框工具
  static border = (color: string = '#EEEEEE', width: number = 1) => {
    return { width, color }
  }
}

// 使用样式工具的组件
@Component
struct StyledComponent {
  @Prop title: string = ''
  @Prop content: string = ''
  @Prop type: 'info' | 'success' | 'warning' | 'error' = 'info'

  build() {
    Column({ space: StyleUtils.spacing.md }) {
      // 标题
      Text(this.title)
        .fontSize(StyleUtils.textStyles.h2.fontSize)
        .fontWeight(StyleUtils.textStyles.h2.fontWeight)
        .fontColor(this.getTitleColor())
      
      // 内容
      Text(this.content)
        .fontSize(StyleUtils.textStyles.body.fontSize)
        .fontColor(StyleUtils.colors.dark)
        .textAlign(TextAlign.Start)
      
      // 装饰线
      Row()
        .width('100%')
        .height(2)
        .backgroundColor(this.getAccentColor())
    }
    .width('100%')
    .padding(StyleUtils.spacing.lg)
    .backgroundColor(Color.White)
    .border(StyleUtils.border(this.getBorderColor()))
    .borderRadius(12)
    .shadow(StyleUtils.shadow(2))
  }

  private getTitleColor(): string {
    switch (this.type) {
      case 'success': return StyleUtils.colors.success
      case 'warning': return StyleUtils.colors.warning
      case 'error': return StyleUtils.colors.danger
      default: return StyleUtils.colors.primary
    }
  }

  private getAccentColor(): string {
    switch (this.type) {
      case 'success': return StyleUtils.colors.success
      case 'warning': return StyleUtils.colors.warning
      case 'error': return StyleUtils.colors.danger
      default: return StyleUtils.colors.primary
    }
  }

  private getBorderColor(): string {
    switch (this.type) {
      case 'success': return StyleUtils.colors.success + '40'
      case 'warning': return StyleUtils.colors.warning + '40'
      case 'error': return StyleUtils.colors.danger + '40'
      default: return StyleUtils.colors.primary + '40'
    }
  }
}

// 样式工具使用示例
@Component
struct StyleUtilsDemo {
  build() {
    Column({ space: StyleUtils.spacing.lg }) {
      StyledComponent({
        title: '信息提示',
        content: '这是一个普通的信息提示组件',
        type: 'info'
      })
      
      StyledComponent({
        title: '成功提示',
        content: '操作成功完成!',
        type: 'success'
      })
      
      StyledComponent({
        title: '警告提示',
        content: '请注意操作风险',
        type: 'warning'
      })
      
      StyledComponent({
        title: '错误提示',
        content: '操作失败,请重试',
        type: 'error'
      })
    }
    .width('100%')
    .padding(StyleUtils.spacing.xl)
    .backgroundColor(StyleUtils.colors.light)
  }
}

六、组件测试与文档

6.1 组件单元测试

// 组件测试示例
@Component
struct ComponentTestHarness {
  build() {
    Column({ space: 20 }) {
      // 测试自定义按钮
      CustomButton({
        buttonText: '测试按钮',
        backgroundColor: '#007DFF',
        disabled: false
      })
      
      // 测试主题组件
      ThemedButton({
        text: '主题按钮',
        type: 'primary'
      })
    }
    .width('100%')
    .padding(20)
  }
}

// 单元测试文件
/*
describe('CustomButton Tests', () => {
  it('test_button_creation', 0, () => {
    const button = new CustomButton()
    expect(button != null).assertTrue()
  })
  
  it('test_button_props', 0, () => {
    const button = new CustomButton()
    button.buttonText = '测试'
    expect(button.buttonText).assertEqual('测试')
  })
})

describe('ThemedButton Tests', () => {
  it('test_themed_button_colors', 0, () => {
    const button = new ThemedButton()
    button.type = 'primary'
    expect(button.getBackgroundColor()).assertEqual('#007DFF')
  })
})
*/

6.2 组件文档生成

// 组件文档注释示例
/**
 * 自定义按钮组件
 * 
 * @component
 * @name CustomButton
 * @description 一个可定制的按钮组件,支持多种样式和状态
 * 
 * @prop {string} buttonText - 按钮显示文本
 * @prop {string} backgroundColor - 按钮背景颜色
 * @prop {string} textColor - 按钮文字颜色
 * @prop {boolean} disabled - 是否禁用按钮
 * 
 * @example
 * // 基本用法
 * CustomButton({ buttonText: '点击我' })
 * 
 * // 自定义样式
 * CustomButton({
 *   buttonText: '自定义按钮',
 *   backgroundColor: '#FF0000',
 *   textColor: '#FFFFFF'
 * })
 */
@Component
struct DocumentedButton {
  @Prop buttonText: string = '按钮'
  @Prop backgroundColor: string = '#007DFF'
  @Prop textColor: string = '#FFFFFF'
  @Prop disabled: boolean = false

  build() {
    Button(this.buttonText)
      .width(120)
      .height(40)
      .backgroundColor(this.disabled ? '#CCCCCC' : this.backgroundColor)
      .fontColor(this.disabled ? '#666666' : this.textColor)
      .enabled(!this.disabled)
  }
}

// 组件使用示例文档
@Component
struct ComponentDocumentation {
  build() {
    Column({ space: 20 }) {
      Text('组件文档示例')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      // 组件使用示例
      DocumentedButton({
        buttonText: '文档化按钮',
        backgroundColor: '#5856D6'
      })
      
      // 参数说明表格
      this.buildPropsTable()
    }
    .width('100%')
    .padding(20)
  }

  @Builder
  buildPropsTable() {
    Column({ space: 10 }) {
      Text('组件参数说明').fontSize(16).fontWeight(FontWeight.Medium)
      
      // 参数表格
      Table() {
        TableRow() {
          Text('参数名').fontWeight(FontWeight.Bold)
          Text('类型').fontWeight(FontWeight.Bold)
          Text('默认值').fontWeight(FontWeight.Bold)
          Text('说明').fontWeight(FontWeight.Bold)
        }
        
        TableRow() {
          Text('buttonText')
          Text('string')
          Text('"按钮"')
          Text('按钮显示文本')
        }
        
        TableRow() {
          Text('backgroundColor')
          Text('string')
          Text('"#007DFF"')
          Text('按钮背景颜色')
        }
        
        TableRow() {
          Text('textColor')
          Text('string')
          Text('"#FFFFFF"')
          Text('按钮文字颜色')
        }
        
        TableRow() {
          Text('disabled')
          Text('boolean')
          Text('false')
          Text('是否禁用按钮')
        }
      }
      .width('100%')
      .border({ width: 1, color: '#EEEEEE' })
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#F8F9FA')
    .borderRadius(8)
  }
}

总结

通过本文的深入学习,你已经掌握了HarmonyOS自定义组件开发的核心技能:

  1. 组件基础:创建和使用自定义组件,理解@Prop参数传递
  2. 事件通信:组件间的事件传递和双向数据绑定
  3. 复杂组件:开发卡片、表单等复杂UI组件
  4. 性能优化:使用@Builder和条件渲染优化组件性能
  5. 组件组合:通过组合简单组件构建复杂界面
  6. 主题化设计:创建支持主题切换的组件
  7. 样式管理:使用工具函数统一管理样式
  8. 测试文档:编写组件测试和文档

自定义组件是构建可维护、可复用应用的基础,熟练掌握这些技能将极大提升你的开发效率和应用质量。

需要参加鸿蒙认证的请点击 鸿蒙认证链接

posted @ 2025-10-30 10:55  ifeng918  阅读(10)  评论(0)    收藏  举报