HarmonyOS 「校园二手交易商城」App应用实战 48:直播与购物袋联动
48 直播与购物袋联动
系列五:直播与画中画 · 第 8 篇
对应工程:features/multishoppinglive+features/multishoppingpay(LiveUtil.ets、LiveBroadCastPage.ets、ShopBag.ets、PayCard.ets)
引言
直播带货的闭环是"看 → 点 → 买"。前几篇把"看"做完了,这篇打通"点"和"买":直播页底部购物袋入口 LiveUtil → 打开购物袋面板 ShopBag → 展示直播商品(live_bag_product_1-4)→ 点击「立即购买」拉起支付卡 PayCard。整个联动横跨 multishoppinglive 与 multishoppingpay 两个特性模块,靠 LiveBroadCastPage 的 @Event 回调和 panelType 状态机串成一条完整的购买链路。
入口:LiveUtil 的购物袋按钮
LiveUtil 是直播页底部操作栏(评论输入 + 分享/点赞/购物袋)。购物袋按钮是红色圆钮,点击通过 @Event 上报父级:
Row() {
SymbolGlyph($r('sys.symbol.bag_fill'))
.fontSize(24)
.fontColor([$r('sys.color.font_on_primary')])
}
.height(40)
.width(40)
.borderRadius(20)
.backgroundColor('#CF0A2C')
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.onOpenShopBag();
})
@Event public onOpenShopBag: () => void 是 V2 组件的事件出口:组件只负责"我点了购物袋",至于打开面板还是跳页面,由父级决定。LiveBroadCast 把它一路转发,最终 LiveBroadCastPage 接住并打开面板。
打开购物袋:panelType 状态机
LiveBroadCastPage 用三个 @Local 字段管理面板状态:
@Local public isDialogOpen: boolean = false; // 面板是否显示
@Local public isMoreDetail: boolean = true; // 是否展开更多详情
@Local public panelType: string = 'ShopBag'; // 当前面板类型
购物袋入口统一走到同一段逻辑:
LiveBroadCast({
onOpenShopBag: () => {
this.panelType = 'ShopBag';
this.isMoreDetail = true;
this.isDialogOpen = true;
}
})
这里做了三件事:切换到"购物袋"面板、重置为"更多详情"态、打开面板。panelType 是面板内容的状态机——只有两个值:'ShopBag'(购物袋)与 'detailPayCard'(支付卡)。面板内容区按 panelType 用 visibility 互斥显示:
Column() {
ShopBag({ ... })
.visibility(this.panelType === 'ShopBag' ? Visibility.Visible : Visibility.None)
PayCard({ ... })
.visibility(this.panelType === 'detailPayCard' ? Visibility.Visible : Visibility.None)
}
面板呈现:bindSheet 与侧栏
同样的面板内容,在不同断点下用不同容器承载:
- 小屏(XS/SM):
Column上挂bindSheet($$this.isDialogOpen, this.panelContent(), { height: '620vp', preferType: SheetType.CENTER, ... }),620vp 的半屏居中弹窗,$$双向绑定让面板收起时自动同步isDialogOpen = false。 - 大屏(MD/LG/XL):
if (this.isDialogOpen) { this.sidePanel() },把面板作为右侧栏(宽 37.5%)直接嵌入页面,左侧直播舞台压缩到 62.5%。
小屏的 bindSheet 还配了 enableOutsideInteractive: false(禁止点击外部交互,保证直播画面不被误操作)与 showClose: false(用面板内部的关闭按钮)。
购物袋列表:ShopBag 与 ShopList
ShopBag 是购物袋容器:头部 ShopBagHeader + Tabs 分类 + ShopList 商品列表。
头部图标来自 ShopBagModel.getHeaderIcons,正是资源文件 icon_bag.svg(购物袋)、icon_search.svg(搜索)、icon_order.svg(订单):
static getHeaderIcons(context: Context): ShopBagHeaderIconConfig[] {
return [
{ icon: $r('app.media.icon_bag'), name: ... },
{ icon: $r('app.media.icon_search'), name: ... },
{ icon: $r('app.media.icon_order'), name: ... }
];
}
ShopList 渲染购物袋商品,图片正是直播专属的 live_bag_product_1-4.png(与直播商品的 live_product_1-8 是两套资源,购物袋是"已加购"的精选集合):
{
image: $r('app.media.live_bag_product_1'),
name: 'iPad Air 5',
remark: g($r('app.string.SHOP_BAG_P1_REMARK')),
discounts: [g($r('app.string.SHOP_BAG_DISCOUNT_POINTS')), g($r('app.string.SHOP_BAG_DISCOUNT_FLAGSHIP'))],
price: 2199
}
每件商品卡片尾部是"立即购买"按钮——它来自 ShopBag 向下注入的 @BuilderParam payCardButton:
@BuilderParam public payCardButton: () => void = this.payCardBuilder;
// ShopList 内
Row() {
Text(this.currencyPrefix + JSON.stringify(item.getPrice())) ...
Blank()
this.payCardButton() // 渲染父级注入的按钮
}
@BuilderParam 让"按钮"成为可注入的插槽:LiveBroadCastPage 注入的正是 payCardButton 构建器——点击后 panelType = 'detailPayCard'、打开支付卡面板:
@Builder
private payCardButton() {
Text($r('app.string.LIVE_PAGE_BUY_NOW'))
.height(28)
.borderRadius(14)
.fontSize(12)
.backgroundColor('#CF0A2C')
.onClick(() => {
this.panelType = 'detailPayCard';
this.isDialogOpen = true;
})
}
购买流程:从直播发起
"立即购买"后链路变为:ShopBag → PayCard。PayCard 是支付卡组件,通过两个 @Event 与页面双向同步:
PayCard({
isOpen: this.isDialogOpen,
detailType: this.panelType,
onIsOpenChange: (value: boolean) => { this.isDialogOpen = value; },
onDetailTypeChange: (value: string) => {
this.panelType = value;
if (value === 'ShopBag') {
this.isDialogOpen = true; // 从支付卡返回购物袋
}
}
})
PayCard 内部用 SelectItem/SelectProjectItem 提供支付方式选择(getPayCardSelectList),头部有"关闭/返回"按钮:detailType === 'detailPayCard' 时关闭面板,否则 onDetailTypeChange('ShopBag') 退回购物袋。至此从直播页发起的"看 → 点 → 买"闭环完成。
注意事项与总结
- 事件只传意图:
onOpenShopBag是纯事件出口,打开什么面板、怎么呈现由页面根据断点决定——小屏弹窗、大屏侧栏,同一事件两套承载。 - panelType 互斥渲染:
ShopBag与PayCard用visibility互斥显示而非条件渲染,两个组件常驻内存、切换零重建;真实项目组件重时可改为if/else。 - BuilderParam 注入按钮:
payCardButton作为插槽从页面注入ShopBag再注入ShopList,按钮的样式与行为可以完全自定义,组件不感知。 - **双向绑定∗∗:‘bindSheet( 双向绑定**:`bindSheet(双向绑定∗∗:‘bindSheet(this.isDialogOpen, …)
让面板关闭事件自动回写状态,不用手动监听onDisappear` 之外的回调。
购买链路在直播页内部闭环:入口按钮 → 购物袋 → 支付卡。接下来两篇回到视频本身:播放控制与异常处理、以及 rawfile 资源管理。
更多推荐



所有评论(0)