Files
uniapp-im-shop/uni_modules/tuikit-atomic-x/components/CoHostPanel.nvue
2026-01-12 17:52:15 +08:00

581 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }" @click.stop>
<view class="audience-header">
<view class="tab-container">
<view class="tab-item">
<text class="active-text">发起连线</text>
<view class="end-right" @tap.stop="handleExitCoHost" v-if="connected?.length > 0">
<image class="end-connect" src="/static/images/logout.png"></image>
<text style="color: #E6594C; padding-left: 10rpx;">断开</text>
</view>
</view>
</view>
</view>
<view v-if="filteredConnected?.length > 0"
style="display: flex; width: 800rpx; padding-left: 80rpx; margin-bottom: 40rpx;">
<text class="title-text">连线中</text>
<view v-for="connect in filteredConnected" :key="connect.userID" class="audience-item">
<view class="audience-info">
<view class="audience-avatar-container">
<image class="audience-avatar" mode="aspectFill" :src="connect.avatarURL || defaultAvatarURL" />
</view>
<view class="audience-item-right">
<view class="audience-detail">
<text class="audience-name" :numberOfLines="1">{{ connect.userName || connect.userID}}</text>
</view>
</view>
</view>
</view>
</view>
<view v-if="liveList?.length > 0" class="audience-grid">
</view>
<view
style="display: flex; width: 800rpx; flex-direction: row; padding-left: 80rpx; padding-right: 80rpx;margin-bottom: 40rpx; justify-content:space-between; "
@click.stop>
<text class="title-text">推荐列表</text>
<text class="title-text" @click="handleRefresh">刷新</text>
</view>
<list class="audience-content" @loadmore="loadMore" :show-scrollbar="false">
<cell v-for="host in currentInviteHosts" :key="host?.liveOwner?.userID">
<view class="audience-info">
<view class="audience-avatar-container">
<image class="audience-avatar" mode="aspectFill" :src="host?.liveOwner?.avatarURL || defaultAvatarURL" />
</view>
<view class="audience-item-right">
<view class="audience-detail">
<text class="audience-name"
:numberOfLines="1">{{ host?.liveOwner?.userName || host?.liveOwner?.userID}}</text>
</view>
<view :class=" isHostInviting(host) ? 'start-link-waiting' : 'start-link' " @tap="onStartLinkTap(host)">
<text class="start-link-text">{{ isHostInviting(host) ? '邀请中' : '邀请连线' }}</text>
</view>
</view>
</view>
<view class="audience-item-bottom-line"></view>
</cell>
</list>
</view>
</view>
</template>
<script setup lang="ts">
import {
ref,
reactive,
onMounted,
computed,
watch,
nextTick
} from 'vue'
import {
useCoHostState
} from "@/uni_modules/tuikit-atomic-x/state/CoHostState"
import {
useLiveListState
} from "@/uni_modules/tuikit-atomic-x/state/LiveListState";
import {
useLoginState
} from "@/uni_modules/tuikit-atomic-x/state/LoginState";
import { useCoGuestState } from "@/uni_modules/tuikit-atomic-x/state/CoGuestState";
const { applicants, rejectApplication } = useCoGuestState(uni?.$liveID);
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue'])
const refreshing = ref('hide')
const isLoading = ref(false)
const scrollTop = ref(0)
const activeTab = ref('pk')
const showDrawer = ref(false)
const selectedAudience = ref(null)
const screenWidth = ref(350)
const {
candidates,
connected,
requestHostConnection,
invitees,
exitHostConnection,
} = useCoHostState(uni?.$liveID)
const {
// 响应式状态
liveListCursor,
currentLive,
// 操作方法 - callback在params中
fetchLiveList,
liveList
} = useLiveListState();
const {
loginUserInfo
} = useLoginState()
const currentCursor = ref('')
const currentUserID = ref(loginUserInfo.value?.userID);
const filteredConnected = computed(() => {
const list = connected?.value || [];
const selfId = uni?.$userID ?? currentUserID.value;
return list.filter(item => item?.userID !== selfId);
})
function filterInviteHosts(list) {
const hosts = Array.isArray(list) ? list : [];
const connectedLiveIds = new Set((connected?.value || []).map(item => item?.liveID));
return hosts.filter(item =>
item?.liveOwner?.userID !== currentLive?.value?.liveOwner?.userID &&
!connectedLiveIds.has(item?.liveID)
);
}
const currentInviteHosts = ref(filterInviteHosts(liveList?.value))
const isRefreshEnd = ref(false)
watch(() => loginUserInfo.value?.userID, (newUserId, oldUserId) => {
console.log('用户ID变化:', {
newUserId
});
if (newUserId) {
// 如果当前标题是默认值或者为空,则更新为新的用户名
currentUserID.value = newUserId;
}
}, {
immediate: true,
deep: true
});
watch(liveList, (newHostsList, olderHostList) => {
if (newHostsList) {
currentInviteHosts.value = filterInviteHosts(newHostsList)
}
nextTick()
}, {
immediate: true,
deep: true
},
)
watch(currentLive, () => {
currentInviteHosts.value = filterInviteHosts(liveList?.value)
}, {
immediate: true,
deep: true
})
watch(applicants, (newVal, oldVal) => {
if (newVal && invitees?.value.length > 0) {
newVal.forEach(applicant => {
rejectApplication({
liveID: uni?.$liveID,
userID: applicant.userID,
});
});
}
}, {
immediate: true,
deep: true
},)
watch(liveListCursor, (newCursor, oldCursor) => {
isRefreshEnd.value = newCursor === ''
})
// 监听连线状态变化,重新过滤推荐列表
watch(connected, (newConnected, oldConnected) => {
currentInviteHosts.value = filterInviteHosts(liveList?.value);
}, {
immediate: true,
deep: true
})
const handleRefresh = () => {
const params = {
cursor: '', // 首次拉起传空不能是null),然后根据回调数据的cursor确认是否拉完
count: 20, // 分页拉取的个数
success: () => {
fetchLiveListRecursively(liveListCursor.value); // 最多尝试3次
}
};
fetchLiveList(params);
}
const fetchLiveListRecursively = (cursor) => {
const params = {
cursor: cursor,
count: 20,
success: () => {
if (liveListCursor.value) {
fetchLiveListRecursively(liveListCursor.value);
} else {
uni.showToast({
title: '刷新完成'
});
}
},
fail: (err) => {
console.error(`fetchLiveListRecursively failed, err: ${JSON.stringify(err)}`);
}
};
fetchLiveList(params);
}
const loadMore = () => {
if (!liveListCursor.value) {
uni.showToast({
title: "没有更多了",
icon: "none"
});
return;
}
const params = {
cursor: liveListCursor.value,
count: 20,
};
fetchLiveList(params);
};
// 判断当前 host 是否处于邀请中(依据 invitees 列表里的 liveID
function isHostInviting(host) {
const inviteesList = invitees?.value;
const targetLiveID = host?.liveID;
if (!targetLiveID) return false;
return inviteesList.some((item) => item?.liveID === targetLiveID);
}
// 点击邀请,若已在邀请中则不重复发起
function onStartLinkTap(host) {
if (isHostInviting(host)) {
return;
}
startLink(host);
}
function close() {
emit('update:modelValue', false)
}
function startLink(host) {
if (applicants.value.length > 0) {
uni.showToast({
title: '有人申请连麦,无法发起连线',
icon: 'none'
})
return
}
requestHostConnection({
liveID: uni?.$liveID,
targetHostLiveID: host.liveID,
layoutTemplate: 'HOST_DYNAMIC_GRID',
timeout: 30,
extensionInfo: "",
success: (res) => {
console.log(res)
},
fail: (errorCode) => {
console.log(errorCode)
if (errorCode === 5) {
uni.showToast({
icon: 'none',
title: '主播连线中,无法发起连线'
})
} else {
uni.showToast({
icon: 'none',
title: '连线失败'
})
}
}
},)
}
const handleExitCoHost = () => {
uni.showModal({
content: '确定要退出连线吗?',
confirmText: '退出连线',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
exitHostConnection({
liveID: uni?.$liveID,
success: () => {
close()
},
fail: (error) => {
console.log(error)
}
})
}
}
})
}
</script>
<style>
.refresh-icon-container {
margin-left: 16rpx;
width: 40rpx;
height: 40rpx;
justify-content: center;
align-items: center;
border-radius: 8rpx;
background-color: #ffffff;
}
.refresh-icon {
width: 32rpx;
height: 32rpx;
display: flex;
justify-content: center;
align-items: center;
}
.refresh-image {
width: 32rpx;
height: 32rpx;
}
.refresh-icon.refreshing {
animation: rotate 1s linear infinite;
}
.bottom-drawer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: 0;
z-index: 1000;
}
.drawer-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
.bottom-drawer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(34, 38, 46, 1);
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
transform: translateY(100%);
display: flex;
flex-direction: column;
align-items: center;
height: 1000rpx;
}
.drawer-open {
transform: translateY(0);
}
.audience-header {
padding: 20rpx 0;
background-color: rgba(34, 38, 46, 1);
z-index: 1;
flex-direction: column;
align-items: center;
}
.tab-container {
flex-direction: row;
justify-content: center;
margin-top: 20rpx;
}
.tab-item {
padding: 20rpx 40rpx;
margin: 0 20rpx;
border-radius: 8rpx;
display: flex;
justify-content: center;
flex-direction: row;
width: 750rpx;
flex: 1;
}
.end-right {
position: absolute;
right: 80rpx;
left: auto;
display: flex;
flex-direction: row;
}
.end-connect {
width: 40rpx;
height: 40rpx;
}
.tab-text {
font-size: 32rpx;
}
.active-text {
color: rgba(255, 255, 255, 0.9);
}
.title-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
}
.active-line-container {
margin-top: 4rpx;
display: flex;
justify-content: center;
align-items: center;
width: 128rpx;
}
.active-line {
border-bottom: 4rpx solid rgba(255, 255, 255, 0.9);
width: 80rpx;
}
.inactive-text {
color: rgba(255, 255, 255, 0.3);
}
.audience-return-arrow-container {
position: absolute;
top: 40rpx;
left: 48rpx;
width: 48rpx;
height: 48rpx;
justify-content: center;
align-items: center;
}
.audience-return-arrow {
height: 28rpx;
width: 16rpx;
}
.audience-content {
flex: 1;
width: 750rpx;
padding: 0 48rpx;
background-color: rgba(34, 38, 46, 1);
}
.audience-grid {
flex-direction: column;
}
.audience-item {
border-radius: 16rpx;
height: 100rpx;
color: #ffffff;
position: relative;
margin-bottom: 32rpx;
padding-top: 10rpx;
}
.audience-info {
flex-direction: row;
align-items: center;
padding: 10rpx 0;
}
.audience-avatar-container {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 24rpx;
overflow: hidden;
}
.audience-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
}
.audience-item-right {
flex: 1;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.audience-detail {
flex-direction: row;
}
.audience-name {
font-size: 32rpx;
font-weight: 500;
color: #ffffff;
width: 300rpx;
lines: 1;
}
.start-link {
width: 120rpx;
height: 40rpx;
border-radius: 30rpx;
justify-content: center;
align-items: center;
background-color: rgba(28, 102, 229, 1);
}
.start-link-waiting {
width: 120rpx;
height: 40rpx;
border-radius: 30rpx;
justify-content: center;
align-items: center;
background-color: transparent;
border-width: 2rpx;
border-style: solid;
border-color: rgba(28, 102, 229, 1);
}
.start-link-text {
font-size: 24rpx;
color: #ffffff;
}
.audience-item-bottom-line {
position: absolute;
border-bottom-width: 2rpx;
border-bottom-style: solid;
border-bottom-color: rgba(79, 88, 107, 0.3);
width: 550rpx;
height: 2rpx;
bottom: 0;
right: 0;
}
.empty-state,
.loading-state {
padding: 64rpx;
justify-content: center;
align-items: center;
}
.drawer-content {
padding: 0 48rpx;
height: 68rpx;
}
</style>