Files
uniapp-im-shop/components/BeforeLivePanel.nvue
2026-01-12 17:52:15 +08:00

577 lines
15 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>