577 lines
15 KiB
Plaintext
577 lines
15 KiB
Plaintext
<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> |