ソースを参照

直播代码提交

yuhongqi 5 日 前
コミット
b73b70b6cc

+ 1 - 1
src/api/live/liveAnchor.js

@@ -50,4 +50,4 @@ export function exportLiveAnchor(query) {
     method: 'get',
     params: query
   })
-}
+}

+ 1 - 1
src/api/live/liveGoods.js

@@ -55,7 +55,7 @@ export function exportLiveGoods(query) {
 // 导出直播商品
 export function listStoreProduct(data) {
   return request({
-    url: '/store/storeProduct/list',
+    url: '/live/liveGoods/liveList',
     method: 'get',
     params: data
   })

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

@@ -50,4 +50,4 @@ export function exportLiveMsg(query) {
     method: 'get',
     params: query
   })
-}
+}

+ 1 - 1
src/api/live/liveOrderitems.js

@@ -50,4 +50,4 @@ export function exportLiveOrderitems(query) {
     method: 'get',
     params: query
   })
-}
+}

+ 1 - 1
src/api/live/liveQuestion.js

@@ -50,4 +50,4 @@ export function exportLiveQuestion(query) {
     method: 'get',
     params: query
   })
-}
+}

+ 1 - 1
src/api/live/liveQuestionBank.js

@@ -33,4 +33,4 @@ export function deleteLiveQuestionBank(questionBankId) {
     url: '/live/liveQuestionBank/' + questionBankId,
     method: 'delete'
   })
-}
+}

+ 4 - 4
src/api/live/liveQuestionLive.js

@@ -54,18 +54,18 @@ export function getConfig(id) {
 }
 
 // 新增直播观看奖励设置
