Browse Source

直播 增加 示范小组功能

dusenyao 3 years ago
parent
commit
7125f90a34

+ 2 - 1
package.json

@@ -9,7 +9,8 @@
     "build:report": "vue-cli-service build --report",
     "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
     "test:unit": "jest --clearCache && vue-cli-service test:unit",
-    "lint": "eslint --ext .js,.vue src"
+    "lint": "eslint --ext .js,.vue src",
+    "lint:css": "stylelint **/*.{html,vue,css,sass,scss,less}"
   },
   "dependencies": {
     "ailp-book-question-ui": "file:../ailp-book-question-ui-0.1.1.tgz",

+ 16 - 0
src/api/live.js

@@ -329,3 +329,19 @@ export function SendGroupMessage(data) {
     data
   });
 }
+
+/**
+ * 把当前分组设为示范讨论组(教师端)
+ * @param {Object} data { task_id 任务ID task_id 小组ID}
+ * @returns
+ */
+export function SetCurGroupToExample_Teacher(data) {
+  let params = getRequestParams('live_room-live_room_dispatch-SetCurGroupToExample_Teacher');
+
+  return request({
+    method: 'post',
+    url: process.env.VUE_APP_LearnWebSI,
+    params,
+    data
+  });
+}

+ 3 - 0
src/icons/svg/arrow-left-white.svg

@@ -0,0 +1,3 @@
+<svg width="9" height="14" viewBox="0 0 9 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.5 13L1.5 7L7.5 1" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
src/icons/svg/arrow-right-white.svg

@@ -0,0 +1,3 @@
+<svg width="9" height="14" viewBox="0 0 9 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1.5 1L7.5 7L1.5 13" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
src/router/index.js

@@ -143,6 +143,10 @@ const routes = [
       {
         path: '/live/student/group',
         component: () => import(/* webpackChunkName: 'live'*/ '@/views/live/student/group.vue')
+      },
+      {
+        path: '/live/student/audit',
+        component: () => import(/* webpackChunkName: 'live' */ '@/views/live/student/audit.vue')
       }
     ]
   },

+ 181 - 0
src/views/live/student/audit.js

@@ -0,0 +1,181 @@
+import { Message } from 'element-ui';
+import { rtc, getHistory } from '@/views/live/common';
+export { initSDK, sendMsg, getLiveStat, createScript, downloadWebSDK, handsDown, chatRoll } from '@/views/live/common';
+
+/**
+ * 取消订阅远程流
+ */
+export function unSubscribeStream(unSubStream) {
+  rtc.unSubscribeStream({
+    unSubStream,
+    success(str) {
+      console.log('取消订阅流成功', str);
+    },
+    fail(str) {
+      console.log(str);
+    }
+  });
+}
+
+/**
+ * 初始化监听事件
+ */
+export function initListener(vue) {
+  rtc.on('login_success', data => {
+    console.log('登录成功', data);
+    Message({
+      message: '登录成功',
+      type: 'success'
+    });
+  });
+
+  rtc.on('login_failed', data => {
+    console.log('登录失败', data);
+    Message({
+      message: '登录失败:' + JSON.stringify(data),
+      type: 'warning'
+    });
+  });
+
+  // 教师 必须在加入房间成功的事件回调里创建本地流
+  rtc.on('conference_join', () => {
+    console.log('加入房间成功');
+
+    getHistory({
+      success(data) {
+        let chatLog = data.datas.meta.chatLog.map(({ content, userName }) => {
+          return { msg: content, username: userName };
+        });
+        vue.chatList = chatLog;
+      },
+      fail(str) {
+        console.log(str);
+      }
+    });
+  });
+
+  rtc.on('conference_join_failed', err => {
+    // 加入房间失败  err为错误原因
+    console.log('加入房间失败', err);
+  });
+
+  // 新增订阅流事件
+  rtc.on('allow_sub', stream => {
+    console.log('新增订阅流事件');
+    if (stream.isMixed()) {
+      console.log('是混合流,不订阅');
+    } else {
+      // 订阅远程流
+      rtc.trySubscribeStream({
+        tryStream: stream,
+        success: stream => {
+          // 订阅流成功
+          let streamType = stream.streamType();
+          console.log('订阅流成功', streamType);
+          if (streamType === 10) {
+            vue.streamList.push(stream);
+          }
+          if (streamType === 0) {
+            stream.show('live', 'contain'); // 将流显示到指定 id 的元素中
+            vue.is_teacher_in_group = true;
+          }
+        },
+        fail(err) {
+          console.log('订阅流失败', err);
+        }
+      });
+    }
+  });
+
+  // 房间全量信息事件(人员进出时广播)
+  rtc.on('room_context', roomData => {
+    vue.roomContext = JSON.parse(roomData);
+    console.log('房间全量信息事件(人员进出时广播)', JSON.parse(roomData));
+  });
+
+  rtc.on('publish_stream', str => {
+    console.log('直播已开启', str);
+    Message({
+      type: 'success',
+      message: '直播已开启'
+    });
+    vue.liveStat = true;
+  });
+
+  rtc.on('end_stream', str => {
+    console.log('分组讨论已关闭', str);
+    Message({
+      type: 'success',
+      message: '分组讨论已关闭'
+    });
+    vue.liveStat = false;
+  });
+
+  // 单个用户配置监听
+  rtc.on('switch_user_settings', settingData => {
+    console.log('单个用户配置监听', JSON.parse(settingData));
+  });
+
+  // 人员列表事件(人员麦序变化时广播)
+  rtc.on('speak_context', speakData => {
+    vue.speakData = JSON.parse(speakData);
+    console.log('人员列表事件(人员麦序变化时广播)', JSON.parse(speakData));
+  });
+
+  rtc.on('switch_settings', data => {
+    console.log('房间设置事件', JSON.parse(data)); // 房间设置事件
+  });
+
+  rtc.on('kick_out', () => {
+    console.log('自己被踢出房间');
+  });
+
+  // 视频无法自动播放
+  rtc.on('playError', data => {
+    console.log('视频无法自动播放', data);
+  });
+
+  // 监听通知移除流事件
+  rtc.on('stream_removed', stream => {
+    console.log('监听通知移除流事件');
+    if (stream.streamType() === 0) {
+      vue.is_teacher_in_group = false;
+    }
+    let num = vue.streamList.findIndex(el => el.id() === stream.id());
+    if (num !== -1) {
+      vue.streamList.splice(num, 1);
+    }
+  });
+
+  // 停止订阅流
+  rtc.on('unSub', stream => {
+    console.log('停止订阅流');
+    unSubscribeStream(stream);
+  });
+
+  /**
+   * 排麦监听事件
+   */
+
+  // 监听自己被邀请事件
+  rtc.on('inviteUp', uid => {
+    console.log('监听自己被邀请事件', uid);
+  });
+
+  rtc.on('videoModeChange', data => {
+    console.log('连麦音视频模式更新成功监听回调', data.settings.video_mode);
+    vue.roomInfo.video_mode = data.settings.video_mode;
+  });
+
+  /**
+   * 监听聊天事件
+   */
+  rtc.on('chat_message', data => {
+    let dat = JSON.parse(data);
+    // 如果返回消息中有 isFilterChat 字段(消息不包含敏感词返回数据中无isFilterChat字段),且isFilterChat的值为1,则说明该消息包含敏感字,除发送者外其他人不会收到这条消息。
+    if (dat.isFilterChat && dat.isFilterChat === 1) {
+      return;
+    }
+    vue.chatList.push(dat);
+  });
+}

