Przeglądaj źródła

直播新代码提交

yuhongqi 4 dni temu
rodzic
commit
85cb2b7eb4

+ 7 - 0
src/api/live/liveData.js

@@ -45,3 +45,10 @@ export function queryStudentData(query) {
   })
 }
 
+export function dashboardData(liveId) {
+  return request({
+    url: '/liveData/liveData/dashboardData' + '?liveId=' +liveId,
+    method: 'get'
+  })
+}
+

+ 8 - 0
src/api/live/liveWatchUser.js

@@ -9,6 +9,14 @@ export function watchUserList(query) {
   })
 }
 // 查询直播间用户列表
+export function dashBoardWatchUserList(query) {
+  return request({
+    url: '/live/liveWatchUser/dashBoardWatchUserList',
+    method: 'get',
+    params: query
+  })
+}
+// 查询直播间用户列表
 export function getLiveUserTotals(query) {
   return request({
     url: '/live/liveWatchUser/liveUserTotals',

+ 10 - 0
src/router/index.js

@@ -6,6 +6,7 @@ Vue.use(Router)
 /* Layout */
 import Layout from '@/layout'
 import ParentView from '@/components/ParentView';
+import LiveConsole from "@/views/live/liveConsole/index.vue";
 
 /**
  * Note: 路由配置项
@@ -261,6 +262,15 @@ export const constantRoutes = [
       }
     ]
   },
+  // 独立页路由:不嵌套根布局,直接渲染目标组件
+  {
+    path: '/live/liveConsole/:liveId',
+    name: 'LiveConsole',
+    component: LiveConsole, // 直接渲染目标组件,无侧边栏
+    meta: {
+      isIndependentPage: true // 标记为“独立页”
+    }
+  },
 
 
 

+ 5 - 1
src/views/live/live/index.vue

@@ -856,7 +856,11 @@ export default {
       this.$router.push('/live/liveConfig/' + row.liveId)
     },
     handleManage(row) {
-      this.$router.push('/live/liveConsole/' + row.liveId)
+      const routeUrl = this.$router.resolve({
+        path: `/live/liveConsole/` + row.liveId
+      }).href;
+      window.open(routeUrl, '_blank')
+      // this.$router.push('/live/liveConsole/' + row.liveId)
     },
     handleEnded(row){
       this.$confirm('是否确认关闭直播间?', "警告", {

+ 61 - 0
src/views/live/liveConsole/EchartsComponent.vue

@@ -0,0 +1,61 @@
+<template>
+  <div :id="chartId" class="chart-container"></div>
+</template>
+
+<script>
+import * as echarts from 'echarts';
+import 'echarts/map/js/china'; // 确保地图模块引入
+
+export default {
+  name: 'EChartsComponent',
+  props: {
+    chartId: {type: String, required: true},
+    option: {type: Object, required: true}
+  },
+  mounted() {
+    this.$nextTick(() => { // 确保DOM渲染完成后初始化
+      this.initChart();
+      window.addEventListener('resize', this.resizeChart);
+    });
+  },
+  // 监听 option 变化,自动更新图表
+  watch: {
+    option: {
+      deep: true, // 深度监听对象内部变化
+      handler(newVal) {
+        if (this.chart) {
+          this.chart.setOption(newVal); // 关键:用新数据更新图表
+        }
+      }
+    }
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.resizeChart);
+    if (this.chart) this.chart.dispose();
+  },
+  methods: {
+    initChart() {
+      const container = document.getElementById(this.chartId);
+      if (!container) return; // 防止容器不存在
+      this.chart = echarts.init(container);
+      this.chart.setOption(this.option);
+    },
+    resizeChart() {
+      if (this.chart) this.chart.resize();
+    },
+    // 暴露手动更新方法(可选,用于特殊场景)
+    updateOption(newOption) {
+      if (this.chart) {
+        this.chart.setOption(newOption);
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+.chart-container {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 948 - 0
src/views/live/liveConsole/LiveConsole.vue

@@ -0,0 +1,948 @@
+<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 ref="manageLeftRef_al" style="height: 800px; width: 100%;">
+            <el-row style="margin-top: 10px" type="flex" align="middle" 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 ref="manageLeftRef_online" style="height: 800px; width: 100%;">
+            <el-row style="margin-top: 10px" type="flex" align="middle" 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 ref="manageLeftRef_offline" style="height: 800px; width: 100%;">
+            <el-row style="margin-top: 10px" type="flex" align="middle" 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 ref="manageLeftRef_silenced" style="height: 800px; width: 100%;">
+            <el-row style="margin-top: 10px" type="flex" align="middle" 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="system-messages">
+        <h3>系统消息</h3>
+        <textarea placeholder="输入系统消息"></textarea>
+        <div class="message-actions">
+          <button>置顶消息</button>
+          <button>弹窗消息</button>
+        </div>
+      </div>
+
+      <div class="discussion-messages">
+        <h3>讨论区消息</h3>
+        <div class="message-settings">
+          <label>
+            <input type="checkbox" v-model="globalVisible">
+            全局用户可见
+          </label>
+        </div>
+        <div class="message-list">
+          <div class="message-item" v-for="msg in messages" :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>
+
+    <div class="right-panel">
+      <h2>运营工具</h2>
+      <div class="live-player">
+        <h3>直播预览</h3>
+        <LivePlayer :stream-url="streamUrl" />
+      </div>
+
+      <div class="automation">
+        <h3>运营自动化</h3>
+        <div class="timeline">
+          <h4>时间轴设置</h4>
+          <div class="timeline-items">
+            <div class="timeline-item" v-for="item in timelineItems" :key="item.time">
+              <div class="time">{{ item.time }}</div>
+              <div class="action">{{ item.action }}</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 placeholder="水军弹幕内容模板,每行一条"></textarea>
+          <div class="watermark-options">
+            <label>
+              发送间隔:
+              <input type="number" v-model.number="interval" min="1">
+              秒
+            </label>
+            <label>
+              <input type="checkbox" v-model="autoWatermark">
+              启用水军自动化
+            </label>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import LivePlayer from './LivePlayer.vue';
+import {blockUser, changeUserStatus, getLiveUserTotals, dashBoardWatchUserList} from '@/api/live/liveWatchUser'
+
+
+export default {
+  components: {
+    LivePlayer
+  },
+  props: {
+    liveId: {
+      type: String,
+      default: null
+    }
+  },
+  data() {
+    return {
+      searchKeyword: '',
+      globalVisible: true,
+      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: [
+        { time: '09:00', action: '置顶欢迎消息' },
+        { time: '09:30', action: '上架主推商品' }
+      ],
+      userTotal: {
+        online: 0,       // 在线总人数
+        offline: 0,      // 离线总人数
+        silenced: 0,      // 禁言总人数
+        al: 0
+      },
+      tabLeft: {
+        activeName: "online",
+      },
+      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 }
+      }
+    };
+  },
+  computed: {
+    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:'online'})
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.restoreChatScrollPosition();
+    });
+    this.initScrollListeners();
+  },
+  methods: {
+    changeUserState(u) {
+      // 修改状态
+      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;
+            }
+          });
+
+          this.userList.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("操作失败");
+      })
+    },
+    blockUser(u){
+      this.$confirm('是否确认封禁用户账号为:"' + u.nickName + '-' + u.userId + '"?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return blockUser(u.userId);
+      }).then(() => {
+        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()
+    },
+    handleUserClick(tab){
+      const tabName = tab.name;
+      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()
+    },
+    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: 10,
+        liveId: this.liveId
+      };
+    },
+    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;
+      }
+    },
+  },
+  // 使用 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;
+}
+</style>

+ 372 - 0
src/views/live/liveConsole/LiveDashboard.vue

@@ -0,0 +1,372 @@
+<template>
+  <div class="dashboard-container">
+    <!-- 实时大屏主体(100%占满父容器) -->
+    <div class="dashboard">
+      <!-- 顶部数据卡片区(占满宽度,高度20%) -->
+      <div class="data-cards">
+        <div class="card" v-for="item in dataCards" :key="item.title">
+          <h3>{{ item.title }}</h3>
+          <p class="value">{{ item.value }}</p>
+        </div>
+      </div>
+
+      <!-- 中间内容区(上下分两部分,各占40%高度) -->
+      <div class="middle-content">
+        <!-- 左侧用户分析区(占50%宽度) -->
+        <div class="user-analysis">
+          <div class="new-old">
+            <h3>新老用户占比</h3>
+            <EChartsComponent chartId="newOldChart" :option="newOldOption" />
+          </div>
+          <div class="region">
+            <h3>地域访客</h3>
+            <EChartsComponent chartId="regionChart" :option="regionOption" />
+          </div>
+        </div>
+
+        <!-- 右侧来源与榜单区(占50%宽度) -->
+        <div class="source-rank">
+          <div class="source">
+            <h3>观众来源</h3>
+            <EChartsComponent chartId="sourceChart" :option="sourceOption" />
+          </div>
+          <div class="rank">
+            <h3>邀请达人榜</h3>
+            <ul class="rank-list">
+              <li v-for="(item, index) in rankList" :key="index">
+                <span class="rank-num">{{ index + 1 }}</span>
+                <span class="rank-name">{{ item.userName }}</span>
+                <span class="rank-count">{{ item.inviteNum }}</span>
+              </li>
+            </ul>
+          </div>
+        </div>
+      </div>
+
+      <!-- 底部趋势图(占20%高度) -->
+<!--      <div class="trend">-->
+<!--        <h3>实时在线人数趋势</h3>-->
+<!--        <EChartsComponent chartId="trendChart" :option="trendOption" />-->
+<!--      </div>-->
+    </div>
+  </div>
+</template>
+
+<script>
+import EChartsComponent from './EchartsComponent.vue';
+import {dashboardData} from '@/api/live/liveData'
+
+export default {
+  components: { EChartsComponent },
+  props: {
+    liveId: {
+      type: String,
+      default: null
+    }
+  },
+  data() {
+    return {
+      // 数据保持不变(与原代码一致)
+      dataCards: [
+        { title: '在线人数', value: '2.5k' },
+        { title: '观看人次', value: '15.2k' },
+        { title: '点赞数', value: '12.8k' },
+        { title: '评论数', value: '3.2k' }
+      ],
+      newOldOption: {
+        tooltip: { trigger: 'item' },
+        series: [{
+          type: 'pie', radius: '70%',
+          data: [{ value: 65, name: '新用户' }, { value: 35, name: '老用户' }],
+          itemStyle: { colors: ['#36cfc9', '#722ed1'] }
+        }]
+      },
+      regionOption: {
+        tooltip: { trigger: 'item' },
+        series: [{
+          type: 'map', map: 'china', roam: true,
+          itemStyle: {
+            normal: { areaColor: '#36cfc9', borderColor: '#fff' },
+            emphasis: { areaColor: '#722ed1' }
+          },
+          data: [{ name: '北京', value: 120 }, { name: '上海', value: 150 }, { name: '广州', value: 90 }, { name: '深圳', value: 80 }, { name: '杭州', value: 70 }]
+        }]
+      },
+      sourceOption: {
+        tooltip: { trigger: 'item' },
+        series: [{
+          type: 'pie', radius: '70%',
+          data: [{ value: 45, name: '分享链接' }, { value: 30, name: '直接访问' }, { value: 25, name: '其他' }],
+          itemStyle: { colors: ['#3b82f6', '#10b981', '#f59e0b'] }
+        }]
+      },
+      rankList: [
+        { userName: '达人1', inviteNum: 258 },
+        { userName: '达人2', inviteNum: 186 },
+        { userName: '达人3', inviteNum: 152 },
+        { userName: '达人4', inviteNum: 121 },
+        { userName: '达人5', inviteNum: 98 }
+      ],
+      trendOption: {
+        // 网格:控制图表与容器的边距,避免内容被裁剪
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '3%',
+          top: '10%',
+          containLabel: true // 确保坐标轴标签不被裁剪
+        },
+        // X轴配置(补充样式,适配深色背景)
+        xAxis: {
+          type: 'category',
+          data: ['09:00', '09:10', '09:20', '09:30', '09:40', '09:50'],
+          axisLine: {
+            lineStyle: {
+              color: '#475569' // 坐标轴线条颜色(深色背景可见)
+            }
+          },
+          axisLabel: {
+            color: '#cbd5e1' // 坐标轴文字颜色
+          }
+        },
+        // Y轴配置(同上)
+        yAxis: {
+          type: 'value',
+          axisLine: {
+            lineStyle: {
+              color: '#475569'
+            }
+          },
+          axisLabel: {
+            color: '#cbd5e1'
+          },
+          splitLine: {
+            lineStyle: {
+              color: 'rgba(71, 85, 105, 0.3)' // 网格线半透明
+            }
+          }
+        },
+        // 系列配置(补充线条和区域样式)
+        series: [{
+          data: [1200, 1500, 1800, 2100, 2300, 2500],
+          type: 'line',
+          smooth: true,
+          // 线条样式
+          lineStyle: {
+            color: '#36cfc9',
+            width: 3
+          },
+          // 点样式
+          itemStyle: {
+            color: '#36cfc9',
+            borderWidth: 2,
+            borderColor: '#fff' // 点边缘白色,突出显示
+          },
+          // 填充区域(可选,增强视觉效果)
+          areaStyle: {
+            color: {
+              type: 'linear',
+              x: 0,
+              y: 0,
+              x2: 0,
+              y2: 1,
+              colorStops: [{
+                offset: 0, color: 'rgba(54, 207, 201, 0.3)'
+              }, {
+                offset: 1, color: 'rgba(54, 207, 201, 0)'
+              }]
+            }
+          }
+        }]
+      },
+      socket:null,
+      timer: null
+    };
+  },
+  created() {
+    this.getInitData()
+    this.timer = setInterval(() => {
+      this.getInitData();
+    }, 30000);
+  },
+  beforeDestroy() {
+    clearInterval(this.timer); // 组件销毁时清除定时器
+  },
+  methods: {
+    async getInitData() {
+      dashboardData(this.liveId).then(res => {
+        if(res.code == 200){
+          this.dealData(res.data)
+        }
+      });
+    },
+    dealData(data){
+      this.dataCards = [
+        { title: '在线人数', value: data.onlineNum },
+        { title: '观看人次', value: data.viewNum },
+        { title: '点赞数', value: data.likeNum },
+        { title: '评论数', value: data.commentNum }
+      ]
+      this.newOldOption= {
+        tooltip: { trigger: 'item' },
+        series: [{
+          type: 'pie', radius: '70%',
+          data: [{ value: data.newUserNum, name: '新用户' }, { value: data.oldUserNum, name: '老用户' }],
+          itemStyle: { colors: ['#36cfc9', '#722ed1'] }
+        }]
+      }
+      this.sourceOption = {
+        tooltip: { trigger: 'item' },
+        series: [{
+          type: 'pie', radius: '70%',
+          data: [{ value: data.shareUrlNum, name: '分享链接' }, { value: data.directAccessNum, name: '直接访问' }, { value: 0, name: '其他' }],
+          itemStyle: { colors: ['#3b82f6', '#10b981', '#f59e0b'] }
+        }]
+      }
+      this.rankList = data.inviteUserList;
+    },
+  }
+};
+</script>
+
+<style scoped>
+/* 容器占满整个屏幕 */
+.dashboard-container {
+  width: 100vw;
+  height: 92vh;
+  overflow: hidden;
+  background-color: #0f172a;
+}
+
+/* 大屏主体:100%宽高,内边距用百分比 */
+.dashboard {
+  width: 100%;
+  height: 100%;
+  padding: 1%; /* 用百分比内边距,适配不同屏幕 */
+  box-sizing: border-box;
+  color: #fff;
+  display: flex;
+  flex-direction: column;
+  gap: 1%; /* 区域间间距 */
+}
+
+/* 顶部数据卡片区:高度20%,横向排列 */
+.data-cards {
+  width: 100%;
+  height: 20%;
+  display: flex;
+  justify-content: space-between;
+  gap: 1%; /* 卡片间间距 */
+}
+
+/* 每个卡片:平均分配宽度(4个卡片各占 ~24%) */
+.card {
+  width: 24%; /* 100% - 3个间距(1%) = 97% → 97%/4 ≈ 24% */
+  height: 100%;
+  background: #1a202c;
+  border-radius: 8px;
+  padding: 1%;
+  box-sizing: border-box;
+  text-align: center;
+}
+
+.card h3 {
+  font-size: 1.2vw; /* 字体用vw单位,随屏幕宽度缩放 */
+  margin: 0 0 5% 0;
+}
+
+.card .value {
+  font-size: 2vw;
+  font-weight: bold;
+  margin: 0;
+}
+
+/* 中间内容区:高度40%,左右分栏 */
+.middle-content {
+  width: 100%;
+  height: 70%;
+  display: flex;
+  gap: 1%;
+}
+
+/* 左侧用户分析区:占50%宽度,内部上下分栏 */
+.user-analysis {
+  width: 50%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  gap: 1%;
+}
+
+/* 右侧来源与榜单区:占50%宽度,内部上下分栏 */
+.source-rank {
+  width: 50%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  gap: 1%;
+}
+
+/* 中间区域的子模块(新老用户/地域/来源/榜单):各占50%高度 */
+.new-old, .region, .source, .rank {
+  width: 100%;
+  height: 60.5%; /* 100% - 1个间距(1%) = 99% → 99%/2 ≈ 49.5% */
+  background: #1a202c;
+  border-radius: 8px;
+  padding: 1%;
+  box-sizing: border-box;
+}
+
+/* 标题样式:用vw单位适配 */
+.new-old h3, .region h3, .source h3, .rank h3, .trend h3 {
+  font-size: 1.1vw;
+  margin: 0 0 2% 0;
+}
+
+/* 达人榜单列表 */
+.rank-list {
+  list-style: none;
+  padding: 0;
+  height: calc(100% - 15%); /* 减去标题高度 */
+  overflow-y: auto;
+}
+
+.rank-list li {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 2%;
+  margin-bottom: 1%;
+  border-bottom: 1px solid #2d3748;
+  font-size: 1vw;
+}
+
+.rank-num {
+  width: 3vw;
+  height: 3vw;
+  max-width: 30px;
+  max-height: 30px;
+  border-radius: 50%;
+  background: #36cfc9;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 0.8vw;
+}
+
+/* 底部趋势图:高度20% */
+.trend {
+  width: 100%;
+  height: 20%;
+  background: #1a202c;
+  border-radius: 8px;
+  padding: 1%;
+  box-sizing: border-box;
+}
+
+/* ECharts图表容器:占满父元素 */
+::v-deep .chart-container {
+  width: 100% !important;
+  height: calc(100% - 15%) !important; /* 减去标题高度 */
+}
+</style>

+ 37 - 0
src/views/live/liveConsole/LivePlayer.vue

@@ -0,0 +1,37 @@
+<template>
+  <div class="live-player">
+    <video ref="videoPlayer" controls autoplay class="player">
+      <source :src="streamUrl" type="video/mp4">
+    </video>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'LivePlayer',
+  props: {
+    streamUrl: {
+      type: String,
+      required: true
+    }
+  },
+  mounted() {
+    // 这里可以添加播放器初始化逻辑
+  },
+  beforeDestroy() {
+    // 销毁播放器实例
+  }
+};
+</script>
+
+<style scoped>
+.live-player {
+  margin-bottom: 20px;
+}
+
+.player {
+  width: 100%;
+  height: auto;
+  border-radius: 8px;
+}
+</style>

+ 55 - 0
src/views/live/liveConsole/ScreenScale.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="container">
+    <div ref="scaleContainer" class="scale-content">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ScreenScale',
+  mounted() {
+    this.calculateRatio();
+    window.addEventListener('resize', this.calculateRatio);
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.calculateRatio);
+  },
+  methods: {
+    calculateRatio() {
+      const baseWidth = 1920;
+      const baseHeight = 1080;
+      const el = this.$refs.scaleContainer;
+      // 基于父容器宽高计算缩放比(而非整个窗口)
+      const parentWidth = el.parentElement.clientWidth;
+      const parentHeight = el.parentElement.clientHeight;
+      const widthRatio = parentWidth / baseWidth;
+      const heightRatio = parentHeight / baseHeight;
+      const scale = Math.min(widthRatio, heightRatio); // 取最小缩放比防止溢出
+
+      el.style.transform = `scale(${scale}) translate(-50%, -50%)`;
+      el.style.transformOrigin = 'top left';
+    }
+  }
+};
+</script>
+
+<style scoped>
+.container {
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: flex-start;
+  overflow: hidden;
+  background-color: #0f172a;
+}
+
+.scale-content {
+  position: relative;
+  width: 1920px;
+  height: 1080px;
+  transform-origin: top left;
+}
+</style>

