1156 lines
45 KiB
Vue
1156 lines
45 KiB
Vue
<template>
|
||
<!-- #ifdef APP-NVUE -->
|
||
<view class="flex-col jc-sb pr pa-10 box-border-box bottom-line-exclude-bottom" :style="'width:' + propWindowWidth + 'px;height:' + propWindowHeight + 'px;'">
|
||
<!-- #endif -->
|
||
<!-- #ifndef APP-NVUE -->
|
||
<view class="flex-col jc-sb pr pa-10 box-border-box bottom-line-exclude-bottom" style="width: 100%;height: 100vh;">
|
||
<!-- #endif -->
|
||
<!-- 顶部主播信息 -->
|
||
<view class="flex-row align-c jc-sb" :style="header_style">
|
||
<view class="top-header flex-row align-c pointer-events-auto">
|
||
<image :src="live_avatar" class="avatar" mode="aspectFill"></image>
|
||
<view class="ml-10 flex-col">
|
||
<text class="nickname text-line-1">{{ live_data && live_data.title ? live_data.title : '直播' }}</text>
|
||
<text class="level">{{ like_count }}本场点赞</text>
|
||
</view>
|
||
</view>
|
||
<view class="flex-row align-c pointer-events-auto">
|
||
<view class="flex-row align-c pr" style="direction: rtl;">
|
||
<view v-for="(item, index) in online_user" :key="index" class="viewer-wrapper" :style="'z-index:' + (index + 1) + ';' + (index == 0 ? 'margin-right: 0;' : '')">
|
||
<image :src="item.avatar" class="viewer-avatar" mode="aspectFill"></image>
|
||
</view>
|
||
</view>
|
||
<view class="ml-5 people-number flex-row align-c jc-c">
|
||
<text class="cr-f size-10">{{ live_data.online_count || 0 }}</text>
|
||
</view>
|
||
<view class="viewer-back ml-5 flex-row align-c jc-c " @tap="live_back">
|
||
<u-icon propName="close-fillup" class="viewer-back-icon" propSize="50rpx" propColor="#fff"></u-icon>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="flex-1 bottom-line-exclude-bottom flex-row">
|
||
<view class="flex-1 flex-col jc-e">
|
||
<view class="pr">
|
||
<view class="bulletin-area pr pointer-events-auto" :style="'width:' + (propWindowWidth - 150) + 'px;'">
|
||
<!-- #ifdef APP-NVUE -->
|
||
<!-- nvue 使用 list进行列表渲染 -->
|
||
<list class="bulletin-area" :style="'width:' + (propWindowWidth - 150) + 'px;'" :show-scrollbar="false" loadmoreoffset="30" @scroll="scroll_event" @loadmore="scroll_to_lower_event">
|
||
<cell v-for="(item, index) in bulletins" :key="item.id" ref="bulletin_index">
|
||
<!-- #endif -->
|
||
<!-- #ifndef APP-NVUE -->
|
||
<!-- scroll-view 只有非nvue的页面使用 -->
|
||
<scroll-view scroll-y class="bulletin-area" :style="'width:' + (propWindowWidth - 150) + 'px;'" :show-scrollbar="false" lower-threshold="30" :scroll-with-animation="true" :scroll-top="scroll_top" @scroll="scroll_event" @scrolltolower="scroll_to_lower_event">
|
||
<view v-for="(item, index) in bulletins" :key="item.id">
|
||
<!-- #endif -->
|
||
<!-- 中间弹幕区域 -->
|
||
<view class="mb-5 flex-row align-c">
|
||
<!-- #ifndef APP-NVUE -->
|
||
<view class="bulletin-item">
|
||
<template v-if="item.type == 'user'">
|
||
<!-- 用户名和文本内容容器 -->
|
||
<view class="inline-block">
|
||
<view class="fl flex-row align-c jc-c padding-top-xsss">
|
||
<!-- 头像 -->
|
||
<image :src="item.user_avatar != null ? item.user_avatar : userAvatar" class="bulletin-item-avatar" mode="aspectFill"></image>
|
||
</view>
|
||
<text class="user-name cr-blue">{{ item.user_name }}:</text>
|
||
<text class="bulletin-text text-line-100">{{ item.text }}</text>
|
||
</view>
|
||
</template>
|
||
<template v-else-if="item.type == 'go'">
|
||
<text class="user-name cr-blue">{{ item.user_name }}</text>
|
||
<text class="user-name cr-d">来了</text>
|
||
</template>
|
||
<template v-else>
|
||
<text class="flex-1 cr-blue text-line-100 size-14">{{ item.text }}</text>
|
||
</template>
|
||
</view>
|
||
<!-- #endif -->
|
||
<!-- #ifdef APP-NVUE -->
|
||
<view class="flex-1 bulletin-item padding-top-sm">
|
||
<template v-if="item.type == 'user'">
|
||
<!-- 头像 -->
|
||
<view class="flex-1 flex-row align-c flex-wrap">
|
||
<image :src="item.user_avatar != null ? item.user_avatar : userAvatar" class="bulletin-item-avatar mb-3" mode="aspectFill"></image>
|
||
<text class="user-name cr-blue mb-3">{{ item.user_name }}:</text>
|
||
<view v-for="item in split_text(item.text)" :key="index" class="mb-3">
|
||
<text class="bulletin-text">{{ item }}</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
<template v-else-if="item.type == 'go'">
|
||
<text class="user-name cr-blue mb-3">{{ item.user_name }}</text>
|
||
<text class="user-name mb-3 cr-d">来了</text>
|
||
</template>
|
||
<template v-else>
|
||
<text class="flex-1 cr-blue text-line-100 mb-3 size-14">{{ item.text }}</text>
|
||
</template>
|
||
</view>
|
||
<!-- #endif -->
|
||
</view>
|
||
<!-- #ifdef APP-NVUE -->
|
||
<!-- nvue 使用 list进行列表渲染 -->
|
||
</cell>
|
||
</list>
|
||
<!-- #endif -->
|
||
<!-- #ifndef APP-NVUE -->
|
||
<!-- scroll-view 只有非nvue的页面使用 -->
|
||
</view>
|
||
</scroll-view>
|
||
<!-- #endif -->
|
||
<view v-if="!is_scroll_to_lower && message_num > 0" class="bulletin-tips flex-row align-c jc-c" :style="'width:' + (propWindowWidth - 150) + 'px;'" @tap="message_num_event">
|
||
<view class="bulletin-tips-content flex-row align-c">
|
||
<text class="cr-10 cr-red">{{ message_num }}条新消息</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="!isEmpty(explain_goods) && is_show_explain_goods" class="explain-goods pointer-events-auto" :data-url="explain_goods.goods_url" @tap="explain_goods_tap">
|
||
<view class="pr" style="width: 198rpx;height: 198rpx;">
|
||
<image :src="explain_goods.images" class="explain-goods-image" style="width: 198rpx;height: 198rpx;" mode="aspectFill"></image>
|
||
<view class="explain-subscript flex-row align-c jc-sb">
|
||
<view class="explain-progress">
|
||
<text class="size-12 cr-f">讲解中</text>
|
||
</view>
|
||
<view class="explain-close flex-row align-c" @tap.stop="explain_goods_close">
|
||
<u-icon propName="close-line" propSize="18rpx" propColor="#fff"></u-icon>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="explain-goods-content mt-10" style="padding: 8rpx;box-sizing: border-box;">
|
||
<text class="explain-goods-name text-line-2 size-12">{{ explain_goods.title }}</text>
|
||
<text class="explain-goods-price cr-red mt-10 size-12">{{ currency_symbol }}{{ explain_goods.price }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 底部交互区域 -->
|
||
<view class="flex-row align-c mt-5 pointer-events-auto pr">
|
||
<template v-if="is_socket_success">
|
||
<view class="flex-1 bottom-actions-input">
|
||
<input :value="comment_value" type="text" confirm-type="done" :adjust-position="false" style="color: #fff;" placeholder="说点什么" @focus="add_comment" @input="(e) => comment_value = e.detail.value" @confirm="comment_input_confirm" />
|
||
</view>
|
||
</template>
|
||
<template v-else>
|
||
<view class="flex-1">
|
||
<button class="bottom-actions-button cr-f size-14" type="primary" style="border-radius: 50rpx;background: rgba(40,40,40,0.45);border: 1rpx solid rgba(40,40,40,0.45);" :hover-class="is_socket_error ? 'none' : 'button-hover'" @tap="socket_connect_manual">{{ socket_error_content }}</button>
|
||
</view>
|
||
</template>
|
||
<view class="bottom-actions-icon" @tap="add_goods">
|
||
<u-icon propName="shopping-cart-tall" propColor="#fff" propSize="32rpx"></u-icon>
|
||
</view>
|
||
<component-like-button ref="likeButton" :propShowImgs="propLiveShowImgs" @handleClick="like_button_click">
|
||
<view class="bottom-actions-icon">
|
||
<u-icon propName="givealike-o" propColor="#fff" propSize="32rpx"></u-icon>
|
||
</view>
|
||
</component-like-button>
|
||
<view class="bottom-actions-icon" @tap="share_event">
|
||
<u-icon propName="share-solid" propColor="#fff" propSize="32rpx"></u-icon>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 添加评论 -->
|
||
<view v-if="is_add_comment" class="keyboard-input pointer-events-auto" :style="'width:' + propWindowWidth + 'px;bottom:' + listener_height + 'px;'">
|
||
<view class="keyboard-input-border" style="padding: 16rpx 22rpx;border: 2rpx solid #ddd;border-radius: 50rpx;">
|
||
<input :value="comment_value" :focus="is_add_comment" type="text" confirm-type="done" :adjust-position="false" :auto-blur="true" placeholder="说点什么" @input="(e) => comment_value = e.detail.value" @blur="() => is_add_comment = false" @confirm="comment_input_confirm" />
|
||
</view>
|
||
</view>
|
||
<!-- 商品弹出框 -->
|
||
<u-popup ref="popupGoodsRef" propMode="bottom" class="pointer-events-auto" propTitle="添加商品" :propCloseable="true">
|
||
<component-goods propIsGoodsPopup :propWindowWidth="propWindowWidth" :propWindowHeight="propWindowHeight"></component-goods>
|
||
</u-popup>
|
||
<!-- 分享弹窗 -->
|
||
<u-share-popup ref="share" class="pointer-events-auto"></u-share-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import componentGoods from "@/pages/plugins/live/pull/components/goods/goods.vue";
|
||
import componentLikeButton from "@/pages/plugins/live/pull/components/like-button/like-button";
|
||
import { isEmpty } from '@/common/js/common/common.js';
|
||
const app = getApp();
|
||
export default {
|
||
name: 'LiveContent',
|
||
components: {
|
||
componentGoods,
|
||
componentLikeButton,
|
||
},
|
||
/**
|
||
* 直播内容组件属性
|
||
*/
|
||
props: {
|
||
/**
|
||
* 直播配置信息
|
||
*/
|
||
propLiveConfig: {
|
||
type: Object,
|
||
default: () => {}
|
||
},
|
||
/**
|
||
* 直播间全部信息
|
||
*/
|
||
propLiveData: {
|
||
type: Object,
|
||
default: () => {}
|
||
},
|
||
/**
|
||
* 直播展示图片
|
||
*/
|
||
propLiveShowImgs: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
propWindowWidth: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
propWindowHeight: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
application_client_type: app.globalData.application_client_type(),
|
||
application_client_brand: app.globalData.application_client_brand(),
|
||
goods_popup_status: false,
|
||
// 点赞计数
|
||
like_count: 0,
|
||
// 临时点赞计数
|
||
casual_like_count: 0,
|
||
like_timer: null,
|
||
// 直播间配置信息
|
||
live_data: {},
|
||
userAvatar: '/static/images/common/user.png',
|
||
// 模拟观看者数据
|
||
online_user: [],
|
||
// 讲解中商品信息
|
||
explain_goods: {},
|
||
// 价格符号
|
||
currency_symbol: app.globalData.currency_symbol(),
|
||
//#region 头部样式和页面宽度处理
|
||
header_style: '',
|
||
//#endregion
|
||
|
||
//#region 评论区
|
||
bulletins: [
|
||
{
|
||
id: '1',
|
||
type: 'message',
|
||
text: 'xxx提倡绿色直播,严禁未成年人直播或打赏,严禁涉政、涉恐、涉黄、聚集闹事、返现等内容,平台将会24小时巡查。请勿参与直播间非官方奖励活动/游戏,切勿私下交易,以防受骗。'
|
||
},
|
||
{
|
||
id: '2',
|
||
type: 'user',
|
||
user_avatar: '/static/images/common/user.png',
|
||
user_name: '陌生网友',
|
||
text: '你好'
|
||
},
|
||
{
|
||
id: '3',
|
||
type: 'user',
|
||
user_avatar: '/static/images/common/user.png',
|
||
user_name: '陌生网友',
|
||
text: '21245445454545454545545445452124544545454545454554544545'
|
||
},
|
||
{
|
||
id: '9',
|
||
type: 'go',
|
||
user_avatar: '/static/images/common/user.png',
|
||
user_name: '陌生网友',
|
||
text: '228'
|
||
}
|
||
],
|
||
scroll_top: 0,
|
||
// 滚动条的高度
|
||
scoll_height: 600,
|
||
//#ifdef APP-NVUE
|
||
domModule: null,
|
||
//#endif
|
||
bulletin_index: null,
|
||
// 列表滚动事件
|
||
// 判断列表是否滚动了,如果滚动了,就认为他不是在底部,就要显示有多少条信息
|
||
is_first_scroll: true,
|
||
is_scroll_to_lower: true,
|
||
message_num: 0,
|
||
//#endregion
|
||
|
||
//#region 获取用户头像信息和socket连接信息
|
||
live_avatar: '/static/images/common/user.png',
|
||
// 连接socket
|
||
task: null,
|
||
// socket消息id
|
||
socket_id: 0,
|
||
// 心跳定时任务
|
||
ping_timer: null,
|
||
// 获取直播间数据定时任务
|
||
pull_live_room_info_timer: null,
|
||
// 心跳间隔时间
|
||
ping_interval: 30,
|
||
// 当前观看直播用户id
|
||
live_user_id: [],
|
||
// 观看用户名称
|
||
// commons_name: '陌生网友', // 用户名称
|
||
// is_user_comes: false,
|
||
// is_user_comes_timer: null,
|
||
//#endregion
|
||
|
||
//#region 监听键盘弹出事件
|
||
comment_value: '',
|
||
is_add_comment: false,
|
||
// 监听键盘高度变化事件
|
||
listener_height: 0,
|
||
//#endregion
|
||
reconnect_count: 0,
|
||
// socket连接错误
|
||
is_socket_error: false,
|
||
socket_error_content: '连接失败点击重试',
|
||
// socket连接成功
|
||
is_socket_success: false,
|
||
// 是否展示讲解商品信息
|
||
is_show_explain_goods: true,
|
||
goods_hide_timer: null, // 讲解商品信息隐藏定时器
|
||
live_room_info_timing_interval_time: 30, // 获取直播间数据定时任务时间间隔
|
||
live_room_goods_explain_auto_close_time: 10, // 讲解商品信息自动关闭时间
|
||
}
|
||
},
|
||
watch: {
|
||
/**
|
||
* 监听直播信息
|
||
*/
|
||
propLiveData: {
|
||
handler(new_value) {
|
||
if (new_value != null) {
|
||
this.live_init(new_value);
|
||
}
|
||
},
|
||
immediate: true,
|
||
deep: true
|
||
},
|
||
/**
|
||
* 监听直播展示图片变更
|
||
*/
|
||
propLiveConfig: {
|
||
handler(new_value) {
|
||
if (new_value != null) {
|
||
// 直播间信息定时任务时间间隔
|
||
this.live_room_info_timing_interval_time = new_value.live_room_info_timing_interval_time;
|
||
// 讲解商品信息自动关闭时间
|
||
this.live_room_goods_explain_auto_close_time = new_value.live_room_goods_explain_auto_close_time;
|
||
}
|
||
},
|
||
immediate: true,
|
||
deep: true
|
||
},
|
||
},
|
||
/**
|
||
* 组件挂载后执行初始化操作
|
||
*/
|
||
mounted() {
|
||
// 初始化窗口信息和滚动条高度
|
||
this.init_window_info();
|
||
// 滚动到评论区底部
|
||
this.scroll_to_lower();
|
||
// 获取用户信息
|
||
this.init_user_info();
|
||
// 创建监听事件
|
||
this.bind_keyboard_listener();
|
||
},
|
||
/**
|
||
* 组件销毁前清理资源
|
||
*/
|
||
beforeDestroy() {
|
||
// 清理socket连接
|
||
this.clear_interval_task();
|
||
this.unbind_keyboard_listener();
|
||
// 关闭socket连接
|
||
this.socket_close();
|
||
},
|
||
methods: {
|
||
isEmpty,
|
||
//#region 头部样式和页面宽度处理
|
||
/**
|
||
* 初始化窗口信息
|
||
* 获取屏幕宽高,并根据不同平台设置头部样式
|
||
*/
|
||
init_window_info() {
|
||
// 菜单按钮位置信息, uniappx中没有这个方法,但是能使用
|
||
this.header_style = 'padding-top: 20rpx;';
|
||
// 设置有胶囊的时候头部显示的位置
|
||
// #ifdef MP
|
||
// 判断是否有胶囊
|
||
const is_page = app.globalData.is_current_single_page();
|
||
// 如果有胶囊的时候,做处理
|
||
if (is_page == 0) {
|
||
const custom = uni.getMenuButtonBoundingClientRect();
|
||
this.header_style = `padding-top: ${custom.top + custom.height}px;`;
|
||
}
|
||
//#endif
|
||
//#ifdef APP
|
||
this.header_style = 'padding-top: 88rpx;'
|
||
//#endif
|
||
|
||
//#ifdef APP-NVUE
|
||
this.scoll_height = app.globalData.rpx_to_px(600);
|
||
this.domModule = uni.requireNativePlugin('dom');
|
||
//#endif
|
||
//#ifndef APP-NVUE
|
||
this.scoll_height = app.globalData.rpx_to_px(600);
|
||
//#endif
|
||
|
||
//#ifdef APP-NVUE
|
||
this.nvueAnimation = uni.requireNativePlugin('animation');
|
||
//#endif
|
||
},
|
||
//#endregion
|
||
|
||
/**
|
||
* 退出直播
|
||
* 触发父组件的liveBack事件
|
||
*/
|
||
live_back() {
|
||
this.$emit('liveBack');
|
||
},
|
||
|
||
//#region 评论区
|
||
/**
|
||
* 滚动到评论区最底部
|
||
*/
|
||
scroll_to_lower() {
|
||
this.$nextTick(() => {
|
||
const num = Math.random() + 20;
|
||
//#ifndef APP-NVUE
|
||
this.scroll_top = this.scoll_height + num;
|
||
//#endif
|
||
//#ifdef APP-NVUE
|
||
if (this.bulletin_index && this.bulletin_index.length > 0) {
|
||
this.domModule.scrollToElement((this.bulletin_index[this.bulletins.length - 1]), {
|
||
offset: this.scoll_height + num, // 偏移量,可根据需要调整
|
||
animated: true // 是否带动画
|
||
});
|
||
}
|
||
//#endif
|
||
})
|
||
},
|
||
/**
|
||
* 将文本拆分为字符数组(用于NVUE环境)
|
||
* @param {String} val - 需要拆分的文本
|
||
* @returns {Array} 拆分后的字符数组
|
||
*/
|
||
split_text(val) {
|
||
return val.split('');
|
||
},
|
||
/**
|
||
* 滚动事件处理
|
||
* @param {Event} e - 滚动事件对象
|
||
*/
|
||
scroll_event(e) {
|
||
this.is_scroll_to_lower = false;
|
||
//#ifdef APP-NVUE
|
||
if (this.is_first_scroll) {
|
||
this.is_first_scroll = false;
|
||
this.scroll_to_lower_event(e);
|
||
}
|
||
//#endif
|
||
//#ifndef APP-NVUE
|
||
// 第一次滚动的时候不会触发滚动到底部事件,需要手动触发一下
|
||
if (e.detail.scrollTop > 0 && this.is_first_scroll) {
|
||
this.is_first_scroll = false;
|
||
this.scroll_to_lower_event(e);
|
||
}
|
||
//#endif
|
||
},
|
||
/**
|
||
* 滚动到底部事件处理
|
||
* @param {Event} e - 滚动事件对象
|
||
*/
|
||
scroll_to_lower_event(e) {
|
||
// 滑动到底部的时候,清除历史存储的消息数据
|
||
this.message_num = 0;
|
||
this.is_scroll_to_lower = true;
|
||
},
|
||
/**
|
||
* 新消息提示点击事件处理
|
||
* 点击后滚动到评论区底部
|
||
*/
|
||
message_num_event() {
|
||
this.scroll_to_lower();
|
||
},
|
||
//#endregion
|
||
|
||
//#region 获取用户头像信息和socket连接信息
|
||
/**
|
||
* 初始化用户信息
|
||
* 从缓存获取用户头像并建立WebSocket连接
|
||
*/
|
||
init_user_info() {
|
||
const new_user = uni.getStorageSync(uni.$store?.state?.cache_user_info_key ?? '') || null;
|
||
if (new_user != null) {
|
||
this.avatar = new_user.avatar;
|
||
}
|
||
// 连接socket
|
||
this.socket_connect();
|
||
},
|
||
/**
|
||
* 手动连接WebSocket
|
||
*/
|
||
socket_connect_manual() {
|
||
if (!this.is_socket_error) {
|
||
this.socket_connect(true);
|
||
}
|
||
},
|
||
/**
|
||
* 建立WebSocket连接
|
||
* @param {Boolean} is_manual - 是否手动连接
|
||
*/
|
||
socket_connect(is_manual = false) {
|
||
// 一开始就设置为false,避免连接失败时,页面显示错误
|
||
this.is_socket_error = false;
|
||
this.is_socket_success = false;
|
||
// 第一次连接时显示连接中...
|
||
if (this.reconnect_count == 0) {
|
||
this.socket_error_content = '连接中...';
|
||
} else {
|
||
this.socket_error_content = `第${this.reconnect_count + 1}次重连中...`;
|
||
}
|
||
|
||
this.task = uni.connectSocket({
|
||
url: "wss://new.shopxo.vip:9502",
|
||
header: {
|
||
"content-type": "application/json",
|
||
},
|
||
complete: () => {}
|
||
});
|
||
|
||
// 连接打开事件
|
||
this.task.onOpen((res) => {
|
||
// 连接成功后重置重连计数
|
||
this.reconnect_count = 0;
|
||
this.is_socket_success = true;
|
||
});
|
||
|
||
this.task.onMessage((res) => {
|
||
this.socket_message_back_handle(res);
|
||
});
|
||
this.task.onClose((res) => {
|
||
this.is_socket_error = true;
|
||
// 尝试重连,最多30次
|
||
if ((this.reconnect_count + 1) < 30) {
|
||
const _this = this;
|
||
setTimeout(() => {
|
||
_this.is_socket_error = true;
|
||
_this.socket_error_content = `第${_this.reconnect_count + 1}次连接失败`;
|
||
console.log(`聊天第${_this.reconnect_count + 1}次连接失败`);
|
||
setTimeout(() => {
|
||
// 增加重连计数
|
||
_this.reconnect_count++;
|
||
_this.socket_connect();
|
||
console.log(`聊天第${_this.reconnect_count + 1}次连接`);
|
||
}, 1000); // 逐步增加重连间隔,最大10秒
|
||
}, 1000); // 逐步增加重连间隔,最大10秒
|
||
} else {
|
||
this.is_socket_error = false;
|
||
this.reconnect_count = 0;
|
||
this.socket_error_content = '连接失败点击重试';
|
||
}
|
||
});
|
||
this.task.onError((res) => {
|
||
this.socket_close();
|
||
this.is_socket_error = false;
|
||
this.reconnect_count = 0;
|
||
this.socket_error_content = `连接失败点击重试`;
|
||
if (is_manual) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: res.result,
|
||
showCancel: false,
|
||
});
|
||
}
|
||
});
|
||
},
|
||
socket_close() {
|
||
if (this.task != null) {
|
||
this.task.close();
|
||
this.task = null;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* WebSocket消息回调处理
|
||
* @param {Object} e - WebSocket消息事件对象
|
||
*/
|
||
socket_message_back_handle(e) {
|
||
let res = JSON.parse(e.data);
|
||
if(res.code !== 0) {
|
||
app.globalData.showToast(res.msg);
|
||
return false;
|
||
}
|
||
let data = res.data;
|
||
switch(data.type) {
|
||
// 初始化
|
||
case 'init' :
|
||
this.socket_id = data.data.fd;
|
||
this.ping_interval = parseInt(data.data.ping_interval || 30);
|
||
// 初始化消息
|
||
this.socket_send('init');
|
||
break;
|
||
|
||
// 初始化成功
|
||
case 'init-success' :
|
||
this.live_user_id = data.data.live_user_id;
|
||
// 启动心跳
|
||
this.socket_ping_handle();
|
||
// 启动直播间数据定时任务
|
||
this.socket_room_info_handle();
|
||
break;
|
||
|
||
// 初始化失败
|
||
case 'init-fail' :
|
||
console.log('connect fail');
|
||
break;
|
||
|
||
// 加入直播间提示
|
||
case 'join' :
|
||
// 如果最后前一条是进入直播间的提示,则更新用户昵称
|
||
if (this.bulletins.length > 0 && this.bulletins[this.bulletins.length - 1].type == 'go') {
|
||
this.bulletins[this.bulletins.length - 1].user_name = data.content;
|
||
} else {
|
||
this.bulletins.push({
|
||
id: Math.random(),
|
||
type: 'go',
|
||
user_avatar: '',
|
||
user_name: data.content,
|
||
text: '',
|
||
});
|
||
}
|
||
// 加入直播间提示之后,需要等待300毫秒,确保消息添加到数组中
|
||
setTimeout(() => {
|
||
// 添加内容之后,如果当前是在最后的需要滚动到最后
|
||
if (this.is_scroll_to_lower) {
|
||
this.scroll_to_lower();
|
||
}
|
||
}, 300);
|
||
break;
|
||
// 消息
|
||
case 'message':
|
||
// 如果最后前一条是进入直播间的提示,则删除
|
||
if (this.bulletins.length > 0 && this.bulletins[this.bulletins.length - 1].type == 'go') {
|
||
this.bulletins.splice(this.bulletins.length - 1, 1);
|
||
}
|
||
this.bulletins.push({
|
||
id: Math.random(),
|
||
type: 'user',
|
||
user_avatar: data.data.user.avatar,
|
||
user_name: data.data.user.nickname,
|
||
text: data.content,
|
||
});
|
||
// 加入直播间提示之后,需要等待300毫秒,确保消息添加到数组中
|
||
setTimeout(() => {
|
||
// 添加内容之后,如果当前是在最后的需要滚动到最后
|
||
if (this.is_scroll_to_lower) {
|
||
this.scroll_to_lower();
|
||
} else {
|
||
this.message_num++;
|
||
}
|
||
}, 300);
|
||
break;
|
||
case 'live-room-info': // 获取直播间数据
|
||
// this.$emit('liveStatus', data.content);
|
||
this.live_init(data.data);
|
||
break;
|
||
}
|
||
},
|
||
// 初始化直播间数据
|
||
live_init(data) {
|
||
// 更新讲解商品信息
|
||
const goods = data.explain_goods;
|
||
// 讲解商品信息更新,讲解商品不为空,并且讲解商品id不一致,需要更新讲解商品信息
|
||
if ((!isEmpty(goods) && !isEmpty(this.explain_goods) && this.explain_goods.id !== goods.id) || isEmpty(this.explain_goods)) {
|
||
this.explain_goods = data.explain_goods;
|
||
this.is_show_explain_goods = true;
|
||
this.explain_goods_close('auto');
|
||
}
|
||
// 更新在线用户头像
|
||
this.online_user = data.online_user;
|
||
// 更新直播间头像
|
||
this.live_avatar = data.room_info.cover;
|
||
// 更新直播间数据
|
||
const new_value = data.room_info;
|
||
this.live_data = new_value;
|
||
// 直播间点赞数
|
||
this.like_count = new_value.like_count;
|
||
// 直播间状态更新
|
||
this.$emit('liveStatus', new_value.status);
|
||
},
|
||
|
||
/**
|
||
* 手动关闭讲解商品讲解商品关闭处理函数
|
||
* @param {string} type - 关闭类型,'auto' 表示自动关闭,'manual' 表示立即关闭
|
||
*/
|
||
explain_goods_close(type) {
|
||
if (this.explain_goods_timer != null) {
|
||
clearTimeout(this.explain_goods_timer);
|
||
this.explain_goods_timer = null;
|
||
}
|
||
// 如何类型是自动的话,就是自动关闭,否则就立即关闭
|
||
if (type == 'auto') {
|
||
// 定时关闭讲解商品
|
||
this.explain_goods_timer = setTimeout(() => {
|
||
this.is_show_explain_goods = false;
|
||
}, this.live_room_goods_explain_auto_close_time * 1000);
|
||
} else {
|
||
this.is_show_explain_goods = false;
|
||
}
|
||
},
|
||
/**
|
||
* 讲解商品点击跳转到具体的商品详情页面
|
||
*/
|
||
explain_goods_tap() {
|
||
if (!isEmpty(this.explain_goods.goods_url)) {
|
||
const url = this.explain_goods.goods_url + '&live_room_id=1';
|
||
app.globalData.url_open(url);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* WebSocket心跳处理
|
||
*/
|
||
socket_ping_handle() {
|
||
// 清除定时任务
|
||
this.clear_interval_task();
|
||
|
||
// 启动定时任务
|
||
this.ping_timer = setInterval(() => {
|
||
this.socket_send('ping', app.globalData.get_timestamp());
|
||
}, this.ping_interval * 1000);
|
||
},
|
||
/**
|
||
* 清除定时任务
|
||
*/
|
||
clear_interval_task() {
|
||
if (this.ping_timer != null) {
|
||
clearInterval(this.ping_timer);
|
||
this.ping_timer = null;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* WebSocket获取直播间数据
|
||
*/
|
||
socket_room_info_handle() {
|
||
// 清除已有的定时任务
|
||
if (this.pull_live_room_info_timer != null) {
|
||
clearInterval(this.pull_live_room_info_timer);
|
||
this.pull_live_room_info_timer = null;
|
||
}
|
||
console.log('立即获取直播间数据');
|
||
// 立即发送获取直播间数据消息
|
||
this.socket_send('live-room-info');
|
||
|
||
// 每30秒发送一次获取直播间数据消息
|
||
this.pull_live_room_info_timer = setInterval(() => {
|
||
console.log('定时获取直播间数据');
|
||
this.socket_send('live-room-info');
|
||
}, this.live_room_info_timing_interval_time * 1000);
|
||
},
|
||
|
||
/**
|
||
* 发送WebSocket消息
|
||
* @param {String} type - 消息类型(init初始化, ping心跳, message消息)
|
||
* @param {String} content - 消息内容
|
||
*/
|
||
socket_send(type = 'message', content = '') {
|
||
if(this.task === null) {
|
||
app.globalData.showToast('socket连接失败!');
|
||
return false;
|
||
}
|
||
// 发送消息
|
||
const user = app.globalData.get_user_cache_info();
|
||
let uuid = app.globalData.request_uuid();
|
||
let token = user == null ? '' : user.token || '';
|
||
this.task.send({data: JSON.stringify({
|
||
application_client_type: this.application_client_type,
|
||
application_client_brand: this.application_client_brand,
|
||
system_type: app.globalData.data.system_type,
|
||
uuid: uuid,
|
||
token: token,
|
||
live_room_id: this.live_data.id, // 直播间id
|
||
live_user_id: this.live_user_id, // 直播用户id
|
||
fd: this.socket_id,
|
||
type: type,
|
||
content: content
|
||
})});
|
||
},
|
||
//#endregion
|
||
|
||
//#region 监听键盘弹出事件
|
||
/**
|
||
* 添加评论
|
||
*/
|
||
add_comment() {
|
||
//#ifndef H5
|
||
this.is_add_comment = true;
|
||
//#endif
|
||
},
|
||
/**
|
||
* 键盘高度变化监听处理
|
||
* @param {Object} res - 键盘高度变化事件对象
|
||
*/
|
||
listener(res) {
|
||
// 减1是为了兼容,避免跟键盘之间会不连贯
|
||
if (res.height > 0) {
|
||
this.listener_height = res.height - 1;
|
||
} else {
|
||
this.listener_height = 0;
|
||
}
|
||
},
|
||
/**
|
||
* 绑定键盘高度变化监听事件
|
||
*/
|
||
bind_keyboard_listener() {
|
||
uni.onKeyboardHeightChange(this.listener);
|
||
},
|
||
/**
|
||
* 解绑键盘高度变化监听事件
|
||
*/
|
||
unbind_keyboard_listener() {
|
||
uni.offKeyboardHeightChange(this.listener);
|
||
},
|
||
|
||
/**
|
||
* 评论输入确认事件处理
|
||
* @param {Event} e - 输入确认事件对象
|
||
*/
|
||
comment_input_confirm(e) {
|
||
const value = e.detail.value;
|
||
if (value != '') {
|
||
this.socket_send('message', e.detail.value);
|
||
}
|
||
this.comment_value = '';
|
||
},
|
||
//#endregion
|
||
|
||
//#region 商品弹出框
|
||
/**
|
||
* 添加商品
|
||
* 打开商品弹出框
|
||
*/
|
||
add_goods() {
|
||
this.$refs.popupGoodsRef.open();
|
||
},
|
||
//#endregion
|
||
|
||
/**
|
||
* 分享事件处理
|
||
*/
|
||
share_event() {
|
||
// 分享菜单处理
|
||
app.globalData.page_share_handle();
|
||
const share_info = {
|
||
title: this.live_data.title,
|
||
desc: this.live_data.describe,
|
||
path: "/pages/plugins/live/pull/pull",
|
||
query: "id=" + this.live_data.id,
|
||
img: this.live_data.icon || "",
|
||
};
|
||
// 调取分享弹窗
|
||
if ((this.$refs.share || null) != null) {
|
||
this.$refs.share.init({
|
||
share_info: share_info
|
||
});
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 点赞点击事件处理
|
||
* @param {Event} e - 点击事件对象
|
||
*/
|
||
like_click(e) {
|
||
this.$refs.likeButton.handleClick(e);
|
||
},
|
||
|
||
/**
|
||
* 点赞按钮点击事件处理
|
||
* @param {Event} e - 点击事件对象
|
||
*/
|
||
like_button_click(e) {
|
||
// 临时存储点赞数量
|
||
this.casual_like_count++;
|
||
// 如果有点击,清除历史定时任务
|
||
if (this.like_timer != null) {
|
||
clearTimeout(this.like_timer);
|
||
}
|
||
// 两秒没有人点赞,则显示临时点赞数量加上原有数量
|
||
this.like_timer = setTimeout(() => {
|
||
// 显示临时点赞数量加上原有数量
|
||
this.like_count = this.like_count + this.casual_like_count;
|
||
this.socket_send('live-room-like', this.casual_like_count);
|
||
// 完成之后重置临时点赞数量
|
||
this.casual_like_count = 0;
|
||
}, 2000);
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.top-header {
|
||
padding: 10rpx 30rpx 10rpx 20rpx;
|
||
border-radius: 100rpx;
|
||
z-index: 3;
|
||
background-color: rgba(40,40,40,0.45);
|
||
}
|
||
.people-number {
|
||
padding: 6rpx 20rpx;
|
||
background-color: rgba(40,40,40,0.45);
|
||
border-radius: 200rpx;
|
||
}
|
||
|
||
.avatar {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border-radius: 30rpx;
|
||
}
|
||
|
||
.nickname {
|
||
max-width: 200rpx;
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.level {
|
||
font-size: 24rpx;
|
||
color: #fff;
|
||
}
|
||
|
||
.viewer-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: -20rpx; /* 重叠部分的宽度 */
|
||
}
|
||
|
||
.viewer-avatar {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border-radius: 30rpx;
|
||
border: 3rpx solid #ffffff; /* 添加白色边框使其更清晰 */
|
||
}
|
||
.viewer-back {
|
||
// width: 70rpx;
|
||
// height: 70rpx;
|
||
border-radius: 30rpx;
|
||
}
|
||
|
||
.follow-btn {
|
||
margin-left: 20rpx;
|
||
padding: 8rpx 20rpx;
|
||
background-color: #ff4d7e;
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
.rank-tags {
|
||
margin-left: 20rpx;
|
||
}
|
||
|
||
.rank-tag {
|
||
font-size: 24rpx;
|
||
color: #fff;
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
.bulletin-area {
|
||
max-height: 600rpx;
|
||
height: auto;
|
||
}
|
||
|
||
.cr-blue {
|
||
color: #9DE3F7
|
||
}
|
||
|
||
.bulletin-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
padding: 6rpx 16rpx;
|
||
background-color: rgba(40,40,40,0.45);
|
||
border-radius: 25rpx;
|
||
}
|
||
|
||
.bulletin-item-avatar {
|
||
width: 30rpx;
|
||
height: 30rpx;
|
||
border-radius: 15rpx;
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
.content-container {
|
||
display: inline-block;
|
||
line-height: 30rpx;
|
||
vertical-align: top;
|
||
width: calc(100% - 40rpx); /* 减去头像宽度和间距 */
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 28rpx;
|
||
margin-right: 10rpx;
|
||
display: inline;
|
||
}
|
||
|
||
.bulletin-text {
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
line-height: 30rpx;
|
||
white-space: pre-line;
|
||
word-break: break-all;
|
||
overflow-wrap: break-word;
|
||
display: inline;
|
||
}
|
||
|
||
.input-field {
|
||
flex: 1;
|
||
padding: 10rpx;
|
||
border: 1rpx solid #ddd;
|
||
border-radius: 20rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
margin-left: 20rpx;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
.bottom-actions-input {
|
||
padding: 16rpx 22rpx;
|
||
border-radius: 50rpx;
|
||
box-sizing: border-box;
|
||
color: #fff;
|
||
background: rgba(40,40,40,0.45);
|
||
}
|
||
.bottom-actions-input input {
|
||
color: #fff;
|
||
}
|
||
.bottom-actions-input input::placeholder {
|
||
color: #fff;
|
||
}
|
||
.bottom-actions-button {
|
||
border-radius: 50rpx;
|
||
background: rgba(40,40,40,0.45);
|
||
}
|
||
.bottom-actions-icon {
|
||
background: rgba(40,40,40,0.45);
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 40rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-left: 20rpx;
|
||
}
|
||
.keyboard-input {
|
||
position: fixed;
|
||
left: 0;
|
||
z-index: 99;
|
||
background: #fff;
|
||
padding: 20rpx 16rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.keyboard-input-input {
|
||
padding: 16rpx 22rpx;
|
||
border: 2rpx solid #ddd;
|
||
border-radius: 50rpx;
|
||
}
|
||
|
||
.bulletin-tips {
|
||
position: absolute;
|
||
left: 0;
|
||
bottom: 8rpx;
|
||
z-index: 3;
|
||
}
|
||
|
||
.bulletin-tips-content {
|
||
background: #fff;
|
||
border-radius: 200rpx;
|
||
padding: 6rpx 20rpx;
|
||
}
|
||
|
||
.user-comes {
|
||
min-width: 100rpx;
|
||
padding: 6rpx 10rpx 6rpx 20rpx;
|
||
background-color: rgba(40,40,40,0.45);
|
||
border-radius: 200rpx;
|
||
}
|
||
|
||
.countdown-display {
|
||
width: 100px;
|
||
height: 100px;
|
||
border-radius: 50px;
|
||
z-index: 999;
|
||
background: rgba(40,40,40,0.45);
|
||
.countdown-text {
|
||
color: #fff;
|
||
font-size: 50px;
|
||
text-align: center;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
.countdown-animation {
|
||
animation: zoomOut 1s ease-in-out forwards;
|
||
}
|
||
@keyframes zoomOut {
|
||
0% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
100% {
|
||
transform: scale(2);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
/* 讲解商品信息 */
|
||
.explain-goods {
|
||
width: 200rpx;
|
||
background: #fff;
|
||
padding: 4rpx;
|
||
box-sizing: border-box;
|
||
position: absolute;
|
||
right: 0;
|
||
bottom: 20rpx;
|
||
border-radius: 20rpx;
|
||
overflow: hidden;
|
||
.explain-subscript {
|
||
position: absolute;
|
||
left: 5rpx;
|
||
top: 5rpx;
|
||
right: 5rpx;
|
||
.explain-progress {
|
||
padding: 0rpx 8rpx;
|
||
background: rgba(40,40,40,0.45);
|
||
border-radius: 10rpx;
|
||
}
|
||
.explain-close {
|
||
margin-right: 5rpx;
|
||
padding: 6rpx;
|
||
background: rgba(40,40,40,0.45);
|
||
border-radius: 50rpx;
|
||
}
|
||
}
|
||
}
|
||
/* #ifdef MP-WEIXIN | APP-PLUS */
|
||
.bulletin-area {
|
||
::v-deep ::-webkit-scrollbar
|
||
{
|
||
width: 0rpx!important;
|
||
height: 0rpx!important;
|
||
background-color: transparent;
|
||
}
|
||
}
|
||
/* #endif */
|
||
</style>
|