diff --git a/api/live.js b/api/live.js new file mode 100644 index 0000000..e577aab --- /dev/null +++ b/api/live.js @@ -0,0 +1,75 @@ +/** + * 直播流相关 API + */ +import { http, Method } from "@/utils/request.js"; + +/** + * 通过 id 获取直播间详情 + * @param {string} id 直播流 ID + */ +export function getLiveRoomById(id) { + return http.request({ + url: `live/room/${id}`, + method: Method.GET, + }); +} + +/** + * 观看的同时注册直播用户 + * @param {string} liveRoomId 直播流 ID + */ +export function registerLiveViewUser(liveRoomId) { + return http.request({ + url: `live/user/view`, + method: Method.POST, + params: { liveRoomId }, + }); +} + +/** + * 发送直播消息 + * @param {object} data LiveMessage + */ +export function sendLiveMessage(data) { + return http.request({ + url: `live/message`, + method: Method.POST, + data, + }); +} + +/** + * 获取直播轮询聚合数据 + * @param {string} id 直播流 ID + */ +export function getLivePollingData(id) { + return http.request({ + url: `live/room/polling/${id}`, + method: Method.GET, + loading: false, + }); +} + +/** + * 领取直播间优惠券 + * @param {string} liveRoomId 直播流 ID + * @param {string} couponId 优惠券 ID + */ +export function receiveLiveCoupon(liveRoomId, couponId) { + return http.request({ + url: `promotion/coupon/receive/live/${liveRoomId}/${couponId}`, + method: Method.GET, + needToken: true, + }); +} + +/** + * 获取直播系统配置(LIVE_SETTING,含 imSdkAppid) + */ +export function getLiveSetting() { + return http.request({ + url: "system/setting/get/LIVE_SETTING", + method: Method.GET, + loading: false, + }); +} diff --git a/api/promotions.js b/api/promotions.js index e7d5eae..69b4455 100644 --- a/api/promotions.js +++ b/api/promotions.js @@ -17,6 +17,23 @@ export function getLiveList(params) { }); } +/** + * 获取直播间详情 + * @deprecated 请使用 api/live.js 中的 getLiveRoomById + * @param {number} roomId 直播间房间号 + */ +export function getLiveDetail(roomId) { + return http.request({ + url: `broadcast/studio`, + method: Method.GET, + params: { + pageNumber: 1, + pageSize: 1, + roomId, + }, + }); +} + /** * 获取当前拼团活动的未成团的会员 */ diff --git a/manifest.json b/manifest.json index 72a36da..8691191 100644 --- a/manifest.json +++ b/manifest.json @@ -208,11 +208,7 @@ } }, "plugins" : {}, - // 直播插件注释 - // "live-player-plugin" : { - // "version" : "1.3.0", - // "provider" : "wx2b03c6e691cd7370" - // } + "libVersion" : "3.5.8", "requiredPrivateInfos" : [ "chooseLocation", "getLocation" ] }, "h5" : { diff --git a/package.json b/package.json index a158889..2f7c476 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ }, "license": "ISC", "dependencies": { + "@tencentcloud/chat": "^3.5.5", + "mqtt": "^5.15.0", "sass": "^1.89.2", "uview-plus": "^3.4.72", "vuex": "^4.1.0" diff --git a/pages.json b/pages.json index 3063f6f..489b660 100644 --- a/pages.json +++ b/pages.json @@ -101,6 +101,13 @@ "style": { "navigationBarTitleText": "专题" } + }, + { + "path": "pages/live/room", + "style": { + "navigationStyle": "custom", + "navigationBarTextStyle": "white" + } } ], diff --git a/pages/live/live-vertical.scss b/pages/live/live-vertical.scss new file mode 100644 index 0000000..39f7bc9 --- /dev/null +++ b/pages/live/live-vertical.scss @@ -0,0 +1,235 @@ +.live-room-page { + width: 100%; + min-height: 100vh; +} + +.live-detail-page-vertical { + width: 100vw; + height: 100vh; + background: #000; + position: relative; + overflow: hidden; +} + +.live-container-vertical { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; +} + +.live-box-vertical { + width: 100%; + height: 100%; + position: relative; + + > video, + > live-player, + > .stream-player { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } +} + +.capsule { + position: absolute; + top: calc(var(--status-bar-height, 44px) + 80rpx); + right: 20rpx; + display: flex; + align-items: center; + background: rgba(0, 0, 0, 0.5); + padding: 12rpx 20rpx; + border-radius: 30rpx; + color: #fff; + font-size: 24rpx; + z-index: 10; +} + +.bottom-container-vertical { + position: absolute; + bottom: 0; + left: 0; + right: 0; + z-index: 10; + padding-bottom: env(safe-area-inset-bottom); + background: linear-gradient(to top, rgba(0, 0, 0, 0.85), transparent); + transition: transform 0.3s ease, opacity 0.3s ease; + + &.hidden { + transform: translateY(100%); + opacity: 0; + pointer-events: none; + } +} + +.bottom-container-vertical .message-container { + padding: 0 24rpx; +} + +.bottom-container-vertical .message-item { + border-bottom: none; + padding: 12rpx 0; +} + +.bottom-container-vertical .username { + color: rgba(255, 255, 255, 0.75); +} + +.bottom-container-vertical .message-text { + color: #fff; +} + +.bottom-container-vertical .cart-btn { + position: absolute; + right: 20rpx; + bottom: 280rpx; + z-index: 20; +} + +.product-showcase { + margin: 0 24rpx 16rpx; + padding: 16rpx; + background: rgba(255, 255, 255, 0.95); + border-radius: 16rpx; + display: flex; + gap: 16rpx; +} + +.product-image { + width: 160rpx; + height: 160rpx; + border-radius: 12rpx; + flex-shrink: 0; +} + +.product-info { + flex: 1; + min-width: 0; +} + +.product-name { + font-size: 26rpx; + color: #333; + font-weight: 600; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.product-tag { + display: inline-block; + background: linear-gradient(135deg, #ff4b2b, #ff416c); + color: #fff; + font-size: 20rpx; + padding: 4rpx 10rpx; + border-radius: 6rpx; + margin-right: 8rpx; +} + +.product-sell-point { + font-size: 22rpx; + color: #999; + margin-top: 8rpx; +} + +.product-price-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 12rpx; +} + +.product-price { + color: #ff4b2b; + font-size: 32rpx; + font-weight: 700; + + &.sold-out { + color: #999; + } +} + +.buy-btn { + padding: 10rpx 24rpx; + border-radius: 28rpx; + font-size: 24rpx; + color: #fff; + background: linear-gradient(135deg, #ff4b2b, #ff416c); + + &.sold-out { + background: #ccc; + } +} + +.bottom-container-vertical .bottom-actions { + background: transparent; + border-top: none; +} + +.bottom-container-vertical .input-box { + background: rgba(255, 255, 255, 0.15); +} + +.bottom-container-vertical .input-content { + color: #fff; +} + +.bottom-popup-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 999; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; + + &.show { + opacity: 1; + visibility: visible; + } +} + +.bottom-popup-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #fff; + border-radius: 24rpx 24rpx 0 0; + max-height: 70vh; + transform: translateY(100%); + transition: transform 0.3s ease; + + &.show { + transform: translateY(0); + } +} + +.popup-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx; + border-bottom: 1rpx solid #eee; +} + +.popup-title { + font-size: 32rpx; + font-weight: 600; + color: #333; +} + +.popup-close { + font-size: 36rpx; + color: #999; +} + +.popup-content { + padding: 20rpx; + max-height: calc(70vh - 100rpx); +} diff --git a/pages/live/live.scss b/pages/live/live.scss new file mode 100644 index 0000000..dd21dbb --- /dev/null +++ b/pages/live/live.scss @@ -0,0 +1,803 @@ +$theme-color: #ff6b35; +$text-color: #333; +$text-color-light: #666; +$text-color-lighter: #999; +$border-color: #eee; + +.flex { + display: flex; +} +.flex-a-c { + align-items: center; +} +.flex-j-sb { + justify-content: space-between; +} + +.live-detail-page { + display: flex; + flex-direction: column; + min-height: 100vh; + background: #fff; +} + +.live-container { + width: 100%; + background: #000; +} + +.live-box { + position: relative; + width: 100%; + overflow: hidden; + + &.layout-horizontal { + height: 420rpx; + } + + &.layout-triple { + height: 560rpx; + } + + > video, + > live-player, + > .stream-player { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } +} + +.live-cover-wrap, +.cover-wrap { + position: absolute; + inset: 0; + z-index: 2; +} + +.live-cover, +.cover-img { + width: 100%; + height: 100%; +} + +.cover-mask { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.45); +} + +.status-tag { + padding: 12rpx 32rpx; + border-radius: 100rpx; + font-size: 28rpx; + color: #fff; + background: rgba(255, 255, 255, 0.2); + margin-bottom: 24rpx; + + &.living { + background: linear-gradient(90deg, #ff6b35, #ff9f28); + } + &.pending { + background: rgba(96, 98, 102, 0.8); + } + &.paused { + background: rgba(255, 159, 40, 0.85); + } + &.ended { + background: rgba(96, 98, 102, 0.6); + } +} + +.tips-text { + font-size: 26rpx; + color: rgba(255, 255, 255, 0.85); + margin-top: 8rpx; +} + +.nav-back { + position: fixed; + top: calc(var(--status-bar-height, 44px) + 12rpx); + left: 24rpx; + z-index: 100; + width: 64rpx; + height: 64rpx; + border-radius: 50%; + background: rgba(0, 0, 0, 0.35); + display: flex; + align-items: center; + justify-content: center; +} + +.bottom-container { + flex: 1; + display: flex; + flex-direction: column; + position: relative; + background: #fff; +} + +.tab-container { + background: #fff; + border-bottom: 1rpx solid $border-color; +} + +.tab-items { + display: flex; +} + +.tab-item { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + height: 88rpx; + font-size: 30rpx; + color: $text-color-light; + position: relative; + + &.active { + color: $theme-color; + font-weight: 600; + + &::after { + content: ""; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 48rpx; + height: 4rpx; + border-radius: 2rpx; + background: $theme-color; + } + } +} + +.message-container { + flex: 1; + overflow: hidden; +} + +.message-scroll, +.live-intro-scroll { + height: 100%; + padding: 0 24rpx; + box-sizing: border-box; +} + +.message-item { + display: flex; + align-items: flex-start; + padding: 20rpx 0; + border-bottom: 1rpx solid #f5f5f5; +} + +.user-avatar { + width: 72rpx; + height: 72rpx; + border-radius: 50%; + flex-shrink: 0; + margin-right: 16rpx; + background: #f0f0f0; +} + +.message-content { + flex: 1; + min-width: 0; +} + +.username { + font-size: 26rpx; + color: $text-color-light; + margin-bottom: 8rpx; + display: block; +} + +.message-text { + font-size: 28rpx; + color: $text-color; + word-break: break-all; + line-height: 1.5; +} + +.floating-right-container { + position: absolute; + right: 24rpx; + bottom: 180rpx; + z-index: 50; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 24rpx; +} + +.floating-recommend-card { + width: 260rpx; + background: rgba(255, 255, 255, 0.95); + border-radius: 20rpx; + padding: 12rpx; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12); +} + +.floating-recommend-img-wrap { + width: 100%; + height: 236rpx; + border-radius: 14rpx; + overflow: hidden; + position: relative; + background: #f8f8f8; +} + +.floating-recommend-img { + width: 100%; + height: 100%; +} + +.floating-recommend-tag { + position: absolute; + top: 0; + left: 0; + background: linear-gradient(135deg, #ff3b30, #ff2d55); + color: #fff; + font-size: 20rpx; + padding: 6rpx 14rpx; + border-radius: 0 0 14rpx 0; + font-weight: 700; +} + +.floating-recommend-info { + display: flex; + flex-direction: column; + gap: 6rpx; + padding: 0 4rpx 4rpx; +} + +.floating-recommend-name { + font-size: 26rpx; + color: #1c1c1e; + font-weight: 600; + margin-top: 12rpx; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.floating-recommend-price { + color: #ff3b30; + font-weight: 700; + font-size: 34rpx; + margin-top: 6rpx; +} + +.cart-btn { + display: flex; + justify-content: flex-end; +} + +.cart-btn-inner { + width: 106rpx; + height: 116rpx; + background: linear-gradient(135deg, #ff6b35, #ff9f28); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.35); +} + +.bottom-actions { + display: flex; + align-items: center; + padding: 16rpx 24rpx; + padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); + background: #fff; + border-top: 1rpx solid #f0f0f0; +} + +.input-box { + flex: 1; + height: 72rpx; + background: #f5f5f5; + border-radius: 36rpx; + padding: 0 24rpx; + margin-right: 16rpx; + display: flex; + align-items: center; +} + +.input-content { + width: 100%; + font-size: 28rpx; + color: $text-color; +} + +.chat-coupon-btn { + width: 72rpx; + height: 72rpx; + margin-right: 16rpx; + border-radius: 50%; + background: #fff5f0; + display: flex; + align-items: center; + justify-content: center; +} + +.send-btn-small { + padding: 0 28rpx; + height: 72rpx; + line-height: 72rpx; + border-radius: 36rpx; + font-size: 28rpx; + color: #fff; + background: linear-gradient(90deg, #ff6b35, #ff9f28); + + &.disabled { + opacity: 0.5; + } +} + +.live-intro-wrapper { + padding: 24rpx; +} + +.live-title { + font-size: 36rpx; + font-weight: 600; + color: $text-color; + margin-bottom: 16rpx; +} + +.live-time { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 26rpx; + color: $text-color-light; + margin-bottom: 24rpx; +} + +.live-intro-title { + font-size: 28rpx; + font-weight: 600; + color: $text-color; + margin-bottom: 12rpx; + padding-top: 24rpx; + border-top: 1rpx solid $border-color; +} + +.live-intro-content-text { + font-size: 26rpx; + color: $text-color-light; + line-height: 1.6; +} + +.dialog-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 999; +} + +.goods-list-container, +.coupon-list-container { + width: 100%; + max-height: 75vh; + background: #f7f8fa; + border-radius: 32rpx 32rpx 0 0; + position: absolute; + bottom: 0; + display: flex; + flex-direction: column; +} + +.goods-list-header, +.coupon-list-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx 40rpx; + background: #fff; + border-radius: 32rpx 32rpx 0 0; +} + +.goods-list-title, +.coupon-list-title { + font-size: 34rpx; + font-weight: 700; + color: #333; +} + +.goods-list-close, +.coupon-list-close { + font-size: 36rpx; + color: #999; + padding: 8rpx; +} + +.goods-list-scroll, +.coupon-list-scroll { + flex: 1; + max-height: calc(75vh - 110rpx); +} + +.goods-list { + padding: 24rpx; +} + +.goods-item-row { + margin-bottom: 24rpx; +} + +.goods-item-content { + display: flex; + padding: 24rpx; + background: #fff; + border-radius: 24rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03); +} + +.goods-image-container { + width: 200rpx; + height: 200rpx; + border-radius: 16rpx; + overflow: hidden; + background: #f5f5f5; + flex-shrink: 0; +} + +.goods-image { + width: 100%; + height: 100%; +} + +.goods-right { + flex: 1; + margin-left: 24rpx; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.goods-name { + font-size: 28rpx; + color: #333; + font-weight: 600; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.hot-tag { + display: inline-block; + background: linear-gradient(135deg, #ff7a18, #ffb11b); + color: #fff; + font-size: 20rpx; + padding: 4rpx 12rpx; + border-radius: 8rpx; + margin-right: 8rpx; +} + +.goods-bottom { + display: flex; + align-items: flex-end; + justify-content: space-between; + margin-top: 16rpx; +} + +.current-price { + color: #ff4b2b; + font-size: 36rpx; + font-weight: 700; +} + +.price-symbol { + font-size: 24rpx; +} + +.original-price-text { + font-size: 24rpx; + color: #999; + text-decoration: line-through; +} + +.goods-btn { + padding: 12rpx 32rpx; + border-radius: 32rpx; + font-size: 26rpx; + font-weight: 600; + + &.btn-active { + background: linear-gradient(135deg, #ff4b2b, #ff416c); + color: #fff; + } + + &.btn-sold-out { + background: #f0f0f0; + color: #999; + } +} + +.goods-empty, +.coupon-empty { + text-align: center; + padding: 100rpx 0; + font-size: 28rpx; + color: #999; +} + +.coupon-list-inner { + padding: 0 32rpx 48rpx; +} + +.coupon-card { + display: flex; + align-items: stretch; + background: #fff; + border-radius: 24rpx; + margin-bottom: 24rpx; + position: relative; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.03); +} + +.coupon-card-left { + width: 220rpx; + min-height: 160rpx; + display: flex; + align-items: center; + justify-content: center; + color: #ff3b30; + flex-shrink: 0; +} + +.coupon-price-value { + font-size: 56rpx; + font-weight: 900; +} + +.coupon-card-right { + flex: 1; + padding: 32rpx; + display: flex; + align-items: center; + justify-content: space-between; +} + +.coupon-card-name { + font-size: 30rpx; + color: #111; + font-weight: 700; +} + +.coupon-claim-btn { + padding: 12rpx 28rpx; + border-radius: 40rpx; + font-size: 26rpx; + color: #fff; + background: #111; + + &.disabled { + background: #f0f0f0; + color: #999; + } +} + +.loading-wrap { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + background: rgba(0, 0, 0, 0.3); +} + +// 售罄置灰 +.goods-item-row.is-sold-out, +.product-showcase.is-sold-out, +.floating-recommend-card.is-sold-out { + opacity: 0.55; + filter: grayscale(0.85); + pointer-events: none; +} + +.current-price.price-sold-out { + color: #999 !important; +} + +// 左上角优惠券浮标 +@keyframes coupon-shake { + 0%, + 100% { + transform: rotate(0deg); + } + 10% { + transform: rotate(-8deg); + } + 20% { + transform: rotate(8deg); + } + 30% { + transform: rotate(-6deg); + } + 40% { + transform: rotate(6deg); + } + 50% { + transform: rotate(-3deg); + } + 60% { + transform: rotate(3deg); + } + 70%, + 100% { + transform: rotate(0deg); + } +} + +.coupon-float-btn { + position: fixed; + top: calc(var(--status-bar-height, 44px) + 88rpx); + left: 24rpx; + z-index: 100; + width: 72rpx; + height: 72rpx; + border-radius: 50%; + background: rgba(255, 255, 255, 0.95); + box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.25); + display: flex; + align-items: center; + justify-content: center; + + &.shake-animation { + animation: coupon-shake 1.2s ease-in-out infinite; + animation-delay: 0.5s; + } +} + +.coupon-float-btn-horizontal { + top: calc(var(--status-bar-height, 44px) + 12rpx); + left: 100rpx; +} + +.recommend-coupon-overlay { + display: flex; + align-items: center; + justify-content: center; + padding: 48rpx 32rpx; + box-sizing: border-box; +} + +.recommend-coupon-container.white-theme { + width: 600rpx; + max-width: 100%; + background: #fff; + border-radius: 32rpx; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 24rpx 64rpx rgba(0, 0, 0, 0.15); + animation: coupon-pop-up 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; +} + +@keyframes coupon-pop-up { + 0% { + transform: scale(0.8) translateY(40rpx); + opacity: 0; + } + 100% { + transform: scale(1) translateY(0); + opacity: 1; + } +} + +.rc-header { + position: relative; + padding: 48rpx 0 32rpx; + text-align: center; +} + +.rc-title { + font-size: 36rpx; + font-weight: 700; + color: #333; +} + +.rc-close { + position: absolute; + top: 24rpx; + right: 24rpx; + width: 48rpx; + height: 48rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + color: #999; +} + +.rc-body { + padding: 0 40rpx 40rpx; +} + +.rc-ticket-box { + background: #fff5f5; + border: 2rpx solid #ffe0e0; + border-radius: 16rpx; + display: flex; + height: 180rpx; + overflow: hidden; +} + +.rc-ticket-left { + width: 180rpx; + display: flex; + align-items: baseline; + justify-content: center; + color: #ff4b2b; + border-right: 2rpx dashed #ffcaca; + flex-shrink: 0; +} + +.rc-symbol { + font-size: 32rpx; + font-weight: 600; + margin-right: 4rpx; +} + +.rc-value { + font-size: 56rpx; + font-weight: 800; + line-height: 180rpx; +} + +.rc-ticket-right { + flex: 1; + padding: 0 24rpx; + display: flex; + align-items: center; +} + +.rc-name { + font-size: 30rpx; + color: #333; + font-weight: 600; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.4; +} + +.rc-footer { + padding: 0 40rpx 48rpx; +} + +.rc-btn { + width: 100%; + height: 88rpx; + border-radius: 44rpx; + background: linear-gradient(135deg, #ff4b2b, #ff416c); + color: #fff; + font-size: 32rpx; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 24rpx rgba(255, 65, 108, 0.25); + + &.disabled { + background: #f0f0f0; + color: #999; + box-shadow: none; + } +} diff --git a/pages/live/mqtt/mqtt.js b/pages/live/mqtt/mqtt.js new file mode 100644 index 0000000..39a624f --- /dev/null +++ b/pages/live/mqtt/mqtt.js @@ -0,0 +1,293 @@ +import api from "@/config/api.js"; +import mqtt from "mqtt"; + +const mqttEndpoint = api.mqtt; + +class MqttClient { + constructor(options = {}) { + const defaultOptions = { + endpoint: mqttEndpoint, + username: "admin", + password: "hivemq", + ssl: true, + keepalive: 30, + clean: true, + reconnectPeriod: 15000, + connectTimeout: 20000, + topicHandlers: [], + }; + + this.options = { ...defaultOptions, ...options }; + this.client = null; + this.connected = false; + this.connecting = false; + this.reconnecting = false; + this.error = null; + this.topics = []; + this.connectCount = 0; + this.MAX_CONNECT_RETRIES = 5; + this.manualDisconnect = false; + + this.onConnectCallback = null; + this.onReconnectCallback = null; + this.onErrorCallback = null; + this.onCloseCallback = null; + this.onOfflineCallback = null; + this.onMessageCallback = null; + + this.onConnect = this.onConnect.bind(this); + this.onReconnect = this.onReconnect.bind(this); + this.onError = this.onError.bind(this); + this.onMessage = this.onMessage.bind(this); + this.onClose = this.onClose.bind(this); + this.onOffline = this.onOffline.bind(this); + } + + connect() { + try { + if (!this.options.endpoint) { + throw new Error("MQTT 地址未配置,请检查 config/api.js 的 mqtt 字段"); + } + + console.log("[MQTT] 开始连接:", this.options.endpoint); + this.manualDisconnect = false; + + if (this.client) { + this.disconnect(); + } + + this.connecting = true; + this.error = null; + + const connectionOptions = { + clientId: this.options.clientId, + keepalive: this.options.keepalive, + clean: this.options.clean, + reconnectPeriod: this.options.reconnectPeriod, + connectTimeout: this.options.connectTimeout, + username: this.options.username || undefined, + password: this.options.password || undefined, + }; + + this.client = mqtt.connect(this.options.endpoint, connectionOptions); + + this.client.on("connect", this.onConnect); + this.client.on("reconnect", this.onReconnect); + this.client.on("error", this.onError); + this.client.on("message", this.onMessage); + this.client.on("close", this.onClose); + this.client.on("offline", this.onOffline); + + const timeoutId = setTimeout(() => { + if (!this.connected) { + this.onError(new Error("连接超时")); + if (this.client && typeof this.client.end === "function" && !this.manualDisconnect) { + try { + this.client.end(true); + } catch (e) { + console.warn("[MQTT] 超时关闭客户端失败:", e); + } + } + } + }, this.options.connectTimeout); + + this.client.once("connect", () => { + clearTimeout(timeoutId); + }); + } catch (err) { + console.error("[MQTT] 连接异常:", err); + this.onError(err); + } + + return this; + } + + disconnect() { + this.manualDisconnect = true; + + if (this.client) { + try { + if (this.client.removeAllListeners) { + this.client.removeAllListeners(); + } + if (typeof this.client.end === "function") { + this.client.end(true); + } + console.log("[MQTT] 连接已断开"); + } catch (err) { + console.warn("[MQTT] 断开连接异常:", err); + } + } + + this.client = null; + this.connected = false; + this.connecting = false; + this.reconnecting = false; + this.topics = []; + this.error = null; + this.connectCount = 0; + + return this; + } + + onConnect() { + console.log("[MQTT] 连接成功"); + this.connected = true; + this.connecting = false; + this.reconnecting = false; + this.error = null; + this.connectCount = 0; + this.subscribeToTopics(); + this.onConnectCallback?.(); + } + + onReconnect() { + console.log("[MQTT] 重连中..."); + this.connected = false; + this.connecting = true; + this.reconnecting = true; + this.connectCount++; + + if (this.connectCount > this.MAX_CONNECT_RETRIES) { + console.warn("[MQTT] 重连次数超限,停止重连"); + if (this.client && typeof this.client.end === "function") { + try { + this.client.end(true); + } catch (err) { + console.warn("[MQTT] 停止客户端异常:", err); + } + } + this.client = null; + this.onError(new Error("重连次数超限")); + } + + this.onReconnectCallback?.(this.connectCount); + } + + onError(err) { + console.error("[MQTT] 错误:", err?.message || err); + this.error = err; + this.connecting = false; + this.onErrorCallback?.(err); + } + + onClose() { + console.log("[MQTT] 连接关闭"); + this.connected = false; + this.onCloseCallback?.(); + } + + onOffline() { + console.log("[MQTT] 客户端离线"); + this.connected = false; + this.onOfflineCallback?.(); + } + + onMessage(topic, message) { + const msgStr = message.toString(); + + this.options.topicHandlers + .filter((handler) => handler.topic === topic || this.matchTopicPattern(handler.topic, topic)) + .forEach((handler) => { + try { + handler.handler(msgStr, topic); + } catch (err) { + console.error(`[MQTT] 处理主题 ${topic} 消息失败:`, err); + } + }); + + this.onMessageCallback?.(topic, msgStr); + } + + isClientValid() { + return this.client && this.connected && typeof this.client.subscribe === "function"; + } + + matchTopicPattern(pattern, topic) { + const regex = new RegExp( + `^${pattern + .replace(/\+/g, "[^/]+") + .replace(/#$/, ".*") + .replace(/\//g, "\\/")}$` + ); + return regex.test(topic); + } + + subscribe(topic, options = { qos: 0 }, handler) { + if (!this.isClientValid()) { + console.warn("[MQTT] 客户端无效,无法订阅"); + return false; + } + + try { + this.client.subscribe(topic, options, (err, granted) => { + if (err) { + console.error(`[MQTT] 订阅 ${topic} 失败:`, err); + return; + } + console.log(`[MQTT] 订阅成功: ${topic}`, granted); + + const idx = this.topics.findIndex((t) => t.topic === topic); + if (idx >= 0) { + this.topics[idx] = { topic, qos: options.qos, handler }; + } else { + this.topics.push({ topic, qos: options.qos, handler }); + } + + if (handler && typeof handler === "function") { + const hIdx = this.options.topicHandlers.findIndex((h) => h.topic === topic); + if (hIdx >= 0) { + this.options.topicHandlers[hIdx] = { topic, qos: options.qos, handler }; + } else { + this.options.topicHandlers.push({ topic, qos: options.qos, handler }); + } + } + }); + return true; + } catch (err) { + console.error(`[MQTT] 订阅 ${topic} 异常:`, err); + return false; + } + } + + subscribeToTopics() { + if (!this.isClientValid()) return; + + this.options.topicHandlers.forEach(({ topic, qos = 0, handler }) => { + this.subscribe(topic, { qos }, handler); + }); + } + + onConnected(callback) { + this.onConnectCallback = callback; + return this; + } + onErrorOccurred(callback) { + this.onErrorCallback = callback; + return this; + } + onClosed(callback) { + this.onCloseCallback = callback; + return this; + } + onClientOffline(callback) { + this.onOfflineCallback = callback; + return this; + } + + isConnected() { + return this.connected; + } +} + +let mqttInstance = null; + +export function createMqttClient(options = {}) { + if (mqttInstance && mqttInstance.isConnected()) { + mqttInstance.disconnect(); + } + mqttInstance = new MqttClient(options); + return mqttInstance; +} + +export { MqttClient }; diff --git a/pages/live/room.vue b/pages/live/room.vue new file mode 100644 index 0000000..1103767 --- /dev/null +++ b/pages/live/room.vue @@ -0,0 +1,1083 @@ + + + + + diff --git a/pages/live/utils/liveChat.js b/pages/live/utils/liveChat.js new file mode 100644 index 0000000..6e80495 --- /dev/null +++ b/pages/live/utils/liveChat.js @@ -0,0 +1,323 @@ +/** + * 直播腾讯云 IM(由 saas-uni-v3 useChat 迁移) + */ +import TencentCloudChat from "@tencentcloud/chat"; +import { fetchLiveSetting } from "./liveSetting.js"; + +const MAX_MESSAGE_COUNT = 100; + +export class LiveChatService { + constructor(options = {}) { + this.liveId = options.liveId || ""; + this.userInfo = options.userInfo || {}; + this.defaultAvatar = options.defaultAvatar || ""; + this.onMessagesChange = options.onMessagesChange || (() => {}); + this.onScrollToBottom = options.onScrollToBottom || (() => {}); + + this.chat = null; + this.groupID = ""; + this.messageList = []; + this.messageIdSet = new Set(); + this.savedViewLiveResult = null; + this.isAtBottom = true; + this.unreadCount = 0; + this.isLoadingHistory = false; + this.hasMoreHistory = false; + this.nextReqMessageID = ""; + + this._onMessageReceived = this.onMessageReceived.bind(this); + this._onSdkReady = this.onSdkReady.bind(this); + this._onSdkNotReady = this.onSdkNotReady.bind(this); + this._onKickedOut = this.onKickedOut.bind(this); + } + + setGroupID(id) { + this.groupID = id ? String(id) : ""; + } + + async initTencentIm(viewLiveResult) { + if (!this.groupID) { + console.warn("groupID 不存在,跳过 IM 初始化"); + return; + } + + const setting = await fetchLiveSetting(); + if (!setting?.imSdkAppid) { + console.warn("IM SDKAppID 未配置,跳过 IM 初始化"); + return; + } + + if (viewLiveResult?.userSig) { + this.savedViewLiveResult = viewLiveResult; + } + + if (this.chat) { + this.loginChat(viewLiveResult); + return; + } + + const chatInstance = TencentCloudChat.create({ + SDKAppID: Number(setting.imSdkAppid), + }); + this.chat = chatInstance; + chatInstance.setLogLevel(1); + + chatInstance.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, this._onMessageReceived); + chatInstance.on(TencentCloudChat.EVENT.SDK_READY, this._onSdkReady); + chatInstance.on(TencentCloudChat.EVENT.SDK_NOT_READY, this._onSdkNotReady); + chatInstance.on(TencentCloudChat.EVENT.KICKED_OUT, this._onKickedOut); + + this.loginChat(viewLiveResult); + } + + onSdkNotReady() { + console.log("[SDK Not Ready,重新登录]"); + this.loginChat(); + } + + loginChat(viewLiveResult) { + if (!this.chat) return; + + const source = + (viewLiveResult?.userSig ? viewLiveResult : null) || this.savedViewLiveResult; + const userId = source?.userId || this.userInfo?.id; + const userSig = source?.userSig; + + if (!userId || !userSig) { + console.warn("用户ID或签名不存在", { userId, userSig }); + return; + } + + this.chat + .login({ + userID: `${userId}`, + userSig, + }) + .then((imResponse) => { + if (imResponse.data.repeatLogin === true) { + this.joinGroup(); + this.getHistoryMessageList(""); + } + }) + .catch((imError) => { + console.warn("login error:", imError); + }); + } + + onSdkReady() { + console.log("[SDK Ready]"); + this.updateUserInfo(); + this.joinGroup(); + this.getHistoryMessageList(""); + } + + onKickedOut() { + console.log("[用户被踢下线,重新登录]"); + this.loginChat(); + } + + updateUserInfo() { + if (!this.chat || !this.userInfo) return; + + this.chat.updateMyProfile({ + nick: this.userInfo.nickName || this.userInfo.username || "用户", + avatar: this.userInfo.face || this.defaultAvatar, + gender: TencentCloudChat.TYPES.GENDER_UNKNOWN, + allowType: TencentCloudChat.TYPES.ALLOW_TYPE_ALLOW_ANY, + }); + } + + joinGroup() { + if (!this.chat || !this.groupID) return; + + this.chat + .joinGroup({ + groupID: this.groupID, + type: TencentCloudChat.TYPES.GRP_AVCHATROOM, + }) + .then((imResponse) => { + switch (imResponse.data.status) { + case TencentCloudChat.TYPES.JOIN_STATUS_SUCCESS: + case TencentCloudChat.TYPES.JOIN_STATUS_ALREADY_IN_GROUP: + this.getHistoryMessageList(""); + break; + default: + break; + } + }) + .catch((imError) => { + console.warn("joinGroup error:", imError); + this.getHistoryMessageList(""); + }); + } + + async getHistoryMessageList(nextReqId) { + if (this.isLoadingHistory || !this.chat || !this.groupID) return; + + this.isLoadingHistory = true; + + const requestParams = { + conversationID: `GROUP${this.groupID}`, + direction: 0, + }; + if (nextReqId) { + requestParams.nextReqMessageID = nextReqId; + } + + try { + const imResponse = await this.chat.getMessageList(requestParams); + const msgList = imResponse.data.messageList || []; + const nextId = imResponse.data.nextReqMessageID; + const isCompleted = imResponse.data.isCompleted; + + msgList.forEach((msg) => { + this.addMessageToList(this.formatReceivedMessage(msg), "append"); + }); + + if (!this.nextReqMessageID) { + this.scrollToBottom(); + } + + this.nextReqMessageID = nextId; + this.hasMoreHistory = !isCompleted; + } catch (imError) { + console.warn("getMessageList error:", imError); + } finally { + this.isLoadingHistory = false; + } + } + + formatReceivedMessage(message) { + let messageText = ""; + let username = ""; + let isSystem = false; + + if (message.from === "@TIM#SYSTEM") { + username = "系统消息"; + isSystem = true; + } else { + username = message.nick || message.from || "用户"; + } + + const avatar = message.avatar || this.defaultAvatar || null; + + if (message.type === TencentCloudChat.TYPES.MSG_TEXT || message.type === "TIMTextElem") { + messageText = message.payload?.text || ""; + } else if ( + message.type === TencentCloudChat.TYPES.MSG_CUSTOM || + message.type === "TIMCustomElem" + ) { + try { + const customData = JSON.parse(message.payload.data); + messageText = customData.content || "系统消息"; + username = "系统消息"; + isSystem = true; + } catch (e) { + messageText = "系统消息"; + username = "系统消息"; + isSystem = true; + } + } else { + messageText = "欢迎加入直播间"; + } + + return { + username, + message: messageText, + avatar: isSystem ? this.defaultAvatar : avatar, + messageId: message.ID, + time: message.time, + type: message.type, + isSystem, + }; + } + + toRoomMessage(item) { + return { + id: item.messageId, + userName: item.username, + userFace: item.avatar || this.defaultAvatar, + message: item.message, + isSystem: item.isSystem, + }; + } + + emitMessagesChange() { + this.onMessagesChange( + this.messageList.map((item) => this.toRoomMessage(item)) + ); + } + + onMessageReceived(event) { + const msgList = event.data || []; + msgList.forEach((message) => { + if (message.type === TencentCloudChat.TYPES.MSG_TEXT) { + const formattedMessage = this.formatReceivedMessage(message); + this.addMessageToList(formattedMessage, "append"); + + if (this.isAtBottom) { + this.scrollToBottom(); + } else { + this.unreadCount += 1; + } + } + }); + } + + addMessageToList(message, mode = "append") { + if (!message.messageId || this.messageIdSet.has(message.messageId)) return; + + this.messageIdSet.add(message.messageId); + + if (mode === "append") { + this.messageList.push(message); + if (this.messageList.length > MAX_MESSAGE_COUNT) { + const removeCount = this.messageList.length - MAX_MESSAGE_COUNT; + const removed = this.messageList.splice(0, removeCount); + removed.forEach((item) => this.messageIdSet.delete(item.messageId)); + } + } else { + this.messageList.unshift(message); + if (this.messageList.length > MAX_MESSAGE_COUNT) { + const removeCount = this.messageList.length - MAX_MESSAGE_COUNT; + const removed = this.messageList.splice(-removeCount, removeCount); + removed.forEach((item) => this.messageIdSet.delete(item.messageId)); + } + } + + this.emitMessagesChange(); + } + + scrollToBottom() { + this.isAtBottom = true; + this.unreadCount = 0; + setTimeout(() => { + this.onScrollToBottom(); + }, 100); + } + + markAtBottom(isBottom) { + this.isAtBottom = isBottom; + if (isBottom) { + this.unreadCount = 0; + } + } + + cleanup() { + if (this.chat) { + try { + this.chat.off(TencentCloudChat.EVENT.MESSAGE_RECEIVED, this._onMessageReceived); + this.chat.off(TencentCloudChat.EVENT.SDK_READY, this._onSdkReady); + this.chat.off(TencentCloudChat.EVENT.SDK_NOT_READY, this._onSdkNotReady); + this.chat.off(TencentCloudChat.EVENT.KICKED_OUT, this._onKickedOut); + this.chat.logout?.(); + } catch (error) { + console.error("清理腾讯云IM失败:", error); + } + this.chat = null; + } + this.messageList = []; + this.messageIdSet.clear(); + this.savedViewLiveResult = null; + } +} diff --git a/pages/live/utils/liveMqtt.js b/pages/live/utils/liveMqtt.js new file mode 100644 index 0000000..63b6c64 --- /dev/null +++ b/pages/live/utils/liveMqtt.js @@ -0,0 +1,222 @@ +/** + * 直播 MQTT 连接管理(由 saas-uni-v3 useMqtt 迁移) + */ +import { createMqttClient } from "../mqtt/mqtt.js"; + +export class LiveMqttService { + constructor(liveId, userId) { + this.liveId = liveId; + this.userId = userId; + this.mqttClient = null; + this.isMqttConnected = false; + this.fallbackMode = false; + this.reconnectAttempts = 0; + this.mqttReconnectTimer = null; + this.lastHeartbeatTime = null; + this.heartbeatCheckTimer = null; + this.maxReconnectAttempts = 60; + this.reconnectInterval = 10000; + this.messageHandlers = {}; + } + + setMessageHandlers(handlers) { + this.messageHandlers = handlers || {}; + } + + initMqtt() { + if (this.mqttClient) return; + + if (!this.liveId || !this.userId) { + console.warn("直播ID或用户ID不完整,跳过MQTT初始化"); + this.fallbackMode = true; + return; + } + + const clientId = `${this.userId}-${this.liveId}-${Date.now()}`; + + try { + const topicList = [ + { + topic: `live/goods/${this.liveId}`, + qos: 1, + handler: (msg, topic) => this.messageHandlers.onGoodsMessage?.(msg, topic), + }, + { + topic: `live/goods/${this.liveId}/#`, + qos: 1, + handler: (msg, topic) => this.messageHandlers.onGoodsMessage?.(msg, topic), + }, + { + topic: `live/coupon/${this.liveId}`, + qos: 1, + handler: (msg, topic) => this.messageHandlers.onCouponMessage?.(msg, topic), + }, + { + topic: `live/coupon/${this.liveId}/#`, + qos: 1, + handler: (msg, topic) => this.messageHandlers.onCouponMessage?.(msg, topic), + }, + { + topic: `live/live/${this.liveId}`, + qos: 1, + handler: (msg, topic) => this.messageHandlers.onLiveMessage?.(msg, topic), + }, + { + topic: `live/blacklist/${this.liveId}/${this.userId}`, + qos: 1, + handler: (msg, topic) => this.messageHandlers.onBlacklistMessage?.(msg, topic), + }, + { + topic: "live/keepalive", + qos: 1, + handler: (msg) => this.handleKeepaliveMessage(msg), + }, + { + topic: `live/push/${this.liveId}`, + qos: 1, + handler: (msg, topic) => this.messageHandlers.onPushStream?.(msg, topic), + }, + ]; + + this.mqttClient = createMqttClient({ + clientId, + topicHandlers: topicList, + }); + + this.mqttClient + .onConnected(() => { + console.log("✅ MQTT连接成功"); + this.isMqttConnected = true; + this.fallbackMode = false; + this.stopMqttReconnect(); + this.reconnectAttempts = 0; + this.startHeartbeatCheck(); + }) + .onErrorOccurred((error) => this.handleMqttFailure(error)) + .onClosed(() => this.handleMqttFailure()) + .onClientOffline(() => this.handleMqttFailure()); + + this.mqttClient.connect(); + } catch (error) { + console.error("❌ MQTT初始化失败:", error); + this.handleMqttFailure(error); + } + } + + handleKeepaliveMessage(message) { + console.log("[MQTT keepalive]", message); + this.lastHeartbeatTime = Date.now(); + } + + handleMqttFailure(error) { + if (error) console.error("MQTT错误:", error); + + this.disconnectMqtt(false); + + if (!this.fallbackMode) { + console.log("❗MQTT异常,启动备用模式和重连机制"); + this.fallbackMode = true; + } + + this.isMqttConnected = false; + this.startMqttReconnect(); + } + + disconnectMqtt(resetFallback = true) { + try { + if (this.mqttClient) { + this.mqttClient.disconnect(); + this.mqttClient = null; + console.log("🔌 MQTT连接已断开"); + } + } catch (error) { + console.error("断开MQTT连接时出错:", error); + } + + this.isMqttConnected = false; + this.stopMqttReconnect(); + if (resetFallback) { + this.fallbackMode = false; + } + } + + startMqttReconnect() { + if (this.mqttReconnectTimer) return; + + console.log(`开始MQTT重连机制,间隔${this.reconnectInterval}ms`); + + this.mqttReconnectTimer = setInterval(() => { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.log(`已达到最大重连次数${this.maxReconnectAttempts},停止重连`); + this.stopMqttReconnect(); + return; + } + + this.reconnectAttempts++; + console.log(`MQTT重连尝试 ${this.reconnectAttempts}/${this.maxReconnectAttempts}`); + + try { + if (this.mqttClient) { + this.mqttClient.disconnect(); + this.mqttClient = null; + } + } catch (error) { + console.warn("断开旧MQTT连接失败:", error); + } + + this.initMqtt(); + }, this.reconnectInterval); + } + + stopMqttReconnect() { + if (this.mqttReconnectTimer) { + clearInterval(this.mqttReconnectTimer); + this.mqttReconnectTimer = null; + } + this.reconnectAttempts = 0; + } + + startHeartbeatCheck() { + if (this.heartbeatCheckTimer) return; + + this.heartbeatCheckTimer = setInterval(() => { + if (!this.isMqttConnected || !this.lastHeartbeatTime) return; + + const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeatTime; + if (timeSinceLastHeartbeat > 30000) { + console.warn("⚠️ MQTT心跳超时,主动重连"); + + if (this.mqttClient) { + try { + this.mqttClient.disconnect(); + this.mqttClient = null; + } catch (error) { + console.warn("断开假死MQTT连接失败:", error); + } + } + + this.isMqttConnected = false; + this.lastHeartbeatTime = null; + this.mqttClient = null; + this.initMqtt(); + } + }, 10000); + } + + stopHeartbeatCheck() { + if (this.heartbeatCheckTimer) { + clearInterval(this.heartbeatCheckTimer); + this.heartbeatCheckTimer = null; + } + } + + isConnected() { + return this.isMqttConnected; + } + + cleanup() { + this.stopHeartbeatCheck(); + this.stopMqttReconnect(); + this.disconnectMqtt(); + } +} diff --git a/pages/live/utils/liveRecommend.js b/pages/live/utils/liveRecommend.js new file mode 100644 index 0000000..985b639 --- /dev/null +++ b/pages/live/utils/liveRecommend.js @@ -0,0 +1,27 @@ +/** + * 直播 MQTT 推荐消息解析(商品 / 优惠券) + */ + +export function toBoolean(value) { + if (typeof value === "boolean") return value; + if (typeof value === "number") return value === 1; + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + return normalized === "true" || normalized === "1"; + } + return false; +} + +export function parseRecommendMessageArray(message) { + try { + const parsed = JSON.parse(message); + return Array.isArray(parsed) ? parsed : []; + } catch (error) { + console.warn("MQTT 消息解析失败:", error); + return []; + } +} + +export function findRecommendPayload(list) { + return list.find((item) => toBoolean(item.recommend) && !toBoolean(item.hideFlag)); +} diff --git a/pages/live/utils/liveSetting.js b/pages/live/utils/liveSetting.js new file mode 100644 index 0000000..164d585 --- /dev/null +++ b/pages/live/utils/liveSetting.js @@ -0,0 +1,34 @@ +import { getLiveSetting } from "@/api/live.js"; + +let cachedSetting = null; +let fetchingPromise = null; + +export async function fetchLiveSetting(force = false) { + if (!force && cachedSetting) return cachedSetting; + if (!force && fetchingPromise) return fetchingPromise; + + fetchingPromise = (async () => { + try { + const res = await getLiveSetting(); + if (res.data?.success && res.data?.result) { + cachedSetting = res.data.result; + return cachedSetting; + } + console.warn("获取直播配置失败", res.data); + return null; + } catch (error) { + console.error("获取直播配置异常:", error); + return null; + } finally { + fetchingPromise = null; + } + })(); + + return fetchingPromise; +} + +export function resolveSdkAppId(setting) { + if (!setting?.imSdkAppid) return 0; + const id = Number(setting.imSdkAppid); + return Number.isNaN(id) ? 0 : id; +} diff --git a/pages/promotion/lives.vue b/pages/promotion/lives.vue index e8a4afe..cd35854 100644 --- a/pages/promotion/lives.vue +++ b/pages/promotion/lives.vue @@ -272,27 +272,11 @@ export default { * 进入直播间 */ handleLivePlayer(val) { - // #ifdef MP-WEIXIN - let roomId = val.roomId; // 填写具体的房间号,可通过下面【获取直播房间列表】 API 获取 - let customParams = encodeURIComponent( - JSON.stringify({ path: "pages/index/index", pid: 1 }) - ); // 开发者在直播间页面路径上携带自定义参数,后续可以在分享卡片链接和跳转至商详页时获取,详见【获取自定义参数】、【直播间到商详页面携带参数】章节(上限600个字符,超过部分会被截断) + const id = val.id || val.roomId; + if (!id) return; uni.navigateTo({ - url: - "plugin-private://wx2b03c6e691cd7370/pages/live-player-plugin?room_id=" + - roomId + - "&custom_params=" + - customParams, + url: `/pages/live/room?id=${id}`, }); - // #endif - - // #ifndef MP-WEIXIN - uni.showToast({ - title: "请从微信小程序中预览直播功能", - duration: 2000, - icon: "none", - }); - // #endif }, }, };