Browse Source

直播新功能修改

yuhongqi 3 days ago
parent
commit
d2102049c8

+ 10 - 1
src/api/live/liveMsg.js

@@ -9,6 +9,15 @@ export function listLiveMsg(query) {
   })
 }
 
+// 查询直播讨论列表
+export function listLiveSingleMsg(query) {
+  return request({
+    url: '/live/liveMsg/singleList',
+    method: 'get',
+    params: query
+  })
+}
+
 // 查询直播讨论详细
 export function getLiveMsg(msgId) {
   return request({
@@ -50,4 +59,4 @@ export function exportLiveMsg(query) {
     method: 'get',
     params: query
   })
-}
+}

+ 9 - 0
src/api/live/task.js

@@ -75,3 +75,12 @@ export function importTemplate() {
     method: 'get'
   })
 }
+
+// 新直播控制台任务列表
+export function consoleList(query) {
+  return request({
+    url: '/live/task/consoleList',
+    method: 'get',
+    params: query
+  })
+}

+ 627 - 54
src/views/live/liveConsole/LiveConsole.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="console">
+
     <div class="left-panel">
       <h2>学员列表</h2>
       <div class="search">
@@ -8,8 +9,8 @@
       </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-scrollbar class="custom-scrollbar" ref="manageLeftRef_al" style="height: 800px; width: 100%;">
+            <el-row class='scrollbar-demo-item' v-for="u in alDisplayList" :key="u.userId">
               <el-col :span="20">
                 <el-row type="flex" align="middle">
                   <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
@@ -30,8 +31,8 @@
           </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-scrollbar class="custom-scrollbar" ref="manageLeftRef_online" style="height: 800px; width: 100%;">
+            <el-row class='scrollbar-demo-item' v-for="u in onlineDisplayList" :key="u.userId">
               <el-col :span="20">
                 <el-row type="flex" align="middle">
                   <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
@@ -52,8 +53,8 @@
           </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-scrollbar class="custom-scrollbar" ref="manageLeftRef_offline" style="height: 800px; width: 100%;">
+            <el-row class='scrollbar-demo-item' v-for="u in offlineDisplayList" :key="u.userId">
               <el-col :span="20">
                 <el-row type="flex" align="middle">
                   <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
@@ -74,8 +75,8 @@
           </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-scrollbar class="custom-scrollbar" ref="manageLeftRef_silenced" style="height: 800px; width: 100%;">
+            <el-row class='scrollbar-demo-item' v-for="u in silencedDisplayList" :key="u.userId">
               <el-col :span="20">
                 <el-row type="flex" align="middle">
                   <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
@@ -100,39 +101,74 @@
 
     <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">
-            全局用户
+            <input type="checkbox" v-model="globalVisible" @change="globalVisibleChange">
+            全局用户自见
           </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>
+        <el-scrollbar class="custom-scrollbar" style="height: 500px; width: 100%;" ref="manageRightRef">
+          <el-row v-for="m in msgList" >
+            <el-row v-if="m.userId !== userId" style="margin-top: 5px" type="flex" align="top" >
+              <el-col :span="3" style="margin-left: 10px"><el-avatar :src="m.avatar"/></el-col>
+              <el-col :span="15">
+                <el-row style="margin-left: 10px">
+                  <el-col><div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div></el-col>
+                  <el-col :span="24" style="max-width: 200px;">
+                    <div style="white-space: normal; word-wrap: break-word;background-color: #f0f2f5; padding: 8px; border-radius: 5px;font-size: 14px;width: 100%;">
+                      {{ m.msg }}
+                    </div>
+                  </el-col>
+                  <el-col>
+                    <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="changeUserState(m)">{{ m.msgStatus === 1 ? '解禁' : '禁言' }}</a>
+                    <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="blockUser(m)">拉黑</a>
+                    <a v-if="m.singleVisible === 1" style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="singleVisible(m)">解除用户自见</a>
+                    <a v-else style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="singleVisible(m)">用户自见</a>
+                  </el-col>
+                </el-row>
+              </el-col>
+            </el-row>
+            <el-row v-if="m.userId === userId" style="padding: 8px 0" type="flex" align="top" justify="end">
+              <div style="display: flex;justify-content: flex-end">
+                <div style="display: flex;justify-content: flex-end;flex-direction: column;max-width: 200px;align-items: flex-end">
+                  <div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div>
+                  <div style="white-space: normal; word-wrap: break-word;width: 100%; background-color: #e6f7ff; padding: 8px; border-radius: 5px;font-size: 14px;">{{ m.msg }}</div>
+                </div>
+                <el-avatar :src="m.avatar" style="margin-left: 10px; margin-right: 10px;"/>
+              </div>
+            </el-row>
+          </el-row>
+          <!-- 底部留白 -->
+          <div style="height: 20px;"></div>
+        </el-scrollbar>
+<!--        <div class="message-list">-->
+<!--          <div class="message-item" v-for="msg in msgList" :key="msg.id">-->
+<!--            <div class="message-avatar">-->
+<!--              <img :src="msg.avatar" alt="用户头像">-->
+<!--            </div>-->
+<!--            <div class="message-content">-->
+<!--              <div class="message-user">{{ msg.user }}</div>-->
+<!--              <div class="message-text">{{ msg.text }}</div>-->
+<!--            </div>-->
+<!--            <div class="message-actions">-->
+<!--&lt;!&ndash;              <button @click="toggleVisible(msg)">&ndash;&gt;-->
+<!--&lt;!&ndash;                {{ msg.isVisible ? '仅用户自见' : '全局可见' }}&ndash;&gt;-->
+<!--&lt;!&ndash;              </button>&ndash;&gt;-->
+<!--              <button @click="deleteMessage(msg)">删除</button>-->
+<!--            </div>-->
+<!--          </div>-->
+<!--        </div>-->
+      </div>
+      <div class="system-messages">
+        <h3>系统消息</h3>
+        <textarea placeholder="输入系统消息" v-model="newMsg"></textarea>
+        <div class="message-actions">
+          <button @click="sendMessage">发送消息</button>
+          <button @click="sendPopMessage">弹窗消息</button>
         </div>
       </div>
     </div>
@@ -140,8 +176,8 @@
     <div class="right-panel">
       <h2>运营工具</h2>
       <div class="live-player">
-        <h3>直播预览</h3>
-        <LivePlayer :stream-url="streamUrl" />
+        <h3>直播观看</h3>
+        <LivePlayer ref="livePlayer" :videoParam="videoParam" />
       </div>
 
       <div class="automation">
@@ -149,45 +185,50 @@
         <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>
+            <div class="timeline-item" v-for="item in timelineItems.slice(0,2)" :key="item.time">
+              <div class="time">{{ formatDate(item.absValue) }}</div>
+              <div class="action">{{ item.taskName }}</div>
               <button class="delete" @click="removeTimelineItem(item)">删除</button>
             </div>
           </div>
-          <button class="add" @click="addTimelineItem">添加时间节点</button>
+<!--          <button class="add" @click="addTimelineItem">添加时间节点</button>-->
         </div>
       </div>
 
       <div class="watermark">
         <h3>直播氛围自动化</h3>
         <div class="watermark-settings">
-          <textarea placeholder="水军弹幕内容模板,每行一条"></textarea>
+          <textarea :disabled="autoWatermark" v-model="watermarkTemplate" placeholder="水军弹幕内容模板,每行一条"></textarea>
           <div class="watermark-options">
             <label>
               发送间隔:
-              <input type="number" v-model.number="interval" min="1">
+              <input type="number" :disabled="autoWatermark" v-model.number="interval" min="1">
             </label>
             <label>
-              <input type="checkbox" v-model="autoWatermark">
+              <input type="checkbox" v-model="autoWatermark" @change="changeAutoWatermark">
               启用水军自动化
             </label>
           </div>
         </div>
       </div>
     </div>
+
   </div>
 </template>
 
 <script>
 import LivePlayer from './LivePlayer.vue';
 import {blockUser, changeUserStatus, getLiveUserTotals, dashBoardWatchUserList} from '@/api/live/liveWatchUser'
