需要添加直播接口

This commit is contained in:
cbb
2026-01-12 17:52:15 +08:00
parent 83fec2617c
commit 13af9eb303
281 changed files with 313157 additions and 104 deletions

View File

@@ -0,0 +1,433 @@
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }">
<view class="audience-header">
<view class="tab-container">
<!-- <view
class="tab-item"
:class="activeTab === 'invite' ? 'active' : 'inactive'"
@tap="activeTab = 'invite'"
>
<text :class="activeTab === 'invite' ? 'active-text' : 'inactive-text'">邀请连麦</text>
<view class="active-line-container" v-if="activeTab === 'invite'">
<view class="active-line"></view>
</view>
</view> -->
<view class="tab-item" :class="activeTab === 'requests' ? 'active' : 'inactive'"
@tap="activeTab = 'requests'">
<text :class="activeTab === 'requests' ? 'active-text' : 'inactive-text'">连麦申请</text>
<!-- <view class="active-line-container" v-if="activeTab === 'requests'">
<view class="active-line"></view>
</view> -->
</view>
</view>
</view>
<scroll-view class="audience-content" scroll-y @scroll="handleScroll" :scroll-top="scrollTop">
<!-- 连麦申请列表 -->
<view v-if="activeTab === 'requests'">
<view v-if="(applicants || []).length > 0" class="audience-grid">
<view v-for="audience in applicants || []" :key="audience?.userID" class="audience-item"
:class="{ 'is-admin': audience?.userRole === 'administrator' }">
<view class="audience-info">
<view class="audience-avatar-container">
<image class="audience-avatar" :src="audience?.avatarURL || defaultAvatarURL" mode="aspectFill" />
</view>
<view class="audience-item-right">
<view class="audience-detail">
<text class="audience-name" :numberOfLines="1">{{ audience?.userName || audience?.userID}}</text>
</view>
<view class="request-actions">
<view class="action-btn accept" @tap="handleRequest(audience, 'accept')">
<text class="btn-text">同意</text>
</view>
<view class="action-btn reject" @tap="handleRequest(audience, 'reject')">
<text class="btn-text">拒绝</text>
</view>
</view>
</view>
</view>
<!-- <view class="audience-info">
<view class="audience-avatar-container">
<image class="audience-avatar" :src="audience.avatarURL || defaultAvatarURL" mode="aspectFill" />
</view>
<view class="audience-item-right">
<view class="audience-detail">
<text class="audience-name" :numberOfLines="1">{{ audience.userName }}</text>
</view>
<view
class="start-link"
:class="audience?.isMessageDisabled ? 'start' : 'waiting'"
@tap="startLink(audience)"
>
<text class="link-text">{{ audience?.isMessageDisabled ? '发起连线' : '邀请中(30)s' }}</text>
</view>
</view>
</view>-->
<view class="audience-item-bottom-line"></view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-text">暂无观众申请连麦</text>
<view></view>
</view>
</view>
<!-- 邀请连麦列表 -->
<!-- <view v-if="activeTab === 'invite'">
<view v-if="(invitableGuests || []).length > 0" class="audience-grid">
<view v-for="request in invitableGuests || []" :key="request.userID" class="audience-item">
<view class="audience-info">
<view class="audience-avatar-container">
<image class="audience-avatar"
src="https://sdk-web-1252463788.cos.ap-hongkong.myqcloud.com/trtc/live/assets/Icon/defaultAvatar.png"
mode="aspectFill" />
</view>
<view class="audience-item-right">
<view class="audience-detail">
<text class="audience-name" :numberOfLines="1">{{ request.userName || request.userID}}</text>
</view>
<view class="request-actions">
<view class="action-btn accept" @tap="handleRequest(request, 'accept')">
<text class="btn-text">同意</text>
</view>
<view class="action-btn reject" @tap="handleRequest(request, 'reject')">
<text class="btn-text">拒绝</text>
</view>
</view>
</view>
</view>
<view class="audience-item-bottom-line"></view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-text">暂无邀请连麦</text>
</view> -->
<!-- </view> -->
<view v-if="isLoading" class="loading-state">
<image src="/static/images/more.png" mode="aspectFit" class="loading-image" />
</view>
</scroll-view>
<!-- <view class="drawer-content">
<view class="divider-line-container">
<view class="divider-line"></view>
</view>
</view> -->
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
computed
} from 'vue'
import {
useCoGuestState
} from "@/uni_modules/tuikit-atomic-x/state/CoGuestState";
const {
applicants,
invitableGuests,
acceptApplication,
cancelApplication,
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
},
activeTab: {
type: String,
default: 'invite',
},
})
const isLoading = ref(false);
const currentCursor = ref(0);
const scrollTop = ref(0);
const emit = defineEmits(['update:modelValue'])
function close() {
emit('update:modelValue', false);
}
const handleScroll = (e) => {
if (this.currentCursor.value === 0) return;
const {
scrollHeight,
scrollTop
} = e.detail;
scrollTop.value = scrollTop;
if (scrollHeight - scrollTop.value < 100) {
// this.loadAudiences(this.currentCursor);
}
};
const handleRequest = (audience, action) => {
console.warn(`${action} request from ${JSON.stringify(audience)}`);
if (action === 'accept') {
acceptApplication({
liveID: uni?.$liveID,
userID: audience?.userID,
success: () => {
console.log('acceptApplication success.');
},
fail: (errCode, errMsg) => {
console.error(`acceptApplication fail errCode: ${errCode}, errMsg: ${errMsg}`);
},
});
return;
}
if (action === 'reject') {
rejectApplication({
liveID: uni?.$liveID,
userID: audience?.userID,
success: () => {
console.log('rejectApplication success.');
},
fail: (errCode, errMsg) => {
console.error(`rejectCoGuestRequest fail errCode: ${errCode}, errMsg: ${errMsg}`);
},
});
uni.$localGuestStatus = 'IDLE'
}
};
const startLink = (audience) => {
console.warn('发起连线');
};
</script>
<style>
.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%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
flex-direction: column;
height: 1000rpx;
}
.drawer-open {
transform: translateY(0);
}
.audience-header {
padding: 40rpx 0;
background-color: rgba(34, 38, 46, 1);
position: relative;
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;
}
.tab-text {
font-size: 32rpx;
}
.active-text {
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-content {
flex: 1;
padding: 0 48rpx;
}
.audience-grid {
flex-direction: column;
}
.audience-item {
padding: 24rpx 0;
position: relative;
}
.audience-info {
flex-direction: row;
align-items: center;
}
.audience-avatar-container {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 24rpx;
}
.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-name {
font-size: 32rpx;
color: #ffffff;
max-width: 200rpx;
lines: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.start-link {
padding: 12rpx 32rpx;
border-radius: 100rpx;
background-color: rgba(43, 106, 214, 1);
}
.start-link.waiting {
background-color: rgba(58, 60, 66, 1);
}
.link-text {
color: #ffffff;
font-size: 28rpx;
}
.request-actions {
flex-direction: row;
}
.action-btn {
padding: 12rpx 32rpx;
border-radius: 100rpx;
margin-left: 16rpx;
}
.action-btn.accept {
background-color: rgba(43, 106, 214, 1);
}
.action-btn.reject {
background-color: rgba(58, 60, 66, 1);
}
.btn-text {
color: #ffffff;
font-size: 28rpx;
}
.audience-item-bottom-line {
position: absolute;
border-bottom-width: 1rpx;
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;
}
.empty-text {
color: #999999;
font-size: 28rpx;
}
.loading-image {
width: 48rpx;
height: 48rpx;
}
.drawer-content {
padding: 24rpx 0;
}
.divider-line-container {
height: 68rpx;
justify-content: center;
position: relative;
}
.divider-line {
width: 268rpx;
height: 10rpx;
border-radius: 200rpx;
background-color: #ffffff;
position: absolute;
bottom: 16rpx;
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<view class="bottom-drawer-container" v-if="modelValue">
<view class="drawer-overlay" @tap="close"></view>
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }">
<view class="drawer-header">
<text style="color: rgba(213, 224, 242, 1); font-size: 32rpx;">选择连麦方式</text>
<text
style="font-size: 24rpx; font-weight: 400; color: rgba(124, 133, 166, 1); margin-top: 20rpx;">选择连麦方式,主播同意后接通</text>
</view>
<view class="drawer-content">
<view class="drawer-actions">
<view style="height: 2rpx; color: rgba(79, 88, 107, 0.3); width: 750rpx; background: #fff; opacity: 0.2;">
</view>
<view style="display: flex; flex-direction: row; align-items: center; padding: 30rpx;"
@click="handleSendCoGuest('video')">
<image src="/static/images/mode.png" style="width: 36rpx; height: 36rpx;"></image>
<text
style="font-size: 32rpx; font-weight: 400; color: rgba(213, 224, 242, 1); padding-left: 10rpx;">申请视频连麦</text>
</view>
<view style="height: 2rpx; color: rgba(79, 88, 107, 0.3); width: 750rpx; background: #fff; opacity: 0.2;">
</view>
<view style="display: flex; flex-direction: row; align-items: center; padding: 30rpx;"
@click="handleSendCoGuest('mic')">
<image src="/static/images/live-comic.png" style="width: 36rpx; height: 36rpx;"></image>
<text
style="font-size: 32rpx; font-weight: 400; color: rgba(213, 224, 242, 1); padding-left: 10rpx;">申请语音连麦</text>
</view>
</view>
<!-- <view class="divider-line-container">
<view class="divider-line"></view>
</view> -->
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { watch, onMounted, ref, onUnmounted } from 'vue';
import { useDeviceState } from "@/uni_modules/tuikit-atomic-x/state/DeviceState";
import { useCoGuestState } from "@/uni_modules/tuikit-atomic-x/state/CoGuestState";
import { useLoginState } from "@/uni_modules/tuikit-atomic-x/state/LoginState";
const { loginUserInfo } = useLoginState()
const currentCoGuestType = ref('')
const {
// 响应式状态 - 麦克风相关
microphoneStatus, microphoneStatusReason, microphoneLastError, hasPublishAudioPermission, captureVolume,
// 响应式状态 - 摄像头相关
cameraStatus, cameraStatusReason, cameraLastError,
// 响应式状态 - 其他设备相关
currentAudioRoute, isScreenSharing, isFrontCamera, screenStatus, screenStatusReason,
// 操作方法 - 麦克风相关callback在params中
openLocalMicrophone, closeLocalMicrophone, muteLocalAudio, unmuteLocalAudio, setAudioRoute,
// 操作方法 - 摄像头相关callback在params中
openLocalCamera, closeLocalCamera, switchCamera, switchMirror, updateVideoQuality,
// 操作方法 - 屏幕共享相关callback在params中
startScreenShare, stopScreenShare,
} = useDeviceState(uni?.$liveID);
const {
// 响应式状态
invitees, applicants,
applyForSeat,
addCoGuestHostListener, removeCoGuestHostListener,
addCoGuestGuestListener, removeCoGuestGuestListener
} = useCoGuestState(uni?.$liveID);
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
liveID: {
type: String,
},
userID: {
type: String,
default: '',
},
seatIndex: {
type: Number,
default: -1,
},
});
const emit = defineEmits(['update:modelValue']);
const close = () => {
emit('update:modelValue', false);
};
onMounted(() => {
addCoGuestGuestListener(uni.$liveID, 'onGuestApplicationResponded', handleGuestApplicationResponded)
addCoGuestGuestListener(uni.$liveID, 'onGuestApplicationNoResponse', handleGuestApplicationNoResponse)
})
onUnmounted(() => {
currentCoGuestType.value = ''
removeCoGuestGuestListener(uni.$liveID, 'onGuestApplicationResponded', handleGuestApplicationResponded)
removeCoGuestGuestListener(uni.$liveID, 'onGuestApplicationNoResponse', handleGuestApplicationNoResponse)
})
const handleGuestApplicationResponded = {
callback: (event) => {
const res = JSON.parse(event)
if (res.isAccept) {
uni.$localGuestStatus = 'CONNECTED'
if (currentCoGuestType.value === 'video') {
openLocalCamera({
isFront: true,
success: () => {
console.log('openLocalCamera success.');
},
fail: (errCode, errMsg) => {
console.error(`openLocalCamera fail errCode: ${errCode}, errMsg: ${errMsg}`);
},
});
}
openLocalMicrophone({
success: () => {
console.log('openLocalMicrophone success.');
},
fail: (errCode, errMsg) => {
console.error(`openLocalMicrophone fail errCode: ${errCode}, errMsg: ${errMsg}`);
},
});
} else {
uni.$localGuestStatus = 'IDLE'
uni.showToast({
title: '上麦申请被拒绝',
icon: 'none',
duration: 2000,
});
}
}
}
const handleGuestApplicationNoResponse = {
callback: (event) => {
const res = JSON.parse(event)
if (res.reason === 'TIMEOUT') {
uni.$localGuestStatus = 'IDLE'
uni.showToast({
title: '上麦申请超时',
icon: 'none',
duration: 2000,
});
}
}
}
const handleSendCoGuest = (type : string) => {
console.log(`goGuest localStatus: ${uni.$localGuestStatus}`);
currentCoGuestType.value = type
if (uni.$localGuestStatus === 'USER_APPLYING') {
console.log(`cancel userID: ${props?.userID}`);
uni.showToast({
title: '你已提交了连麦申请 \n请勿重复申请',
icon: 'none',
duration: 2000,
position: 'center',
});
close();
return
}
if (uni.$localGuestStatus === 'IDLE') {
uni.showToast({
title: '你提交了连麦申请 \n请等待主播同意',
icon: 'none',
duration: 2000,
position: 'center',
});
applyForSeat({
liveID: props.liveID,
seatIndex: props.seatIndex, // 申请上麦传 -1, 随机分配麦位, xinlxin 反馈
timeout: 30,
});
uni.$localGuestStatus = 'USER_APPLYING'
close();
return
}
}
</script>
<style>
.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%);
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease;
flex-direction: column;
height: 500rpx;
}
.drawer-open {
transform: translateY(0);
}
.drawer-header {
padding: 48rpx;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.drawer-header-title {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.9);
}
.drawer-content {
flex: 1;
}
.drawer-actions {
display: flex;
flex-direction: column;
/* justify-content: space-around; */
/* justify-content: flex-start; */
}
.action-btn {
flex-direction: column;
align-items: center;
margin-right: 40rpx;
flex: 1;
height: 150rpx;
}
.action-btn-image-container {
width: 100rpx;
height: 100rpx;
background-color: rgba(43, 44, 48, 1);
margin-bottom: 12rpx;
border-radius: 25rpx;
justify-content: center;
align-items: center;
}
.action-btn-image {
width: 50rpx;
height: 50rpx;
}
.action-btn-content {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
}
.divider-line-container {
height: 68rpx;
justify-content: center;
position: relative;
}
.divider-line {
width: 268rpx;
height: 10rpx;
border-radius: 200rpx;
background-color: #ffffff;
position: absolute;
bottom: 16rpx;
}
.camera-mic-setting {
flex: 1;
background-color: #1f1024;
}
</style>