Pārlūkot izejas kodu

销售直播代码 直播间

yuhongqi 2 mēneši atpakaļ
vecāks
revīzija
5d6dea0288

+ 49 - 1
src/api/common.js

@@ -14,4 +14,52 @@ export function getTask(taskId) {
     method: 'get'
   })
 }
- 
+export function getSignature() {
+  return request({
+    url: '/common/getSignature',
+    method: 'post',
+  })
+}
+
+export function uploadHuaWeiVod(file) {
+  const formData = new FormData();
+  formData.append('file', file);
+
+  return request({
+    url: '/hwcloud/uploadHuaWeiVod',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+}
+
+export function uploadHuaWeiObs(file, progressCallback) {
+  const formData = new FormData();
+  formData.append('file', file);
+  return request({
+    url: '/hwcloud/uploadHuaWeiObs',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+}
+
+export function cdnStatistics(query) {
+  return request({
+    url: '/hwcloud/cdnStatistics',
+    method: 'get',
+    params:query
+  })
+}
+
+export function getTmpSecretKey(query) {
+  return request({
+    url: '/common/getTmpSecretKey',
+    method: 'get',
+    params:query
+  })
+}

+ 106 - 0
src/api/course/userVideo.js

@@ -0,0 +1,106 @@
+import request from '@/utils/request'
+
+// 查询课堂视频列表
+export function listUserVideo(query) {
+  return request({
+    url: '/course/userVideo/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询课堂视频详细
+export function getUserVideo(videoId) {
+  return request({
+    url: '/course/userVideo/' + videoId,
+    method: 'get'
+  })
+}
+
+export function getUserVideoItem(videoId) {
+  return request({
+    url: '/course/userVideo/getVideoDetails',
+    method: 'get',
+    params:{
+      videoId:videoId
+    }
+  })
+}
+
+// 新增课堂视频
+export function addUserVideo(data) {
+  return request({
+    url: '/course/userVideo/addVideo',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改课堂视频
+export function updateUserVideo(data) {
+  return request({
+    url: '/course/userVideo',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除课堂视频
+export function delUserVideo(videoId) {
+  return request({
+    url: '/course/userVideo/' + videoId,
+    method: 'delete'
+  })
+}
+
+// 导出课堂视频
+export function exportUserVideo(query) {
+  return request({
+    url: '/course/userVideo/export',
+    method: 'get',
+    params: query
+  })
+}
+
+
+export function getVideoListByCourseId(query) {
+  return request({
+    url: '/course/userVideo/getVideoListByCourseId',
+    method: 'get',
+    params: query
+  })
+}
+
+export function auditUserVideo(data) {
+  return request({
+    url: '/course/userVideo/auditVideo',
+    method: 'post',
+    data: data
+  })
+}
+
+export function putOn(videoIds) {
+  return request({
+    url: '/course/userVideo/putOn/' + videoIds,
+    method: 'post'
+  })
+}
+export function pullOff(videoIds) {
+  return request({
+    url: '/course/userVideo/pullOff/' + videoIds,
+    method: 'post'
+  })
+}
+
+export function getThumbnail(file) {
+  const formData = new FormData();
+  formData.append('file', file);
+  return request({
+    url: '/course/userVideo/getThumbnail',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+}

+ 54 - 0
src/api/course/videoResource.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 查询视频资源列表
+export function listVideoResource(query) {
+  return request({
+    url: '/course/videoResource/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询视频资源详细
+export function getVideoResource(resourceId) {
+  return request({
+    url: '/course/videoResource/' + resourceId,
+    method: 'get'
+  })
+}
+
+// 新增视频资源
+export function addVideoResource(data) {
+  return request({
+    url: '/course/videoResource',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改视频资源
+export function updateVideoResource(data) {
+  return request({
+    url: '/course/videoResource',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除视频资源
+export function deleteVideoResource(resourceId) {
+  return request({
+    url: '/course/videoResource/' + resourceId,
+    method: 'delete'
+  })
+}
+
+// 批量新增视频资源
+export function batchAddVideoResource(data) {
+  return request({
+    url: '/course/videoResource/batchAddVideoResource',
+    method: 'post',
+    data: data
+  })
+}
+

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

@@ -107,6 +107,14 @@ export function finishLive(data) {
     params: data
   })
 }
+// 复制录播直播间
+export function copyLive(data) {
+  return request({
+    url: '/live/live/copyLive',
+    method: 'get',
+    params: data
+  })
+}
 
 // 结束录播直播间
 export function startLive(data) {

+ 15 - 0
src/api/live/liveGoods.js

@@ -78,3 +78,18 @@ export function handleDeleteSelected(data) {
     data: data
   })
 }
+
+export function handleIsShowChange(data) {
+  return request({
+    url: '/live/liveGoods/handleIsShowChange',
+    method: 'post',
+    data: data
+  })
+}
+export function updateGoodsStock(data) {
+  return request({
+    url: '/live/liveGoods',
+    method: 'put',
+    data: data
+  })
+}

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

@@ -17,3 +17,11 @@ export function changeUserStatus(query) {
     params: query
   })
 }
+
+// 直播间用户禁言
+export function blockUser(query) {
+  return request({
+    url: '/live/liveWatchUser/blockUser/' + query,
+    method: 'get',
+  })
+}

+ 0 - 30
src/api/live/talentLive.js

@@ -1,30 +0,0 @@
-import request from '@/utils/request'
-
-// 检查直播间
-export function checkLive(data) {
-  return request({
-    url: '/live/live/checkLive',
-    method: 'post',
-    data: data
-  })
-}
-
-// 更新直播间推流地址
-export function createLive(data) {
-  return request({
-    url: '/live/live/create',
-    method: 'post',
-    data: data
-  })
-}
-
-// 更新直播间推流地址
-export function closeLiving(data) {
-  return request({
-    url: '/live/live/closeLiving',
-    method: 'post',
-    data: data
-  })
-}
-
-

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

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询直播间自动化任务配置列表
+export function listTask(query) {
+  return request({
+    url: '/live/task/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询直播间自动化任务配置详细
+export function getTask(id) {
+  return request({
+    url: '/live/task/' + id,
+    method: 'get'
+  })
+}
+
+// 新增直播间自动化任务配置
+export function addTask(data) {
+  return request({
+    url: '/live/task',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改直播间自动化任务配置
+export function updateTask(data) {
+  return request({
+    url: '/live/task',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除直播间自动化任务配置
+export function delTask(id) {
+  return request({
+    url: '/live/task/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出直播间自动化任务配置
+export function exportTask(query) {
+  return request({
+    url: '/live/task/export',
+    method: 'get',
+    params: query
+  })
+}

+ 276 - 0
src/components/LiveVideoUpload/index.vue

@@ -0,0 +1,276 @@
+<template>
+  <div>
+    <el-form-item label="视频上传">
+      <div class="upload_video" id="upload_video">
+        <el-upload
+          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>
+          <!-- 线路一 -->
+          <div class="progress-container" style="width:100% !important;">
+            <span class="progress-label">线路一</span>
+            <el-progress
+              style="margin-top: 5px;"
+              class="progress"
+              :text-inside="true"
+              :stroke-width="18"
+              :percentage="txProgress"
+              status="success">
+            </el-progress>
+          </div>
+
+          <!-- 线路二 -->
+<!--          <div class="progress-container">-->
+<!--            <span class="progress-label">线路er</span>-->
+<!--            <el-progress-->
+<!--              style="margin-top: 5px;"-->
+<!--              class="progress"-->
+<!--              :text-inside="true"-->
+<!--              :stroke-width="18"-->
+<!--              :percentage="hwProgress"-->
+<!--              status="success">-->
+<!--            </el-progress>-->
+<!--          </div>-->
+          <div slot="tip" class="el-upload__tip">只能上传mp4文件,且不超过500M</div>
+        </el-upload>
+      </div>
+    </el-form-item>
+    <el-form-item label="视频播放">
+      <video v-if="videoUrl" ref="myvideo" :src="videoUrl" id="video" width="100%" height="300px" controls></video>
+      <div v-if="fileName">视频文件名: {{ fileName }}</div>
+      <div v-if="fileKey">文件Key: {{ fileKey }}</div>
+      <div v-if="fileSize">文件大小(MB): {{ (fileSize / (1024 * 1024)).toFixed(2) }} MB</div>
+    </el-form-item>
+    <!-- 仅当showControl为true时显示播放线路选择器 -->
+    <el-form-item v-if="showControl" label="播放线路">
+      <el-radio-group v-model="localUploadType">
+        <el-radio :label="1" >线路一</el-radio>
+<!--        <el-radio :label="2" >线路二</el-radio>-->
+        <!--        <el-radio :label="3" >线路三</el-radio>-->
+      </el-radio-group>
+    </el-form-item>
+
+  </div>
+</template>
+
+<script>
+import {getSignature, uploadHuaWeiObs, uploadHuaWeiVod} from "@/api/common";
+import {getThumbnail} from "@/api/course/userVideo";
+import TcVod from "vod-js-sdk-v6";
+import { uploadObject } from "@/utils/cos.js";
+import { uploadToOBS } from "@/utils/obs.js";
+import Pagination from "@/components/Pagination";
+import { listVideoResource } from '@/api/course/videoResource';
+
+export default {
+  components: {
+    Pagination
+  },
+  props: {
+    videoUrl: {
+      type: String,
+      default: "",
+    },
+    fileKey: {
+      type: String,
+      default: "",
+    },
+    fileSize: {
+      type: Number,
+      default: null,
+    },
+    fileName: {
+      type: String,
+      default: "",
+    },
+    line_1: {
+      type: String,
+      default: "",
+    },
+    line_2: {
+      type: String,
+      default: "",
+    },
+    line_3: {
+      type: String,
+      default: "",
+    },
+    thumbnail: {
+      type: String,
+      default: "",
+    },
+    uploadType: {
+      type: Number,
+      default: null,
+    },
+    type: {
+      type: Number,
+      default: null,
+    },
+    isPrivate: {
+      type: Number,
+      default: 0,
+    },
+
+    // 使用一个变量控制显示,默认为true显示所有控制项
+    showControl: {
+      type: Boolean,
+      default: false,
+    }
+  },
+  data() {
+    return {
+      videoName:'',
+      isHidden : false,
+      localUploadType: this.uploadType,
+      duration: 0,
+      fileId: "",
+      uploadTypeOptions: [
+        { dictLabel: "线路一", dictValue: 1 }, // 腾讯pcdn
+        // { dictLabel: "线路二", dictValue: 2 }, // 华为云obs
+        // { dictLabel: "华为云VOD", dictValue: 4 },
+        // { dictLabel: "腾讯云VOD", dictValue: 5 },
+      ],
+      fileList: [],
+      txProgress: 0,
+      hwProgress: 0,
+      uploadLoading: false,
+      uploadKey: 0,
+    };
+  },
+  watch: {
+    localUploadType(newType) {
+      this.$emit("update:uploadType", newType);
+      this.$emit("change");
+    },
+    uploadType(newType) {
+      this.localUploadType = newType;
+    },
+  },
+  methods: {
+
+    handleChange(file, fileList) {
+      this.fileList = fileList;
+      this.getVideoDuration(file.raw);
+    },
+    getVideoDuration(file) {
+      const video = document.createElement("video");
+      video.preload = "metadata";
+      video.onloadedmetadata = () => {
+        window.URL.revokeObjectURL(video.src);
+        this.duration = parseInt(video.duration.toFixed(2));
+        console.log("视频时长=========>",this.duration);
+        this.$emit("video-duration", this.duration);
+        console.log("文件大小=====>",file.size);
+        this.$emit("update:fileSize", file.size);
+      };
+      video.src = URL.createObjectURL(file);
+    },
+    async submitUpload() {
+      if (this.fileList.length < 1) {
+        return this.$message.error("请先选取视频,再进行上传");
+      }
+      //同时上传个线路
+      await this.uploadVideoToTxPcdn();
+      // await this.uploadVideoToHwObs();
+      this.$emit("update:fileName", this.fileList[0].name);
+    },
+    //获取第一帧封面
+    async getFirstThumbnail(){
+      const file = this.fileList[0].raw;
+      getThumbnail(file).then(response => {
+        console.log("获取到第一帧为封面======>",response.url)
+        this.$emit("update:thumbnail", response.url);
+      })
+    },
+    //更新华为线路进度条
+    updateHwProgress(progress) {
+      this.hwProgress = progress;
+    },
+    //更新腾讯线路进度条
+    updateTxProgress(progressData) {
+      this.txProgress = Math.round(progressData.percent * 100);
+    },
+    //上传腾讯云Pcdn
+    async uploadVideoToTxPcdn() {
+      try {
+        const file = this.fileList[0].raw;
+        const data = await uploadObject(file, this.updateTxProgress,this.type);
+        console.log("腾讯COS返回========>",data);
+        console.log("isPrivate=======>",this.isPrivate)
+        let line_1='' ;
+        if (this.isPrivate===0){
+          line_1 = `${process.env.VUE_APP_VIDEO_LINE_1}${data.urlPath}`;
+        }else {
+          line_1 = `${process.env.VUE_APP_VIDEO_LINE_1}${data.urlPath}`;
+        }
+
+        let urlPathWithoutFirstSlash = data.urlPath.substring(1);
+        this.$emit("update:fileKey", urlPathWithoutFirstSlash);
+
+        console.log("文件key",urlPathWithoutFirstSlash);
+        console.log("组装URL========>",line_1);
+        this.$emit("update:videoUrl", line_1);
+        this.$emit("update:line_1", line_1);
+        // this.$emit("update:line_2", line_2);
+        this.$message.success("线路一上传成功");
+      } catch (error) {
+        this.$message.error("线路一上传失败");
+      }
+    },
+    //上传华为云Obs
+    async uploadVideoToHwObs() {
+      try {
+        const file = this.fileList[0].raw;
+        const data = await uploadToOBS(file, this.updateHwProgress,this.type);
+        console.log("华为OBS返回========>",data);
+        let line_2='' ;
+        if (this.isPrivate===0){
+          line_2 = `${process.env.VUE_APP_VIDEO_LINE_2}/${data.urlPath}`;
+        }else {
+          line_2 = `${process.env.VUE_APP_VIDEO_LINE_2}/${data.urlPath}`;
+        }
+        this.$emit("update:videoUrl", line_2);
+        this.$emit("update:line_2", line_2);
+        this.$message.success("线路二上传成功");
+      } catch (error) {
+        this.$message.error("线路二上传失败");
+      }
+    },
+    handleRemove(file, fileList) {
+      console.log(file, fileList.length);
+    },
+  }
+};
+</script>
+
+<style>
+.progress-container {
+  margin-bottom: 5px; /* 进度条之间的间距 */
+}
+
+.progress-label {
+  display: block;
+  font-weight: bold;
+  font-size: 13px;
+  color: #303331; /* 标签颜色,可以根据需要调整 */
+}
+
+/* 视频库选择对话框样式 */
+.library-search {
+  margin-bottom: 15px;
+}
+
+.el-table .el-table__row:hover {
+  cursor: pointer;
+}
+</style>

+ 96 - 0
src/utils/cos.js

@@ -0,0 +1,96 @@
+import COS from 'cos-js-sdk-v5';
+import { Message } from 'element-ui';
+import { getTmpSecretKey } from '@/api/common';
+
+console.log('环境变量:', process.env);
+console.log('NODE_ENV:', process.env.NODE_ENV);
+console.log('VUE_APP_COS_BUCKET:', process.env.VUE_APP_COS_BUCKET);
+console.log('VUE_APP_COS_REGION:', process.env.VUE_APP_COS_REGION);
+
+const config = {
+  Bucket: process.env.VUE_APP_COS_BUCKET,
+  Region: process.env.VUE_APP_COS_REGION,
+};
+console.log('COS配置:', config);
+
+// 上传到腾讯云cos
+export const uploadObject = async (file,onProgress,type,callBackUp) => {
+    try {
+        console.log(type);
+        const response = await getTmpSecretKey(); // 后台接口返回 密钥相关信息
+        console.log("Key  ",response);
+        const data = response.data;
+        const credentials = data && data.credentials;
+
+        if (!data || !credentials) {
+            console.error('未获取到参数');
+            return;
+        }
+
+        // 初始化
+        const cos = new COS({
+            getAuthorization: (options, callback) => {
+                callback({
+                    TmpSecretId: credentials.tmpSecretId,
+                    TmpSecretKey: credentials.tmpSecretKey,
+                    XCosSecurityToken: credentials.sessionToken,
+                    StartTime: data.startTime,
+                    ExpiredTime: data.expiredTime,
+                });
+            },
+        });
+
+        console.log("初始化成功")
+        let fileName = file.name || ""
+        const upload_file_name = new Date().getTime() + '.' + fileName.split(".")[fileName.split(".").length - 1];
+        let date =  new Date()
+        let year = date.getFullYear()
+        let month = date.getMonth() + 1
+        let strDate = date.getDate()
+        let uploadDay = `${year}${month}${strDate}`
+        let videoKey = `/userVideo/${uploadDay}/${upload_file_name}`
+        let courseKey = `/live/${uploadDay}/${upload_file_name}`
+        let key = type ===1 ? courseKey : videoKey;
+        console.log("开始上传")
+        return new Promise((resolve, reject) => {
+            console.log("uploadFile")
+            cos.uploadFile(
+                {
+                    Bucket: config.Bucket, /* 必须 */
+                    Region: config.Region, /* 存储桶所在地域,必须字段 */
+                    Key: key, // 文件名
+                    StorageClass: 'STANDARD', // 上传类型,可选
+                    Body: file, // 上传文件对象
+                    // onTaskReady: function (taskId) {
+                    //     // 用于中断分片上传回调
+                    //     console.log('Task ready:', taskId);
+                    //     callBackUp && callBackUp({cos,taskId})
+                    // },
+                    onProgress: function (progressData) {
+                        console.log('COS上传进度=======>:', JSON.stringify(progressData));
+                        onProgress(progressData);
+                    },
+                    // onFileFinish: function (err, data, options) {
+                    //     console.log(options.Key + '上传' + (err ? '失败' : '完成'));
+                    // },
+                },
+                (err, data) => {
+                    if (err) {
+                        reject(err);
+                    } else {
+                        // 将上传的key包含在返回的数据中
+                        const result = {
+                            ...data,
+                            urlPath: key
+                        };
+                        console.log('上传成功', result);
+                        resolve(result);
+                    }
+                }
+            );
+        });
+    } catch (error) {
+        console.error('Error during upload:', error);
+        throw error;
+    }
+};

+ 56 - 0
src/utils/obs.js

@@ -0,0 +1,56 @@
+import ObsClient from "esdk-obs-browserjs/src/obs";
+
+
+export const uploadToOBS = async(file,progressCallback,type) =>  {
+    try {
+        const obsClient = new ObsClient({
+          access_key_id: process.env.VUE_APP_OBS_ACCESS_KEY_ID,
+          secret_access_key: process.env.VUE_APP_OBS_SECRET_ACCESS_KEY,
+          server: process.env.VUE_APP_OBS_SERVER,
+          timeout: 1200,
+        });
+        let fileName = file.name || ""
+        const upload_file_name = new Date().getTime() + '.' + fileName.split(".")[fileName.split(".").length - 1];
+        let date =  new Date()
+        let year = date.getFullYear()
+        let month = date.getMonth() + 1
+        let strDate = date.getDate()
+        let uploadDay = `${year}${month}${strDate}`
+        let videoKey = `userVideo/${uploadDay}/${upload_file_name}`
+        let courseKey = `live/${uploadDay}/${upload_file_name}`
+        let key = type ===1 ? courseKey : videoKey;
+        var callback = function (transferredAmount, totalAmount, totalSeconds) {
+            // 获取上传进度百分比
+            const progress = parseInt(transferredAmount * 100.0 / totalAmount);
+            console.log("OBS上传进度=========>",progress);
+            if (progressCallback) {
+              progressCallback(progress);
+            }
+          };
+        return new Promise((resolve, reject) => {
+            //上传对象
+            obsClient.putObject({
+                Bucket: process.env.VUE_APP_OBS_BUCKET,//桶名称
+                Key: key,//文件名
+                Body: file,
+                ProgressCallback: callback,//进度回调
+            }, (err, result) => {
+                if(err){
+                    reject(err);
+                    console.error('Error-->' + err);
+                }else{
+                  // 将上传的key包含在返回的数据中
+                  const a = {
+                    ...result,
+                    urlPath: key
+                  };
+                  console.log('上传成功', a);
+                  resolve(a);
+                }
+            });
+        });
+    } catch (error) {
+        console.error('Error during upload:', error);
+        throw error;
+    }
+}

+ 134 - 46
src/views/live/live/index.vue

@@ -104,6 +104,12 @@
           <el-tag type="danger" v-if="scope.row.isShow == 2">下架</el-tag>
         </template>
       </el-table-column>
+      <el-table-column label="审核状态" align="center" prop="isAudit">
+        <template slot-scope="scope">
+          <el-tag type="danger" v-if="scope.row.isAudit == 0">审核未通过</el-tag>
+          <el-tag v-if="scope.row.isAudit == 1">审核通过</el-tag>
+        </template>
+      </el-table-column>
       <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button
@@ -120,30 +126,6 @@
             @click="handleConfig(scope.row)"
             v-hasPermi="['live:config:list']"
           >配置</el-button>
-          <el-button
-            v-if="scope.row.status == 2 && scope.row.liveType == 1"
-            size="mini"
-            type="text"
-            icon="el-icon-set-up"
-            @click="showLivingUrl(scope.row)"
-            v-hasPermi="['live:live:remove']"
-          >推流码</el-button>
-          <el-button
-            v-if="scope.row.status == 2"
-            size="mini"
-            type="text"
-            icon="el-icon-switch-button"
-            @click="handleEnded(scope.row)"
-            v-hasPermi="['live:config:edit']"
-          >结束</el-button>
-          <el-button
-            v-else
-            size="mini"
-            type="text"
-            icon="el-icon-monitor"
-            @click="handleStart(scope.row)"
-            v-hasPermi="['live:config:edit']"
-          >去直播</el-button>
           <el-button
             size="mini"
             type="text"
@@ -151,6 +133,42 @@
             @click="handleManage(scope.row)"
             v-hasPermi="['live:console:list']"
           >进入直播间</el-button>
+          <el-dropdown trigger="hover">
+            <el-button size="mini" type="text" icon="el-icon-more">
+              更多
+            </el-button>
+            <el-dropdown-menu slot="dropdown">
+              <!-- 结束按钮 -->
+              <el-dropdown-item
+                v-if="scope.row.status == 2 && scope.row.liveType == 1"
+                @click.native="showLivingUrl(scope.row)"
+              >
+                <i class="el-icon-switch-button"></i> 推流码
+              </el-dropdown-item>
+
+              <!-- 去直播按钮 -->
+              <el-dropdown-item
+                v-if="scope.row.status != 2"
+                @click.native="handleStart(scope.row)"
+              >
+                <i class="el-icon-monitor"></i> 去直播
+              </el-dropdown-item>
+
+              <!-- 进入直播间按钮 -->
+              <el-dropdown-item
+                v-if="scope.row.status == 2"
+                @click.native="handleEnded(scope.row)"
+              >
+                <i class="el-icon-service"></i> 结束
+              </el-dropdown-item>
+              <el-dropdown-item
+                @click.native="handleCopy(scope.row)"
+              >
+                <i class="el-icon-service"></i> 复制直播间
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+
         </template>
       </el-table-column>
     </el-table>
@@ -195,17 +213,37 @@
           <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">
-          <file-upload v-model="form.videoUrl" :limit="1" :file-size="3" :file-type="['mp4']" />
-          <el-button @click="getVideoDuration" v-loading="timeLoading">读取视屏时长</el-button>
-          <p style="margin: 0;padding: 0;" v-loading="timeLoading">视屏时长:<span style="color: #ff4949;">{{form.durationTime}}</span></p>
-        </el-form-item>
+        <video-upload
+          v-if="form.liveType == 2"
+          :type = "1"
+          :isPrivate = "isPrivate"
+          :fileKey.sync = "form.fileKey"
+          :fileSize.sync = "form.fileSize"
+          :videoUrl.sync="videoUrl"
+          :fileName.sync="form.fileName"
+          :line_2.sync="form.lineTwo"
+          :thumbnail.sync="form.thumbnail"
+          :uploadType.sync="form.uploadType"
+          :isTranscode.sync="form.isTranscode"
+          :transcodeFileKey.sync="form.transcodeFileKey"
+          @video-duration="handleVideoDuration"
+          @change="handleVideoChange"
+          ref="videoUpload"
+          append-to-body
+        />
         <el-form-item label="开始时间" prop="startTime">
           <el-date-picker size="small"
                           v-model="form.startTime"
                           @change="timeChange"
                           type="datetime"
+                          format="yyyy-MM-dd HH:mm"
                           value-format="yyyy-MM-dd HH:mm:ss"
+                          :picker-options="{
+                            timePickerOptions: {
+                              selectableRange: '00:00 - 23:59',
+                              format: 'HH:mm'
+                            }
+                          }"
                           placeholder="选择开始时间">
           </el-date-picker>
         </el-form-item>
@@ -213,8 +251,14 @@
           <el-date-picker size="small"
                           v-model="form.finishTime"
                           type="datetime"
-                          disabled
+                          format="yyyy-MM-dd HH:mm"
                           value-format="yyyy-MM-dd HH:mm:ss"
+                          :picker-options="{
+                            timePickerOptions: {
+                              selectableRange: '00:00 - 23:59',
+                              format: 'HH:mm'
+                            }
+                          }"
                           placeholder="视屏播放结束">
           </el-date-picker>
         </el-form-item>
@@ -259,17 +303,20 @@ import {
   handleShelfOrUn,
   handleDeleteSelected,
   finishLive,
-  startLive
+  startLive, copyLive
 } from "@/api/live/live";
 import Editor from '@/components/Editor/wang';
 import user from '@/store/modules/user';
+import VideoUpload from "@/components/LiveVideoUpload/index.vue";
 
 export default {
   name: "Live",
-  components: { Editor },
+  components: { Editor,VideoUpload },
   data() {
     return {
       baseUrl: process.env.VUE_APP_BASE_API,
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
+      isPrivate:null,
       // 遮罩层
       loading: true,
       // 导出遮罩层
@@ -313,7 +360,11 @@ export default {
         rtmpUrl: null,
       },
       // 表单参数
-      form: {},
+      form: {
+        uploadType: 2,
+        isTranscode:0,
+        transcodeFileKey:null
+      },
       // 表单校验
       rules: {
         liveName: [
@@ -347,12 +398,29 @@ export default {
       rtmpUrlVisible:false,
       serverName: '',
       livingCode:'',
+      videoUrl: "",
     };
   },
   created() {
     this.getList();
   },
   methods: {
+    beforeAvatarUpload(file) {
+      const isLt1M = file.size / 1024 / 1024 < 1;
+      if (!isLt1M) {
+        this.$message.error('上传图片大小不能超过 1MB!');
+      }
+      return   isLt1M;
+    },
+    handleAvatarSuccess(res, file) {
+      if(res.code==200){
+        this.form.thumbnail=res.url;
+        this.$forceUpdate()
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
     handleShelf(){
       if (this.multipleSelection.length > 0) {
         var liveList = []
@@ -446,20 +514,20 @@ export default {
         this.loading = false;
       });
     },
-    urlChange(url) {
-      this.form.videoUrl = url;
-      this.getVideoDuration();
+    handleVideoDuration(duration) {
+      this.form.duration = duration;
     },
-    getVideoDuration() {
-      this.timeLoading = true;
-      const videoElement = document.createElement('video');
-      videoElement.src = this.form.videoUrl;
-      videoElement.onloadedmetadata = () => {
-        this.form.duration = Math.floor(videoElement.duration);  // 秒
-        this.form.durationTime = this.secondsToTime(this.form.duration);
-        this.timeChange();
-        this.timeLoading = false;
-      };
+    handleVideoChange(){
+      console.log(this.videoUrl)
+      if(this.form.uploadType==1){
+        this.videoUrl = this.form.lineOne;
+      }else if(this.form.uploadType==2){
+        this.videoUrl = this.form.lineTwo;
+        this.form.videoUrl = this.form.lineTwo;
+      }else if(this.form.uploadType==3){
+        this.videoUrl = this.form.lineThree;
+      }
+      console.log(this.videoUrl)
     },
     changeDuration(e){
       this.form.duration = e.duration;
@@ -511,7 +579,10 @@ export default {
         showType: 1,
         liveType: 2,
         isShow: 1,
+        isTranscode:0,
+        transcodeFileKey:null
       };
+      this.videoUrl = "";
       this.resetForm("form");
     },
     /** 搜索按钮操作 */
@@ -549,6 +620,7 @@ export default {
       const liveId = row.liveId || this.ids
       getLive(liveId).then(response => {
         this.form = response.data;
+        this.videoUrl = this.form.videoUrl;
         if(this.form.duration){
           this.form.durationTime = this.secondsToTime(this.form.duration)
         }
@@ -565,8 +637,11 @@ export default {
     },
     /** 提交按钮 */
     submitForm() {
+      if(this.form.liveId != null) { this.videoUrl = this.form.videoUrl; }
+      if(this.form.liveType==2 && this.videoUrl.length == 0) {return this.$message.error("请上传视频");}
       this.$refs["form"].validate(valid => {
         if (valid) {
+          this.form.videoUrl = this.videoUrl;
           if (this.form.liveId != null) {
             updateLive(this.form).then(response => {
               this.msgSuccess("修改成功");
@@ -613,7 +688,20 @@ export default {
         finishLive({"liveId":row.liveId}).then(response=>{this.getList()})
       }).catch(() => {});
     },
+    handleCopy(row){
+      this.$confirm('是否确认复制直播间?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        copyLive({"liveId":row.liveId}).then(response=>{this.getList()})
+      }).catch(() => {});
+    },
     handleStart(row){
+      if(row.isShow == 2){
+        this.$message.error("直播间已下架")
+        return
+      }
       this.$confirm('是否确认开启直播间?', "警告", {
         confirmButtonText: "确定",
         cancelButtonText: "取消",

+ 113 - 6
src/views/live/liveConfig/goods.vue

@@ -57,6 +57,20 @@
         label="销量"
       ></el-table-column>
 
+      <el-table-column
+        prop="isShow"
+        label="显示状态"
+      >
+        <template slot-scope="scope">
+            <el-switch
+              v-model="scope.row.isShow"
+              @click.native.capture.prevent="handleSwitchClick(scope.row)"
+              active-color="#13ce66"
+              inactive-color="#ff4949">
+            </el-switch>
+        </template>
+      </el-table-column>
+
       <el-table-column
         prop="status"
         label="上下架"
@@ -74,6 +88,12 @@
         fixed="right"
       >
         <template slot-scope="scope">
+          <el-button
+            type="text"
+            size="small"
+            style="color: #0066FF;"
+            @click="handleGoodStock(scope.row)"
+          >调整库存</el-button>
           <el-button
             type="text"
             size="small"
@@ -166,11 +186,35 @@
         </div>
       </div>
     </el-dialog>
+
+    <el-dialog
+      title="调整库存"
+      :visible.sync="stockDialogVisible"
+      width="400px"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+    >
+      <el-form :model="stockForm" ref="stockForm" :rules="stockRules">
+        <el-form-item label="调整后库存" prop="newStock">
+          <el-input-number
+            v-model="stockForm.stock"
+            :min="0"
+            :max="999999"
+            controls-position="right"
+            style="width: 100%;"
+          ></el-input-number>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="stockDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="confirmStockChange">确 定</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import {addLiveGoods, delLiveGoods, listLiveGoods, listStoreProduct, handleShelfOrUn, handleDeleteSelected} from "@/api/live/liveGoods";
+import {addLiveGoods, delLiveGoods, listLiveGoods, listStoreProduct, handleShelfOrUn, handleDeleteSelected,handleIsShowChange,updateGoodsStock} from "@/api/live/liveGoods";
 
 export default {
   data() {
@@ -200,14 +244,55 @@ export default {
       multipleSelection: [],
       allChecked: false,
       isIndeterminate: false,
+      socket: null,
+      stockDialogVisible: false,
+      stockForm: {
+        goodsId: '',
+        stock: 0,
+        productId: 0,
+      },
+      stockRules: {
+        stock: [
+        { required: true, message: '请输入库存数量', trigger: 'blur' },
+        { type: 'number', min: 0, message: '库存数量不能小于0', trigger: 'blur' }
+      ]
+    }
     };
   },
   created() {
-    this.liveId = this.$route.params.liveId
+    if (this.$route.params.liveId) {
+      this.liveId = this.$route.params.liveId;
+    }else {
+      this.liveId = this.$route.query.liveId;
+    }
     this.goodsParams.liveId = this.liveId
     this.getLiveGoodsList();
+    this.socket = this.$store.state.liveWs[this.liveId]
   },
   methods: {
+    handleSwitchClick(row) {
+      // 1. 获取「即将切换到的目标状态」(当前状态取反)
+      const targetStatus = !row.isShow
+      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)
+          } else if(res.isShow){
+            if (this.socket == null) {
+              this.$message.error("请从直播间开启展示状态!");
+            } else {
+              const msg = {
+                cmd: 'goods',
+                data: {"liveId":this.liveId,"goodsId":goodsList[0]}
+              }
+              this.$store.state.liveWs[this.liveId].send(JSON.stringify(msg));
+            }
+          }
+        }
+      })
+    },
     handleShelf(){
       this.handleShelfOrUn(1)
     },
@@ -325,6 +410,32 @@ export default {
         this.getLiveGoodsList()
       })
     },
+    handleGoodStock(row){
+      this.stockForm.goodsId = row.goodsId;
+      this.stockForm.stock = row.stock;
+      this.stockForm.productId = row.productId;
+      this.stockDialogVisible = true;
+    },
+    // 添加确认修改库存方法
+    confirmStockChange() {
+      this.$refs.stockForm.validate((valid) => {
+        if (valid) {
+          updateGoodsStock({
+            goodsId: this.stockForm.goodsId,
+            stock: this.stockForm.stock,
+            productId: this.stockForm.productId
+          }).then(response => {
+            if (response.code === 200) {
+              this.$message.success('库存修改成功');
+              this.stockDialogVisible = false;
+              this.getLiveGoodsList(); // 重新获取列表数据
+            } else {
+              this.$message.error(response.msg || '库存修改失败');
+            }
+          });
+        }
+      });
+    },
     /** 处理行点击事件 */
     handleGoodsRowClick(row, column) {
       // 如果点击的是复选框列,不进行处理
@@ -346,10 +457,6 @@ export default {
     },
     getStoreProductLists() {
       this.queryGoodParams.liveId = this.liveId
-      console.log(this.goodsLiveList)
-      if(this.goodsLiveList.length > 0){
-        this.queryGoodParams.storeId = this.goodsLiveList[0].storeId
-      }
       listStoreProduct(this.queryGoodParams).then(response => {
         this.goodsList = response.rows
         this.goodsTotal = response.total

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

@@ -20,7 +20,7 @@ export default {
     return {
       test: "1test",
       idCardUrl: "",
-      liveId: this.liveInfo.liveId,
+      liveId: null,
     };
   },
   created() {

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

@@ -104,6 +104,7 @@ import Answer from './answer.vue';
 import Goods from './goods.vue';
 import WatchScore from './watchScore.vue';
 import IdCard from './idCard.vue';
+import Task from './task.vue';
 import LiveRedConf from './liveRedConf.vue'
 import LiveLotteryConf from './liveLotteryConf.vue'
 import { listLive, getLive, delLive, addLive, updateLive, exportLive,selectCompanyTalent,handleShelfOrUn,handleDeleteSelected } from "@/api/live/live";
@@ -118,7 +119,8 @@ export default {
     Answer,
     Goods,
     WatchScore,
-    IdCard
+    IdCard,
+    Task
   },
   data() {
     return {
@@ -132,7 +134,7 @@ export default {
         { name: '抽奖配置', label: '抽奖配置', index: 'liveLotteryConf'},
         // { name: '答题', label: '答题', index: 'answer'},
         { name: '直播商品', label: '直播商品', index: 'goods'},
-        // { name: '观看积分', label: '观看积分', index: 'watchScore'},
+        { name: '运营自动化', label: '运营自动化', index: 'task'},
         { name: '身份认证', label: '身份认证', index: 'idCard'},
       ],
 

+ 34 - 5
src/views/live/liveConfig/liveLotteryConf.vue

@@ -229,7 +229,7 @@
           <el-row :gutter="20">
             <el-col :span="12">
               <el-form-item
-                label="商品ID"
+                label="商品名称"
                 :prop="'prizes.' + index + '.productId'"
                 :rules="[{ required: true, message: '请输入商品', trigger: 'blur' }]">
 <!--                <el-input v-model="prize.productId" placeholder="请输入商品ID" />-->
@@ -458,8 +458,11 @@ export default {
     this.getDicts("sys_live_lottery_status").then(response => {
       this.lotteryStatusOptions = response.data;
     });
-
-    this.liveId = this.$route.query.liveId;
+    if (this.$route.params.liveId) {
+      this.liveId = this.$route.params.liveId;
+    }else {
+      this.liveId = this.$route.query.liveId;
+    }
     this.parentLiveId = this.liveId;
     this.queryParams.liveId = this.parentLiveId;
     if(this.queryParams.liveId){
@@ -586,6 +589,15 @@ export default {
     },
     /** 重置按钮操作 */
     resetQuery() {
+      this.queryParams = {
+          pageNum: 1,
+          pageSize: 10,
+          liveId: null,
+          require: null,
+          desc: null,
+          createTime: null,
+          lotteryStatus: null
+      }
       this.resetForm("queryForm");
       this.checkParentLiveId();
       this.handleQuery();
@@ -694,11 +706,17 @@ export default {
       }
       const doLotteryParam = {
         lotteryId: row.lotteryId,
-        lotteryStatus: status
+        lotteryStatus: status,
+        duration: row.duration
       };
       updateLiveLotteryConf(doLotteryParam).then(response => {
         if(response.code === 200){
-          this.$store.state.liveWs[this.liveId].sendWs("lottery",response.msg, row.lotteryId,this.liveId);
+          const msg = {
+            cmd: 'lottery',
+            data: doLotteryParam
+          };
+          this.$store.state.liveWs[this.liveId].send(JSON.stringify(msg));
+
           this.msgSuccess("修改成功");
         }else{
           this.msgError(response.msg);
@@ -729,6 +747,8 @@ export default {
     },
     submitForm1() {
       this.$refs["form1"].validate(valid => {
+        console.log("form1")
+        console.log(this.$refs["form1"])
         if (valid) {
           updateLiveLotteryProductConf(this.form1).then(response => {
             //200 成功
@@ -756,6 +776,15 @@ export default {
         }).then(() => {
           this.getList();
           this.msgSuccess("删除成功");
+          const msg = {
+            cmd: 'lottery',
+            data: {
+              lotteryId: row.lotteryId,
+              lotteryStatus: 2,
+              duration: row.duration
+            }
+          };
+          this.$store.state.liveWs[this.liveId].send(JSON.stringify(msg));
         }).catch(() => {});
     },
     /** 导出按钮操作 */

+ 39 - 23
src/views/live/liveConfig/liveRedConf.vue

@@ -30,22 +30,22 @@
           @keyup.enter.native="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="创建日期" prop="createTime">
-        <el-date-picker clearable size="small"
-          v-model="queryParams.createTime"
-          type="date"
-          value-format="yyyy-MM-dd"
-          placeholder="选择创建日期">
-        </el-date-picker>
-      </el-form-item>
-      <el-form-item label="修改日期" prop="updateTime">
-        <el-date-picker clearable size="small"
-          v-model="queryParams.updateTime"
-          type="date"
-          value-format="yyyy-MM-dd"
-          placeholder="选择修改日期">
-        </el-date-picker>
-      </el-form-item>
+<!--      <el-form-item label="创建日期" prop="createTime">-->
+<!--        <el-date-picker clearable size="small"-->
+<!--          v-model="queryParams.createTime"-->
+<!--          type="date"-->
+<!--          value-format="yyyy-MM-dd"-->
+<!--          placeholder="选择创建日期">-->
+<!--        </el-date-picker>-->
+<!--      </el-form-item>-->
+<!--      <el-form-item label="修改日期" prop="updateTime">-->
+<!--        <el-date-picker clearable size="small"-->
+<!--          v-model="queryParams.updateTime"-->
+<!--          type="date"-->
+<!--          value-format="yyyy-MM-dd"-->
+<!--          placeholder="选择修改日期">-->
+<!--        </el-date-picker>-->
+<!--      </el-form-item>-->
       <el-form-item>
         <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@@ -261,7 +261,8 @@ export default {
         desc: [
           { required: true, message: "描述不能为空", trigger: "blur" }
         ],
-      }
+      },
+      liveId: null,
     };
   },
   created() {
@@ -271,7 +272,12 @@ export default {
     this.getDicts("sys_live_red_type").then(response => {
       this.redTypeOptions = response.data;
     });
-    this.parentLiveId = this.$route.query.liveId;
+    if (this.$route.params.liveId) {
+      this.liveId = this.$route.params.liveId;
+    }else {
+      this.liveId = this.$route.query.liveId;
+    }
+    this.parentLiveId = this.liveId;
     this.queryParams.liveId = this.parentLiveId;
     if(this.queryParams.liveId){
       this.form.liveId = this.parentLiveId;
@@ -281,9 +287,7 @@ export default {
     this.getList();
   },
   computed: {
-    liveId() {
-      return this.$route.query.liveId;
-    }
+
   },
   methods: {
     handleStatusChange(row, status) {
@@ -323,14 +327,16 @@ export default {
       }
       const doRedParam = {
         redId: row.redId,
-        redStatus: status
+        redStatus: status,
+        totalLots: row.totalLots,
+        duration: row.duration
       };
       updateLiveRedConf(doRedParam).then(response => {
         if(response.code === 200){
           const msg = {
             cmd: 'red',
             data: doRedParam
-          }
+          };
           this.$store.state.liveWs[this.liveId].send(JSON.stringify(msg));
           this.msgSuccess("修改成功");
           console.log(1)
@@ -466,6 +472,16 @@ export default {
         }).then(() => {
           this.getList();
           this.msgSuccess("删除成功");
+          const msg = {
+            cmd: 'red',
+            data: {
+              redId: row.redId,
+              redStatus: 2,
+              totalLots: row.totalLots,
+              duration: row.duration
+            }
+          };
+          this.$store.state.liveWs[this.liveId].send(JSON.stringify(msg));
         }).catch(() => {});
     },
     /** 导出按钮操作 */

+ 602 - 0
src/views/live/liveConfig/task.vue

@@ -0,0 +1,602 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="直播间ID" prop="liveId">
+        <el-input
+          v-model="queryParams.liveId"
+          placeholder="请输入直播间ID"
+          clearable
+          :disabled="liveAbled"
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="任务名称" prop="taskName">
+        <el-input
+          v-model="queryParams.taskName"
+          placeholder="请输入任务名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="任务类型" prop="taskType">
+        <el-select v-model="queryParams.taskType" placeholder="请选择任务类型" clearable size="small">
+          <el-option v-for="i in taskTypeOptions" :key="i.value" :label="i.label" :value="i.value"></el-option>
+        </el-select>
+      </el-form-item>
+<!--      <el-form-item label="触发类型" prop="triggerType">-->
+<!--        <el-select v-model="queryParams.triggerType" placeholder="请选择触发类型" clearable size="small">-->
+<!--          <el-option v-for="i in triggerTypeOptions" :key="i.value" :label="i.label" :value="i.value"></el-option>-->
+<!--        </el-select>-->
+<!--      </el-form-item>-->
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option v-for="i in statusOptions" :key="i.value" :label="i.label" :value="i.value"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['shop:task: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="['shop:task:edit']"
+        >修改</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="['shop:task:remove']"
+        >删除</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="['shop:task:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="taskList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="任务名称" align="center" prop="taskName" />
+      <el-table-column label="任务类型" align="center" prop="taskType" :formatter="taskTypeFormatter" />
+      <el-table-column label="触发类型" align="center" prop="triggerType" :formatter="triggerTypeFormatter" />
+      <el-table-column label="触发时间" align="center" prop="triggerValue" :formatter="triggerValueFormatter" />
+      <el-table-column label="状态" align="center" prop="status" :formatter="statusFormatter" />
+      <el-table-column label="创建时间" align="center" prop="createdTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="更新时间" align="center" prop="updatedTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['shop:task:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['shop:task:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改直播间自动化任务配置对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="直播间ID" prop="liveId">
+          <el-input :disabled="liveAbled" v-model="form.liveId" placeholder="请输入直播间ID" />
+        </el-form-item>
+        <el-form-item label="任务名称" prop="taskName">
+          <el-input v-model="form.taskName" placeholder="请输入任务名称" />
+        </el-form-item>
+        <el-form-item label="任务类型" prop="taskType">
+          <el-select v-model="form.taskType" placeholder="请选择任务类型" @change="updateTaskType()">
+            <el-option v-for="i in taskTypeOptions" :key="i.value" :label="i.label" :value="i.value"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="商品选择" prop="content" v-if="form.taskType == 1">
+          <el-select v-model="form.content" placeholder="请选择商品" ref="selectRef" >
+            <el-option v-for="i in productOptions" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            <!-- 加载载中状态 -->
+            <div v-if="isLoading" class="loading-indicator">
+              <i class="el-icon-loading"></i>
+              <span>加载中...</span>
+            </div>
+
+            <!-- 没有更多数据 -->
+            <div v-if="!hasMore && !isLoading" class="no-more">
+              没有更多数据了
+            </div>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="触发时间" prop="content">
+          <el-time-picker
+            v-model="form.triggerValue"
+            :picker-options="{
+      selectableRange: '00:00:00 - 23:59:59'
+    }"
+            placeholder="任意时间点">
+          </el-time-picker>
+        </el-form-item>
+<!--        <el-form-item label="触发类型" prop="triggerType">-->
+<!--          <el-select v-model="form.triggerType" placeholder="请选择触发类型">-->
+<!--            <el-option label="请选择字典生成" value="" />-->
+<!--          </el-select>-->
+<!--        </el-form-item>-->
+<!--        <el-form-item label="触发值" prop="triggerValue">-->
+<!--          <el-input v-model="form.triggerValue" placeholder="请输入触发值" />-->
+<!--        </el-form-item>-->
+<!--        <el-form-item label="任务内容">-->
+<!--          <editor v-model="form.content" :min-height="192"/>-->
+<!--        </el-form-item>-->
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</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 @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listTask, getTask, delTask, addTask, updateTask, exportTask } from "@/api/live/task";
+import {listLiveGoods} from "@/api/live/liveGoods";
+
+export default {
+  name: "Task",
+  data() {
+    return {
+      taskTypeOptions:[
+        {
+          value: "1",
+          label: "定时卡片推荐商品"
+        },
+        // {
+        //   value: "1",
+        //   label: "请选择字典生成"
+        // },
+        // {
+        //   value: "1",
+        //   label: "请选择字典生成"
+        // }
+      ],
+      // triggerTypeOptions:[
+      //   {
+      //     value: "1",
+      //     label: "请选择字典生成"
+      //   },
+      //   {
+      //     value: "1",
+      //     label: "请选择字典生成"
+      //   }
+      // ],
+      statusOptions:[{
+        value: "0",
+        label: "禁用"
+      },
+        {
+          value: "1",
+          label: "启用"
+        }
+      ],
+      productOptions:[],
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      liveAbled: false,
+      // 直播间自动化任务配置表格数据
+      taskList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        liveId: null,
+        taskName: null,
+        taskType: null,
+        triggerType: null,
+        triggerValue: null,
+        content: null,
+        status: null,
+        createdTime: null,
+        updatedTime: null
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        liveId: [
+          { required: true, message: "直播间ID不能为空", trigger: "blur" }
+        ],
+        taskName: [
+          { required: true, message: "任务名称不能为空", trigger: "blur" }
+        ],
+        taskType: [
+          { required: true, message: "任务类型:1-定时推送卡片商品 2-定时发送红包 ", trigger: "change" }
+        ],
+        triggerType: [
+          { required: true, message: "触发类型:相对直播开始时间不能为空", trigger: "change" }
+        ],
+        triggerValue: [
+          { required: true, message: "触发值:绝对时间用yyyy-MM-dd HH:mm:ss,相对时间用分钟数不能为空", trigger: "blur" }
+        ],
+        status: [
+          { required: true, message: "状态:0-禁用 1-启用不能为空", trigger: "blur" }
+        ],
+        createdTime: [
+          { required: true, message: "创建时间不能为空", trigger: "blur" }
+        ],
+        updatedTime: [
+          { required: true, message: "更新时间不能为空", trigger: "blur" }
+        ]
+      },
+      liveId: null,
+      goodsParams: {
+        pageNum: 1,
+        pageSize: 10,
+        liveId: null
+      },
+      socket: null,
+      isLoading: false, // 是否正在加载
+      hasMore: true, // 是否还有更多数据
+    };
+  },
+  created() {
+    if (this.$route.params.liveId) {
+      this.liveId = this.$route.params.liveId;
+    }else {
+      this.liveId = this.$route.query.liveId;
+    }
+    if(this.liveId == null) {
+      this.$message.error("页面错误,请联系管理员");
+      return;
+    }
+    this.liveAbled = true
+    this.queryParams.liveId = this.liveId;
+    this.getList();
+    this.socket = this.$store.state.liveWs[this.liveId]
+  },
+  methods: {
+
+    statusFormatter(row, column, value){
+      if (!value) return '--'; // 空值处理
+      switch ( value){
+        case 0:
+          return "禁用";
+        case 1:
+          return "启用";
+        default:
+          return "--";
+      }
+    },
+    taskTypeFormatter(row, column, value){
+      if (!value) return '--'; // 空值处理
+      switch (value) {
+        case 1:
+          return "定时推送卡片商品";
+        case 2:
+          return "定时发送红包";
+        default:
+          return "--";
+      }
+    },
+    triggerTypeFormatter(row, column, value){
+      if (!value) return '--'; // 空值处理
+      switch (value) {
+        case 1:
+          return "相对直播开始时间";
+        case 2:
+          return "相对时间";
+        default:
+          return "--";
+      }
+    },
+    triggerValueFormatter(row, column, value) {
+      if (!value) return '--'; // 空值处理
+
+      // 创建日期对象(兼容时间戳和字符串)
+      let date;
+      if (typeof value === 'number') {
+        // 处理时间戳(注意:如果是10位时间戳需要乘以1000)
+        date = new Date(value.toString().length === 10 ? value * 1000 : value);
+      } else if (typeof value === 'string') {
+        // 处理字符串格式(尝试直接转换)
+        date = new Date(value);
+      } else {
+        return '格式错误';
+      }
+
+      // 检查日期是否有效
+      if (isNaN(date.getTime())) {
+        return '无效时间';
+      }
+      // 格式化日期为 "yyyy-MM-dd HH:mm:ss"
+      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 `${hours}:${minutes}:${seconds}`;
+    },
+    async updateTaskType(){
+      if (this.form.taskType == 1) {
+        if (this.isLoading || !this.hasMore) return;
+        this.isLoading = true;
+        this.goodsParams.liveId = this.liveId;
+        try{
+          await this.addGoodsList();
+        }catch ( err){
+          console.error('加载数据失败:', err);
+        }finally {
+          this.isLoading = false;
+        }
+      }
+    },
+    addGoodsList() {
+      listLiveGoods(this.goodsParams).then(res => {
+        if(res.rows.length > 0) {
+          res.rows.forEach(item => {
+            // 根据productName和goodsId组装成为label和value
+            this.productOptions.push({
+              value: item.goodsId,
+              label: item.productName
+            })
+          })
+          this.goodsParams.pageNum++;
+          // 判断是否还有更多数据
+          this.hasMore = this.productOptions.length < res.total;
+          if(this.hasMore) {
+            this.addGoodsList();
+          }
+        } else {
+          this.hasMore = false;
+        }
+      });
+    },
+    /** 查询直播间自动化任务配置列表 */
+    getList() {
+      if(this.liveId == null) {
+        this.$message.error("页面错误,请联系管理员");
+        return;
+      }
+      this.loading = true;
+      listTask(this.queryParams).then(res => {
+        if(res.rows.length > 0) {
+          this.taskList = res.rows;
+        }
+        this.total = res.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        liveId: this.liveId,
+        taskName: null,
+        taskType: null,
+        triggerType: null,
+        triggerValue: null,
+        content: null,
+        status: 1,
+        createdTime: null,
+        updatedTime: null
+      };
+      this.goodsParams={
+          pageNum: 1,
+          pageSize: 10,
+          liveId: this.liveId
+      }
+      this.productOptions = [];
+      this.hasMore = true;
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加直播间自动化任务配置";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getTask(id).then(response => {
+        this.form = response.data;
+        this.form.content = null;
+        this.form.taskType = null;
+        this.open = true;
+        this.title = "修改直播间自动化任务配置";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.form.liveId = this.liveId;
+      if(this.liveId == null) {
+        this.msgError("请选择直播间");
+        return;
+      }
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateTask(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addTask(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      if(this.socket == null) {
+        this.$message.error("请进入直播间后,在进行操作!")
+        return;
+      }
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除直播间自动化任务配置编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delTask(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+          const msg={
+            cmd:'delAutoTask',
+            data:row.absValue,
+            liveId:this.liveId,
+            userType:1
+          }
+          this.socket.send(JSON.stringify( msg))
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有直播间自动化任务配置数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportTask(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    },
+  }
+};
+</script>
+
+<style scoped>
+.loading-indicator {
+  padding: 10px;
+  text-align: center;
+  color: #606266;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.loading-indicator .el-icon-loading {
+  margin-right: 5px;
+  animation: rotate 1s linear infinite;
+}
+
+.no-more {
+  padding: 10px;
+  text-align: center;
+  color: #909399;
+  font-size: 12px;
+}
+
+@keyframes rotate {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+</style>

+ 29 - 29
src/views/live/liveConfig/watchReward.vue

@@ -49,9 +49,9 @@
         </el-form-item>
 
         <!-- 领取提示语 -->
-        <el-form-item label="领取提示语" prop="receivePrompt">
-          <el-input v-model="watchRewardForm.receivePrompt" placeholder="请输入领取提示语"></el-input>
-        </el-form-item>
+<!--        <el-form-item label="领取提示语" prop="receivePrompt">-->
+<!--          <el-input v-model="watchRewardForm.receivePrompt" placeholder="请输入领取提示语"></el-input>-->
+<!--        </el-form-item>-->
 
         <!-- 红包设置 -->
         <div>
@@ -98,12 +98,12 @@
             </el-form-item>
 
             <!-- 最大领取人数 -->
-            <el-form-item label="最大领取人数" prop="maxReceivers">
-              <el-input
-                v-model="watchRewardForm.maxReceivers"
-                placeholder="请输入最大领取人数"                style="width: 300px;"
-              ></el-input>
-            </el-form-item>
+<!--            <el-form-item label="最大领取人数" prop="maxReceivers">-->
+<!--              <el-input-->
+<!--                v-model="watchRewardForm.maxReceivers"-->
+<!--                placeholder="请输入最大领取人数"                style="width: 300px;"-->
+<!--              ></el-input>-->
+<!--            </el-form-item>-->
           </template>
         </div>
 
@@ -147,12 +147,12 @@
             </el-form-item>
 
             <!-- 引导语 -->
-            <el-form-item label="引导语" prop="guideText">
-              <el-input
-                v-model="watchRewardForm.guideText"
-                placeholder="请输入引导语"                style="width: 300px;"
-              ></el-input>
-            </el-form-item>
+<!--            <el-form-item label="引导语" prop="guideText">-->
+<!--              <el-input-->
+<!--                v-model="watchRewardForm.guideText"-->
+<!--                placeholder="请输入引导语"                style="width: 300px;"-->
+<!--              ></el-input>-->
+<!--            </el-form-item>-->
           </template>
         </div>
 
@@ -183,7 +183,7 @@ export default {
         // 观看时长
         watchDuration: '',
         // 实施动作
-        action: '1',
+        action: '2',
         // 领取提示语
         receivePrompt: '',
         // 红包发放方式(固定金额/随机金额)
@@ -200,8 +200,8 @@ export default {
         guideText: '',
         // 积分值
         scoreAmount: '',
-        // 最大领取人数
-        maxReceivers: '',
+        // // 最大领取人数
+        // maxReceivers: '',
         // 积分使用引导语
         scoreGuideText: '',
         // 积分使用引导链接
@@ -217,9 +217,9 @@ export default {
         action:[
           { required: true, message: '请选择实施动作', trigger: 'blur'}
         ],
-        receivePrompt:[
-          { required: true, message: '请输入领取提示语', trigger: 'blur'}
-        ],
+        // receivePrompt:[
+        //   { required: true, message: '请输入领取提示语', trigger: 'blur'}
+        // ],
         redPacketType:[
           { required: true, message: '请选择红包发放方式', trigger: 'blur'}
         ],
@@ -229,19 +229,19 @@ export default {
         receiveMethod:[
           { required: true, message: '请选择红包领取方式', trigger: 'blur'}
         ],
-        guideText:[
-          { required: true, message: '请输入客服引导语', trigger: 'blur'}
-        ],
+        // guideText:[
+        //   { required: true, message: '请输入客服引导语', trigger: 'blur'}
+        // ],
         showGuide:[
           { required: true, message: '请选择是否显示客服引导', trigger: 'blur'}
         ]
       },
       // 添加实施动作选项
       actionOptions: [
-        {
-          label: '现金红包',
-          value: '1'
-        },
+        // {
+        //   label: '现金红包',
+        //   value: '1'
+        // },
         {
           label: '积分红包',
           value: '2'
@@ -258,7 +258,7 @@ export default {
   methods: {
     getLiveConfig(){
       getConfig(this.liveId).then(response => {
-        if(response.code === 200 && response.data != null){
+        if(response.code === 200 && response.data != null && response.data.length > 0){
           this.watchRewardForm = JSON.parse(response.data)
         }
         this.loading = false

+ 39 - 8
src/views/live/liveConsole/index.vue

@@ -19,6 +19,7 @@
                     </el-col>
                     <el-col>
                       <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="changeUserState(m)">{{ m.msgStatus === 1 ? '解禁' : '禁言' }}</a>
+                      <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="blockUser(m)">拉黑</a>
                     </el-col>
                   </el-row>
                 </el-col>
@@ -173,6 +174,7 @@
                   width="100"
                   trigger="click">
                   <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
+                  <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
                   <i class="el-icon-more" slot="reference"></i>
                 </el-popover>
               </el-col>
@@ -195,6 +197,7 @@
                   trigger="click">
                   <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
                   <i class="el-icon-more" slot="reference"></i>
+                  <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
                 </el-popover>
               </el-col>
             </el-row>
@@ -215,6 +218,7 @@
                   width="100"
                   trigger="click">
                   <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
+                  <a style="cursor: pointer;color: #ff0000;margin-left:10px" @click="blockUser(u)">拉黑</a>
                   <i class="el-icon-more" slot="reference"></i>
                 </el-popover>
               </el-col>
@@ -229,15 +233,15 @@
 </template>
 
 <script>
-import { changeUserStatus, watchUserList } from '@/api/live/liveWatchUser'
+import {blockUser, changeUserStatus, watchUserList} from '@/api/live/liveWatchUser'
 import { getLiveVideoByLiveId } from '@/api/live/liveVideo'
-import { getLivingUrl,getLive } from '@/api/live/live'
+import {getLivingUrl, getLive, delLive} from '@/api/live/live'
 import { getLiveOrderTimeGranularity } from '@/api/live/liveOrder'
 import { listLiveMsg } from '@/api/live/liveMsg'
 import Hls from 'hls.js';
 import LiveLotteryConf from '@/views/live/liveConfig/liveLotteryConf.vue'
 import LiveRedConf from '@/views/live/liveConfig/liveRedConf.vue'
-import LiveGoods from '@/views/live/liveGoods/index.vue'
+import LiveGoods from '@/views/live/liveConfig/goods.vue'
 import LiveOrder from '@/views/live/liveOrder/index.vue'
 import echarts from 'echarts'
 
@@ -295,10 +299,12 @@ export default {
       startTime: null,
       processInterval: null,
       // ... 其他数据
-      chatScrollTop: 0 // 保存聊天滚动位置
+      chatScrollTop: 0, // 保存聊天滚动位置,
+      liveId: null,
     }
   },
   created() {
+    this.liveId = this.$route.params.liveId
     // this.getLiveVideo()
     this.getList()
     this.connectWebSocket()
@@ -306,9 +312,6 @@ export default {
     this.searchQuery.liveId = this.liveId
   },
   computed: {
-    liveId() {
-      return this.$route.params.liveId;
-    },
     userId() {
       return this.$store.state.user.user.userId
     },
@@ -530,7 +533,12 @@ export default {
     },
 
     handleClickRed(){
-      this.$router.push('/live/liveConfig/' + this.liveId)
+      this.$router.push({
+        name: 'LiveRedConf',
+        query: {
+          liveId: this.liveId
+        }
+      })
     },
     handleClickLottery(){
       this.$router.push({
@@ -680,6 +688,29 @@ export default {
     manageRightScroll() {
       this.saveChatScrollPosition();
     },
+    blockUser(u){
+      console.log(u)
+      this.$confirm('是否确认封禁用户账号为:"' + u.nickName + '-' + u.userId + '"?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return blockUser(u.userId);
+      }).then(() => {
+        let msg = {
+          msg: "",
+          liveId: this.liveId,
+          userId: u.userId,
+          userType: 0,
+          cmd: 'blockUser',
+          avatar: this.$store.state.user.user.avatar,
+          nickName: this.$store.state.user.user.nickName
+        }
+        console.log(msg)
+        this.socket.send(JSON.stringify(msg))
+        this.msgSuccess("封禁成功");
+      }).catch(() => {});
+    },
     changeUserState(u) {
       // 修改状态
       changeUserStatus({liveId: u.liveId, userId: u.userId}).then(response => {

+ 4 - 2
src/views/live/liveOrder/liveOrderDetails.vue

@@ -752,7 +752,6 @@ export default {
       getLiveOrder(orderId).then(response => {
         this.item = response.data;
         this.tuiMoneyLogs = response.tuiMoneyLogs;
-        this.getlistOrderitem(orderId);
         // this.getlogList(orderId);
         // this.getPayment(orderId);
         this.msgForm.userId=response.data.userId;
@@ -788,6 +787,9 @@ export default {
     },
 
     getlistOrderitem(orderId){
+      this.prod = null
+      this.showProd=[]
+      this.showList = true
       listOrderitem(orderId).then(response => {
         this.prod = response.rows;
         if (this.prod.length > 0) {
@@ -806,7 +808,7 @@ export default {
     //     this.pay = response.data;
     //   });
     // }
-  }
+  },
 }
 
 </script>

+ 0 - 646
src/views/live/talentLive/index.vue

@@ -1,646 +0,0 @@
-
-<template>
-  <div v-loading="isLoading">
-    <div class="red-box-content" >
-      <!-- 直播名称和关注 -->
-<!--        <span>直播名称:</span>-->
-<!--        <span>关注:</span>-->
-      <el-button v-show="status" @click="createLive" class="live-console-btn">开启直播 </el-button>
-      <el-button v-show="!status" @click="showLivingUrl" class="live-console-btn">推流地址</el-button>
-      <el-button v-show="!status" @click="closeLiving" class="live-console-btn">关闭直播</el-button>
-<!--      <el-button v-show="true" @click="createLive" class="live-console-btn">开启直播 </el-button>-->
-<!--      <el-button v-show="true" @click="showLivingUrl" class="live-console-btn">推流地址</el-button>-->
-<!--      <el-button v-show="true" @click="closeLiving" class="live-console-btn">关闭直播</el-button>-->
-
-      <!-- 视频流和TAB区域 -->
-      <el-row :gutter="20" >
-        <el-col :span="4">
-          <!-- 用户列表 start -->
-          <el-col class="live-console-col" >
-            <el-tabs class="live-console-tab-left" v-model="tabLeft.activeName" @tab-click="handleClick" :stretch="true">
-              <el-tab-pane :label="onlineLabel" name="online">
-                <el-scrollbar ref="manageLeftRef_online" style="width: 100%;">
-                  <el-row style="margin-top: 10px" type="flex" align="middle" v-for="u in onlineUserList">
-                    <el-col :span="20">
-                      <el-row type="flex" align="middle">
-                        <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
-                        <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
-                        <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
-                      </el-row>
-                    </el-col>
-                    <el-col :span="4" >
-                      <el-popover
-                        width="100"
-                        trigger="click">
-                        <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
-                        <i class="el-icon-more" slot="reference"></i>
-                      </el-popover>
-                    </el-col>
-                  </el-row>
-                </el-scrollbar>
-              </el-tab-pane>
-              <el-tab-pane :label="offlineLabel" name="offline">
-                <el-scrollbar ref="manageLeftRef_offline" style="width: 100%;">
-                  <el-row style="margin-top: 10px" type="flex" align="middle" v-for="u in offlineUserList">
-                    <el-col :span="20">
-                      <el-row type="flex" align="middle">
-                        <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
-                        <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
-                        <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
-                      </el-row>
-                    </el-col>
-                    <el-col :span="4" >
-                      <el-popover
-                        width="100"
-                        trigger="click">
-                        <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
-                        <i class="el-icon-more" slot="reference"></i>
-                      </el-popover>
-                    </el-col>
-                  </el-row>
-                </el-scrollbar>
-              </el-tab-pane>
-              <el-tab-pane :label="silencedUserLabel" name="silenced">
-                <el-scrollbar ref="manageLeftRef_silenced" style=" width: 100%;">
-                  <el-row style="margin-top: 10px" type="flex" align="middle" v-for="u in silencedUserList">
-                    <el-col :span="20">
-                      <el-row type="flex" align="middle">
-                        <el-col :span="4" style="padding-left: 10px;"><el-avatar :src="u.avatar"></el-avatar></el-col>
-                        <el-col :span="19" :offset="1">{{ u.nickName }}</el-col>
-                        <el-col :span="19" :offset="1">{{ u.userId }}</el-col>
-                      </el-row>
-                    </el-col>
-                    <el-col :span="4" >
-                      <el-popover
-                        width="100"
-                        trigger="click">
-                        <a style="cursor: pointer;color: #ff0000;" @click="changeUserState(u)">{{ u.msgStatus === 1 ? '解禁' : '禁言' }}</a>
-                        <i class="el-icon-more" slot="reference"></i>
-                      </el-popover>
-                    </el-col>
-                  </el-row>
-                </el-scrollbar>
-              </el-tab-pane>
-            </el-tabs>
-          </el-col>
-          <!-- 用户列表 end -->
-        </el-col>
-        <el-col :span="12">
-          <div style="background: #000; border-radius: 5px; overflow: hidden; margin: 10px 5px;">
-            <div style="border-radius: 5px; overflow: hidden;">
-              <video
-                controls
-                ref="videoPlayer"
-                autoplay
-                width="100%"
-                style="display: block; background: #000;"
-              ></video>
-            </div>
-          </div>
-        </el-col>
-        <el-col class="live-console-col" :span="8">
-          <el-tabs class="live-console-tab-right" v-model="tabRight.activeName" @tab-click="handleClick">
-            <el-tab-pane label="聊天" name="talk">
-              <el-scrollbar style="height: 500px; width: 100%;" ref="manageRightRef">
-                <el-row v-for="m in msgList" >
-                  <el-row v-if="m.userId !== userId" style="margin-top: 5px" type="flex" align="top" >
-                    <el-col :span="3" style="margin-left: 10px"><el-avatar :src="m.avatar"/></el-col>
-                    <el-col :span="15">
-                      <el-row style="margin-left: 10px">
-                        <el-col><div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div></el-col>
-                        <el-col :span="24" style="max-width: 200px;">
-                          <div style="white-space: normal; word-wrap: break-word;background-color: #f0f2f5; padding: 8px; border-radius: 5px;font-size: 14px;width: 100%;">
-                            {{ m.msg }}
-                          </div>
-                        </el-col>
-                        <el-col>
-                          <a style="cursor: pointer;color: #ff0000;padding: 8px 8px 0 0;font-size: 12px;" @click="changeUserState(m)">{{ m.msgStatus === 1 ? '解禁' : '禁言' }}</a>
-                        </el-col>
-                      </el-row>
-                    </el-col>
-                  </el-row>
-                  <el-row v-if="m.userId === userId" style="padding: 8px 0" type="flex" align="top" justify="end">
-                    <div style="display: flex;justify-content: flex-end">
-                      <div style="display: flex;justify-content: flex-end;flex-direction: column;max-width: 200px;align-items: flex-end">
-                        <div style="font-size: 12px; color: #999; margin-bottom: 3px;">{{ m.nickName }}</div>
-                        <div style="white-space: normal; word-wrap: break-word;width: 100%; background-color: #e6f7ff; padding: 8px; border-radius: 5px;font-size: 14px;">{{ m.msg }}</div>
-                      </div>
-                      <el-avatar :src="m.avatar" style="margin-left: 10px; margin-right: 10px;"/>
-                    </div>
-                  </el-row>
-                </el-row>
-                <!-- 底部留白 -->
-                <div style="height: 20px;"></div>
-              </el-scrollbar>
-
-              <!-- 消息输入区域 -->
-              <div style="padding: 10px; border-top: 1px solid #ebeef5; background-color: #fff; min-height: 120px;">
-                <el-input
-                  type="textarea"
-                  v-model="newMsg"
-                  placeholder="请输入消息..."
-                  :rows="8"
-                  @keyup.enter.native="sendMessage"
-                  clearable
-                  resize="none"
-                  style="flex: 1; margin-right: 10px;"
-                >
-                </el-input>
-                <div style="display: flex; justify-content: flex-end; margin-top: 10px;">
-                  <el-button plain @click="sendMessage">发送</el-button>
-                </div>
-              </div>
-            </el-tab-pane>
-          </el-tabs>
-        </el-col>
-      </el-row>
-    </div>
-    <el-dialog
-      title="提示"
-      :visible.sync="rtmpUrlVisible"
-      width="30%"
-      >
-      <div>服务器地址:{{serverName}}</div>
-      <div>推流码:{{livingCode}}</div>
-      <span slot="footer" class="dialog-footer">
-    <el-button type="primary" @click="rtmpUrlVisible = false">确 定</el-button>
-  </span>
-    </el-dialog>
-  </div>
-
-</template>
-
-<script>
-import { changeUserStatus, watchUserList } from '@/api/live/liveWatchUser'
-import { checkLive, createLive, closeLiving } from '@/api/live/talentLive'
-import { listLiveMsg } from '@/api/live/liveMsg'
-import { getLivingUrl } from '@/api/live/live'
-import { LiveWS } from '@/utils/liveWS'
-import Hls from 'hls.js';
-
-export default {
-  name: "TalentLive",
-  data() {
-    return {
-      isLoading: true,
-      tabLeft: {
-        activeName: "online",
-      },
-      tabRight: {
-        activeName: "talk",
-      },
-      liveWsUrl: process.env.VUE_APP_LIVE_WS_URL + '/app/webSocket',
-      liveList: [
-      ],
-      liveId: '',
-      socket: null,
-      status: true,
-      livingUrl: '',
-      rtmpUrl: '',
-      rtmpUrlVisible:false,
-      userList: [],
-      userParams:{
-        pageNum: 1,
-        pageSize: 10,
-        liveId: null
-      },
-      msgList: [],
-      msgParams: {
-        pageNum: 1,
-        pageSize: 10,
-        liveId: null
-      },
-      newMsg: '',
-      serverName: '',
-      livingCode:'',
-      hls: null,
-    };
-  },
-  computed: {
-    userId() {
-      return this.$store.state.user.user.userId
-    },
-    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)
-    },
-    offlineLabel() {
-      if (this.offlineUserList.length > 0) {
-        return '离线(' + this.offlineUserList.length + ')'
-      }
-      return '离线'
-    },
-    silencedUserList() {
-      return this.userList.filter(u => u.msgStatus === 1)
-    },
-    silencedUserLabel() {
-      if (this.silencedUserList.length > 0) {
-        return '禁言(' + this.silencedUserList.length + ')'
-      }
-      return '禁言'
-    }
-  },
-  created() {
-    this.checkLive()
-    this.isLoading = false
-  },
-  mounted() {
-    // this.checkUserNetwork();
-  },
-  methods: {
-    connectWebSocket() {
-      let socket = new LiveWS(this.liveWsUrl, this.liveId, this.userId);
-      socket.onmessage = (event) => this.handleWsMessage(event)
-      this.socket = socket
-    },
-    getList() {
-      this.resetParams()
-      this.loadUserList()
-      this.loadMsgList()
-    },
-    resetParams() {
-      this.userList= []
-      this.userParams = {
-        pageNum: 1,
-        pageSize: 10,
-        liveId: this.liveId
-      }
-      this.msgList = []
-      this.msgParams = {
-        pageNum: 1,
-        pageSize: 10,
-        liveId: this.liveId
-      }
-    },
-    handleClick(tab) {
-      console.log("click",tab.name)
-      console.log("liveId", this.liveId)
-    },
-    createLive(){
-      createLive({"liveId": this.liveId}).then(res => {
-        if (res.code === 200) {
-          this.status = false;
-          this.rtmpUrl = res.rtmpUrl
-          this.serverName = this.rtmpUrl.slice(0,this.rtmpUrl.lastIndexOf('/') + 1)
-          this.livingCode = this.rtmpUrl.slice(this.rtmpUrl.lastIndexOf('/') + 1)
-          this.rtmpUrlVisible = true
-        } else {
-          this.$message.error(res.msg);
-        }
-      })
-    },
-    showLivingUrl(){
-      this.rtmpUrlVisible = true
-    },
-    checkLive() {
-      checkLive({"companyId":this.companyId,"userId":this.userId}).then(res => {
-        if (res.code === 200) {
-          this.liveId = res.data.liveId
-          if(res.data.status === 2) {
-            this.rtmpUrl = res.data.rtmpUrl
-            this.status = false
-            this.serverName = this.rtmpUrl.slice(0,this.rtmpUrl.lastIndexOf('/') + 1)
-            this.livingCode = this.rtmpUrl.slice(this.rtmpUrl.lastIndexOf('/') + 1)
-          }
-        } else {
-          this.$message.error(res.data.msg)
-        }
-        // 获取确认正在直播之后开始连接直播
-        if (this.status === false) {
-          this.getLiveUrl()
-        }
-        this.connectWebSocket();
-        this.getList()
-      })
-    },
-    getLiveUrl(){
-      getLivingUrl(this.liveId).then(res=>{
-        if(res.code === 200) {
-          this.livingUrl = res.livingUrl
-          if (res.liveStatus === 2) {
-            this.initPlayer();
-          }
-        } else {
-          this.msgError(res.msg);
-        }
-      })
-    },
-    closeLiving(){
-      closeLiving({"liveId": this.liveId}).then(res=>{
-        if(res.code === 200) {
-          this.status = true
-          // 1. 停止视频播放
-          const videoElement = this.$refs.videoPlayer;
-          if (videoElement) {
-            videoElement.pause();
-            videoElement.src = '';
-          }
-          this.hls.destroy();
-          this.hls = null; // 重置 hls 实例
-        } else {
-          this.msgError(res.msg)
-        }
-      })
-    },
-    loadUserList() {
-      // 直播间用户
-      watchUserList({
-        liveId: this.liveId,
-        pageNum: this.userParams.pageNum,
-        pageSize: this.userParams.pageSize
-      }).then(response => {
-        let {code,rows,total} = response
-        if (code === 200) {
-          let totalPage = (total % this.userParams.pageSize == 0) ? Math.floor(total / this.userParams.pageSize) : Math.floor(total / this.userParams.pageSize + 1);
-          rows.forEach(row => {
-            if (!this.userList.some(u => u.userId === row.userId)) {
-              this.userList.push(row)
-            }
-          })
-
-          // 没加载完继续加载
-          if (this.userParams.pageNum < totalPage) {
-            this.userParams.pageNum = parseInt(this.userParams.pageNum) + 1;
-            this.loadUserList()
-          }
-        }
-      })
-    },
-    loadMsgList() {
-      // 直播间消息
-      listLiveMsg({
-        liveId:this.liveId,
-        pageNum: this.msgParams.pageNum,
-        pageSize: this.msgParams.pageSize
-      }).then(response => {
-        let {code, rows,total} = response;
-        if (code === 200) {
-          let totalPage = (total % this.msgParams.pageSize == 0) ? Math.floor(total / this.msgParams.pageSize) : Math.floor(total / this.msgParams.pageSize + 1);
-          rows.forEach(row => {
-            if (!this.msgList.some(m => m.msgId === row.msgId)) {
-
-              let user = this.userList.find(u => u.userId === row.userId)
-              if (user) {
-                row.msgStatus = user.msgStatus
-              } else {
-                row.msgStatus = 0
-              }
-
-              this.msgList.push(row)
-
-              // 移动到底部
-              this.$nextTick(() => {
-                setTimeout(() => {
-                  this.$refs.manageRightRef.wrap.scrollTop = this.$refs.manageRightRef.wrap.scrollHeight - this.$refs.manageRightRef.wrap.clientHeight
-                }, 200)
-              })
-            }
-          })
-
-          // 没加载完继续加载
-          if (this.msgParams.pageNum < totalPage) {
-            this.msgParams.pageNum = parseInt(this.msgParams.pageNum) + 1;
-            this.loadMsgList()
-          }
-
-          // 同步更新消息列表中相同用户的状态
-          this.userList.forEach(u => {
-            this.msgList.filter(m => m.userId === u.userId).forEach(m => m.msgStatus = u.msgStatus)
-          })
-        }
-      })
-
-      // 添加滚动监听
-      this.$nextTick(() => {
-        this.$refs.manageRightRef.wrap.addEventListener("scroll", this.manageRightScroll)
-      })
-    },
-    initPlayer(){
-      var isUrl = this.livingUrl === null || this.livingUrl.trim() === ''
-      if (Hls.isSupported() && !isUrl) {
-        const videoElement = this.$refs.videoPlayer
-        // ✅ 配置项在这里传入
-        const config = {
-          lowLatencyMode: true,        // 启用低延迟模式
-          liveSyncDuration: 1,         // 与直播流同步的时间(秒)
-          maxBufferLength: 1,          // 控制最大缓冲时间(秒)
-          backBufferLength: 30,        // 控制回放缓存大小(单位:秒)
-          maxBufferSize: 1 * 1024 * 1024, // 最大缓存大小(字节)
-          liveMaxLatencyDuration: 3,   // 最大延迟容忍值(秒)
-        };
-        this.hls = new Hls(config);
-        this.hls.attachMedia(videoElement);
-        this.hls.on(Hls.Events.MEDIA_ATTACHED, () => {
-          this.hls.loadSource(this.livingUrl);
-          this.hls.on(Hls.Events.STREAM_LOADED, (event, data) => {
-            videoElement.play();
-          });
-        });
-      }
-    },
-    changeUserState(u) {
-      // 修改状态
-      changeUserStatus({liveId: u.liveId, userId: u.userId}).then(response => {
-        let { code } = response;
-        if (200 === code) {
-          u.msgStatus = u.msgStatus === 0 ? 1 : 0
-          // 同步更新消息列表中相同用户的状态
-          this.msgList.forEach(msg => {
-            if (msg.userId === u.userId) {
-              msg.msgStatus = u.msgStatus;
-            }
-          });
-
-          this.userList.forEach(user => {
-            if (user.userId === u.userId) {
-              user.msgStatus = u.msgStatus;
-            }
-          });
-
-          let msg = u.msgStatus === 0 ? "已解禁" : "已禁言"
-          this.msgSuccess(msg);
-          return
-        }
-        this.msgError("操作失败");
-      })
-    },
-    handleWsMessage(event) {
-      let { code, data } = JSON.parse(event.data)
-      if (code === 200) {
-        let { cmd } = data
-        if (cmd === 'sendMsg') {
-
-          let message = JSON.parse(data.data)
-
-          let user = this.userList.find(u => u.userId === message.userId)
-          if (user) {
-            message.msgStatus = user.msgStatus
-          } else {
-            message.msgStatus = 0
-          }
-          delete message.params
-          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') {
-
-          let user = data
-          if(this.userList.length > 0){
-            this.userList = this.userList.filter(u => u.userId !== user.userId)
-          }
-          this.userList.push(user)
-        }
-        else if (cmd === 'live_start') {
-          console.log(data)
-          this.getLiveUrl();
-        }
-      }
-    },
-    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 = '';
-    },
-    async  checkWebRTCNetworkQuality(stunServer = 'http://192.168.172.137:8080') {
-      return new Promise((resolve, reject) => {
-        const pc = new RTCPeerConnection({ iceServers: [{ urls: stunServer }] });
-        const dc = pc.createDataChannel('network-test');
-
-        pc.onicecandidate = async (event) => {
-          if (event.candidate) return;
-
-          try {
-            await pc.setLocalDescription(await pc.createOffer());
-            const offer = pc.localDescription;
-            await pc.setRemoteDescription(await pc.createAnswer());
-
-            setTimeout(async () => {
-              const stats = await pc.getStats();
-              let rtt = null;
-              let packetsLost = null;
-
-              stats.forEach(report => {
-                if (report.type === 'candidate-pair' && report.nominated) {
-                  rtt = report.currentRoundTripTime * 1000; // ms
-                  packetsLost = report.packetsLost || 0;
-                }
-              });
-
-              pc.close();
-              resolve({ rtt, packetsLost });
-            }, 2000);
-          } catch (e) {
-            pc.close();
-            reject(e);
-          }
-        };
-
-        pc.oniceconnectionstatechange = () => {
-          if (pc.iceConnectionState === 'failed') {
-            pc.close();
-            reject('ICE 连接失败');
-          }
-        };
-
-        pc.createOffer().then(offer => pc.setLocalDescription(offer));
-      });
-    },
-
-    // 检测服务器延迟(ping)
-    async pingServer(url) {
-      const start = Date.now();
-      try {
-        await fetch(url, { method: 'HEAD', cache: 'no-cache' });
-        const latency = Date.now() - start;
-        return latency;
-      } catch (e) {
-        console.error('无法连接服务器', e);
-        return null;
-      }
-    },
-
-    // 综合检测并提示用户
-    async checkUserNetwork() {
-      //TODO 暂时写死
-      const serverPingUrl = 'http://192.168.172.137:8080'; // 替换为你的 SRS 服务器地址或后端接口
-
-      try {
-        // const webrtcResult = await this.checkWebRTCNetworkQuality();
-        const latency = await this.pingServer(serverPingUrl);
-
-        // const { rtt, packetsLost } = webrtcResult;
-
-        // const isRttOk = rtt < 200;
-        // const isLossOk = packetsLost < 5;
-        const isLatencyOk = latency < 300;
-
-        // const isNetworkOk = isRttOk && isLossOk && isLatencyOk;
-        const isNetworkOk =  isLatencyOk;
-
-        let msg = `
-          服务器延迟(Ping): ${latency} ms,
-          检测结果:${isNetworkOk ? '当前网络适合直播' : '网络不稳定,建议更换网络环境'}
-        `;
-
-        this.$alert(msg, '网络检测', {
-          confirmButtonText: '确定'
-        });
-
-        return isNetworkOk;
-      } catch (e) {
-        this.$alert(`<p style="color:red;">网络检测失败: ${e.message}</p>`, '网络检测', {
-          confirmButtonText: '确定'
-        });
-        return false;
-      }
-    },
-
-
-  },
-  destroyed() {
-    this.socket?.close()
-    this.hls?.destroy()
-  }
-};
-</script>
-<style scoped>
-
-.live-console-btn{
-  text-align: right;
-}
-
-
-
-</style>