需要添加直播接口

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

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>