HarmonyOS 「校园二手交易商城」App应用实战 46 直播商品列表:LiveShopList 与带货商品
46 直播商品列表:LiveShopList 与带货商品
系列五:直播与画中画 · 第 6 篇
对应工程:
features/multishoppinglive/src/main/ets/components/LiveShopList.ets
引言
直播间的终极目标是卖货。LiveShopList 承载"正在讲解的这件商品":主播讲到哪件,列表就高亮哪件,用户点击可加购/看详情。它比普通商品列表多了一个特殊形态——侧栏列表:平板/PC 大屏时商品列表竖排在视频右侧,点击任意商品通知父级打开购物袋。同一个组件通过 isSideListLayout 参数在"底部横条"与"右侧竖栏"两种形态间切换,是断点驱动布局的又一范例。
商品模型:LiveProduct
带货商品是轻量模型,三个字段走起:
export class LiveProduct {
public name: string;
public image: Resource;
public price: string;
constructor(name: string, image: Resource, price: string) {
this.name = name;
this.image = image;
this.price = price;
}
getName(): string { return this.name; }
getImage(): Resource { return this.image; }
getPrice(): string { return this.price; }
}数据由 LiveShopListModel.buildProductList(context) 拼装:8 件商品的名称(LIVE_SHOP_NAME_1-8)、图片(live_product_1-8.png)、价格(LIVE_SHOP_PRICE_1-8)三组资源按索引一一配对:
static buildProductList(context: Context): LiveProduct[] {
const names = LiveShopListModel.getProductNames(context);
const imgs = LiveShopListModel.getProductResources();
const prices = LiveShopListModel.getProductPrices(context);
const list: LiveProduct[] = [];
for (let i = 0; i < names.length; i++) {
list.push(new LiveProduct(names[i], imgs[i], prices[i]));
}
return list;
}组件内部用 @Local 持有列表与选中索引:
@Local private currentProductIndex: number = 0;
@Local private liveProductList: LiveProduct[] = [];布局适配:横条与侧栏两种形态
aboutToAppear 时按形态准备数据——侧栏形态下数据反转、选中最后一件(对应"当前讲解商品"在侧栏底部最显眼):
aboutToAppear(): void {
this.applyLayout();
}
private applyLayout(): void {
const all = LiveShopListModel.buildProductList(this.context);
if (this.isSideListLayout) {
this.liveProductList = [...all].reverse(); // 侧栏:最新商品放最底
this.currentProductIndex = this.liveProductList.length - 1;
} else {
this.liveProductList = all; // 横条:从第一件开始
this.currentProductIndex = 0;
}
this.scroller.scrollToIndex(this.currentProductIndex);
}两种形态的差异贯穿整个 build:
| 维度 | 底部横条(isSideListLayout=false) | 右侧竖栏(=true) |
| 方向 | listDirection(Axis.Horizontal) | Axis.Vertical |
| 高度/宽度 | 高 60、宽 100% | 宽 216、高 auto |
| 商品卡片 | 60×60,选中展开 180 | 72×72,选中展开 216 |
| 列表对齐 | ListItemAlign.Start | ListItemAlign.End |
| 点击行为 | 小屏无动作 | 通知父级开购物袋 |
选中卡片"展开"是直播间的经典交互:未选中只显示商品小图,选中的那张横向/纵向展开露出名称与价格。
商品卡片与选中态
卡片是一个 Row,内部是"商品图 + (选中时才显示)名称/价格":
ListItem() {
Row() {
Image(item.getImage())
.width(this.isSideListLayout ? 72 : 60)
.aspectRatio(1)
.borderRadius(8)
Column() {
Text(item.getName()).fontSize(14).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.getPrice()).fontSize(14).fontColor('#CF0A2C')
}
.visibility(index === this.currentProductIndex ? Visibility.Visible : Visibility.None)
.layoutWeight(1)
}
.borderRadius(8)
.backgroundColor($r('app.color.surface_primary'))
.width(index === this.currentProductIndex ?
(this.isSideListLayout ? 216 : 180) : (this.isSideListLayout ? 72 : 60))
.height(this.isSideListLayout ? 72 : 60)
.onClick(() => {
if (!this.isOnlySmBreakpoint()) {
this.onOpenProductDetail();
}
})
}细节拆解:
- 名称/价格列
visibility控制:只有index === currentProductIndex的卡片显示文字列,未选中卡片是纯图,紧凑不遮挡视频。 - 宽度动画感:选中卡片宽 216/180,未选中 72/60,宽度变化由布局自然过渡(演示工程未加
animation,真实项目可补.animation({ duration: 200 })获得展开动画)。 - 价格色
#CF0A2C:全项目统一的品牌红,与评论角标、购物袋按钮呼应。
点击进入详情与加购:onOpenProductDetail
大屏形态下点击商品卡片触发 onOpenProductDetail,由父级决定后续动作。在 LiveBroadCastPage 中它被接到"打开购物袋":
LiveShopList({
isSideListLayout: true,
onOpenProductDetail: () => {
this.panelType = 'ShopBag';
this.isMoreDetail = true;
this.isDialogOpen = true;
}
})而 LiveBroadCast 容器里传给两个形态的回调都统一是 onOpenShopBag——也就是说"点商品"与"点购物袋按钮"最终都打开购物袋(第 8 篇详述购物袋面板)。真实项目里这里应替换为 pushPath({ name: 'ProductDetail', param: { productId } }) 或打开半屏商品详情。
isOnlySmBreakpoint() 做了小屏降级:XS/SM 横条形态点击不响应(手机直播页以观看为主,点商品进详情容易误触)。
与视频区的叠加布局
LiveBroadCast 用 Stack 把两种形态的商品列表叠在视频上,用断点 + 高度断点控制显隐:
Column() { // 底层横条:小屏显示
LiveComment({ ... })
LiveShopList({ isSideListLayout: false, ... })
.visibility(this.breakpoint.heightBreakpoint === HeightBreakpoint.HEIGHT_MD &&
this.breakpoint.widthBreakpoint === WidthBreakpoint.WIDTH_SM ? Visibility.None :
this.isSmOrMd() ? Visibility.Visible : Visibility.None)
}
Row() { // 侧栏:大屏显示
LiveShopList({ isSideListLayout: true, ... })
}
.visibility(this.breakpoint.heightBreakpoint === HeightBreakpoint.HEIGHT_MD ?
Visibility.None : this.isSmOrMd() ? Visibility.None : Visibility.Visible)- 小屏(XS/SM/MD):显示底部横条;其中"SM 宽 + MD 高"(折叠屏展开)特殊组合下横条也隐藏,给视频让空间。
- 大屏(LG/XL):显示右侧竖栏(
FlexAlign.End + VerticalAlign.Bottom靠右靠下)。 - 两种形态永远不会同时出现,互斥关系由两个
visibility表达式保证。
注意事项与总结
- 同一组件两种形态:
isSideListLayout贯穿方向、尺寸、对齐、点击行为,比复制两个组件更易维护,是"参数化形态"的范式。 - 选中态驱动展开:
currentProductIndex是唯一选中源,卡片宽高、文字显隐、滚动定位都以它为准;主播切品时只改这一个值。 - 点击行为按断点降级:小屏横条不响应点击,避免误触;真实项目可改为小屏点击也开半屏详情。
- 反转数据适配侧栏:侧栏"最新商品在最底",用
[...all].reverse()不改原数组、不污染横条数据。
LiveHeader 与 LiveHeaderPC。
更多推荐



所有评论(0)