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自定义组件开发的核心技能:
- 组件基础:创建和使用自定义组件,理解@Prop参数传递
- 事件通信:组件间的事件传递和双向数据绑定
- 复杂组件:开发卡片、表单等复杂UI组件
- 性能优化:使用@Builder和条件渲染优化组件性能
- 组件组合:通过组合简单组件构建复杂界面
- 主题化设计:创建支持主题切换的组件
- 样式管理:使用工具函数统一管理样式
- 测试文档:编写组件测试和文档
自定义组件是构建可维护、可复用应用的基础,熟练掌握这些技能将极大提升你的开发效率和应用质量。
需要参加鸿蒙认证的请点击 鸿蒙认证链接

浙公网安备 33010602011771号