-export function addConfig(data) {
+export function addConfig(data,liveId) {
   return request({
-    url: '/live/config',
+    url: '/live/config'  + '?liveId=' + liveId,
     method: 'post',
     data: data
   })
 }
 
 // 修改直播观看奖励设置
-export function updateConfig(data) {
+export function updateConfig(data,liveId) {
   return request({
-    url: '/live/config',
+    url: '/live/config'  + '?liveId=' + liveId,
     method: 'put',
     data: data
   })

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

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

+ 14 - 14
src/components/LiveVideoUpload/single.vue

@@ -2,20 +2,20 @@
   <div>
     <el-form-item label="">
       <div class="upload_video" id="upload_video">
-        <el-upload
-          class="upload-demo"
-          ref="upload"
-          action="#"
-          :http-request="uploadVideoToTxPcdn"
-          accept=".mp4"
-          :limit="1"
-          :on-remove="handleRemove"
-          :on-change="handleChange"
-          :auto-upload="false"
-          :key="uploadKey"
-        >
-          <el-button slot="trigger" size="small" type="primary" >选取视频</el-button>
-          <el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">点击上传</el-button>
+<!--        <el-upload-->
+<!--          class="upload-demo"-->
+<!--          ref="upload"-->
+<!--          action="#"-->
+<!--          :http-request="uploadVideoToTxPcdn"-->
+<!--          accept=".mp4"-->
+<!--          :limit="1"-->
+<!--          :on-remove="handleRemove"-->
+<!--          :on-change="handleChange"-->
+<!--          :auto-upload="false"-->
+<!--          :key="uploadKey"-->
+<!--        >-->
+<!--          <el-button slot="trigger" size="small" type="primary" >选取视频</el-button>-->
+<!--          <el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">点击上传</el-button>-->
           <!-- 仅当showControl为true时显示视频库选取按钮 -->
           <el-button v-if="showControl" style="margin-left: 10px;" size="small" type="success" @click="openVideoLibrary">视频库选取</el-button>
           <!-- 线路一 -->

+ 15 - 0
src/utils/util.js

@@ -34,3 +34,18 @@ export function randomString(len) {
 }
 
 
+function isEmpty(str) {
+  // 检查是否为 null 或 undefined
+  if (str == null) return true;
+
+  // 检查是否为字符串类型
+  if (typeof str !== 'string') {
+    // 如果不是字符串,尝试转换为字符串再判断
+    return String(str).trim() === '';
+  }
+
+  // 字符串类型直接去除空格后判断
+  return str.trim() === '';
+}
+
+

+ 61 - 31
src/views/live/live/index.vue

@@ -11,28 +11,7 @@
           v-hasPermi="['live:live:add']"
         >新增</el-button>
       </el-col>
-<!--      <el-col :span="1.5">-->
-<!--        <el-button-->
-<!--          type="success"-->
-<!--          plain-->
-<!--          icon="el-icon-edit"-->
-<!--          size="mini"-->
-<!--          :disabled="single"-->
-<!--          @click="handleUpdate"-->
-<!--          v-hasPermi="['live:live:edit']"-->
-<!--        >修改</el-button>-->
-<!--      </el-col>-->
-<!--      <el-col :span="1.5">-->
-<!--        <el-button-->
-<!--          type="warning"-->
-<!--          plain-->
-<!--          icon="el-icon-download"-->
-<!--          size="mini"-->
-<!--          :loading="exportLoading"-->
-<!--          @click="handleExport"-->
-<!--          v-hasPermi="['live:live:export']"-->
-<!--        >导出</el-button>-->
-<!--      </el-col>-->
+
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
@@ -196,12 +175,22 @@
       <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button
+            v-if="scope.row.companyName != '总台'"
             size="mini"
             type="text"
             icon="el-icon-edit"
             @click="handleUpdate(scope.row)"
             v-hasPermi="['live:live:edit']"
           >修改</el-button>
+          <el-button
+            v-if="scope.row.companyName == '总台'"
+            size="mini"
+            type="text"
+            style="color: #00CC66"
+            icon="el-icon-edit"
+            @click="handleView(scope.row)"
+            v-hasPermi="['live:live:edit']"
+          >查看</el-button>
           <el-button
             size="mini"
             type="text"
@@ -299,16 +288,16 @@
     <el-dialog :title="title" :visible.sync="open" width="900px" append-to-body>
       <el-form ref="form" :model="form" :rules="rules" label-width="80px">
         <el-form-item label="直播名称" prop="liveName">
-          <el-input v-model="form.liveName" placeholder="请输入直播名称" />
+          <el-input v-model="form.liveName" placeholder="请输入直播名称" :disabled="isViewOnly"/>
         </el-form-item>
         <el-form-item label="显示类型" prop="showType">
-          <el-radio-group v-model="form.showType">
+          <el-radio-group v-model="form.showType" :disabled="isViewOnly">
             <el-radio :label="1">横屏</el-radio>
             <el-radio :label="2">竖屏</el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="直播类型" prop="liveType">
-          <el-radio-group v-model="form.liveType">
+          <el-radio-group v-model="form.liveType" :disabled="isViewOnly">
             <el-radio :label="1">直播</el-radio>
             <el-radio :label="2">录播</el-radio>
           </el-radio-group>
@@ -324,13 +313,14 @@
 <!--          </el-select>-->
 <!--        </el-form-item>-->
         <el-form-item label="直播描述" prop="liveDesc">
-          <Editor ref="myeditor" :height="300" @on-text-change="updateText"/>
+          <Editor ref="myeditor" :height="300" @on-text-change="updateText"  />
           <!--          <Editor v-model="form.liveDesc" :height="300" placeholder="直播描述" />-->
         </el-form-item>
-        <el-form-item label="录播视屏" prop="videoUrl" v-if="form.liveType == 2">
+        <el-form-item label="录播视屏" prop="videoUrl" v-if="form.liveType == 2" >
           <video-upload :fileKey.sync="form.fileKey" :fileSize.sync="form.fileSize"
                         :videoUrl.sync="form.videoUrl" :fileName.sync="form.fileName" :line_1.sync="form.lineOne"
                         :uploadType.sync="form.uploadType" :isTranscode.sync="form.isTranscode"
+                        :isViewOnly="isViewOnly"
                         ref="videoUpload"
                         :transcodeFileKey.sync="form.transcodeFileKey" @video-duration="handleVideoDuration"
                         @change="handleVideoChange"></video-upload>
@@ -359,6 +349,7 @@
                           v-model="form.startTime"
                           @change="timeChange"
                           type="datetime"
+                          :disabled="isViewOnly"
                           format="yyyy-MM-dd HH:mm:ss"
                           value-format="yyyy-MM-dd HH:mm:ss"
                           :picker-options="{
@@ -374,6 +365,7 @@
           <el-date-picker size="small"
                           v-model="form.finishTime"
                           type="datetime"
+                          :disabled="isViewOnly"
                           format="yyyy-MM-dd HH:mm:ss"
                           value-format="yyyy-MM-dd HH:mm:ss"
                           :picker-options="{
@@ -386,17 +378,17 @@
           </el-date-picker>
         </el-form-item>
         <el-form-item label="直播封面" prop="liveImgUrl">
-          <image-upload v-model="form.liveImgUrl" :limit="1" />
+          <image-upload v-model="form.liveImgUrl" :limit="1" :disabled="isViewOnly"/>
         </el-form-item>
         <el-form-item label="上下架" prop="isShow">
-          <el-radio-group v-model="form.isShow">
+          <el-radio-group v-model="form.isShow" :disabled="isViewOnly">
             <el-radio :label="1">上架</el-radio>
             <el-radio :label="2">下架</el-radio>
           </el-radio-group>
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button type="primary" @click="submitForm" v-show="!isViewOnly">确 定</el-button>
         <el-button @click="cancel">取 消</el-button>
       </div>
     </el-dialog>
@@ -437,6 +429,8 @@ export default {
   components: { Editor,VideoUpload },
   data() {
     return {
+      // 是否只读
+      isViewOnly:false,
       baseUrl: process.env.VUE_APP_BASE_API,
       uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
       isPrivate:null,
@@ -463,6 +457,7 @@ export default {
       title: "",
       // 是否显示弹出层
       open: false,
+      liveDesc:null,
       // 查询参数
       queryParams: {
         pageNum: 1,
@@ -750,6 +745,7 @@ export default {
 
     /** 新增按钮操作 */
     handleAdd() {
+      this.isViewOnly = false
       this.reset();
       this.open = true;
 
@@ -760,10 +756,19 @@ export default {
       this.title = "添加直播间";
     },
     updateText(text){
-      this.form.liveDesc=text
+      setTimeout(() => {
+        if (this.isViewOnly) {
+          this.$refs.myeditor.setText(this.liveDesc);
+          this.form.liveDesc=this.liveDesc;
+        } else {
+          this.form.liveDesc=text;
+        }
+      }, 1);
+
     },
     /** 修改按钮操作 */
     handleUpdate(row) {
+      this.isViewOnly = false
       this.reset();
       const liveId = row.liveId || this.ids
       getLive(liveId).then(response => {
@@ -771,6 +776,7 @@ export default {
         if(this.form.duration){
           this.form.durationTime = this.secondsToTime(this.form.duration)
         }
+        this.liveDesc = this.form.liveDesc
 
         setTimeout(() => {
           if(this.form.liveDesc==null){
@@ -784,6 +790,30 @@ export default {
         this.title = "修改直播间";
       });
     },
+    /** 修改按钮操作 */
+    handleView(row) {
+      this.isViewOnly = true
+      this.reset();
+      const liveId = row.liveId || this.ids
+      getLive(liveId).then(response => {
+        this.form = response.data;
+        if(this.form.duration){
+          this.form.durationTime = this.secondsToTime(this.form.duration)
+        }
+        this.liveDesc = this.form.liveDesc
+
+        setTimeout(() => {
+          if(this.form.liveDesc==null){
+            this.$refs.myeditor.setText("");
+          }else{
+            this.$refs.myeditor.setText(this.form.liveDesc);
+          }
+          this.form.videoUrl = row.videoUrl
+        }, 1);
+        this.open = true;
+        this.title = "查看直播间";
+      });
+    },
     /** 提交按钮 */
     submitForm() {
       if(this.form.liveId != null) { this.videoUrl = this.form.videoUrl; }

+ 13 - 2
src/views/live/liveConfig/barrage.vue

@@ -8,21 +8,24 @@
       </el-col>
       <el-col :span="1.5">
         <el-button
+          :disabled="isViewOnly"
           type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['live:task:add']"
         >新增</el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
-          type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['live:task:remove']"
+          type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple || isViewOnly" @click="handleDelete" v-hasPermi="['live:task:remove']"
         >删除</el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
+          :disabled="isViewOnly"
           type="warning" plain icon="el-icon-download" size="mini" :loading="exportLoading" @click="handleExport" v-hasPermi="['live:task:export']"
         >导出</el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
+          :disabled="isViewOnly"
           type="success" plain icon="el-icon-download" size="mini" :loading="exportLoading" @click="handleImport" v-hasPermi="['live:task:export']"
         >导入</el-button>
       </el-col>
@@ -40,9 +43,11 @@
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button
+            :disabled="isViewOnly"
             size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['live:task:edit']"
           >修改</el-button>
           <el-button
+            :disabled="isViewOnly"
             size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['live:task:remove']"
           >删除</el-button>
         </template>
@@ -124,6 +129,12 @@ import { listTaskBarrage, getTask, delTask, addTask, updateTask, exportTaskBarra
 import { getToken } from "@/utils/auth";
 
 export default {
+  props:{
+    isViewOnly: {
+      type: Boolean,
+      default: false,
+    }
+  },
   name: "Barrage",
   data() {
     return {
@@ -216,7 +227,7 @@ export default {
           this.liveId = this.$route.query.liveId;
         }
         if(this.liveId == null) {
-          this.$message.error("页面错误,请联系管理员");
+
           return;
         }
         this.liveAbled = true

+ 39 - 18
src/views/live/liveConfig/goods.vue

@@ -6,10 +6,10 @@
       <el-checkbox :indeterminate="isIndeterminate" v-model="allChecked" @change="toggleSelectAll">
         {{ multipleSelection.length > 0 ? `已选 ${multipleSelection.length} 条` : '选中本页' }}
       </el-checkbox>
-      <el-button  plain size="mini" @click="handleShelf">上架</el-button>
-      <el-button  plain size="mini" @click="handleUnshelf">下架</el-button>
-      <el-button  plain size="mini" @click="handleDeleteSelected">删除</el-button>
-      <el-button  plain type="mini" icon="el-icon-plus" @click="handleAddLiveGoods">添加商品</el-button>
+      <el-button  plain size="mini" @click="handleShelf" :disabled="isViewOnly">上架</el-button>
+      <el-button  plain size="mini" @click="handleUnshelf" :disabled="isViewOnly">下架</el-button>
+      <el-button  plain size="mini" @click="handleDeleteSelected" :disabled="isViewOnly">删除</el-button>
+      <el-button  plain type="mini" icon="el-icon-plus" @click="handleAddLiveGoods" :disabled="isViewOnly">添加商品</el-button>
     </div>
     <el-table
       ref="goodTable"
@@ -64,8 +64,9 @@
       >
         <template slot-scope="scope">
             <el-switch
+              :disabled="isViewOnly"
               v-model="scope.row.isShow"
-              @click.native.capture.prevent="handleSwitchClick(scope.row)"
+              @change="handleSwitchClick(scope.row)"
               active-color="#13ce66"
               inactive-color="#ff4949">
             </el-switch>
@@ -100,18 +101,21 @@
       >
         <template slot-scope="scope">
           <el-button
+            :disabled="isViewOnly"
             type="text"
             size="small"
             style="color: #0066FF;"
             @click="handleGoodStock(scope.row)"
           >调整库存</el-button>
           <el-button
+            :disabled="isViewOnly"
             type="text"
             size="small"
             style="color: #0066FF;"
             @click="handleGoodSale(scope.row)"
           >调整销量</el-button>
           <el-button
+            :disabled="isViewOnly"
             type="text"
             size="small"
             style="color: #F56C6C;"
@@ -264,6 +268,12 @@ import {
 } from "@/api/live/liveGoods";
 
 export default {
+  props:{
+    isViewOnly: {
+      type: Boolean,
+      default: false,
+    }
+  },
   data() {
     return {
       liveId: '',
@@ -272,7 +282,7 @@ export default {
       queryGoodParams: {
         pageNum: 1,
         pageSize: 10,
-        productName: null,
+        keywords: null,
         liveId: null,
         storeId: null
       },
@@ -353,30 +363,41 @@ export default {
       console.log('路由变化:', from.path, '->', to.path);
     },
     handleSwitchClick(row) {
+      if(this.isViewOnly) return
       // 1. 获取「即将切换到的目标状态」(当前状态取反)
-      const targetStatus = !row.isShow
+      const targetStatus = row.isShow;
+      const originalIsShow = !row.isShow
+      const goodsList = [row.goodsId];
       if (this.socket == null) {
-        row.isShow = targetStatus
         this.$message.error("请从直播间开启展示状态!");
+
+        this.$nextTick(() => {
+          row.isShow = originalIsShow;
+        });
         return;
       }
-      const goodsList = [row.goodsId];
       handleIsShowChange({"goodsIds":goodsList,"isShow":targetStatus,"liveId":this.liveId}).then(res=>{
         if(res.code == 200){
           row.isShow = res.isShow
           if (res.msg == "目前仅支持单一物品展示") {
             this.$message.error(res.msg)
-            return;
           }
-
-          const msg = {
-            cmd: 'goods',
-            data: {"liveId":this.liveId,"goodsId":goodsList[0],"status":targetStatus ? 1 : 0}
+          if (this.socket != null) {
+            const msg = {
+              cmd: 'goods',
+              data: {"liveId":this.liveId,"goodsId":goodsList[0],"status":targetStatus ? 1 : 0}
+            };
+            this.socket.send(JSON.stringify(msg));
           }
-          this.socket.send(JSON.stringify(msg));
-
+        } else {
+          row.isShow = originalIsShow;
         }
-      })
+      }).catch(err => {
+        // 网络错误时,同样恢复原始状态
+        row.isShow = originalIsShow;
+        // this.$set(row, 'isShow', originalIsShow); // 响应式修复
+        console.error("状态切换失败:", err);
+      });
     },
     handleShelf(){
       this.handleShelfOrUn(1)
@@ -461,7 +482,7 @@ export default {
     },
     handleGoodsSearch(){
       this.queryGoodParams.pageNum = 1
-      this.queryGoodParams.productName = this.searchTitle
+      this.queryGoodParams.keywords = this.searchTitle
       this.getStoreProductLists()
     },
     handleGoodsChange(goods) {

+ 7 - 1
src/views/live/liveConfig/idCard.vue

@@ -5,7 +5,7 @@
         <img :src="idCardUrl" alt="身份证" style="height: 150px; width: 150px" v-if="idCardUrl != null">
         <ImageUpload @input="handleUrl" type="image" :num="10" :width="150" :height="150" />
       </el-form-item>
-      <el-button type="primary" size="mini" @click="submit">保存</el-button>
+      <el-button type="primary" size="mini" @click="submit" v-show="!isViewOnly">保存</el-button>
     </el-form-item>
   </el-form>
 </template>
@@ -15,6 +15,12 @@ import ImageUpload from '@/components/ImageUpload/index';
 import {verifyIdInfo, getLive} from '@/api/live/live'
 
 export default {
+  props:{
+    isViewOnly: {
+      type: Boolean,
+      default: false,
+    }
+  },
   components: { ImageUpload },
   data() {
     return {

+ 7 - 2
src/views/live/liveConfig/index.vue

@@ -47,7 +47,7 @@
       <!-- 左边菜单 -->
         <div class="left-menu">
           <el-menu default-active="1" class="el-menu-vertical-demo" @select="handleSelect">
-            <el-menu-item :index="item.index" v-for="item in menuList" >
+            <el-menu-item :index="item.index" v-for="item in menuList">
               <span>{{item.name}}</span>
             </el-menu-item>
           </el-menu>
@@ -55,7 +55,7 @@
         <!-- 右边信息 -->
         <div class="right-info">
           <!-- 动态组件 -->
-          <component :is="currentComponent" ></component>
+          <component :is="currentComponent" :isViewOnly="isViewOnly"></component>
         </div>
       </div>
     </el-card>
@@ -94,6 +94,7 @@ export default {
   },
   data() {
     return {
+      isViewOnly: false,
       activeTab: 'watchReward',
       liveId: null,
       liveInfo: {},
@@ -125,6 +126,10 @@ export default {
     getLiving() {
       getLive(this.liveId).then(res => {
         this.liveInfo = res.data
+        if(this.liveInfo.companyId == null || this.liveInfo.companyId == ''){
+          this.isViewOnly = true
+          console.log(this.isViewOnly)
+        }
       })
     },
   }

+ 12 - 2
src/views/live/liveConfig/liveCoupon.vue

@@ -8,8 +8,8 @@
       </el-checkbox>
       <!--      <el-button  plain size="mini" @click="handleShelf">上架</el-button>-->
       <!--      <el-button  plain size="mini" @click="handleUnshelf">下架</el-button>-->
-      <el-button  plain size="mini" @click="handleDeleteSelected">删除</el-button>
-      <el-button  plain type="mini" icon="el-icon-plus" @click="handleAddLiveCoupon">添加优惠券</el-button>
+      <el-button  plain size="mini" @click="handleDeleteSelected" :disabled="isViewOnly">删除</el-button>
+      <el-button  plain type="mini" icon="el-icon-plus" @click="handleAddLiveCoupon" :disabled="isViewOnly">添加优惠券</el-button>
     </div>
     <el-table
       ref="couponTable"
@@ -56,6 +56,7 @@
       >
         <template slot-scope="scope">
           <el-switch
+            :disabled="isViewOnly"
             v-model="scope.row.isShow == 1"
             @click.native.capture.prevent="handleSwitchClick(scope.row)"
             active-color="#13ce66"
@@ -74,12 +75,14 @@
       >
         <template slot-scope="scope">
           <el-button
+            :disabled="isViewOnly"
             type="text"
             size="small"
             style="color: #0fae11;"
             @click="handleCouponBind(scope.row)"
           >绑定商品</el-button>
           <el-button
+            :disabled="isViewOnly"
             type="text"
             size="small"
             style="color: #F56C6C;"
@@ -222,6 +225,12 @@ import { listLiveGoods} from "@/api/live/liveGoods";
 
 
 export default {
+  props:{
+    isViewOnly: {
+      type: Boolean,
+      default: false,
+    }
+  },
   data() {
     return {
       liveId: '',
@@ -320,6 +329,7 @@ export default {
       console.log('路由变化:', from.path, '->', to.path);
     },
     handleSwitchClick(row) {
+      if(this.isViewOnly) return;
       // 1. 获取「即将切换到的目标状态」(当前状态取反)
       const targetStatus = !row.isShow
       const couponList = row.id;

+ 29 - 9
src/views/live/liveConfig/liveLotteryConf.vue

@@ -47,6 +47,7 @@
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
         <el-button
+          :disabled="isViewOnly"
           type="primary"
           plain
           icon="el-icon-plus"
@@ -61,7 +62,7 @@
           plain
           icon="el-icon-edit"
           size="mini"
-          :disabled="single"
+          :disabled="single || isViewOnly"
           @click="handleUpdate"
           v-hasPermi="['live:liveLotteryConf:edit']"
         >修改</el-button>
@@ -79,6 +80,7 @@
       </el-col>-->
       <el-col :span="1.5">
         <el-button
+          :disabled="isViewOnly"
           type="warning"
           plain
           icon="el-icon-download"
@@ -114,6 +116,7 @@
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-edit"
@@ -123,6 +126,7 @@
           >修改</el-button>
           <!-- 开始 -->
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-delete"
@@ -131,6 +135,7 @@
           >开始</el-button>
           <!-- 暂停 -->
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-delete"
@@ -139,6 +144,7 @@
           >暂停</el-button>
           <!-- 结算 -->
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-delete"
@@ -147,6 +153,7 @@
           >结算</el-button>
 
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-delete"
@@ -209,7 +216,7 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="抽奖ID" prop="lotteryId">
-              <el-input v-model="form1.lotteryId" placeholder="请输入抽奖ID" />
+              <el-input v-model="form1.lotteryId" placeholder="请输入抽奖ID" :disabled="isViewOnly"/>
             </el-form-item>
           </el-col>
           <el-col :span="12">
@@ -226,6 +233,7 @@
           <div slot="header" class="prize-header">
             <span><b>奖品等级 :{{ prize.prizeLevel }}</b></span>
             <el-button
+              :disabled="isViewOnly"
               v-if="form1.prizes.length > 1"
               @click="removePrize(index)"
               type="danger"
@@ -243,6 +251,7 @@
                 :rules="[{ required: true, message: '请输入商品', trigger: 'blur' }]">
 <!--                <el-input v-model="prize.productId" placeholder="请输入商品ID" />-->
                 <el-select
+                  :disabled="isViewOnly"
                   v-model="prize.productId"
                   :filterable="true"
                   clearable
@@ -255,11 +264,11 @@
                 >
                 <el-option
                   v-for="product in productOptions"
-                  :key="product.goodsId"
+                  :key="product.productId"
                   :label="product.productName"
-                  :value="product.goodsId"
+                  :value="product.productId"
                 />
-                <span style="float: left">{{ product.goodsId }}</span>
+                <span style="float: left">{{ product.productId }}</span>
                 <span style="margin-left: 30px ;">{{product.productName}}</span>
                 </el-select>
               </el-form-item>
@@ -269,7 +278,7 @@
                 label="奖品等级"
                 :prop="'prizes.' + index + '.prizeLevel'"
                 :rules="[{ required: true, message: '请输入奖品等级', trigger: 'blur' }]">
-                <el-input v-model="prize.prizeLevel" placeholder="请输入奖品等级" />
+                <el-input v-model="prize.prizeLevel" placeholder="请输入奖品等级" :disabled="isViewOnly"/>
               </el-form-item>
             </el-col>
           </el-row>
@@ -284,6 +293,7 @@
                 { type: 'number', message: '必须为数字值' }
               ]">
                 <el-input-number
+                  :disabled="isViewOnly"
                   v-model="prize.perLotteryNum"
                   :min="1"
                   :max="100"
@@ -300,6 +310,7 @@
                 { type: 'number', message: '必须为数字值' }
               ]">
                 <el-input-number
+                  :disabled="isViewOnly"
                   v-model="prize.totalLots"
                   :min="1"
                   controls-position="right"
@@ -327,6 +338,7 @@
 
         <div class="add-prize-btn">
           <el-button
+            :disabled="isViewOnly"
             @click="addPrize"
             type="primary"
             icon="el-icon-plus"
@@ -339,7 +351,7 @@
 
       <div slot="footer" class="dialog-footer">
         <el-button @click="cancel1">取 消</el-button>
-        <el-button type="primary" @click="submitForm1">确 定</el-button>
+        <el-button type="primary" @click="submitForm1" v-show="!isViewOnly">确 定</el-button>
       </div>
     </el-dialog>
 
@@ -370,8 +382,15 @@ import {
 } from '@/api/live/liveLotteryProductConf'
 import {info} from '@/api/live/liveUserLotteryRecord'
 import { listLiveGoods } from '@/api/live/liveGoods'
+import {isView} from "core-js/internals/array-buffer-view-core";
 
 export default {
+  props:{
+    isViewOnly: {
+      type: Boolean,
+      default: false,
+    }
+  },
   name: "LiveLotteryConf",
   components: { LiveLotteryProductConf },
   data() {
@@ -522,6 +541,7 @@ export default {
     );
   },
   methods: {
+    isView,
     getProducts(){
       const queryParam = {
           liveId : this.liveId
@@ -802,9 +822,9 @@ export default {
       });
     },
     submitForm1() {
+      console.log("form1")
+      console.log(this.form1)
       this.$refs["form1"].validate(valid => {
-        console.log("form1")
-        console.log(this.$refs["form1"])
         if (valid) {
           updateLiveLotteryProductConf(this.form1).then(response => {
             //200 成功

+ 14 - 1
src/views/live/liveConfig/liveRedConf.vue

@@ -55,6 +55,7 @@
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
         <el-button
+          :disabled="isViewOnly"
           type="primary"
           plain
           icon="el-icon-plus"
@@ -69,13 +70,14 @@
           plain
           icon="el-icon-edit"
           size="mini"
-          :disabled="single"
+          :disabled="single || isViewOnly"
           @click="handleUpdate"
           v-hasPermi="['live:liveRedConf:edit']"
         >修改</el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
+          :disabled="isViewOnly"
           type="warning"
           plain
           icon="el-icon-download"
@@ -112,6 +114,7 @@
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-edit"
@@ -121,6 +124,7 @@
           >修改</el-button>
           <!-- 开始 -->
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-delete"
@@ -129,6 +133,7 @@
           >开始</el-button>
           <!-- 暂停 -->
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-delete"
@@ -137,6 +142,7 @@
           >暂停</el-button>
           <!-- 结算 -->
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-delete"
@@ -145,6 +151,7 @@
           >结算</el-button>
 
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-delete"
@@ -199,6 +206,12 @@
 import { listLiveRedConf, getLiveRedConf, delLiveRedConf, addLiveRedConf, updateLiveRedConf, exportLiveRedConf } from "@/api/live/liveRedConf";
 
 export default {
+  props:{
+    isViewOnly: {
+      type: Boolean,
+      default: false,
+    }
+  },
   name: "LiveRedConf",
   data() {
     return {

+ 8 - 0
src/views/live/liveConfig/liveReplay.vue

@@ -7,6 +7,7 @@
       <el-switch
         v-model="replayForm.isPlaybackOpen"
         @change="handlePlaybackSwitch"
+        :disabled="isViewOnly"
       />
       <span class="switch-desc" style="color: #9c9c9c;margin-left: 10px">开启回放,直播结束后学员可查看回放视频</span>
     </div>
@@ -165,6 +166,12 @@ import {getLive, updateLive,} from '@/api/live/live'
 import {getLiveVideoByLiveId,} from '@/api/live/liveVideo'
 
 export default {
+  props:{
+    isViewOnly: {
+      type: Boolean,
+      default: false,
+    }
+  },
   name: "LiveReplay",
   data() {
     return {
@@ -299,6 +306,7 @@ export default {
     },
     // 直播回放开关变更处理
     handlePlaybackSwitch(val) {
+      if(this.isViewOnly) return;
       if (this.liveInfo.liveType != 1 && this.liveInfo.liveType != 3) {
         this.replayForm.isPlaybackOpen = !val;
         this.$message.error("直播回放开关仅支持直播");

+ 10 - 2
src/views/live/liveConfig/preview.vue

@@ -6,7 +6,7 @@
     <div class="playback-content">
       <div>
         <span>预告内容:</span>
-        <el-button class="upload-btn" type="normal" @click="handleUploadVideo">上传视频</el-button>
+        <el-button class="upload-btn" type="normal" @click="handleUploadVideo" v-show="!isViewOnly">上传视频</el-button>
         <!--        <el-button-->
         <!--          class="upload-btn"-->
         <!--          type="text"-->
@@ -84,6 +84,12 @@ import {getLiveVideoByLiveIdAndType,addLiveVideo} from '@/api/live/liveVideo'
 import VideoUpload from "@/components/LiveVideoUpload/index.vue";
 
 export default {
+  props:{
+    isViewOnly: {
+      type: Boolean,
+      default: false,
+    }
+  },
   name: "Preview",
   components: {VideoUpload},
   data() {
@@ -140,9 +146,11 @@ export default {
   methods: {
     submitForm() {
       this.open = false;
+      var line_1 = this.videoUrl;
+      line_1 = line_1.replace(/\.mp4$/, '.m3u8');
       const doParam = {
         liveId: this.liveId,
-        videoUrl: this.videoUrl,
+        videoUrl: line_1,
         videoType: 3,
         duration: this.form.duration,
         fileSize: this.form.fileSize,

+ 17 - 6
src/views/live/liveConfig/task.vue

@@ -44,6 +44,7 @@
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
         <el-button
+          :disabled="isViewOnly"
           type="primary"
           plain
           icon="el-icon-plus"
@@ -58,24 +59,26 @@
           plain
           icon="el-icon-edit"
           size="mini"
-          :disabled="single"
+          :disabled="single || isViewOnly"
           @click="handleUpdate"
           v-hasPermi="['live:task:edit']"
         >修改</el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
+
           type="danger"
           plain
           icon="el-icon-delete"
           size="mini"
-          :disabled="multiple"
+          :disabled="multiple || isViewOnly"
           @click="handleDelete"
           v-hasPermi="['live:task:remove']"
         >删除</el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
+          :disabled="isViewOnly"
           type="warning"
           plain
           icon="el-icon-download"
@@ -106,6 +109,7 @@
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-edit"
@@ -113,6 +117,7 @@
             v-hasPermi="['live:task:edit']"
           >修改</el-button>
           <el-button
+            :disabled="isViewOnly"
             size="mini"
             type="text"
             icon="el-icon-delete"
@@ -234,6 +239,12 @@ import {listLiveRedConfOn} from "@/api/live/liveRedConf";
 import {listLiveLotteryConfOn} from "@/api/live/liveLotteryConf";
 
 export default {
+  props:{
+    isViewOnly: {
+      type: Boolean,
+      default: false,
+    }
+  },
   name: "Task",
   data() {
     return {
@@ -353,7 +364,7 @@ export default {
           this.liveId = this.$route.query.liveId;
         }
         if(this.liveId == null) {
-          this.$message.error("页面错误,请联系管理员");
+
           return;
         }
         this.liveAbled = true
@@ -560,9 +571,9 @@ export default {
       }
       this.loading = true;
       listTask(this.queryParams).then(res => {
-        if(res.rows.length > 0) {
-          this.taskList = res.rows;
-        }
+
+        this.taskList = res.rows;
+
         this.total = res.total;
         this.loading = false;
       });

+ 22 - 11
src/views/live/liveConfig/watchReward.vue

@@ -8,7 +8,7 @@
     <!-- 开启观看奖励开关 -->
     <div class="reward-switch">
       <span class="switch-label">开启观看奖励</span>
-      <el-switch v-model="watchRewardForm.enabled"></el-switch>
+      <el-switch v-model="watchRewardForm.enabled" :disabled="isViewOnly"></el-switch>
     </div>
 
     <!-- 观看奖励设置 -->
@@ -31,14 +31,14 @@
 
         <!-- 观看时长 -->
         <el-form-item label="观看时长" prop="watchDuration">
-          <el-input v-model="watchRewardForm.watchDuration" placeholder="请输入观看时长" class="duration-input">
+          <el-input v-model="watchRewardForm.watchDuration" placeholder="请输入观看时长" class="duration-input" :disabled="isViewOnly">
             <template #append>分钟</template>
           </el-input>
         </el-form-item>
 
         <!-- 实施动作 -->
         <el-form-item label="实施动作" prop="action">
-          <el-select v-model="watchRewardForm.action" placeholder="请选择实施动作" style="width: 300px;">
+          <el-select v-model="watchRewardForm.action" placeholder="请选择实施动作" style="width: 300px;" :disabled="isViewOnly">
             <el-option
               v-for="item in actionOptions"
               :key="item.value"
@@ -62,7 +62,7 @@
             <!-- 现金红包设置 -->
             <!-- 红包发放方式   1固定金额 2随机金额 -->
             <el-form-item label="红包发放方式" prop="redPacketType">
-              <el-radio-group v-model="watchRewardForm.redPacketType">
+              <el-radio-group v-model="watchRewardForm.redPacketType" :disabled="isViewOnly">
                 <el-radio label="1">固定金额</el-radio>
                 <el-radio label="2">随机金额</el-radio>
               </el-radio-group>
@@ -70,17 +70,17 @@
 
             <!-- 红包金额 -->
             <el-form-item label="红包金额" prop="redPacketAmount">
-              <el-input v-model="watchRewardForm.redPacketAmount" placeholder="请输入红包金额"></el-input>
+              <el-input v-model="watchRewardForm.redPacketAmount" placeholder="请输入红包金额" :disabled="isViewOnly"></el-input>
             </el-form-item>
 
             <!-- 红包发放数量 -->
             <el-form-item label="红包发放数量" prop="redPacketCount">
-              <el-input v-model="watchRewardForm.redPacketCount" placeholder="红包数量+28888人数"></el-input>
+              <el-input v-model="watchRewardForm.redPacketCount" placeholder="红包数量+28888人数" :disabled="isViewOnly"></el-input>
             </el-form-item>
 
             <!-- 红包领取方式  1二维码核销 2微信提现 -->
             <el-form-item label="红包领取方式" prop="receiveMethod">
-              <el-radio-group v-model="watchRewardForm.receiveMethod">
+              <el-radio-group v-model="watchRewardForm.receiveMethod" :disabled="isViewOnly">
                 <el-radio label="1">二维码领取</el-radio>
                 <el-radio label="2">微信发放</el-radio>
               </el-radio-group>
@@ -92,6 +92,7 @@
             <!-- 积分值 -->
             <el-form-item label="积分值" prop="scoreAmount">
               <el-input
+                :disabled="isViewOnly"
                 v-model="watchRewardForm.scoreAmount"
                 placeholder="请输入积分值"                style="width: 300px;"
               ></el-input>
@@ -114,7 +115,7 @@
           <template v-if="watchRewardForm.action === '1'">
             <!-- 客服引导  1跟进企业微信 2不设置 -->
             <el-form-item label="客服引导" prop="showGuide">
-              <el-radio-group v-model="watchRewardForm.showGuide">
+              <el-radio-group v-model="watchRewardForm.showGuide" :disabled="isViewOnly">
                 <el-radio label="1">跟进企业微信</el-radio>
                 <el-radio label="2">不设置</el-radio>
               </el-radio-group>
@@ -123,6 +124,7 @@
             <!-- 客服引导语 -->
             <el-form-item label="客服引导语" prop="guideText">
               <el-input
+                :disabled="isViewOnly"
                 v-model="watchRewardForm.guideText"
                 placeholder="请输入客服引导语"                style="width: 300px;"
               ></el-input>
@@ -133,6 +135,7 @@
             <!-- 积分使用引导语 -->
             <el-form-item label="积分使用引导语" prop="scoreGuideText">
               <el-input
+                :disabled="isViewOnly"
                 v-model="watchRewardForm.scoreGuideText"
                 placeholder="请输入积分使用引导语"                style="width: 300px;"
               ></el-input>
@@ -141,6 +144,7 @@
             <!-- 积分使用引导链接 -->
             <el-form-item label="积分使用引导链接" prop="scoreGuideLink">
               <el-input
+                :disabled="isViewOnly"
                 v-model="watchRewardForm.scoreGuideLink"
                 placeholder="请输入积分使用引导链接"                style="width: 300px;"
               ></el-input>
@@ -158,7 +162,7 @@
 
         <!-- 保存按钮 -->
         <div class="form-actions">
-          <el-button type="primary" @click="saveWatchReward">保存</el-button>
+          <el-button type="primary" @click="saveWatchReward" v-show="!isViewOnly">保存</el-button>
         </div>
       </el-form>
     </div>
@@ -169,6 +173,12 @@
 import {addConfig, getConfig, updateConfig} from "@/api/live/liveQuestionLive";
 
 export default {
+  props:{
+    isViewOnly: {
+      type: Boolean,
+      default: false,
+    }
+  },
   data() {
     return {
       loading: true,
@@ -267,6 +277,7 @@ export default {
     }
   },
   created() {
+
   },
   methods: {
     getLiveConfig(){
@@ -284,13 +295,13 @@ export default {
           if (this.watchRewardForm.id == null) {
             // 调用保存观看奖励接口
             // 实现保存逻辑
-            addConfig(this.watchRewardForm).then(res => {
+            addConfig(this.watchRewardForm,this.liveId).then(res => {
               if (res.code == 200) {
                 this.msgSuccess("修改成功");
               }
             })
           } else {
-            updateConfig(this.watchRewardForm).then(response => {
+            updateConfig(this.watchRewardForm,this.liveId).then(response => {
               this.msgSuccess("修改成功");
             });
           }

+ 415 - 59
src/views/live/liveConsole/index.vue

@@ -6,7 +6,7 @@
       <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" :key="m.id">
+            <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">
@@ -101,7 +101,7 @@
             class="custom-video"
             style="display: block; background: #000; height: 40vh;"
           >
-            <source :src="videoUrl" type="video/mp4">
+            <source :src="videoUrl" type="application/x-mpegURL">
           </video>
           <!-- 自定义进度条容器 -->
           <div ref="progressBar" class="progress-container">
@@ -123,7 +123,7 @@
             playsinline
             style="display: block; background: #000; height: 40vh;"
           >
-            <source :src="videoUrl" type="video/mp4">
+            <source :src="videoUrl" type="application/x-mpegURL">
           </video>
         </div>
         <div style="border-radius: 5px; overflow: hidden;" v-else>
@@ -186,7 +186,7 @@
       <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 onlineUserList" :key="u.userId">
+            <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>
@@ -208,7 +208,7 @@
         </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 offlineUserList" :key="u.userId">
+            <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>
@@ -230,7 +230,7 @@
         </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 silencedUserList" :key="u.userId">
+            <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>
@@ -258,7 +258,7 @@
 </template>
 
 <script>
-import {blockUser, changeUserStatus, watchUserList} from '@/api/live/liveWatchUser'
+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'
@@ -328,6 +328,50 @@ export default {
       // ... 其他数据
       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() {
@@ -340,7 +384,6 @@ export default {
     this.getList()
     this.connectWebSocket()
     this.getLive()
-    console.log(this.liveId)
     this.searchQuery.liveId = this.liveId
   },
   computed: {
@@ -350,32 +393,14 @@ export default {
     companyId() {
       return this.$store.state.user.user.companyId
     },
-    onlineUserList() {
-      return this.userList.filter(u => u.online === 0)
-    },
     onlineLabel() {
-      if (this.onlineUserList.length > 0) {
-        return '在线(' + this.onlineUserList.length + ')'
-      }
-      return '在线'
-    },
-    offlineUserList() {
-      return this.userList.filter(u => u.online === 1)
+      return `在线(${this.userTotal.online})`;
     },
     offlineLabel() {
-      if (this.offlineUserList.length > 0) {
-        return '离线(' + this.offlineUserList.length + ')'
-      }
-      return '离线'
-    },
-    silencedUserList() {
-      return this.userList.filter(u => u.msgStatus === 1)
+      return `离线(${this.userTotal.offline})`;
     },
     silencedUserLabel() {
-      if (this.silencedUserList.length > 0) {
-        return '禁言(' + this.silencedUserList.length + ')'
-      }
-      return '禁言'
+      return `禁言(${this.userTotal.silenced})`;
     }
   },
   mounted() {
@@ -389,6 +414,24 @@ export default {
         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() {
@@ -401,7 +444,6 @@ export default {
     this.$nextTick(() => {
       const video = this.$refs.videoPlayer;
       if (video != null) {
-        video.play();
         this.initVideoPlayer(this.liveInfo.startTime)
       }
     })
@@ -457,12 +499,9 @@ export default {
               this.startTime = new Date(res.data.startTime).getTime()
               this.processInterval = setInterval(this.updateLiveProgress, 1000);
             } else {
-              console.log("getinfo")
               this.liveType = 2
               this.videoUrl = res.data.videoUrl;
               this.$nextTick(() => {
-                const video = this.$refs.videoPlayer;
-                video.play()
                 this.initVideoPlayer(res.data.startTime)
               })
             }
@@ -477,9 +516,18 @@ export default {
     },
     initVideoPlayer: function (startTime) {
       const video = this.$refs.videoPlayer;
-      console.log(123)
-
 
+      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;
@@ -543,7 +591,6 @@ export default {
 
       const currentTime = new Date().getTime();
       const elapsedTime = currentTime - this.startTime; // 总流逝时间(毫秒)
-      console.log(this.startTime)
 
       if (elapsedTime < 0) {
         elapsedTimeEl.textContent = '00:00:00';
@@ -658,27 +705,221 @@ export default {
       }
     },
     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.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() {
-      this.userList= []
-      this.userParams = {
-        pageNum: 1,
-        pageSize: 10,
-        liveId: this.liveId
-      }
-      this.msgList = []
+      // 重置各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,
@@ -755,7 +996,6 @@ export default {
       this.saveChatScrollPosition();
     },
     blockUser(u){
-      console.log(u)
       this.$confirm('是否确认封禁用户账号为:"' + u.nickName + '-' + u.userId + '"?', "警告", {
         confirmButtonText: "确定",
         cancelButtonText: "取消",
@@ -772,7 +1012,6 @@ export default {
           avatar: this.$store.state.user.user.avatar,
           nickName: this.$store.state.user.user.nickName
         }
-        console.log(msg)
         this.socket.send(JSON.stringify(msg))
         this.msgSuccess("封禁成功");
       }).catch(() => {});
@@ -795,6 +1034,8 @@ export default {
               user.msgStatus = u.msgStatus;
             }
           });
+          // 4. 关键:重新筛选所有Tab的显示列表,确保状态同步
+          this.refreshUserDisplayLists(u);
 
           let msg = u.msgStatus === 0 ? "已解禁" : "已禁言"
           this.msgSuccess(msg);
@@ -803,9 +1044,51 @@ export default {
         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() {
-      console.log('connectWebSocket')
-      console.log(this.liveWsUrl)
       this.$store.dispatch('initLiveWs', {
         liveWsUrl: this.liveWsUrl,
         liveId: this.liveId,
@@ -828,6 +1111,9 @@ export default {
             message.msgStatus = 0
           }
           delete message.params
+          if(this.msgList.length > 50){
+            this.msgList.shift()
+          }
           this.msgList.push(message)
           // 移动到底部
           this.$nextTick(() => {
@@ -836,16 +1122,49 @@ export default {
             }, 200)
           })
         } else if (cmd === 'entry' || cmd === 'out') {
-          let user = data
-          if(this.userList.length > 0){
-            this.userList = this.userList.filter(u => u.userId !== user.userId)
+          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条
           }
-          let online = 0
-          if (cmd === 'out') {
-            online = 1
+          // 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);
           }
-          let info = {"avatar":user.avatar, "nickName":user.nickName,"liveId":this.liveId, "msgStatus":user.msgStatus,"online": online,"userId":user.userId};
-          this.userList.push(info)
+
         } else if (cmd === 'live_start') {
 
         } else if (cmd === 'live_end') {
@@ -873,6 +1192,43 @@ export default {
 
       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();
@@ -950,11 +1306,11 @@ export default {
   outline: none !important; /* 移除焦点轮廓 */
 }
 /* 额外的兼容性隐藏 */
-.custom-video {
+.custom-video::-webkit-media-controls {
   display: none !important;
 }
 
-.custom-video {
+.custom-video::-webkit-media-controls-panel {
   display: none !important;
 }
 /* 进度条容器 */

+ 15 - 22
src/views/live/liveRewardRecord/index.vue

@@ -2,14 +2,13 @@
   <div class="app-container">
     <div class="reward-container">
       <!-- 时间步长选择 -->
-      <span class="demonstration">月份选择</span>
+      <span class="demonstration">月份选择(可多选)</span>
       <el-date-picker
         type="month"
-        clearable
-
         v-model="selectedTimeStep"
+        format="yyyy-MM"
         @change="handleTimeStepChange"
-        placeholder="选择一个日期">
+        placeholder="选择一个或多个日期">
       </el-date-picker>
 
       <el-button
@@ -75,7 +74,8 @@ export default {
   computed: {
   },
   mounted() {
-    this.initData()
+    this.selectedTimeStep.push(new Date())
+    this.initData(this.selectedTimeStep)
   },
   methods: {
     handleExport(){
@@ -83,8 +83,12 @@ export default {
         this.$message.error('请选择导出月份')
         return
       }
-      var date = this.checkDate();
-
+      var date = []
+      if (this.selectedTimeStep == null || this.selectedTimeStep.length === 0) {
+        date.push(new Date());
+      } else {
+        date = this.selectedTimeStep
+      }
       this.$confirm('是否确认导出所有直播积分记录?', "警告", {
         confirmButtonText: "确定",
         cancelButtonText: "取消",
@@ -97,20 +101,7 @@ export default {
         this.exportLoading = false;
       }).catch(() => {});
     },
-    checkDate(){
-      var date = []
-
-      if (this.selectedTimeStep == null || this.selectedTimeStep.length === 0) {
-        date.push(new Date());
-      } else {
-        date.push(this.selectedTimeStep);
-      }
-      return date
-    },
-    initData() {
-
-      var date = this.checkDate();
-
+    initData(date) {
       statistics({ 'step': date, companyId:0}).then(res => {
         this.timeSeries = res.timeSeries || []
         this.tableData = res.data || []
@@ -131,7 +122,9 @@ export default {
       })
     },
     handleTimeStepChange() {
-      this.initData()
+      const date = [];
+      date.push(this.selectedTimeStep)
+      this.initData(date)
     },
   }
 }

+ 3 - 0
src/views/live/liveVideo/index.vue

@@ -165,6 +165,9 @@ export default {
       this.loading = true;
       listLiveVideo(this.queryParams).then(response => {
         this.liveVideoList = response.rows;
+        this.liveVideoList.forEach(item => {
+          item.videoUrl = item.videoUrl.replace(".m3u8", ".mp4");
+        });
         this.total = response.total;
         this.loading = false;
       });