Files
uniapp-im-shop/pages/anchor/index.nvue
cbb 20ccbf1f14 注释搜索:主播发送消息
观看列表只能主播跟管理员查看
2026-02-10 17:49:33 +08:00

1426 lines
35 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="live-container"
@click="handleHideInput"
:style="{
height: systemInfo?.windowHeight + 'px',
width: systemInfo?.safeArea?.width + 'px'
}"
>
<LiveStreamView
v-if="liveID.length > 0"
:liveID="liveID || ''"
:isAnchor="true"
:templateLayout="templateLayout"
:isLiving="isStartLive"
:enableClickPanel="true"
:onStreamViewClick="ShowAnchorViewClickPanel"
></LiveStreamView>
<view
class="navigate-back-arrow"
@tap="goBackToTabBar"
v-if="!isStartLive"
>
<image
class="navigate-back-arrow-image"
src="/static/images/left-arrow.png"
></image>
</view>
<!-- 顶部信息栏 -->
<view class="header" v-if="isStartLive">
<view class="header-left">
<view class="stream-info" @tap="showAnchorInfoDrawer">
<image
class="avatar"
:src="loginUserInfo?.avatarURL || defaultAvatarURL"
mode="aspectFill"
/>
<view class="stream-details">
<text class="stream-title" :numberOfLines="1">
{{ loginUserInfo.nickname || loginUserInfo.userID || '' }}
</text>
</view>
</view>
</view>
<view class="header-right">
<view class="participants" @tap="showAudienceList">
<view v-for="(user, index) in audienceList.slice(0, 2)">
<image
class="participant-avatar"
:src="user?.avatarURL || defaultAvatarURL"
mode="aspectFill"
/>
</view>
<view class="participant-count">
<text v-if="topNUmber" class="count-text">
{{ Number(topNUmber) > 100 ? '99+' : topNUmber }}
</text>
<text v-else class="count-text">{{ audienceCount }}</text>
</view>
</view>
<view class="control-icons" @tap="navigateBack()">
<!-- <image class="control-icon" src="/static/images/live-share.png" /> -->
<image
class="control-icon"
src="/static/images/live-end.png"
/>
</view>
</view>
</view>
<!-- 主播开播前画面区域 -->
<BeforeLivePanel
v-if="!isStartLive"
:coverURL="coverURL"
:liveCategory="liveCategory"
:liveMode="liveMode"
:templateLayout="templateLayout"
:liveTitle="liveTitle"
@editCover="editCover"
@editTitle="editTitle"
@chooseCategory="chooseCategory"
@chooseMode="chooseMode"
@chooseTemplate="chooseTemplate"
@startLive="startLive"
@camera="handleCamera"
@settings="handleSettings"
ref="beforeLivePanelRef"
/>
<!-- 主播直播画面区域 -->
<view class="live-content" v-if="isStartLive">
<view
class="live-network-container"
@tap="isShowLiveStatusInfoCard = true"
>
<image
class="live-network"
src="/static/images/network-good.png"
alt=""
/>
<text class="live-timer">{{ liveDurationText }}</text>
</view>
<view
class="go-guest-request-container"
v-if="(applicants || []).length > 0 && isShowCoGuestPanelAvatar"
@tap="showCoGuestPanel('requests')"
>
<view
class="avatar-overlay-container"
:style="getAvatarContainerStyle()"
>
<!-- 显示前两张申请者的头像 -->
<image
v-for="(applicant, index) in applicants.slice(0, 2)"
:key="index"
class="go-guest-request-img"
:style="getAvatarStyle(index, Math.min(applicants.length, 3))"
:src="applicant?.avatarURL || defaultAvatarURL"
/>
<!-- 第三张图片始终显示默认头像 -->
<image
v-if="applicants.length >= 3"
class="go-guest-request-img"
:style="getAvatarStyle(2, 3)"
src="/static/images/live-more.png"
/>
</view>
<text class="go-guest-request-text">
申请连麦({{ (applicants || []).length }})
</text>
</view>
<BarrageList
mode="anchor"
:bottomPx="(safeArea.height * 1) / 8"
@itemTap="audienceOperator"
ref="barrageListRef"
/>
<view
class="live-bottom-Panel"
:style="{ width: safeArea.width + 'px' }"
>
<BarrageInput></BarrageInput>
<view class="action-buttons">
<view class="action-button-item" @tap="showCoHostPanel">
<image
class="action-button-icon"
src="/static/images/link-host.png"
mode="aspectFit"
/>
<text class="action-button-text">连主播</text>
</view>
<!-- <view class="action-button-item">
<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="userAddActivity">
<image
class="action-button-icon"
src="/static/images/activity.png"
mode="aspectFit"
/>
<text class="action-button-text">活动</text>
</view>
<view
class="action-button-item"
@tap="showCoGuestPanel('requests')"
>
<image
class="action-button-icon"
src="/static/images/link-guest.png"
mode="aspectFit"
/>
<text class="action-button-text">连观众</text>
</view>
<view class="action-button-item" @tap="handleSettings">
<image
class="action-button-icon"
src="/static/images/live-more.png"
mode="aspectFit"
/>
<text class="action-button-text">更多</text>
</view>
<Like role="anchor" />
</view>
</view>
<!-- 活动弹框 -->
<Activity
v-model="isShowActivity"
:roomId="roomDataId"
:groupID="groupId"
:creatorType="creatorType"
></Activity>
<LiveAudienceList v-model="isShowAudienceList" @adminBack="handleAdminBack"></LiveAudienceList>
<AudienceActionPanel
v-if="liveID && isShowAudienceActionPanel"
v-model="isShowAudienceActionPanel"
:userInfo="selectedAudience"
:liveID="liveID"
@adminBack="handleAdminBack"
></AudienceActionPanel>
<CoGuestPanel
v-if="liveID"
v-model="isShowCoGuestPanel"
:activeTab="activeTab"
></CoGuestPanel>
<CoHostPanel
v-if="liveID"
v-model="isShowCoHostPanel"
></CoHostPanel>
<LiveMoreActionsPanel
v-if="liveID"
v-model="isShowLiveMoreActionsPanel"
:currentLive="currentLive"
></LiveMoreActionsPanel>
<UserInfoPanel
v-if="liveID"
v-model="isShowUserInfoPanel"
:userInfo="clickUserInfo"
:isShowAnchor="isShowAnchorInfo"
></UserInfoPanel>
</view>
<GiftPlayer
ref="giftPlayerRef"
v-model="isLargeSizeGiftPlayer"
:url="giftInfo?.resourceURL"
:safeArea="safeArea"
@finished="svgaPlayerFinished"
/>
<LiveStatusInfoCard v-model="isShowLiveStatusInfoCard" />
<ActionSheet
v-model="isShowEndSheet"
:title="endSheetTitle"
:itemList="endSheetItems"
@select="onEndSheetSelect"
/>
<!-- 自定义Modal组件 -->
<view class="custom-modal-overlay" v-if="showCustomModal">
<view class="custom-modal" @click.stop>
<view class="modal-content">
<text class="modal-text">{{ modalContent }}</text>
</view>
<view class="modal-actions">
<view
class="modal-btn modal-btn-cancel"
@tap="handleModalCancel"
>
<text class="modal-btn-reject">拒绝</text>
</view>
<view
class="modal-btn modal-btn-confirm"
@tap="handleModalConfirm"
>
<text class="modal-btn-accept">接受</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app'
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
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'
import LiveAudienceList from '@/uni_modules/tuikit-atomic-x/components/LiveAudienceList/LiveAudienceList.nvue'
import CoGuestPanel from '@/uni_modules/tuikit-atomic-x/components/CoGuestPanel/CoGuestPanel.nvue'
import CoHostPanel from '@/uni_modules/tuikit-atomic-x/components/CoHostPanel.nvue'
import AudienceActionPanel from '@/uni_modules/tuikit-atomic-x/components/LiveAudienceList/AudienceActionPanel.nvue'
import LiveMoreActionsPanel from '../../components/LiveMoreActionsPanel.nvue'
import LiveStreamView from '@/uni_modules/tuikit-atomic-x/components/LiveStreamView/LiveStreamView.nvue'
import GiftPlayer from '@/uni_modules/tuikit-atomic-x/components/GiftPlayer/GiftPlayer.nvue'
import { giftService } from '@/uni_modules/tuikit-atomic-x/components/GiftPlayer/giftService'
import Like from '@/uni_modules/tuikit-atomic-x/components/Like.nvue'
import BarrageInput from '@/uni_modules/tuikit-atomic-x/components/BarrageInput.vue'
import BarrageList from '@/uni_modules/tuikit-atomic-x/components/BarrageList.nvue'
import { useLiveListState } from '@/uni_modules/tuikit-atomic-x/state/LiveListState'
import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState'
import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState'
import { useBarrageState } from '@/uni_modules/tuikit-atomic-x/state/BarrageState'
import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState'
import { useCoHostState } from '@/uni_modules/tuikit-atomic-x/state/CoHostState'
import { useLiveSeatState } from '@/uni_modules/tuikit-atomic-x/state/LiveSeatState'
import { useLoginState } from '@/uni_modules/tuikit-atomic-x/state/LoginState'
import { useAudioEffectState } from '@/uni_modules/tuikit-atomic-x/state/AudioEffectState'
import { useBaseBeautyState } from '@/uni_modules/tuikit-atomic-x/state/BaseBeautyState'
import { useGiftState } from '@/uni_modules/tuikit-atomic-x/state/GiftState'
import { useLiveSummaryState } from '@/uni_modules/tuikit-atomic-x/state/LiveSummaryState'
import ActionSheet from '@/components/ActionSheet.nvue'
import Activity from './components/activity.nvue'
import { LIVE_BUSINESS } from '@/constants/live-keys'
const liveID = ref(uni.$liveID)
const dom = uni.requireNativePlugin('dom')
const { loginUserInfo } = useLoginState()
// uni.$liveID = `live_${uni.$userID}`
uni.$localGuestStatus = 'IDLE'
const {
setVoiceEarMonitorEnable,
setVoiceEarMonitorVolume,
setAudioChangerType,
setAudioReverbType
} = useAudioEffectState(liveID.value)
const {
setSmoothLevel,
setWhitenessLevel,
setRuddyLevel,
whitenessLevel,
ruddyLevel,
smoothLevel
} = useBaseBeautyState(liveID.value)
const {
addCoHostListener,
removeCoHostListener,
acceptHostConnection,
rejectHostConnection,
exitHostConnection,
coHostStatus,
invitees
} = useCoHostState(liveID.value)
const {
joinLive,
createLive,
fetchLiveList,
liveList,
endLive,
currentLive,
liveListCursor,
callExperimentalAPI
} = useLiveListState()
const { applicants, rejectApplication, connected } = useCoGuestState(
liveID.value
)
const { messageList, sendTextMessage, sendCustomMessage } =
useBarrageState(liveID.value)
const {
openLocalCamera,
openLocalMicrophone,
isFrontCamera,
switchCamera,
closeLocalMicrophone,
closeLocalCamera
} = useDeviceState(liveID.value)
const { audienceList } = useLiveAudienceState(liveID.value)
const { seatList, canvas, lockSeat } = useLiveSeatState(liveID.value)
const { addGiftListener, removeGiftListener } = useGiftState(
liveID.value
)
const defaultCoverURL =
'https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover1.png'
const defaultAvatarURL =
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png'
const topNUmber = ref('')
const audienceCount = computed(() => audienceList.value.length)
const isShowEndSheet = ref(false)
const endSheetTitle = ref('')
const endSheetItems = ref(['关闭直播间'])
const systemInfo = ref({})
const safeArea = ref({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 375,
height: 750
})
const beforeLivePanelRef = ref()
// 活动弹框状态
const isShowActivity = ref(false)
const isShowUserInfoPanel = ref(false)
const isShowAudienceList = ref(false)
const isShowCoHostPanel = ref(false)
const isShowCoGuestPanel = ref(false)
const isShowAudienceActionPanel = ref(false)
const isShowLiveMoreActionsPanel = ref(false)
const isShowLiveStatusInfoCard = ref(false)
// 主播直播状态
const isStartLive = ref(false)
// 通过点击 message 信息选中的观众
const selectedAudience = ref({})
const isLargeSizeGiftPlayer = ref(false)
const giftInfo = ref({})
const activeTab = ref('requests')
// 自定义Modal相关状态
const showCustomModal = ref(false)
const modalContent = ref('')
const currentModalUserInfo = ref(null)
const coverURL = ref(defaultCoverURL)
const liveCategory = ref('日常聊天')
const liveMode = ref('公开')
const templateLayout = ref(600)
const liveTitle = ref(loginUserInfo.value?.nickname)
const currentUserID = ref(loginUserInfo.value?.userID)
const clickUserInfo = ref({})
const isShowCoGuestPanelAvatar = ref(true)
const barrageListRef = ref()
const giftPlayerRef = ref()
const { showGift, onGiftFinished } = giftService({
roomId: liveID.value,
giftPlayerRef
})
const isShowAnchorInfo = ref(true)
// 监听用户名变化,更新 liveTitle
watch(
() => loginUserInfo.value?.nickname,
(newUserName, oldUserName) => {
if (newUserName) {
// 如果当前标题是默认值或者为空,则更新为新的用户名
if (!liveTitle.value || liveTitle.value === oldUserName) {
liveTitle.value = newUserName
}
}
},
{ immediate: true, deep: true }
)
watch(
() => loginUserInfo.value?.userID,
(newUserId, oldUserId) => {
if (newUserId) {
// 如果当前标题是默认值或者为空,则更新为新的用户名
currentUserID.value = newUserId
}
},
{ 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
}
}
})
const editCover = (data: string) => {
coverURL.value = data
}
const editTitle = (data: string) => {
liveTitle.value = data
}
const chooseCategory = (data: string) => {
liveCategory.value = data
}
const chooseMode = (data: string) => {
liveMode.value = data
}
const chooseTemplate = (data: number) => {
templateLayout.value = data
}
/** 设置管理员回调(发送自定义消息) */
const handleAdminBack = (e) => {
const data = {
liveID: liveID.value,
businessID: LIVE_BUSINESS.ANCHOR,
data: JSON.stringify({
...e,
count: `${e.userName}${e.show ? '成为' : '撤销'}管理员`
})
}
sendCustomMessage(data)
}
// 自定义Modal相关方法
const showCustomModalDialog = userInfo => {
currentModalUserInfo.value = userInfo
modalContent.value = `${
userInfo.userName || userInfo.userID
}向你发来连线邀请`
showCustomModal.value = true
setTimeout(() => {
closeCustomModal()
}, 30000)
}
// 关闭Modal
const closeCustomModal = () => {
showCustomModal.value = false
currentModalUserInfo.value = null
}
// 处理确认操作
const handleModalConfirm = () => {
if (currentModalUserInfo.value) {
acceptHostConnection({
liveID: liveID.value,
fromHostLiveID: currentModalUserInfo.value.liveID
})
}
closeCustomModal()
}
// 处理取消操作
const handleModalCancel = () => {
if (currentModalUserInfo.value) {
rejectHostConnection({
liveID: liveID.value,
fromHostLiveID: currentModalUserInfo.value.liveID
})
}
isShowCoGuestPanelAvatar.value = true
closeCustomModal()
}
watch(
applicants,
(newVal, oldVal) => {
if (!isShowCoGuestPanelAvatar.value) {
applicants.value.forEach(applicant => {
rejectApplication({
liveID: liveID.value,
userID: applicant.userID
})
})
}
},
{
deep: true,
immediate: true
}
)
watch(
coHostStatus,
newVal => {
if (newVal === 'DISCONNECTED') {
isShowCoGuestPanelAvatar.value = true
}
},
{
deep: true,
immediate: true
}
)
watch(invitees, newVal => {
if (!newVal && !isShowCoGuestPanelAvatar.value) {
isShowCoGuestPanelAvatar.value = true
} else {
applicants.value.forEach(applicant => {
rejectApplication({
liveID: liveID.value,
userID: applicant.userID
})
})
}
})
// 聊天渲染与自动滚动由 ChatList 组件处理
const handleReceiveGift = {
callback: event => {
const res = JSON.parse(event)
showGiftToast(res?.gift || {})
}
}
// 显示礼物提示
const showGiftToast = async (giftData?: any) => {
if (!giftData) return
giftInfo.value = giftData
showGift(giftData, {
onlyDisplay: true,
isFromSelf: false
})
// 使用 BarrageList 的 showToast 方法显示礼物提示
if (barrageListRef.value) {
barrageListRef.value.showToast({
name:
giftData?.sender?.userName || giftData?.sender?.userID || '',
avatarURL: giftData?.sender?.avatarURL || '',
desc: giftData?.name || '',
iconURL: giftData?.iconURL || '',
duration: 3000
})
}
}
const groupId = ref('')
const creatorType = ref('')
// 页面加载
onLoad(options => {
// uni.$liveID = options.roomId
liveID.value = options.roomId
creatorType.value = options?.creatorType
groupId.value = decodeURIComponent(options?.groupId)
// 禁用右滑返回(仅 iOS 有效)
if (uni.getSystemInfoSync().platform === 'ios') {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
currentPage.$page.style.disableSwipeBack = true
}
}
uni.setKeepScreenOn({
keepScreenOn: true
})
openLocalCamera({
isFront: true
})
openLocalMicrophone()
uni.getSystemInfo({
success: res => {
systemInfo.value = res
safeArea.value = res.safeArea
console.warn(`systemInfo: ${systemInfo.value.windowHeight}`)
}
})
addCoHostListener(
liveID.value,
'onCoHostRequestAccepted',
handleCoHostRequestAccepted
)
addCoHostListener(
liveID.value,
'onCoHostRequestRejected',
handleCoHostRequestRejected
)
addCoHostListener(
liveID.value,
'onCoHostRequestTimeout',
handleCoHostRequestTimeout
)
addCoHostListener(
liveID.value,
'onCoHostRequestReceived',
handleCoHostRequestReceived
)
addGiftListener(liveID.value, 'onReceiveGift', handleReceiveGift)
})
onMounted(() => {
})
onUnmounted(() => {
removeCoHostListener(
liveID.value,
'onCoHostRequestAccepted',
handleCoHostRequestAccepted
)
removeCoHostListener(
liveID.value,
'onCoHostRequestRejected',
handleCoHostRequestRejected
)
removeCoHostListener(
liveID.value,
'onCoHostRequestTimeout',
handleCoHostRequestTimeout
)
removeCoHostListener(
liveID.value,
'onCoHostRequestReceived',
handleCoHostRequestReceived
)
removeGiftListener(liveID.value, 'onReceiveGift', handleReceiveGift)
})
const handleCoHostRequestAccepted = {
callback: event => {
uni.showToast({
icon: 'none',
title: '连线被同意'
})
}
}
const handleCoHostRequestRejected = {
callback: event => {
uni.showToast({
icon: 'none',
title: '连线被拒绝'
})
}
}
const handleCoHostRequestTimeout = {
callback: event => {
uni.showToast({
icon: 'none',
title: '连线请求超时'
})
isShowCoGuestPanelAvatar.value = true
}
}
// 添加活动
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
) {
isShowCoGuestPanelAvatar.value = false
}
const res = JSON.parse(event)
if (connected.value.length > 1 || applicants.value.length > 0) {
rejectHostConnection({
liveID: liveID.value,
fromHostLiveID: res.inviter.liveID
})
return
}
// 使用自定义Modal替代uni.showModal
showCustomModalDialog(res.inviter)
}
}
const clearAudioEffectSet = () => {
setVoiceEarMonitorEnable({
enable: false
})
setVoiceEarMonitorVolume({
volume: 100
})
setAudioChangerType({
changerType: 'NONE'
})
setAudioReverbType({
reverbType: 'NONE'
})
}
const handleHideInput = () => {
uni.hideKeyboard()
}
const clearBeautyPanelSet = () => {
setSmoothLevel({ smoothLevel: 0 })
setWhitenessLevel({ whitenessLevel: 0 })
setRuddyLevel({ ruddyLevel: 0 })
}
// 未开直播:退出
const goBackToTabBar = () => {
clearAudioEffectSet()
clearBeautyPanelSet()
closeLocalMicrophone()
closeLocalCamera()
console.warn(` 后退 `)
uni.navigateBack({
delta: 1,
// url: '/TUIKit/components/TUIChat/index',
success: () => {
console.log(`redirect success`)
},
fail: error => {
console.error(`redirect, error: ${JSON.stringify(error)}`)
}
})
}
const navigateBack = () => {
if (coHostStatus.value === 'CONNECTED') {
endSheetItems.value = ['断开连线', '关闭直播间']
endSheetTitle.value =
'当前处于连线状态,是否需要「断开连线」或「关闭直播间」'
}
isShowEndSheet.value = true
}
const { summaryData } = useLiveSummaryState(liveID.value)
const onEndSheetSelect = (res: { tapIndex: number }) => {
const index = res.tapIndex
if (coHostStatus.value === 'CONNECTED' && index === 0) {
exitHostConnection({
liveID: liveID.value
})
endSheetItems.value = ['关闭直播间']
endSheetTitle.value = ''
return
}
if (
(coHostStatus.value === 'CONNECTED' && index === 1) ||
(coHostStatus.value !== 'CONNECTED' && index === 0)
) {
clearAudioEffectSet()
clearBeautyPanelSet()
uni.$summaryData = summaryData.value
console.warn(` 退出直播===`, summaryData.value)
imDataEndLive(roomDataId.value, audienceCount.value).then(() => {
uni.redirectTo({ url: '/pages/liveend/index' })
})
// ===================原本代码
// endLive({
// success: () => {
// console.warn(` 退出直播 imDataEndLive`)
// uni.redirectTo({ url: '/pages/liveend/index' })
// }
// })
}
}
const handleCamera = () => {
switchCamera({ isFront: !isFrontCamera.value })
}
const handleSettings = () => {
isShowLiveMoreActionsPanel.value = true
}
const roomDataId = ref('')
const startLive = async () => {
console.log(uni.$liveID,'=====', liveID.value,'点击开始直播')
try {
const data = {
coverUrl: coverURL.value,
roomName: liveTitle.value,
groupId: groupId.value,
roomId: liveID.value
}
const roomData = await imAddLive(data)
const roomId = roomData.data.roomId
const res = await imDataStartLive(roomId)
console.log('========11111111', roomData)
console.log('========22222222', res)
roomDataId.value = liveID.value
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('====22', err)
}
// ======================原本代码
// createLive({
// liveInfo: {
// liveID: liveID.value,
// 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) => {
// console.log('============1111',errCode)
// console.log('============2222',errMsg)
// uni.showToast({
// title: '创建直播间失败'
// })
// }
// })
// isStartLive.value = true
}
const ShowAnchorViewClickPanel = userInfo => {
;(clickUserInfo.value = userInfo), showUserInfoPanel()
isShowAnchorInfo.value = false
}
const showUserInfoPanel = () => {
isShowUserInfoPanel.value = true
}
const setLocalVideoMuteImage = () => {
const tempFilePath = '../../static/images/live-mute-local-video.png' // 本地存放的图片文件
let imageFilePath = ''
uni.saveFile({
tempFilePath: tempFilePath,
success: res => {
console.warn('保存文件成功 = ', JSON.stringify(res)) // 获取的是相对路径
imageFilePath = res.savedFilePath
imageFilePath = plus.io.convertLocalFileSystemURL(imageFilePath) // 转绝对路径
const data = {
api: 'setLocalVideoMuteImage',
params: { image: imageFilePath }
}
callExperimentalAPI({
jsonData: JSON.stringify(data)
})
},
fail: err => {
console.error('保存文件失败')
}
})
}
const showAudienceList = () => {
isShowAudienceList.value = true
}
const showAnchorInfoDrawer = () => {
isShowAnchorInfo.value = true
clickUserInfo.value = {
...(currentLive?.value.liveOwner || {}),
liveID: currentLive?.value.liveID || ''
}
showUserInfoPanel()
}
const audienceOperator = (message: any) => {
console.warn(`click message: ${JSON.stringify(message)}`)
if (message?.sender?.userID === currentLive?.value.liveOwner.userID)
return
if (message?.sender?.userID === currentUserID.value) {
return
}
const targetAudience = audienceList.value.find(
audience => audience.userID === message?.sender?.userID
)
if (!targetAudience) {
isShowAnchorInfo.value = false
clickUserInfo.value = message?.sender
showUserInfoPanel()
} else {
selectedAudience.value = targetAudience
console.warn(
`click message selectedAudience: ${JSON.stringify(
selectedAudience.value
)}`
)
isShowAudienceActionPanel.value = true
}
}
// 直播中控制面板操作
const showCoHostPanel = () => {
if (connected.value.length > 1) {
uni.showToast({
title: '连麦中,不可以使用连线功能',
icon: 'none'
})
return
}
console.warn('-> go host Panel')
const params = {
cursor: '', // 首次拉起传空不能是null),然后根据回调数据的cursor确认是否拉完
count: 20 // 分页拉取的个数
}
fetchLiveList(params)
isShowCoHostPanel.value = true
}
const showCoGuestPanel = (activeTabValue: string = 'requests') => {
console.warn(`go guest Panel activeTab = ${activeTabValue}`)
activeTab.value = activeTabValue
isShowCoGuestPanel.value = true
}
// 计时器相关
import {
ref as vueRef,
onMounted as vueOnMounted,
onUnmounted as vueOnUnmounted
} from 'vue'
const liveDuration = vueRef(0) // 秒
const liveDurationText = vueRef('00:00:00')
let timer: any = null
watch(isStartLive, val => {
if (val) {
liveDuration.value = 0
updateLiveDurationText()
timer = setInterval(() => {
liveDuration.value++
updateLiveDurationText()
}, 1000)
} else {
clearInterval(timer)
timer = null
liveDuration.value = 0
updateLiveDurationText()
}
})
function updateLiveDurationText() {
const h = String(Math.floor(liveDuration.value / 3600)).padStart(
2,
'0'
)
const m = String(
Math.floor((liveDuration.value % 3600) / 60)
).padStart(2, '0')
const s = String(liveDuration.value % 60).padStart(2, '0')
liveDurationText.value = `${h}:${m}:${s}`
}
vueOnUnmounted(() => {
if (timer) clearInterval(timer)
})
const svgaPlayerFinished = () => {
isLargeSizeGiftPlayer.value = false
onGiftFinished()
}
// 头像叠加配置
const avatarOverlayConfig = {
size: 80, // 头像大小 (rpx)
overlapRatio: 0.6, // 覆盖比例 (0.5 = 50%覆盖, 0.6 = 60%覆盖)
enableScale: false, // 是否启用缩放效果
enableOpacity: false // 是否启用透明度效果
}
// 计算头像容器样式,确保水平垂直居中
const getAvatarContainerStyle = () => {
const count = applicants.value?.length || 0
if (count === 0) return {}
// 使用配置计算叠加偏移量
const overlapOffset =
avatarOverlayConfig.size * (1 - avatarOverlayConfig.overlapRatio)
// 容器总宽度 = 头像大小 + (头像数量 - 1) * 偏移量
const containerWidth =
avatarOverlayConfig.size + (count - 1) * overlapOffset
return {
position: 'relative',
width: `${containerWidth}rpx`,
height: `${avatarOverlayConfig.size}rpx`,
margin: '0 auto' // 水平居中
}
}
// 计算每个头像的样式,实现真正的叠加效果
const getAvatarStyle = (index: number, totalCount: number) => {
// 使用配置计算叠加偏移量
const overlapOffset =
avatarOverlayConfig.size * (1 - avatarOverlayConfig.overlapRatio)
// 计算每个头像的left位置后面的头像向左偏移覆盖前面的头像
const left = index * overlapOffset
// 基础样式
const baseStyle = {
position: 'absolute',
left: `${left}rpx`,
top: '0',
width: `${avatarOverlayConfig.size}rpx`,
height: `${avatarOverlayConfig.size}rpx`,
borderRadius: `${avatarOverlayConfig.size / 2}rpx`
}
// 添加视觉效果
if (avatarOverlayConfig.enableScale) {
baseStyle.transform = `scale(${1 - index * 0.05})` // 后面的头像稍微小一点
}
if (avatarOverlayConfig.enableOpacity) {
baseStyle.opacity = 1 - index * 0.1 // 后面的头像稍微透明一点
}
return baseStyle
}
</script>
<style>
.live-container {
flex: 1;
position: relative;
background: rgba(15, 16, 20, 0.5);
overflow: hidden;
}
.navigate-back-arrow {
position: absolute;
top: 130rpx;
left: 60rpx;
z-index: 1000;
width: 60rpx;
height: 60rpx;
}
.navigate-back-arrow-image {
width: 20rpx;
height: 35rpx;
}
.header {
display: flex;
position: absolute;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 20rpx 32rpx;
margin-top: 80rpx;
width: 750rpx;
top: 40rpx;
}
.header-left {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
}
.stream-info {
display: flex;
flex-direction: row;
align-items: center;
background-color: rgba(0, 0, 0, 0.3);
padding: 6rpx 10rpx;
border-radius: 40rpx;
}
.avatar {
width: 56rpx;
height: 56rpx;
border-radius: 30rpx;
border-width: 2rpx;
border-color: #ffffff;
margin-right: 16rpx;
}
.stream-details {
display: flex;
flex-direction: column;
}
.stream-title {
color: #ffffff;
font-size: 28rpx;
font-weight: 500;
margin-bottom: 4rpx;
width: 120rpx;
height: 40rpx;
lines: 1;
}
.header-right {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
width: 300rpx;
}
.participants {
display: flex;
flex-direction: row;
align-items: center;
}
.participant-avatar {
width: 36rpx;
height: 36rpx;
border-radius: 24rpx;
border-width: 2rpx;
border-color: #ffffff;
margin-right: 8rpx;
}
.participant-count {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.count-text {
color: #ffffff;
font-size: 24rpx;
font-weight: 500;
}
.control-icons {
display: flex;
flex-direction: row;
align-items: center;
}
.control-icon {
width: 40rpx;
height: 40rpx;
margin-left: 20rpx;
}
.live-content {
flex: 1;
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.go-guest-request-container {
position: fixed;
top: 280rpx;
right: 30rpx;
width: 200rpx;
height: 200rpx;
border-radius: 40rpx;
background: #4f586b;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* 头像叠加容器样式 */
.avatar-overlay-container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.go-guest-request-img {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
transition: all 0.3s ease;
/* 确保图片内容正确显示 */
object-fit: cover;
}
.go-guest-request-text {
margin-top: 30rpx;
font-size: 24rpx;
color: #fff;
}
.live-bottom-Panel {
flex: 1;
position: fixed;
bottom: 40rpx;
left: 20rpx;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.action-buttons {
flex-direction: row;
align-items: center;
padding-right: 70rpx;
}
.action-button-item {
width: 64rpx;
height: 92rpx;
margin-left: 24rpx;
justify-content: center;
align-items: center;
}
.action-button-icon {
width: 56rpx;
height: 56rpx;
margin-bottom: 4rpx;
}
.action-button-text {
color: #fff;
font-size: 20rpx;
}
.live-network {
width: 36rpx;
height: 36rpx;
}
.live-network-container {
position: fixed;
top: 220rpx;
right: 10rpx;
width: 180rpx;
height: 40rpx;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 45rpx;
flex-direction: row;
display: flex;
justify-content: center;
align-items: center;
}
.live-timer {
color: #fff;
font-size: 24rpx;
margin-left: 12rpx;
}
/* 自定义Modal样式 */
.custom-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.custom-modal {
width: 500rpx;
background-color: #ffffff;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.3);
flex-direction: column;
align-items: center;
}
.modal-content {
flex: 1;
flex-direction: column;
align-items: center;
margin-top: 40rpx;
}
.modal-text {
color: #000000;
font-size: 32rpx;
font-weight: 600;
text-align: center;
line-height: 40rpx;
margin-bottom: 30rpx;
max-width: 400rpx;
word-wrap: break-word;
word-break: break-all;
white-space: normal;
}
.modal-actions {
flex-direction: row;
justify-content: space-around;
align-items: center;
border-top: 2rpx solid rgba(213, 224, 242, 1);
width: 500rpx;
}
.modal-btn {
width: 250rpx;
padding: 28rpx;
justify-content: center;
align-items: center;
}
.modal-btn-confirm {
border-left: 2rpx solid rgba(213, 224, 242, 1);
}
.modal-btn-reject {
font-size: 32rpx;
font-weight: bold;
color: rgba(79, 88, 107, 1);
}
.modal-btn-accept {
font-size: 32rpx;
font-weight: bold;
color: rgba(28, 102, 229, 1);
}
</style>