添加直播间功能,直播间右上角人数需要完善

This commit is contained in:
cbb
2026-01-14 17:57:17 +08:00
parent 0c88d29dce
commit db1b797b68
10 changed files with 632 additions and 87 deletions

View File

@@ -1,11 +1,12 @@
<script setup>
import { reactive } from 'vue'
import { useUI } from '../../../utils/use-ui'
import { TUIGroupService } from '@tencentcloud/chat-uikit-engine-lite'
import { useAuthUser } from '../../../composables/useAuthUser'
import { addLiveActivity } from '@/api/tui-kit'
import { useBarrageState } from '@/uni_modules/tuikit-atomic-x/state/BarrageState';
import { LIVE_BUSINESS } from '@/constants/live-keys'
const { showToast } = useUI()
const { tencentUserSig } = useAuthUser()
const props = defineProps({
modelValue: {
@@ -35,6 +36,9 @@
}
})
const { sendCustomMessage } = useBarrageState(props.roomId);
const emit = defineEmits(['update:modelValue'])
/**
@@ -86,12 +90,6 @@
})
const submitForm = async () => {
const res = await TUIGroupService.getGroupMemberProfile({
groupID: props.groupID,
userIDList: [tencentUserSig.value.userId]
})
console.log(res)
if (!formData.title) {
showToast('请填写标题')
return
@@ -134,7 +132,18 @@
creatorType: props.creatorType,
roomId: props.roomId
}
console.log(data)
const res = await addLiveActivity(data)
sendCustomMessage({
liveID: props.roomId,
businessID: LIVE_BUSINESS.SIGN,
data: JSON.stringify(res.data)
});
formData.title = ''
formData.endTime = ''
formData.rewardValue = ''
formData.maxParticipants = ''
close()
}
</script>

View File

@@ -75,7 +75,7 @@
<image class="action-button-icon" src="/static/images/host-pk.png" mode="aspectFit" />
<text class="action-button-text">主播pk</text>
</view> -->
<view class="action-button-item" @tap="isShowActivity = true">
<view class="action-button-item" @tap="userAddActivity">
<image class="action-button-icon" src="/static/images/activity.png" mode="aspectFit" />
<text class="action-button-text">活动</text>
</view>
@@ -91,7 +91,7 @@
</view>
</view>
<!-- 活动弹框 -->
<Activity v-model="isShowActivity" :roomId="roomDataId" :groupID="groupId"></Activity>
<Activity v-model="isShowActivity" :roomId="roomDataId" :groupID="groupId" :creatorType="creatorType"></Activity>
<LiveAudienceList v-model="isShowAudienceList"></LiveAudienceList>
<AudienceActionPanel v-if="liveID" v-model="isShowAudienceActionPanel" :userInfo="selectedAudience"
:liveID="liveID"></AudienceActionPanel>
@@ -130,7 +130,7 @@
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app';
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import { imAddLive, imDataStartLive, imDataEndLive } from '@/api/tui-kit'
import { imAddLive, imDataStartLive, getLiveActivityDetail, imDataEndLive } from '@/api/tui-kit'
import BeforeLivePanel from '../../components/BeforeLivePanel.nvue';
import LiveStatusInfoCard from '@/uni_modules/tuikit-atomic-x/components/LiveStatusInfoCard.nvue';
import UserInfoPanel from '@/uni_modules/tuikit-atomic-x/components/LiveStreamView/UserInfoPanel.nvue';
@@ -376,8 +376,10 @@
}
};
const groupId = ref('')
const creatorType = ref('')
// 页面加载
onLoad((options) => {
creatorType.value = options?.creatorType
groupId.value = decodeURIComponent(options?.groupId)
// 禁用右滑返回(仅 iOS 有效)
if (uni.getSystemInfoSync().platform === 'ios') {
@@ -475,6 +477,23 @@
}
// 添加活动
const userAddActivity = () => {
getLiveActivityDetail(roomDataId.value).then(res => {
if (res?.data && res.data.status === 1) {
// status: 0-未开始 1-进行中 2-已结束 3-已取消
uni.showModal({
title: `提示`,
content: '您有一个活动正在进行中,请勿重复添加活动',
showCancel: false,
confirmText: '确定'
})
} else {
isShowActivity.value = true
}
})
};
const handleCoHostRequestReceived = {
callback: (event) => {
if (isShowCoGuestPanelAvatar.value && applicants.value.length === 0) {
@@ -581,65 +600,65 @@
const roomDataId = ref('')
const startLive = async () => {
// try {
// console.log('点击开始直播')
// const data = {
// coverUrl: coverURL.value,
// roomName: liveTitle.value,
// groupId: groupId.value
// }
// const roomData = await imAddLive(data)
// const roomId = roomData.data.roomId
// uni.$liveID = roomId
// liveID.value = roomId
// const res = await imDataStartLive(roomId)
// console.log(roomData, '========11111')
// console.log(res, '========22222')
// roomDataId.value = roomId
// const params = {
// cursor: "", // 首次拉起传空不能是null),然后根据回调数据的cursor确认是否拉完
// count: 20, // 分页拉取的个数
// };
// joinLive({ liveID: roomId })
// fetchLiveList(params);
// openLocalCamera({ isFront: isFrontCamera.value });
// openLocalMicrophone();
// setLocalVideoMuteImage();
// isStartLive.value = true;
// } catch (err) {
// console.log(err, '====22')
// }
try {
console.log('点击开始直播')
const data = {
coverUrl: coverURL.value,
roomName: liveTitle.value,
groupId: groupId.value
}
const roomData = await imAddLive(data)
const roomId = roomData.data.roomId
uni.$liveID = roomId
liveID.value = roomId
const res = await imDataStartLive(roomId)
console.log(roomData, '========11111')
console.log(res, '========22222')
roomDataId.value = roomId
const params = {
cursor: "", // 首次拉起传空不能是null),然后根据回调数据的cursor确认是否拉完
count: 20, // 分页拉取的个数
};
joinLive({ liveID: roomId })
fetchLiveList(params);
openLocalCamera({ isFront: isFrontCamera.value });
openLocalMicrophone();
setLocalVideoMuteImage();
isStartLive.value = true;
} catch (err) {
console.log(err, '====22')
}
// ======================原本代码
createLive({
liveInfo: {
liveID: uni?.$liveID,
liveName: liveTitle.value,
coverURL: coverURL.value,
isSeatEnabled: true,
seatMode: 'APPLY',
maxSeatCount: 0,
isPublicVisible: liveMode.value === '公开',
keepOwnerOnSeat: true,
seatLayoutTemplateID: templateLayout.value,
},
success: () => {
const params = {
cursor: "", // 首次拉起传空不能是null),然后根据回调数据的cursor确认是否拉完
count: 20, // 分页拉取的个数
};
fetchLiveList(params);
// createLive({
// liveInfo: {
// liveID: uni?.$liveID,
// liveName: liveTitle.value,
// coverURL: coverURL.value,
// isSeatEnabled: true,
// seatMode: 'APPLY',
// maxSeatCount: 0,
// isPublicVisible: liveMode.value === '公开',
// keepOwnerOnSeat: true,
// seatLayoutTemplateID: templateLayout.value,
// },
// success: () => {
// const params = {
// cursor: "", // 首次拉起传空不能是null),然后根据回调数据的cursor确认是否拉完
// count: 20, // 分页拉取的个数
// };
// fetchLiveList(params);
openLocalCamera({ isFront: isFrontCamera.value });
openLocalMicrophone();
setLocalVideoMuteImage();
},
fail: (errCode, errMsg) => {
uni.showToast({
title: '创建直播间失败',
});
},
});
isStartLive.value = true;
// openLocalCamera({ isFront: isFrontCamera.value });
// openLocalMicrophone();
// setLocalVideoMuteImage();
// },
// fail: (errCode, errMsg) => {
// uni.showToast({
// title: '创建直播间失败',
// });
// },
// });
// isStartLive.value = true;
};
const ShowAnchorViewClickPanel = (userInfo) => {

View File

@@ -0,0 +1,414 @@
<script setup>
import { ref, watch, reactive } from 'vue'
import { useUI } from '../../../utils/use-ui'
import { confirmLiveActivity } from '@/api/tui-kit'
const { showToast } = useUI()
const showData = defineModel('info', {
type: Object,
default: () => ({})
})
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
/**
* 直播间ID
*/
roomId: {
type: String,
default: ''
},
// // 显示数据
// showData: {
// type: Object,
// default: () => ({})
// }
})
const loading = ref(true)
// 响应式倒计时状态
const countdown = ref({ type: 'second', value: 0 })
const formData = reactive({
title: '',
rewardValue: '',
endTime: '',
activityId: '',
/** 用户是否参与活动 */
isParticipated: false
})
// 定时器引用
let timer = null
/**
* 根据结束时间返回倒计时(>=1分钟时按分钟倒计时<1分钟时按秒倒计时
*/
const getSmartCountdown = endTime => {
// 兼容 Safari将空格替换为 'T' 以符合 ISO 8601
const end = new Date(endTime.replace(' ', 'T'))
const now = new Date()
const diffMs = end - now
if (diffMs <= 0) {
return { type: 'second', value: 0 }
}
const totalSeconds = Math.floor(diffMs / 1000)
return { type: 'second', value: totalSeconds }
}
/** 启动倒计时 */
const startCountdown = () => {
// 先清除可能存在的旧定时器
stopCountdown()
const update = () => {
countdown.value = getSmartCountdown(formData.endTime)
// 如果已结束可选择是否继续更新这里仍每秒更新但值为0
if (countdown.value.value <= 0) {
showData.value = {}
close()
}
}
update() // 立即更新一次
timer = setInterval(update, 1000)
}
/** 停止倒计时 */
const stopCountdown = () => {
if (timer) {
clearInterval(timer)
timer = null
}
}
const getData = async () => {
loading.value = true
formData.isParticipated = showData.value.isParticipated
formData.endTime = showData.value.endTime
formData.title = showData.value.title
formData.rewardValue = showData.value.rewardValue
formData.activityId = showData.value.activityId
startCountdown()
loading.value = false
}
watch(
() => props.modelValue,
newVal => {
if (newVal) {
getData()
} else {
stopCountdown()
}
}
)
const emit = defineEmits(['update:modelValue'])
const close = () => {
emit('update:modelValue', false)
}
const submitForm = async () => {
const data = {
activityId: formData.activityId,
roomId: showData.value.roomId
}
console.log('确认活动:', data)
await confirmLiveActivity(data)
await showToast('参与活动成功', 'success')
showData.value.isParticipated = true
close()
}
</script>
<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="drawer-header">
<text class="drawer-title">{{ formData.title }}</text>
<text v-if="formData.isParticipated" class="drawer-done" @tap="close">关闭</text>
<text v-if="!formData.isParticipated" class="drawer-done" @tap="submitForm">确定参与</text>
</view>
<view class="setting-item">
<text class="setting-label">结束时间</text>
<view class="live-list-quick-join">
<text class="text">{{ countdown.value }} </text>
</view>
</view>
<view class="setting-item">
<text class="setting-label">奖励值积分</text>
<view class="live-list-quick-join">
<text class="text" style="color: #e6431a">
{{ formData.rewardValue }} 积分
</text>
</view>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.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;
padding: 32rpx;
}
.drawer-open {
transform: translateY(0);
}
.drawer-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
}
.drawer-title {
font-size: 32rpx;
color: #ffffff;
font-weight: 500;
}
.drawer-done {
font-size: 32rpx;
color: #2b65fb;
}
.setting-item {
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 88rpx;
border-bottom-width: 1px;
border-bottom-color: rgba(255, 255, 255, 0.1);
.live-list-quick-join {
flex-direction: row;
align-items: center;
.text {
color: #ffffff;
font-size: 32rpx;
}
}
.quick-join-input {
flex: 1;
width: 360rpx;
height: 64rpx;
border-radius: 999rpx;
padding: 10rpx 20rpx;
margin-top: 20rpx;
font-size: 28rpx;
text-align: right;
}
}
.setting-label {
font-size: 28rpx;
color: #ffffff;
}
.setting-value {
flex-direction: row;
align-items: center;
}
.setting-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
margin-right: 8rpx;
}
.setting-arrow {
width: 24rpx;
height: 24rpx;
}
.volume-settings {
margin-top: 32rpx;
}
.section-title {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 24rpx;
}
.slider-item {
margin-bottom: 24rpx;
display: flex;
flex-direction: column;
}
.slider-label {
font-size: 28rpx;
color: #ffffff;
margin-bottom: 16rpx;
}
/* 自定义控制区域样式 */
.custom-slider {
flex: 1;
flex-direction: row;
align-items: center;
margin: 0 20rpx;
}
.control-btn {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
display: flex;
justify-content: center;
align-items: center;
border: 2rpx solid #2b6ad6;
}
.minus-btn {
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(43, 106, 214, 0.1);
}
.plus-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: #2b6ad6;
}
.btn-text {
font-size: 32rpx;
font-weight: bold;
color: #ffffff;
}
.plus-btn .btn-text {
color: #ffffff;
}
.progress-section {
flex: 1;
margin: 0 20rpx;
align-items: center;
flex-direction: row;
justify-content: center;
}
.progress-bar {
width: 400rpx;
height: 8rpx;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4rpx;
position: relative;
overflow: hidden;
margin-right: 16rpx;
}
.progress-fill {
height: 8rpx;
background-color: #2b6ad6;
border-radius: 4rpx;
}
.current-value {
font-size: 24rpx;
color: #ffffff;
font-weight: 600;
text-align: center;
z-index: 10;
}
.voice-effects,
.reverb-effects {
margin-top: 32rpx;
}
.effects-grid {
flex-direction: row;
flex-wrap: wrap;
margin: 0 -8rpx;
}
.effect-item {
margin: 8rpx;
/* background-color: rgba(255, 255, 255, 0.1); */
justify-content: center;
align-items: center;
}
.effect-active {
background-color: rgba(43, 101, 251, 0.2);
border-width: 2rpx;
border-color: #2b65fb;
}
.effect-icon-container {
width: 112rpx;
height: 112rpx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 16rpx;
background-color: rgba(255, 255, 255, 0.1);
margin-bottom: 12rpx;
}
.effect-icon {
width: 48rpx;
height: 48rpx;
margin-bottom: 8rpx;
}
.effect-name {
font-size: 24rpx;
color: #ffffff;
}
</style>

