Просмотр исходного кода

销售直播代码 修改直播间播放器,直播间身份认证

yuhongqi 1 день назад
Родитель
Сommit
a59812bf0c

+ 1 - 1
package.json

@@ -67,9 +67,9 @@
     "echarts": "4.2.1",
     "element-ui": "2.15.5",
     "file-saver": "2.0.1",
-    "flv.js": "^1.6.2",
     "form-making": "^1.2.9",
     "fuse.js": "^3.4.4",
+    "hls.js": "^1.6.7",
     "js-beautify": "1.10.2",
     "js-cookie": "2.2.0",
     "jsencrypt": "3.0.0-rc.1",

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

@@ -73,3 +73,11 @@ export function getLivingUrl(liveId) {
     method: 'get'
   })
 }
+
+export function verifyIdInfo(data) {
+  return request({
+    url: '/live/live/verifyIdInfo',
+    method: 'post',
+    data: data
+  })
+}

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

@@ -0,0 +1,30 @@
+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
+  })
+}
+
+

+ 1 - 1
src/utils/liveWS.js

@@ -12,7 +12,7 @@ export class LiveWS {
     let timestamp = new Date().getTime()
     let userType = 1
     let signature = CryptoJS.HmacSHA256(
-      CryptoJS.enc.Utf8.parse(liveId + userId + userType + timestamp), 
+      CryptoJS.enc.Utf8.parse('' + liveId + userId + userType + timestamp),
       CryptoJS.enc.Utf8.parse(timestamp)).toString(CryptoJS.enc.Hex)
     this.url = url + `?liveId=${liveId}&userId=${userId}&userType=${userType}&timestamp=${timestamp}&signature=${signature}`;
     this.liveId = liveId;

+ 52 - 0
src/views/live/liveConfig/idCard.vue

@@ -0,0 +1,52 @@
+<template>
+  <el-form ref="form"  label-width="80px">
+    <el-form-item>
+      <el-form-item label="上传图片" prop="materialUrl">
+        <ImageUpload @input="handleUrl" type="image" :num="10" :width="150" :height="150" />
+      </el-form-item>
+      <el-button type="primary" size="mini" @click="submit">保存</el-button>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+import ImageUpload from "@/views/qw/material/ImageUpload.vue";
+import {verifyIdInfo, getLive} from '@/api/live/live'
+
+export default {
+  components: { ImageUpload },
+  props: {
+    liveId: {
+      type: String,
+      default: "",
+    },
+  },
+  data() {
+    return {
+      test: "1test",
+      idCardUrl: "",
+    };
+  },
+  created() {
+    getLive(this.liveId).then(res => {
+      this.idCardUrl = res.data.idCardUrl;
+    })
+  },
+  methods: {
+    submit(){
+      verifyIdInfo({ liveId: this.liveId, idCardUrl: this.idCardUrl}).then(response => {
+        if (response.code == 200) {
+          this.$message.success("保存成功");
+        } else {
+          this.$message.success(response.msg);
+        }
+      })
+    },
+    handleUrl(data){
+      if (data.length > 0) {
+        this.idCardUrl = data;
+      }
+    }
+  }
+};
+</script>

+ 14 - 0
src/views/live/liveConfig/index.vue

@@ -480,6 +480,13 @@
             </div>
             <!-- 直播商品end -->
 
+
+            <div v-if="marketItem.name == 'idCard'">
+              <idCard :liveId="liveId" />
+            </div>
+
+
+
           </el-tab-pane>
         </el-tabs>
         <!-- 营销内容 end -->
@@ -500,8 +507,11 @@ import {
   updateConfig
 } from '@/api/live/liveQuestionLive'
 import {listLiveGoods, delLiveGoods, listStoreProduct,addLiveGoods} from '@/api/live/liveGoods'
+import idCard from "./idCard";
+
 export default {
   name: 'LiveConfig',
+  components: { idCard },
   data() {
     return {
       liveId: null,
@@ -534,6 +544,10 @@ export default {
         {
           label: '观看积分 ',
           name: 'watchScore'
+        },
+        {
+          label: '身份认证',
+          name: 'idCard'
         }
       ],
       questionDialogVisible: false,

+ 21 - 15
src/views/live/liveConsole/index.vue

@@ -182,7 +182,7 @@ import { getLiveVideoByLiveId } from '@/api/live/liveVideo'
 import { getLivingUrl } from '@/api/live/live'
 import { listLiveMsg } from '@/api/live/liveMsg'
 import { LiveWS } from '@/utils/liveWS'
-import flvjs from 'flv.js';
+import Hls from 'hls.js';
 
 export default {
   name: "LiveConsole",
@@ -260,29 +260,35 @@ export default {
   methods: {
     getLiveUrl(){
       getLivingUrl(this.liveId).then(res=>{
-        if(res.code === 200){
+        if(res.code === 200) {
           this.livingUrl = res.livingUrl
           this.initPlayer()
+        } else {
+          this.msgError(res.msg);
         }
       })
     },
     initPlayer(){
       var isUrl = this.livingUrl === null || this.livingUrl.trim() === ''
-      if (flvjs.isSupported() && !isUrl) {
+      if (Hls.isSupported() && !isUrl) {
         const videoElement = this.$refs.videoPlayer
-        var flvPlayer = flvjs.createPlayer({
-          type: 'flv',
-          // qfedu 是推流的时候的路径名称
-          // dixon 是stream 自定义的名称
-          url: this.livingUrl,
-          enableWorker: true,
-          enableStashBuffer: false, // 禁用内部缓冲区
-          stashInitialSize: 128,   // 减小初始缓冲大小
-          autoCleanupSourceBuffer: true
+        // ✅ 配置项在这里传入
+        const config = {
+          lowLatencyMode: true,        // 启用低延迟模式
+          liveSyncDuration: 1,         // 与直播流同步的时间(秒)
+          maxBufferLength: 1,          // 控制最大缓冲时间(秒)
+          backBufferLength: 30,        // 控制回放缓存大小(单位:秒)
+          maxBufferSize: 1 * 1024 * 1024, // 最大缓存大小(字节)
+          liveMaxLatencyDuration: 3,   // 最大延迟容忍值(秒)
+        };
+        const hls = new Hls(config);
+        hls.attachMedia(videoElement);
+        hls.on(Hls.Events.MEDIA_ATTACHED, () => {
+          hls.loadSource(this.livingUrl);
+          hls.on(Hls.Events.STREAM_LOADED, (event, data) => {
+            videoElement.play();
+          });
         });
-        flvPlayer.attachMediaElement(videoElement);
-        flvPlayer.load();
-        flvPlayer.play();
       }
     },
     handleClick(tab) {

+ 15 - 7
src/views/live/liveOrder/index.vue

@@ -107,9 +107,9 @@
           placeholder="选择完成时间">
         </el-date-picker>
       </el-form-item>
-      <el-form-item label="状态 1待支付 2已支付 3已发货 4已完成 -1已取消 -2已退款" prop="status">
-        <el-select v-model="queryParams.status" placeholder="请选择状态 1待支付 2已支付 3已发货 4已完成 -1已取消 -2已退款" clearable size="small">
-          <el-option label="请选择字典生成" value="" />
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option v-for="(item,index) in orderStatusOptions" :key="item.dictValue+index" :label="item.dictLabel" :value="item.dictValue" />
         </el-select>
       </el-form-item>
       <el-form-item>
@@ -167,7 +167,7 @@
 
     <el-table border v-loading="loading" :data="liveOrderList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="状态 1待支付 2已支付 3已发货 4已完成 -1已取消 -2已退款" align="center" prop="orderId" />
+      <el-table-column label="订单ID" align="center" prop="orderId"/>
       <el-table-column label="订单号" align="center" prop="orderSn" />
       <el-table-column label="用户ID" align="center" prop="userId" />
       <el-table-column label="收货人" align="center" prop="userName" />
@@ -188,7 +188,7 @@
           <span>{{ parseTime(scope.row.finishTime, '{y}-{m}-{d}') }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="状态 1待支付 2已支付 3已发货 4已完成 -1已取消 -2已退款" align="center" prop="status" />
+      <el-table-column label="状态" align="center" prop="status" :formatter="orderStatusFormatter"/>
       <el-table-column label="备注" align="center" prop="remark" />
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
@@ -267,9 +267,9 @@
             placeholder="选择完成时间">
           </el-date-picker>
         </el-form-item>
-        <el-form-item label="状态 1待支付 2已支付 3已发货 4已完成 -1已取消 -2已退款">
+        <el-form-item label="状态">
           <el-radio-group v-model="form.status">
-            <el-radio label="1">请选择字典生成</el-radio>
+            <el-radio :label="item.dictValue" v-for="item in orderStatusOptions" >{{item.dictLabel}}</el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="备注" prop="remark">
@@ -291,6 +291,8 @@ export default {
   name: "LiveOrder",
   data() {
     return {
+      //字典
+      orderStatusOptions: [],
       // 遮罩层
       loading: true,
       // 导出遮罩层
@@ -338,6 +340,9 @@ export default {
   },
   created() {
     this.getList();
+    this.getDicts("sys_live_order_status").then(response => {
+      this.orderStatusOptions = response.data;
+    });
   },
   methods: {
     /** 查询订单列表 */
@@ -349,6 +354,9 @@ export default {
         this.loading = false;
       });
     },
+    orderStatusFormatter(row, column) {
+      return this.selectDictLabel(this.orderStatusOptions, row.status);
+    },
     // 取消按钮
     cancel() {
       this.open = false;

+ 14 - 6
src/views/live/liveOrderitems/index.vue

@@ -46,9 +46,9 @@
           @keyup.enter.native="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="状态 1正常 2已退款" prop="status">
-        <el-select v-model="queryParams.status" placeholder="请选择状态 1正常 2已退款" clearable size="small">
-          <el-option label="请选择字典生成" value="" />
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option v-for="(item,index) in orderGoodsStatusOptions" :key="item.dictValue+index" :label="item.dictLabel" :value="item.dictValue" />
         </el-select>
       </el-form-item>
       <el-form-item>
@@ -113,7 +113,7 @@
       <el-table-column label="商品名" align="center" prop="goodsName" />
       <el-table-column label="单价" align="center" prop="price" />
       <el-table-column label="数量" align="center" prop="num" />
-      <el-table-column label="状态 1正常 2已退款" align="center" prop="status" />
+      <el-table-column label="状态" align="center" prop="status" :formatter="orderGoodsStatusFormatter"/>
       <el-table-column label="备注" align="center" prop="remark" />
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
@@ -164,9 +164,9 @@
         <el-form-item label="数量" prop="num">
           <el-input v-model="form.num" placeholder="请输入数量" />
         </el-form-item>
-        <el-form-item label="状态 1正常 2已退款">
+        <el-form-item label="状态">
           <el-radio-group v-model="form.status">
-            <el-radio label="1">请选择字典生成</el-radio>
+            <el-radio :label="item.dictValue" v-for="item in orderGoodsStatusOptions" >{{item.dictLabel}}</el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="备注" prop="remark">
@@ -188,6 +188,8 @@ export default {
   name: "LiveOrderitems",
   data() {
     return {
+      //字典
+      orderGoodsStatusOptions: [],
       // 遮罩层
       loading: true,
       // 导出遮罩层
@@ -229,6 +231,9 @@ export default {
   },
   created() {
     this.getList();
+    this.getDicts("sys_live_order_goods_status").then(response => {
+      this.orderGoodsStatusOptions = response.data;
+    });
   },
   methods: {
     /** 查询订单商品列表 */
@@ -240,6 +245,9 @@ export default {
         this.loading = false;
       });
     },
+    orderGoodsStatusFormatter(row, column) {
+      return this.selectDictLabel(this.orderGoodsStatusOptions, row.status);
+    },
     // 取消按钮
     cancel() {
       this.open = false;

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

@@ -0,0 +1,645 @@
+
+<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()
+  }
+};
+</script>
+<style scoped>
+
+.live-console-btn{
+  text-align: right;
+}
+
+
+
+</style>

+ 35 - 0
src/views/live/talentLiveInfo/index.vue

@@ -0,0 +1,35 @@
+
+<template>
+  <div>
+    <h1>个人设置</h1>
+    <form @submit.prevent="updateProfile">
+      <div>
+        <label>用户名:</label>
+        <input type="text" v-model="userProfile.username" required />
+      </div>
+      <div>
+        <label>邮箱:</label>
+        <input type="email" v-model="userProfile.email" required />
+      </div>
+      <button type="submit">更新</button>
+    </form>
+  </div>
+</template>
+
+<script>export default {
+  data() {
+    return {
+      userProfile: {
+        username: '用户1',
+        email: 'user@example.com'
+      }
+    };
+  },
+  methods: {
+    updateProfile() {
+      // 更新个人资料的逻辑
+      console.log('更新后的信息:', this.userProfile);
+    }
+  }
+};
+</script>