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,选中展开 18072×72,选中展开 216
列表对齐ListItemAlign.StartListItemAlign.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 横条形态点击不响应(手机直播页以观看为主,点商品进详情容易误触)。

与视频区的叠加布局

LiveBroadCastStack 把两种形态的商品列表叠在视频上,用断点 + 高度断点控制显隐:

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() 不改原数组、不污染横条数据。
带货商品列表讲完,直播页的三块悬浮层(评论、商品)已就位。下一篇看最上面的"门面":LiveHeaderLiveHeaderPC
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