需要添加直播接口
4
.env
@@ -1,5 +1,5 @@
|
|||||||
# API
|
# API
|
||||||
VITE_SYSTEM_URL = "http://u85e65a4.natappfree.cc"
|
VITE_SYSTEM_URL = "http://jb96776a.natappfree.cc"
|
||||||
|
|
||||||
#文档地址
|
#文档地址
|
||||||
VITE_DOC_URL = "http://k5cb6eb9.natappfree.cc"
|
VITE_DOC_URL = "http://k7986286.natappfree.cc"
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
"type" : "uni-app:app-ios"
|
"type" : "uni-app:app-ios"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"customPlaygroundType" : "device",
|
"customPlaygroundType" : "local",
|
||||||
"playground" : "standard",
|
"playground" : "custom",
|
||||||
"type" : "uni-app:app-android"
|
"type" : "uni-app:app-android"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
11
App.vue
@@ -3,15 +3,18 @@
|
|||||||
import { reLaunch } from './utils/router'
|
import { reLaunch } from './utils/router'
|
||||||
import { useAuthUser } from './composables/useAuthUser'
|
import { useAuthUser } from './composables/useAuthUser'
|
||||||
import { useUserStore } from './stores/user'
|
import { useUserStore } from './stores/user'
|
||||||
|
|
||||||
import { TUIChatKit } from './TUIKit'
|
import { TUIChatKit } from './TUIKit'
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
import { setSdkLanguageFromSystem } from '@/uni_modules/tuikit-atomic-x/utils/setSdkLanguageFromSystem'
|
||||||
|
// #endif
|
||||||
|
|
||||||
TUIChatKit.init()
|
TUIChatKit.init()
|
||||||
|
|
||||||
const { token } = useAuthUser()
|
const { token } = useAuthUser()
|
||||||
const { loginTencentIM } = useUserStore()
|
const { loginTencentIM } = useUserStore()
|
||||||
/** 静默登录逻辑 */
|
/** 静默登录逻辑 */
|
||||||
const silentLogin = async () => {
|
const silentLogin = async () => {
|
||||||
console.log(token.value, '==')
|
|
||||||
if (token.value) {
|
if (token.value) {
|
||||||
loginTencentIM()
|
loginTencentIM()
|
||||||
reLaunch('/TUIKit/components/TUIConversation/index')
|
reLaunch('/TUIKit/components/TUIConversation/index')
|
||||||
@@ -25,6 +28,10 @@
|
|||||||
onLaunch(() => {
|
onLaunch(() => {
|
||||||
console.log('App Launch111')
|
console.log('App Launch111')
|
||||||
silentLogin()
|
silentLogin()
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
setSdkLanguageFromSystem()
|
||||||
|
// #endif
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ unaipp多端im+会议+积分商城
|
|||||||
```https://ext.dcloud.net.cn/plugin?id=3935```
|
```https://ext.dcloud.net.cn/plugin?id=3935```
|
||||||
```https://z-paging.zxlee.cn/start/use.html#%E5%BB%B6%E8%BF%9F%E5%8A%A0%E8%BD%BD%E5%88%97%E8%A1%A8%E7%A4%BA%E4%BE%8B```
|
```https://z-paging.zxlee.cn/start/use.html#%E5%BB%B6%E8%BF%9F%E5%8A%A0%E8%BD%BD%E5%88%97%E8%A1%A8%E7%A4%BA%E4%BE%8B```
|
||||||
|
|
||||||
|
<!-- show-loading-more-no-more-view 去除没有更多 -->
|
||||||
|
|
||||||
### TUILiveKit 文档地址
|
### TUILiveKit 文档地址
|
||||||
|
|
||||||
```https://trtc.io/zh/document/60034?platform=ios&product=live```
|
```https://trtc.io/zh/document/60034?platform=ios&product=live```
|
||||||
|
|||||||
@@ -67,11 +67,14 @@
|
|||||||
<RedEnvelope />
|
<RedEnvelope />
|
||||||
|
|
||||||
<!-- 直播按钮 -->
|
<!-- 直播按钮 -->
|
||||||
|
<!-- #ifdef APP-PLUS -->
|
||||||
<LiveStream
|
<LiveStream
|
||||||
v-if="
|
v-if="
|
||||||
currentConversation?.type === TUIChatEngine.TYPES.CONV_GROUP
|
currentConversation?.type === TUIChatEngine.TYPES.CONV_GROUP
|
||||||
"
|
"
|
||||||
|
:groupId="currentConversation?.conversationID"
|
||||||
/>
|
/>
|
||||||
|
<!-- #endif -->
|
||||||
</template>
|
</template>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
<swiper-item
|
<swiper-item
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import { ref } from '../../../../adapter-vue'
|
import { ref } from '../../../../adapter-vue'
|
||||||
import custom from '../../../../assets/icon/live-stream.svg'
|
import custom from '../../../../assets/icon/live-stream.svg'
|
||||||
import ToolbarItemContainer from '../toolbar-item-container/index.vue'
|
import ToolbarItemContainer from '../toolbar-item-container/index.vue'
|
||||||
@@ -10,6 +10,14 @@
|
|||||||
const { showDialog } = useUI()
|
const { showDialog } = useUI()
|
||||||
const evaluateIcon = custom
|
const evaluateIcon = custom
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 距离顶部高度 */
|
||||||
|
groupId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const container = ref()
|
const container = ref()
|
||||||
/**
|
/**
|
||||||
* 主播申请状态
|
* 主播申请状态
|
||||||
@@ -34,7 +42,12 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (stateData.value === 1) {
|
if (stateData.value === 1) {
|
||||||
console.log('去直播间')
|
// 跳转到开播页面
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/anchor/index?groupId=${encodeURIComponent(
|
||||||
|
props.groupId
|
||||||
|
)}`
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (stateData.value === 3) {
|
if (stateData.value === 3) {
|
||||||
@@ -45,6 +58,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<ToolbarItemContainer
|
<ToolbarItemContainer
|
||||||
ref="container"
|
ref="container"
|
||||||
:iconFile="evaluateIcon"
|
:iconFile="evaluateIcon"
|
||||||
|
|||||||
@@ -80,3 +80,28 @@ export const getAnchorDetail = () => {
|
|||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 添加修改直播 */
|
||||||
|
export const imAddLive = (data, method = 'post') => {
|
||||||
|
return http({
|
||||||
|
url: '/api/service/imLiveRoom',
|
||||||
|
method,
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开始直播 */
|
||||||
|
export const imDataStartLive = (roomId) => {
|
||||||
|
return http({
|
||||||
|
url: `/api/service/imLiveRoom/start/${roomId}`,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 结束直播 */
|
||||||
|
export const imDataEndLive = (roomId) => {
|
||||||
|
return http({
|
||||||
|
url: `/api/service/imLiveRoom/${roomId}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
173
components/ActionSheet.nvue
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="internalVisible" class="as-mask" @click="onMaskClick">
|
||||||
|
<view class="as-container" :style="{
|
||||||
|
width: systemInfo?.safeArea?.width + 'px',
|
||||||
|
}" @click.stop>
|
||||||
|
<view class="as-panel">
|
||||||
|
<view v-if="title" class="as-header">
|
||||||
|
<text class="as-header-text">{{ title }}</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view class="as-list" scroll-y>
|
||||||
|
<view v-for="(item, index) in itemList" :key="index"
|
||||||
|
:class="['as-item', index !== itemList.length - 1 ? 'as-item--divider' : '']" @click="onSelect(index)">
|
||||||
|
<text class="as-item-text">{{ item }}</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showCancel" class="as-cancel" @click="onCancel">
|
||||||
|
<text class="as-cancel-text">{{ cancelText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, defineEmits, defineProps, nextTick, defineExpose, onMounted } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
itemList: { type: Array as () => string[], default: () => [] },
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
cancelText: { type: String, default: '取消' },
|
||||||
|
closeOnMask: { type: Boolean, default: true },
|
||||||
|
showCancel: { type: Boolean, default: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const systemInfo = ref({});
|
||||||
|
onMounted(() => {
|
||||||
|
uni.getSystemInfo({
|
||||||
|
success: (res) => {
|
||||||
|
systemInfo.value = res;
|
||||||
|
console.log(systemInfo.value)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'select', 'cancel', 'close'])
|
||||||
|
|
||||||
|
const internalVisible = ref<boolean>(props.modelValue)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
internalVisible.value = val
|
||||||
|
})
|
||||||
|
const open = () => {
|
||||||
|
if (!internalVisible.value) {
|
||||||
|
internalVisible.value = true
|
||||||
|
emit('update:modelValue', true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (internalVisible.value) {
|
||||||
|
internalVisible.value = false
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMaskClick = () => {
|
||||||
|
if (props.closeOnMask) {
|
||||||
|
emit('cancel')
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
emit('cancel')
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelect = (index : number) => {
|
||||||
|
emit('select', { tapIndex: index })
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
defineExpose({ open, close })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.as-mask {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
z-index: 5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.as-container {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.as-panel {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.as-header {
|
||||||
|
padding: 28rpx 32rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom-width: 1rpx;
|
||||||
|
border-bottom-color: #eef0f3;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.as-header-text {
|
||||||
|
color: rgba(124, 133, 166, 1);
|
||||||
|
font-size: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.as-list {
|
||||||
|
max-height: 600rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.as-item {
|
||||||
|
padding: 32rpx;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.as-item--divider {
|
||||||
|
border-bottom-width: 1rpx;
|
||||||
|
border-bottom-color: #eef0f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.as-item-text {
|
||||||
|
color: #111826;
|
||||||
|
font-size: 34rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.as-item-text--danger {
|
||||||
|
color: #ff3b30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.as-cancel {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
padding: 28rpx 32rpx;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.as-cancel-text {
|
||||||
|
color: #111826;
|
||||||
|
font-size: 34rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
577
components/BeforeLivePanel.nvue
Normal file
@@ -0,0 +1,577 @@
|
|||||||
|
<template>
|
||||||
|
<view class="before-live-content">
|
||||||
|
<!-- 直播设置卡片 -->
|
||||||
|
<view class="live-info-card">
|
||||||
|
<view class="cover-section" @tap="onEditCover">
|
||||||
|
<image class="cover-image" style="position: relative;" :src="localCoverURL" mode="aspectFill" />
|
||||||
|
<view
|
||||||
|
style="display: flex; flex-direction: column; justify-content: center; align-items: center; width: 140rpx; position: absolute; bottom: 0; padding: 5rpx 0;border-bottom-left-radius: 24rpx; border-bottom-right-radius: 24rpx; background-color: rgba(0, 0, 0, 0.5); ">
|
||||||
|
<text class="live-detail" style="margin-left: 0;">
|
||||||
|
修改封面</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-section">
|
||||||
|
<view class="title-row">
|
||||||
|
<input class="live-title" v-model="localLiveTitle" @input="onInputTitle" @blur="onInputBlur"
|
||||||
|
:focus="isInputFocused" placeholder="请输入直播标题" />
|
||||||
|
<view class="edit-icon-container" @tap="onEditTitle">
|
||||||
|
<image class="edit-icon" src="/static/images/edit.png" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- <view class="underline"></view>
|
||||||
|
<view class="info-row" @tap="onChooseMode">
|
||||||
|
<image class="info-icon" src="/static/images/mode.png" />
|
||||||
|
<text class="live-detail" style="margin-left: 10rpx;">直播模式:</text>
|
||||||
|
<text class="live-detail">{{ localLiveMode }}</text>
|
||||||
|
<image class="arrow-icon" src="/static/images/right-arrow.png" />
|
||||||
|
</view> -->
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 封面选择弹窗 -->
|
||||||
|
<view class="bottom-drawer-container" v-if="isShowOverDialog">
|
||||||
|
<view class="drawer-overlay" @tap="close"></view>
|
||||||
|
<view class="bottom-drawer" :class="{ 'drawer-open': isShowOverDialog }">
|
||||||
|
<text class="list-title">封面</text>
|
||||||
|
<list class="audience-content" :show-scrollbar="false">
|
||||||
|
<cell class="tab-item">
|
||||||
|
<view v-for="(url, idx) in avatarList" :key="url" class="cover-dialog-item" @tap="selectAvatar(idx)">
|
||||||
|
<image :src="url" :class="{selected: idx === selectedAvatarIndex}" class="cover-dialog-img" />
|
||||||
|
</view>
|
||||||
|
</cell>
|
||||||
|
</list>
|
||||||
|
<view class="home-footer">
|
||||||
|
<view class="create-btn" @click="setCover">
|
||||||
|
<text class="btn-text">设为封面</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部操作按钮 -->
|
||||||
|
<view class="bottom-actions">
|
||||||
|
<view class="action-row">
|
||||||
|
<view class="action-button" @tap="handleBeauty">
|
||||||
|
<image class="action-icon" src="/static/images/beauty.png" mode="aspectFit" />
|
||||||
|
<text class="action-text">美颜</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-button" @tap="handleAudioEffect">
|
||||||
|
<image class="action-icon" src="/static/images/sound-effect.png" mode="aspectFit" />
|
||||||
|
<text class="action-text">音效</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-button" @tap="handleCamera">
|
||||||
|
<image class="action-icon" src="/static/images/flip-b.png" mode="aspectFit" />
|
||||||
|
<text class="action-text">翻转</text>
|
||||||
|
</view>
|
||||||
|
<!-- <view class="action-button" @tap="handleSettings">
|
||||||
|
<image class="action-icon" src="/static/images/setting.png" mode="aspectFit" />
|
||||||
|
<text class="action-text">设置</text>
|
||||||
|
</view> -->
|
||||||
|
</view>
|
||||||
|
<view class="start-live-button" @tap="handleStartLive">
|
||||||
|
<text class="start-live-text">开始直播</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<BeautyPanel v-model="isShowBeautyPanel" />
|
||||||
|
<AudioEffectPanel v-model="isShowAudioEffect" />
|
||||||
|
<ActionSheet v-model="isShowModeSheet" :itemList="modeSheetItems" :showCancel="false" @select="onModeSheetSelect" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed } from 'vue';
|
||||||
|
import BeautyPanel from '@/uni_modules/tuikit-atomic-x/components/BeautyPanel.nvue';
|
||||||
|
import AudioEffectPanel from '@/uni_modules/tuikit-atomic-x/components/AudioEffectPanel.nvue';
|
||||||
|
import { useLoginState } from "@/uni_modules/tuikit-atomic-x/state/LoginState";
|
||||||
|
import ActionSheet from '@/components/ActionSheet.nvue'
|
||||||
|
const { loginUserInfo } = useLoginState();
|
||||||
|
const isShowModeSheet = ref(false)
|
||||||
|
const modeSheetItems = ref(['公开', '隐私'])
|
||||||
|
// Props 定义
|
||||||
|
interface Props {
|
||||||
|
coverURL : string;
|
||||||
|
liveCategory : string;
|
||||||
|
liveMode : string;
|
||||||
|
templateLayout : number;
|
||||||
|
liveTitle : string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
coverURL: '',
|
||||||
|
liveCategory: '日常聊天',
|
||||||
|
liveMode: '公开',
|
||||||
|
templateLayout: 600,
|
||||||
|
liveTitle: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits 定义
|
||||||
|
const emit = defineEmits<{
|
||||||
|
editCover : [value: string];
|
||||||
|
editTitle : [value: string];
|
||||||
|
chooseCategory : [value: string];
|
||||||
|
chooseMode : [value: string];
|
||||||
|
chooseTemplate : [value: number];
|
||||||
|
startLive : [];
|
||||||
|
beauty : [];
|
||||||
|
audioEffect : [];
|
||||||
|
camera : [];
|
||||||
|
settings : [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 内部状态
|
||||||
|
const isShowBeautyPanel = ref(false);
|
||||||
|
const isShowAudioEffect = ref(false);
|
||||||
|
|
||||||
|
// LiveSetupCard 相关状态
|
||||||
|
const localCoverURL = ref(props.coverURL);
|
||||||
|
const localLiveTitle = ref('');
|
||||||
|
const localLiveCategory = ref(props.liveCategory);
|
||||||
|
const localLiveMode = ref(props.liveMode);
|
||||||
|
const isShowOverDialog = ref(false);
|
||||||
|
const isInputFocused = ref(false);
|
||||||
|
const selectedAvatarIndex = ref(0);
|
||||||
|
const hasUserInteracted = ref(false);
|
||||||
|
const lastValidTitle = ref('');
|
||||||
|
|
||||||
|
// 封面列表
|
||||||
|
const avatarList = [
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover1.png",
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover2.png",
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover3.png",
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover4.png",
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover5.png",
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover6.png",
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover7.png",
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover8.png",
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover9.png",
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover10.png",
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover11.png",
|
||||||
|
"https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover12.png",
|
||||||
|
];
|
||||||
|
|
||||||
|
// 计算默认标题
|
||||||
|
const defaultLiveTitle = computed(() => {
|
||||||
|
const userName = loginUserInfo.value?.nickname;
|
||||||
|
return userName || loginUserInfo.value?.userID;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算字节长度
|
||||||
|
const getByteLength = (str : string) : number => {
|
||||||
|
let len = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const charCode = str.charCodeAt(i);
|
||||||
|
if (charCode <= 0x7f) {
|
||||||
|
len += 1;
|
||||||
|
} else if (charCode <= 0x7ff) {
|
||||||
|
len += 2;
|
||||||
|
} else if (charCode <= 0xffff) {
|
||||||
|
len += 3;
|
||||||
|
} else {
|
||||||
|
len += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 截取指定字节长度的字符串
|
||||||
|
const truncateByByteLength = (str : string, maxBytes : number) : string => {
|
||||||
|
let result = '';
|
||||||
|
let currentBytes = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const charCode = str.charCodeAt(i);
|
||||||
|
let charBytes = 1;
|
||||||
|
|
||||||
|
if (charCode <= 0x7f) {
|
||||||
|
charBytes = 1;
|
||||||
|
} else if (charCode <= 0x7ff) {
|
||||||
|
charBytes = 2;
|
||||||
|
} else if (charCode <= 0xffff) {
|
||||||
|
charBytes = 3;
|
||||||
|
} else {
|
||||||
|
charBytes = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentBytes + charBytes <= maxBytes) {
|
||||||
|
result += str[i];
|
||||||
|
currentBytes += charBytes;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听默认标题变化
|
||||||
|
watch(defaultLiveTitle, (newDefaultTitle) => {
|
||||||
|
if (!localLiveTitle.value && !hasUserInteracted.value) {
|
||||||
|
localLiveTitle.value = newDefaultTitle;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听用户名变化
|
||||||
|
watch(() => loginUserInfo.value?.nickname, (newUserName) => {
|
||||||
|
if (newUserName && !localLiveTitle.value && !hasUserInteracted.value) {
|
||||||
|
localLiveTitle.value = newUserName;
|
||||||
|
}
|
||||||
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
|
// 同步 props 变化
|
||||||
|
watch(() => props.coverURL, (val) => { localCoverURL.value = val; });
|
||||||
|
watch(() => props.liveCategory, (val) => { localLiveCategory.value = val; });
|
||||||
|
watch(() => props.liveMode, (val) => { localLiveMode.value = val; });
|
||||||
|
|
||||||
|
// 监听本地标题变化,通知父组件
|
||||||
|
watch(localLiveTitle, (val) => {
|
||||||
|
emit('editTitle', val);
|
||||||
|
});
|
||||||
|
|
||||||
|
// LiveSetupCard 相关方法
|
||||||
|
const onInputTitle = (e : any) => {
|
||||||
|
const inputValue = e.detail.value;
|
||||||
|
hasUserInteracted.value = true;
|
||||||
|
const byteLength = getByteLength(inputValue);
|
||||||
|
localLiveTitle.value = inputValue;
|
||||||
|
if (byteLength <= 100) {
|
||||||
|
lastValidTitle.value = inputValue;
|
||||||
|
} else if (byteLength > 100) {
|
||||||
|
setTimeout(() => {
|
||||||
|
localLiveTitle.value = lastValidTitle.value || loginUserInfo.value?.nickname;
|
||||||
|
}, 0);
|
||||||
|
uni.showToast({
|
||||||
|
title: '标题最多100字节,已恢复到上次有效内容',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
emit('editTitle', localLiveTitle.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditTitle = () => {
|
||||||
|
isInputFocused.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputBlur = () => {
|
||||||
|
isInputFocused.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditCover = () => {
|
||||||
|
isShowOverDialog.value = true;
|
||||||
|
emit('editCover', localCoverURL.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onModeSheetSelect = (res : { tapIndex : number }) => {
|
||||||
|
if (res.tapIndex === 0) {
|
||||||
|
localLiveMode.value = '公开';
|
||||||
|
} else {
|
||||||
|
localLiveMode.value = '隐私';
|
||||||
|
}
|
||||||
|
emit('chooseMode', localLiveMode.value);
|
||||||
|
}
|
||||||
|
const onChooseMode = () => {
|
||||||
|
isShowModeSheet.value = true
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
isShowOverDialog.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAvatar = (idx : number) => {
|
||||||
|
selectedAvatarIndex.value = idx;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCover = () => {
|
||||||
|
localCoverURL.value = avatarList[selectedAvatarIndex.value];
|
||||||
|
emit('editCover', localCoverURL.value);
|
||||||
|
isShowOverDialog.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 原有事件处理函数
|
||||||
|
const handleStartLive = () => {
|
||||||
|
emit('startLive');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBeauty = () => {
|
||||||
|
isShowBeautyPanel.value = true;
|
||||||
|
emit('beauty');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAudioEffect = () => {
|
||||||
|
isShowAudioEffect.value = true;
|
||||||
|
emit('audioEffect');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCamera = () => {
|
||||||
|
emit('camera');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettings = () => {
|
||||||
|
emit('settings');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暴露给父组件的方法
|
||||||
|
defineExpose({
|
||||||
|
isShowBeautyPanel,
|
||||||
|
isShowAudioEffect
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.before-live-content {
|
||||||
|
flex: 1;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LiveSetupCard 样式 */
|
||||||
|
.live-info-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
border-radius: 24rpx;
|
||||||
|
margin: 48rpx 32rpx 32rpx 32rpx;
|
||||||
|
padding: 16rpx;
|
||||||
|
position: fixed;
|
||||||
|
top: 200rpx;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-section {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-image {
|
||||||
|
width: 140rpx;
|
||||||
|
height: 188rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 24rpx;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.underline {
|
||||||
|
flex: 1;
|
||||||
|
height: 2rpx;
|
||||||
|
background: #fff;
|
||||||
|
opacity: 0.2;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-title {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-icon-container {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-icon {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-detail {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 封面选择弹窗样式 */
|
||||||
|
.bottom-drawer-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-drawer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(34, 38, 46, 1);
|
||||||
|
border-top-left-radius: 32rpx;
|
||||||
|
border-top-right-radius: 32rpx;
|
||||||
|
transform: translateY(100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 1400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-open {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audience-content {
|
||||||
|
background-color: rgba(34, 38, 46, 1);
|
||||||
|
padding-left: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-dialog-item {
|
||||||
|
position: relative;
|
||||||
|
width: 200rpx;
|
||||||
|
height: 230rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #23242a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-dialog-img.selected {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 230rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 6rpx solid #238CFE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-dialog-img {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 230rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-title {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 60rpx;
|
||||||
|
width: 750rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 104rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-btn {
|
||||||
|
background-color: #0468FC;
|
||||||
|
border-radius: 50rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 25rpx 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部操作按钮样式 */
|
||||||
|
.bottom-actions {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 50rpx;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0 100rpx;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-bottom: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-live-button {
|
||||||
|
height: 100rpx;
|
||||||
|
background-color: #2B65FB;
|
||||||
|
border-radius: 200rpx;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-live-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
283
components/LiveMoreActionsPanel.nvue
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
<template>
|
||||||
|
<view class="bottom-drawer-container" v-if="modelValue">
|
||||||
|
<view class="drawer-overlay" @tap="close"></view>
|
||||||
|
<view class="bottom-drawer" :class="{ 'drawer-open': modelValue }">
|
||||||
|
<text class="title">更多功能</text>
|
||||||
|
|
||||||
|
<view class="drawer-content">
|
||||||
|
<view class="drawer-actions">
|
||||||
|
<view class="action-btn" @tap="handleBeauty">
|
||||||
|
<view class="action-btn-image-container">
|
||||||
|
<image class="action-btn-image" src="/static/images/live-beauty.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<text class="action-btn-content">美颜</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn" @tap="handleAudioEffect">
|
||||||
|
<view class="action-btn-image-container">
|
||||||
|
<image class="action-btn-image" src="/static/images/live-effects.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<text class="action-btn-content">音效</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn" @tap="handleCamera">
|
||||||
|
<view class="action-btn-image-container">
|
||||||
|
<image class="action-btn-image" src="/static/images/live-flip.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<text class="action-btn-content">翻转</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn" @tap="handleSwitchMirror">
|
||||||
|
<view class="action-btn-image-container">
|
||||||
|
<image class="action-btn-image" src="/static/images/mirror.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<text class="action-btn-content">镜像</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="action-btn" @tap="openNetworkQualityPanel">
|
||||||
|
<view class="action-btn-image-container">
|
||||||
|
<image class="action-btn-image" src="/static/images/live-dashboard.png" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<text class="action-btn-content">仪表盘</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<NetworkQualityPanel v-model="isShowNetworkQualityPanel"></NetworkQualityPanel>
|
||||||
|
<BeautyPanel v-model="isShowBeautyPanel"></BeautyPanel>
|
||||||
|
<AudioEffectPanel v-model="isShowAudioEffect"></AudioEffectPanel>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
onMounted
|
||||||
|
} from 'vue';
|
||||||
|
import NetworkQualityPanel from '@/uni_modules/tuikit-atomic-x/components/NetworkQualityPanel.nvue';
|
||||||
|
import BeautyPanel from '@/uni_modules/tuikit-atomic-x/components/BeautyPanel.nvue';
|
||||||
|
import AudioEffectPanel from '@/uni_modules/tuikit-atomic-x/components/AudioEffectPanel.nvue';
|
||||||
|
import {
|
||||||
|
useDeviceState
|
||||||
|
} from "@/uni_modules/tuikit-atomic-x/state/DeviceState";
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFrontCamera,
|
||||||
|
switchCamera,
|
||||||
|
switchMirror,
|
||||||
|
localMirrorType
|
||||||
|
} = useDeviceState(uni?.$liveID);
|
||||||
|
|
||||||
|
const defaultavatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
userInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
userName: '',
|
||||||
|
userID: '',
|
||||||
|
avatarURL: '',
|
||||||
|
isMessageDisabled: false,
|
||||||
|
userRole: 1
|
||||||
|
})
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isShowNetworkQualityPanel = ref(false);
|
||||||
|
const isShowBeautyPanel = ref(false);
|
||||||
|
const isShowAudioEffect = ref(false);
|
||||||
|
|
||||||
|
const handleBeauty = () => {
|
||||||
|
isShowBeautyPanel.value = true;
|
||||||
|
};
|
||||||
|
const openNetworkQualityPanel = () => {
|
||||||
|
isShowNetworkQualityPanel.value = true
|
||||||
|
}
|
||||||
|
const handleAudioEffect = () => {
|
||||||
|
isShowAudioEffect.value = true;
|
||||||
|
};
|
||||||
|
const handleCamera = () => {
|
||||||
|
switchCamera({
|
||||||
|
isFront: !isFrontCamera.value
|
||||||
|
})
|
||||||
|
};
|
||||||
|
const handleSwitchMirror = () => {
|
||||||
|
if(!isFrontCamera.value) {
|
||||||
|
uni.showToast({
|
||||||
|
icon: 'none',
|
||||||
|
title: '仅前置摄像头支持该设置'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (localMirrorType.value === 'AUTO') {
|
||||||
|
switchMirror({
|
||||||
|
mirrorType: 'DISABLE'
|
||||||
|
})
|
||||||
|
} else if (localMirrorType.value === 'DISABLE') {
|
||||||
|
switchMirror({
|
||||||
|
mirrorType: 'ENABLE'
|
||||||
|
})
|
||||||
|
} else if (localMirrorType.value === 'ENABLE') {
|
||||||
|
switchMirror({
|
||||||
|
mirrorType: 'DISABLE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const close = () => {
|
||||||
|
emit('update:modelValue', false);
|
||||||
|
};
|
||||||
|
</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(31, 32, 36, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-open {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-header {
|
||||||
|
padding: 48rpx;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-header-left-container {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-header-avatar-container {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 20rpx
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-header-avatar {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-header-content-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-name {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-id {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 48rpx;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
/* height: 300rpx; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
31
debug/GenerateTestUserSig.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import LibGenerateTestUserSig from './lib-generate-test-usersig-es.min.js'
|
||||||
|
|
||||||
|
import useAuthUser from '@/composables/useAuthUser'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expiration time for the signature, it is recommended not to set it too short.
|
||||||
|
* Time unit: seconds
|
||||||
|
* Default time: 7 x 24 x 60 x 60 = 604800 = 7 days
|
||||||
|
*/
|
||||||
|
const EXPIRETIME = 604800
|
||||||
|
|
||||||
|
export function genTestUserSig(userID) {
|
||||||
|
const { tencentUserSig } = useAuthUser()
|
||||||
|
const SDKAppID = tencentUserSig.value.sdkappID
|
||||||
|
const SDKSECRETKEY = tencentUserSig.value.secretKey
|
||||||
|
console.log('SDKAppID===============', SDKAppID)
|
||||||
|
|
||||||
|
const generator = new LibGenerateTestUserSig(
|
||||||
|
SDKAppID,
|
||||||
|
SDKSECRETKEY,
|
||||||
|
EXPIRETIME
|
||||||
|
)
|
||||||
|
const userSig = generator.genTestUserSig(userID)
|
||||||
|
const userName = userID || `user_${Math.ceil(Math.random() * 10)}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
SDKAppID,
|
||||||
|
userSig,
|
||||||
|
userName
|
||||||
|
}
|
||||||
|
}
|
||||||
6217
debug/lib-generate-test-usersig-es.min.js
vendored
Normal file
195
manifest.json
@@ -1,97 +1,108 @@
|
|||||||
{
|
{
|
||||||
"name": "uniapp-imitate-wx",
|
"name" : "uniapp-imitate-wx",
|
||||||
"appid": "__UNI__9EFDC69",
|
"appid" : "__UNI__9EFDC69",
|
||||||
"description": "",
|
"description" : "",
|
||||||
"versionName": "1.0.0",
|
"versionName" : "1.0.0",
|
||||||
"versionCode": "100",
|
"versionCode" : "100",
|
||||||
"transformPx": false,
|
"transformPx" : false,
|
||||||
/* 5+App特有相关 */
|
/* 5+App特有相关 */
|
||||||
"app-plus": {
|
"app-plus" : {
|
||||||
"usingComponents": true,
|
"usingComponents" : true,
|
||||||
"nvueStyleCompiler": "uni-app",
|
"nvueStyleCompiler" : "uni-app",
|
||||||
"compilerVersion": 3,
|
"compilerVersion" : 3,
|
||||||
"splashscreen": {
|
"splashscreen" : {
|
||||||
"alwaysShowBeforeRender": true,
|
"alwaysShowBeforeRender" : true,
|
||||||
"waiting": true,
|
"waiting" : true,
|
||||||
"autoclose": true,
|
"autoclose" : true,
|
||||||
"delay": 0
|
"delay" : 0
|
||||||
|
},
|
||||||
|
"safearea" : {
|
||||||
|
"background" : "#ffffff",
|
||||||
|
"backgroundDark" : "#2f0508",
|
||||||
|
"bottom" : {
|
||||||
|
"offset" : "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* 模块配置 */
|
||||||
|
"modules" : {},
|
||||||
|
/* 应用发布信息 */
|
||||||
|
"distribute" : {
|
||||||
|
/* android打包配置 */
|
||||||
|
"android" : {
|
||||||
|
"permissions" : [
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
/* ios打包配置 */
|
||||||
|
"ios" : {
|
||||||
|
"dSYMs" : false,
|
||||||
|
"privacyDescription" : {
|
||||||
|
"NSCameraUsageDescription" : "应用需要访问相机",
|
||||||
|
"NSMicrophoneUsageDescription" : "应用需要访问麦克风",
|
||||||
|
"NSLocalNetworkUsageDescription" : "应用访问本地网络"
|
||||||
|
},
|
||||||
|
"UIBackgroundModes" : [ "audio" ]
|
||||||
|
},
|
||||||
|
/* SDK配置 */
|
||||||
|
"sdkConfigs" : {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"safearea": {
|
"h5" : {
|
||||||
"background": "#ffffff",
|
"optimization" : {
|
||||||
"backgroundDark": "#2f0508",
|
"treeShaking" : {
|
||||||
"bottom": {
|
"enable" : false
|
||||||
"offset": "none"
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/* 模块配置 */
|
"app-harmony" : {
|
||||||
"modules": {},
|
"safearea" : {
|
||||||
/* 应用发布信息 */
|
// HarmonyOS 平台的安全区域
|
||||||
"distribute": {
|
"background" : "#ffffff",
|
||||||
/* android打包配置 */
|
"backgroundDark" : "#2f0508",
|
||||||
"android": {
|
"bottom" : {
|
||||||
"permissions": [
|
"offset" : "none" // 在没有 tabBar 时,底部区域是否需要占位
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
}
|
||||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
}
|
||||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
|
||||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
|
||||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
/* ios打包配置 */
|
|
||||||
"ios": {},
|
|
||||||
/* SDK配置 */
|
|
||||||
"sdkConfigs": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"h5": {
|
|
||||||
"optimization": {
|
|
||||||
"treeShaking": {
|
|
||||||
"enable": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"app-harmony": {
|
|
||||||
"safearea": {
|
|
||||||
// HarmonyOS 平台的安全区域
|
|
||||||
"background": "#ffffff",
|
|
||||||
"backgroundDark": "#2f0508",
|
|
||||||
"bottom": {
|
|
||||||
"offset": "none" // 在没有 tabBar 时,底部区域是否需要占位
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/* 快应用特有相关 */
|
|
||||||
"quickapp": {},
|
|
||||||
/* 小程序特有相关 */
|
|
||||||
"mp-weixin": {
|
|
||||||
"appid": "",
|
|
||||||
"setting": {
|
|
||||||
"urlCheck": false
|
|
||||||
},
|
},
|
||||||
"usingComponents": true
|
/* 快应用特有相关 */
|
||||||
},
|
"quickapp" : {},
|
||||||
"mp-alipay": {
|
/* 小程序特有相关 */
|
||||||
"usingComponents": true
|
"mp-weixin" : {
|
||||||
},
|
"appid" : "",
|
||||||
"mp-baidu": {
|
"setting" : {
|
||||||
"usingComponents": true
|
"urlCheck" : false
|
||||||
},
|
},
|
||||||
"mp-toutiao": {
|
"usingComponents" : true
|
||||||
"usingComponents": true
|
},
|
||||||
},
|
"mp-alipay" : {
|
||||||
"uniStatistics": {
|
"usingComponents" : true
|
||||||
"enable": false
|
},
|
||||||
},
|
"mp-baidu" : {
|
||||||
"vueVersion": "3",
|
"usingComponents" : true
|
||||||
"fallbackLocale": "zh-Hans"
|
},
|
||||||
|
"mp-toutiao" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"uniStatistics" : {
|
||||||
|
"enable" : false
|
||||||
|
},
|
||||||
|
"vueVersion" : "3",
|
||||||
|
"fallbackLocale" : "zh-Hans"
|
||||||
}
|
}
|
||||||
|
|||||||
38
pages.json
@@ -329,6 +329,42 @@
|
|||||||
"navigationBarTitleText": "直播",
|
"navigationBarTitleText": "直播",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/anchor/index",
|
||||||
|
"disableSwipeBack": true,
|
||||||
|
"style": {
|
||||||
|
"backgroundColor": "#000000",
|
||||||
|
"navigationBarTextStyle": "white",
|
||||||
|
"app-plus": {
|
||||||
|
"titleNView": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/liveend/index",
|
||||||
|
"disableSwipeBack": true,
|
||||||
|
"style": {
|
||||||
|
"navigationBarTextStyle": "white",
|
||||||
|
"app-plus": {
|
||||||
|
"titleNView": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/discover/livelist/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/audience/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTextStyle": "white",
|
||||||
|
"app-plus": {
|
||||||
|
"titleNView": false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
@@ -370,4 +406,4 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1113
pages/anchor/index.nvue
Normal file
818
pages/audience/index.nvue
Normal 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>
|
||||||
@@ -8,7 +8,10 @@
|
|||||||
{ name: '朋友圈', icon: 'circle' },
|
{ name: '朋友圈', icon: 'circle' },
|
||||||
{ name: '线上商城', icon: 'mall' },
|
{ name: '线上商城', icon: 'mall' },
|
||||||
{ name: '我的拼团', icon: 'team' },
|
{ name: '我的拼团', icon: 'team' },
|
||||||
{ name: '项目入口', icon: 'project' }
|
{ name: '项目入口', icon: 'project' },
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
{ name: '直播列表', icon: 'liveStream' }
|
||||||
|
// #endif
|
||||||
]
|
]
|
||||||
|
|
||||||
const onGo = item => {
|
const onGo = item => {
|
||||||
@@ -36,6 +39,10 @@
|
|||||||
navigateTo('/pages/shop-together/index')
|
navigateTo('/pages/shop-together/index')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (item === 'liveStream') {
|
||||||
|
navigateTo('/pages/discover/livelist/index')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
252
pages/discover/livelist/index.nvue
Normal 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
@@ -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>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
url: '/pages/my-index/wallet/index'
|
url: '/pages/my-index/wallet/index'
|
||||||
},
|
},
|
||||||
{ name: '我的团队', icon: 'team', url: '/pages/my-index/my-team' },
|
{ name: '我的团队', icon: 'team', url: '/pages/my-index/my-team' },
|
||||||
{ name: '群创建直播', icon: 'videocam', url: '' },
|
// { name: '群创建直播', icon: 'videocam', url: '' },
|
||||||
{
|
{
|
||||||
name: '会议记录',
|
name: '会议记录',
|
||||||
icon: 'meeting',
|
icon: 'meeting',
|
||||||
|
|||||||
BIN
static/images/allow-camera.png
Normal file
|
After Width: | Height: | Size: 621 B |
BIN
static/images/back-black.png
Normal file
|
After Width: | Height: | Size: 288 B |
BIN
static/images/beauty-close.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
static/images/beauty.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
static/images/call.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
static/images/category.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/images/close.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
static/images/connect-audience.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
static/images/create-live.png
Normal file
|
After Width: | Height: | Size: 466 B |
BIN
static/images/dashboard.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
static/images/default-avatar.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
static/images/default-background.jpg
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
static/images/disable-camera.png
Normal file
|
After Width: | Height: | Size: 824 B |
BIN
static/images/discover/liveStream.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
static/images/edit.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
static/images/emoji.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
static/images/end-camera.png
Normal file
|
After Width: | Height: | Size: 824 B |
BIN
static/images/end-join.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
static/images/flip-b.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
static/images/flip.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/flower.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
static/images/gift.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
static/images/gift_heart0.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
static/images/gift_heart1.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
static/images/gift_heart2.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/images/gift_heart3.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
static/images/gift_heart4.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/images/gift_heart5.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
static/images/gift_heart6.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/images/gift_heart7.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
static/images/gift_heart8.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
static/images/hangup.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/images/heart.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
static/images/host-pk.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
static/images/kick-out-room.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
static/images/left-arrow.png
Normal file
|
After Width: | Height: | Size: 339 B |
BIN
static/images/like.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
static/images/link-guest.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
static/images/link-host.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
static/images/live-beauty.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/images/live-comic.png
Normal file
|
After Width: | Height: | Size: 985 B |
BIN
static/images/live-dashboard.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/images/live-disconnect.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
static/images/live-effects.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
static/images/live-emoji.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
static/images/live-end.png
Normal file
|
After Width: | Height: | Size: 1017 B |
BIN
static/images/live-flip.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/images/live-gift.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
static/images/live-like.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
static/images/live-mask.png
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
static/images/live-more.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
static/images/live-music.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
static/images/live-mute-local-video.png
Normal file
|
After Width: | Height: | Size: 715 KiB |
BIN
static/images/live-request.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
static/images/live-ruddy.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
static/images/live-setting.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
static/images/live-settings.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
static/images/live-share.png
Normal file
|
After Width: | Height: | Size: 369 B |
BIN
static/images/logout.png
Normal file
|
After Width: | Height: | Size: 507 B |
BIN
static/images/mine-hover.png
Normal file
|
After Width: | Height: | Size: 730 B |
BIN
static/images/mine-normal.png
Normal file
|
After Width: | Height: | Size: 752 B |
BIN
static/images/mirror.png
Normal file
|
After Width: | Height: | Size: 896 B |
BIN
static/images/mode.png
Normal file
|
After Width: | Height: | Size: 494 B |
BIN
static/images/more.png
Normal file
|
After Width: | Height: | Size: 509 B |
BIN
static/images/mute-mic.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
static/images/mute-speak.png
Normal file
|
After Width: | Height: | Size: 863 B |
BIN
static/images/network-good.png
Normal file
|
After Width: | Height: | Size: 651 B |
BIN
static/images/no-effect.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
static/images/refresh.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
static/images/reverb-bass.png
Normal file
|
After Width: | Height: | Size: 494 B |
BIN
static/images/reverb-bright.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
static/images/reverb-ktv.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/images/reverb-metal.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/images/right-arrow.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
static/images/rocket.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/images/rtc-logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
static/images/sendlike.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
static/images/setting.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
static/images/share.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
static/images/smooth.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |