需要添加直播接口

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

4
.env
View File

@@ -1,5 +1,5 @@
# 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"

View File

@@ -7,8 +7,8 @@
"type" : "uni-app:app-ios"
},
{
"customPlaygroundType" : "device",
"playground" : "standard",
"customPlaygroundType" : "local",
"playground" : "custom",
"type" : "uni-app:app-android"
}
]

11
App.vue
View File

@@ -3,15 +3,18 @@
import { reLaunch } from './utils/router'
import { useAuthUser } from './composables/useAuthUser'
import { useUserStore } from './stores/user'
import { TUIChatKit } from './TUIKit'
// #ifdef APP-PLUS
import { setSdkLanguageFromSystem } from '@/uni_modules/tuikit-atomic-x/utils/setSdkLanguageFromSystem'
// #endif
TUIChatKit.init()
const { token } = useAuthUser()
const { loginTencentIM } = useUserStore()
/** 静默登录逻辑 */
const silentLogin = async () => {
console.log(token.value, '==')
if (token.value) {
loginTencentIM()
reLaunch('/TUIKit/components/TUIConversation/index')
@@ -25,6 +28,10 @@
onLaunch(() => {
console.log('App Launch111')
silentLogin()
// #ifdef APP-PLUS
setSdkLanguageFromSystem()
// #endif
})
onShow(() => {

View File

@@ -15,6 +15,8 @@ unaipp多端im+会议+积分商城
```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```
<!-- show-loading-more-no-more-view 去除没有更多 -->
### TUILiveKit 文档地址
```https://trtc.io/zh/document/60034?platform=ios&product=live```

View File

@@ -67,11 +67,14 @@
<RedEnvelope />
<!-- 直播按钮 -->
<!-- #ifdef APP-PLUS -->
<LiveStream
v-if="
currentConversation?.type === TUIChatEngine.TYPES.CONV_GROUP
"
:groupId="currentConversation?.conversationID"
/>
<!-- #endif -->
</template>
</swiper-item>
<swiper-item

View File

@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup>
import { ref } from '../../../../adapter-vue'
import custom from '../../../../assets/icon/live-stream.svg'
import ToolbarItemContainer from '../toolbar-item-container/index.vue'
@@ -10,6 +10,14 @@
const { showDialog } = useUI()
const evaluateIcon = custom
const props = defineProps({
/** 距离顶部高度 */
groupId: {
type: String,
default: ''
}
})
const container = ref()
/**
* 主播申请状态
@@ -34,7 +42,12 @@
return
}
if (stateData.value === 1) {
console.log('去直播间')
// 跳转到开播页面
uni.navigateTo({
url: `/pages/anchor/index?groupId=${encodeURIComponent(
props.groupId
)}`
})
return
}
if (stateData.value === 3) {
@@ -45,6 +58,7 @@
</script>
<template>
<ToolbarItemContainer
ref="container"
:iconFile="evaluateIcon"

View File

@@ -80,3 +80,28 @@ export const getAnchorDetail = () => {
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
View 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>

View 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>

View 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>

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,97 +1,108 @@
{
"name": "uniapp-imitate-wx",
"appid": "__UNI__9EFDC69",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
/* 5+App */
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
"name" : "uniapp-imitate-wx",
"appid" : "__UNI__9EFDC69",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"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": {
"background": "#ffffff",
"backgroundDark": "#2f0508",
"bottom": {
"offset": "none"
}
"h5" : {
"optimization" : {
"treeShaking" : {
"enable" : false
}
}
},
/* */
"modules": {},
/* */
"distribute": {
/* android */
"android": {
"permissions": [
"<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
"app-harmony" : {
"safearea" : {
// HarmonyOS 平台的安全区域
"background" : "#ffffff",
"backgroundDark" : "#2f0508",
"bottom" : {
"offset" : "none" // 在没有 tabBar 时,底部区域是否需要占位
}
}
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"fallbackLocale": "zh-Hans"
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"fallbackLocale" : "zh-Hans"
}

View File

@@ -329,6 +329,42 @@
"navigationBarTitleText": "直播",
"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": {
@@ -370,4 +406,4 @@
}
]
}
}
}

1113
pages/anchor/index.nvue Normal file

File diff suppressed because it is too large Load Diff

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

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

View File

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

View File

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

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

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
static/images/beauty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
static/images/call.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
static/images/category.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/images/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

BIN
static/images/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/images/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
static/images/emoji.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

BIN
static/images/end-join.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
static/images/flip-b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
static/images/flip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
static/images/flower.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
static/images/gift.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
static/images/hangup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/images/heart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
static/images/host-pk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

BIN
static/images/like.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
static/images/link-host.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
static/images/live-end.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 B

BIN
static/images/live-flip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/images/live-gift.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
static/images/live-like.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
static/images/live-mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

BIN
static/images/live-more.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

BIN
static/images/logout.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

BIN
static/images/mirror.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

BIN
static/images/mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

BIN
static/images/more.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

BIN
static/images/mute-mic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

BIN
static/images/no-effect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
static/images/refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
static/images/rocket.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
static/images/rtc-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/images/sendlike.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
static/images/setting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
static/images/share.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/images/smooth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Some files were not shown because too many files have changed in this diff Show More