View File

@@ -16,7 +16,7 @@
<view class="stream-info">
<image class="avatar" :src="currentLive?.liveOwner?.avatarURL || defaultAvatarURL" mode="aspectFill" />
<view class="stream-details">
<text class="stream-title"
<text class="stream-title"
:numberOfLines="1">{{ currentLive?.liveOwner?.userName || currentLive?.liveOwner?.userID}}</text>
</view>
<!-- <view
@@ -36,7 +36,8 @@
<image class="participant-avatar" :src="user?.avatarURL || defaultAvatarURL" mode="aspectFill" />
</view>
<view class="participant-count">
<text class="count-text">{{ audienceList.length }}</text>
<text v-if="topNUmber" class="count-text">{{ audienceList.length }}</text>
<text v-else class="count-text">{{ audienceList.length }}</text>
</view>
</view>
<view class="control-icons" @click.stop="navigateBack()">
@@ -58,6 +59,7 @@
<view class="footer">
<BarrageInput></BarrageInput>
<view class="action-buttons">
<image v-if="activityData?.id" class="action-btn" @click="isShowActivity = true" src="/static/images/activity.png" />
<image class="action-btn" @click="showNetworkQualityPanel()" src="/static/images/dashboard.png" />
<image class="action-btn" @click="showGiftPicker()" src="/static/images/live-gift.png" />
<image class="action-btn" :class="{ 'disabled': shouldDisableCoGuestButton }"
@@ -71,6 +73,8 @@
</view>
</view>
<!-- 活动信息 :showData="activityData" -->
<ActivityInfo v-model="isShowActivity" v-model:info="activityData" :roomId="liveID" ></ActivityInfo>
<UserInfoPanel v-model="isShowUserInfoPanel" :userInfo="clickUserInfo" :isShowAnchor="isShowAnchorInfo">
</UserInfoPanel>
<LiveAudienceList v-model="isShowAudienceList"></LiveAudienceList>
@@ -94,7 +98,7 @@
</template>
<script setup lang="ts">
import { imDataEndLive } from '@/api/tui-kit'
import { imDataEndLive, getLiveActivityDetail, getLiveActivityRecord } from '@/api/tui-kit'
import { onLoad } from '@dcloudio/uni-app';
import { ref, onMounted, computed, onUnmounted, watch, nextTick } from 'vue';
import UserInfoPanel from '@/uni_modules/tuikit-atomic-x/components/LiveStreamView/UserInfoPanel.nvue';
@@ -127,8 +131,13 @@
const { disconnect, connected, cancelApplication } = useCoGuestState(uni?.$liveID)
const { addGiftListener, removeGiftListener } = useGiftState(uni?.$liveID);
const { connected: hostConnected } = useCoHostState(uni?.$liveID)
import ActivityInfo from './components/activity-info.vue'
import { LIVE_BUSINESS } from '@/constants/live-keys'
const dom = uni.requireNativePlugin('dom')
const isShowActivity = ref(false)
const activityData = ref({})
const systemInfo = ref({});
const safeArea = ref({
left: 0,
@@ -185,6 +194,7 @@
const exitSheetItems = ref(['退出直播间'])
const coGuestSheetItems = ref(['取消连麦申请'])
const coGuestSheetTitle = ref('')
const topNUmber = ref('')
// 监听座位变化:当自身不在 seatList 时,将本地连麦状态重置为 IDLE
@@ -201,11 +211,45 @@
}
}, { immediate: true, deep: true });
// 监听自定义消息列表更新
watch(messageList, (newMessages) => {
if (newMessages && newMessages.length > 0) {
console.log('弹幕消息列表更新:', newMessages);
if (newMessages.some(v => v.businessID === LIVE_BUSINESS.ADMIN)) {
console.log('管理员消息====================', newMessages.find(v => v.businessID === LIVE_BUSINESS.ADMIN))
topNUmber.value = newMessages.find(v => v.businessID === LIVE_BUSINESS.ADMIN).data;
}
if (newMessages.some(v => v.businessID === LIVE_BUSINESS.SIGN)) {
activityData.value = {
...JSON.parse(newMessages.find(v => v.businessID === LIVE_BUSINESS.SIGN)?.data),
isParticipated: false
};
isShowActivity.value = true;
}
}
});
// 页面加载
onLoad((options) => {
console.warn('Live page onLoad = ', options);
liveID.value = options?.liveID;
getLiveActivityDetail(liveID.value).then(res => {
if (res?.data && res.data.status === 1) {
// status: 0-未开始 1-进行中 2-已结束 3-已取消
console.log('活动数据============= ', res.data);
getLiveActivityRecord(res.data.activityId).then(show => {
activityData.value = {
...res.data,
isParticipated: show.data
};
console.log('是否参加============= ', show);
isShowActivity.value = !show.data;
})
} else {
isShowActivity.value = false;
}
})
if (liveID.value) {
joinLive({
liveID: liveID.value,

View File

@@ -1,14 +1,15 @@
<script setup>
import { useUserStore } from '@/stores/user'
import { navigateTo } from '../../../utils/router'
// 基础设置
const basicSetting = [
{ name: '字体大小', value: '', url: '' },
{ name: '聊天背景', value: '', url: '' },
// { name: '聊天背景', value: '', url: '' },
{ name: '朋友圈设置', value: '', url: '' },
{ name: '消息通知', value: '', url: '' },
{ name: '安全设置', value: '', url: '' },
{ name: '群发消息', value: '', url: '' },
// { name: '安全设置', value: '', url: '' },
// { name: '群发消息', value: '', url: '' },
{ name: '登录设备', value: '', url: '' }
]
@@ -17,10 +18,14 @@
{ name: '隐私设置', value: '', url: '' },
{ name: '清除缓存', value: '', url: '' },
{ name: '意见反馈', value: '', url: '' },
{ name: '关于我们', value: '', url: '' }
{ name: '关于我们', value: '', url: '/pages/discover/company' }
]
const { clearUserInfo } = useUserStore()
const onItem = item => {
item.url && navigateTo(item.url)
}
</script>
<template>
@@ -30,6 +35,7 @@
v-for="(item, index) in basicSetting"
:key="index"
class="item"
@click="onItem(item)"
>
<text class="left-title">{{ item.name }}</text>
<view class="right-box">
@@ -44,6 +50,7 @@
v-for="(item, index) in systemSetting"
:key="index"
class="item"
@click="onItem(item)"
>
<text class="left-title">{{ item.name }}</text>
<view class="right-box">