431 lines
11 KiB
Plaintext
431 lines
11 KiB
Plaintext
<template>
|
||
<view class="barrage-container">
|
||
<!-- 聊天消息列表 -->
|
||
<list class="chat-list" scroll-y :show-scrollbar="false" :style="{ bottom: bottomPx + 'px' }">
|
||
<cell class="chat-item" v-if="mixMessageList.length > 0" v-for="(message) in mixMessageList"
|
||
:key="message?.sequence" @tap="onItemTap(message)">
|
||
<view class="message-content-wrapper">
|
||
<view class="nickname-content-gift" v-if="message?.gift">
|
||
<text class="chat-nickname"
|
||
numberOfLines="1">{{ message?.sender?.userName || message?.sender?.userID }}:</text>
|
||
<view class="gift-row">
|
||
<text class="gift-prefix">{{ giftPrefix }}</text>
|
||
<text class="gift-recipient">{{ giftReceiverName }}</text>
|
||
<text class="gift-name">{{ message?.textContent || '' }}</text>
|
||
<image class="gift-icon" :src="message?.gift?.iconURL || ''" alt="" />
|
||
<text class="gift-count" style="font-size: 26rpx;">x{{ message?.count || '1' }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="nickname-content-wrapper" v-else>
|
||
<view class="message-right" v-if="message?.sender?.userID === currentLive?.liveOwner.userID">
|
||
<text class="message-role">主播</text>
|
||
</view>
|
||
<view class="nickname-content-wrapper">
|
||
<text class="chat-nickname"
|
||
numberOfLines="1">{{ message?.sender?.userName || message?.sender?.userID }}:</text>
|
||
<!-- 主播发送消息
|
||
<text v-if="message?.businessID == 'anchor'" class="chat-content">
|
||
{{ getAnchorMessageText(message) }}
|
||
</text> -->
|
||
<text class="chat-content">
|
||
{{ message?.textContent || '' }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</cell>
|
||
<cell ref="ListBottom" style="height: 50rpx;"></cell>
|
||
</list>
|
||
|
||
<!-- GiftToast 提示 -->
|
||
<view class="toast-container" v-for="toast in visibleToasts" :key="toast.id" :style="getToastStyle(toast)">
|
||
<view class="toast-content">
|
||
<!-- 左侧用户信息 -->
|
||
<view class="user-info">
|
||
<image class="user-avatar" :src="toast?.avatarURL || defaultAvatarURL" mode="aspectFill" />
|
||
<view class="user-details">
|
||
<text class="username">{{ toast?.name || '' }}</text>
|
||
<text class="action-text">{{ toast?.desc || '' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 右侧图标 -->
|
||
<view class="icon-container" v-if="toast.iconURL">
|
||
<image class="icon" :src="toast?.iconURL || ''" mode="aspectFit" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, watch, computed, onMounted, onUnmounted } from 'vue';
|
||
import { useBarrageState } from "@/uni_modules/tuikit-atomic-x/state/BarrageState";
|
||
import { useGiftState } from "@/uni_modules/tuikit-atomic-x/state/GiftState";
|
||
import { useLiveListState } from "@/uni_modules/tuikit-atomic-x/state/LiveListState";
|
||
|
||
const props = defineProps<{
|
||
mode ?: 'anchor' | 'audience',
|
||
bottomPx ?: number,
|
||
liveID ?: string,
|
||
toast ?: any, // GiftToast 的 toast 属性
|
||
}>();
|
||
|
||
const mode = computed(() => props.mode || 'audience');
|
||
const bottomPx = computed(() => props.bottomPx ?? 0);
|
||
|
||
const liveID = computed(() => props.liveID || uni?.$liveID);
|
||
|
||
const { messageList } = useBarrageState(liveID.value);
|
||
const { addGiftListener, removeGiftListener } = useGiftState(liveID.value);
|
||
const { currentLive } = useLiveListState(liveID.value);
|
||
|
||
const mixMessageList = ref<any[]>(messageList.value || []);
|
||
|
||
const dom = uni.requireNativePlugin('dom');
|
||
const ListBottom = ref('ListBottom');
|
||
|
||
const giftPrefix = computed(() => mode.value === 'anchor' ? '送给我' : '送给');
|
||
const giftReceiverName = computed(() => {
|
||
if (mode.value === 'anchor') return '';
|
||
const owner = currentLive?.value?.liveOwner || {};
|
||
return owner?.userName || owner?.userID || '';
|
||
});
|
||
|
||
// GiftToast 相关状态和逻辑
|
||
const defaultAvatarURL = 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_01.png';
|
||
const defaultPosition = {
|
||
top: 'auto',
|
||
bottom: 720,
|
||
left: 32,
|
||
right: 'auto',
|
||
};
|
||
const toastHeight = 105;
|
||
|
||
const visibleToasts = ref<any[]>([]);
|
||
|
||
const generateId = () => {
|
||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||
};
|
||
|
||
const showToast = (toastConfig : Partial<any>) => {
|
||
const toast = {
|
||
id: generateId(),
|
||
duration: 3000,
|
||
autoHide: true,
|
||
position: {
|
||
top: defaultPosition.top,
|
||
bottom: defaultPosition.bottom + (visibleToasts.value || []).length * toastHeight,
|
||
left: '32rpx',
|
||
right: defaultPosition.bottom,
|
||
},
|
||
...toastConfig
|
||
};
|
||
|
||
visibleToasts.value.push(toast);
|
||
|
||
if (toast.autoHide && toast.duration) {
|
||
setTimeout(() => {
|
||
hideToast(toast.id);
|
||
}, toast.duration);
|
||
}
|
||
|
||
return toast.id;
|
||
};
|
||
|
||
const hideToast = (id : string) => {
|
||
const index = visibleToasts.value.findIndex(toast => toast.id === id);
|
||
if (index > -1) {
|
||
const removedToast = visibleToasts.value.splice(index, 1)[0];
|
||
emit('toastClosed', removedToast);
|
||
}
|
||
};
|
||
|
||
const hideAllToasts = () => {
|
||
visibleToasts.value = [];
|
||
};
|
||
|
||
const getToastStyle = (toast : any) => {
|
||
const style : any = {
|
||
position: 'fixed',
|
||
zIndex: 999
|
||
};
|
||
|
||
if (toast.position) {
|
||
if (toast.position.top !== undefined) {
|
||
style.top = typeof toast.position.top === 'number' ? `${toast.position.top}rpx` : toast.position.top;
|
||
}
|
||
if (toast.position.bottom !== undefined) {
|
||
style.bottom = typeof toast.position.bottom === 'number' ? `${toast.position.bottom}rpx` : toast.position.bottom;
|
||
}
|
||
if (toast.position.left !== undefined) {
|
||
style.left = typeof toast.position.left === 'number' ? `${toast.position.left}rpx` : toast.position.left;
|
||
}
|
||
if (toast.position.right !== undefined) {
|
||
style.right = typeof toast.position.right === 'number' ? `${toast.position.right}rpx` : toast.position.right;
|
||
}
|
||
}
|
||
|
||
return style;
|
||
};
|
||
|
||
watch(messageList, (newVal, oldVal) => {
|
||
if (!newVal) return;
|
||
const value = newVal.slice((oldVal || []).length, (newVal || []).length);
|
||
if (value.length > 0) {
|
||
mixMessageList.value = [...mixMessageList.value, ...value];
|
||
dom.scrollToElement(ListBottom.value);
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(['itemTap', 'toastClosed']);
|
||
const onItemTap = (message : any) => {
|
||
emit('itemTap', message);
|
||
};
|
||
|
||
/** 主播发送自定义消息显示文本 */
|
||
const getAnchorMessageText = (message: any) => {
|
||
const data = JSON.parse(message.data)
|
||
return data.count
|
||
}
|
||
|
||
const handleReceiveGift = {
|
||
callback: (event) => {
|
||
const res = JSON.parse(event)
|
||
console.log('===消息接收===', res)
|
||
const value = {
|
||
...res,
|
||
textContent: `${res.gift?.name || ''}`,
|
||
};
|
||
mixMessageList.value = [...mixMessageList.value, value];
|
||
dom.scrollToElement(ListBottom.value);
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (props.toast && Object.keys(props.toast).length > 0) {
|
||
showToast(props.toast);
|
||
}
|
||
addGiftListener(uni.$liveID, 'onReceiveGift', handleReceiveGift)
|
||
});
|
||
onUnmounted(() => {
|
||
removeGiftListener(uni.$liveID, 'onReceiveGift', handleReceiveGift)
|
||
});
|
||
|
||
// 暴露给父组件的方法
|
||
defineExpose({
|
||
showToast,
|
||
hideToast,
|
||
hideAllToasts
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.barrage-container {
|
||
position: relative;
|
||
}
|
||
|
||
.chat-list {
|
||
position: fixed;
|
||
left: 32rpx;
|
||
right: 32rpx;
|
||
bottom: 800rpx;
|
||
height: 380rpx;
|
||
width: 500rpx;
|
||
}
|
||
|
||
/* GiftToast 样式 */
|
||
.toast-container {
|
||
/* 基础样式由getToastStyle动态设置 */
|
||
}
|
||
|
||
.toast-content {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: rgba(34, 38, 46, 0.4);
|
||
padding: 20rpx 24rpx;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.1);
|
||
height: 100rpx;
|
||
width: 320rpx;
|
||
border-radius: 50rpx;
|
||
backdrop-filter: blur(10rpx);
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 40rpx;
|
||
border-width: 2rpx;
|
||
border-color: #8B5CF6;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.user-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
}
|
||
|
||
.username {
|
||
color: #ffffff;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
margin-bottom: 4rpx;
|
||
max-width: 120rpx;
|
||
lines: 1;
|
||
text-overflow: ellipsis;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.action-text {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
font-size: 24rpx;
|
||
font-weight: 400;
|
||
max-width: 120rpx;
|
||
lines: 1;
|
||
text-overflow: ellipsis;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.icon-container {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.icon {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
}
|
||
|
||
.chat-item {
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
border-radius: 32rpx;
|
||
padding: 6rpx;
|
||
width: fit-content;
|
||
max-width: 500rpx;
|
||
}
|
||
|
||
.message-content-wrapper {
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
min-width: 0;
|
||
max-width: 500rpx;
|
||
width: fit-content;
|
||
background-color: rgba(0, 0, 0, 0.25);
|
||
border-radius: 999rpx;
|
||
padding: 8rpx 12rpx;
|
||
}
|
||
|
||
.nickname-content-wrapper {
|
||
flex: 1;
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.nickname-content-gift {
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
min-width: 0;
|
||
max-width: 500rpx;
|
||
width: fit-content;
|
||
}
|
||
|
||
.message-role {
|
||
background-color: #0468FC;
|
||
border-radius: 999px;
|
||
margin-right: 5rpx;
|
||
color: #fff;
|
||
padding: 5rpx 15rpx;
|
||
font-size: 20rpx;
|
||
}
|
||
|
||
.chat-nickname {
|
||
color: #80BEF6;
|
||
font-size: 24rpx;
|
||
line-height: 24rpx;
|
||
margin-right: 8rpx;
|
||
padding: 5rpx 0;
|
||
flex-shrink: 0;
|
||
max-width: 200rpx;
|
||
lines: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.gift-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
max-width: 300rpx;
|
||
flex: 1;
|
||
justify-content: flex-start;
|
||
word-wrap: break-word;
|
||
word-break: break-all;
|
||
white-space: normal;
|
||
flex-wrap: wrap;
|
||
padding-top: 2rpx;
|
||
}
|
||
|
||
.gift-icon {
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
flex-shrink: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.gift-recipient,
|
||
.gift-name {
|
||
color: #ffffff;
|
||
font-size: 24rpx;
|
||
line-height: 24rpx;
|
||
font-weight: 500;
|
||
z-index: 999;
|
||
padding: 2rpx 0;
|
||
max-width: 300rpx;
|
||
word-wrap: break-word;
|
||
word-break: break-all;
|
||
white-space: normal;
|
||
text-align: left;
|
||
min-width: 0;
|
||
}
|
||
|
||
.gift-prefix,
|
||
.gift-count {
|
||
color: #ffffff;
|
||
font-size: 24rpx;
|
||
line-height: 24rpx;
|
||
font-weight: 500;
|
||
z-index: 999;
|
||
}
|
||
|
||
.chat-content {
|
||
color: #ffffff;
|
||
font-size: 24rpx;
|
||
line-height: 24rpx;
|
||
font-weight: 500;
|
||
z-index: 999;
|
||
padding: 5rpx 0;
|
||
word-wrap: break-word;
|
||
word-break: break-all;
|
||
white-space: normal;
|
||
flex: 1;
|
||
min-width: 0;
|
||
text-indent: 0;
|
||
padding-left: 0;
|
||
}
|
||
</style> |