需要添加直播接口

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

1113
pages/anchor/index.nvue Normal file

File diff suppressed because it is too large Load Diff

818
pages/audience/index.nvue Normal file
View File

@@ -0,0 +1,818 @@
<template>
<view class="live-container" @click="handleHideInput" :style="{
height: systemInfo?.windowHeight + 'px',
width: systemInfo?.safeArea?.width + 'px',
}">
<!-- 主播画面区域 -->
<view class="live-content">
<LiveStreamView v-if="liveID.length > 0" :liveID="liveID" :isAnchor="false" :templateLayout="templateLayout"
:currentLoginUserId="currentLoginUserId" :onStreamViewClick="ShowAudienceViewClickPanel"
:enableClickPanel="true" :isLiving="true">
</LiveStreamView>
<!-- 顶部信息栏 -->
<view class="live-header">
<view class="header-left" @click="showAnchorInfoDrawer">
<view class="stream-info">
<image class="avatar" :src="currentLive?.liveOwner?.avatarURL || defaultAvatarURL" mode="aspectFill" />
<view class="stream-details">
<text class="stream-title"
:numberOfLines="1">{{ currentLive?.liveOwner?.userName || currentLive?.liveOwner?.userID}}</text>
</view>
<!-- <view
class="follow-btn"
:class="{ 'followed': isFollowed }"
@click.stop="handleFollowClick"
>
<text :style="isFollowed ? 'color: #338aff; font-size: 28rpx;' : 'color: #fff; font-size: 28rpx;'">
{{ isFollowed ? '已关注' : '关注' }}
</text>
</view> -->
</view>
</view>
<view class="header-right">
<view class="participants" @click="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 class="count-text">{{ audienceList.length }}</text>
</view>
</view>
<view class="control-icons" @click.stop="navigateBack()">
<!-- <image class="control-icon" src="/static/images/live-share.png" /> -->
<image class="control-icon" src="/static/images/close.png" />
</view>
</view>
</view>
<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>
<!-- 聊天消息列表 -->
<BarrageList mode="audience" :bottomPx="safeArea.height * 1/8" @itemTap="audienceOperator" ref="barrageListRef" />
<!-- 底部互动区域 -->
<view class="footer">
<BarrageInput></BarrageInput>
<view class="action-buttons">
<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 }"
v-if="templateLayout !== 200 && uni.$localGuestStatus === 'IDLE'" @click="handleCoGuestButtonClick"
src="/static/images/link-guest.png" />
<image class="action-btn" v-if="templateLayout !== 200 && uni.$localGuestStatus === 'USER_APPLYING'"
@click="ShowCoGuestRequestPanel()" src="/static/images/live-request.png" />
<image class="action-btn" v-if="templateLayout !== 200 && uni.$localGuestStatus === 'CONNECTED'"
@click="ShowCoGuestRequestPanel()" src="/static/images/live-disconnect.png" />
<Like />
</view>
</view>
<UserInfoPanel v-model="isShowUserInfoPanel" :userInfo="clickUserInfo" :isShowAnchor="isShowAnchorInfo">
</UserInfoPanel>
<LiveAudienceList v-model="isShowAudienceList"></LiveAudienceList>
<CoGuestRequestPanel v-model="isShowCoGuestRequestPanel" :liveID="currentLive.liveID" :userID="currentLoginUserId"
:seatIndex="seatIndex"></CoGuestRequestPanel>
<GiftPicker v-model="isShowGiftPicker" :onGiftSelect="showGiftToast"></GiftPicker>
<GiftPlayer ref="giftPlayerRef" v-model="isLargeSizeGiftPlayer" :url="giftInfo?.resourceURL" :safeArea="safeArea"
@finished="svgaPlayerFinished" />
<NetworkQualityPanel v-model="isShowNewWorkPanel"></NetworkQualityPanel>
</view>
</view>
<LiveStatusInfoCard v-model="isShowLiveStatusInfoCard" videoQuality="4K" audioMode="高保真人声" :audioVolume="70"
:latency="45" :downLoss="0" :upLoss="2" />
<ActionSheet v-model="isShowExitSheet" :title="exitSheetTitle" :itemList="exitSheetItems"
@select="onExitSheetSelect" />
<ActionSheet v-model="isShowCoGuestSheet" :title="coGuestSheetTitle" :itemList="coGuestSheetItems"
@select="onCoGuestSheetSelect" />
</template>
<script setup lang="ts">
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';
import LiveAudienceList from '@/uni_modules/tuikit-atomic-x/components/LiveAudienceList/LiveAudienceList.nvue';
import CoGuestRequestPanel from '@/uni_modules/tuikit-atomic-x/components/CoGuestPanel/CoGuestRequestPanel.nvue';
import GiftPicker from '@/uni_modules/tuikit-atomic-x/components/GiftPicker.nvue';
import NetworkQualityPanel from '@/uni_modules/tuikit-atomic-x/components/NetworkQualityPanel.nvue'
import LiveStatusInfoCard from '@/uni_modules/tuikit-atomic-x/components/LiveStatusInfoCard.nvue';
import Like from '@/uni_modules/tuikit-atomic-x/components/Like.nvue';
import LiveStreamView from '@/uni_modules/tuikit-atomic-x/components/LiveStreamView/LiveStreamView.nvue';
import BarrageInput from '@/uni_modules/tuikit-atomic-x/components/BarrageInput.vue';
import BarrageList from '@/uni_modules/tuikit-atomic-x/components/BarrageList.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 { useBarrageState } from "@/uni_modules/tuikit-atomic-x/state/BarrageState";
import { useLiveListState } from "@/uni_modules/tuikit-atomic-x/state/LiveListState";
import { useLiveSeatState } from "@/uni_modules/tuikit-atomic-x/state/LiveSeatState";
import { useLiveAudienceState } from '@/uni_modules/tuikit-atomic-x/state/LiveAudienceState';
import { useCoGuestState } from '@/uni_modules/tuikit-atomic-x/state/CoGuestState';
import { useLoginState } from "@/uni_modules/tuikit-atomic-x/state/LoginState";
import { useGiftState } from "@/uni_modules/tuikit-atomic-x/state/GiftState";
import { useCoHostState } from "@/uni_modules/tuikit-atomic-x/state/CoHostState";
import ActionSheet from '@/components/ActionSheet.nvue'
uni.$localGuestStatus = 'IDLE'
const { loginUserInfo } = useLoginState()
const { messageList, sendTextMessage, sendCustomMessage } = useBarrageState(uni?.$liveID);
const { joinLive, createLive, fetchLiveList, liveList, leaveLive, currentLive, addLiveListListener, removeLiveListListener } = useLiveListState(uni?.$liveID);
const { seatList, addLiveSeatEventListener, removeLiveSeatEventListener } = useLiveSeatState(uni?.$liveID);
const { audienceList } = useLiveAudienceState(uni?.$liveID);
const { disconnect, connected, cancelApplication } = useCoGuestState(uni?.$liveID)
const { addGiftListener, removeGiftListener } = useGiftState(uni?.$liveID);
const { connected: hostConnected } = useCoHostState(uni?.$liveID)
const dom = uni.requireNativePlugin('dom')
const systemInfo = ref({});
const safeArea = ref({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 375,
height: 750,
});
const isShowUserInfoPanel = ref(false);
const isShowAudienceList = ref(false);
const isShowCoGuestRequestPanel = ref(false);
const isShowGiftPicker = ref(false);
const isShowLiveStatusInfoCard = ref(false);
const isLargeSizeGiftPlayer = ref(false);
const giftInfo = ref({});
const isShowNewWorkPanel = ref(false)
const selectedAudience = ref({});
const defaultCoverURL = '/static/images/default-background.jpg';
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
// 连麦按钮禁用状态
const isCoGuestButtonDisabled = ref(false);
// 计算连麦按钮是否应该被禁用
const shouldDisableCoGuestButton = computed(() => {
// 当 hostConnected 数组长度大于0时按钮置灰
return hostConnected.value.length > 0;
});
const liveID = ref('');
const isFollowed = ref(false);
const inputValue = ref("");
// Removed direct svga-player ref; handled inside GiftPlayer
const liveDuration = ref(0); // 秒
const liveDurationText = ref('00:00:00');
let timer : any = null;
const currentLoginUserId = ref();
const clickUserInfo = ref({});
const templateLayout = ref(600);
const seatIndex = ref(-1);
const barrageListRef = ref();
const giftPlayerRef = ref();
const { showGift, onGiftFinished } = giftService({
roomId: uni?.$liveID,
giftPlayerRef,
})
const isShowAnchorInfo = ref(true)
// action sheets
const isShowExitSheet = ref(false)
const isShowCoGuestSheet = ref(false)
const exitSheetTitle = ref('')
const exitSheetItems = ref(['退出直播间'])
const coGuestSheetItems = ref(['取消连麦申请'])
const coGuestSheetTitle = ref('')
// 监听座位变化:当自身不在 seatList 时,将本地连麦状态重置为 IDLE
watch(connected, (newList, oldList) => {
const list = Array.isArray(newList) ? newList : [];
const hasSelfInConnected = list.some(item => item?.userID === uni.$userID);
if (!hasSelfInConnected) {
uni.$localGuestStatus = 'IDLE'
}
}, { deep: true, immediate: true })
watch(() => loginUserInfo.value?.userID, (newUserId, oldUserId) => {
if (newUserId) {
currentLoginUserId.value = newUserId;
}
}, { immediate: true, deep: true });
// 页面加载
onLoad((options) => {
console.warn('Live page onLoad = ', options);
liveID.value = options?.liveID;
if (liveID.value) {
joinLive({
liveID: liveID.value,
success: () => {
liveDuration.value = 0;
updateLiveDurationText();
timer = setInterval(() => {
updateLiveDurationText();
}, 1000);
templateLayout.value = currentLive.value?.seatLayoutTemplateID || templateLayout.value;
console.log('joinLive success templateLayout: ', templateLayout.value);
},
fail: () => {
uni.showToast({ icon: 'none', title: "直播已结束" });
setTimeout(() => uni.redirectTo({ url: `/pages/livelist/index` }), 500);
},
});
return;
}
uni.showToast({ title: 'liveID 为空', icon: 'none' });
});
watch(currentLive, (newVal, oldVal) => {
if (newVal) {
templateLayout.value = newVal.seatLayoutTemplateID || templateLayout.value;
console.log(`currentLive change: ${JSON.stringify(newVal)}`);
}
});
function updateLiveDurationText() {
// 如果 currentLive 存在且有 createTime则基于创建时间计算
if (currentLive.value && currentLive.value.createTime) {
const currentTime = Math.floor(Date.now() / 1000); // 当前时间戳(秒)
const createTime = Math.floor(currentLive.value.createTime / 1000); // 直播间创建时间(秒)
const duration = Math.max(0, currentTime - createTime); // 直播时长(秒)
const h = String(Math.floor(duration / 3600)).padStart(2, '0');
const m = String(Math.floor((duration % 3600) / 60)).padStart(2, '0');
const s = String(duration % 60).padStart(2, '0');
liveDurationText.value = `${h}:${m}:${s}`;
}
}
onUnmounted(() => {
if (timer) clearInterval(timer);
removeLiveListListener('onLiveEnded', handleLiveEnded)
removeLiveListListener('onKickedOutOfLive', handleKickedOutOfLive)
removeGiftListener(uni.$liveID, 'onReceiveGift', handleReceiveGift)
removeLiveSeatEventListener(uni.$liveID, 'onLocalCameraOpenedByAdmin', handleLocalCameraOpenedByAdmin)
removeLiveSeatEventListener(uni.$liveID, 'onLocalCameraClosedByAdmin', handleLocalCameraClosedByAdmin)
removeLiveSeatEventListener(uni.$liveID, 'onLocalMicrophoneOpenedByAdmin', handleLocalMicrophoneOpenedByAdmin)
removeLiveSeatEventListener(uni.$liveID, 'onLocalMicrophoneClosedByAdmin', handleLocalMicrophoneClosedByAdmin)
});
onMounted(() => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.getSystemInfo({
success: (res) => {
systemInfo.value = res;
safeArea.value = res.safeArea;
}
});
addLiveListListener('onLiveEnded', handleLiveEnded)
addLiveListListener('onKickedOutOfLive', handleKickedOutOfLive)
addGiftListener(uni.$liveID, 'onReceiveGift', handleReceiveGift)
addLiveSeatEventListener(uni.$liveID, 'onLocalCameraOpenedByAdmin', handleLocalCameraOpenedByAdmin)
addLiveSeatEventListener(uni.$liveID, 'onLocalCameraClosedByAdmin', handleLocalCameraClosedByAdmin)
addLiveSeatEventListener(uni.$liveID, 'onLocalMicrophoneOpenedByAdmin', handleLocalMicrophoneOpenedByAdmin)
addLiveSeatEventListener(uni.$liveID, 'onLocalMicrophoneClosedByAdmin', handleLocalMicrophoneClosedByAdmin)
});
const handleLocalCameraOpenedByAdmin = {
callback: (event) => {
uni.showToast({
title: `您已被解除禁画`,
icon: 'none'
})
}
}
const handleLocalCameraClosedByAdmin = {
callback: (event) => {
uni.showToast({
title: `您已被禁画`,
icon: 'none'
})
}
}
const handleLocalMicrophoneOpenedByAdmin = {
callback: (event) => {
uni.showToast({
title: `您已被解除静音`,
icon: 'none'
})
}
}
const handleLocalMicrophoneClosedByAdmin = {
callback: (event) => {
uni.showToast({
title: `您已被静音`,
icon: 'none'
})
}
}
const handleLiveEnded = {
callback: (event) => {
if (timer) clearInterval(timer);
removeLiveListListener('onLiveEnded', handleLiveEnded)
removeLiveListListener('onKickedOutOfLive', handleKickedOutOfLive)
removeGiftListener(uni.$liveID, 'onReceiveGift', handleReceiveGift)
removeLiveSeatEventListener(uni.$liveID, 'onLocalCameraOpenedByAdmin', handleLocalCameraOpenedByAdmin)
removeLiveSeatEventListener(uni.$liveID, 'onLocalCameraClosedByAdmin', handleLocalCameraClosedByAdmin)
removeLiveSeatEventListener(uni.$liveID, 'onLocalMicrophoneOpenedByAdmin', handleLocalMicrophoneOpenedByAdmin)
removeLiveSeatEventListener(uni.$liveID, 'onLocalMicrophoneClosedByAdmin', handleLocalMicrophoneClosedByAdmin)
uni.showToast({
icon: 'none',
title: '直播已结束'
})
uni.$liveID = ''
uni.redirectTo({
url: `/pages/livelist/index`,
delta: 1,
success: () => {
console.log('返回成功');
},
fail: (err) => {
console.error('返回失败', err);
}
});
}
}
const handleKickedOutOfLive = {
callback: (event) => {
if (timer) clearInterval(timer);
removeLiveListListener('onLiveEnded', handleLiveEnded)
removeLiveListListener('onKickedOutOfLive', handleKickedOutOfLive)
removeGiftListener(uni.$liveID, 'onReceiveGift', handleReceiveGift)
removeLiveSeatEventListener(uni.$liveID, 'onLocalCameraOpenedByAdmin', handleLocalCameraOpenedByAdmin)
removeLiveSeatEventListener(uni.$liveID, 'onLocalCameraClosedByAdmin', handleLocalCameraClosedByAdmin)
removeLiveSeatEventListener(uni.$liveID, 'onLocalMicrophoneOpenedByAdmin', handleLocalMicrophoneOpenedByAdmin)
removeLiveSeatEventListener(uni.$liveID, 'onLocalMicrophoneClosedByAdmin', handleLocalMicrophoneClosedByAdmin)
uni.showToast({
icon: 'none',
title: '被踢出直播间'
})
uni.$liveID = ''
uni.redirectTo({
url: `/pages/livelist/index`,
delta: 1,
success: () => {
console.log('返回成功');
},
fail: (err) => {
console.error('返回失败', err);
}
});
}
}
const handleReceiveGift = {
callback: (event) => {
const res = JSON.parse(event)
if (res.sender.userID !== uni.$userID) {
showGiftToast(res?.gift || {}, true);
}
}
}
const audienceCount = computed(() => audienceList.value?.length);
watch(liveList, (newValue, oldValue) => {
for (let i = 0; i < (oldValue || []).length; i++) {
const liveNewInfo = (newValue || [])[i];
const liveOldInfo = (oldValue || [])[i];
if (!liveNewInfo || !liveOldInfo) continue;
if (!liveOldInfo?.isMessageDisable && liveNewInfo?.isMessageDisable) {
uni.showToast({
title: `${liveNewInfo?.liveOwner?.userName}被禁言`,
icon: 'none',
duration: 2000,
position: 'center',
});
}
}
}, { immediate: true, deep: true });
const audienceOperator = (message : any) => {
console.warn(`click message: ${JSON.stringify(message)}`);
if (message?.sender?.userID === loginUserInfo.value.userID) {
return;
}
clickUserInfo.value = { ...message?.sender || {}, liveID: uni?.$liveID };
console.warn(`click message clickUserInfo: ${JSON.stringify(clickUserInfo.value)}`);
isShowAnchorInfo.value = false
showUserInfoPanel();
};
const handleHideInput = () => {
uni.hideKeyboard()
}
const navigateBack = () => {
if (uni.$localGuestStatus === 'CONNECTED') {
exitSheetItems.value = ['断开连麦', '退出直播间']
exitSheetTitle.value = '当前处于连麦状态,是否需要「断开连麦」或「退出直播间」'
}
isShowExitSheet.value = true
};
const onExitSheetSelect = (res : { tapIndex : number }) => {
const index = res.tapIndex
// 当处于连麦索引0是“断开连麦”索引1是“退出直播间”否则只有“退出直播间”在索引0
if (uni.$localGuestStatus === 'CONNECTED' && index === 0) {
disconnect({
liveID: uni?.$liveID,
})
exitSheetItems.value = ['退出直播间']
exitSheetTitle.value = ''
uni.$localGuestStatus = 'IDLE'
return
}
if ((uni.$localGuestStatus === 'CONNECTED' && index === 1) || (uni.$localGuestStatus !== 'CONNECTED' && index === 0)) {
leaveLive({
success: () => {
uni.$liveID = ''
uni.redirectTo({
url: `/pages/livelist/index`,
delta: 1,
animationType: 'pop-out',
animationDuration: 300,
success: () => {
console.log('返回成功');
},
fail: (err) => {
console.error('返回失败', err);
}
});
}
});
}
}
const ShowAudienceViewClickPanel = (userInfo) => {
if (!userInfo) return;
if (userInfo?.userID === currentLive.value.liveOwner.userID) return
console.warn(`ShowAudienceViewClickPanel userID: ${userInfo?.userID}, currentLoginUserId: ${currentLoginUserId?.value}`);
clickUserInfo.value = userInfo;
isShowAnchorInfo.value = false
showUserInfoPanel();
};
const showAnchorInfoDrawer = () => {
isShowAnchorInfo.value = true
clickUserInfo.value = { ...(currentLive?.value.liveOwner || {}), liveID: currentLive?.value.liveID || '' }
showUserInfoPanel()
}
const showUserInfoPanel = () => {
isShowUserInfoPanel.value = true;
};
const showAudienceList = () => {
isShowAudienceList.value = true;
};
// 处理连麦按钮点击事件
const handleCoGuestButtonClick = () => {
if (shouldDisableCoGuestButton.value) {
return; // 如果按钮被禁用,直接返回
}
ShowCoGuestRequestPanel();
};
const ShowCoGuestRequestPanel = () => {
if (uni.$localGuestStatus === 'CONNECTED' || uni.$localGuestStatus === 'USER_APPLYING') {
if (uni.$localGuestStatus === 'USER_APPLYING') {
coGuestSheetItems.value = ['取消连麦申请']
} else if (uni.$localGuestStatus === 'CONNECTED') {
coGuestSheetItems.value = ['断开连麦']
}
isShowCoGuestSheet.value = true
} else {
isShowCoGuestRequestPanel.value = true;
}
};
const onCoGuestSheetSelect = (res : { tapIndex : number }) => {
if (uni.$localGuestStatus === 'CONNECTED') {
if (res.tapIndex === 0) {
disconnect({
liveID: uni?.$liveID,
})
uni.$localGuestStatus = 'IDLE'
}
return
}
if (uni.$localGuestStatus === 'USER_APPLYING') {
if (res.tapIndex === 0) {
cancelApplication({
liveID: uni?.$liveID,
})
uni.$localGuestStatus = 'IDLE'
}
}
}
const selectParticipant = (participant) => {
console.warn('选择参与者:', participant);
selectedAudience.value = participant;
// 可以在这里添加更多逻辑,比如显示参与者详情
};
const showGiftPicker = () => {
isShowGiftPicker.value = true;
};
const showNetworkQualityPanel = () => {
isShowNewWorkPanel.value = true;
}
const handleFollowClick = () => {
isFollowed.value = !isFollowed.value;
uni.showToast({
title: isFollowed.value ? '已关注' : '已取消关注',
icon: 'success',
duration: 1500
});
};
// 显示礼物提示
const showGiftToast = async (giftData ?: any, isOnlyDisplay : boolean = false) => {
if (!giftData) return;
const giftDataCopy = {
...giftData,
resourceURL: giftData.resourceURL ? String(giftData.resourceURL) : '',
name: giftData.name ? String(giftData.name) : '',
giftID: giftData.giftID || 0,
};
giftInfo.value = giftDataCopy;
const isFromSelf = !giftData?.sender || giftData?.sender?.userID === uni.$userID;
showGift(giftDataCopy, {
onlyDisplay: isOnlyDisplay,
isFromSelf: isFromSelf
});
// 使用 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 svgaPlayerFinished = () => {
isLargeSizeGiftPlayer.value = false;
onGiftFinished();
}
</script>
<style>
.live-container {
flex: 1;
position: relative;
width: 750rpx;
background: rgba(15, 16, 20, 1);
}
.live-content {
flex: 1;
position: relative;
width: 750rpx;
}
.live-background {
position: relative;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
}
.live-header {
position: absolute;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 20rpx 32rpx;
margin-top: 80rpx;
width: 750rpx;
z-index: 1000;
}
.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: 58rpx;
height: 58rpx;
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;
padding-left: 40rpx;
}
.participants {
display: flex;
flex-direction: row;
align-items: center;
}
.participant-avatar {
width: 48rpx;
height: 48rpx;
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: 48rpx;
height: 48rpx;
margin-left: 16rpx;
}
.footer {
flex: 1;
position: fixed;
left: 0;
right: 0;
bottom: 80rpx;
padding-left: 32rpx;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 686rpx;
}
.input-wrapper {
position: relative;
background: rgba(34, 38, 46, 0.5);
border-radius: 50%;
display: flex;
flex-direction: row;
align-items: center;
height: 72rpx;
padding-left: 40rpx;
color: #ffffff;
font-size: 28rpx;
width: 260rpx;
border: 1px solid rgba(255, 255, 255, 0.14);
}
.input-prefix {
position: absolute;
display: flex;
flex-direction: row;
align-items: center;
top: 50rpx;
left: 230rpx;
flex: 1;
}
.input-emoji {
width: 36rpx;
height: 36rpx;
}
.action-buttons {
position: fixed;
right: 40rpx;
bottom: 80rpx;
flex-direction: row;
align-items: center;
}
.action-btn {
width: 64rpx;
height: 64rpx;
margin-left: 16rpx;
}
.action-btn.disabled {
opacity: 0.5;
filter: grayscale(100%);
pointer-events: none;
}
.follow-btn {
padding: 0 16rpx;
height: 56rpx;
background: #338aff;
color: #fff;
border-radius: 32rpx;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
/* width: 88rpx; */
}
.follow-btn.followed {
background: #fff;
color: #338aff;
border: 2rpx solid #338aff;
}
.live-network {
width: 36rpx;
height: 36rpx;
}
.live-network-container {
position: fixed;
top: 200rpx;
right: 30rpx;
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;
}
</style>