+ 683 - 0
src/views/live/student/audit.vue

@@ -0,0 +1,683 @@
+<template>
+  <div class="group">
+    <!--顶部-->
+    <div class="group-top">
+      <div class="live-title">
+        <div class="live-title-name">{{ roomInfo.cs_item_name }} {{ roomInfo.task_name }}</div>
+        <div>
+          <el-button @click="exitRoom">退出房间</el-button>
+        </div>
+      </div>
+      <div class="live-course-name">{{ roomInfo.course_name }}</div>
+      <div class="live-teacher">
+        <span class="live-teacher-name"> <svg-icon icon-class="person" />{{ roomInfo.teacher_name }} </span>
+        <span><svg-icon icon-class="people" />{{ roomInfo.student_count }}</span>
+      </div>
+    </div>
+    <!-- 主容器 -->
+    <div class="group-container">
+      <!-- 左侧 -->
+      <div class="group-container-left">
+        <div class="group-discussion">
+          <!-- 有流列表 -->
+          <div v-for="(item, i) in streamList" :key="item.id()" class="group-wrapper">
+            <div :id="`group-${i}`" class="group-box"></div>
+            <div class="live-wrapper-stream">
+              {{ searchStudentName(item.id()) }}
+              <svg-icon :icon-class="item.hasAudio() ? 'mike-on-grey' : ''" />
+              <svg-icon v-if="isMobile(item.id())" icon-class="mobile" />
+            </div>
+          </div>
+          <!-- 无流列表 -->
+          <div v-for="item in noStreamList" :key="item.student_id" class="group-box student-info">
+            <el-avatar icon="el-icon-user" :src="item.student_image_url" :size="80" />
+            <span class="student_name">{{ item.student_name }}</span>
+          </div>
+        </div>
+
+        <div class="audit-list">
+          <svg-icon icon-class="arrow-left-white" @click="listMove('left')" />
+          <div ref="avatar" class="audit-list-container">
+            <div ref="list" class="avatar-list" :style="{ 'margin-left': marginLeft + 'px' }">
+              <el-avatar
+                v-for="item in audience_list"
+                :key="item.student_id"
+                :size="32"
+                :src="item.student_image_url"
+              />
+            </div>
+          </div>
+          <svg-icon icon-class="arrow-right-white" @click="listMove('right')" />
+        </div>
+
+        <div class="button-group">
+          <div class="button-group-left"></div>
+          <div class="button-group-right"></div>
+        </div>
+        <div class="group-container-left-chat">
+          <div class="chat-top">
+            <span>聊天</span>
+          </div>
+          <div class="chat-window">
+            <ul ref="chat" class="chat-window-ul">
+              <li v-for="(item, i) in chatList" :key="i">
+                <div class="msg-normal">
+                  <span>{{ item.username }}: </span>
+                  <span>{{ item.msg }}</span>
+                </div>
+              </li>
+            </ul>
+          </div>
+          <div class="chat-speak">
+            <el-input v-model="msg" placeholder="输入发言" maxlength="400" disabled @keydown.enter.native="sendMsg">
+              <el-button slot="append" disabled @click="sendMsg">发送</el-button>
+            </el-input>
+          </div>
+        </div>
+      </div>
+      <!-- 右侧 -->
+      <div class="group-container-right">
+        <div class="live-teacher-lens" @mouseover="liveMenuShow = true" @mouseout="liveMenuShow = false">
+          <div class="live-container">
+            <div v-show="is_teacher_in_group" id="live" />
+            <el-image v-show="!is_teacher_in_group" :src="roomInfo.teacher_image_url" fit="contain" />
+          </div>
+          <div :style="{ bottom: liveMenuShow ? '0' : '-40px' }" class="live-wrapper">
+            <div>
+              {{ roomInfo.teacher_name }}
+            </div>
+            <div></div>
+          </div>
+        </div>
+        <div class="student-list">
+          <div class="student-list-title">小组成员</div>
+          <ul>
+            <li v-for="item in student_list" :key="item.room_user_id">
+              <div class="student-list-left">
+                <el-avatar icon="el-icon-user" size="small" :src="item.student_image_url" />
+                <span class="name">{{ item.student_name }}</span>
+              </div>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  StudentExitLiveRoom,
+  GetLiveRoomInfo,
+  GetGroupStatus,
+  GetMyGroupInfo_Student,
+  CreateEnterLiveRoomSession
+} from '@/api/live';
+import * as common from './audit';
+
+export default {
+  data() {
+    return {
+      task_id: this.$route.query.task_id,
+      room_user_id: '',
+      session_id: '',
+      live_room_sys_user_id: '',
+      room_id: '',
+      hasVideo: false,
+      hasAudio: false,
+      // 定时器
+      timer: null,
+      rtc: null,
+      streamList: [],
+      roomData: {
+        desc: '直播间标题',
+        name: '姓名',
+        user: {
+          id: '',
+          name: '',
+          role: 'talker',
+          rommid: ''
+        },
+        max_users: 1,
+        allow_chat: true,
+        allow_audio: true,
+        allow_speak: true
+      },
+      roomInfo: {
+        room_id: '',
+        video_mode: 1,
+        task_name: '',
+        cs_item_name: '',
+        course_name: '',
+        teacher_name: '',
+        student_count: 0,
+        teacher_image_url: ''
+      },
+      loadedNumber: 0,
+      speakData: {},
+      roomContext: {},
+      msg: '',
+      chatList: [],
+      // 直播间学员列表
+      student_list: [],
+      // 直播状态
+      liveStat: false,
+      liveMenuShow: false,
+      is_teacher_in_group: false,
+      // 无远程流学员列表
+      noStreamList: [],
+      // 旁听学员列表
+      audience_list: [],
+      marginLeft: 0
+    };
+  },
+  watch: {
+    loadedNumber(newVal) {
+      if (newVal === 5) {
+        common.createScript('https://class.csslcloud.net/static/SDK/docSDK/drawSdk_3.0.js').onload = () => {
+          this.rtc = common.initSDK({
+            userid: this.live_room_sys_user_id,
+            roomid: this.room_id,
+            sessionid: this.session_id
+          });
+          common.initListener(this); // 注册监听事件
+          this.getLiveStat();
+          this.$loading().close();
+        };
+      }
+    },
+    streamList(newVal) {
+      let list = this.student_list.filter(item => {
+        if (item.is_self === 'false') {
+          let isNoStream = true;
+          for (let i = 0; i < newVal.length; i++) {
+            if (newVal[i].id().split('-')[0] === item.room_user_id) isNoStream = false;
+          }
+          return isNoStream;
+        }
+        return false;
+      });
+      this.noStreamList = list;
+
+      if (newVal.length > 0) {
+        this.$nextTick(() => {
+          newVal[newVal.length - 1].show(`group-${newVal.length - 1}`);
+        });
+      }
+    },
+    // 聊天列表滚动
+    chatList() {
+      common.chatRoll(this);
+    }
+  },
+  created() {
+    this.getLiveRoomInfo();
+    this.getMyGroupInfo_Student();
+  },
+  mounted() {
+    this.getGroupStatus();
+  },
+  beforeDestroy() {
+    clearInterval(this.timer);
+    StudentExitLiveRoom({ task_id: this.task_id, room_user_id: this.room_user_id });
+    common.handsDown({
+      uid: this.room_user_id,
+      success: str => {
+        console.log('下麦成功', str);
+      },
+      fail: data => {
+        console.log('下麦失败', data);
+        common.closeVideo('picture');
+      }
+    });
+    this.streamList.forEach(item => {
+      common.unSubscribeStream(item);
+    });
+  },
+  methods: {
+    getLiveRoomInfo() {
+      GetLiveRoomInfo({ task_id: this.task_id }).then(
+        ({
+          room_id,
+          video_mode,
+          task_name,
+          cs_item_name,
+          course_name,
+          teacher_name,
+          student_count,
+          teacher_image_url
+        }) => {
+          this.roomInfo = {
+            room_id,
+            video_mode,
+            task_name,
+            cs_item_name,
+            course_name,
+            teacher_name,
+            student_count,
+            teacher_image_url
+          };
+        }
+      );
+    },
+
+    exitRoom() {
+      StudentExitLiveRoom({ task_id: this.task_id, room_user_id: this.room_user_id }).then(() => {
+        this.$router.push('/');
+      });
+    },
+
+    getLiveStat() {
+      common.getLiveStat({
+        success: data => {
+          this.liveStat = data.started;
+        },
+        fail: str => {
+          this.liveStat = false;
+          console.log('直播关闭状态或查询直播失败', str);
+        }
+      });
+    },
+
+    // 发消息
+    sendMsg() {
+      common.sendMsg(this.msg);
+      this.msg = '';
+    },
+
+    // 分组讨论
+    getGroupStatus() {
+      this.timer = setInterval(() => {
+        GetGroupStatus({ task_id: this.task_id }).then(
+          ({ is_enable_group, is_has_group_message, group_message_text }) => {
+            if (is_enable_group === 'false') {
+              clearInterval(this.timer);
+              CreateEnterLiveRoomSession({
+                task_id: this.task_id
+              }).then(({ live_room_sys_user_id, room_id, session_id, room_user_id }) => {
+                this.$router.push({
+                  path: `/live/student`,
+                  query: {
+                    live_room_sys_user_id,
+                    room_id,
+                    session_id,
+                    task_id: this.task_id,
+                    room_user_id
+                  }
+                });
+              });
+            }
+
+            if (is_has_group_message === 'true') {
+              this.$alert(group_message_text, '群消息', {
+                confirmButtonText: '确认'
+              });
+            }
+          }
+        );
+      }, 5000);
+    },
+
+    searchStudentName(id) {
+      let uid = id.split('-')[0];
+      if (uid) {
+        let student = this.student_list.find(item => item.room_user_id === uid);
+        return student ? student.student_name : '';
+      }
+      return '';
+    },
+
+    isMobile(id) {
+      let uid = id.split('-')[0];
+      if (uid) {
+        let student = this.student_list.find(item => item.room_user_id === uid);
+        return student ? student.is_mobile === 'true' : false;
+      }
+      return false;
+    },
+
+    listMove(direction) {
+      let w = this.$refs.list.clientWidth - this.$refs.avatar.clientWidth;
+      if (w > 60) {
+        let left = Number(this.$refs.list.style['margin-left'].slice(0, -2));
+        let width = direction === 'right' ? left - 60 : left + 60;
+        if (Math.abs(width) > w) width = -w;
+        this.marginLeft = width > 0 ? 0 : width;
+      }
+    },
+
+    getMyGroupInfo_Student() {
+      GetMyGroupInfo_Student({
+        task_id: this.task_id
+      }).then(({ live_room_sys_user_id, room_id, student_list, audience_list }) => {
+        console.log(audience_list);
+        let data = audience_list.find(el => el.is_self === 'true');
+        this.session_id = data.session_id;
+        this.room_user_id = data.room_user_id;
+        this.live_room_sys_user_id = live_room_sys_user_id;
+        this.room_id = room_id;
+        this.student_list = student_list;
+        this.noStreamList = student_list.filter(item => item.is_self === 'false');
+        this.audience_list = audience_list;
+        common.downloadWebSDK(this);
+      });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import '~@/styles/mixin';
+$live-bc: #3d3938;
+
+.group {
+  @include container;
+
+  // 顶部
+  &-top {
+    padding: 24px 32px;
+    background-color: #fff;
+    border-top-left-radius: 8px;
+    border-top-right-radius: 8px;
+
+    .live-title {
+      display: flex;
+      justify-content: space-between;
+
+      &-name {
+        font-size: 22px;
+      }
+
+      .el-button {
+        padding: 7px 12px;
+        border-radius: 4px;
+      }
+    }
+
+    .live-course-name {
+      font-size: 14px;
+      line-height: 30px;
+      color: #737373;
+    }
+
+    .live-teacher {
+      margin-top: 12px;
+
+      .svg-icon {
+        margin-right: 8px;
+      }
+
+      &-name {
+        margin-right: 60px;
+      }
+    }
+  }
+
+  // 主容器
+  &-container {
+    display: flex;
+    justify-content: left;
+
+    &-left {
+      width: 832px;
+      background-color: #fff;
+      border-radius: 8px;
+
+      // 分组讨论
+      .group-discussion {
+        display: grid;
+        grid-template-rows: 144px;
+        grid-template-columns: 1fr 1fr 1fr;
+        gap: 8px;
+        width: 100%;
+        height: 404px;
+        padding: 24px;
+        overflow-y: auto;
+        background-color: $live-bc;
+
+        .group-box {
+          width: 100%;
+          height: 144px;
+
+          &.student-info {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: space-between;
+            border: 2px solid #625c5b;
+
+            .el-avatar {
+              margin-top: 12px;
+            }
+
+            .student_name {
+              height: 28px;
+              color: #fff;
+            }
+          }
+        }
+
+        .group-wrapper {
+          position: relative;
+
+          %live-wrapper-stream,
+          .live-wrapper-stream {
+            position: absolute;
+            top: 112px;
+            width: calc(100% - 16px);
+            height: 32px;
+            padding: 0 15px;
+            margin: 0 8px;
+            line-height: 32px;
+            color: #fff;
+            text-align: center;
+            background-color: rgba(0, 0, 0, 0.7);
+            transition: all 300ms ease-in 0s;
+
+            > .svg-icon {
+              margin-left: 12px;
+            }
+          }
+
+          .local-stream {
+            @extend %live-wrapper-stream;
+
+            display: flex;
+
+            > div:first-child {
+              flex: 6;
+            }
+
+            > div:last-child {
+              flex: 4;
+              text-align: right;
+
+              .svg-icon {
+                margin-right: 18px;
+                cursor: pointer;
+              }
+            }
+          }
+        }
+      }
+
+      // 旁听列表
+      .audit-list {
+        display: flex;
+        align-items: center;
+        width: 100%;
+        height: 64px;
+        padding: 16px 24px;
+        background-color: #5a5a5a;
+
+        &-container {
+          width: 100%;
+          height: 100%;
+          margin: 0 24px;
+          overflow: hidden;
+
+          .avatar-list {
+            display: inline-block;
+            white-space: nowrap;
+
+            .el-avatar {
+              margin-left: 8px;
+            }
+          }
+        }
+
+        .svg-icon {
+          cursor: pointer;
+        }
+      }
+
+      .button-group {
+        display: flex;
+        justify-content: space-between;
+        height: 48px;
+        padding: 0 15px;
+        background-color: #4d4d4d;
+        border-bottom-left-radius: 5px;
+
+        .svg-icon {
+          font-size: 20px;
+        }
+
+        &-left {
+          > span {
+            display: inline-block;
+            height: 100%;
+            padding: 14px 16px;
+            cursor: pointer;
+
+            &:active,
+            &:hover {
+              background-color: #3d3d3d;
+            }
+          }
+        }
+      }
+
+      // 聊天窗口
+      &-chat {
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        height: 278px;
+        border: 1px solid #ccc;
+        border-bottom-left-radius: 8px;
+
+        .chat-top {
+          display: flex;
+          justify-content: space-between;
+          padding: 15px 15px 10px;
+          color: #959595;
+          border-bottom: 1px solid #e6e6e6;
+
+          label {
+            cursor: pointer;
+          }
+
+          .allow-chat {
+            margin-right: 12px;
+          }
+        }
+
+        .chat-window {
+          position: relative;
+          width: 100%;
+          height: 100%;
+          overflow: hidden;
+
+          &-ul {
+            position: absolute;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            overflow: auto;
+
+            .msg-normal {
+              padding: 7px 16px;
+            }
+          }
+        }
+
+        .chat-speak {
+          padding: 16px;
+        }
+      }
+    }
+
+    &-right {
+      height: 794px;
+      padding: 8px;
+      background-color: #2c2c2c;
+      border-end-end-radius: 8px;
+
+      .live-teacher-lens {
+        position: relative;
+        overflow: hidden;
+
+        .live-container {
+          width: 352px;
+          height: 198px;
+          background-color: $live-bc;
+
+          #live {
+            width: 100%;
+            height: 100%;
+          }
+
+          .el-image {
+            width: 100%;
+            height: 100%;
+          }
+        }
+
+        .live-wrapper {
+          position: absolute;
+          width: 100%;
+          height: 40px;
+          padding: 0 16px;
+          line-height: 40px;
+          color: #fff;
+          background-color: rgba(0, 0, 0, 0.7);
+          transition: all 300ms ease-in 0s;
+        }
+      }
+
+      .student-list {
+        width: 100%;
+        height: calc(100% - 200px);
+        padding: 24px 16px;
+        margin-top: 2px;
+        font-size: 14px;
+        color: #fff;
+        background-color: #2c2c2c;
+
+        &-title {
+          margin-bottom: 16px;
+        }
+
+        ul {
+          height: calc(100% - 20px);
+          overflow-x: auto;
+        }
+
+        li {
+          display: flex;
+          margin-bottom: 16px;
+
+          .student-list-left {
+            .name {
+              margin-left: 8px;
+              vertical-align: super;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 90 - 13
src/views/live/student/group.vue

@@ -18,7 +18,7 @@
     <div class="group-container">
       <!-- 左侧 -->
       <div class="group-container-left">
-        <div class="group-discussion">
+        <div class="group-discussion" :style="{ height: is_example_group ? '404px' : '468px' }">
           <!-- 本地流 -->
           <div v-show="localStream" class="group-wrapper">
             <div id="group-local" class="group-box"></div>
@@ -51,6 +51,22 @@
             <span class="student_name">{{ item.student_name }}</span>
           </div>
         </div>
+
+        <div v-if="is_example_group" class="audit-list">
+          <svg-icon icon-class="arrow-left-white" @click="listMove('left')" />
+          <div ref="avatar" class="audit-list-container">
+            <div ref="list" class="avatar-list" :style="{ 'margin-left': marginLeft + 'px' }">
+              <el-avatar
+                v-for="item in audience_list"
+                :key="item.student_id"
+                :size="32"
+                :src="item.student_image_url"
+              />
+            </div>
+          </div>
+          <svg-icon icon-class="arrow-right-white" @click="listMove('right')" />
+        </div>
+
         <div class="button-group">
           <div class="button-group-left"></div>
           <div class="button-group-right"></div>
@@ -169,7 +185,11 @@ export default {
       // 无远程流学员列表
       noStreamList: [],
       // 学员自身信息
-      studentSelf: {}
+      studentSelf: {},
+      // 旁听学员列表
+      audience_list: [],
+      is_example_group: false,
+      marginLeft: 0
     };
   },
   watch: {
@@ -216,7 +236,7 @@ export default {
     this.getMyGroupInfo_Student();
   },
   mounted() {
-    this.GetGroupStatus();
+    this.getGroupStatus();
   },
   beforeDestroy() {
     clearInterval(this.timer);
@@ -349,10 +369,10 @@ export default {
     },
 
     // 分组讨论
-    GetGroupStatus() {
+    getGroupStatus() {
       this.timer = setInterval(() => {
         GetGroupStatus({ task_id: this.task_id }).then(
-          ({ is_enable_group, is_has_group_message, group_message_text }) => {
+          ({ is_enable_group, is_has_group_message, group_message_text, is_audience, is_example_group }) => {
             if (is_enable_group === 'false') {
               clearInterval(this.timer);
               CreateEnterLiveRoomSession({
@@ -371,6 +391,21 @@ export default {
               });
             }
 
+            if (is_audience === 'true') {
+              this.$router.push({
+                path: '/live/student/audit',
+                query: {
+                  task_id: this.task_id
+                }
+              });
+            }
+
+            // 如果是示范讨论组,获取旁听学员列表
+            if (is_example_group === 'true') {
+              this.getMyGroupInfo_Student();
+              this.is_example_group = true;
+            }
+
             if (is_has_group_message === 'true') {
               this.$alert(group_message_text, '群消息', {
                 confirmButtonText: '确认'
@@ -399,10 +434,20 @@ export default {
       return false;
     },
 
+    listMove(direction) {
+      let w = this.$refs.list.clientWidth - this.$refs.avatar.clientWidth;
+      if (w > 60) {
+        let left = Number(this.$refs.list.style['margin-left'].slice(0, -2));
+        let width = direction === 'right' ? left - 60 : left + 60;
+        if (Math.abs(width) > w) width = -w;
+        this.marginLeft = width > 0 ? 0 : width;
+      }
+    },
+
     getMyGroupInfo_Student() {
       GetMyGroupInfo_Student({
         task_id: this.task_id
-      }).then(({ live_room_sys_user_id, room_id, student_list }) => {
+      }).then(({ live_room_sys_user_id, room_id, student_list, audience_list }) => {
         let data = student_list.find(el => {
           return el.is_self === 'true';
         });
@@ -413,6 +458,7 @@ export default {
         this.student_list = student_list;
         this.noStreamList = student_list.filter(item => item.is_self === 'false');
         this.studentSelf = student_list.find(item => item.is_self === 'true');
+        this.audience_list = audience_list;
         common.downloadWebSDK(this);
       });
     }
@@ -479,18 +525,19 @@ $live-bc: #3d3938;
 
       // 分组讨论
       .group-discussion {
-        position: relative;
-        display: flex;
-        flex-wrap: wrap;
+        display: grid;
+        grid-template-rows: 144px;
+        grid-template-columns: 1fr 1fr 1fr;
+        gap: 8px;
         width: 100%;
         height: 468px;
-        overflow: auto;
+        padding: 24px;
+        overflow-y: auto;
         background-color: $live-bc;
 
         .group-box {
-          width: 254px;
+          width: 100%;
           height: 144px;
-          margin: 8px;
 
           &.student-info {
             display: flex;
@@ -516,7 +563,7 @@ $live-bc: #3d3938;
           %live-wrapper-stream,
           .live-wrapper-stream {
             position: absolute;
-            top: 120px;
+            top: 112px;
             width: calc(100% - 16px);
             height: 32px;
             padding: 0 15px;
@@ -554,6 +601,36 @@ $live-bc: #3d3938;
         }
       }
 
+      // 旁听列表
+      .audit-list {
+        display: flex;
+        align-items: center;
+        width: 100%;
+        height: 64px;
+        padding: 16px 24px;
+        background-color: #5a5a5a;
+
+        &-container {
+          width: 100%;
+          height: 100%;
+          margin: 0 24px;
+          overflow: hidden;
+
+          .avatar-list {
+            display: inline-block;
+            white-space: nowrap;
+
+            .el-avatar {
+              margin-left: 8px;
+            }
+          }
+        }
+
+        .svg-icon {
+          cursor: pointer;
+        }
+      }
+
       .button-group {
         display: flex;
         justify-content: space-between;

+ 130 - 47
src/views/live/teacher/group.vue

@@ -6,7 +6,8 @@
         <div class="live-title-name">{{ roomInfo.cs_item_name }} {{ roomInfo.task_name }}</div>
         <div>
           <el-button @click="stopGroup">结束群组讨论</el-button>
-          <el-button v-show="isGroup" @click="exitCurGroup_Teacher">退出小组讨论</el-button>
+          <el-button v-show="isGroup && !isAudit" @click="setCurGroupToExample_Teacher">设为示范讨论组</el-button>
+          <el-button v-show="isGroup && !isAudit" @click="exitCurGroup_Teacher">退出小组讨论</el-button>
         </div>
       </div>
       <div class="live-course-name">{{ roomInfo.course_name }}</div>
@@ -34,7 +35,8 @@
             </div>
           </template>
         </div>
-        <div v-show="isGroup" class="group-discussion">
+
+        <div v-show="isGroup" class="group-discussion" :style="{ height: isAudit ? '404px' : '468px' }">
           <div v-for="(item, i) in streamList" :key="item.id()" class="group-wrapper">
             <div :id="`group-${i}`" class="group-box"></div>
             <div class="live-wrapper-stream">
@@ -49,6 +51,22 @@
             <span class="student_name">{{ item.student_name }}</span>
           </div>
         </div>
+
+        <div v-if="isAudit" class="audit-list">
+          <svg-icon icon-class="arrow-left-white" @click="listMove('left')" />
+          <div ref="avatar" class="audit-list-container">
+            <div ref="list" class="avatar-list" :style="{ 'margin-left': marginLeft + 'px' }">
+              <el-avatar
+                v-for="item in audience_list"
+                :key="item.student_id"
+                :size="32"
+                :src="item.student_image_url"
+              />
+            </div>
+          </div>
+          <svg-icon icon-class="arrow-right-white" @click="listMove('right')" />
+        </div>
+
         <div class="button-group">
           <div class="button-group-left">
             <span class="stop-group" @click="stopGroup">结束群组讨论</span>
@@ -134,7 +152,8 @@ import {
   ExitCurGroup_Teacher,
   GetMyGroupInfo_Teacher,
   GetGroupStatus,
-  SendGroupMessage
+  SendGroupMessage,
+  SetCurGroupToExample_Teacher
 } from '@/api/live';
 import * as common from './group';
 
@@ -143,8 +162,12 @@ export default {
     return {
       task_id: this.$route.query.task_id,
       isGroup: false,
+      group_id: '',
+      // 是否旁听
+      isAudit: false,
       // 定时器
       timer: null,
+      marginLeft: 0,
       rtc: null,
       roomData: {
         desc: '直播间标题',
@@ -185,7 +208,9 @@ export default {
       streamList: [],
       student_list: [],
       // 无远程流学员列表
-      noStreamList: []
+      noStreamList: [],
+      // 旁听学员列表
+      audience_list: []
     };
   },
   watch: {
@@ -228,12 +253,15 @@ export default {
       if (is_teacher_in_group === 'true') {
         GetMyGroupInfo_Teacher({
           task_id: this.task_id
-        }).then(({ room_id, teacher: { session_id }, student_list }) => {
+        }).then(({ room_id, is_example, group_id, teacher: { session_id }, student_list, audience_list }) => {
           this.room_id = room_id;
           this.session_id = session_id;
           this.student_list = student_list;
           common.downloadWebSDK(this);
           this.isGroup = true;
+          this.group_id = group_id;
+          this.isAudit = is_example === 'true';
+          this.audience_list = audience_list;
         });
       }
     });
@@ -332,6 +360,7 @@ export default {
           this.session_id = session_id;
           common.downloadWebSDK(this);
           this.isGroup = true;
+          this.group_id = group_id;
           return GetMyGroupInfo_Teacher({ task_id: this.task_id });
         })
         .then(({ student_list }) => {
@@ -358,9 +387,20 @@ export default {
       return false;
     },
 
+    listMove(direction) {
+      let w = this.$refs.list.clientWidth - this.$refs.avatar.clientWidth;
+      if (w > 60) {
+        let left = Number(this.$refs.list.style['margin-left'].slice(0, -2));
+        let width = direction === 'right' ? left - 60 : left + 60;
+        if (Math.abs(width) > w) width = -w;
+        this.marginLeft = width > 0 ? 0 : width;
+      }
+    },
+
     exitCurGroup_Teacher() {
       ExitCurGroup_Teacher({ task_id: this.task_id }).then(() => {
         this.isGroup = false;
+        this.group_id = '';
         this.room_id = '';
         this.session_id = '';
         common.closeVideo('main');
@@ -371,13 +411,25 @@ export default {
         common.removeWebSDK();
         this.$message.success('退出小组讨论成功');
       });
+    },
+
+    setCurGroupToExample_Teacher() {
+      SetCurGroupToExample_Teacher({ task_id: this.task_id, group_id: this.group_id })
+        .then(() => {
+          this.isAudit = true;
+          this.$message.success('把当前分组设为示范讨论组成功');
+          return GetMyGroupInfo_Teacher({ task_id: this.task_id });
+        })
+        .then(({ audience_list }) => {
+          this.audience_list = audience_list;
+        });
     }
   }
 };
 </script>
 
 <style lang="scss" scoped>
-@import '~@/styles/mixin.scss';
+@import '~@/styles/mixin';
 $live-bc: #3d3938;
 
 .live {
@@ -385,8 +437,8 @@ $live-bc: #3d3938;
 
   // 顶部
   &-top {
-    background-color: #fff;
     padding: 24px 32px;
+    background-color: #fff;
     border-top-left-radius: 8px;
     border-top-right-radius: 8px;
 
@@ -399,15 +451,15 @@ $live-bc: #3d3938;
       }
 
       .el-button {
-        border-radius: 4px;
         padding: 7px 12px;
+        border-radius: 4px;
       }
     }
 
     .live-course-name {
       font-size: 14px;
-      color: #737373;
       line-height: 30px;
+      color: #737373;
     }
 
     .live-teacher {
@@ -434,28 +486,28 @@ $live-bc: #3d3938;
       border-radius: 8px;
 
       .student-group {
-        width: 100%;
-        height: 468px;
         display: flex;
         flex-wrap: wrap;
-        background-color: #4d4d4d;
-        overflow: auto;
+        width: 100%;
+        height: 468px;
         padding: 10px 0 10px 24px;
+        overflow: auto;
+        background-color: #4d4d4d;
 
         &-list {
-          background-color: #646464;
           position: relative;
-          padding: 24px;
           width: 255px;
           height: 144px;
+          padding: 24px;
           margin: 0 8px 8px 0;
           cursor: pointer;
+          background-color: #646464;
 
           &-avatar {
             display: flex;
-            height: 92px;
             flex-wrap: wrap;
             align-items: center;
+            height: 92px;
             margin-top: 12px;
             overflow: auto;
 
@@ -466,33 +518,34 @@ $live-bc: #3d3938;
 
           .group-serial {
             position: absolute;
-            display: inline-block;
             top: 0;
             left: 0;
-            height: 24px;
+            display: inline-block;
             width: 24px;
+            height: 24px;
+            line-height: 24px;
             color: #fff;
-            background-color: #3d3d3d;
             text-align: center;
-            line-height: 24px;
+            background-color: #3d3d3d;
           }
         }
       }
 
       // 分组讨论
       .group-discussion {
-        display: flex;
-        flex-wrap: wrap;
+        display: grid;
+        grid-template-rows: 144px;
+        grid-template-columns: 1fr 1fr 1fr;
+        gap: 8px;
         width: 100%;
         height: 468px;
-        position: relative;
+        padding: 24px;
+        overflow-y: auto;
         background-color: $live-bc;
-        overflow: auto;
 
         .group-box {
-          width: 254px;
+          width: 100%;
           height: 144px;
-          margin: 8px;
 
           &.student-info {
             display: flex;
@@ -517,15 +570,15 @@ $live-bc: #3d3938;
 
           .live-wrapper-stream {
             position: absolute;
-            top: 120px;
-            height: 32px;
+            top: 112px;
             width: calc(100% - 16px);
-            background-color: rgba(0, 0, 0, 0.7);
-            color: #fff;
+            height: 32px;
+            padding: 0 15px;
+            margin: 0 8px;
             line-height: 32px;
+            color: #fff;
             text-align: center;
-            margin: 0 8px;
-            padding: 0 15px;
+            background-color: rgba(0, 0, 0, 0.7);
             transition: all 300ms ease-in 0s;
 
             > .svg-icon {
@@ -535,12 +588,42 @@ $live-bc: #3d3938;
         }
       }
 
+      // 旁听列表
+      .audit-list {
+        display: flex;
+        align-items: center;
+        width: 100%;
+        height: 64px;
+        padding: 16px 24px;
+        background-color: #5a5a5a;
+
+        &-container {
+          width: 100%;
+          height: 100%;
+          margin: 0 24px;
+          overflow: hidden;
+
+          .avatar-list {
+            display: inline-block;
+            white-space: nowrap;
+
+            .el-avatar {
+              margin-left: 8px;
+            }
+          }
+        }
+
+        .svg-icon {
+          cursor: pointer;
+        }
+      }
+
       .button-group {
         display: flex;
         justify-content: space-between;
         height: 48px;
-        background-color: #4d4d4d;
         padding: 0 15px;
+        background-color: #4d4d4d;
         border-bottom-left-radius: 5px;
 
         .svg-icon {
@@ -568,20 +651,20 @@ $live-bc: #3d3938;
 
       // 聊天窗口
       &-chat {
-        height: 278px;
-        border: 1px solid #ccc;
-        border-bottom-left-radius: 8px;
         display: flex;
         flex-direction: column;
         justify-content: space-between;
+        height: 278px;
+        border: 1px solid #ccc;
+        border-bottom-left-radius: 8px;
 
         .chat-top {
           display: flex;
           align-items: center;
           justify-content: space-between;
           padding: 15px 15px 10px;
-          border-bottom: 1px solid #e6e6e6;
           color: #959595;
+          border-bottom: 1px solid #e6e6e6;
 
           label {
             cursor: pointer;
@@ -619,8 +702,8 @@ $live-bc: #3d3938;
     }
 
     &-right {
-      padding: 8px;
       height: 794px;
+      padding: 8px;
       background-color: #2c2c2c;
       border-end-end-radius: 8px;
 
@@ -636,12 +719,12 @@ $live-bc: #3d3938;
 
         .live-wrapper {
           position: absolute;
-          height: 40px;
           width: 100%;
-          background-color: rgba(0, 0, 0, 0.7);
-          color: #fff;
-          line-height: 40px;
+          height: 40px;
           padding: 0 16px;
+          line-height: 40px;
+          color: #fff;
+          background-color: rgba(0, 0, 0, 0.7);
           transition: all 300ms ease-in 0s;
         }
       }
@@ -649,12 +732,12 @@ $live-bc: #3d3938;
       // 学员列表
       .student-list {
         width: 100%;
+        height: calc(100% - 200px);
         padding: 24px 16px;
         margin-top: 2px;
-        height: calc(100% - 200px);
-        background-color: #2c2c2c;
         font-size: 14px;
         color: #fff;
+        background-color: #2c2c2c;
 
         &-title {
           margin-bottom: 16px;
@@ -673,8 +756,8 @@ $live-bc: #3d3938;
             flex: 8;
 
             .name {
-              vertical-align: super;
               margin-left: 8px;
+              vertical-align: super;
             }
           }
 
@@ -682,10 +765,10 @@ $live-bc: #3d3938;
             flex: 2;
 
             .svg-icon {
-              font-size: 18px;
-              cursor: pointer;
               margin-top: 7px;
               margin-right: 8px;
+              font-size: 18px;
+              cursor: pointer;
             }
           }
         }