
鸿蒙5自定义组件封装:可复用的商品卡片组件
一、组件设计目标
在鸿蒙5应用开发中,商品卡片是电商类应用的核心UI元素。本组件将封装一个可复用的商品卡片组件,具备以下特性:
支持多种布局模式(列表/网格)
响应式布局适应不同设备
支持主题切换(日间/夜间模式)
支持加载动画和骨架屏
内置点击动画效果
二、组件参数设计
// ProductCardTypes.ets
export interface ProductInfo {
id: string;
image: ResourceStr; // 商品图片
title: string; // 商品标题
description: string; // 商品描述
price: number; // 当前价格
originalPrice?: number; // 原价(可选)
discount?: number; // 折扣率(可选)
rating?: number; // 评分(0-5)
isFavorite?: boolean; // 是否收藏
}
export interface ProductCardConfig {
layout: ‘list’ | ‘grid’; // 布局模式
theme: ‘light’ | ‘dark’; // 主题模式
showSkeleton: boolean; // 是否显示骨架屏
}
三、商品卡片组件完整实现
// ProductCard.ets
import { ProductInfo, ProductCardConfig } from ‘./ProductCardTypes’;
@Component
export default struct ProductCard {
// 组件配置参数
@State config: ProductCardConfig = {
layout: ‘grid’,
theme: ‘light’,
showSkeleton: true
};
// 商品数据
@Prop productInfo: ProductInfo;
// 全局主题上下文
@StorageLink(‘appTheme’) appTheme: ‘light’ | ‘dark’ = ‘light’;
// 收藏点击事件
@Emit favoriteChange: (productId: string, isFavorite: boolean) => void;
// 卡片点击事件
@Emit cardClick: (productId: string) => void;
// 构建加载骨架屏
buildSkeleton() {
Column() {
Stack() {
Rect().width(‘100%’).height(200).fill(‘#E0E0E0’)
Column({ space: 8 }) {
Rect().width('60%').height(16).fill('#EEEEEE')
Rect().width('40%').height(16).fill('#EEEEEE')
Rect().width('30%').height(14).fill('#EEEEEE')
Row({ space: 12 }) {
Rect().width(80).height(14).fill('#EEEEEE')
Rect().width(60).height(14).fill('#EEEEEE')
}
}
.padding(12)
.alignItems(HorizontalAlign.Start)
.position({ x: 0, y: 200 })
}
.width('100%')
}
.opacity(0.8)
.animation({
duration: 1000,
curve: Curve.EaseInOut,
iterations: -1, // 无限循环
playMode: PlayMode.AlternateReverse
})
}
// 构建实际卡片内容
buildCardContent() {
Column() {
// 商品图片区域
Stack() {
// 商品主图
Image(this.productInfo.image)
.objectFit(ImageFit.Cover)
.width(‘100%’)
.aspectRatio(1)
// 折扣标签
if (this.productInfo.discount && this.productInfo.discount > 0) {
Row() {
Text($r('app.string.discount', this.productInfo.discount))
.fontSize(10)
.fontColor('#FFFFFF')
}
.padding(4)
.borderRadius({
topLeft: 4,
bottomRight: 4
})
.backgroundColor('#FF2157')
.position({ x: 0, y: 0 })
}
// 收藏按钮
Button() {
Image($r(this.productInfo.isFavorite ?
'app.media.ic_favorite_filled' :
'app.media.ic_favorite'))
.width(20)
.height(20)
}
.type(ButtonType.Icon)
.backgroundColor('#00000033') // 半透背景
.width(36)
.height(36)
.borderRadius(18)
.onClick(() => {
// 触发收藏状态变化
this.favoriteChange(this.productInfo.id, !this.productInfo.isFavorite);
// 点击动画
animateTo({
duration: 300,
curve: Curve.Friction
}, () => {
// 动画效果
});
})
.position({ x: '80%', y: '90%' })
}
// 商品信息区域
Column({ space: 4 }) {
// 商品标题
Text(this.productInfo.title)
.fontSize(16)
.fontWeight(500)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 商品描述
Text(this.productInfo.description)
.fontSize(12)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(2)
.fontColor(this.appTheme === 'light' ? '#666666' : '#AAAAAA')
// 价格区域
Row({ space: 8 }) {
// 当前价格
Text(`¥${this.productInfo.price.toFixed(2)}`)
.fontColor('#FF2157')
.fontSize(18)
.fontWeight(FontWeight.Bold)
// 原价(带删除线)
if (this.productInfo.originalPrice &&
this.productInfo.originalPrice > this.productInfo.price) {
Text(`¥${this.productInfo.originalPrice.toFixed(2)}`)
.fontColor('#999999')
.fontSize(12)
.decoration({ type: TextDecorationType.LineThrough })
}
Blank()
}
.layoutWeight(1)
.height(36)
.alignItems(VerticalAlign.Center)
// 评分区域
if (this.productInfo.rating) {
Row() {
ForEach(Array.from({length: 5}), (_, index) => {
Image($r(index < Math.floor(this.productInfo.rating) ?
'app.media.ic_star_filled' :
'app.media.ic_star'))
.width(16)
.height(16)
})
Text(`(${this.productInfo.rating.toFixed(1)})`)
.fontSize(12)
.margin({ left: 4 })
}
}
}
.padding(12)
}
.backgroundColor(this.appTheme === 'light' ? '#FFFFFF' : '#1E1E1E')
.borderRadius(12)
.shadow({
radius: this.appTheme === 'light' ? 4 : 0,
color: this.appTheme === 'light' ? '#00000010' : '#000000',
offsetX: 1,
offsetY: 2
})
}
build() {
Column() {
if (this.config.showSkeleton) {
this.buildSkeleton();
} else {
this.buildCardContent();
}
}
.onClick(() => {
// 点击动画效果
animateTo({
duration: 200,
curve: Curve.EaseOut
}, () => {
// 缩放动画
});
// 触发点击事件
this.cardClick(this.productInfo.id);
})
}
}
四、组件使用示例
// HomePage.ets
import ProductCard from ‘…/components/ProductCard’;
@Entry
@Component
struct HomePage {
// 示例商品数据
@State productList: Array<ProductInfo> = [
{
id: ‘p001’,
image: $r(‘app.media.product1’),
title: ‘华为Mate 60 Pro’,
description: ‘麒麟9000s芯片 昆仑玻璃’,
price: 6999,
originalPrice: 7999,
discount: 12,
rating: 4.8,
isFavorite: false
},
{
id: ‘p002’,
image: $r(‘app.media.product2’),
title: ‘华为Watch 4’,
description: ‘智能健康手表’,
price: 2699,
originalPrice: 2999,
discount: 10,
rating: 4.7,
isFavorite: true
}
];
// 收藏状态切换处理
handleFavorite(productId: string, isFavorite: boolean) {
// 在实际应用中这里应该调用API更新状态
let index = this.productList.findIndex(p => p.id === productId);
if (index !== -1) {
this.productList[index].isFavorite = isFavorite;
}
}
build() {
Column() {
// 列表布局
List({ space: 20 }) {
ForEach(this.productList, (item: ProductInfo) => {
ListItem() {
ProductCard({
productInfo: item,
config: {
layout: ‘list’,
theme: ‘light’,
showSkeleton: false
},
favoriteChange: this.handleFavorite.bind(this),
cardClick: (productId: string) => {
// 跳转到商品详情页
router.push({ url: ‘pages/DetailPage’, params: { id: productId } });
}
})
.margin({ top: 10, bottom: 10 })
}
})
}
.layoutWeight(1)
}
.padding(16)
.backgroundColor(‘#F5F5F5’)
}
}
五、组件的优势与特性
响应式设计
@StorageLink(‘appTheme’) appTheme: ‘light’ | ‘dark’ = ‘light’;
自动适配日间/夜间模式,颜色和阴影动态调整
交互体验优化
.onClick(() => {
animateTo({…}, () => { /* 动画 */ });
this.cardClick(this.productInfo.id);
})
内置点击动画,提升用户体验
性能优化
使用 @Prop 确保数据单向流动
ForEach 渲染提高列表性能
骨架屏优化加载体验
可扩展性
通过 ProductCardConfig 配置不同布局模式
自定义事件系统(@Emit)
支持国际化资源引用
自适应布局
layout: ‘list’ | ‘grid’
组件自动根据布局模式调整UI结构和间距
六、组件使用建议
数据加载最佳实践
// 加载数据时显示骨架屏
this.config.showSkeleton = true;
fetchProducts().then(data => {
this.productList = data;
this.config.showSkeleton = false;
});
主题切换处理
// 监听系统主题变化
themeManager.on(‘themeChange’, (theme) => {
this.appTheme = theme;
});
内存优化
aboutToDisappear() {
// 释放资源引用
this.productList.forEach(item => {
releaseResource(item.image);
});
}
这个可复用的商品卡片组件封装了电商应用的常见UI需求,遵循鸿蒙5的设计规范,可无缝集成到各类电商应用中。通过参数化配置,开发者可以轻松实现不同布局风格的商品展示需求。