+ 1344 - 0
src/views/live/liveConsole/index-old.vue

@@ -0,0 +1,1344 @@
+<template>
+  <div class="el-container">
+
+
+  <!-- 直播中控台 start -->
+  <el-row type="flex" justify="center" class="live-console" :gutter="10" v-loading="loading">>
+    <!-- 聊天 start -->
+    <el-col class="live-console-col" :span="6">
+      <el-tabs class="live-console-tab-left" v-model="tabRight.activeName" @tab-click="handleClick" :stretch="true">
+        <el-tab-pane label="讨论" name="talk">
+          <el-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>
+                    </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 style="padding: 10px; border-top: 1px solid #ebeef5; background-color: #fff; min-height: 120px;">
+            <el-input
+              type="textarea"
+              v-model="newMsg"
+              placeholder="请输入消息..."
+              :rows="8"
+              @keyup.enter.native="sendMessage"
+              clearable
+              resize="none"
+              style="flex: 1; margin-right: 10px;"
+            >
+            </el-input>
+            <div style="display: flex; justify-content: flex-end; margin-top: 10px;">
+              <el-button plain @click="sendMessage">发送</el-button>
+            </div>
+          </div>
+        </el-tab-pane>
+      </el-tabs>
+    </el-col>
+    <!-- 聊天 end -->
+
+
+    <!-- 直播/视频 start -->
+    <el-col class="live-console-col" :span="12">
+      <div style="background: #000; border-radius: 5px; overflow: hidden; margin: 10px 5px;">
+        <div style="border-radius: 5px; overflow: hidden;" v-if="!isAudit">
+          <img :src="require('@/assets/images/videoIsAudit.png')" style="width: 100%; height: 45vh;">
+        </div>
+        <div style="border-radius: 5px; overflow: hidden;" v-else-if="status != 2 && status != 4">
+          <img :src="require('@/assets/images/videoNotStart.png')" style="width: 100%; height: 45vh;">
+        </div>
+        <div style="border-radius: 5px; overflow: hidden;" v-else-if="liveType == 1">
+          <video
+            controls
+            ref="livingPlayer"
+            autoplay
+            @click.prevent
+            @contextmenu.prevent
+            class="custom-video"
+            width="100%"
+            style="display: block; background: #000; height: 45vh;"
+          ></video>
+
+          <!-- 时间显示(可选) -->
+          <div ref="liveElapsedTime" class="time-display">
+            已播放:<span id="liveElapsedTime">00:00:00</span>
+          </div>
+        </div>
+        <div style="border-radius: 5px; overflow: hidden;" v-else-if="liveType == 2">
+          <video
+            controls
+            ref="videoPlayer"
+            loop
+            autoplay
+            width="100%"
+            muted
+            playsinline
+            @click.prevent
+            @contextmenu.prevent
+            class="custom-video"
+            style="display: block; background: #000; height: 40vh;"
+          >
+            <source :src="videoUrl" type="application/x-mpegURL">
+          </video>
+          <!-- 自定义进度条容器 -->
+          <div ref="progressBar" class="progress-container">
+            <div id="progressBar" class="progress-bar"></div>
+          </div>
+
+          <!-- 时间显示(可选) -->
+          <div ref="elapsedTime" class="time-display">
+            已播放:<span id="elapsedTime">00:00:00</span>
+          </div>
+        </div>
+        <div style="border-radius: 5px; overflow: hidden;" v-else-if="liveType == 3">
+          <video
+            controls
+            ref="liveReplay"
+            loop
+            autoplay
+            width="100%"
+            playsinline
+            style="display: block; background: #000; height: 40vh;"
+          >
+            <source :src="videoUrl" type="application/x-mpegURL">
+          </video>
+        </div>
+        <div style="border-radius: 5px; overflow: hidden;" v-else>
+          <img :src="require('@/assets/images/videoNotStart.png')" style="width: 100%; height: 45vh;">
+        </div>
+      </div>
+      <!-- 底部导航栏 -->
+      <div style="display: flex; justify-content: space-around; padding: 15px 0; background: #fff; border-top: 1px solid #f0f0f0;">
+        <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickRed">
+          <i class="el-icon-money" style="font-size: 20px;"></i>
+          <span style="font-size: 12px; margin-top: 4px;">红包配置</span>
+        </div>
+        <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickLottery">
+          <i class="el-icon-present" style="font-size: 20px;"></i>
+          <span style="font-size: 12px; margin-top: 4px;">抽奖配置</span>
+        </div>
+        <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickGoods">
+          <i class="el-icon-goods" style="font-size: 20px;"></i>
+          <span style="font-size: 12px; margin-top: 4px;">商品</span>
+        </div>
+        <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickOrder">
+          <i class="el-icon-goods" style="font-size: 20px;"></i>
+          <span style="font-size: 12px; margin-top: 4px;">直播订单</span>
+        </div>
+        <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickCoupon">
+          <i class="el-icon-goods" style="font-size: 20px;"></i>
+          <span style="font-size: 12px; margin-top: 4px;">直播优惠券</span>
+        </div>
+      </div>
+      <el-radio-group v-model="tableRadio" >
+        <el-radio-button label="订单数">订单数</el-radio-button>
+      </el-radio-group>
+      <div  style="position: relative;width: 100%; height: 300px;">
+        <div ref="chartContainer" style="width: 100%; height: 100%;"></div>
+        <div style="position: absolute; top: 10px; right: 10px; background: #fff; padding: 5px; z-index: 1;">
+          <el-select v-model="searchQuery.timeOptions" placeholder="请选择" style="width: 150px"  @change="timeChange">
+            <el-option
+              v-for="item in timeOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+          <el-select v-model="searchQuery.timeGranularity" placeholder="请选择" style="width: 150px"  @change="timeGranularityChange">
+            <el-option
+              v-for="item in timeGranularity"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+<!--          <el-button type="primary" @click="applyFilter">搜索</el-button>-->
+        </div>
+      </div>
+    </el-col>
+    <!-- 直播/视频 end -->
+
+    <!-- 用户列表 start -->
+    <el-col class="live-console-col" :span="6">
+      <el-tabs class="live-console-tab-right" v-model="tabLeft.activeName" @tab-click="handleClick" :stretch="true">
+        <el-tab-pane :label="onlineLabel" name="online">
+          <el-scrollbar ref="manageLeftRef_online" style="height: 800px; width: 100%;">
+            <el-row style="margin-top: 10px" type="flex" align="middle" 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 ref="manageLeftRef_offline" style="height: 800px; width: 100%;">
+            <el-row style="margin-top: 10px" type="flex" align="middle" 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 ref="manageLeftRef_silenced" style="height: 800px; width: 100%;">
+            <el-row style="margin-top: 10px" type="flex" align="middle" 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>
+    </el-col>
+    <!-- 用户列表 end -->
+  </el-row>
+  </div>
+  <!-- 直播中控台  end -->
+</template>
+
+
+<script>
+import {blockUser, changeUserStatus, getLiveUserTotals, watchUserList} from '@/api/live/liveWatchUser'
+import { getLiveVideoByLiveId } from '@/api/live/liveVideo'
+import {getLivingUrl, getLive, delLive} from '@/api/live/live'
+import { getLiveOrderTimeGranularity } from '@/api/live/liveOrder'
+import { listLiveMsg } from '@/api/live/liveMsg'
+import Hls from 'hls.js';
+import {onBeforeUnmount} from 'vue';
+import LiveLotteryConf from '@/views/live/liveConfig/liveLotteryConf.vue'
+import LiveRedConf from '@/views/live/liveConfig/liveRedConf.vue'
+import LiveGoods from '@/views/live/liveConfig/goods.vue'
+import LiveOrder from '@/views/live/liveOrder/index.vue'
+import LiveCoupon from '@/views/live/liveConfig/liveCoupon.vue'
+import echarts from 'echarts'
+
+
+export default {
+  name: "LiveConsole",
+  components: { LiveLotteryConf,LiveRedConf,LiveGoods },
+  data() {
+    return {
+      loading: true,
+      tabLeft: {
+        activeName: "online",
+      },
+      tabRight: {
+        activeName: "talk",
+      },
+      livingUrl: "",
+      videoUrl: "",
+      status: 0,
+      loadMsgMaxPage: 2,
+      his: null,
+      liveVideo: {},
+      socket: null,
+      liveWsUrl: process.env.VUE_APP_LIVE_WS_URL + '/app/webSocket',
+      userParams:{
+        pageNum: 1,
+        pageSize: 10,
+        liveId: null
+      },
+      msgParams: {
+        pageNum: 1,
+        pageSize: 10,
+        liveId: null
+      },
+      userList: [],
+      msgList: [],
+      newMsg: '',
+      isAudit: false,
+      myChart: null, // 用于存储 ECharts 实例
+      liveType: 1,
+      tableRadio: '订单数',
+      // ... 其他数据 ...
+      searchQuery: {timeOptions:'2',timeGranularity:'10',liveId: null}, // 搜索查询条件
+      timeOptions: [
+        {value:'2',label:'最近2小时',key:'2'},
+        {value:'4',label:'最近4小时',key:'4'},
+        {value:'all',label:'全场',key:'all'},
+      ],
+      timeGranularity: [
+        {value:'10',label:'10分钟',key:'10'},
+        {value:'30',label:'30分钟',key:'30'},
+        {value:'60',label:'1小时',key:'60'},
+      ],
+      videoDuration: 0,
+      startTime: null,
+      processInterval: null,
+      // ... 其他数据
+      chatScrollTop: 0, // 保存聊天滚动位置,
+      liveId: null,
+      userTotal: {
+        online: 0,       // 在线总人数
+        offline: 0,      // 离线总人数
+        silenced: 0      // 禁言总人数
+      },
+      // 各Tab的显示列表(仅存储当前需要展示的数据)
+      onlineDisplayList: [],    // 在线用户显示列表
+      offlineDisplayList: [],   // 离线用户显示列表
+      silencedDisplayList: [],  // 禁言用户显示列表
+      // 各Tab的分页参数
+      pageParams: {
+        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: {
+        online: { next: false, prev: false },
+        offline: { next: false, prev: false },
+        silenced: { next: false, prev: false }
+      }
+    }
+  },
+  created() {
+    if (this.$route.params.liveId) {
+      this.liveId = this.$route.params.liveId;
+    }else {
+      this.liveId = this.$route.query.liveId;
+    }
+    // this.getLiveVideo()
+    this.getList()
+    this.connectWebSocket()
+    this.getLive()
+    this.searchQuery.liveId = this.liveId
+  },
+  computed: {
+    userId() {
+      return this.$store.state.user.user.userId
+    },
+    companyId() {
+      return this.$store.state.user.user.companyId
+    },
+    onlineLabel() {
+      return `在线(${this.userTotal.online})`;
+    },
+    offlineLabel() {
+      return `离线(${this.userTotal.offline})`;
+    },
+    silencedUserLabel() {
+      return `禁言(${this.userTotal.silenced})`;
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.restoreChatScrollPosition();
+    });
+    this.getEchartsTables();
+    // 添加滚动事件监听器
+    this.$nextTick(() => {
+      if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
+        this.$refs.manageRightRef.wrap.addEventListener('scroll', this.saveChatScrollPosition);
+      }
+    });
+    this.initScrollListeners();
+  },
+  beforeDestroy() {
+    this.saveTabScrollPositions()
+    // 移除滚动监听(避免内存泄漏)
+    const scrollRefs = {
+      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.removeEventListener('scroll', () =>
+          this.handleTabScroll(tabName, scrollEl)
+        );
+      }
+    })
+  },
+  // 使用 deactivated 和 activated 钩子替代 beforeDestroy 和 destroyed
+  deactivated() {
+    this.saveChatScrollPosition();
+  },
+  activated() {
+    this.$nextTick(() => {
+      this.restoreChatScrollPosition();
+    });
+    this.$nextTick(() => {
+      const video = this.$refs.videoPlayer;
+      if (video != null) {
+        this.initVideoPlayer(this.liveInfo.startTime)
+      }
+    })
+  },
+  methods: {
+    handleClickCoupon(){
+      this.$router.push({
+        name: 'LiveCoupon',
+        query: {
+          liveId: this.liveId
+        }
+      })
+    },
+    // 保存聊天滚动位置
+    saveChatScrollPosition() {
+      if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
+        this.chatScrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight;
+      }
+    },
+
+    // 恢复聊天滚动位置
+    restoreChatScrollPosition() {
+      if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
+        this.$refs.manageRightRef.wrap.scrollTop = this.chatScrollTop;
+      }
+    },
+    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
+
+          if(res.data.status == 4){
+            this.liveType = 3
+            this.videoUrl = res.data.videoUrl;
+          }else {
+            if (res.data.status != 2) {
+              this.$message.error("当前直播间未直播");
+              this.loading = false
+              return
+            }
+            if (res.data.liveType == 1) {
+              this.livingUrl = res.data.flvHlsUrl
+              this.livingUrl = this.livingUrl.replace("flv","m3u8")
+              this.$nextTick(() => {
+                this.initPlayer()
+              })
+              this.startTime = new Date(res.data.startTime).getTime()
+              this.processInterval = setInterval(this.updateLiveProgress, 1000);
+            } else {
+              this.liveType = 2
+              this.videoUrl = res.data.videoUrl;
+              this.$nextTick(() => {
+                this.initVideoPlayer(res.data.startTime)
+              })
+            }
+          }
+          this.loading = false
+        } else {
+          this.$message.error(res.msg)
+          this.loading = false
+        }
+        this.liveInfo = res.data
+      })
+    },
+    initVideoPlayer: function (startTime) {
+      const video = this.$refs.videoPlayer;
+
+      this.hls = new Hls();
+      this.hls.attachMedia(video);
+      this.hls.on(Hls.Events.MEDIA_ATTACHED, () => {
+        this.hls.loadSource(this.videoUrl);
+        this.hls.on(Hls.Events.STREAM_LOADED, (event, data) => {
+          video.play();
+        });
+      });
+      this.hls.on(Hls.Events.ERROR, (event, data) => {
+        console.error('HLS 错误:', data);
+      });
+      // 1. 初始化开播时间
+      startTime = new Date(startTime).getTime();
+      this.startTime = startTime;
+      // 2. 监听视频元数据加载完成(获取视频时长)
+      video.addEventListener('loadedmetadata', () => {
+        this.videoDuration = video.duration; // 获取视频时长(秒)
+
+        // 初始化视频播放位置
+        this.updateVideoPosition(video);
+
+        // 启动实时进度更新(每秒刷新一次)
+        this.processInterval = setInterval(this.updateProgress, 1000);
+      });
+
+    },
+    updateVideoPosition(video){
+      const currentTime = new Date().getTime(); // 当前时间戳(毫秒)
+      const elapsedTime = currentTime - this.startTime; // 已流逝时间(毫秒)
+
+      if (elapsedTime < 0) {
+        // 未开播:视频停在初始位置
+        video.currentTime = 0;
+        return;
+      }
+
+      // 已开播:计算视频循环后的位置(流逝时间 % 视频时长)
+      const elapsedSeconds = elapsedTime / 1000; // 转换为秒
+      const videoPosition = elapsedSeconds % this.videoDuration; // 视频内的播放位置(秒)
+
+      // 设置视频播放位置
+      video.currentTime = videoPosition;
+    },
+    updateProgress() {
+      const progressBar = this.$refs.progressBar;
+      const elapsedTimeEl = this.$refs.elapsedTime;
+      if (!this.videoDuration) return; // 视频时长未加载时不更新
+
+      const currentTime = new Date().getTime();
+      const elapsedTime = currentTime - this.startTime; // 总流逝时间(毫秒)
+
+      if (elapsedTime < 0) {
+        // 未开播状态
+        progressBar.style.width = '0%';
+        elapsedTimeEl.textContent = '00:00:00';
+        return;
+      }
+
+      // 计算进度百分比(基于视频循环)
+      const elapsedSeconds = elapsedTime / 1000;
+      const videoPosition = elapsedSeconds % this.videoDuration; // 当前在视频中的位置
+      const progressPercent = (videoPosition / this.videoDuration) * 100; // 进度百分比
+
+      // 更新进度条宽度
+      progressBar.style.width = `${progressPercent}%`;
+
+      // 格式化总流逝时间为“时:分:秒”并显示
+      elapsedTimeEl.textContent = this.formatTime(elapsedTime);
+    },
+    updateLiveProgress() {
+      const elapsedTimeEl = this.$refs.liveElapsedTime;
+
+      const currentTime = new Date().getTime();
+      const elapsedTime = currentTime - this.startTime; // 总流逝时间(毫秒)
+
+      if (elapsedTime < 0) {
+        elapsedTimeEl.textContent = '00:00:00';
+        return;
+      }
+
+
+      // 格式化总流逝时间为“时:分:秒”并显示
+      elapsedTimeEl.textContent = this.formatTime(elapsedTime);
+    },
+    formatTime(ms) {
+      const totalSeconds = Math.floor(ms / 1000);
+      const hours = Math.floor(totalSeconds / 3600);
+      const minutes = Math.floor((totalSeconds % 3600) / 60);
+      const seconds = totalSeconds % 60;
+
+      // 补零处理(确保两位数)
+      return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+    },
+    // ... 其他方法 ...
+    timeChange(val) {
+      this.searchQuery.timeOptions = val
+      this.getEchartsTables(this.searchQuery)
+      this.initChart()
+    },
+    timeGranularityChange(val) {
+      this.searchQuery.timeGranularity =  val
+      this.getEchartsTables()
+      this.initChart()
+    },
+    getEchartsTables() {
+      getLiveOrderTimeGranularity(this.searchQuery).then(res => {
+        if (res.code == 200) {
+          this.echartsXLine = res.hourlySlots
+          this.echartsXValue = res.hourlySlotsValue
+          this.initChart()
+        }
+      })
+    },
+    initChart() {
+      const chartDom = this.$refs.chartContainer;
+      this.myChart = echarts.init(chartDom);
+      const option = {
+        tooltip: {trigger: 'axis'},
+        legend: {data: ['订单数']},
+        xAxis: {type: 'category', boundaryGap: false, data: this.echartsXLine},
+        yAxis: {type: 'value'},
+        series: [
+          {name: '订单数', type: 'line', data: this.echartsXValue}
+        ],
+      };
+      this.myChart.setOption(option);
+    },
+
+    handleClickRed(){
+      this.$router.push({
+        name: 'LiveRedConf',
+        query: {
+          liveId: this.liveId
+        }
+      })
+    },
+    handleClickLottery(){
+      this.$router.push({
+        name: 'LiveLotteryConf',
+        query: {
+          liveId: this.liveId
+        }
+      })
+    },
+    handleClickGoods(){
+      this.$router.push({
+        name: 'LiveGoods',
+        query: {
+          liveId: this.liveId
+        }
+      })
+    },
+    handleClickOrder(){
+      this.$router.push({
+        name: 'LiveOrder',
+        query: {
+          liveId: this.liveId
+        }
+      })
+    },
+    initPlayer(){
+      var isUrl = this.livingUrl === null || this.livingUrl.trim() === ''
+      if (isUrl) {
+        console.error('直播地址为空,无法初始化播放器')
+        return
+      }
+      if (Hls.isSupported() && !isUrl) {
+        const videoElement = this.$refs.livingPlayer
+        if (!videoElement) {
+          console.error('找不到 video 元素')
+          return
+        }
+        this.hls = new Hls();
+        this.hls.attachMedia(videoElement);
+        this.hls.on(Hls.Events.MEDIA_ATTACHED, () => {
+          this.hls.loadSource(this.livingUrl);
+          this.hls.on(Hls.Events.STREAM_LOADED, (event, data) => {
+            videoElement.play();
+          });
+        });
+        this.hls.on(Hls.Events.ERROR, (event, data) => {
+          console.error('HLS 错误:', data);
+        });
+      } else {
+        console.error('浏览器不支持 HLS')
+      }
+    },
+    handleClick(tab) {
+      const tabName = tab.name;
+      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;
+          }
+        });
+      }
+    },
+    saveTabScrollPositions() {
+      this.tabScrollPositions = {
+        online: this.getScrollElement('online')?.scrollTop || 0,
+        offline: this.getScrollElement('offline')?.scrollTop || 0,
+        silenced: this.getScrollElement('silenced')?.scrollTop || 0
+      };
+    },
+    // 加载指定Tab的用户列表(核心加载逻辑)
+    loadNextPage(tabName) {
+      const params = this.pageParams[tabName];
+      const displayList = this[`${tabName}DisplayList`];
+      console.log(`加载 ${tabName} 用户列表`)
+      console.log(!params.hasMore || this.scrLoading[tabName].next)
+      console.log(params.currentPage)
+      // 若没有更多数据或正在加载,直接返回
+      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
+      };
+      // 调用接口加载对应状态的分页数据(需后端支持按状态筛选)
+      watchUserList(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
+      };
+      watchUserList(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;
+      });
+    },
+    // 辅助:获取Tab对应的滚动容器
+    getScrollElement(tabName) {
+      const scrollRefs = {
+        online: this.$refs.manageLeftRef_online,
+        offline: this.$refs.manageLeftRef_offline,
+        silenced: this.$refs.manageLeftRef_silenced
+      };
+      return scrollRefs[tabName]?.wrap;
+    },
+
+    getList() {
+      this.resetParams()
+      // this.loadUserList()
+      this.loadUserTotals(); // 先加载总人数
+      // this.handleClick('online')
+      this.handleClick({name:'online'})
+      this.loadMsgList()
+    },
+    loadUserTotals() {
+      if (!this.liveId) return;
+      // 假设后端提供一个接口返回总人数(如果没有,可通过首次加载全量数据后统计)
+      getLiveUserTotals({ liveId: this.liveId }).then(res => {
+        if (res.code === 200) {
+          this.userTotal = res.data; // { online, offline, silenced }
+        }
+      });
+    },
+    resetParams() {
+      // 重置各Tab的显示列表和分页参数
+      this.onlineDisplayList = [];
+      this.offlineDisplayList = [];
+      this.silencedDisplayList = [];
+      this.pageParams = {
+        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.msgList = [];
+      this.msgParams = {
+        pageNum: 1,
+        pageSize: 10,
+        liveId: this.liveId
+      };
+    },
+    loadUserList() {
+      if(this.liveId == null)  return
+      // 直播间用户
+      watchUserList({
+        liveId: this.liveId,
+        pageNum: this.userParams.pageNum,
+        pageSize: this.userParams.pageSize
+      }).then(response => {
+        let {code,rows,total} = response
+        if (code === 200) {
+          let totalPage = (total % this.userParams.pageSize == 0) ? Math.floor(total / this.userParams.pageSize) : Math.floor(total / this.userParams.pageSize + 1);
+          rows.forEach(row => {
+            if (!this.userList.some(u => u.userId === row.userId)) {
+              this.userList.push(row)
+            }
+          })
+
+          // 没加载完继续加载
+          if (this.userParams.pageNum < totalPage) {
+            this.userParams.pageNum = parseInt(this.userParams.pageNum) + 1;
+            this.loadUserList()
+          }
+        }
+      })
+    },
+    loadMsgList() {
+      // 直播间消息
+      listLiveMsg({
+        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.userList.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)
+                })
+              }
+            })
+
+            // 没加载完继续加载
+            if (this.msgParams.pageNum < this.loadMsgMaxPage) {
+              this.msgParams.pageNum = parseInt(this.msgParams.pageNum) + 1;
+              this.loadMsgList()
+            }
+
+            // 同步更新消息列表中相同用户的状态
+            this.userList.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)
+      })
+    },
+    manageRightScroll() {
+      this.saveChatScrollPosition();
+    },
+    blockUser(u){
+      this.$confirm('是否确认封禁用户账号为:"' + u.nickName + '-' + u.userId + '"?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return blockUser(u.userId);
+      }).then(() => {
+        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(() => {});
+    },
+    changeUserState(u) {
+      // 修改状态
+      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;
+            }
+          });
+
+          this.userList.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("操作失败");
+      })
+    },
+
+    // 新增:重新筛选所有Tab的显示列表
+    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;
+        }
+      }
+    },
+    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)
+    },
+    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.userList.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') {
+
+        }
+      }
+    },
+    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 = '';
+    },
+    // 初始化滚动监听(在mounted中调用)
+    initScrollListeners() {
+      // 为每个Tab的滚动容器添加监听
+      this.$nextTick(() => {
+        const scrollRefs = {
+          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);
+      }
+    },
+  },
+  destroyed() {
+    this.hls?.destroy();
+    clearInterval(this.processInterval)
+  }
+}
+</script>
+
+<style scoped>
+.talk-list{
+  display: flex;
+}
+  .live-console {
+    width: 100vw;
+
+    background-color: #f5f4f4;
+  }
+  .live-console .live-console-col {
+    height: 100vh;
+    margin-left: 5px;
+
+    background-color: white;
+    border-radius: 4px;
+  }
+  /*隐藏水平滚动条*/
+  ::v-deep .el-scrollbar__wrap {
+    overflow-x: hidden;
+  }
+  /* 消息输入区域 */
+  .chat-input {
+    display: flex;
+    padding: 10px;
+    border-top: 1px solid #ebeef5;
+    background-color: #fff;
+    min-height: 120px;
+  }
+
+  .chat-input .el-input {
+    flex: 1;
+    margin-right: 10px;
+  }
+
+  .chat-input .el-textarea__inner {
+    resize: none;
+    min-height: 100px;
+  }
+  ::v-deep .el-textarea__inner {
+    border: none !important;
+    box-shadow: none !important;
+    resize: none !important;
+  }
+  ::v-deep .el-textarea__inner:focus {
+    border: none !important;
+    box-shadow: none !important;
+  }
+::v-deep .el-tabs__item {
+  padding: 0;
+}
+.live-console-tab-left ::v-deep .el-tabs__active-bar {
+  width: 41px!important;
+  margin-left: calc((100% / 1 - 41px) / 2);
+  height: 4px;
+  border-radius: 4px;
+}
+
+/* calc 3是tab数量 */
+.live-console-tab-right ::v-deep .el-tabs__active-bar {
+  width: 51px!important;
+  margin-left: calc((100% / 3 - 51px) / 2);
+  height: 4px;
+  border-radius: 4px;
+}
+.custom-video {
+  pointer-events: none !important; /* 完全禁止鼠标交互,避免悬停时显示工具栏 */
+  outline: none !important; /* 移除焦点轮廓 */
+}
+/* 额外的兼容性隐藏 */
+.custom-video::-webkit-media-controls {
+  display: none !important;
+}
+
+.custom-video::-webkit-media-controls-panel {
+  display: none !important;
+}
+/* 进度条容器 */
+.progress-container {
+  width: 100%;
+  height: 6px;
+  background: #eee;
+  border-radius: 3px;
+  margin: 10px 0;
+  cursor: pointer;
+}
+
+/* 进度条填充部分 */
+.progress-bar {
+  height: 100%;
+  background: #42b983;  /* Vue 绿色主题示例 */
+  border-radius: 3px;
+  width: 0%;  /* 初始进度为 0 */
+}
+
+/* 时间显示样式 */
+.time-display {
+  color: #ffffff;
+  font-size: 14px;
+}
+</style>