View File

@@ -8,7 +8,10 @@
{ name: '朋友圈', icon: 'circle' },
{ name: '线上商城', icon: 'mall' },
{ name: '我的拼团', icon: 'team' },
{ name: '项目入口', icon: 'project' }
{ name: '项目入口', icon: 'project' },
// #ifdef APP-PLUS
{ name: '直播列表', icon: 'liveStream' }
// #endif
]
const onGo = item => {
@@ -36,6 +39,10 @@
navigateTo('/pages/shop-together/index')
return
}
if (item === 'liveStream') {
navigateTo('/pages/discover/livelist/index')
return
}
}
</script>

View File

@@ -0,0 +1,252 @@
<template>
<view
style="position: relative; height: 160rpx; padding-top: 80rpx; background-color: #fff; display: flex; flex-direction: row; justify-content: center; align-items: center;">
<image style="width: 21rpx; height: 34rpx; position: absolute; left: 60rpx;" src="/static/images/back-black.png"
@tap="handleGoBack" />
<text>在线直播</text>
<image style="width: 36rpx; height: 36rpx; position: absolute; right: 60rpx;" src="/static/images/refresh.png"
@tap="handlePageRefresh" />
</view>
<view class="home-container" :style="{ height: safeArea.height + 'px' }">
<live-list />
<!-- 创建房间按钮 -->
<!-- <view class="home-footer">
<view class="create-btn" @click="goAnchorPage()">
<image style="width: 36rpx; height: 36rpx; margin-right: 10rpx;" src="/static/images/create-live.png" />
<text class="btn-text">开2直播</text>
</view>
</view> -->
</view>
</template>
<script setup>
import { reLaunch } from '@/utils/router';
import {
ref,
onMounted,
watch
} from 'vue';
import {
onLoad,
onShow
} from '@dcloudio/uni-app';
import LiveList from '@/uni_modules/tuikit-atomic-x/components/LiveList.nvue';
import {
useLiveListState
} from "@/uni_modules/tuikit-atomic-x/state/LiveListState";
const {
fetchLiveList,
liveListCursor
} = useLiveListState();
const safeArea = ref({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 375,
height: 750,
});
onMounted(() => {
uni.getSystemInfo({
success: (res) => {
safeArea.value = res.safeArea;
}
});
});
onShow(() => {
console.warn(`home onShow`);
needRefresh.value = true;
});
const goAnchorPage = () => {
uni.redirectTo({
url: '/pages/anchor/index'
});
}
const handleGoBack = () => {
console.log('返回')
reLaunch('/pages/discover/discover')
}
const handlePageRefresh = () => {
const params = {
cursor: '',
count: 20,
success: () => {
fetchLiveListRecursively(liveListCursor.value);
}
};
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);
}
</script>
<style>
.home-container {
flex: 1;
background-color: #F2F5FC;
position: relative;
}
.header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 20rpx 32rpx;
background-color: #ffffff;
border-bottom-width: 1rpx;
border-bottom-color: #f0f0f0;
position: relative;
height: 120rpx;
}
.header-left {
width: 80rpx;
display: flex;
align-items: center;
justify-content: flex-start;
}
.back-icon {
width: 40rpx;
height: 40rpx;
opacity: 0.8;
}
.header-center {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #333333;
margin-bottom: 8rpx;
}
.title-underline {
width: 60rpx;
height: 4rpx;
background-color: #007AFF;
border-radius: 2rpx;
}
.header-right {
width: 80rpx;
display: flex;
justify-content: flex-end;
align-items: center;
}
.help-icon {
width: 40rpx;
height: 40rpx;
opacity: 0.8;
}
.user-bar {
flex-direction: row;
align-items: center;
padding-top: 32rpx;
padding-bottom: 16rpx;
padding-left: 32rpx;
padding-right: 32rpx;
background-color: #fff;
}
.avatar {
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
margin-right: 16rpx;
}
.user-info {
flex: 1;
flex-direction: column;
justify-content: center;
}
.user-name {
font-size: 28rpx;
color: #222;
font-weight: 700;
}
.user-id {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
.user-icon {
width: 40rpx;
height: 40rpx;
justify-content: center;
align-items: center;
}
.icon-help {
width: 40rpx;
height: 40rpx;
}
.home-footer {
position: absolute;
bottom: 60rpx;
width: 750rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.create-btn {
background-color: #0468FC;
border-radius: 999px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 25rpx 60rpx;
}
.btn-text {
color: #fff;
font-size: 32rpx;
font-weight: 700;
}
</style>

193
pages/liveend/index.nvue Normal file
View File

@@ -0,0 +1,193 @@
<template>
<view class="container" :style="{ height: safeArea.height + 'px'}">
<image @tap="handleToLive" class="back-btn" src="/static/images/close.png" mode="aspectFit" />
<view class="header">
<text class="title">直播已结束</text>
</view>
<view class="stats-card">
<view class="stats-row">
<view class="stats-item">
<text class="stats-value">{{ formattedDuration }}</text>
<text class="stats-label">直播时长</text>
</view>
<view class="stats-item">
<text class="stats-value">{{ summaryData?.totalViewers }}</text>
<text class="stats-label">累计观看</text>
</view>
<view class="stats-item">
<text class="stats-value">{{ summaryData?.totalMessageSent }}</text>
<text class="stats-label">消息数量</text>
</view>
</view>
<view class="stats-row">
<view class="stats-item">
<text class="stats-value">{{ summaryData?.totalGiftCoins }}</text>
<text class="stats-label">礼物收入</text>
</view>
<view class="stats-item">
<text class="stats-value">{{ summaryData?.totalGiftUniqueSenders }}</text>
<text class="stats-label">送礼人数</text>
</view>
<view class="stats-item">
<text class="stats-value">{{ summaryData?.totalLikesReceived }}</text>
<text class="stats-label">点赞数量</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
const safeArea = ref({
left: 0,
right: 0,
top: 0,
bottom: 0,
width: 375,
height: 750,
});
const summaryData = ref()
onLoad((options) => {
summaryData.value = uni.$summaryData
})
// 计算属性: 格式化后的直播时长
const formattedDuration = computed(() => {
return formatDuration(summaryData.value?.totalDuration);
});
onMounted(() => {
uni.getSystemInfo({
success: (res) => {
safeArea.value = res.safeArea;
}
});
});
const handleToLive = () => {
uni.redirectTo({
url: '/pages/discover/livelist/index',
success() {
console.log('跳转成功');
},
fail(err) {
console.error('跳转失败:', err);
}
});
}
// 格式化直播时长(输入单位为毫秒)
const formatDuration = (milliseconds) => {
// 处理无效输入
if (!milliseconds || milliseconds <= 0 || isNaN(milliseconds)) {
return '00:00:00';
}
// 将毫秒转换为秒(向下取整)
const totalSeconds = Math.floor(milliseconds / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const remainingSeconds = totalSeconds % 60;
// 始终显示 HH:MM:SS 格式
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
};
</script>
<style scoped>
.container {
flex: 1;
background-color: rgba(19, 20, 23, 1);
align-items: center;
justify-content: flex-start;
/* 顶部对齐 */
position: relative;
flex-direction: column;
}
.header {
margin-top: 300rpx;
}
.back-btn {
width: 48rpx;
height: 48rpx;
position: absolute;
top: 100rpx;
right: 80rpx;
z-index: 99;
}
.title {
color: #fff;
font-size: 36rpx;
font-weight: bold;
}
.time {
color: #bdbdbd;
font-size: 26rpx;
margin-bottom: 40rpx;
margin-left: 32rpx;
width: 100%;
}
.stats-card {
/* position: absolute; */
/* top: 450rpx; */
left: 0;
right: 0;
background-color: rgba(43, 44, 48, 1);
border-radius: 24rpx;
padding: 40rpx 0;
width: 700rpx;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.2);
margin-top: 80rpx;
/* 与header间距 */
}
.stats-row {
width: 700rpx;
flex-direction: row;
justify-content: space-around;
margin: 24rpx;
}
.stats-row:last-child {
margin-bottom: 0;
}
.stats-item {
flex: 1;
align-items: center;
}
.stats-value {
color: #fff;
font-size: 36rpx;
font-weight: bold;
text-align: center;
}
.stats-label {
color: #bdbdbd;
font-size: 22rpx;
margin-top: 8rpx;
text-align: center;
}
</style>

View File

@@ -11,7 +11,7 @@
url: '/pages/my-index/wallet/index'
},
{ name: '我的团队', icon: 'team', url: '/pages/my-index/my-team' },
{ name: '群创建直播', icon: 'videocam', url: '' },
// { name: '群创建直播', icon: 'videocam', url: '' },
{
name: '会议记录',
icon: 'meeting',