|
|
@@ -0,0 +1,1521 @@
|
|
|
+<template>
|
|
|
+ <div class="console">
|
|
|
+
|
|
|
+ <div class="left-panel">
|
|
|
+ <h2>学员列表</h2>
|
|
|
+ <div class="search">
|
|
|
+ <input type="text" placeholder="搜索用户昵称" v-model="searchKeyword">
|
|
|
+ <button @click="searchUsers()">搜索</button>
|
|
|
+ </div>
|
|
|
+ <el-tabs class="live-console-tab-right" v-model="tabLeft.activeName" @tab-click="handleUserClick" :stretch="true">
|
|
|
+ <el-tab-pane :label="alLabel" name="al">
|
|
|
+ <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_al" style="height: 800px; width: 100%;">
|
|
|
+ <el-row class='scrollbar-demo-item' v-for="u in alDisplayList" :key="u.userId">
|
|
|
+ <el-col :span="20">
|
|
|
+ <el-row type="flex" align="middle">
|
|
|
+ <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
|
|
|
+ <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
|
|
|
+ <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4" >
|
|
|
+ <el-popover
|
|
|
+ width="100"
|
|
|
+ trigger="click">
|
|
|
+ <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
|
|
|
+ <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
|
|
|
+ <i class="el-icon-more" slot="reference"></i>
|
|
|
+ </el-popover>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-scrollbar>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane :label="onlineLabel" name="online">
|
|
|
+ <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_online" style="height: 800px; width: 100%;">
|
|
|
+ <el-row class='scrollbar-demo-item' v-for="u in onlineDisplayList" :key="u.userId">
|
|
|
+ <el-col :span="20">
|
|
|
+ <el-row type="flex" align="middle">
|
|
|
+ <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
|
|
|
+ <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
|
|
|
+ <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4" >
|
|
|
+ <el-popover
|
|
|
+ width="100"
|
|
|
+ trigger="click">
|
|
|
+ <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
|
|
|
+ <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
|
|
|
+ <i class="el-icon-more" slot="reference"></i>
|
|
|
+ </el-popover>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-scrollbar>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane :label="offlineLabel" name="offline">
|
|
|
+ <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_offline" style="height: 800px; width: 100%;">
|
|
|
+ <el-row class='scrollbar-demo-item' v-for="u in offlineDisplayList" :key="u.userId">
|
|
|
+ <el-col :span="20">
|
|
|
+ <el-row type="flex" align="middle">
|
|
|
+ <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
|
|
|
+ <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
|
|
|
+ <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4" >
|
|
|
+ <el-popover
|
|
|
+ width="100"
|
|
|
+ trigger="click">
|
|
|
+ <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
|
|
|
+ <i class="el-icon-more" slot="reference"></i>
|
|
|
+ <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
|
|
|
+ </el-popover>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-scrollbar>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane :label="silencedUserLabel" name="silenced">
|
|
|
+ <el-scrollbar class="custom-scrollbar" ref="manageLeftRef_silenced" style="height: 800px; width: 100%;">
|
|
|
+ <el-row class='scrollbar-demo-item' v-for="u in silencedDisplayList" :key="u.userId">
|
|
|
+ <el-col :span="20">
|
|
|
+ <el-row type="flex" align="middle">
|
|
|
+ <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
|
|
|
+ <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
|
|
|
+ <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4" >
|
|
|
+ <el-popover
|
|
|
+ width="100"
|
|
|
+ trigger="click">
|
|
|
+ <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
|
|
|
+ <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
|
|
|
+ <i class="el-icon-more" slot="reference"></i>
|
|
|
+ </el-popover>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-scrollbar>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="middle-panel">
|
|
|
+ <h2>消息管理</h2>
|
|
|
+
|
|
|
+
|
|
|
+ <div class="discussion-messages">
|
|
|
+ <h3>讨论区消息</h3>
|
|
|
+ <div class="message-settings">
|
|
|
+ <label>
|
|
|
+ <input type="checkbox" v-model="globalVisible" @change="globalVisibleChange">
|
|
|
+ 全局用户自见
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ <el-scrollbar class="custom-scrollbar" style="height: 500px; width: 100%;" ref="manageRightRef">
|
|
|
+ <el-row v-for="m in msgList" >
|
|
|
+ <el-row v-if="m.userId !== userId" style="margin-top: 5px" type="flex" align="top" >
|
|
|
+ <el-col :span="3" style="margin-left: 10px"><el-avatar :src="m.avatar"/></el-col>
|
|
|
+ <el-col :span="15">
|
|
|
+ <el-row style="margin-left: 10px">
|
|
|
+ <el-col><div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div></el-col>
|
|
|
+ <el-col :span="24" style="max-width: 200px;">
|
|
|
+ <div style="white-space: normal; word-wrap: break-word;background-color: #f0f2f5; padding: 8px; border-radius: 5px;font-size: 14px;width: 100%;">
|
|
|
+ {{ m.msg }}
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col>
|
|
|
+ <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="changeUserState(m)">{{ m.msgStatus === 1 ? '解禁' : '禁言' }}</a>
|
|
|
+ <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="blockUser(m)">拉黑</a>
|
|
|
+ <a v-if="m.singleVisible === 1" style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="singleVisible(m)">解除用户自见</a>
|
|
|
+ <a v-else style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="singleVisible(m)">用户自见</a>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row v-if="m.userId === userId" style="padding: 8px 0" type="flex" align="top" justify="end">
|
|
|
+ <div style="display: flex;justify-content: flex-end">
|
|
|
+ <div style="display: flex;justify-content: flex-end;flex-direction: column;max-width: 200px;align-items: flex-end">
|
|
|
+ <div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div>
|
|
|
+ <div style="white-space: normal; word-wrap: break-word;width: 100%; background-color: #e6f7ff; padding: 8px; border-radius: 5px;font-size: 14px;">{{ m.msg }}</div>
|
|
|
+ </div>
|
|
|
+ <el-avatar :src="m.avatar" style="margin-left: 10px; margin-right: 10px;"/>
|
|
|
+ </div>
|
|
|
+ </el-row>
|
|
|
+ </el-row>
|
|
|
+ <!-- 底部留白 -->
|
|
|
+ <div style="height: 20px;"></div>
|
|
|
+ </el-scrollbar>
|
|
|
+<!-- <div class="message-list">-->
|
|
|
+<!-- <div class="message-item" v-for="msg in msgList" :key="msg.id">-->
|
|
|
+<!-- <div class="message-avatar">-->
|
|
|
+<!-- <img :src="msg.avatar" alt="用户头像">-->
|
|
|
+<!-- </div>-->
|
|
|
+<!-- <div class="message-content">-->
|
|
|
+<!-- <div class="message-user">{{ msg.user }}</div>-->
|
|
|
+<!-- <div class="message-text">{{ msg.text }}</div>-->
|
|
|
+<!-- </div>-->
|
|
|
+<!-- <div class="message-actions">-->
|
|
|
+<!--<!– <button @click="toggleVisible(msg)">–>-->
|
|
|
+<!--<!– {{ msg.isVisible ? '仅用户自见' : '全局可见' }}–>-->
|
|
|
+<!--<!– </button>–>-->
|
|
|
+<!-- <button @click="deleteMessage(msg)">删除</button>-->
|
|
|
+<!-- </div>-->
|
|
|
+<!-- </div>-->
|
|
|
+<!-- </div>-->
|
|
|
+ </div>
|
|
|
+ <div class="system-messages">
|
|
|
+ <h3>系统消息</h3>
|
|
|
+ <textarea placeholder="输入系统消息" v-model="newMsg"></textarea>
|
|
|
+ <div class="message-actions">
|
|
|
+ <button @click="sendMessage">发送消息</button>
|
|
|
+ <button @click="sendPopMessage">弹窗消息</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="right-panel">
|
|
|
+ <h2>运营工具</h2>
|
|
|
+ <div class="live-player">
|
|
|
+ <h3>直播观看</h3>
|
|
|
+ <LivePlayer ref="livePlayer" :videoParam="videoParam" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="automation">
|
|
|
+ <h3>运营自动化</h3>
|
|
|
+ <div class="timeline">
|
|
|
+ <h4>时间轴设置</h4>
|
|
|
+ <div class="timeline-items">
|
|
|
+ <div class="timeline-item" v-for="item in timelineItems.slice(0,2)" :key="item.time">
|
|
|
+ <div class="time">{{ formatDate(item.absValue) }}</div>
|
|
|
+ <div class="action">{{ item.taskName }}</div>
|
|
|
+ <button class="delete" @click="removeTimelineItem(item)">删除</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+<!-- <button class="add" @click="addTimelineItem">添加时间节点</button>-->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="watermark">
|
|
|
+ <h3>直播氛围自动化</h3>
|
|
|
+ <div class="watermark-settings">
|
|
|
+ <textarea :disabled="autoWatermark" v-model="watermarkTemplate" placeholder="水军弹幕内容模板,每行一条"></textarea>
|
|
|
+ <div class="watermark-options">
|
|
|
+ <label>
|
|
|
+ 发送间隔:
|
|
|
+ <input type="number" :disabled="autoWatermark" v-model.number="interval" min="1">
|
|
|
+ 秒
|
|
|
+ </label>
|
|
|
+ <label>
|
|
|
+ <input type="checkbox" v-model="autoWatermark" @change="changeAutoWatermark">
|
|
|
+ 启用水军自动化
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import LivePlayer from './LivePlayer.vue';
|
|
|
+import {blockUser, changeUserStatus, getLiveUserTotals, dashBoardWatchUserList} from '@/api/live/liveWatchUser'
|
|
|
+import { listLiveSingleMsg } from '@/api/live/liveMsg'
|
|
|
+import { getLive } from '@/api/live/live'
|
|
|
+import { consoleList } from '@/api/live/task'
|
|
|
+import ScreenScale from './ScreenScale.vue'; // 路径根据实际位置调整
|
|
|
+
|
|
|
+
|
|
|
+export default {
|
|
|
+ components: {
|
|
|
+ LivePlayer,ScreenScale
|
|
|
+ },
|
|
|
+ props: {
|
|
|
+ liveId: {
|
|
|
+ type: String,
|
|
|
+ default: null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ watermarkIndex: 0,
|
|
|
+ watermarkList:[],
|
|
|
+ watermarkTemplate: '',
|
|
|
+ liveInfo: {},
|
|
|
+ videoParam:{
|
|
|
+ startTime:'',
|
|
|
+ livingUrl: '',
|
|
|
+ liveType: 1,
|
|
|
+ videoUrl: '',
|
|
|
+ },
|
|
|
+ msgList:[],
|
|
|
+ currentTab: 'al',
|
|
|
+ searchKeyword: '',
|
|
|
+ globalVisible: false,
|
|
|
+ interval: 10,
|
|
|
+ autoWatermark: false,
|
|
|
+ streamUrl: 'rtmp://your-live-stream-url',
|
|
|
+ users: [
|
|
|
+ { id: 1, name: '用户1', avatar: 'https://via.placeholder.com/40', status: 'online', statusText: '在线', silenced: false, msgStatus: false },
|
|
|
+ { id: 2, name: '用户2', avatar: 'https://via.placeholder.com/40', status: 'online', statusText: '在线', silenced: false, msgStatus: true },
|
|
|
+ { id: 3, name: '用户3', avatar: 'https://via.placeholder.com/40', status: 'offline', statusText: '离线', silenced: true, msgStatus: false },
|
|
|
+ { id: 4, name: '用户4', avatar: 'https://via.placeholder.com/40', status: 'online', statusText: '在线', silenced: false, msgStatus: false },
|
|
|
+ { id: 5, name: '用户5', avatar: 'https://via.placeholder.com/40', status: 'offline', statusText: '离线', silenced: false, msgStatus: false }
|
|
|
+ ],
|
|
|
+ messages: [
|
|
|
+ { id: 1, user: '用户1', avatar: 'https://via.placeholder.com/30', text: '这个产品怎么样?', isVisible: true },
|
|
|
+ { id: 2, user: '用户2', avatar: 'https://via.placeholder.com/30', text: '看起来不错', isVisible: true },
|
|
|
+ { id: 3, user: '用户3', avatar: 'https://via.placeholder.com/30', text: '有优惠吗?', isVisible: true }
|
|
|
+ ],
|
|
|
+ timelineItems: [
|
|
|
+ { "searchValue": null,
|
|
|
+ "createBy": null,
|
|
|
+ "createTime": "2025-09-23 10:36:17",
|
|
|
+ "updateBy": null,
|
|
|
+ "updateTime": "2025-10-17 09:18:00",
|
|
|
+ "remark": null,
|
|
|
+ "params": {},
|
|
|
+ "id": 6573,
|
|
|
+ "liveId": 128,
|
|
|
+ "taskName": "2",
|
|
|
+ "taskType": 1,
|
|
|
+ "triggerType": 2,
|
|
|
+ "triggerValue": "2025-01-01T00:02:00.000+0800",
|
|
|
+ "absValue": "2025-11-23T10:43:00.000+0800",
|
|
|
+ "status": 1,
|
|
|
+ "finishStatus": 0 },
|
|
|
+ ],
|
|
|
+ userTotal: {
|
|
|
+ online: 0, // 在线总人数
|
|
|
+ offline: 0, // 离线总人数
|
|
|
+ silenced: 0, // 禁言总人数
|
|
|
+ al: 0
|
|
|
+ },
|
|
|
+ tabLeft: {
|
|
|
+ activeName: "al",
|
|
|
+ },
|
|
|
+ taskParams:{
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ prevPage: 0,
|
|
|
+ totalLoaded: 0,
|
|
|
+ total: 0,
|
|
|
+ hasMore: true,
|
|
|
+ hasPrev: false
|
|
|
+ },
|
|
|
+ loadMsgMaxPage: 2,
|
|
|
+ liveWsUrl: process.env.VUE_APP_LIVE_WS_URL + '/app/webSocket',
|
|
|
+ alDisplayList: [],
|
|
|
+ onlineDisplayList: [], // 在线用户显示列表
|
|
|
+ offlineDisplayList: [], // 离线用户显示列表
|
|
|
+ silencedDisplayList: [], // 禁言用户显示列表
|
|
|
+ // 各Tab的分页参数
|
|
|
+ pageParams: {
|
|
|
+ al: {
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ prevPage: 0,
|
|
|
+ totalLoaded: 0,
|
|
|
+ total: 0,
|
|
|
+ hasMore: true,
|
|
|
+ hasPrev: false
|
|
|
+ },
|
|
|
+ online: {
|
|
|
+ currentPage: 1, // 当前页(下一页加载用)
|
|
|
+ pageSize: 20, // 当前页(下一页加载用)
|
|
|
+ prevPage: 0, // 上一页页码(上一页加载用)
|
|
|
+ totalLoaded: 0, // 已加载总条数
|
|
|
+ total: 0, // 总数据量
|
|
|
+ hasMore: true, // 是否有下一页
|
|
|
+ hasPrev: false // 是否有上一页
|
|
|
+ },
|
|
|
+ offline: {
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ prevPage: 0,
|
|
|
+ totalLoaded: 0,
|
|
|
+ total: 0,
|
|
|
+ hasMore: true,
|
|
|
+ hasPrev: false
|
|
|
+ },
|
|
|
+ silenced: {
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ prevPage: 0,
|
|
|
+ totalLoaded: 0,
|
|
|
+ total: 0,
|
|
|
+ hasMore: true,
|
|
|
+ hasPrev: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scrLoading: {
|
|
|
+ al: { next: false, prev: false },
|
|
|
+ online: { next: false, prev: false },
|
|
|
+ offline: { next: false, prev: false },
|
|
|
+ silenced: { next: false, prev: false }
|
|
|
+ },
|
|
|
+ newMsg:'',
|
|
|
+ autoMsgTimer: null,
|
|
|
+ checkInterval: 2000, // 检查间隔(1分钟,可根据需求调整)
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ userId() {
|
|
|
+ return this.$store.state.user.user.userId
|
|
|
+ },
|
|
|
+ companyId() {
|
|
|
+ return this.$store.state.user.user.companyId
|
|
|
+ },
|
|
|
+ onlineLabel() {
|
|
|
+ return `在线(${this.userTotal.online})`;
|
|
|
+ },
|
|
|
+ alLabel() {
|
|
|
+ return `全部(${this.userTotal.al})`;
|
|
|
+ },
|
|
|
+ offlineLabel() {
|
|
|
+ return `离线(${this.userTotal.offline})`;
|
|
|
+ },
|
|
|
+ silencedUserLabel() {
|
|
|
+ return `禁言(${this.userTotal.silenced})`;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ if(!this.liveId) return
|
|
|
+ this.getList()
|
|
|
+ this.handleUserClick({name:'al'})
|
|
|
+ this.connectWebSocket()
|
|
|
+ this.getLive()
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.restoreChatScrollPosition();
|
|
|
+
|
|
|
+ if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
|
|
|
+ this.$refs.manageRightRef.wrap.addEventListener('scroll', this.saveChatScrollPosition);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ this.initScrollListeners();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ singleVisible(m){
|
|
|
+ // 过滤当前所有消息 找到userId的相同的消息 更改他们的自可见状态
|
|
|
+ m.singleVisible= m.singleVisible === 1 ? 0 : 1
|
|
|
+ this.msgList.forEach(m1 => {m1.singleVisible = m1.userId === m.userId ? m.singleVisible : !m.singleVisible})
|
|
|
+ // 消息自可见
|
|
|
+ let msg = {
|
|
|
+ liveId: this.liveId,
|
|
|
+ userId: m.userId,
|
|
|
+ userType: 0,
|
|
|
+ cmd: 'singleVisible',
|
|
|
+ status:m.singleVisible
|
|
|
+ }
|
|
|
+ this.socket.send(JSON.stringify(msg))
|
|
|
+ },
|
|
|
+ globalVisibleChange( val){
|
|
|
+ // 消息自可见
|
|
|
+ let msg = {
|
|
|
+ liveId: this.liveId,
|
|
|
+ userId: '9999',
|
|
|
+ userType: 0,
|
|
|
+ cmd: 'globalVisible',
|
|
|
+ status:this.globalVisible ? 1 :0
|
|
|
+ }
|
|
|
+ this.socket.send(JSON.stringify(msg))
|
|
|
+ },
|
|
|
+ changeAutoWatermark( val){
|
|
|
+ this.watermarkList = this.watermarkTemplate
|
|
|
+ .split('\n')
|
|
|
+ .map(line => line.trim())
|
|
|
+ .filter(line => line !== '');
|
|
|
+ if(this.watermarkTemplate.length < 1 || this.watermarkList.length === 0) {
|
|
|
+ this.$message.warning('请先填写水印模板')
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.autoWatermark = false
|
|
|
+ });
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.disabled = this.autoWatermark
|
|
|
+ if(this.autoWatermark){
|
|
|
+ this.autoMsgTimer = setInterval(() => {
|
|
|
+ this.sendNormalMessage()
|
|
|
+ }, 1000 * this.interval)
|
|
|
+ } else {
|
|
|
+ clearInterval(this.autoMsgTimer)
|
|
|
+ this.watermarkIndex = 0;
|
|
|
+ this.watermarkList = [];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ sendNormalMessage() {
|
|
|
+ if(this.watermarkIndex >= this.watermarkList.length){
|
|
|
+ clearInterval(this.autoMsgTimer)
|
|
|
+ this.watermarkIndex = 0;
|
|
|
+ this.watermarkList = [];
|
|
|
+ this.$message.success('自动发送消息已结束');
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.autoWatermark = false
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const curMsg = this.watermarkList[this.watermarkIndex]
|
|
|
+ // 发送前简单校验
|
|
|
+ if (curMsg.trim() === '') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let msg = {
|
|
|
+ msg: curMsg,
|
|
|
+ liveId: this.liveId,
|
|
|
+ userId: '9999',
|
|
|
+ userType: 0,
|
|
|
+ cmd: 'sendNormalMsg',
|
|
|
+ avatar: this.$store.state.user.user.avatar,
|
|
|
+ nickName: this.$store.state.user.user.nickName,
|
|
|
+ }
|
|
|
+ this.socket.send(JSON.stringify(msg))
|
|
|
+ this.watermarkIndex += 1
|
|
|
+ },
|
|
|
+ // 弹窗消息
|
|
|
+ sendPopMessage() {
|
|
|
+ // 发送前简单校验
|
|
|
+ if (this.newMsg.trim() === '') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let msg = {
|
|
|
+ msg: this.newMsg,
|
|
|
+ liveId: this.liveId,
|
|
|
+ userId: this.userId,
|
|
|
+ userType: 1,
|
|
|
+ cmd: 'sendPopMsg',
|
|
|
+ avatar: this.$store.state.user.user.avatar,
|
|
|
+ nickName: this.$store.state.user.user.nickName,
|
|
|
+ }
|
|
|
+
|
|
|
+ this.socket.send(JSON.stringify(msg))
|
|
|
+
|
|
|
+ this.newMsg = '';
|
|
|
+ },
|
|
|
+ sendMessage() {
|
|
|
+ // 发送前简单校验
|
|
|
+ if (this.newMsg.trim() === '') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let msg = {
|
|
|
+ msg: this.newMsg,
|
|
|
+ liveId: this.liveId,
|
|
|
+ userId: this.userId,
|
|
|
+ userType: 1,
|
|
|
+ cmd: 'sendMsg',
|
|
|
+ avatar: this.$store.state.user.user.avatar,
|
|
|
+ nickName: this.$store.state.user.user.nickName,
|
|
|
+ }
|
|
|
+
|
|
|
+ this.socket.send(JSON.stringify(msg))
|
|
|
+
|
|
|
+ this.newMsg = '';
|
|
|
+ },
|
|
|
+ handleWsMessage(event) {
|
|
|
+ let { code, data } = JSON.parse(event.data)
|
|
|
+ if (code === 200) {
|
|
|
+ let { cmd } = data
|
|
|
+ if (cmd === 'sendMsg') {
|
|
|
+ let message = JSON.parse(data.data)
|
|
|
+
|
|
|
+ let user = this.alDisplayList.find(u => u.userId === message.userId)
|
|
|
+ if (user) {
|
|
|
+ message.msgStatus = user.msgStatus
|
|
|
+ } else {
|
|
|
+ message.msgStatus = 0
|
|
|
+ }
|
|
|
+ delete message.params
|
|
|
+ if(this.msgList.length > 50){
|
|
|
+ this.msgList.shift()
|
|
|
+ }
|
|
|
+ this.msgList.push(message)
|
|
|
+ // 移动到底部
|
|
|
+ this.$nextTick(() => {
|
|
|
+ setTimeout(() => {
|
|
|
+ this.$refs.manageRightRef.wrap.scrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight
|
|
|
+ }, 200)
|
|
|
+ })
|
|
|
+ } else if (cmd === 'entry' || cmd === 'out') {
|
|
|
+ const user = data;
|
|
|
+ const online = cmd === 'entry' ? 0 : 1; // 0=在线,1=离线
|
|
|
+ const info = {
|
|
|
+ online:online,
|
|
|
+ msgStatus: user.msgStatus || 0,
|
|
|
+ nickName: user.nickName || '',
|
|
|
+ userType: user.userType || 0,
|
|
|
+ userId: user.userId || '',
|
|
|
+ };
|
|
|
+
|
|
|
+ // 1. 更新总人数(在线/离线互转)
|
|
|
+ if (cmd === 'entry') {
|
|
|
+ this.userTotal.online += 1;
|
|
|
+ this.userTotal.offline = Math.max(0, this.userTotal.offline - 1); // 确保不小于0
|
|
|
+ } else {
|
|
|
+ this.userTotal.offline += 1;
|
|
|
+ this.userTotal.online = Math.max(0, this.userTotal.online - 1); // 确保不小于0
|
|
|
+ }
|
|
|
+ // 2. 强制更新相关列表(无论当前激活哪个Tab)
|
|
|
+ if (cmd === 'entry') {
|
|
|
+ // 用户进入:从离线列表删除,添加到在线列表
|
|
|
+ this.offlineDisplayList = this.offlineDisplayList.filter(u => u.userId !== user.userId);
|
|
|
+ const newOnlineList = this.onlineDisplayList.filter(u => u.userId !== user.userId);
|
|
|
+ newOnlineList.push(info);
|
|
|
+ this.onlineDisplayList = newOnlineList.slice(-40); // 限制最大50条
|
|
|
+ } else {
|
|
|
+ // 用户离开:从在线列表删除,添加到离线列表
|
|
|
+ this.onlineDisplayList = this.onlineDisplayList.filter(u => u.userId !== user.userId);
|
|
|
+ const newOfflineList = this.offlineDisplayList.filter(u => u.userId !== user.userId);
|
|
|
+ newOfflineList.push(info);
|
|
|
+ this.offlineDisplayList = newOfflineList.slice(-40); // 限制最大50条
|
|
|
+ }
|
|
|
+ // 3. 处理禁言列表(如果用户是禁言状态,无论进出都要同步)
|
|
|
+ if (info.msgStatus === 1) {
|
|
|
+ // 禁言用户:从禁言列表删除旧数据,添加新数据
|
|
|
+ const newSilencedList = this.silencedDisplayList.filter(u => u.userId !== user.userId);
|
|
|
+ newSilencedList.push(info);
|
|
|
+ this.silencedDisplayList = newSilencedList.slice(-40);
|
|
|
+ } else {
|
|
|
+ // 非禁言用户:从禁言列表删除(如果存在)
|
|
|
+ this.silencedDisplayList = this.silencedDisplayList.filter(u => u.userId !== user.userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ } else if (cmd === 'live_start') {
|
|
|
+
|
|
|
+ } else if (cmd === 'live_end') {
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ getLive(){
|
|
|
+ getLive(this.liveId).then(res => {
|
|
|
+ if (res.code == 200) {
|
|
|
+ if (res.data.isAudit != 1) {
|
|
|
+ this.$message.error("当前直播间未经审核");
|
|
|
+ this.loading = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.isAudit = true
|
|
|
+ this.status = res.data.status
|
|
|
+ this.videoParam.startTime = new Date(res.data.startTime).getTime()
|
|
|
+ if(res.data.status == 4){
|
|
|
+ this.videoParam.liveType = 3
|
|
|
+ this.videoParam.videoUrl = res.data.videoUrl;
|
|
|
+ }else {
|
|
|
+ if (res.data.status != 2) {
|
|
|
+ this.$message.error("当前直播间未直播");
|
|
|
+ this.loading = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (res.data.liveType == 1) {
|
|
|
+ this.videoParam.livingUrl = res.data.flvHlsUrl
|
|
|
+ this.videoParam.livingUrl = this.livingUrl.replace("flv","m3u8")
|
|
|
+ // this.$nextTick(() => {
|
|
|
+ // this.initPlayer()
|
|
|
+ // })
|
|
|
+ // this.processInterval = setInterval(this.updateLiveProgress, 1000);
|
|
|
+ } else {
|
|
|
+ this.videoParam.liveType = 2
|
|
|
+ this.videoParam.videoUrl = res.data.videoUrl;
|
|
|
+ // this.$nextTick(() => {
|
|
|
+ // this.initVideoPlayer(res.data.startTime)
|
|
|
+ // })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.globalVisible = res.data.globalVisible
|
|
|
+ this.$refs.livePlayer.initPlayer()
|
|
|
+ })
|
|
|
+ this.loading = false
|
|
|
+ } else {
|
|
|
+ this.$message.error(res.msg)
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ this.liveInfo = res.data
|
|
|
+ })
|
|
|
+ },
|
|
|
+ connectWebSocket() {
|
|
|
+ this.$store.dispatch('initLiveWs', {
|
|
|
+ liveWsUrl: this.liveWsUrl,
|
|
|
+ liveId: this.liveId,
|
|
|
+ userId: this.userId
|
|
|
+ })
|
|
|
+ this.socket = this.$store.state.liveWs[this.liveId]
|
|
|
+ this.socket.onmessage = (event) => this.handleWsMessage(event)
|
|
|
+ },
|
|
|
+ changeUserState(u) {
|
|
|
+ const displayList = this[`${this.currentTab}DisplayList`];
|
|
|
+ // 修改状态
|
|
|
+ changeUserStatus({liveId: u.liveId, userId: u.userId}).then(response => {
|
|
|
+ let { code } = response;
|
|
|
+ if (200 === code) {
|
|
|
+ u.msgStatus = u.msgStatus === 0 ? 1 : 0
|
|
|
+ // 同步更新消息列表中相同用户的状态
|
|
|
+ this.msgList.forEach(msg => {
|
|
|
+ if (msg.userId === u.userId) {
|
|
|
+ msg.msgStatus = u.msgStatus;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ displayList.forEach(user => {
|
|
|
+ if (user.userId === u.userId) {
|
|
|
+ user.msgStatus = u.msgStatus;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // 4. 关键:重新筛选所有Tab的显示列表,确保状态同步
|
|
|
+ this.refreshUserDisplayLists(u);
|
|
|
+
|
|
|
+ let msg = u.msgStatus === 0 ? "已解禁" : "已禁言"
|
|
|
+ this.msgSuccess(msg);
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.msgError("操作失败");
|
|
|
+ })
|
|
|
+ },
|
|
|
+ refreshUserDisplayLists(user) {
|
|
|
+ const { userId, msgStatus: newStatus, online } = user;
|
|
|
+ const oldStatus = newStatus === 1 ? 0 : 1; // 操作前的状态(反向)
|
|
|
+
|
|
|
+ // 1. 禁言操作(newStatus=1):从原在线/离线列表移除,加入禁言列表
|
|
|
+ if (newStatus === 1) {
|
|
|
+ // 从在线/离线列表中移除该用户(根据当前在线状态)
|
|
|
+ if (online === 0) {
|
|
|
+ this.onlineDisplayList = this.onlineDisplayList.filter(u => u.userId !== userId);
|
|
|
+ this.userTotal.online = Math.max(0, this.userTotal.online - 1);
|
|
|
+ } else {
|
|
|
+ this.offlineDisplayList = this.offlineDisplayList.filter(u => u.userId !== userId);
|
|
|
+ this.userTotal.offline = Math.max(0, this.userTotal.offline - 1);
|
|
|
+ }
|
|
|
+ this.userTotal.silenced = this.userTotal.silenced + 1;
|
|
|
+ // 添加到禁言列表(去重+限制长度)
|
|
|
+ const silencedList = this.silencedDisplayList.filter(u => u.userId !== userId);
|
|
|
+ silencedList.push(user);
|
|
|
+ this.silencedDisplayList = silencedList.slice(-40);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 解禁操作(newStatus=0):从禁言列表移除,回到原在线/离线列表
|
|
|
+ else {
|
|
|
+ // 从禁言列表移除
|
|
|
+ this.silencedDisplayList = this.silencedDisplayList.filter(u => u.userId !== userId);
|
|
|
+ this.userTotal.silenced = Math.max(0, this.userTotal.silenced - 1);
|
|
|
+ // 回到对应的在线/离线列表(根据当前在线状态)
|
|
|
+ if (online === 0) {
|
|
|
+ // 加入在线列表(去重+限制长度)
|
|
|
+ const onlineList = this.onlineDisplayList.filter(u => u.userId !== userId);
|
|
|
+ onlineList.push(user);
|
|
|
+ this.onlineDisplayList = onlineList.slice(-40);
|
|
|
+ this.userTotal.online = this.userTotal.online + 1;
|
|
|
+ } else {
|
|
|
+ // 加入离线列表(去重+限制长度)
|
|
|
+ const offlineList = this.offlineDisplayList.filter(u => u.userId !== userId);
|
|
|
+ offlineList.push(user);
|
|
|
+ this.offlineDisplayList = offlineList.slice(-40);
|
|
|
+ this.userTotal.offline = this.userTotal.offline + 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ blockUser(u){
|
|
|
+ this.$confirm('是否确认封禁用户账号为:"' + u.nickName + '-' + u.userId + '"?', "警告", {
|
|
|
+ confirmButtonText: "确定",
|
|
|
+ cancelButtonText: "取消",
|
|
|
+ type: "warning"
|
|
|
+ }).then(function() {
|
|
|
+ return blockUser(u.userId);
|
|
|
+ }).then(() => {
|
|
|
+ if(u.msgStatus === 1){
|
|
|
+ this.userTotal.silenced -= 1
|
|
|
+ this.silencedDisplayList = this.silencedDisplayList.filter(user => user.userId !== u.userId)
|
|
|
+ } else if( u.online === 0){
|
|
|
+ this.userTotal.online -= 1
|
|
|
+ this.onlineDisplayList = this.onlineDisplayList.filter(user => user.userId !== u.userId)
|
|
|
+ } else {
|
|
|
+ this.userTotal.offline -= 1
|
|
|
+ this.offlineDisplayList = this.offlineDisplayList.filter(user => user.userId !== u.userId)
|
|
|
+ }
|
|
|
+ this.userTotal.al -= 1
|
|
|
+ this.alDisplayList = this.alDisplayList.filter(user => user.userId !== u.userId)
|
|
|
+ let msg = {
|
|
|
+ msg: "",
|
|
|
+ liveId: this.liveId,
|
|
|
+ userId: u.userId,
|
|
|
+ userType: 0,
|
|
|
+ cmd: 'blockUser',
|
|
|
+ avatar: this.$store.state.user.user.avatar,
|
|
|
+ nickName: this.$store.state.user.user.nickName
|
|
|
+ }
|
|
|
+ this.socket.send(JSON.stringify(msg))
|
|
|
+ this.msgSuccess("封禁成功");
|
|
|
+
|
|
|
+
|
|
|
+ }).catch(() => {});
|
|
|
+ },
|
|
|
+ searchUsers(){
|
|
|
+ this.resetUserParams()
|
|
|
+ this.loadNextPage(this.currentTab)
|
|
|
+ },
|
|
|
+ handleUserClick(tab){
|
|
|
+ const tabName = tab.name;
|
|
|
+ this.currentTab = tabName;
|
|
|
+ const params = this.pageParams[tabName];
|
|
|
+ const displayList = this[`${tabName}DisplayList`];
|
|
|
+ // 首次切换到该Tab或列表为空时初始化
|
|
|
+ if (displayList.length < 20) {
|
|
|
+ // 重置分页参数
|
|
|
+ params.currentPage = 1;
|
|
|
+ params.pageSize = 20;
|
|
|
+ params.prevPage = 0;
|
|
|
+ params.totalLoaded = 0;
|
|
|
+ params.hasMore = true;
|
|
|
+ params.hasPrev = false;
|
|
|
+ // 加载第一页
|
|
|
+ this.loadNextPage(tabName);
|
|
|
+ } else {
|
|
|
+ // 非首次切换,恢复滚动位置
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const scrollEl = this.getScrollElement(tabName);
|
|
|
+ if (scrollEl) {
|
|
|
+ scrollEl.scrollTop = this.tabScrollPositions[tabName] || 0;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ loadNextPage(tabName) {
|
|
|
+ const params = this.pageParams[tabName];
|
|
|
+ const displayList = this[`${tabName}DisplayList`];
|
|
|
+
|
|
|
+ // 若没有更多数据或正在加载,直接返回
|
|
|
+ if (!params.hasMore || this.scrLoading[tabName].next) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.scrLoading[tabName].next = true;
|
|
|
+ const queryParams = {
|
|
|
+ liveId: this.liveId,
|
|
|
+ pageNum: params.currentPage,
|
|
|
+ pageSize: 20,
|
|
|
+ online: tabName === 'online' ? 0 : 1,
|
|
|
+ msgStatus: tabName === 'silenced' ? 1 : 0,
|
|
|
+ all: tabName === 'al' ? 1 : 0,
|
|
|
+ userName: this.searchKeyword
|
|
|
+ };
|
|
|
+ // 调用接口加载对应状态的分页数据(需后端支持按状态筛选)
|
|
|
+ dashBoardWatchUserList(queryParams).then(response => {
|
|
|
+ this.scrLoading[tabName].next = false;
|
|
|
+ if (response.code !== 200) return;
|
|
|
+
|
|
|
+ const { rows, total } = response;
|
|
|
+ params.total = total; // 记录总数据量
|
|
|
+ // 过滤重复数据(基于userId)
|
|
|
+ const newRows = rows.filter(row =>
|
|
|
+ !displayList.some(u => u.userId === row.userId)
|
|
|
+ );
|
|
|
+ displayList.push(...newRows)
|
|
|
+ // 添加新数据并限制最大长度(避免内存占用过大)
|
|
|
+ if (displayList.length >= 40) { // 最大保留100条
|
|
|
+ this[`${tabName}DisplayList`] = displayList.slice(-40);
|
|
|
+ // 记录滚动位置(用于加载后校准)
|
|
|
+ const scrollEl = this.getScrollElement(tabName);
|
|
|
+ // 校准滚动位置(保持视觉连续性)
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (scrollEl) {
|
|
|
+ scrollEl.scrollTop = scrollEl.scrollHeight * 0.5;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // 更新分页状态
|
|
|
+ params.hasMore = params.currentPage * params.pageSize < total;
|
|
|
+ params.currentPage += 1;
|
|
|
+ params.hasPrev = params.currentPage > 2; // 当前页>2时一定有上一页
|
|
|
+ params.prevPage = params.currentPage - 2;
|
|
|
+ }).catch(() => {
|
|
|
+ this.scrLoading[tabName].next = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ // 新增:加载上一页(向上滚动时)
|
|
|
+ loadPrevPage(tabName) {
|
|
|
+ const params = this.pageParams[tabName];
|
|
|
+ const displayList = this[`${tabName}DisplayList`];
|
|
|
+ // 边界校验:无上一页/正在加载/当前页<=1
|
|
|
+ console.log(`加载 ${tabName} 上一页`);
|
|
|
+ console.log(!params.hasPrev || this.scrLoading[tabName].prev || params.currentPage <= 1)
|
|
|
+ if (!params.hasPrev || this.scrLoading[tabName].prev || params.currentPage <= 1) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.scrLoading[tabName].prev = true;
|
|
|
+ const targetPage = params.prevPage > 0 ? params.prevPage : params.currentPage - 2;
|
|
|
+ const queryParams = {
|
|
|
+ liveId: this.liveId,
|
|
|
+ pageNum: targetPage,
|
|
|
+ pageSize: 20,
|
|
|
+ online: tabName === 'online' ? 0 : 1,
|
|
|
+ msgStatus: tabName === 'silenced' ? 1 : 0,
|
|
|
+ all: tabName === 'al' ? 1 : 0,
|
|
|
+ userName: this.searchKeyword
|
|
|
+ };
|
|
|
+ dashBoardWatchUserList(queryParams).then(response => {
|
|
|
+ this.scrLoading[tabName].prev = false;
|
|
|
+ if (response.code !== 200) return;
|
|
|
+
|
|
|
+ const { rows } = response;
|
|
|
+ if (rows.length === 0) {
|
|
|
+ params.hasPrev = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 记录滚动位置(用于加载后校准)
|
|
|
+ const scrollEl = this.getScrollElement(tabName);
|
|
|
+ const scrollTop = scrollEl?.scrollTop || 0;
|
|
|
+ const itemHeight = 80; // 预估行高(根据实际样式调整)
|
|
|
+ const newItemsHeight = rows.length * itemHeight;
|
|
|
+
|
|
|
+ // 过滤重复数据并添加到列表头部
|
|
|
+ const newRows = rows.filter(row => !displayList.some(u => u.userId === row.userId));
|
|
|
+ this[`${tabName}DisplayList`] = [...newRows, ...displayList];
|
|
|
+ params.totalLoaded += newRows.length;
|
|
|
+
|
|
|
+ // 限制最大长度
|
|
|
+ if (this[`${tabName}DisplayList`].length > 40) {
|
|
|
+ this[`${tabName}DisplayList`] = this[`${tabName}DisplayList`].slice(0, 40);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新分页状态
|
|
|
+ params.prevPage = targetPage - 1;
|
|
|
+ params.hasPrev = targetPage > 1; // 上一页页码>1时还有更多上一页
|
|
|
+ params.currentPage = params.currentPage - 1;
|
|
|
+ if(params.currentPage * 20 < params.total) params.hasMore = true;
|
|
|
+ // 校准滚动位置(保持视觉连续性)
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (scrollEl) {
|
|
|
+ scrollEl.scrollTop = scrollEl.scrollHeight * 0.5;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }).catch(() => {
|
|
|
+ this.scrLoading[tabName].prev = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ getList() {
|
|
|
+ this.resetUserParams()
|
|
|
+ this.resetMsgParams()
|
|
|
+ // this.loadUserList()
|
|
|
+ this.loadUserTotals(); // 先加载总人数
|
|
|
+ // this.handleClick('online')
|
|
|
+ // this.handleClick({name:'online'})
|
|
|
+ this.loadMsgList()
|
|
|
+ this.loadLiveTask()
|
|
|
+ },
|
|
|
+ formatDate(value) {
|
|
|
+ // 检查值是否存在且为日期类型(或可转换为日期)
|
|
|
+ if (!value) return '';
|
|
|
+
|
|
|
+ let date;
|
|
|
+ // 处理字符串类型的日期
|
|
|
+ if (typeof value === 'string') {
|
|
|
+ date = new Date(value);
|
|
|
+ }
|
|
|
+ // 处理Date对象
|
|
|
+ else if (value instanceof Date) {
|
|
|
+ date = value;
|
|
|
+ }
|
|
|
+ // 无效类型直接返回原值
|
|
|
+ else {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否为有效日期
|
|
|
+ if (isNaN(date.getTime())) {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 格式化年月日时分秒(补零处理)
|
|
|
+ const year = date.getFullYear();
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(date.getDate()).padStart(2, '0');
|
|
|
+ const hours = String(date.getHours()).padStart(2, '0');
|
|
|
+ const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
|
+ const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
|
+
|
|
|
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
|
+ },
|
|
|
+ loadLiveTask(){
|
|
|
+ if(!this.taskParams.hasMore) return
|
|
|
+ const queryParams = {
|
|
|
+ liveId: this.liveId,
|
|
|
+ pageSize: 10,
|
|
|
+ pageNum: 1
|
|
|
+ }
|
|
|
+ consoleList(queryParams).then(res => {
|
|
|
+ let {code, rows,total} = res;
|
|
|
+ if (code === 200) {
|
|
|
+ this.taskParams.total = total
|
|
|
+ this.timelineItems = rows
|
|
|
+ if(rows.length < this.taskParams.pageSize){
|
|
|
+ this.taskParams.hasMore = false
|
|
|
+ }
|
|
|
+ this.startTaskTimer()
|
|
|
+ } else {
|
|
|
+ this.stopTaskTimer()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ loadMsgList(){
|
|
|
+ // 直播间消息
|
|
|
+ listLiveSingleMsg({
|
|
|
+ liveId:this.liveId,
|
|
|
+ pageNum: this.msgParams.pageNum,
|
|
|
+ pageSize: this.msgParams.pageSize
|
|
|
+ }).then(response => {
|
|
|
+ let {code, rows,total} = response;
|
|
|
+ if (code === 200) {
|
|
|
+ let totalPage = (total % this.msgParams.pageSize == 0) ? Math.floor(total / this.msgParams.pageSize) : Math.floor(total / this.msgParams.pageSize + 1);
|
|
|
+ rows.forEach(row => {
|
|
|
+ if (!this.msgList.some(m => m.msgId === row.msgId)) {
|
|
|
+
|
|
|
+ let user = this.alDisplayList.find(u => u.userId === row.userId)
|
|
|
+ if (user) {
|
|
|
+ row.msgStatus = user.msgStatus
|
|
|
+ } else {
|
|
|
+ row.msgStatus = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ this.msgList.push(row)
|
|
|
+
|
|
|
+ // 移动到底部
|
|
|
+ this.$nextTick(() => {
|
|
|
+ setTimeout(() => {
|
|
|
+ this.$refs.manageRightRef.wrap.scrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight
|
|
|
+ }, 200)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ this.msgList.reverse()
|
|
|
+ // 同步更新消息列表中相同用户的状态
|
|
|
+ this.alDisplayList.forEach(u => {
|
|
|
+ this.msgList.filter(m => m.userId === u.userId).forEach(m => m.msgStatus = u.msgStatus)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 添加滚动监听
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.$refs.manageRightRef.wrap.addEventListener("scroll", this.manageRightScroll)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ loadUserTotals() {
|
|
|
+ if (!this.liveId) return;
|
|
|
+ // 假设后端提供一个接口返回总人数(如果没有,可通过首次加载全量数据后统计)
|
|
|
+ getLiveUserTotals({ liveId: this.liveId }).then(res => {
|
|
|
+ if (res.code === 200) {
|
|
|
+ this.userTotal = res.data; // { online, offline, silenced }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ toggleBlack(user) {
|
|
|
+ user.isBlack = !user.isBlack;
|
|
|
+ },
|
|
|
+ toggleMute(user) {
|
|
|
+ user.isMuted = !user.isMuted;
|
|
|
+ },
|
|
|
+ toggleVisible(msg) {
|
|
|
+ msg.isVisible = !msg.isVisible;
|
|
|
+ },
|
|
|
+ deleteMessage(msg) {
|
|
|
+ const index = this.messages.indexOf(msg);
|
|
|
+ if (index > -1) {
|
|
|
+ this.messages.splice(index, 1);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ addTimelineItem() {
|
|
|
+ this.timelineItems.push({ time: '00:00', action: '新动作' });
|
|
|
+ },
|
|
|
+ removeTimelineItem(item) {
|
|
|
+ const index = this.timelineItems.indexOf(item);
|
|
|
+ if (index > -1) {
|
|
|
+ this.timelineItems.splice(index, 1);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ resetUserParams() {
|
|
|
+ // 重置各Tab的显示列表和分页参数
|
|
|
+ this.alDisplayList = [];
|
|
|
+ this.onlineDisplayList = []; // 在线用户显示列表
|
|
|
+ this.offlineDisplayList = []; // 离线用户显示列表
|
|
|
+ this.silencedDisplayList = []; // 禁言用户显示列表
|
|
|
+ this.pageParams= {
|
|
|
+ al: {
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ prevPage: 0,
|
|
|
+ totalLoaded: 0,
|
|
|
+ total: 0,
|
|
|
+ hasMore: true,
|
|
|
+ hasPrev: false
|
|
|
+ },
|
|
|
+ online: {
|
|
|
+ currentPage: 1, // 当前页(下一页加载用)
|
|
|
+ pageSize: 20, // 当前页(下一页加载用)
|
|
|
+ prevPage: 0, // 上一页页码(上一页加载用)
|
|
|
+ totalLoaded: 0, // 已加载总条数
|
|
|
+ total: 0, // 总数据量
|
|
|
+ hasMore: true, // 是否有下一页
|
|
|
+ hasPrev: false // 是否有上一页
|
|
|
+ },
|
|
|
+ offline: {
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ prevPage: 0,
|
|
|
+ totalLoaded: 0,
|
|
|
+ total: 0,
|
|
|
+ hasMore: true,
|
|
|
+ hasPrev: false
|
|
|
+ },
|
|
|
+ silenced: {
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ prevPage: 0,
|
|
|
+ totalLoaded: 0,
|
|
|
+ total: 0,
|
|
|
+ hasMore: true,
|
|
|
+ hasPrev: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ this.scrLoading = {
|
|
|
+ al: { next: false, prev: false },
|
|
|
+ online: { next: false, prev: false },
|
|
|
+ offline: { next: false, prev: false },
|
|
|
+ silenced: { next: false, prev: false }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ resetMsgParams() {
|
|
|
+ // 消息参数保留
|
|
|
+ this.msgList = [];
|
|
|
+ this.msgParams = {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 30,
|
|
|
+ liveId: this.liveId
|
|
|
+ };
|
|
|
+ this.taskParams = {
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ prevPage: 0,
|
|
|
+ totalLoaded: 0,
|
|
|
+ total: 0,
|
|
|
+ hasMore: true,
|
|
|
+ hasPrev: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ getScrollElement(tabName) {
|
|
|
+ const scrollRefs = {
|
|
|
+ al: this.$refs.manageLeftRef_al,
|
|
|
+ online: this.$refs.manageLeftRef_online,
|
|
|
+ offline: this.$refs.manageLeftRef_offline,
|
|
|
+ silenced: this.$refs.manageLeftRef_silenced
|
|
|
+ };
|
|
|
+ return scrollRefs[tabName]?.wrap;
|
|
|
+ },
|
|
|
+ // 初始化滚动监听(在mounted中调用)
|
|
|
+ initScrollListeners() {
|
|
|
+ // 为每个Tab的滚动容器添加监听
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const scrollRefs = {
|
|
|
+ al: this.$refs.manageLeftRef_al,
|
|
|
+ online: this.$refs.manageLeftRef_online,
|
|
|
+ offline: this.$refs.manageLeftRef_offline,
|
|
|
+ silenced: this.$refs.manageLeftRef_silenced
|
|
|
+ };
|
|
|
+
|
|
|
+ Object.keys(scrollRefs).forEach(tabName => {
|
|
|
+ const scrollEl = scrollRefs[tabName]?.wrap;
|
|
|
+ if (scrollEl) {
|
|
|
+ scrollEl.addEventListener('scroll', () =>
|
|
|
+ this.handleTabScroll(tabName, scrollEl)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+ // 处理Tab滚动事件(判断是否触底)
|
|
|
+ handleTabScroll(tabName, scrollEl) {
|
|
|
+ const { scrollTop, scrollHeight, clientHeight } = scrollEl;
|
|
|
+ const bottomThreshold = 50; // 距离底部100px触发下一页
|
|
|
+ const topThreshold = 50; // 距离顶部100px触发上一页
|
|
|
+
|
|
|
+ // 加载下一页(滚动到底部附近)
|
|
|
+ if (scrollHeight - scrollTop - clientHeight < bottomThreshold) {
|
|
|
+ this.loadNextPage(tabName);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载上一页(滚动到顶部附近)
|
|
|
+ if (scrollTop < topThreshold) {
|
|
|
+ this.loadPrevPage(tabName);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 恢复聊天滚动位置
|
|
|
+ restoreChatScrollPosition() {
|
|
|
+ if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
|
|
|
+ this.$refs.manageRightRef.wrap.scrollTop = this.chatScrollTop;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 保存聊天滚动位置
|
|
|
+ saveChatScrollPosition() {
|
|
|
+ if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
|
|
|
+ this.chatScrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 停止任务检测定时器
|
|
|
+ */
|
|
|
+ stopTaskTimer() {
|
|
|
+ if (this.taskTimer) {
|
|
|
+ clearInterval(this.taskTimer);
|
|
|
+ this.taskTimer = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 启动任务检测定时器
|
|
|
+ */
|
|
|
+ startTaskTimer() {
|
|
|
+ // 先清除已有定时器,避免重复
|
|
|
+ if (this.taskTimer) {
|
|
|
+ clearInterval(this.taskTimer);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 立即执行一次检查
|
|
|
+ this.checkTaskExpiration();
|
|
|
+
|
|
|
+ // 启动定时器,定期检查
|
|
|
+ this.taskTimer = setInterval(() => {
|
|
|
+ this.checkTaskExpiration();
|
|
|
+ }, this.checkInterval);
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 检查时间轴第一条任务是否过期
|
|
|
+ */
|
|
|
+ checkTaskExpiration() {
|
|
|
+ // 如果没有任务,直接返回
|
|
|
+ if (!this.timelineItems || this.timelineItems.length === 0) {
|
|
|
+ this.stopTaskTimer()
|
|
|
+ return;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 获取第一条任务的时间
|
|
|
+ const firstTask = this.timelineItems[0];
|
|
|
+ const taskTime = new Date(firstTask.absValue).getTime();
|
|
|
+ const currentTime = new Date().getTime();
|
|
|
+
|
|
|
+ // 如果任务时间已过当前时间(过期),重新加载任务列表
|
|
|
+ if (taskTime < currentTime) {
|
|
|
+ this.timelineItems.shift()
|
|
|
+ this.loadLiveTask(); // 重新加载任务列表
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ if (this.autoMsgTimer != null) {
|
|
|
+ clearInterval(this.autoMsgTimer);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 使用 deactivated 和 activated 钩子替代 beforeDestroy 和 destroyed
|
|
|
+ deactivated() {
|
|
|
+ this.saveChatScrollPosition();
|
|
|
+ },
|
|
|
+ activated() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.restoreChatScrollPosition();
|
|
|
+ });
|
|
|
+ // todo yhq
|
|
|
+ // this.$nextTick(() => {
|
|
|
+ // const video = this.$refs.videoPlayer;
|
|
|
+ // if (video != null) {
|
|
|
+ // this.initVideoPlayer(this.liveInfo.startTime)
|
|
|
+ // }
|
|
|
+ // })
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.console {
|
|
|
+ display: flex;
|
|
|
+ height: 100vh;
|
|
|
+}
|
|
|
+
|
|
|
+.left-panel, .middle-panel, .right-panel {
|
|
|
+ padding: 20px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.left-panel {
|
|
|
+ width: 30%;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-right: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.middle-panel {
|
|
|
+ width: 40%;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-right: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.right-panel {
|
|
|
+ width: 30%;
|
|
|
+ background: #f8fafc;
|
|
|
+}
|
|
|
+
|
|
|
+.search {
|
|
|
+ margin: 10px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.search input {
|
|
|
+ width: 70%;
|
|
|
+ padding: 8px;
|
|
|
+ border: 1px solid #cbd5e1;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.search button {
|
|
|
+ padding: 8px 15px;
|
|
|
+ background: #3b82f6;
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.tabs {
|
|
|
+ display: flex;
|
|
|
+ margin: 10px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.tabs button {
|
|
|
+ padding: 8px 15px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ background: #fff;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.tabs button.active {
|
|
|
+ background: #3b82f6;
|
|
|
+ color: #fff;
|
|
|
+ border-color: #3b82f6;
|
|
|
+}
|
|
|
+
|
|
|
+.user-list {
|
|
|
+ max-height: 600px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.user-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px;
|
|
|
+ border-bottom: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.user-item img {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 50%;
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.user-info {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.user-name {
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.user-status {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.online {
|
|
|
+ color: #10b981;
|
|
|
+}
|
|
|
+
|
|
|
+.offline {
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+.user-actions {
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+
|
|
|
+.user-actions button {
|
|
|
+ padding: 5px 10px;
|
|
|
+ margin-left: 5px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.block {
|
|
|
+ background: #ef4444;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.unblock {
|
|
|
+ background: #10b981;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.mute {
|
|
|
+ background: #f59e0b;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.unmute {
|
|
|
+ background: #3b82f6;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.system-messages, .discussion-messages {
|
|
|
+ margin: 20px 0;
|
|
|
+ background: #fff;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.system-messages textarea {
|
|
|
+ width: 100%;
|
|
|
+ height: 100px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 8px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.message-actions {
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-actions button {
|
|
|
+ padding: 5px 10px;
|
|
|
+ margin-right: 5px;
|
|
|
+ background: #3b82f6;
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.message-list {
|
|
|
+ max-height: 300px;
|
|
|
+ overflow-y: auto;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-item {
|
|
|
+ display: flex;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ border-bottom: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.message-avatar img {
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+ border-radius: 50%;
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-content {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.message-user {
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.message-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.message-actions button {
|
|
|
+ padding: 3px 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ background: #3b82f6;
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.live-player, .automation, .watermark {
|
|
|
+ margin: 20px 0;
|
|
|
+ background: #fff;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.timeline-items {
|
|
|
+ margin: 10px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.timeline-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px 0;
|
|
|
+ border-bottom: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.delete {
|
|
|
+ background: #ef4444;
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 3px 8px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.add {
|
|
|
+ background: #10b981;
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 8px 15px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.watermark-settings textarea {
|
|
|
+ width: 100%;
|
|
|
+ height: 100px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 8px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.watermark-options {
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.watermark-options label {
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 5px;
|
|
|
+}
|
|
|
+/* 隐藏 el-scrollbar 的横向滚动条 */
|
|
|
+.el-scrollbar__wrap {
|
|
|
+ overflow-x: hidden !important;
|
|
|
+}
|
|
|
+.custom-scrollbar .el-scrollbar__wrap {
|
|
|
+ overflow-x: hidden !important;
|
|
|
+}
|
|
|
+.scrollbar-demo-item{
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 50px;
|
|
|
+ margin: 10px;
|
|
|
+ text-align: center;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+</style>
|