+ 34 - 1316
src/views/live/liveConsole/index.vue

@@ -1,1339 +1,57 @@
 <template>
-  <!-- 直播中控台 start -->
-  <el-row type="flex" justify="center" class="live-console" :gutter="10" v-loading="loading">>
-    <!-- 聊天 start -->
-    <el-col class="live-console-col" :span="6">
-      <el-tabs class="live-console-tab-left" v-model="tabRight.activeName" @tab-click="handleClick" :stretch="true">
-        <el-tab-pane label="讨论" name="talk">
-          <el-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>
-                    </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 style="padding: 10px; border-top: 1px solid #ebeef5; background-color: #fff; min-height: 120px;">
-            <el-input
-              type="textarea"
-              v-model="newMsg"
-              placeholder="请输入消息..."
-              :rows="8"
-              @keyup.enter.native="sendMessage"
-              clearable
-              resize="none"
-              style="flex: 1; margin-right: 10px;"
-            >
-            </el-input>
-            <div style="display: flex; justify-content: flex-end; margin-top: 10px;">
-              <el-button plain @click="sendMessage">发送</el-button>
-            </div>
-          </div>
-        </el-tab-pane>
-      </el-tabs>
-    </el-col>
-    <!-- 聊天 end -->
-
-
-    <!-- 直播/视频 start -->
-    <el-col class="live-console-col" :span="12">
-      <div style="background: #000; border-radius: 5px; overflow: hidden; margin: 10px 5px;">
-        <div style="border-radius: 5px; overflow: hidden;" v-if="!isAudit">
-          <img :src="require('@/assets/images/videoIsAudit.png')" style="width: 100%; height: 45vh;">
-        </div>
-        <div style="border-radius: 5px; overflow: hidden;" v-else-if="status != 2 && status != 4">
-          <img :src="require('@/assets/images/videoNotStart.png')" style="width: 100%; height: 45vh;">
-        </div>
-        <div style="border-radius: 5px; overflow: hidden;" v-else-if="liveType == 1">
-          <video
-            controls
-            ref="livingPlayer"
-            autoplay
-            @click.prevent
-            @contextmenu.prevent
-            class="custom-video"
-            width="100%"
-            style="display: block; background: #000; height: 45vh;"
-          ></video>
-
-          <!-- 时间显示(可选) -->
-          <div ref="liveElapsedTime" class="time-display">
-            已播放:<span id="liveElapsedTime">00:00:00</span>
-          </div>
-        </div>
-        <div style="border-radius: 5px; overflow: hidden;" v-else-if="liveType == 2">
-          <video
-            controls
-            ref="videoPlayer"
-            loop
-            autoplay
-            width="100%"
-            muted
-            playsinline
-            @click.prevent
-            @contextmenu.prevent
-            class="custom-video"
-            style="display: block; background: #000; height: 40vh;"
-          >
-            <source :src="videoUrl" type="application/x-mpegURL">
-          </video>
-          <!-- 自定义进度条容器 -->
-          <div ref="progressBar" class="progress-container">
-            <div id="progressBar" class="progress-bar"></div>
-          </div>
-
-          <!-- 时间显示(可选) -->
-          <div ref="elapsedTime" class="time-display">
-            已播放:<span id="elapsedTime">00:00:00</span>
-          </div>
-        </div>
-        <div style="border-radius: 5px; overflow: hidden;" v-else-if="liveType == 3">
-          <video
-            controls
-            ref="liveReplay"
-            loop
-            autoplay
-            width="100%"
-            playsinline
-            style="display: block; background: #000; height: 40vh;"
-          >
-            <source :src="videoUrl" type="application/x-mpegURL">
-          </video>
-        </div>
-        <div style="border-radius: 5px; overflow: hidden;" v-else>
-          <img :src="require('@/assets/images/videoNotStart.png')" style="width: 100%; height: 45vh;">
-        </div>
-      </div>
-      <!-- 底部导航栏 -->
-      <div style="display: flex; justify-content: space-around; padding: 15px 0; background: #fff; border-top: 1px solid #f0f0f0;">
-        <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickRed">
-          <i class="el-icon-money" style="font-size: 20px;"></i>
-          <span style="font-size: 12px; margin-top: 4px;">红包配置</span>
-        </div>
-        <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickLottery">
-          <i class="el-icon-present" style="font-size: 20px;"></i>
-          <span style="font-size: 12px; margin-top: 4px;">抽奖配置</span>
-        </div>
-        <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickGoods">
-          <i class="el-icon-goods" style="font-size: 20px;"></i>
-          <span style="font-size: 12px; margin-top: 4px;">商品</span>
-        </div>
-        <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickOrder">
-          <i class="el-icon-goods" style="font-size: 20px;"></i>
-          <span style="font-size: 12px; margin-top: 4px;">直播订单</span>
-        </div>
-        <div style="display: flex; flex-direction: column; align-items: center; cursor: pointer;" @click="handleClickCoupon">
-          <i class="el-icon-goods" style="font-size: 20px;"></i>
-          <span style="font-size: 12px; margin-top: 4px;">直播优惠券</span>
-        </div>
-      </div>
-      <el-radio-group v-model="tableRadio" >
-        <el-radio-button label="订单数">订单数</el-radio-button>
-      </el-radio-group>
-      <div  style="position: relative;width: 100%; height: 300px;">
-        <div ref="chartContainer" style="width: 100%; height: 100%;"></div>
-        <div style="position: absolute; top: 10px; right: 10px; background: #fff; padding: 5px; z-index: 1;">
-          <el-select v-model="searchQuery.timeOptions" placeholder="请选择" style="width: 150px"  @change="timeChange">
-            <el-option
-              v-for="item in timeOptions"
-              :key="item.value"
-              :label="item.label"
-              :value="item.value">
-            </el-option>
-          </el-select>
-          <el-select v-model="searchQuery.timeGranularity" placeholder="请选择" style="width: 150px"  @change="timeGranularityChange">
-            <el-option
-              v-for="item in timeGranularity"
-              :key="item.value"
-              :label="item.label"
-              :value="item.value">
-            </el-option>
-          </el-select>
-<!--          <el-button type="primary" @click="applyFilter">搜索</el-button>-->
-        </div>
-      </div>
-    </el-col>
-    <!-- 直播/视频 end -->
-
-    <!-- 用户列表 start -->
-    <el-col class="live-console-col" :span="6">
-      <el-tabs class="live-console-tab-right" v-model="tabLeft.activeName" @tab-click="handleClick" :stretch="true">
-        <el-tab-pane :label="onlineLabel" name="online">
-          <el-scrollbar ref="manageLeftRef_online" style="height: 800px; width: 100%;">
-            <el-row style="margin-top: 10px" type="flex" align="middle" 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 ref="manageLeftRef_offline" style="height: 800px; width: 100%;">
-            <el-row style="margin-top: 10px" type="flex" align="middle" 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 ref="manageLeftRef_silenced" style="height: 800px; width: 100%;">
-            <el-row style="margin-top: 10px" type="flex" align="middle" 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>
-    </el-col>
-    <!-- 用户列表 end -->
-  </el-row>
-  <!-- 直播中控台  end -->
+  <div id="app">
+    <div class="nav">
+      <button @click="currentView = 'dashboard'">实时大屏</button>
+      <button @click="currentView = 'console'">中控台</button>
+    </div>
+    <component :is="currentView" :liveId="liveId"></component>
+  </div>
 </template>
 
 <script>