+import { listLiveSingleMsg } from '@/api/live/liveMsg'
+import { getLive } from '@/api/live/live'
+import { consoleList } from '@/api/live/task'
+import ScreenScale from './ScreenScale.vue'; // 路径根据实际位置调整
 
 
 export default {
   components: {
-    LivePlayer
+    LivePlayer,ScreenScale
   },
   props: {
     liveId: {
@@ -197,8 +238,20 @@ export default {
   },
   data() {
     return {
+      watermarkIndex: 0,
+      watermarkList:[],
+      watermarkTemplate: '',
+      liveInfo: {},
+      videoParam:{
+        startTime:'',
+        livingUrl: '',
+        liveType: 1,
+        videoUrl: '',
+      },
+      msgList:[],
+      currentTab: 'al',
       searchKeyword: '',
-      globalVisible: true,
+      globalVisible: false,
       interval: 10,
       autoWatermark: false,
       streamUrl: 'rtmp://your-live-stream-url',
@@ -215,8 +268,22 @@ export default {
         { id: 3, user: '用户3', avatar: 'https://via.placeholder.com/30', text: '有优惠吗?', isVisible: true }
       ],
       timelineItems: [
-        { time: '09:00', action: '置顶欢迎消息' },
-        { time: '09:30', action: '上架主推商品' }
+        { "searchValue": null,
+          "createBy": null,
+          "createTime": "2025-09-23 10:36:17",
+          "updateBy": null,
+          "updateTime": "2025-10-17 09:18:00",
+          "remark": null,
+          "params": {},
+          "id": 6573,
+          "liveId": 128,
+          "taskName": "2",
+          "taskType": 1,
+          "triggerType": 2,
+          "triggerValue": "2025-01-01T00:02:00.000+0800",
+          "absValue": "2025-11-23T10:43:00.000+0800",
+          "status": 1,
+          "finishStatus": 0 },
       ],
       userTotal: {
         online: 0,       // 在线总人数
@@ -225,7 +292,16 @@ export default {
         al: 0
       },
       tabLeft: {
-        activeName: "online",
+        activeName: "al",
+      },
+      taskParams:{
+        currentPage: 1,
+        pageSize: 20,
+        prevPage: 0,
+        totalLoaded: 0,
+        total: 0,
+        hasMore: true,
+        hasPrev: false
       },
       loadMsgMaxPage: 2,
       liveWsUrl: process.env.VUE_APP_LIVE_WS_URL + '/app/webSocket',
@@ -277,10 +353,19 @@ export default {
         online: { next: false, prev: false },
         offline: { next: false, prev: false },
         silenced: { next: false, prev: false }
-      }
+      },
+      newMsg:'',
+      autoMsgTimer: null,
+      checkInterval: 2000, // 检查间隔(1分钟,可根据需求调整)
     };
   },
   computed: {
+    userId() {
+      return this.$store.state.user.user.userId
+    },
+    companyId() {
+      return this.$store.state.user.user.companyId
+    },
     onlineLabel() {
       return `在线(${this.userTotal.online})`;
     },
@@ -297,16 +382,271 @@ export default {
   created() {
     if(!this.liveId) return
     this.getList()
-    this.handleUserClick({name:'online'})
+    this.handleUserClick({name:'al'})
+    this.connectWebSocket()
+    this.getLive()
   },
   mounted() {
     this.$nextTick(() => {
       this.restoreChatScrollPosition();
+
+      if (this.$refs.manageRightRef && this.$refs.manageRightRef.wrap) {
+        this.$refs.manageRightRef.wrap.addEventListener('scroll', this.saveChatScrollPosition);
+      }
     });
     this.initScrollListeners();
   },
   methods: {
+    singleVisible(m){
+      // 过滤当前所有消息 找到userId的相同的消息 更改他们的自可见状态
+      m.singleVisible= m.singleVisible === 1 ? 0 : 1
+      this.msgList.forEach(m1 => {m1.singleVisible = m1.userId === m.userId ? m.singleVisible : !m.singleVisible})
+      // 消息自可见
+      let msg = {
+        liveId: this.liveId,
+        userId: m.userId,
+        userType: 0,
+        cmd: 'singleVisible',
+        status:m.singleVisible === 1 ? 0 :1
+      }
+      this.socket.send(JSON.stringify(msg))
+    },
+    globalVisibleChange( val){
+      // 消息自可见
+      let msg = {
+        liveId: this.liveId,
+        userId: '9999',
+        userType: 0,
+        cmd: 'globalVisible',
+        status:this.globalVisible ? 1 :0
+      }
+      this.socket.send(JSON.stringify(msg))
+    },
+    changeAutoWatermark( val){
+      this.watermarkList = this.watermarkTemplate
+        .split('\n')
+        .map(line => line.trim())
+        .filter(line => line !== '');
+      if(this.watermarkTemplate.length < 1 || this.watermarkList.length === 0) {
+        this.$message.warning('请先填写水印模板')
+        this.$nextTick(() => {
+          this.autoWatermark = false
+        });
+        return
+      }
+      this.disabled = this.autoWatermark
+      if(this.autoWatermark){
+          this.autoMsgTimer = setInterval(() => {
+            this.sendNormalMessage()
+          }, 1000 * this.interval)
+      } else {
+        clearInterval(this.autoMsgTimer)
+        this.watermarkIndex = 0;
+        this.watermarkList = [];
+      }
+    },
+    sendNormalMessage() {
+      if(this.watermarkIndex >= this.watermarkList.length){
+        clearInterval(this.autoMsgTimer)
+        this.watermarkIndex = 0;
+        this.watermarkList = [];
+        this.$message.success('自动发送消息已结束');
+        this.$nextTick(() => {
+          this.autoWatermark = false
+        });
+        return;
+      }
+      const curMsg = this.watermarkList[this.watermarkIndex]
+      // 发送前简单校验
+      if (curMsg.trim() === '') {
+        return;
+      }
+      let msg = {
+        msg: curMsg,
+        liveId: this.liveId,
+        userId: '9999',
+        userType: 0,
+        cmd: 'sendNormalMsg',
+        avatar: this.$store.state.user.user.avatar,
+        nickName: this.$store.state.user.user.nickName,
+      }
+      this.socket.send(JSON.stringify(msg))
+      this.watermarkIndex += 1
+    },
+    // 弹窗消息
+    sendPopMessage() {
+      // 发送前简单校验
+      if (this.newMsg.trim() === '') {
+        return;
+      }
+
+      let msg = {
+        msg: this.newMsg,
+        liveId: this.liveId,
+        userId: this.userId,
+        userType: 1,
+        cmd: 'sendPopMsg',
+        avatar: this.$store.state.user.user.avatar,
+        nickName: this.$store.state.user.user.nickName,
+      }
+
+      this.socket.send(JSON.stringify(msg))
+
+      this.newMsg = '';
+    },
+    sendMessage() {
+      // 发送前简单校验
+      if (this.newMsg.trim() === '') {
+        return;
+      }
+
+      let msg = {
+        msg: this.newMsg,
+        liveId: this.liveId,
+        userId: this.userId,
+        userType: 1,
+        cmd: 'sendMsg',
+        avatar: this.$store.state.user.user.avatar,
+        nickName: this.$store.state.user.user.nickName,
+      }
+
+      this.socket.send(JSON.stringify(msg))
+
+      this.newMsg = '';
+    },
+    handleWsMessage(event) {
+      let { code, data } = JSON.parse(event.data)
+      if (code === 200) {
+        let { cmd } = data
+        if (cmd === 'sendMsg') {
+          let message = JSON.parse(data.data)
+
+          let user = this.alDisplayList.find(u => u.userId === message.userId)
+          if (user) {
+            message.msgStatus = user.msgStatus
+          } else {
+            message.msgStatus = 0
+          }
+          delete message.params
+          if(this.msgList.length > 50){
+            this.msgList.shift()
+          }
+          this.msgList.push(message)
+          // 移动到底部
+          this.$nextTick(() => {
+            setTimeout(() => {
+              this.$refs.manageRightRef.wrap.scrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight
+            }, 200)
+          })
+        } else if (cmd === 'entry' || cmd === 'out') {
+          const user = data;
+          const online = cmd === 'entry' ? 0 : 1; // 0=在线,1=离线
+          const info = {
+            online:online,
+            msgStatus: user.msgStatus || 0,
+            nickName: user.nickName || '',
+            userType: user.userType || 0,
+            userId: user.userId || '',
+          };
+
+          // 1. 更新总人数(在线/离线互转)
+          if (cmd === 'entry') {
+            this.userTotal.online += 1;
+            this.userTotal.offline = Math.max(0, this.userTotal.offline - 1); // 确保不小于0
+          } else {
+            this.userTotal.offline += 1;
+            this.userTotal.online = Math.max(0, this.userTotal.online - 1); // 确保不小于0
+          }
+          // 2. 强制更新相关列表(无论当前激活哪个Tab)
+          if (cmd === 'entry') {
+            // 用户进入:从离线列表删除,添加到在线列表
+            this.offlineDisplayList = this.offlineDisplayList.filter(u => u.userId !== user.userId);
+            const newOnlineList = this.onlineDisplayList.filter(u => u.userId !== user.userId);
+            newOnlineList.push(info);
+            this.onlineDisplayList = newOnlineList.slice(-40); // 限制最大50条
+          } else {
+            // 用户离开:从在线列表删除,添加到离线列表
+            this.onlineDisplayList = this.onlineDisplayList.filter(u => u.userId !== user.userId);
+            const newOfflineList = this.offlineDisplayList.filter(u => u.userId !== user.userId);
+            newOfflineList.push(info);
+            this.offlineDisplayList = newOfflineList.slice(-40); // 限制最大50条
+          }
+          // 3. 处理禁言列表(如果用户是禁言状态,无论进出都要同步)
+          if (info.msgStatus === 1) {
+            // 禁言用户:从禁言列表删除旧数据,添加新数据
+            const newSilencedList = this.silencedDisplayList.filter(u => u.userId !== user.userId);
+            newSilencedList.push(info);
+            this.silencedDisplayList = newSilencedList.slice(-40);
+          } else {
+            // 非禁言用户:从禁言列表删除(如果存在)
+            this.silencedDisplayList = this.silencedDisplayList.filter(u => u.userId !== user.userId);
+          }
+
+        } else if (cmd === 'live_start') {
+
+        } else if (cmd === 'live_end') {
+
+        }
+      }
+    },
+    getLive(){
+      getLive(this.liveId).then(res => {
+        if (res.code == 200) {
+          if (res.data.isAudit != 1) {
+            this.$message.error("当前直播间未经审核");
+            this.loading = false
+            return
+          }
+          this.isAudit = true
+          this.status = res.data.status
+          this.videoParam.startTime = new Date(res.data.startTime).getTime()
+          if(res.data.status == 4){
+            this.videoParam.liveType = 3
+            this.videoParam.videoUrl = res.data.videoUrl;
+          }else {
+            if (res.data.status != 2) {
+              this.$message.error("当前直播间未直播");
+              this.loading = false
+              return
+            }
+            if (res.data.liveType == 1) {
+              this.videoParam.livingUrl = res.data.flvHlsUrl
+              this.videoParam.livingUrl = this.livingUrl.replace("flv","m3u8")
+              // this.$nextTick(() => {
+              //   this.initPlayer()
+              // })
+              // this.processInterval = setInterval(this.updateLiveProgress, 1000);
+            } else {
+              this.videoParam.liveType = 2
+              this.videoParam.videoUrl = res.data.videoUrl;
+              // this.$nextTick(() => {
+              //   this.initVideoPlayer(res.data.startTime)
+              // })
+            }
+          }
+          this.$nextTick(() => {
+            this.globalVisible = res.data.globalVisible
+            this.$refs.livePlayer.initPlayer()
+          })
+          this.loading = false
+        } else {
+          this.$message.error(res.msg)
+          this.loading = false
+        }
+        this.liveInfo = res.data
+      })
+    },
+    connectWebSocket() {
+      this.$store.dispatch('initLiveWs', {
+        liveWsUrl: this.liveWsUrl,
+        liveId: this.liveId,
+        userId: this.userId
+      })
+      this.socket = this.$store.state.liveWs[this.liveId]
+      this.socket.onmessage = (event) => this.handleWsMessage(event)
+    },
     changeUserState(u) {
+      const displayList = this[`${this.currentTab}DisplayList`];
       // 修改状态
       changeUserStatus({liveId: u.liveId, userId: u.userId}).then(response => {
         let { code } = response;
@@ -319,7 +659,7 @@ export default {
             }
           });
 
-          this.userList.forEach(user => {
+          displayList.forEach(user => {
             if (user.userId === u.userId) {
               user.msgStatus = u.msgStatus;
             }
@@ -334,6 +674,48 @@ export default {
         this.msgError("操作失败");
       })
     },
+    refreshUserDisplayLists(user) {
+      const { userId, msgStatus: newStatus, online } = user;
+      const oldStatus = newStatus === 1 ? 0 : 1; // 操作前的状态(反向)
+
+      // 1. 禁言操作(newStatus=1):从原在线/离线列表移除,加入禁言列表
+      if (newStatus === 1) {
+        // 从在线/离线列表中移除该用户(根据当前在线状态)
+        if (online === 0) {
+          this.onlineDisplayList = this.onlineDisplayList.filter(u => u.userId !== userId);
+          this.userTotal.online = Math.max(0, this.userTotal.online - 1);
+        } else {
+          this.offlineDisplayList = this.offlineDisplayList.filter(u => u.userId !== userId);
+          this.userTotal.offline = Math.max(0, this.userTotal.offline - 1);
+        }
+        this.userTotal.silenced = this.userTotal.silenced + 1;
+        // 添加到禁言列表(去重+限制长度)
+        const silencedList = this.silencedDisplayList.filter(u => u.userId !== userId);
+        silencedList.push(user);
+        this.silencedDisplayList = silencedList.slice(-40);
+      }
+
+      // 2. 解禁操作(newStatus=0):从禁言列表移除,回到原在线/离线列表
+      else {
+        // 从禁言列表移除
+        this.silencedDisplayList = this.silencedDisplayList.filter(u => u.userId !== userId);
+        this.userTotal.silenced = Math.max(0, this.userTotal.silenced - 1);
+        // 回到对应的在线/离线列表(根据当前在线状态)
+        if (online === 0) {
+          // 加入在线列表(去重+限制长度)
+          const onlineList = this.onlineDisplayList.filter(u => u.userId !== userId);
+          onlineList.push(user);
+          this.onlineDisplayList = onlineList.slice(-40);
+          this.userTotal.online = this.userTotal.online + 1;
+        } else {
+          // 加入离线列表(去重+限制长度)
+          const offlineList = this.offlineDisplayList.filter(u => u.userId !== userId);
+          offlineList.push(user);
+          this.offlineDisplayList = offlineList.slice(-40);
+          this.userTotal.offline = this.userTotal.offline + 1;
+        }
+      }
+    },
     blockUser(u){
       this.$confirm('是否确认封禁用户账号为:"' + u.nickName + '-' + u.userId + '"?', "警告", {
         confirmButtonText: "确定",
@@ -342,6 +724,18 @@ export default {
       }).then(function() {
         return blockUser(u.userId);
       }).then(() => {
+        if(u.msgStatus === 1){
+          this.userTotal.silenced -= 1
+          this.silencedDisplayList = this.silencedDisplayList.filter(user => user.userId !== u.userId)
+        } else if( u.online === 0){
+          this.userTotal.online -= 1
+          this.onlineDisplayList = this.onlineDisplayList.filter(user => user.userId !== u.userId)
+        } else {
+          this.userTotal.offline -= 1
+          this.offlineDisplayList = this.offlineDisplayList.filter(user => user.userId !== u.userId)
+        }
+        this.userTotal.al -= 1
+        this.alDisplayList = this.alDisplayList.filter(user => user.userId !== u.userId)
         let msg = {
           msg: "",
           liveId: this.liveId,
@@ -353,14 +747,17 @@ export default {
         }
         this.socket.send(JSON.stringify(msg))
         this.msgSuccess("封禁成功");
+
+
       }).catch(() => {});
     },
     searchUsers(){
       this.resetUserParams()
-      this.loadNextPage()
+      this.loadNextPage(this.currentTab)
     },
     handleUserClick(tab){
       const tabName = tab.name;
+      this.currentTab = tabName;
       const params = this.pageParams[tabName];
       const displayList = this[`${tabName}DisplayList`];
       // 首次切换到该Tab或列表为空时初始化
@@ -505,7 +902,106 @@ export default {
       this.loadUserTotals(); // 先加载总人数
       // this.handleClick('online')
       // this.handleClick({name:'online'})
-      // this.loadMsgList()
+      this.loadMsgList()
+      this.loadLiveTask()
+    },
+    formatDate(value) {
+      // 检查值是否存在且为日期类型(或可转换为日期)
+      if (!value) return '';
+
+      let date;
+      // 处理字符串类型的日期
+      if (typeof value === 'string') {
+        date = new Date(value);
+      }
+      // 处理Date对象
+      else if (value instanceof Date) {
+        date = value;
+      }
+      // 无效类型直接返回原值
+      else {
+        return value;
+      }
+
+      // 检查是否为有效日期
+      if (isNaN(date.getTime())) {
+        return value;
+      }
+
+      // 格式化年月日时分秒(补零处理)
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, '0');
+      const day = String(date.getDate()).padStart(2, '0');
+      const hours = String(date.getHours()).padStart(2, '0');
+      const minutes = String(date.getMinutes()).padStart(2, '0');
+      const seconds = String(date.getSeconds()).padStart(2, '0');
+
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+    },
+    loadLiveTask(){
+      if(!this.taskParams.hasMore) return
+      const queryParams = {
+        liveId: this.liveId,
+        pageSize: 10,
+        pageNum: 1
+      }
+      consoleList(queryParams).then(res => {
+        let {code, rows,total} = res;
+        if (code === 200) {
+          this.taskParams.total = total
+          this.timelineItems = rows
+          if(rows.length  < this.taskParams.pageSize){
+            this.taskParams.hasMore = false
+          }
+          this.startTaskTimer()
+        } else {
+          this.stopTaskTimer()
+        }
+      })
+    },
+    loadMsgList(){
+      // 直播间消息
+      listLiveSingleMsg({
+        liveId:this.liveId,
+        pageNum: this.msgParams.pageNum,
+        pageSize: this.msgParams.pageSize
+      }).then(response => {
+        let {code, rows,total} = response;
+        if (code === 200) {
+          let totalPage = (total % this.msgParams.pageSize == 0) ? Math.floor(total / this.msgParams.pageSize) : Math.floor(total / this.msgParams.pageSize + 1);
+          rows.forEach(row => {
+            if (!this.msgList.some(m => m.msgId === row.msgId)) {
+
+              let user = this.alDisplayList.find(u => u.userId === row.userId)
+              if (user) {
+                row.msgStatus = user.msgStatus
+              } else {
+                row.msgStatus = 0
+              }
+
+              this.msgList.push(row)
+
+              // 移动到底部
+              this.$nextTick(() => {
+                setTimeout(() => {
+                  this.$refs.manageRightRef.wrap.scrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight
+                }, 200)
+              })
+            }
+          })
+
+          this.msgList.reverse()
+          // 同步更新消息列表中相同用户的状态
+          this.alDisplayList.forEach(u => {
+            this.msgList.filter(m => m.userId === u.userId).forEach(m => m.msgStatus = u.msgStatus)
+          })
+        }
+      })
+
+      // 添加滚动监听
+      this.$nextTick(() => {
+        this.$refs.manageRightRef.wrap.addEventListener("scroll", this.manageRightScroll)
+      })
     },
     loadUserTotals() {
       if (!this.liveId) return;
@@ -596,9 +1092,18 @@ export default {
       this.msgList = [];
       this.msgParams = {
         pageNum: 1,
-        pageSize: 10,
+        pageSize: 30,
         liveId: this.liveId
       };
+      this.taskParams = {
+          currentPage: 1,
+          pageSize: 20,
+          prevPage: 0,
+          totalLoaded: 0,
+          total: 0,
+          hasMore: true,
+          hasPrev: false
+      }
     },
     getScrollElement(tabName) {
       const scrollRefs = {
@@ -658,6 +1163,58 @@ export default {
         this.chatScrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight;
       }
     },
+    /**
+     * 停止任务检测定时器
+     */
+    stopTaskTimer() {
+      if (this.taskTimer) {
+        clearInterval(this.taskTimer);
+        this.taskTimer = null;
+      }
+    },
+    /**
+     * 启动任务检测定时器
+     */
+    startTaskTimer() {
+      // 先清除已有定时器,避免重复
+      if (this.taskTimer) {
+        clearInterval(this.taskTimer);
+      }
+
+      // 立即执行一次检查
+      this.checkTaskExpiration();
+
+      // 启动定时器,定期检查
+      this.taskTimer = setInterval(() => {
+        this.checkTaskExpiration();
+      }, this.checkInterval);
+    },
+    /**
+     * 检查时间轴第一条任务是否过期
+     */
+    checkTaskExpiration() {
+      // 如果没有任务,直接返回
+      if (!this.timelineItems || this.timelineItems.length === 0) {
+        this.stopTaskTimer()
+        return;
+      };
+
+      // 获取第一条任务的时间
+      const firstTask = this.timelineItems[0];
+      const taskTime = new Date(firstTask.absValue).getTime();
+      const currentTime = new Date().getTime();
+
+      // 如果任务时间已过当前时间(过期),重新加载任务列表
+      if (taskTime < currentTime) {
+        this.timelineItems.shift()
+        this.loadLiveTask(); // 重新加载任务列表
+      }
+    },
+  },
+  beforeDestroy() {
+    if (this.autoMsgTimer != null) {
+      clearInterval(this.autoMsgTimer);
+    }
   },
   // 使用 deactivated 和 activated 钩子替代 beforeDestroy 和 destroyed
   deactivated() {
@@ -945,4 +1502,20 @@ export default {
   display: block;
   margin-bottom: 5px;
 }
+/* 隐藏 el-scrollbar 的横向滚动条 */
+.el-scrollbar__wrap {
+  overflow-x: hidden !important;
+}
+.custom-scrollbar .el-scrollbar__wrap {
+  overflow-x: hidden !important;
+}
+.scrollbar-demo-item{
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 50px;
+  margin: 10px;
+  text-align: center;
+  border-radius: 4px;
+}
 </style>

+ 2 - 1
src/views/live/liveConsole/LiveDashboard.vue

@@ -32,13 +32,14 @@
           </div>
           <div class="rank">
             <h3>邀请达人榜</h3>
-            <ul class="rank-list">
+            <ul class="rank-list" v-if="rankList.length > 0">
               <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>
+            <p class="no-data" style="text-align: center;" v-if="rankList.length == 0">暂无数据</p>
           </div>
         </div>
       </div>

+ 112 - 5
src/views/live/liveConsole/LivePlayer.vue

@@ -1,24 +1,131 @@
 <template>
   <div class="live-player">
-    <video ref="videoPlayer" controls autoplay class="player">
-      <source :src="streamUrl" type="video/mp4">
+    <video ref="videoPlayer" loop autoplay class="player">
+      <source :src="videoParam.videoUrl" type="application/x-mpegURL">
     </video>
   </div>
 </template>
 
 <script>
+import Hls from "hls.js";
+
 export default {
   name: 'LivePlayer',
   props: {
-    streamUrl: {
-      type: String,
-      required: true
+    videoParam: {
+      type: Object,
+      required: true,
+      default: () => ({
+        startTime: '',
+        livingUrl: '',
+        liveType: 1,
+        videoUrl: ''
+      }),
     }
   },
+  data() {
+    return {
+      videoDuration: 0, // 视频总时长(秒)
+      startTime: 0, // 开播时间戳(毫秒)
+      hls: null, // HLS 实例
+    };
+  },
   mounted() {
+
     // 这里可以添加播放器初始化逻辑
   },
+  methods: {
+    videoPlay(url) {
+      if (Hls.isSupported()) {
+        const videoElement = this.$refs.videoPlayer
+        if (!videoElement) {
+          console.error('找不到 video 元素')
+          return
+        }
+        this.hls = new Hls();
+        this.hls.attachMedia(videoElement);
+        this.hls.on(Hls.Events.MEDIA_ATTACHED, () => {
+          this.hls.loadSource(url);
+          this.hls.on(Hls.Events.STREAM_LOADED, (event, data) => {
+            videoElement.play();
+          });
+        });
+        this.hls.on(Hls.Events.ERROR, (event, data) => {
+          console.error('HLS 错误:', data);
+        });
+        // 1. 初始化开播时间
+        this.startTime = new Date(this.videoParam.startTime).getTime();
+        if (this.videoParam.liveType === 2) {
+          // 2. 监听视频元数据加载完成(获取视频时长)
+          videoElement.addEventListener('loadedmetadata', () => {
+            this.videoDuration = videoElement.duration; // 获取视频时长(秒)
+
+            // 初始化视频播放位置
+            this.updateVideoPosition(videoElement);
+
+          });
+        }
+      } else {
+        console.error('浏览器不支持 HLS')
+      }
+    },
+    updateVideoPosition(video){
+      const currentTime = new Date().getTime(); // 当前时间戳(毫秒)
+      const elapsedTime = currentTime - this.startTime; // 已流逝时间(毫秒)
+      if (elapsedTime < 0) {
+        // 未开播:视频停在初始位置
+        video.currentTime = 0;
+        return;
+      }
+
+      // 已开播:计算视频循环后的位置(流逝时间 % 视频时长)
+      const elapsedSeconds = elapsedTime / 1000; // 转换为秒
+       // 视频内的播放位置(秒)
+      // 设置视频播放位置
+      video.currentTime = elapsedSeconds % this.videoDuration;
+    },
+    initPlayer() {
+
+      if (this.videoParam.liveType === 1) {
+        // 直播
+        const isUrl = this.videoParam.livingUrl === null || this.videoParam.livingUrl.trim() === '';
+        if (isUrl) {
+          console.error('直播地址为空,无法初始化播放器')
+          return
+        }
+        this.videoPlay(this.videoParam.livingUrl);
+        return;
+      }
+      const viUrl = this.videoParam.videoUrl === null || this.videoParam.videoUrl.trim() === ''
+      if (viUrl) {
+        console.error('播放地址为空,无法初始化播放器')
+        return
+      }
+      if(this.videoParam.liveType === 2){
+        // 录播
+        this.videoPlay(this.videoParam.videoUrl);
+      } else if(this.videoParam.liveType === 3){
+        // 直播回放
+        this.videoPlay(this.videoParam.videoUrl);
+      }else {
+        console.error('直播类型错误,无法初始化播放器')
+      }
+      // 创建播放器实例
+      const videoPlayer = this.$refs.videoPlayer;
+      // 添加播放器事件监听器
+      videoPlayer.addEventListener('play', () => {
+        console.log('播放器已开始播放');
+      });
+      videoPlayer.addEventListener('pause', () => {
+        console.log('播放器已暂停');
+      });
+      videoPlayer.addEventListener('ended', () => {
+        console.log('播放器已结束');
+      });
+    }
+  },
   beforeDestroy() {
+    this.hls?.destroy()
     // 销毁播放器实例
   }
 };

+ 1444 - 0
src/views/live/liveData/index-old.vue

@@ -0,0 +1,1444 @@
+<template>
+  <div class="app-container">
+    <div class="title">近期直播</div>
+    <div class="live-container">
+      <div class="live-card" v-for="live in displayLives" :key="live.liveId">
+        <!-- 直播状态 -->
+        <div class="status" :class="getStatusClass(live.status)">
+          {{ getStatusText(live.status) }}
+        </div>
+        <!-- 直播信息 -->
+        <div class="content">
+          <img :src="live.liveImgUrl" alt="直播封面" class="cover-image" />
+          <div class="info">
+            <h3 class="live-title">{{ live.liveName }}</h3>
+            <p class="time">开播时间: {{ live.startTime }}</p>
+          </div>
+        </div>
+
+        <!-- 直播数据 -->
+        <div class="stats">
+          <div class="stat-item">
+            <p class="label">直播间浏览量</p>
+            <p class="value">{{ live.pageViews }}</p>
+          </div>
+          <div class="stat-item">
+            <p class="label">直播间访客数</p>
+            <p class="value">{{ live.uniqueVisitors }}</p>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- 直播趋势统计 -->
+    <div class="trend-section">
+      <div class="trend-header">
+        <h3>直播趋势</h3>
+        <div class="filter-container">
+        <!-- 时间范围选择(下拉框) -->
+          <span class="filter-label">时间筛选:</span>
+          <el-select v-model="selectedTimeRange" placeholder="选择时间范围" @change="changeTimeRange" style="width: 180px">
+            <el-option label="自然天" value="day"></el-option>
+            <el-option label="自然周" value="week"></el-option>
+            <el-option label="自然月" value="month"></el-option>
+          </el-select>
+
+          <!-- 日期选择器 -->
+          <!-- 选择日期 -->
+          <el-date-picker
+            ref="datePickerRef"
+            v-model="selectedDate"
+            :type="datePickerType"
+            :format="selectedTimeRange === 'week' ? weekRange : dateFormat"
+            :value-format="valueFormat"
+            placeholder="选择日期"
+            style="width: 280px"
+            @change="changeDate"
+            @blur="setDefaultDate"
+          />
+        </div>
+      </div>
+
+      <div class="trend-cards">
+        <div class="trend-card"
+             :class="{ 'active': selectedMetric === 'page_views' }"
+             @click="changeMetric('page_views')">
+          <p class="trend-title">
+            浏览量
+            <el-tooltip class="item" effect="dark" content="观看页面的总浏览量" placement="top">
+              <i class="el-icon-info"></i>
+            </el-tooltip>
+          </p>
+          <p class="trend-value">{{ trendData.views || 0 }}</p>
+          <p class="trend-tip">{{ changeLabel }}: {{ trendData.viewsChange }}%</p>
+
+        </div>
+
+        <div class="trend-card"
+             :class="{ 'active': selectedMetric === 'unique_visitors' }"
+             @click="changeMetric('unique_visitors')">
+          <p class="trend-title">
+            访客数
+            <el-tooltip class="item" effect="dark" content="进入直播间的访客数" placement="top">
+              <i class="el-icon-info"></i>
+            </el-tooltip>
+          </p>
+          <p class="trend-value">{{ trendData.visitors|| 0 }}</p>
+          <p class="trend-tip">{{ changeLabel }}: {{ trendData.visitorsChange }}%</p>
+
+        </div>
+
+        <div class="trend-card"
+             :class="{ 'active': selectedMetric === 'streams' }"
+             @click="changeMetric('streams')">
+          <p class="trend-title">
+            创建直播数
+            <el-tooltip class="item" effect="dark" content="创建的直播间数量" placement="top">
+              <i class="el-icon-info"></i>
+            </el-tooltip>
+          </p>
+          <p class="trend-value">{{ trendData.streams|| 0 }}</p>
+          <p class="trend-tip">{{ changeLabel }}: {{ trendData.streamsChange }}%</p>
+
+        </div>
+
+        <div class="trend-card"
+             :class="{ 'active': selectedMetric === 'total_views' }"
+             @click="changeMetric('total_views')">
+          <p class="trend-title">
+            累计观看人次
+            <el-tooltip class="item" effect="dark" content="直播间被观看的总人次" placement="top">
+              <i class="el-icon-info"></i>
+            </el-tooltip>
+          </p>
+          <p class="trend-value">{{ trendData.pv|| 0 }}</p>
+          <p class="trend-tip">{{ changeLabel }}: {{ trendData.pvChange }}%</p>
+
+        </div>
+
+        <div class="trend-card"
+             :class="{ 'active': selectedMetric === 'unique_viewers' }"
+             @click="changeMetric('unique_viewers')">
+          <p class="trend-title">
+            累计观看人数
+            <el-tooltip class="item" effect="dark" content="去重后的观看人数" placement="top">
+              <i class="el-icon-info"></i>
+            </el-tooltip>
+          </p>
+          <p class="trend-value">{{ trendData.uv|| 0 }}</p>
+          <p class="trend-tip">{{ changeLabel }}: {{ trendData.uvChange }}%</p>
+
+        </div>
+      </div>
+
+
+      <!-- 直播趋势折线图 -->
+      <div id="liveChart" class="chart"></div>
+    </div>
+    <!-- 直播TOP10排行榜 -->
+    <div class="top10-section">
+      <h3>直播TOP10排行榜</h3>
+      <div class="ranking-container">
+          <!-- 红榜 -->
+          <div class="ranking-section red-rank">
+            <span class="rank-title">红榜</span>
+            <div class="rank-filters">
+              <button
+                v-for="item in redRankTypes"
+                :key="item.value"
+                :class="{ active: selectedRank === item.value }"
+                @click="selectRank(item.value)"
+              >
+                {{ item.label }}
+              </button>
+            </div>
+          </div>
+
+          <!-- 黑榜 -->
+          <div class="ranking-section black-rank">
+            <div class="rank-filters">
+              <button
+                v-for="item in blackRankTypes"
+                :key="item.value"
+                :class="{ active: selectedRank === item.value }"
+                @click="selectRank(item.value)"
+              >
+                {{ item.label }}
+              </button>
+            </div>
+            <span class="rank-title">黑榜</span>
+          </div>
+      </div>
+      <el-table :data="top10List" border style="width: 100%">
+        <el-table-column prop="rank" label="排名" width="80"></el-table-column>
+        <el-table-column prop="liveName" label="直播名称">
+          <template #default="scope">
+            <div class="live-name">
+              <img :src="scope.row.liveImgUrl" class="live-cover" alt="封面">
+              <span class="live-title">{{ scope.row.liveName }}</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="pageViews" label="直播间浏览量(PV)"></el-table-column>
+        <el-table-column prop="uniqueVisitors" label="直播间访客数(UV)"></el-table-column>
+        <el-table-column prop="totalViews" label="累计观看人次(PV)"></el-table-column>
+        <el-table-column prop="uniqueViewers" label="累计观看人数(UV)"></el-table-column>
+        <!--<el-table-column prop="avgTime" label="人均观看时长"></el-table-column>-->
+        <el-table-column prop="peakConcurrentViewers" label="最高在线人数"></el-table-column>
+      </el-table>
+
+    </div>
+<!--    <div class="student-section">-->
+<!--      <h3>直播间学员</h3>-->
+<!--        <el-form :inline="true" v-show="showAdvancedSearch" label-width="100px">-->
+<!--          &lt;!&ndash; 直播列表弹框 &ndash;&gt;-->
+<!--          <el-dialog title="选择直播" :visible.sync="dialogVisible" width="70%">-->
+<!--            <el-row :gutter="20">-->
+<!--              <el-col :span="8" :sm="12" :md="8">-->
+<!--                <el-form-item label="直播名称:">-->
+<!--                  <el-input v-model="liveFiltersParam.liveName" placeholder="请输入直播名称"></el-input>-->
+<!--                </el-form-item>-->
+<!--              </el-col>-->
+<!--              <el-col :span="8" :sm="12" :md="8">-->
+<!--                <el-form-item label="直播状态:">-->
+<!--                  <el-select v-model="liveFiltersParam.status" placeholder="请选择直播状态" clearable>-->
+<!--                    <el-option label="全部" value=""></el-option>-->
+<!--                    <el-option label="已结束" value="3"></el-option>-->
+<!--                    <el-option label="进行中" value="2"></el-option>-->
+<!--                    <el-option label="未开始" value="1"></el-option>-->
+<!--                  </el-select>-->
+<!--                </el-form-item>-->
+<!--              </el-col>-->
+<!--              <el-col :span="8" :sm="12" :md="8">-->
+<!--                <el-form-item label="直播时间:">-->
+<!--                  <el-date-picker-->
+<!--                    v-model="liveDateRange"-->
+<!--                    type="daterange"-->
+<!--                    value-format="yyyy-MM-dd"-->
+<!--                    range-separator="至"-->
+<!--                    start-placeholder="开始日期"-->
+<!--                    end-placeholder="结束日期"-->
+<!--                    clearable-->
+<!--                    style="width: 250px"-->
+<!--                    @change="handleLiveDateChange"-->
+<!--                  ></el-date-picker>-->
+<!--                </el-form-item>-->
+<!--              </el-col>-->
+<!--              &lt;!&ndash; 查询 & 重置 按钮 &ndash;&gt;-->
+<!--              <el-col :span="4" class="button-group">-->
+<!--                <el-button type="primary" @click="getLive">查询</el-button>-->
+<!--                <el-button @click="resetLiveFilters">重置</el-button>-->
+<!--              </el-col>-->
+<!--            </el-row>-->
+
+<!--            &lt;!&ndash; 直播列表 &ndash;&gt;-->
+<!--            <el-table :data="liveList" border @selection-change="handleSelectionChange">-->
+<!--              <el-table-column type="selection" width="50" align="center" />-->
+<!--              &lt;!&ndash; 直播信息列 &ndash;&gt;-->
+<!--              <el-table-column label="直播名称" min-width="250">-->
+<!--                <template slot-scope="{ row }">-->
+<!--                  <div class="live-info">-->
+<!--                    &lt;!&ndash; 直播封面图 &ndash;&gt;-->
+<!--                    <img :src="row.liveImgUrl" class="live-cover" />-->
+<!--                    &lt;!&ndash; 直播名称 + 时间 &ndash;&gt;-->
+<!--                    <div class="live-text">-->
+<!--                      <div type="text"class="live-name">{{ row.liveName }}</div>-->
+<!--                      <div class="live-time">{{ row.startTime }}-{{row.finishTime}}</div>-->
+<!--                    </div>-->
+<!--                  </div>-->
+<!--                </template>-->
+<!--              </el-table-column>-->
+
+<!--              &lt;!&ndash; 直播状态 &ndash;&gt;-->
+<!--              <el-table-column label="直播状态" min-width="120">-->
+<!--                <template slot-scope="{ row }">-->
+<!--                  <div class="status-container">-->
+<!--                    <span :class="['status-dot', getStatusClass(row.status)]"></span>-->
+<!--                    <span>{{ getStatusText(row.status) }}</span>-->
+<!--                  </div>-->
+<!--                </template>-->
+<!--              </el-table-column>-->
+
+<!--              &lt;!&ndash; 讲师 &ndash;&gt;-->
+<!--              <el-table-column label="讲师" prop="teacher" min-width="120"></el-table-column>-->
+<!--            </el-table>-->
+<!--            <pagination-->
+<!--              v-show="liveTotal>0"-->
+<!--              :total="liveTotal"-->
+<!--              :page.sync="liveFiltersParam.pageNum"-->
+<!--              :limit.sync="liveFiltersParam.pageSize"-->
+<!--              @pagination="getLive"-->
+<!--            />-->
+<!--            &lt;!&ndash; 确认按钮 &ndash;&gt;-->
+<!--            <span slot="footer" class="dialog-footer">-->
+<!--              <el-button @click="dialogVisible = false">取消</el-button>-->
+<!--              <el-button type="primary" @click="confirmSelection">确认</el-button>-->
+<!--            </span>-->
+<!--          </el-dialog>-->
+<!--          &lt;!&ndash; 基础筛选项:始终显示 &ndash;&gt;-->
+<!--          <el-row :gutter="20">-->
+<!--            <el-col :span="8" :sm="12" :md="8">-->
+<!--              <el-form-item label="直播名称:" class="el-form-item-ellipsis">-->
+<!--                <el-input-->
+<!--                  v-model="filters.liveName"-->
+<!--                  placeholder="请选择直播"-->
+<!--                  @focus="openDialog"-->
+<!--                  type="text"-->
+<!--                  readonly-->
+<!--                >-->
+<!--                  <template slot="prepend">-->
+<!--                    <div v-if="filters.liveNames.length > 0" class="selected-lives">-->
+<!--                      <el-tag-->
+<!--                        v-for="(name, index) in filters.liveNames"-->
+<!--                        :key="index"-->
+<!--                        closable-->
+<!--                        @close="removeLive(index)"-->
+<!--                        class="live-name-tag"-->
+<!--                      >-->
+<!--                        {{ name }}-->
+<!--                      </el-tag>-->
+<!--                    </div>-->
+<!--                  </template>-->
+<!--                </el-input>-->
+<!--              </el-form-item>-->
+<!--            </el-col>-->
+<!--            <el-col :span="8" :sm="12" :md="8">-->
+<!--              <el-form-item label="用户名:" class="el-form-item-ellipsis">-->
+<!--                <el-input v-model="filters.userName" placeholder="请输入用户名"></el-input>-->
+<!--              </el-form-item>-->
+<!--            </el-col>-->
+<!--            <el-col :span="8" :sm="12" :md="8">-->
+<!--              <el-form-item label="手机号:" class="el-form-item-ellipsis">-->
+<!--                <el-input v-model="filters.userPhone" placeholder="请输入手机号"></el-input>-->
+<!--              </el-form-item>-->
+<!--            </el-col>-->
+<!--          </el-row>-->
+<!--          &lt;!&ndash; 高级筛选项 &ndash;&gt;-->
+<!--          <el-row :gutter="24" v-show="showAllFilters">-->
+<!--            <el-col :span="8" :sm="12" :md="8" >-->
+<!--              <el-form-item label="用户创建时间:">-->
+<!--                <el-date-picker-->
+<!--                  v-model="dateRange"-->
+<!--                  type="daterange"-->
+<!--                  range-separator="至"-->
+<!--                  start-placeholder="开始日期"-->
+<!--                  end-placeholder="结束日期"-->
+<!--                  value-format="yyyy-MM-dd"-->
+<!--                  @change="handleDateChange"-->
+<!--                  class="full-width-picker"-->
+<!--                />-->
+<!--              </el-form-item>-->
+<!--            </el-col>-->
+<!--            <el-col :span="8" :sm="12" :md="8">-->
+<!--              <el-form-item label="分享次数:" class="el-form-item-ellipsis">-->
+<!--                <el-input v-model="filters.shareCount" placeholder="请输入分享次数"></el-input>-->
+<!--              </el-form-item>-->
+<!--            </el-col>-->
+<!--            <el-col :span="8" :sm="12" :md="8">-->
+<!--              <el-form-item label="所属部门:" prop="deptId">-->
+<!--                <treeselect style="width:220px" v-model="filters.deptId" :options="deptOptions" :show-count="true" placeholder="请选择所属部门" />-->
+<!--              </el-form-item>-->
+<!--            </el-col>-->
+<!--          </el-row>-->
+<!--          <el-row :gutter="20" v-show="showAllFilters">-->
+<!--            <el-col :span="8" :sm="12" :md="8"> &lt;!&ndash; 每列 span 总和控制在 24 内 &ndash;&gt;-->
+<!--              <el-form-item label="跟进人:" class="el-form-item-ellipsis">-->
+<!--                <el-input v-model="filters.companyUserName" placeholder="请输入跟进人"></el-input>-->
+<!--              </el-form-item>-->
+<!--            </el-col>-->
+<!--            <el-col :span="8" :sm="12" :md="8">-->
+<!--              <el-form-item label="购买商品数:" class="el-form-item-ellipsis">-->
+<!--                <el-input v-model="filters.goodsCount" placeholder="请输入购买商品数"></el-input>-->
+<!--              </el-form-item>-->
+<!--            </el-col>-->
+<!--            <el-col :span="8" :sm="12" :md="8">-->
+<!--              <el-form-item label="提问数:" class="flex-container">-->
+<!--                <el-row>-->
+<!--                  <el-col :span="8" :sm="12" :md="8">-->
+<!--                    <el-input-number v-model="filters.minQuestionCount" placeholder="最小提问数" :min="0" style="width: 100%"></el-input-number>-->
+<!--                  </el-col>-->
+<!--                  <el-col :span="2" class="text-center">至</el-col>-->
+<!--                  <el-col :span="8" :sm="12" :md="8">-->
+<!--                    <el-input-number v-model="filters.maxQuestionCount" placeholder="最大提问数" :min="0" style="width: 100%"></el-input-number>-->
+<!--                  </el-col>-->
+<!--                </el-row>-->
+<!--              </el-form-item>-->
+<!--            </el-col>-->
+<!--          </el-row>-->
+
+<!--          &lt;!&ndash; 按钮组:始终显示 &ndash;&gt;-->
+<!--          <el-row>-->
+<!--            <el-col :span="24" class="button-group">-->
+<!--              <el-button type="primary" @click="getStudentData" class="custom-button">查询</el-button>-->
+<!--              <el-button @click="resetFilters" class="custom-button">重置</el-button>-->
+<!--              <el-button type="text" @click="toggleAllFilters" class="custom-button toggle-button">-->
+<!--                {{ showAllFilters ? '收起' : '展开' }}-->
+<!--                <i :class="showAllFilters ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>-->
+<!--              </el-button>-->
+<!--            </el-col>-->
+<!--          </el-row>-->
+<!--        </el-form>-->
+<!--      &lt;!&ndash;<div class="column-button-container">-->
+<!--        <el-button-->
+<!--          type="primary"-->
+<!--          @click="showColumnSettings = true"-->
+<!--          class="custom-column-button"-->
+<!--        >-->
+<!--          自定义列-->
+<!--        </el-button>-->
+<!--      </div>&ndash;&gt;-->
+<!--      &lt;!&ndash; 自定义列弹窗 &ndash;&gt;-->
+<!--      <el-drawer-->
+<!--        title="自定义列设置"-->
+<!--        :visible.sync="showColumnSettings"-->
+<!--        :before-close="handleClose"-->
+<!--        size="25%"-->
+<!--      >-->
+<!--        &lt;!&ndash; 抽屉内容区域 &ndash;&gt;-->
+<!--        &lt;!&ndash;<div style="display: flex; flex-direction: column; height: 100%;">-->
+<!--          &lt;!&ndash; 可拖拽的列设置区域 &ndash;&gt;-->
+<!--          <draggable-->
+<!--            v-model="columnOrder"-->
+<!--            group="columns"-->
+<!--            style="flex: 1; overflow-y: auto;"-->
+<!--          >-->
+<!--            <div v-for="col in columnOrder" :key="col.dataIndex" class="column-item">-->
+<!--              <el-checkbox v-model="col.status" true-label="ENABLE" false-label="DISABLE">-->
+<!--                {{ col.title }}-->
+<!--              </el-checkbox>-->
+<!--              <i class="el-icon-rank drag-handle"></i>-->
+<!--            </div>-->
+<!--          </draggable>-->
+
+<!--          &lt;!&ndash; 底部按钮区域 &ndash;&gt;-->
+<!--          <div style="padding: 12px; text-align: right; border-top: 2px solid;">-->
+<!--            <el-button @click="showColumnSettings = false" style="color: black;">取消</el-button>-->
+<!--            <el-button-->
+<!--              type="primary"-->
+<!--              @click="saveColumnsConfig"-->
+<!--              style="color: black; background-color: #409EFF; border-color: #409EFF;"-->
+<!--            >-->
+<!--              确定-->
+<!--            </el-button>-->
+<!--          </div>-->
+<!--        </div>&ndash;&gt;-->
+<!--      </el-drawer>-->
+
+
+<!--      <div style="overflow-x: auto; white-space: nowrap;">-->
+<!--        <el-table :data="liveStudentList" border style="min-width: 1200px;">-->
+<!--          &lt;!&ndash; 固定列,内容不换行 &ndash;&gt;-->
+<!--          <el-table-column prop="userName" label="客户名" fixed :style="{whiteSpace: 'nowrap',textOverflow: 'ellipsis',overflow: 'hidden',textAlign: 'center'}"></el-table-column>-->
+<!--          <el-table-column prop="liveName" label="直播间" fixed :show-overflow-tooltip="true" width=151px :min-width="'直播间'.length * 12 + 30":style="{whiteSpace: 'nowrap',textOverflow: 'ellipsis',overflow: 'hidden',textAlign: 'center'}"></el-table-column>-->
+<!--          <el-table-column prop="companyUserName" label="跟进人" fixed class="no-wrap-column"></el-table-column>-->
+<!--          <el-table-column prop="deptName" label="归属部门" width="150" show-overflow-tooltip/>-->
+<!--          <el-table-column prop="userCreateTime" label="客户创建时间" width="150" show-overflow-tooltip/>-->
+<!--          <el-table-column prop="userPhone" label="联系方式" width="150" show-overflow-tooltip/>-->
+<!--          <el-table-column prop="goodsCount" label="去下单实物商品" width="150" show-overflow-tooltip/>-->
+<!--          <el-table-column prop="shareCount" label="分享直播间次数" width="150" show-overflow-tooltip/>-->
+<!--          <el-table-column prop="questionCount" label="答题次数" width="150" show-overflow-tooltip/>-->
+
+<!--          &lt;!&ndash; 动态列 &ndash;&gt;-->
+<!--          &lt;!&ndash;<el-table-column-->
+<!--            v-for="col in filteredColumns"-->
+<!--            :key="col.dataIndex"-->
+<!--            :prop="col.dataIndex"-->
+<!--            :label="col.title"-->
+<!--            :width="col.width"-->
+<!--            :min-width="col.title.length * 12 + 30"-->
+<!--            :show-overflow-tooltip="true">-->
+<!--          </el-table-column>&ndash;&gt;-->
+
+<!--        </el-table>-->
+<!--        <pagination-->
+<!--          v-show="liveStudentTotal>0"-->
+<!--          :total="liveStudentTotal"-->
+<!--          :page.sync="filters.pageNum"-->
+<!--          :limit.sync="filters.pageSize"-->
+<!--          @pagination="getStudentData"-->
+<!--        />-->
+<!--      </div>-->
+<!--    </div>-->
+  </div>
+</template>
+
+
+<script>
+  import * as echarts from "echarts";
+  import Editor from '@/components/Editor/wang';
+  import {recentLive,liveTop,getTrendData,columns,updateColumns,queryStudentData} from "@/api/live/liveData";
+  import {selectLiveToStudent} from "@/api/live/live";
+  import { treeselect } from "@/api/company/companyDept";
+  import Treeselect from "@riophae/vue-treeselect";
+  import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+  import draggable from "vuedraggable";
+  export default {
+
+    name: "LiveData",
+    components: { Editor,draggable,Treeselect },
+    data() {
+      return {
+        // 部门树选项
+        deptOptions: [],
+        liveDateRange:"",
+        liveFiltersParam:{
+          liveName:"",
+          status:"",
+          startTime:"",
+          finishTime:"",
+          pageNum: 1,
+          pageSize: 10,
+        },
+        liveList: [],
+        dialogVisible: false,
+        span:8,
+        showAllFilters: true, // 默认收起
+        showAdvancedSearch: true, // 控制表单的显示
+        // 直播间总条数
+        liveTotal: 0,
+        //学员数据总条数
+        liveStudentTotal:0,
+        dateRange: [],//存选择的日期范围
+        filters: {
+          minQuestionCount: 0,  // 大于提问数
+          maxQuestionCount: 0,  // 小于提问数
+          liveNames: [],  // 存放已选的直播名称
+          liveIds: [],
+          liveName: '',
+          userName: '',
+          userPhone: '',
+          userCreateTime: '',
+          startTime: '', // 开始时间
+          finishTime: '',
+          shareCount: '',
+          deptName: '',
+          deptId:null,
+          companyUserName: '',
+          goodsCount: '',
+          questionCount: '',
+          pageNum: 1,
+          pageSize: 2
+        },
+        selectedLives: [], // 临时存储选中的直播数据
+        showColumnSettings: false, // 控制自定义列弹窗
+        columnOrder: [],
+        tempOrderedColumns : [],  // 用于渲染表格的列顺序
+        selectedColumns: [],  // 用于控制哪些列被选中
+        liveStudentList:[],
+        top10List: [],
+        selectedRank: 'highFlow',
+        redRankTypes: [
+          { label: '高流量', value: 'highFlow' },
+          { label: '高观看', value: 'highView' },
+          /*{ label: '高时长', value: 'highTime' },*/
+        ],
+        blackRankTypes: [
+          { label: '低观看', value: 'lowView' },
+          { label: '低流量', value: 'lowFlow' },
+        ],
+        selectedTimeRange: 'day',  // 默认选择自然天
+        selectedDate: null,  // 用户选择的日期
+        datePickerType: 'date',  // 默认日期选择类型
+        weekRange: 'yyyy-ww',  // 周选择的日期格式
+        queryParams: {
+          type: 'day',  // 用来存储时间筛选方式(如 day, week, month)
+          date: new Date().toISOString().split("T")[0],  // 用来存储用户选择的日期默认今天
+          category:"page_views"
+        },
+        chart: null,
+        trendData: {}, // 直播趋势数据
+        selectedMetric: "page_views", // 默认选中浏览量
+        lives: [],
+      };
+    },
+    computed: {
+
+      filteredColumns() {
+        return this.columnOrder.filter(col => col.status === "ENABLE");
+      },
+      changeLabel() {
+        switch (this.selectedTimeRange) {
+          case 'day': return '比前一天';
+          case 'week': return '比上周';
+          case 'month': return '比上月';
+          default: return '变化';
+        }
+      },
+      displayLives() {
+        return this.lives.slice(0, 4); // 最多只显示前 4 条数据
+      },
+
+      dateFormat() {
+        return this.selectedTimeRange === "day"
+          ? "yyyy-MM-dd"
+          : this.selectedTimeRange === "week"
+            ? "yyyy-MM-dd 至 yyyy-MM-dd"
+            : "yyyy-MM-dd";
+      },
+      valueFormat() {
+        return this.selectedTimeRange === "day"
+          ? "yyyy-MM-dd"
+          : this.selectedTimeRange === "week"
+            ? "yyyy-MM-dd"
+            : "yyyy-MM-dd";
+      },
+    },
+    created() {
+      this.getRecentLive();
+      this.getLiveTop();
+      this.getTrendData();
+      //this.getColumns();
+      this.getTreeselect();
+
+    },
+    mounted() {
+      this.selectedColumns = this.columnOrder.map((col) => col.prop); // 默认全选
+      this.selectedMetric = this.trendData[0]; // 默认选中第一个
+      this.initChart(); // 初始化折线图
+      if (!this.selectedMetric) {
+        this.selectedMetric = "page_views";
+      };
+      this.updateChart();
+    },
+    watch: {
+      // 如果 columnOrder 更新,确保 orderedColumns 跟随更新
+      columnOrder(newOrder) {
+        this.tempOrderedColumns = [...newOrder];
+      },
+    },
+
+    methods: {
+      handleLiveDateChange(value) {
+        if (value) {
+          this.liveFiltersParam.startTime = value[0];
+          this.liveFiltersParam.finishTime = value[1];
+        } else {
+          this.liveFiltersParam.startTime = null;
+          this.liveFiltersParam.finishTime = null;
+        }
+        console.log("直播时间范围:", this.liveFiltersParam.startTime, this.liveFiltersParam.finishTime);
+      },
+      handleDateChange(value) {
+        if (value) {
+          this.filters.startTime = value[0];
+          this.filters.finishTime = value[1];
+        } else {
+          this.filters.startTime = '';
+          this.filters.finishTime = '';
+        }
+        console.log("用户创建时间范围:", this.filters.startTime, this.filters.finishTime);
+      },
+      getTreeselect() {
+        treeselect().then((response) => {
+          this.deptOptions = response.data;
+          console.log(this.deptOptions)
+        });
+      },
+      getLive(){
+        selectLiveToStudent(this.liveFiltersParam).then(response => {
+          this.liveList = response.rows;
+          this.liveTotal = response.total;
+        });
+      },
+      resetLiveFilters(){
+        this.liveDateRange="",
+        this.liveFiltersParam = {
+          liveName:"",
+          status:"",
+          startTime:"",
+          finishTime:""
+        }
+      },
+      // 确认选择
+      confirmSelection() {
+        this.filters.liveNames = this.selectedLives.map(item => item.liveName);
+        this.filters.liveIds = this.selectedLives.map(item => item.liveId);
+        console.log(this.filters.liveIds)
+        this.dialogVisible = false; // 关闭弹框
+      },
+      // 移除已选直播
+      removeLive(index) {
+        this.filters.liveNames.splice(index, 1);
+        this.filters.liveIds.splice(index, 1);
+      },
+      // 处理表格选中
+      handleSelectionChange(selectedRows) {
+        this.selectedLives = selectedRows; // 存储选中的直播数据
+      },
+
+      openDialog() {
+        this.getLive();
+        this.dialogVisible = true;
+      },
+      toggleAllFilters() {
+        this.showAllFilters = !this.showAllFilters;
+      },
+      getStudentData() {
+        console.log("查询条件:", this.filters);
+        queryStudentData(this.filters).then(response => {
+          console.log(response)
+          this.liveStudentList = response.rows;
+          this.liveStudentTotal = response.total;
+          console.log(response.total)
+        });
+      },
+      resetFilters() {
+        this.filters = {
+          liveName: "",
+          liveNames:[],
+          liveIds:[],
+          phoneNumber: "",
+          wechatNickname: "",
+          customerId: "",
+          visitTime: [],
+          isWinner: "",
+          department: "",
+          customerTag: "",
+        };
+      },
+      handleClose(done) {
+        this.$confirm('确认关闭?')
+          .then(_ => {
+            done();
+          })
+          .catch(_ => {});
+      },
+      saveColumnsConfig() {
+        this.tempOrderedColumns = [...this.columnOrder];  // 更新 orderedColumns
+        console.log(this.tempOrderedColumns)
+        updateColumns(this.tempOrderedColumns).then(response=>{
+          //this.getColumns()
+          location.reload()
+          this.showColumnSettings = false;
+        })
+      },
+      /*getColumns(){
+        this.loading = true;
+        columns().then(response => {
+          this.columnOrder = response.data;
+          this.selectedColumns = response.data.map((col) => col.prop);
+          console.log(this.columnOrder)
+          this.loading = false;
+        });
+      },*/
+      getTrendData(){
+        getTrendData(this.queryParams).then(response=>{
+          this.trendData = response.data
+          this.updateChart()
+        })
+      },
+      // 更新折线图
+      getMetricTitle(metricKey) {
+        const titles = {
+          page_views: "浏览量",
+          unique_visitors: "访客数",
+          streams: "创建直播数",
+          total_views: "累计观看人次",
+          unique_viewers: "累计观看人数"
+        };
+        return titles [metricKey] || "未知指标";
+      },
+
+      // 点击卡片切换指标
+      changeMetric(metric) {
+        this.selectedMetric = metric;
+        // 更新 queryParams 中的 category
+        this.queryParams.category = metric;  // 默认值为 prevViews
+        this.updateChart();  // 更新图表
+        this.getTrendData();  // 重新请求数据
+      },
+      getLiveTop() {
+
+        liveTop({ rankType: this.selectedRank })
+          .then(response => {
+
+            if (response.data) {
+              // 遍历数据并添加排名字段
+              this.top10List = response.data.map((item, index) => ({
+                ...item,
+                rank: index + 1 // 排名从 1 开始
+              }));
+            } else {
+              this.top10List = [];
+            }
+          })
+          .catch(error => {
+          });
+      },
+      getStatusText(status) {
+        if (status === 1) return "待直播";
+        if (status === 2) return "直播中";
+        return "已结束";
+      },
+      getStatusClass(status) {
+        if (status === 1) return "status-upcoming";
+        if (status === 2) return "status-live";
+        return "status-ended";
+      },
+      getRecentLive(){
+        this.loading = true;
+        recentLive().then(response => {
+          this.lives = response.data;
+          this.loading = false;
+        });
+      },
+
+      getWeekRange(selectedWeek) {
+        let date = new Date(selectedWeek);
+
+        if (isNaN(date.getTime())) {
+          return "日期错误";
+        }
+
+        let dayOfWeek = date.getDay(); // 0(星期天)- 6(星期六)
+
+        // 计算周一
+        let startDate = new Date(date); // 创建一个新的 Date 实例
+        startDate.setDate(date.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1));
+
+        // 计算周日(确保 `endDate` 是新实例)
+        let endDate = new Date(startDate.getTime()); // 复制 `startDate`
+        endDate.setDate(startDate.getDate() + 6);
+
+
+        return `${this.formatDate(startDate)} 至 ${this.formatDate(endDate)}`;
+      },
+      changeDate(value) {
+        if (this.selectedTimeRange === 'week' && value) {
+          this.weekRange = this.getWeekRange(value);
+          console.log("周"+this.weekRange)
+        } else {
+          this.weekRange = '';  // 非周选择时清空周范围
+        }
+
+        // 更新 queryParams.date
+        if (value) {
+          this.queryParams.date = this.formatDate(value);
+        }
+        this.getTrendData();
+      },
+      formatDate(date) {
+        let date_ = new Date(date);
+        console.log(date_)
+        const year = date_.getFullYear();
+        const month = String(date_.getMonth() + 1).padStart(2, '0');
+        const day = String(date_.getDate()).padStart(2, '0');
+        return `${year}-${month}-${day}`;
+      },
+
+      // 获取一周的起始日期(周一)
+      getStartOfWeek(date) {
+        let parsedDate = new Date(date);
+        if (isNaN(parsedDate.getTime())) {
+          console.error("无效的日期:", date);
+          return null;
+        }
+
+        const day = parsedDate.getDay();
+        const diff = parsedDate.getDate() - day + (day === 0 ? -6 : 1); // 调整到周一
+        return new Date(parsedDate.setDate(diff));
+      },
+
+      getEndOfWeek(date) {
+        const startOfWeek = this.getStartOfWeek(date);
+        if (!startOfWeek) return null;
+
+        let endOfWeek = new Date(startOfWeek);
+        endOfWeek.setDate(startOfWeek.getDate() + 6); // 调整到周日
+        return endOfWeek;
+      },
+      computedChange(item) {
+        switch (this.selectedTimeRange) {
+          case 'day': return item.dailyChange;
+          case 'week': return item.weeklyChange;
+          case 'month': return item.monthlyChange;
+          default: return 0;
+        }
+      },
+      selectRank(type) {
+        this.selectedRank = type; // 只允许一个按钮被选中
+        this.getLiveTop();  // 重新获取排行榜数据
+      },
+      // 切换时间范围
+      changeTimeRange() {
+        // 根据选中的时间范围修改 datePicker 类型
+        if (this.selectedTimeRange === 'day') {
+          this.datePickerType = 'date';  // 自然天
+          this.weekRange = '';  // 清空周范围
+        } else if (this.selectedTimeRange === 'month') {
+          this.datePickerType = 'month';  // 自然月
+          this.weekRange = '';  // 清空周范围
+        } else if (this.selectedTimeRange === 'week') {
+          this.datePickerType = 'week';  // 自然周
+        }
+        // 立即弹出日期选择器
+        this.$nextTick(() => {
+          this.$refs.datePickerRef.focus();
+        });
+        // 重置已选日期
+        this.queryParams.type = this.selectedTimeRange;
+        this.selectedDate = null;
+        this.queryParams.date = '';  // 清空 queryParams 中的日期
+      },
+      setDefaultDate() {
+        // 如果用户没有选择日期,则使用当前日期
+        if (!this.selectedDate) {
+          this.selectedDate = this.formatDate(new Date());
+          this.queryParams.date = this.selectedDate;
+        }
+      },
+      // 切换日期时更新数据
+      updateTrendData() {
+        // 这里可以加接口请求,获取新的数据
+        this.getTrendData()
+        this.updateChart();
+      },
+      initChart() {
+        let chartDom = document.getElementById("liveChart");
+        if (!chartDom) return;
+        this.chart = echarts.init(chartDom);
+        this.updateChart();
+      },
+      // 更新折线图
+      updateChart() {
+        if (!this.chart) return;
+
+        // 获取当前选中的指标对应的数据
+        let metricKey = this.selectedMetric;
+        let metricTitle = this.getMetricTitle(metricKey); // 获取指标标题
+        let chartData = this.trendData.data || [];
+        let dates = this.trendData.dates || []; // 确保 dates 不是 null
+        let options = {
+          tooltip: {
+            trigger: "axis",
+            backgroundColor: "rgba(0, 0, 0, 0.7)",
+            textStyle: { color: "#fff" },
+            formatter: function (params) {
+              let date = params[0].name; // x 轴的日期
+              let value = params[0].value; // 对应的数值
+              return `${date}<br/>${params[0].seriesName}: ${value}`;
+            }
+          },
+          grid: {
+            left: "5%", right: "5%", bottom: "10%", top: "10%", containLabel: true
+          },
+          xAxis: {
+            type: "category",
+            data: this.trendData.dates || [],
+            axisLine: {lineStyle: {color: "#ddd"}}
+          },
+          yAxis: {
+            type: "value",
+            splitLine: {lineStyle: {type: "dashed", color: "#ddd"}}
+          },
+          series: [
+            {
+              name: metricTitle,
+              data: chartData,
+              type: "line",
+              smooth: true,
+              symbol: "circle",
+              symbolSize: 8,
+              itemStyle: {
+                color: "#007BFF",
+                borderColor: "#fff",
+                borderWidth: 2
+              },
+              lineStyle: {
+                width: 3,
+                color: "#007BFF"
+              },
+              areaStyle: {
+                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                  {offset: 0, color: "rgba(0, 123, 255, 0.5)"},
+                  {offset: 1, color: "rgba(0, 123, 255, 0)"}
+                ])
+              },
+              emphasis: {
+                focus: "series",
+                label: {
+                  show: true,
+                  formatter: "{c}",
+                  fontSize: 14,
+                  color: "#333",
+                  backgroundColor: "#fff",
+                  padding: [4, 6]
+                }
+              },
+              animationDuration: 1200,
+              animationEasing: "quadraticOut"
+            }
+          ]
+        };
+
+        this.chart.setOption(options);
+      },
+    }
+  };
+</script>
+
+<style scoped>
+  .app-container {
+    padding: 20px;
+  }
+
+  .title {
+    font-size: 18px;
+    font-weight: bold;
+    margin-bottom: 10px;
+  }
+  .live-container {
+    display: flex;
+    gap: 15px; /* 控制卡片之间的间距 */
+    overflow-x: auto; /* 允许横向滚动 */
+    padding-bottom: 10px;
+  }
+
+  /* 卡片样式 */
+  .live-card {
+    width: 380px;
+    height: 225px;
+    background: rgb(250,250,250);
+    border-radius: 10px;
+    padding: 15px;
+    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+  }
+
+  /* 直播状态 */
+  .status {
+    background: #d3d3d3;
+    color: #333;
+    font-size: 14px;
+    padding: 4px 8px;
+    border-radius: 5px;
+    display: inline-block;
+  }
+
+  /* 不同状态颜色 */
+  .status-upcoming {
+    background: #FFC107; /* 黄色 */
+    color: #333;
+  }
+
+  .status-live {
+    background: #28A745; /* 绿色 */
+    color: white;
+  }
+
+  .status-ended {
+    background: #D3D3D3; /* 灰色 */
+    color: #333;
+  }
+  /* 直播信息部分 */
+  .content {
+    background: rgb(250,250,250);
+    display: flex;
+    gap: 10px;
+    align-items: center;
+    width: 90%;
+    margin-top: 0px;
+  }
+
+  /* 直播封面图片 */
+  .cover-image {
+    width: 70px;
+    height: 70px;
+    border-radius: 10px;
+    object-fit: cover;
+  }
+
+  /* 直播文本信息 */
+  .info {
+    flex: 1;
+  }
+
+  .live-title {
+    font-size: 16px;
+    font-weight: bold;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    max-width: 250px; /* 防止标题过长 */
+  }
+
+  .time {
+    font-size: 14px;
+    color: #666;
+  }
+
+  /* 直播数据部分 */
+  .stats {
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+    flex-shrink: 0;
+    margin-top: 10px;
+  }
+
+  .stat-item {
+    text-align: center;
+    flex: 1;
+  }
+
+  .label {
+    font-size: 14px;
+    color: #888;
+  }
+
+  .value {
+    font-size: 18px;
+    font-weight: bold;
+    color: #333;
+  }
+  /* 直播趋势 */
+  .trend-section {
+    margin-top: 30px;
+  }
+  .trend-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+  .trend-cards {
+    display: flex;
+    gap: 15px;
+  }
+  .trend-card {
+    width: 280px;
+    hight: 100px;
+    border: 1px solid #ddd;
+    border-radius: 8px;
+    padding: 16px;
+    transition: all 0.3s;
+    position: relative;
+  }
+
+  .trend-card.active {
+    border-color: #007bff; /* 选中时边框变蓝 */
+    box-shadow: 0px 0px 10px rgba(0, 123, 255, 0.3);
+  }
+
+  .trend-card.active::after {
+    content: "✔";
+    position: absolute;
+    bottom: 8px;
+    right: 8px;
+    color: white;
+    background: #007bff;
+    border-radius: 50%;
+    width: 20px;
+    height: 20px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 14px;
+    font-weight: bold;
+  }
+  .trend-value {
+    font-size: 24px;
+    font-weight: bold;
+  }
+
+  /* 折线图 */
+  .chart {
+    width: 100%;
+    height: 300px;
+    margin-top: 20px;
+  }
+  .top10-section {
+    margin-top: 20px;
+    background: #fff;
+    padding: 16px;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  }
+  .live-name {
+    display: flex;
+    align-items: center;
+  }
+
+  .live-cover {
+    width: 80px;  /* 直播封面宽度 */
+    height: 45px; /* 直播封面高度 */
+    border-radius: 4px;
+    margin-right: 10px;
+    object-fit: cover;
+  }
+
+  .live-title {
+    font-size: 14px;
+    color: #333;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+  .ranking-tabs {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 20px;
+  }
+
+  .ranking-container {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin: 10px 0;
+    border-radius: 8px;
+    overflow: hidden;
+  }
+
+  .ranking-section {
+    display: flex;
+    align-items: center;
+    width: 50%;
+    padding: 10px 15px;
+  }
+
+  .red-rank {
+    background: linear-gradient(90deg, #ff6b6b, #ff8e8e);
+    justify-content: flex-start;
+  }
+
+  .black-rank {
+    background: linear-gradient(90deg, #4a4a4a, #6b6b6b);
+    justify-content: flex-end;
+  }
+
+  .rank-title {
+    font-size: 18px;
+    font-weight: bold;
+    color: white;
+    margin: 0 15px;
+  }
+
+  .rank-filters {
+    display: flex;
+  }
+
+  button {
+    background: rgba(255, 255, 255, 0.2);
+    border: 1px solid rgba(255, 255, 255, 0.5);
+    color: white;
+    font-size: 14px;
+    padding: 5px 12px;
+    margin: 0 5px;
+    border-radius: 15px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+  }
+
+  button.active {
+    background: white;
+    color: #ff4d4f;
+  }
+  .filter-container {
+    display: flex;
+    align-items: center;
+    gap: 8px; /* 控制两个组件的间距 */
+  }
+
+  .filter-label {
+    font-weight: bold;
+    white-space: nowrap; /* 防止换行 */
+  }
+
+
+  .student-section {
+    margin-top: 20px;
+    background: #fff;
+    padding: 16px;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  }
+   .column-item {
+     display: flex;
+     align-items: center;
+     justify-content: space-between;
+     padding: 8px;
+     border: 1px solid #ddd;
+     background: #f9f9f9;
+     margin-bottom: 5px;
+     cursor: grab;
+   }
+
+  .drag-handle {
+    cursor: grab;
+  }
+  /* 自定义列按钮的样式 */
+  .column-button-container {
+    text-align: right; /* 使按钮右对齐 */
+    margin-bottom: 10px; /* 为按钮和表格之间增加间距 */
+  }
+
+  .custom-column-button {
+    background-color: #409EFF;
+    color: black;
+    padding: 10px 20px;
+    border-radius: 5px;
+    margin-right: 10px; /* 如果有多个按钮,确保按钮之间有间距 */
+  }
+
+  /* 表格区域的样式 */
+  .student-section {
+    padding: 20px;
+  }
+
+  .el-table {
+    margin-top: 20px; /* 为表格添加顶部间距 */
+  }
+
+  .el-table-column {
+    /* 让列文本超出时显示省略号 */
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+  }
+
+  .el-table-column:hover {
+    cursor: pointer;
+  }
+  .no-wrap-column .cell {
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    text-align: center; /* 使列内容居中 */
+  }
+  .advanced-search {
+    margin-bottom: 10px;
+    padding: 15px;
+  }
+  .button-group {
+    display: flex;
+    justify-content: flex-end;
+    width: 100%;
+  }
+
+  /* 让按钮默认显示 */
+  .el-button {
+    opacity: 1; /* 确保默认状态可见 */
+    color: #606266; /* 默认文字颜色 */
+    border-color: #dcdfe6; /* 默认边框颜色 */
+    background-color: #f5f7fa; /* 默认背景色 */
+    transition: all 0.3s; /* 添加动画过渡 */
+  }
+
+  /* 鼠标悬停时高亮 */
+  .el-button:hover {
+    color: #409eff !important; /* 文字变蓝 */
+    border-color: #409eff !important; /* 边框变蓝 */
+    background-color: #ecf5ff !important; /* 背景变浅蓝 */
+  }
+
+  /* “展开/收起” 按钮特殊处理 */
+  .el-button[type="text"] {
+    background: none;
+    border: none;
+    color: #606266;
+  }
+
+  /* 鼠标悬停时高亮 */
+  .el-button[type="text"]:hover {
+    color: #409eff !important;
+  }
+  .el-form-item-ellipsis .el-input__inner,
+  .el-form-item-ellipsis .el-select .el-input__inner {
+    text-align: center; /* 输入框内容居中对齐 */
+  }
+
+  /* 控制 label 长度过长时显示省略号 */
+  .el-form-item-ellipsis .el-form-item__label {
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    width: 100%; /* 确保标签可以适配宽度 */
+    text-align: right;
+    vertical-align: middle;
+    float: left;
+    font-size: 14px;
+    color: #606266;
+    line-height: 40px;
+    padding: 0 10px 0 0;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    cursor: pointer;
+  }
+
+  /* 直播信息整体布局 */
+  .live-info {
+    display: flex;
+    align-items: center;
+  }
+
+  /* 直播封面图 */
+  .live-cover {
+    width: 80px;
+    height: 80px;
+    object-fit: cover;
+    border-radius: 6px;
+    margin-right: 10px;
+  }
+
+  /* 直播名称 + 时间 */
+  .live-text {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+  }
+
+
+  /* 直播时间 */
+  .live-time {
+    font-size: 12px;
+    color: #888;
+    margin-top: 4px;
+  }
+
+  /* 直播状态 */
+  .status-container {
+    display: flex;
+    align-items: center;
+  }
+
+  /* 状态圆点 */
+  .status-dot {
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    display: inline-block;
+    margin-right: 6px;
+  }
+  .live-name-input .el-input__inner {
+    white-space: normal;  /* 允许文本换行 */
+    word-wrap: break-word; /* 长文本自动换行 */
+    padding-left: 10px;
+  }
+
+  .live-name-tag {
+    margin-right: 5px;
+    display: inline-block; /* 确保标签显示为块级元素 */
+  }
+  .flex-container {
+    display: flex;
+    align-items: center;
+    gap: 10px; /* 调整 label 与输入框间距 */
+    width: 90%;
+  }
+
+  .ellipsis-label .el-form-item__label {
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    max-width: 80px; /* 你可以调整这个值 */
+    display: block;
+    padding: 0 10px 0 0;
+  }
+
+  .full-width-picker {
+    flex: 1; /* 让日期选择器填充剩余空间 */
+  }
+
+</style>
+