-import {blockUser, changeUserStatus, getLiveUserTotals, watchUserList} from '@/api/live/liveWatchUser'
-import { getLiveVideoByLiveId } from '@/api/live/liveVideo'
-import {getLivingUrl, getLive, delLive} from '@/api/live/live'
-import { getLiveOrderTimeGranularity } from '@/api/live/liveOrder'
-import { listLiveMsg } from '@/api/live/liveMsg'
-import Hls from 'hls.js';
-import {onBeforeUnmount} from 'vue';
-import LiveLotteryConf from '@/views/live/liveConfig/liveLotteryConf.vue'
-import LiveRedConf from '@/views/live/liveConfig/liveRedConf.vue'
-import LiveGoods from '@/views/live/liveConfig/goods.vue'
-import LiveOrder from '@/views/live/liveOrder/index.vue'
-import LiveCoupon from '@/views/live/liveConfig/liveCoupon.vue'
-import echarts from 'echarts'
-
+import LiveDashboard from './LiveDashboard.vue';
+import LiveConsole from './LiveConsole.vue';
 
 export default {
-  name: "LiveConsole",
-  components: { LiveLotteryConf,LiveRedConf,LiveGoods },
+  name: 'LiveConsole',
+  components: {
+    dashboard: LiveDashboard,
+    console: LiveConsole
+  },
   data() {
     return {
-      loading: true,
-      tabLeft: {
-        activeName: "online",
-      },
-      tabRight: {
-        activeName: "talk",
-      },
-      livingUrl: "",
-      videoUrl: "",
-      status: 0,
-      loadMsgMaxPage: 2,
-      his: null,
-      liveVideo: {},
-      socket: null,
-      liveWsUrl: process.env.VUE_APP_LIVE_WS_URL + '/app/webSocket',
-      userParams:{
-        pageNum: 1,
-        pageSize: 10,
-        liveId: null
-      },
-      msgParams: {
-        pageNum: 1,
-        pageSize: 10,
-        liveId: null
-      },
-      userList: [],
-      msgList: [],
-      newMsg: '',
-      isAudit: false,
-      myChart: null, // 用于存储 ECharts 实例
-      liveType: 1,
-      tableRadio: '订单数',
-      // ... 其他数据 ...
-      searchQuery: {timeOptions:'2',timeGranularity:'10',liveId: null}, // 搜索查询条件
-      timeOptions: [
-        {value:'2',label:'最近2小时',key:'2'},
-        {value:'4',label:'最近4小时',key:'4'},
-        {value:'all',label:'全场',key:'all'},
-      ],
-      timeGranularity: [
-        {value:'10',label:'10分钟',key:'10'},
-        {value:'30',label:'30分钟',key:'30'},
-        {value:'60',label:'1小时',key:'60'},
-      ],
-      videoDuration: 0,
-      startTime: null,
-      processInterval: null,
-      // ... 其他数据
-      chatScrollTop: 0, // 保存聊天滚动位置,
-      liveId: null,
-      userTotal: {
-        online: 0,       // 在线总人数
-        offline: 0,      // 离线总人数
-        silenced: 0      // 禁言总人数
-      },
-      // 各Tab的显示列表(仅存储当前需要展示的数据)
-      onlineDisplayList: [],    // 在线用户显示列表
-      offlineDisplayList: [],   // 离线用户显示列表
-      silencedDisplayList: [],  // 禁言用户显示列表
-      // 各Tab的分页参数
-      pageParams: {
-        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: {
-        online: { next: false, prev: false },
-        offline: { next: false, prev: false },
-        silenced: { next: false, prev: false }
-      }
-    }
-  },
-  created() {
-    if (this.$route.params.liveId) {
-      this.liveId = this.$route.params.liveId;
-    }else {
-      this.liveId = this.$route.query.liveId;
-    }
-    // this.getLiveVideo()
-    this.getList()
-    this.connectWebSocket()
-    this.getLive()
-    this.searchQuery.liveId = this.liveId
-  },
-  computed: {
-    userId() {
-      return this.$store.state.user.user.userId
-    },
-    companyId() {
-      return this.$store.state.user.user.companyId
-    },
-    onlineLabel() {
-      return `在线(${this.userTotal.online})`;
-    },
-    offlineLabel() {
-      return `离线(${this.userTotal.offline})`;
-    },
-    silencedUserLabel() {
-      return `禁言(${this.userTotal.silenced})`;
-    }
-  },
-  mounted() {
-    this.$nextTick(() => {
-      this.restoreChatScrollPosition();
-    });
-    this.getEchartsTables();
-    // 添加滚动事件监听器
-    this.$nextTick(() => {
-      if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
-        this.$refs.manageRightRef.wrap.addEventListener('scroll', this.saveChatScrollPosition);
-      }
-    });
-    this.initScrollListeners();
-  },
-  beforeDestroy() {
-    this.saveTabScrollPositions()
-    // 移除滚动监听(避免内存泄漏)
-    const scrollRefs = {
-      online: this.$refs.manageLeftRef_online,
-      offline: this.$refs.manageLeftRef_offline,
-      silenced: this.$refs.manageLeftRef_silenced
+      liveId: this.$route.params.liveId,
+      currentView: 'dashboard'
     };
-    Object.keys(scrollRefs).forEach(tabName => {
-      const scrollEl = scrollRefs[tabName]?.wrap;
-      if (scrollEl) {
-        scrollEl.removeEventListener('scroll', () =>
-          this.handleTabScroll(tabName, scrollEl)
-        );
-      }
-    })
-  },
-  // 使用 deactivated 和 activated 钩子替代 beforeDestroy 和 destroyed
-  deactivated() {
-    this.saveChatScrollPosition();
   },
-  activated() {
-    this.$nextTick(() => {
-      this.restoreChatScrollPosition();
-    });
-    this.$nextTick(() => {
-      const video = this.$refs.videoPlayer;
-      if (video != null) {
-        this.initVideoPlayer(this.liveInfo.startTime)
-      }
-    })
-  },
-  methods: {
-    handleClickCoupon(){
-      this.$router.push({
-        name: 'LiveCoupon',
-        query: {
-          liveId: this.liveId
-        }
-      })
-    },
-    // 保存聊天滚动位置
-    saveChatScrollPosition() {
-      if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
-        this.chatScrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight;
-      }
-    },
-
-    // 恢复聊天滚动位置
-    restoreChatScrollPosition() {
-      if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
-        this.$refs.manageRightRef.wrap.scrollTop = this.chatScrollTop;
-      }
-    },
-    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
-
-          if(res.data.status == 4){
-            this.liveType = 3
-            this.videoUrl = res.data.videoUrl;
-          }else {
-            if (res.data.status != 2) {
-              this.$message.error("当前直播间未直播");
-              this.loading = false
-              return
-            }
-            if (res.data.liveType == 1) {
-              this.livingUrl = res.data.flvHlsUrl
-              this.livingUrl = this.livingUrl.replace("flv","m3u8")
-              this.$nextTick(() => {
-                this.initPlayer()
-              })
-              this.startTime = new Date(res.data.startTime).getTime()
-              this.processInterval = setInterval(this.updateLiveProgress, 1000);
-            } else {
-              this.liveType = 2
-              this.videoUrl = res.data.videoUrl;
-              this.$nextTick(() => {
-                this.initVideoPlayer(res.data.startTime)
-              })
-            }
-          }
-          this.loading = false
-        } else {
-          this.$message.error(res.msg)
-          this.loading = false
-        }
-        this.liveInfo = res.data
-      })
-    },
-    initVideoPlayer: function (startTime) {
-      const video = this.$refs.videoPlayer;
-
-      this.hls = new Hls();
-      this.hls.attachMedia(video);
-      this.hls.on(Hls.Events.MEDIA_ATTACHED, () => {
-        this.hls.loadSource(this.videoUrl);
-        this.hls.on(Hls.Events.STREAM_LOADED, (event, data) => {
-          video.play();
-        });
-      });
-      this.hls.on(Hls.Events.ERROR, (event, data) => {
-        console.error('HLS 错误:', data);
-      });
-      // 1. 初始化开播时间
-      startTime = new Date(startTime).getTime();
-      this.startTime = startTime;
-      // 2. 监听视频元数据加载完成(获取视频时长)
-      video.addEventListener('loadedmetadata', () => {
-        this.videoDuration = video.duration; // 获取视频时长(秒)
-
-        // 初始化视频播放位置
-        this.updateVideoPosition(video);
-
-        // 启动实时进度更新(每秒刷新一次)
-        this.processInterval = setInterval(this.updateProgress, 1000);
-      });
-
-    },
-    updateVideoPosition(video){
-      const currentTime = new Date().getTime(); // 当前时间戳(毫秒)
-      const elapsedTime = currentTime - this.startTime; // 已流逝时间(毫秒)
-
-      if (elapsedTime < 0) {
-        // 未开播:视频停在初始位置
-        video.currentTime = 0;
-        return;
-      }
-
-      // 已开播:计算视频循环后的位置(流逝时间 % 视频时长)
-      const elapsedSeconds = elapsedTime / 1000; // 转换为秒
-      const videoPosition = elapsedSeconds % this.videoDuration; // 视频内的播放位置(秒)
-
-      // 设置视频播放位置
-      video.currentTime = videoPosition;
-    },
-    updateProgress() {
-      const progressBar = this.$refs.progressBar;
-      const elapsedTimeEl = this.$refs.elapsedTime;
-      if (!this.videoDuration) return; // 视频时长未加载时不更新
-
-      const currentTime = new Date().getTime();
-      const elapsedTime = currentTime - this.startTime; // 总流逝时间(毫秒)
-
-      if (elapsedTime < 0) {
-        // 未开播状态
-        progressBar.style.width = '0%';
-        elapsedTimeEl.textContent = '00:00:00';
-        return;
-      }
-
-      // 计算进度百分比(基于视频循环)
-      const elapsedSeconds = elapsedTime / 1000;
-      const videoPosition = elapsedSeconds % this.videoDuration; // 当前在视频中的位置
-      const progressPercent = (videoPosition / this.videoDuration) * 100; // 进度百分比
-
-      // 更新进度条宽度
-      progressBar.style.width = `${progressPercent}%`;
-
-      // 格式化总流逝时间为“时:分:秒”并显示
-      elapsedTimeEl.textContent = this.formatTime(elapsedTime);
-    },
-    updateLiveProgress() {
-      const elapsedTimeEl = this.$refs.liveElapsedTime;
-
-      const currentTime = new Date().getTime();
-      const elapsedTime = currentTime - this.startTime; // 总流逝时间(毫秒)
-
-      if (elapsedTime < 0) {
-        elapsedTimeEl.textContent = '00:00:00';
-        return;
-      }
-
-
-      // 格式化总流逝时间为“时:分:秒”并显示
-      elapsedTimeEl.textContent = this.formatTime(elapsedTime);
-    },
-    formatTime(ms) {
-      const totalSeconds = Math.floor(ms / 1000);
-      const hours = Math.floor(totalSeconds / 3600);
-      const minutes = Math.floor((totalSeconds % 3600) / 60);
-      const seconds = totalSeconds % 60;
-
-      // 补零处理(确保两位数)
-      return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
-    },
-    // ... 其他方法 ...
-    timeChange(val) {
-      this.searchQuery.timeOptions = val
-      this.getEchartsTables(this.searchQuery)
-      this.initChart()
-    },
-    timeGranularityChange(val) {
-      this.searchQuery.timeGranularity =  val
-      this.getEchartsTables()
-      this.initChart()
-    },
-    getEchartsTables() {
-      getLiveOrderTimeGranularity(this.searchQuery).then(res => {
-        if (res.code == 200) {
-          this.echartsXLine = res.hourlySlots
-          this.echartsXValue = res.hourlySlotsValue
-          this.initChart()
-        }
-      })
-    },
-    initChart() {
-      const chartDom = this.$refs.chartContainer;
-      this.myChart = echarts.init(chartDom);
-      const option = {
-        tooltip: {trigger: 'axis'},
-        legend: {data: ['订单数']},
-        xAxis: {type: 'category', boundaryGap: false, data: this.echartsXLine},
-        yAxis: {type: 'value'},
-        series: [
-          {name: '订单数', type: 'line', data: this.echartsXValue}
-        ],
-      };
-      this.myChart.setOption(option);
-    },
-
-    handleClickRed(){
-      this.$router.push({
-        name: 'LiveRedConf',
-        query: {
-          liveId: this.liveId
-        }
-      })
-    },
-    handleClickLottery(){
-      this.$router.push({
-        name: 'LiveLotteryConf',
-        query: {
-          liveId: this.liveId
-        }
-      })
-    },
-    handleClickGoods(){
-      this.$router.push({
-        name: 'LiveGoods',
-        query: {
-          liveId: this.liveId
-        }
-      })
-    },
-    handleClickOrder(){
-      this.$router.push({
-        name: 'LiveOrder',
-        query: {
-          liveId: this.liveId
-        }
-      })
-    },
-    initPlayer(){
-      var isUrl = this.livingUrl === null || this.livingUrl.trim() === ''
-      if (isUrl) {
-        console.error('直播地址为空,无法初始化播放器')
-        return
-      }
-      if (Hls.isSupported() && !isUrl) {
-        const videoElement = this.$refs.livingPlayer
-        if (!videoElement) {
-          console.error('找不到 video 元素')
-          return
-        }
-        this.hls = new Hls();
-        this.hls.attachMedia(videoElement);
-        this.hls.on(Hls.Events.MEDIA_ATTACHED, () => {
-          this.hls.loadSource(this.livingUrl);
-          this.hls.on(Hls.Events.STREAM_LOADED, (event, data) => {
-            videoElement.play();
-          });
-        });
-        this.hls.on(Hls.Events.ERROR, (event, data) => {
-          console.error('HLS 错误:', data);
-        });
-      } else {
-        console.error('浏览器不支持 HLS')
-      }
-    },
-    handleClick(tab) {
-      const tabName = tab.name;
-      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;
-          }
-        });
-      }
-    },
-    saveTabScrollPositions() {
-      this.tabScrollPositions = {
-        online: this.getScrollElement('online')?.scrollTop || 0,
-        offline: this.getScrollElement('offline')?.scrollTop || 0,
-        silenced: this.getScrollElement('silenced')?.scrollTop || 0
-      };
-    },
-    // 加载指定Tab的用户列表(核心加载逻辑)
-    loadNextPage(tabName) {
-      const params = this.pageParams[tabName];
-      const displayList = this[`${tabName}DisplayList`];
-      console.log(`加载 ${tabName} 用户列表`)
-      console.log(!params.hasMore || this.scrLoading[tabName].next)
-      console.log(params.currentPage)
-      // 若没有更多数据或正在加载,直接返回
-      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
-      };
-      // 调用接口加载对应状态的分页数据(需后端支持按状态筛选)
-      watchUserList(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
-      };
-      watchUserList(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;
-      });
-    },
-    // 辅助:获取Tab对应的滚动容器
-    getScrollElement(tabName) {
-      const scrollRefs = {
-        online: this.$refs.manageLeftRef_online,
-        offline: this.$refs.manageLeftRef_offline,
-        silenced: this.$refs.manageLeftRef_silenced
-      };
-      return scrollRefs[tabName]?.wrap;
-    },
-
-    getList() {
-      this.resetParams()
-      // this.loadUserList()
-      this.loadUserTotals(); // 先加载总人数
-      // this.handleClick('online')
-      this.handleClick({name:'online'})
-      this.loadMsgList()
-    },
-    loadUserTotals() {
-      if (!this.liveId) return;
-      // 假设后端提供一个接口返回总人数(如果没有,可通过首次加载全量数据后统计)
-      getLiveUserTotals({ liveId: this.liveId }).then(res => {
-        if (res.code === 200) {
-          this.userTotal = res.data; // { online, offline, silenced }
-        }
-      });
-    },
-    resetParams() {
-      // 重置各Tab的显示列表和分页参数
-      this.onlineDisplayList = [];
-      this.offlineDisplayList = [];
-      this.silencedDisplayList = [];
-      this.pageParams = {
-        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.msgList = [];
-      this.msgParams = {
-        pageNum: 1,
-        pageSize: 10,
-        liveId: this.liveId
-      };
-    },
-    loadUserList() {
-      if(this.liveId == null)  return
-      // 直播间用户
-      watchUserList({
-        liveId: this.liveId,
-        pageNum: this.userParams.pageNum,
-        pageSize: this.userParams.pageSize
-      }).then(response => {
-        let {code,rows,total} = response
-        if (code === 200) {
-          let totalPage = (total % this.userParams.pageSize == 0) ? Math.floor(total / this.userParams.pageSize) : Math.floor(total / this.userParams.pageSize + 1);
-          rows.forEach(row => {
-            if (!this.userList.some(u => u.userId === row.userId)) {
-              this.userList.push(row)
-            }
-          })
-
-          // 没加载完继续加载
-          if (this.userParams.pageNum < totalPage) {
-            this.userParams.pageNum = parseInt(this.userParams.pageNum) + 1;
-            this.loadUserList()
-          }
-        }
-      })
-    },
-    loadMsgList() {
-      // 直播间消息
-      listLiveMsg({
-        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.userList.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)
-                })
-              }
-            })
-
-            // 没加载完继续加载
-            if (this.msgParams.pageNum < this.loadMsgMaxPage) {
-              this.msgParams.pageNum = parseInt(this.msgParams.pageNum) + 1;
-              this.loadMsgList()
-            }
-
-            // 同步更新消息列表中相同用户的状态
-            this.userList.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)
-      })
-    },
-    manageRightScroll() {
-      this.saveChatScrollPosition();
-    },
-    blockUser(u){
-      this.$confirm('是否确认封禁用户账号为:"' + u.nickName + '-' + u.userId + '"?', "警告", {
-        confirmButtonText: "确定",
-        cancelButtonText: "取消",
-        type: "warning"
-      }).then(function() {
-        return blockUser(u.userId);
-      }).then(() => {
-        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(() => {});
-    },
-    changeUserState(u) {
-      // 修改状态
-      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;
-            }
-          });
-
-          this.userList.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("操作失败");
-      })
-    },
-
-    // 新增:重新筛选所有Tab的显示列表
-    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;
-        }
-      }
-    },
-    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)
-    },
-    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.userList.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') {
-
-        }
-      }
-    },
-    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 = '';
-    },
-    // 初始化滚动监听(在mounted中调用)
-    initScrollListeners() {
-      // 为每个Tab的滚动容器添加监听
-      this.$nextTick(() => {
-        const scrollRefs = {
-          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);
-      }
+  created() {
 
-      // 加载上一页(滚动到顶部附近)
-      if (scrollTop < topThreshold) {
-        this.loadPrevPage(tabName);
-      }
-    },
-  },
-  destroyed() {
-    this.hls?.destroy();
-    clearInterval(this.processInterval)
   }
-}
+};
 </script>
 
-<style scoped>
-.talk-list{
-  display: flex;
-}
-  .live-console {
-    width: 90vw;
-    padding: 10px 0;
-    background-color: #f5f4f4;
-  }
-  .live-console .live-console-col {
-    height: 88vh;
-    margin-left: 5px;
-    padding: 0 10px;
-    background-color: white;
-    border-radius: 4px;
-  }
-  /*隐藏水平滚动条*/
-  ::v-deep .el-scrollbar__wrap {
-    overflow-x: hidden;
-  }
-  /* 消息输入区域 */
-  .chat-input {
-    display: flex;
-    padding: 10px;
-    border-top: 1px solid #ebeef5;
-    background-color: #fff;
-    min-height: 120px;
-  }
-
-  .chat-input .el-input {
-    flex: 1;
-    margin-right: 10px;
-  }
-
-  .chat-input .el-textarea__inner {
-    resize: none;
-    min-height: 100px;
-  }
-  ::v-deep .el-textarea__inner {
-    border: none !important;
-    box-shadow: none !important;
-    resize: none !important;
-  }
-  ::v-deep .el-textarea__inner:focus {
-    border: none !important;
-    box-shadow: none !important;
-  }
-::v-deep .el-tabs__item {
-  padding: 0;
-}
-.live-console-tab-left ::v-deep .el-tabs__active-bar {
-  width: 41px!important;
-  margin-left: calc((100% / 1 - 41px) / 2);
-  height: 4px;
-  border-radius: 4px;
+<style>
+body {
+  margin: 0;
+  font-family: 'Arial', sans-serif;
 }
 
-/* calc 3是tab数量 */
-.live-console-tab-right ::v-deep .el-tabs__active-bar {
-  width: 51px!important;
-  margin-left: calc((100% / 3 - 51px) / 2);
-  height: 4px;
-  border-radius: 4px;
-}
-.custom-video {
-  pointer-events: none !important; /* 完全禁止鼠标交互,避免悬停时显示工具栏 */
-  outline: none !important; /* 移除焦点轮廓 */
-}
-/* 额外的兼容性隐藏 */
-.custom-video::-webkit-media-controls {
-  display: none !important;
+.nav {
+  display: flex;
+  background: #1e3a8a;
+  color: white;
 }
 
-.custom-video::-webkit-media-controls-panel {
-  display: none !important;
-}
-/* 进度条容器 */
-.progress-container {
-  width: 100%;
-  height: 6px;
-  background: #eee;
-  border-radius: 3px;
-  margin: 10px 0;
+.nav button {
+  padding: 15px 20px;
+  border: none;
+  background: none;
+  color: white;
   cursor: pointer;
+  font-size: 16px;
 }
 
-/* 进度条填充部分 */
-.progress-bar {
-  height: 100%;
-  background: #42b983;  /* Vue 绿色主题示例 */
-  border-radius: 3px;
-  width: 0%;  /* 初始进度为 0 */
-}
-
-/* 时间显示样式 */
-.time-display {
-  color: #ffffff;
-  font-size: 14px;
+.nav button:hover {
+  background: #3b82f6;
 }
 </style>