Bläddra i källkod

Merge branch 'master' of http://gcls-git.helxsoft.cn/dsy/gcls_sys_learn_web

dusenyao 3 år sedan
förälder
incheckning
7adb426337
48 ändrade filer med 1425 tillägg och 23427 borttagningar
  1. 2 4
      .prettierrc.js
  2. 13 23118
      package-lock.json
  3. 18 17
      package.json
  4. BIN
      public/favicon.ico
  5. 0 1
      public/index.html
  6. 20 0
      src/api/course.js
  7. 13 0
      src/api/live.js
  8. 1 2
      src/api/table.js
  9. BIN
      src/assets/doc-Q.png
  10. BIN
      src/assets/pay/active.png
  11. BIN
      src/assets/pay/shouxinyi.png
  12. BIN
      src/assets/pdf-Q.png
  13. BIN
      src/assets/ppt-Q.png
  14. BIN
      src/assets/xls-Q.png
  15. 21 61
      src/common/show_file/index.vue
  16. 9 1
      src/components/course/CompletionView.vue
  17. 12 4
      src/components/course/FinishCourseware.vue
  18. 35 63
      src/components/live/CurMaterial.vue
  19. 9 6
      src/components/payment/Audit.vue
  20. 392 0
      src/components/payment/Payment.vue
  21. 8 1
      src/components/select/SelectCourse.vue
  22. 9 1
      src/components/select/SelectTemplate.vue
  23. 19 7
      src/layouts/components/LayoutHeader.vue
  24. 26 1
      src/router/index.js
  25. 1 0
      src/settings.js
  26. 3 1
      src/store/modules/app.js
  27. 20 0
      src/views/OrderPaySuccess.vue
  28. 75 10
      src/views/course_details/index.vue
  29. 3 6
      src/views/live/SelectDevice.vue
  30. 10 5
      src/views/live/student/audit.js
  31. 0 4
      src/views/live/student/group.js
  32. 17 3
      src/views/live/student/group.vue
  33. 0 4
      src/views/live/student/live.js
  34. 26 58
      src/views/live/teacher/CompleteList.vue
  35. 127 6
      src/views/live/teacher/group.vue
  36. 9 3
      src/views/live/teacher/index.vue
  37. 0 4
      src/views/live/teacher/live.js
  38. 4 2
      src/views/main/components/MonthlyCalendar.vue
  39. 1 0
      src/views/main/curricula_list/teacher.vue
  40. 9 2
      src/views/task_details/ShowCourseware.vue
  41. 48 11
      src/views/task_details/TaskTop.vue
  42. 32 5
      src/views/task_details/student/index.vue
  43. 10 3
      src/views/task_details/teacher/index.vue
  44. 1 0
      src/views/teacher/create_course/index.vue
  45. 18 4
      src/views/teacher/create_course/step_table/CourseInfo.vue
  46. 6 8
      src/views/teacher/create_course/step_table/SelectBook.vue
  47. 396 0
      src/views/template_details/index.vue
  48. 2 1
      stylelint.config.js

+ 2 - 4
.prettierrc.js

@@ -1,11 +1,9 @@
 module.exports = {
   tabWidth: 2,
-  useTabs: false,
+  printWidth: 120,
   semi: true,
   singleQuote: true,
   trailingComma: 'none',
   endOfLine: 'auto',
-  bracketSpacing: true,
-  arrowParens: 'avoid',
-  printWidth: 120
+  arrowParens: 'avoid'
 };

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 13 - 23118
package-lock.json


+ 18 - 17
package.json

@@ -16,54 +16,55 @@
     "ailp-book-question-ui": "file:../ailp-book-question-ui-0.1.1.tgz",
     "awe-dnd": "^0.3.4",
     "axios": "^0.26.1",
-    "book-ui": "file:../book-ui-0.2.27.tgz",
+    "book-ui": "file:../book-ui-0.2.34.tgz",
     "core-js": "^3.21.1",
-    "dayjs": "^1.10.8",
+    "dayjs": "^1.11.0",
     "element-ui": "^2.15.6",
     "gcls-book-question-ui": "file:../gcls-book-question-ui-0.1.0.tgz",
     "jquery": "^3.6.0",
+    "js-base64": "^3.7.2",
     "js-cookie": "^3.0.1",
     "jsplumb": "^2.15.6",
     "normalize.css": "^8.0.1",
     "nprogress": "^0.2.0",
     "vue": "^2.6.14",
-    "vue-i18n": "^8.27.0",
+    "vue-i18n": "^8.27.1",
     "vue-pdf": "^4.3.0",
     "vue-router": "^3.5.3",
     "vue-video-player": "^5.0.2",
     "vuex": "^3.6.2"
   },
   "devDependencies": {
-    "@babel/core": "^7.17.5",
+    "@babel/core": "^7.17.8",
     "@babel/eslint-parser": "^7.17.0",
     "@babel/preset-env": "^7.16.11",
-    "@rushstack/eslint-patch": "^1.1.0",
-    "@vue/cli-plugin-babel": "~4.5.15",
-    "@vue/cli-plugin-eslint": "~4.5.15",
-    "@vue/cli-plugin-router": "~4.5.15",
-    "@vue/cli-plugin-unit-jest": "^4.5.15",
-    "@vue/cli-plugin-vuex": "~4.5.15",
-    "@vue/cli-service": "~4.5.15",
+    "@rushstack/eslint-patch": "^1.1.1",
+    "@vue/cli-plugin-babel": "~4.5.17",
+    "@vue/cli-plugin-eslint": "~4.5.17",
+    "@vue/cli-plugin-router": "~4.5.17",
+    "@vue/cli-plugin-unit-jest": "^4.5.17",
+    "@vue/cli-plugin-vuex": "~4.5.17",
+    "@vue/cli-service": "~4.5.17",
     "@vue/eslint-config-prettier": "^7.0.0",
     "@vue/test-utils": "^1.3.0",
     "babel-jest": "^27.5.1",
-    "babel-loader": "^8.2.3",
+    "babel-loader": "^8.2.4",
     "babel-plugin-dynamic-import-node": "^2.3.3",
     "compression-webpack-plugin": "^6.1.1",
     "eslint": "^7.32.0",
     "eslint-plugin-prettier": "^4.0.0",
     "eslint-plugin-vue": "^8.5.0",
     "html-webpack-plugin": "^5.3.1",
-    "postcss": "^8.4.8",
+    "postcss": "^8.4.12",
     "postcss-html": "^1.3.0",
-    "prettier": "2.5.1",
-    "sass": "^1.49.9",
+    "prettier": "2.6.1",
+    "sass": "^1.49.10",
     "sass-loader": "^10.2.1",
     "script-ext-html-webpack-plugin": "^2.1.5",
-    "stylelint": "14.5.3",
+    "stylelint": "14.6.1",
     "stylelint-config-prettier": "^9.0.3",
     "stylelint-config-recess-order": "^3.0.0",
-    "stylelint-config-recommended-vue": "^1.3.0",
+    "stylelint-config-recommended-vue": "^1.4.0",
     "stylelint-config-standard-scss": "^3.0.0",
     "stylelint-declaration-block-no-ignored-properties": "^2.5.0",
     "stylelint-webpack-plugin": "^3.1.0",

BIN
public/favicon.ico


+ 0 - 1
public/index.html

@@ -4,7 +4,6 @@
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
-    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
     <title><%= webpackConfig.name %></title>
   </head>
   <body>

+ 20 - 0
src/api/course.js

@@ -569,3 +569,23 @@ export function SendMessageToCourseStudent(data) {
     data
   });
 }
+
+// 支付我的订单
+export function PayMyOrder(data) {
+  return request({
+    method: 'post',
+    url: process.env.VUE_APP_LearnWebSI,
+    params: getRequestParams('order-order_manager-PayMyOrder'),
+    data
+  });
+}
+
+// 得到我的订单信息
+export function GetMyOrder(data) {
+  return request({
+    method: 'post',
+    url: process.env.VUE_APP_LearnWebSI,
+    params: getRequestParams('order-order_manager-GetMyOrder'),
+    data
+  });
+}

+ 13 - 0
src/api/live.js

@@ -344,3 +344,16 @@ export function SetCurGroupToExample_Teacher(data) {
     data
   });
 }
+
+/**
+ * 教师调用这个方法,调整分组
+ * @param { Object } data
+ */
+export function AdjustGroup(data) {
+  return request({
+    method: 'post',
+    url: process.env.VUE_APP_LearnWebSI,
+    params: getRequestParams('live_room-live_room_dispatch-AdjustGroup'),
+    data
+  });
+}

+ 1 - 2
src/api/table.js

@@ -35,11 +35,10 @@ export function GetMyTaskList(data) {
  * @param {Object} data finish_status 完成状态 page_capacity 每页容量 cur_page 当前页码
  */
 export function PageQueryMyCourseList(data) {
-  const params = getRequestParams('teaching-course_manager-PageQueryMyCourseList');
   return request({
     method: 'post',
     url: process.env.VUE_APP_LearnWebSI,
-    params,
+    params: getRequestParams('page_query-PageQueryMyCourseList'),
     data
   });
 }

BIN
src/assets/doc-Q.png


BIN
src/assets/pay/active.png


BIN
src/assets/pay/shouxinyi.png


BIN
src/assets/pdf-Q.png


BIN
src/assets/ppt-Q.png


BIN
src/assets/xls-Q.png


+ 21 - 61
src/common/show_file/index.vue

@@ -1,37 +1,15 @@
 <template>
-  <el-dialog class="show-file" :visible="dialogVisibleShowFile" width="900px" @close="dialogShowFileClose">
+  <el-dialog class="show-file" :visible="dialogVisibleShowFile" width="1100px" @close="dialogShowFileClose">
     <div slot="title">{{ $t('Key322') }}【{{ fileName }}】</div>
-
-    <template v-if="fileType === 'pdf'">
-      <pdf v-for="i in numPages" :key="i" :src="pdfSrc" :page="i" />
-    </template>
-
-    <template v-else-if="isImage(fileType)">
-      <div class="image-parent">
-        <el-image fit="contain" :src="fileUrl" />
-      </div>
-    </template>
-
-    <div v-else-if="fileType === 'mp3'" class="audio-file">
-      <audio :src="fileUrl" controls />
-    </div>
-
-    <div v-else-if="fileType === 'mp4'" class="video-file">
-      <video :src="fileUrl" controls />
-    </div>
-
-    <div v-else-if="fileType === 'txt'" class="text-file">
-      <el-input v-model="text" type="textarea" :readonly="true" resize="none" />
-    </div>
-
-    <template v-else>
+    <div v-loading="loading">
       <iframe
-        :src="'https://view.officeapps.live.com/op/view.aspx?src=' + `${fileUrl}`"
+        v-if="fileUrl.length > 0"
+        id="iframe"
+        :src="`${$store.state.app.config.doc_preview_service_address}/onlinePreview?url=${fileUrl}`"
         width="100%"
-        height="490px"
-        scrolling="no"
+        height="540px"
       />
-    </template>
+    </div>
 
     <div slot="footer">
       <el-button @click="dialogShowFileClose">
@@ -42,13 +20,12 @@
 </template>
 
 <script>
-import pdf from 'vue-pdf';
 import { GetFileStoreInfo } from '@/api/app';
 import { getFileType } from '@/utils/filter';
+import { encode } from 'js-base64';
 
 export default {
   name: 'ShowFile',
-  components: { pdf },
   props: {
     fileName: {
       default: '',
@@ -61,6 +38,7 @@ export default {
   },
   data() {
     return {
+      loading: false,
       dialogVisibleShowFile: false,
       pdfSrc: '',
       numPages: 1,
@@ -90,33 +68,15 @@ export default {
   },
   methods: {
     getFileStoreInfo() {
-      GetFileStoreInfo({ file_id: this.fileId }).then(({ file_url_https, file_relative_path }) => {
-        this.fileUrl = file_url_https;
-
-        if (this.fileType === 'pdf') {
-          this.getNumPages(file_relative_path);
-        }
-
-        if (this.fileType === 'txt') {
-          fetch(`${process.env.VUE_APP_PDF}${file_relative_path}`).then(async res => {
-            if (!res.ok) return;
-            this.text = await res.text();
-          });
-        }
-      });
-    },
-
-    getNumPages(url) {
-      const loadingTask = pdf.createLoadingTask(`${process.env.VUE_APP_PDF}${url}`);
-      loadingTask.promise
-        .then(pdf => {
-          this.pdfSrc = loadingTask;
-          this.numPages = pdf.numPages;
-        })
-        .catch(err => {
-          console.error('pdf加载失败', err);
-          this.$message.error(this.$i18n.t('Key323'));
+      GetFileStoreInfo({ file_id: this.fileId }).then(({ file_url_https }) => {
+        this.loading = true;
+        this.fileUrl = encodeURIComponent(encode(file_url_https));
+        this.$nextTick(() => {
+          document.getElementById('iframe').onload = () => {
+            this.loading = false;
+          };
         });
+      });
     },
 
     showDialog() {
@@ -126,10 +86,6 @@ export default {
     dialogShowFileClose() {
       this.dialogVisibleShowFile = false;
       this.$emit('dialogShowFileClose');
-    },
-
-    isImage(type) {
-      return ['jpeg', 'gif', 'jpg', 'png', 'bmp', 'pic', 'svg'].includes(type);
     }
   }
 };
@@ -157,6 +113,10 @@ export default {
 
   .video-file {
     @extend %image-parent;
+
+    video {
+      width: 100%;
+    }
   }
 
   .text-file {

+ 9 - 1
src/components/course/CompletionView.vue

@@ -37,7 +37,15 @@
       />
     </template>
     <template v-if="category == 'NNPE'">
-      <booknnpe v-if="context" :context="context" :theme-color="themeColor" />
+      <booknnpe
+        v-if="context"
+        :context="context"
+        :theme-color="themeColor"
+        task-model="ANSWER"
+        :is-show-title="true"
+        :is-show-save="false"
+        :book-answer-content="bookAnswerContent"
+      />
     </template>
 
     <div slot="footer" />

+ 12 - 4
src/components/course/FinishCourseware.vue

@@ -31,15 +31,23 @@
         task-model=""
         :context="context"
         :theme-color="themeColor"
-        @finishTaskMaterial="saveNPCAnswer"
+        @finishTaskMaterial="finishMyTaskMaterial_Student"
       />
     </template>
 
     <template v-if="category == 'NNPE'">
-      <booknnpe v-if="context" :context="context" :theme-color="themeColor" />
+      <booknnpe
+        v-if="context"
+        :context="context"
+        :theme-color="themeColor"
+        :is-show-title="true"
+        task-model=""
+        :is-show-save="true"
+        @finishTaskMaterial="finishMyTaskMaterial_Student"
+      />
     </template>
 
-    <div v-if="category !== 'NPC'" slot="footer">
+    <div v-if="category !== 'NPC' && category !== 'NNPE'" slot="footer">
       <el-button type="primary" @click="finishTaskMaterial">
         {{ $t('Key82') }}
       </el-button>
@@ -126,7 +134,7 @@ export default {
     }
   },
   methods: {
-    saveNPCAnswer(content, duration) {
+    finishMyTaskMaterial_Student(content, duration) {
       const loading = this.$loading();
       FinishMyTaskMaterial_Student({
         task_id: this.id,

+ 35 - 63
src/components/live/CurMaterial.vue

@@ -62,44 +62,38 @@
         />
       </template>
       <template v-if="category == 'NNPE'">
-        <booknnpe v-if="context" :context="context" :theme-color="themeColor" />
+        <booknnpe
+          v-if="context"
+          :context="context"
+          :theme-color="themeColor"
+          :task-model="isFinished ? 'ANSWER' : ''"
+          :is-show-save="!isFinished"
+          :is-show-title="true"
+          :book-answer-content="bookAnswerContent"
+          @finishTaskMaterial="saveNPCAnswer"
+        />
       </template>
     </template>
 
     <template v-else>
-      <template v-if="fileType === 'pdf'">
-        <pdf v-for="i in numPages" :key="i" :src="pdfSrc" :page="i" />
-      </template>
-
-      <template v-else-if="isImage(fileType)">
-        <el-image fit="contain" :src="file_url_https" />
-      </template>
-
-      <div v-else-if="fileType === 'mp3'" class="audio-file">
-        <audio :src="file_url_https" controls />
-      </div>
-
-      <div v-else-if="fileType === 'mp4'" class="video-file">
-        <video :src="file_url_https" controls />
-      </div>
-
-      <div v-else-if="fileType === 'txt'" class="text-file">
-        <el-input v-model="text" type="textarea" :readonly="true" resize="none" />
-      </div>
-
-      <template v-else>
+      <div v-loading="file_loading">
         <iframe
-          :src="'https://view.officeapps.live.com/op/view.aspx?src=' + `${file_url_https}`"
+          v-if="fileUrl.length > 0"
+          id="iframe"
+          :src="`${$store.state.app.config.doc_preview_service_address}/onlinePreview?url=${fileUrl}`"
           width="100%"
-          height="490px"
-          scrolling="no"
+          height="540px"
         />
-      </template>
+      </div>
     </template>
 
     <div slot="footer">
       <el-button
-        v-if="category !== 'NPC' && (isCurMaterial || (!isFinished && material_type === 'COURSEWARE'))"
+        v-if="
+          category !== 'NPC' &&
+          category !== 'NNPE' &&
+          (isCurMaterial || (!isFinished && material_type === 'COURSEWARE'))
+        "
         type="primary"
         @click="finishTaskMaterial"
       >
@@ -110,17 +104,14 @@
 </template>
 
 <script>
-import pdf from 'vue-pdf';
 import { GetCoursewareContent_View, GetMaterialInfo } from '@/api/course';
 import { GetFileStoreInfo, getContentFile } from '@/api/app';
 import { FinishMyMaterial, GetCurMaterialSent, GetStudentExamAnswer_FinishMaterial } from '@/api/live';
 import { getToken } from '@/utils/auth';
+import { encode } from 'js-base64';
 
 export default {
   name: 'CurMaterial',
-  components: {
-    pdf
-  },
   props: {
     taskId: {
       default: '',
@@ -141,6 +132,7 @@ export default {
   },
   data() {
     return {
+      file_loading: false,
       visible: false,
       material_id: '',
       material_name: '',
@@ -150,8 +142,9 @@ export default {
       exam_answer: '',
       ui_type: '',
       category: '',
-      file_relative_path: '',
+      fileUrl: '',
       file_url_https: '',
+      pdfNum: 0,
       pdfSrc: '',
       numPages: 1,
       curStudentID: '',
@@ -187,7 +180,7 @@ export default {
       if (!newVal) {
         this.context = null;
         this.exam_answer = '';
-        this.file_relative_path = '';
+        this.fileUrl = '';
         this.file_url_https = '';
         this.pdfSrc = '';
         this.numPages = 1;
@@ -310,20 +303,16 @@ export default {
     },
 
     getFileStoreInfo() {
-      GetFileStoreInfo({ file_id: this.material_id }).then(({ file_relative_path, file_url_https }) => {
-        this.file_relative_path = file_relative_path;
+      GetFileStoreInfo({ file_id: this.material_id }).then(({ file_url_https }) => {
+        this.category = 'file';
         this.file_url_https = file_url_https;
-        const fileType = file_url_https.slice(file_url_https.lastIndexOf('.') + 1, file_url_https.length);
-        if (fileType === 'pdf') {
-          this.getNumPages(file_relative_path);
-        }
-
-        if (fileType === 'txt') {
-          fetch(`${process.env.VUE_APP_PDF}${file_relative_path}`).then(async res => {
-            if (!res.ok) return;
-            this.text = await res.text();
-          });
-        }
+        this.fileUrl = encodeURIComponent(encode(file_url_https));
+        this.file_loading = true;
+        this.$nextTick(() => {
+          document.getElementById('iframe').onload = () => {
+            this.file_loading = false;
+          };
+        });
       });
     },
 
@@ -347,19 +336,6 @@ export default {
       });
     },
 
-    getNumPages(url) {
-      const loadingTask = pdf.createLoadingTask(`${process.env.VUE_APP_PDF}${url}`);
-      loadingTask.promise
-        .then(pdf => {
-          this.pdfSrc = loadingTask;
-          this.numPages = pdf.numPages;
-        })
-        .catch(err => {
-          console.error('pdf加载失败', err);
-          this.$message.error(this.$i18n.t('Key323'));
-        });
-    },
-
     handleBookUserAnswer(data) {
       this.exam_answer = data;
     },
@@ -407,10 +383,6 @@ export default {
         });
     },
 
-    isImage(type) {
-      return ['jpeg', 'gif', 'jpg', 'png', 'bmp', 'pic', 'svg'].includes(type);
-    },
-
     /**
      * 课件方法
      */

+ 9 - 6
src/components/payment/Audit.vue

@@ -77,10 +77,8 @@ export default {
   },
   props: {
     data: {
-      default: () => {
-        return {};
-      },
-      type: Object
+      type: Object,
+      required: true
     },
     goodsId: {
       default: '',
@@ -106,8 +104,13 @@ export default {
       ApplyJoinCourse({
         course_id: this.goodsId, // 课程 ID
         discount_code: this.discount_code // 优惠码 (目前暂时没有用到)
-      }).then(() => {
+      }).then(({ is_audited, order_id }) => {
         this.$message.success(this.$i18n.t('Key631'));
+        if (is_audited === 'true') {
+          this.$emit('auditedSuccess', order_id);
+          return;
+        }
+
         this.visible = true;
       });
     },
@@ -139,7 +142,7 @@ export default {
       margin-left: 24px;
 
       .p1 {
-        display: box;
+        display: -webkit-box;
         width: 360px;
         max-height: 48px;
         margin-top: 22px;

+ 392 - 0
src/components/payment/Payment.vue

@@ -0,0 +1,392 @@
+<template>
+  <!-- 支付 -->
+  <div v-if="data" v-loading="loading" class="Nopyment">
+    <div class="message">
+      <div :class="['coverUrl', data.goods_type !== 401 ? 'coverUrl-border' : '']">
+        <template v-if="data.goods_type !== 401">
+          <img :src="data.goods_picture_url" />
+        </template>
+      </div>
+      <div class="text">
+        <p class="p1">
+          {{ data.goods_name }}
+        </p>
+        <p class="p2">
+          <span>{{ $t('Key44') }}</span>
+        </p>
+        <p class="p3">{{ data.goods_person_name_desc }}</p>
+      </div>
+      <div class="price">
+        <p v-if="data.hasOwnProperty('price')">¥<span v-html="changePrice(data.price, 16)"></span></p>
+        <p v-if="data.hasOwnProperty('goods_price')">¥<span v-html="changePrice(data.goods_price, 16)"></span></p>
+      </div>
+    </div>
+    <div class="pay-platform">
+      <!-- 选择支付平台 -->
+      <p class="pay-platform-title">{{ $t('Key473') }}</p>
+      <ul class="pay-platform-list">
+        <li v-for="(item, index) in platList" :key="'plat' + index" :class="item.isSelected ? 'active' : ''">
+          <img :src="item.img" class="plat-logo" />
+          <img src="../../assets/pay/active.png" class="active-icon" />
+        </li>
+      </ul>
+    </div>
+    <div class="total">
+      <p class="p1">
+        <!-- 一件商品,总金额 -->
+        <span> {{ $t('Key53') }}: </span>
+        <span v-if="data.hasOwnProperty('price')" class="co-value">
+          ¥<span v-html="changePrice(data.price, 16)"></span>
+        </span>
+        <span v-if="data.hasOwnProperty('goods_price')" class="co-value">
+          ¥<span v-html="changePrice(data.goods_price, 16)"></span>
+        </span>
+      </p>
+      <p class="p2">
+        <!-- 优惠折扣 -->
+        <span> {{ $t('Key54') }}: </span>
+        <span class="co-value">-¥<span v-html="changePrice(data.discount_money, 16)"></span></span>
+      </p>
+      <p class="p4">
+        <span></span>
+        <!-- 优惠码  未使用优惠码-->
+        <span class="no-code">{{ data.discount_code ? $t('Key232') + ':' + data.discount_code : $t('Key108') }}</span>
+      </p>
+      <p class="p3">
+        <!-- 应付: -->
+        <span> {{ $t('Key55') }}: </span>
+        <span class="co-value">¥<span v-html="changePrice(data.receivables_money, 24)"></span></span>
+      </p>
+    </div>
+    <div class="submitBtn">
+      <!-- 去支付 -->
+      <button @click="buy">{{ $t('Key474') }}</button>
+    </div>
+  </div>
+</template>
+
+<script>
+import { PayMyOrder, GetMyOrder } from '@/api/course';
+
+export default {
+  props: {
+    orderId: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    // 这里存放数据
+    return {
+      data: {},
+      loading: false,
+      platList: [
+        {
+          img: require('../../assets/pay/shouxinyi.png'),
+          isSelected: true
+        }
+      ]
+    };
+  },
+  watch: {
+    orderId(newVal) {
+      if (!newVal) return;
+      GetMyOrder({ id: newVal }).then(res => {
+        this.data = res;
+      });
+    }
+  },
+  created() {
+    GetMyOrder({ id: this.orderId }).then(res => {
+      this.data = res;
+    });
+  },
+  methods: {
+    // 购买
+    buy() {
+      this.loading = true;
+      // 首先添加订单
+      PayMyOrder({
+        id: this.orderId,
+        pay_money: this.data.receivables_money, // 实际入账金额,由易支付成功返回
+        bank_transaction_sn: '' // 银行交易流水号,由易支付成功返回
+      })
+        .then(res => {
+          this.loading = false;
+          this.$emit('judgePayResult', true);
+          // 调取支付接口
+          if (res.pay_page_url) {
+            window.location.href = res.pay_page_url;
+          } else {
+            this.$router.push('/OrderPaySuccess');
+          }
+        })
+        .catch(() => {
+          this.loading = false;
+        });
+    },
+    changePrice(price, fontSize1, fontSize2) {
+      let prices = price || 0;
+      prices = prices.toFixed(2);
+      prices = prices.toString();
+      const arr = prices.split('.');
+      const str = `<span style="font-size: ${fontSize1 || 16}px;">${arr[0]}</span>.<span style="font-size: ${
+        fontSize2 || 16
+      }px;">${arr[1]}</span>`;
+
+      return str;
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.Nopyment {
+  .pay-platform {
+    box-sizing: border-box;
+    width: 656px;
+    padding: 16px 0;
+    border-top: 1px rgba(44, 44, 44, 15%) solid;
+    border-bottom: 1px rgba(44, 44, 44, 15%) solid;
+
+    &-title {
+      padding: 0;
+      margin: 0;
+      margin-bottom: 16px;
+      font-size: 16px;
+      font-style: normal;
+      font-weight: normal;
+      line-height: 150%;
+      color: #000;
+    }
+
+    &-list {
+      display: flex;
+      flex-wrap: wrap;
+      padding: 0;
+      margin: 0;
+
+      > li {
+        position: relative;
+        box-sizing: border-box;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 152px;
+        height: 60px;
+        margin: 0 8px 8px 0;
+        cursor: pointer;
+        background: #fff;
+        border: 1px solid rgba(44, 44, 44, 15%);
+        border-radius: 4px;
+
+        > .plat-logo {
+          width: 99px;
+          height: 26px;
+        }
+
+        > .active-icon {
+          position: absolute;
+          right: -1px;
+          bottom: -1px;
+          display: block;
+          width: 20px;
+          height: 20px;
+        }
+
+        &.active {
+          border-color: #f90;
+        }
+      }
+    }
+  }
+
+  .message {
+    display: flex;
+    width: 656px;
+    height: 152px;
+    margin-bottom: 24px;
+    background: rgba(70, 70, 70, 3%);
+    border-radius: 8px;
+
+    .coverUrl {
+      box-sizing: border-box;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 120px;
+      height: 120px;
+      margin-top: 16px;
+      margin-left: 16px;
+
+      &-border {
+        border: 1px rgba(0, 0, 0, 15%) solid;
+      }
+
+      > img {
+        max-width: 100%;
+        max-height: 100%;
+      }
+    }
+
+    .text {
+      margin-left: 24px;
+
+      .p1 {
+        display: -webkit-box;
+        width: 360px;
+        margin-top: 22px;
+        overflow: hidden;
+
+        //   height: 45px;
+        font-size: 16px;
+        line-height: 20px;
+        color: #2c2c2c;
+        text-overflow: ellipsis;
+        word-break: break-all;
+        -webkit-box-orient: vertical;
+        -webkit-line-clamp: 2;
+      }
+
+      .p2 {
+        margin-top: 10px;
+
+        span {
+          width: 64px;
+          height: 24px;
+          padding: 2px 8px;
+          font-size: 12px;
+          font-weight: bold;
+          line-height: 24px;
+          color: #f90;
+          text-align: center;
+          background: #ffefd8;
+          border-radius: 4px;
+        }
+      }
+
+      .p3 {
+        margin-top: 10px;
+      }
+    }
+
+    .price {
+      box-sizing: border-box;
+      width: 110px;
+      padding-right: 16px;
+
+      p {
+        width: 100%;
+        margin-top: 22px;
+        font-size: 16px;
+        font-weight: bold;
+        color: #ff4c00;
+        text-align: right;
+      }
+    }
+  }
+
+  .promotionCode {
+    display: flex;
+    align-self: center;
+    justify-content: flex-start;
+    width: 100%;
+    height: 58px;
+    margin-top: 24px;
+    line-height: 56px;
+    border-top: 1px solid rgba(44, 44, 44, 15%);
+    border-bottom: 1px solid rgba(44, 44, 44, 15%);
+
+    .sp1 {
+      font-size: 16px;
+      color: #000;
+    }
+
+    input {
+      box-sizing: border-box;
+      flex: 1;
+      height: 55px;
+      padding: 0;
+      padding: 0 24px;
+      border: none;
+      outline: none;
+    }
+
+    .sp2 {
+      font-size: 16px;
+      color: #f90;
+      cursor: pointer;
+    }
+  }
+
+  .total {
+    width: 656px;
+    padding-top: 24px;
+    font-size: 16px;
+    color: #000;
+    text-align: right;
+
+    > p {
+      > span {
+        display: inline-block;
+      }
+
+      .co-value {
+        width: 160px;
+      }
+    }
+
+    .p1 {
+      > span {
+        display: inline-block;
+        line-height: 24px;
+      }
+    }
+
+    .p2 {
+      margin: 16px 0 8px;
+    }
+
+    .p3 {
+      > span {
+        line-height: 36px;
+      }
+
+      .co-value {
+        font-size: 24px;
+        color: #ff4c00;
+      }
+    }
+
+    .p4 {
+      margin-bottom: 16px;
+
+      .no-code {
+        font-size: 12px;
+        font-weight: normal;
+        line-height: 150%;
+        color: rgba(0, 0, 0, 50%);
+        text-align: center;
+      }
+    }
+  }
+
+  .submitBtn {
+    margin-top: 16px;
+    text-align: right;
+
+    button {
+      width: 120px;
+      height: 40px;
+      line-height: 40px;
+      color: white;
+      text-align: center;
+      cursor: pointer;
+      background: #ff4d00;
+      border: none;
+      border-radius: 4px;
+      outline: none;
+    }
+  }
+}
+</style>

+ 8 - 1
src/components/select/SelectCourse.vue

@@ -32,7 +32,14 @@
         </template>
 
         <template v-if="category == 'NNPE'">
-          <booknnpe v-if="context" :context="context" :theme-color="themeColor" />
+          <booknnpe
+            v-if="context"
+            :context="context"
+            :theme-color="themeColor"
+            task-model=""
+            :is-show-save="false"
+            :is-show-title="true"
+          />
         </template>
       </div>
     </div>

+ 9 - 1
src/components/select/SelectTemplate.vue

@@ -28,10 +28,18 @@
       highlight-current-row
       @current-change="handleCurrentChange"
     >
-      <el-table-column prop="name" :label="$t('Key191')" width="320" />
+      <el-table-column prop="name" :label="$t('Key134')" width="150" />
+      <el-table-column prop="creator_name" :label="$t('Key280')" width="120" />
+      <el-table-column prop="org_name" :label="$t('Key136')" width="120" />
       <el-table-column :label="$t('Key250')">
         <template slot-scope="{ row }"> <i class="el-icon-date" /> {{ row.begin_date }} - {{ row.end_date }} </template>
       </el-table-column>
+
+      <el-table-column fixed="right" width="100" label="操作">
+        <template slot-scope="{ row }">
+          <router-link :to="`/TemplateDetails?id=${row.id}`">查看</router-link>
+        </template>
+      </el-table-column>
     </el-table>
 
     <el-pagination

+ 19 - 7
src/layouts/components/LayoutHeader.vue

@@ -2,7 +2,7 @@
   <!-- 顶部登录导航 -->
   <div class="LoginNav">
     <div class="logo">
-      <span class="logo-img">
+      <span class="logo-img" @click="handleSelect">
         <el-image :src="$store.state.app.config.logo_image_url" />
       </span>
       <ul class="logo-projectlist">
@@ -58,7 +58,7 @@
 
 <script>
 import { mapGetters } from 'vuex';
-import { getToken, removeToken } from '@/utils/auth';
+import { getToken, removeToken, getConfig } from '@/utils/auth';
 import { GetLanguageList, GetChildSysList_CanEnter_PC } from '@/api/app';
 import { loadLanguageAsync } from '@/locales/i18n';
 import { IsExistMyMessage_NotRead } from '@/api/user';
@@ -100,14 +100,21 @@ export default {
 
       this.getLangList();
     }
+
+    const { token: config, isHas: isConfigHas } = getConfig();
+    if (isConfigHas) {
+      const link = document.querySelector("link[rel*='icon']") || document.createElement('link');
+      link.type = 'image/x-icon';
+      link.rel = 'shortcut icon';
+      link.href = config.title_icon_url;
+      console.log(config.title_icon_url);
+      document.getElementsByTagName('head')[0].appendChild(link);
+    }
   },
   methods: {
     // 切换导航
-    handleSelect(key) {
-      this.activeIndex = key;
-      if (this.activeIndex === '1') {
-        window.location.href = '/';
-      }
+    handleSelect() {
+      window.location.href = '/';
     },
     // 改变下拉框导航索引
     changeLoginNavIndex(key) {
@@ -193,6 +200,11 @@ export default {
 
     &-img {
       min-width: 242px;
+
+      .el-image {
+        height: 48px;
+        cursor: pointer;
+      }
     }
 
     &-projectlist {

+ 26 - 1
src/router/index.js

@@ -111,6 +111,18 @@ const routes = [
       }
     ]
   },
+  // 0元付款成功
+  {
+    path: '/OrderPaySuccess',
+    component: Layout,
+    redirect: '/OrderPaySuccess/index',
+    children: [
+      {
+        path: '/OrderPaySuccess/index',
+        component: () => import('@/views/OrderPaySuccess.vue')
+      }
+    ]
+  },
   {
     path: '/task_detail/show_courseware/:coursewareId',
     component: () => import('@/views/task_details/ShowCourseware')
@@ -155,6 +167,7 @@ const routes = [
       }
     ]
   },
+  // 课程详情
   {
     path: '/course_details',
     component: Layout,
@@ -162,7 +175,19 @@ const routes = [
     children: [
       {
         path: '/GoodsDetail',
-        component: () => import('@/views/course_details/index')
+        component: () => import('@/views/course_details')
+      }
+    ]
+  },
+  // 模板详情
+  {
+    path: '/template_details',
+    component: Layout,
+    redirect: '/TemplateDetails',
+    children: [
+      {
+        path: '/TemplateDetails',
+        component: () => import('@/views/template_details')
       }
     ]
   },

+ 1 - 0
src/settings.js

@@ -2,6 +2,7 @@ const Cookies = require('js-cookie');
 
 let title = '';
 const config = Cookies.get('GCLS_Config');
+
 if (config) {
   const configObj = JSON.parse(config);
   title = configObj.title;

+ 3 - 1
src/store/modules/app.js

@@ -23,7 +23,9 @@ const getDefaultSate = () => {
       title: isHas ? token.title : '',
       logo_image_url: isHas ? token.logo_image_url : '',
       logo_image_url_home: isHas ? token.logo_image_url_home : '',
-      sys_type: isHas ? token.sys_type : ''
+      title_icon_url: isHas ? token.title_icon_url : '',
+      sys_type: isHas ? token.sys_type : '',
+      doc_preview_service_address: isHas ? token.doc_preview_service_address : ''
     }
   };
 };

+ 20 - 0
src/views/OrderPaySuccess.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="paymentSuccess">
+    <Header />
+    <div class="main">{{ $t('Key657') }}</div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.paymentSuccess {
+  height: 100%;
+
+  .main {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    font-size: 50px;
+  }
+}
+</style>

+ 75 - 10
src/views/course_details/index.vue

@@ -1,7 +1,15 @@
 <template>
   <!-- 课程详情 -->
-  <div>
-    <div v-if="isData" v-loading="loading" class="CourseDetail">
+  <div class="course_info">
+    <div
+      v-if="
+        isData &&
+        ($store.state.user.user_type === 'TEACHER' ||
+          (CourseData.is_release === 'true' && CourseData.is_deleted === 'false'))
+      "
+      v-loading="loading"
+      class="CourseDetail"
+    >
       <div class="main">
         <div class="bookDetail">
           <div class="rightUp">
@@ -236,19 +244,37 @@
           width="720px"
           :before-close="closeaudit"
         >
-          <audit :goods-id="goods_id" :data="CourseData" :goods-type="goods_type" />
+          <audit :goods-id="goods_id" :data="CourseData" :goods-type="goods_type" @auditedSuccess="auditedSuccess" />
         </el-dialog>
       </div>
     </div>
 
     <div v-else-if="!loading" class="non-existent">
-      <h1>{{ $t('Key395') }}</h1>
+      {{
+        CourseData.is_deleted === 'true'
+          ? '无法查看,课程已删除'
+          : CourseData.is_release === 'false'
+          ? '无法查看,课程已下架'
+          : '课程不存在'
+      }}
     </div>
+
+    <el-dialog
+      class="orderDialog"
+      top="50px"
+      :title="$t('Key471') + ':' + orderId"
+      :visible.sync="visiblePay"
+      width="720px"
+      :before-close="closePayment"
+    >
+      <Payment ref="Confirmorder" :order-id="orderId" @judgePayResult="judgePayResult"></Payment>
+    </el-dialog>
   </div>
 </template>
 
 <script>
 import Audit from '@/components/payment/Audit.vue';
+import Payment from '@/components/payment/Payment.vue';
 import {
   GetCourseInfoBox,
   CheckMyGoodsBuyStatus,
@@ -261,7 +287,8 @@ import { GetMyGoodsBuyInfo } from '@/api/user';
 export default {
   name: 'CourseDetails',
   components: {
-    Audit
+    Audit,
+    Payment
   },
   data() {
     const query = this.$route.query;
@@ -285,7 +312,9 @@ export default {
         { id: 'pre_task_list', name: 'Key353' },
         { id: 'mid_task_list', name: 'Key354' },
         { id: 'after_task_list', name: 'Key355' }
-      ]
+      ],
+      visiblePay: false,
+      orderId: ''
     };
   },
   created() {
@@ -394,6 +423,29 @@ export default {
       });
     },
 
+    auditedSuccess(orderId) {
+      this.orderId = orderId;
+      this.auditShow = false;
+      this.visiblePay = true;
+    },
+
+    closePayment() {
+      this.visiblePay = false;
+      this.$refs.Confirmorder.clearData();
+    },
+
+    judgePayResult(bool) {
+      this.isPayment = false;
+      if (bool) {
+        this.$message.success(this.$t('Key657'));
+      } else {
+        this.$message.warning('支付失败');
+      }
+      CheckMyGoodsBuyStatus({ goods_type: this.goods_type, goods_id: this.goods_id }).then(({ is_buy }) => {
+        this.is_buy = is_buy === 'true';
+      });
+    },
+
     closeaudit() {
       this.auditShow = false;
     },
@@ -436,6 +488,9 @@ export default {
 
 <style lang="scss" scoped>
 .CourseDetail {
+  width: 100vw;
+  min-height: calc(100vh - 64px);
+
   .main {
     min-height: 100%;
     padding: 52px 0 20px;
@@ -487,7 +542,7 @@ export default {
         }
 
         .intro {
-          display: box;
+          display: -webkit-box;
           height: 36px;
           -webkit-box-orient: vertical;
           -webkit-line-clamp: 2;
@@ -969,9 +1024,19 @@ export default {
   }
 }
 
-.non-existent {
-  margin-top: 15vh;
-  text-align: center;
+.course_info {
+  display: flex;
+  justify-content: center;
+
+  .non-existent {
+    padding: 8px;
+    margin-top: 15vh;
+    color: #e05153;
+    text-align: center;
+    background-color: #fbf3f4;
+    border: 1px solid;
+    border-radius: 4px;
+  }
 }
 </style>
 

+ 3 - 6
src/views/live/SelectDevice.vue

@@ -77,16 +77,13 @@ export default {
   watch: {
     dialogVisibleDevice(newValue) {
       if (newValue) {
-        const device = this.$store.state.app.liveDevice;
-        this.video = this.device.video.length === 0 ? '' : this.selectValue(this.device.video, device.video);
-        this.audio = this.device.audio.length === 0 ? '' : this.selectValue(this.device.audio, device.audio);
+        const { video, audio } = this.$store.state.app.liveDevice;
+        this.video = video;
+        this.audio = audio;
       }
     }
   },
   methods: {
-    selectValue(arr, value) {
-      return arr.some(item => item.deviceId === value) ? value : arr[0].deviceId;
-    },
     dialogDeviceClose() {
       this.$emit('dialogDeviceClose', { audio: this.audio, video: this.video });
     }

+ 10 - 5
src/views/live/student/audit.js

@@ -1,6 +1,15 @@
 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 {
+  initSDK,
+  sendMsg,
+  getLiveStat,
+  createScript,
+  downloadWebSDK,
+  handsDown,
+  chatRoll,
+  closeVideo
+} from '@/views/live/common';
 import i18n from '@/locales/i18n';
 
 /**
@@ -24,10 +33,6 @@ export function unSubscribeStream(unSubStream) {
 export function initListener(vue) {
   rtc.on('login_success', data => {
     console.log('登录成功', data);
-    Message({
-      message: i18n.t('Key442'),
-      type: 'success'
-    });
   });
 
   rtc.on('login_failed', data => {

+ 0 - 4
src/views/live/student/group.js

@@ -62,10 +62,6 @@ function createLocalStream(vue) {
 export function initListener(vue) {
   rtc.on('login_success', data => {
     console.log('登录成功', data);
-    Message({
-      message: i18n.t('Key442'),
-      type: 'success'
-    });
   });
 
   rtc.on('login_failed', data => {

+ 17 - 3
src/views/live/student/group.vue

@@ -193,7 +193,9 @@ export default {
       // 旁听学员列表
       audience_list: [],
       is_example_group: false,
-      marginLeft: 0
+      marginLeft: 0,
+      // 所在组的实例标记号
+      group_instance_mark: ''
     };
   },
   watch: {
@@ -370,7 +372,18 @@ export default {
     getGroupStatus() {
       this.timer = setInterval(() => {
         GetGroupStatus({ task_id: this.task_id }).then(
-          ({ is_enable_group, is_has_group_message, group_message_text, is_audience, is_example_group }) => {
+          ({
+            is_enable_group,
+            is_has_group_message,
+            group_message_text,
+            is_audience,
+            is_example_group,
+            group_instance_mark
+          }) => {
+            if (this.group_instance_mark.length > 0 && this.group_instance_mark !== group_instance_mark) {
+              return this.$router.go(0);
+            }
+
             if (is_enable_group === 'false') {
               clearInterval(this.timer);
               CreateEnterLiveRoomSession({
@@ -449,8 +462,9 @@ export default {
     getMyGroupInfo_Student() {
       GetMyGroupInfo_Student({
         task_id: this.task_id
-      }).then(({ live_room_sys_user_id, room_id, student_list, audience_list }) => {
+      }).then(({ live_room_sys_user_id, room_id, student_list, audience_list, group_instance_mark }) => {
         const data = student_list.find(el => el.is_self === 'true');
+        this.group_instance_mark = group_instance_mark;
         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;

+ 0 - 4
src/views/live/student/live.js

@@ -53,10 +53,6 @@ function unSubscribeStream(unSubStream) {
 export function initListener(vue) {
   rtc.on('login_success', data => {
     console.log('登录成功', data);
-    Message({
-      message: i18n.t('Key442'),
-      type: 'success'
-    });
     vue.roomData = data;
     // 初始化画板
     rtc.canvasInit({

+ 26 - 58
src/views/live/teacher/CompleteList.vue

@@ -76,39 +76,27 @@
           />
         </template>
         <template v-if="category == 'NNPE'">
-          <booknnpe v-if="context" :context="context" :theme-color="themeColor" />
+          <booknnpe
+            v-if="context"
+            :context="context"
+            :theme-color="themeColor"
+            :is-show-title="true"
+            task-model="ANSWER"
+            :is-show-save="false"
+            :book-answer-content="bookAnswerContent"
+          />
         </template>
       </template>
       <template v-else>
-        <!-- pdf -->
-        <template v-if="fileType === 'pdf'">
-          <pdf v-for="i in numPages" :key="i" :src="pdfSrc" :page="i" />
-        </template>
-
-        <template v-else-if="isImage(fileType)">
-          <el-image fit="contain" :src="file_url_https" />
-        </template>
-
-        <div v-else-if="fileType === 'mp3'" class="audio-file">
-          <audio :src="file_url_https" controls />
-        </div>
-
-        <div v-else-if="fileType === 'mp4'" class="video-file">
-          <video :src="file_url_https" controls />
-        </div>
-
-        <div v-else-if="fileType === 'txt'" class="text-file">
-          <el-input v-model="text" type="textarea" :readonly="true" resize="none" />
-        </div>
-
-        <template v-else-if="fileType !== 'pdf'">
+        <div v-loading="file_loading">
           <iframe
-            :src="'https://view.officeapps.live.com/op/view.aspx?src=' + `${file_url_https}`"
+            v-if="fileUrl.length > 0"
+            id="iframe"
+            :src="`${$store.state.app.config.doc_preview_service_address}/onlinePreview?url=${fileUrl}`"
             width="100%"
-            height="490px"
-            scrolling="no"
+            height="540px"
           />
-        </template>
+        </div>
       </template>
     </div>
 
@@ -117,15 +105,12 @@
 </template>
 
 <script>
-import pdf from 'vue-pdf';
 import { GetCurMaterialSent, GetStudentList_FinishMaterial, GetStudentExamAnswer_FinishMaterial } from '@/api/live';
 import { GetCoursewareContent_View, GetMaterialInfo } from '@/api/course';
 import { GetFileStoreInfo, getContentFile } from '@/api/app';
+import { encode } from 'js-base64';
 
 export default {
-  components: {
-    pdf
-  },
   props: {
     dialogVisibleComplete: {
       default: false,
@@ -160,6 +145,8 @@ export default {
       count_not_done: 0,
       count_right: 0,
       count_error: 0,
+      file_loading: false,
+      fileUrl: '',
       file_relative_path: '',
       file_url_https: '',
       pdfSrc: '',
@@ -196,6 +183,7 @@ export default {
       if (!newVal) {
         clearInterval(this.listTimer);
         this.material_id = '';
+        this.fileUrl = '';
         this.material_name = '';
         this.student_list = [];
         this.marginLeft = 0;
@@ -301,40 +289,20 @@ export default {
       GetFileStoreInfo({ file_id: this.material_id }).then(({ file_relative_path, file_url_https }) => {
         this.file_relative_path = file_relative_path;
         this.file_url_https = file_url_https;
-        const fileType = file_url_https.slice(file_url_https.lastIndexOf('.') + 1, file_url_https.length);
-        if (fileType === 'pdf') {
-          this.getNumPages(file_relative_path);
-        }
-
-        if (fileType === 'txt') {
-          fetch(`${process.env.VUE_APP_PDF}${file_relative_path}`).then(async res => {
-            if (!res.ok) return;
-            this.text = await res.text();
-          });
-        }
-      });
-    },
-
-    getNumPages(url) {
-      const loadingTask = pdf.createLoadingTask(`${process.env.VUE_APP_PDF}${url}`);
-      loadingTask.promise
-        .then(pdf => {
-          this.pdfSrc = loadingTask;
-          this.numPages = pdf.numPages;
-        })
-        .catch(err => {
-          console.error('pdf加载失败', err);
+        this.fileUrl = encodeURIComponent(encode(file_url_https));
+        this.file_loading = true;
+        this.$nextTick(() => {
+          document.getElementById('iframe').onload = () => {
+            this.file_loading = false;
+          };
         });
+      });
     },
 
     dialogCompleteClose() {
       this.$emit('dialogCompleteClose');
     },
 
-    isImage(type) {
-      return ['jpeg', 'gif', 'jpg', 'png', 'bmp', 'pic', 'svg'].includes(type);
-    },
-
     // 下载文件zip
     downloadBookWriteParent(idList) {
       getContentFile('file_store_manager-StartCreateFileCompressPack', {

+ 127 - 6
src/views/live/teacher/group.vue

@@ -5,6 +5,28 @@
       <div class="live-title">
         <div class="live-title-name">{{ roomInfo.cs_item_name }} {{ roomInfo.task_name }}</div>
         <div>
+          <el-popover
+            v-show="!isGroup"
+            v-model="visible"
+            width="352"
+            popper-class="adjust"
+            trigger="click"
+            placement="bottom-start"
+          >
+            <div class="adjust-title">调整分组</div>
+            <div class="adjust-list">
+              <div v-for="item in adjustGroupList" :key="item.student_id" class="adjust-list-item">
+                <el-avatar icon="el-icon-user" :src="item.student_image_url" />
+                <span class="student-name">{{ item.student_name }}</span>
+                <el-select v-model="item.group_id" size="mini">
+                  <el-option v-for="{ value, label } in groupList" :key="value" :value="value" :label="label" />
+                </el-select>
+              </div>
+            </div>
+            <div class="adjust-button" @click="adjustGroup">确定</div>
+
+            <el-button slot="reference">调整分组</el-button>
+          </el-popover>
           <el-button @click="stopGroup">
             {{ $t('Key404') }}
           </el-button>
@@ -172,7 +194,8 @@ import {
   GetMyGroupInfo_Teacher,
   GetGroupStatus,
   SendGroupMessage,
-  SetCurGroupToExample_Teacher
+  SetCurGroupToExample_Teacher,
+  AdjustGroup
 } from '@/api/live';
 import * as common from './group';
 
@@ -229,9 +252,24 @@ export default {
       // 无远程流学员列表
       noStreamList: [],
       // 旁听学员列表
-      audience_list: []
+      audience_list: [],
+      // 调整分组用列表
+      adjustGroupList: [],
+      visible: false
     };
   },
+  computed: {
+    groupList() {
+      const list = [];
+      this.group_list.forEach(({ group_id, group_num }) => {
+        list.push({
+          label: `小组${group_num}`,
+          value: group_id
+        });
+      });
+      return list;
+    }
+  },
   watch: {
     loadedNumber(newVal) {
       if (newVal === 2) {
@@ -286,10 +324,7 @@ export default {
       }
     });
     this.getLiveRoomInfo();
-    GetGroupInfo_Teacher({ task_id: this.task_id }).then(({ live_room_sys_user_id, group_list }) => {
-      this.group_list = group_list;
-      this.live_room_sys_user_id = live_room_sys_user_id;
-    });
+    this.getGroupInfo_Teacher();
   },
   beforeDestroy() {
     common.closeVideo('main');
@@ -451,6 +486,40 @@ export default {
         .finally(() => {
           loading.close();
         });
+    },
+
+    getGroupInfo_Teacher() {
+      GetGroupInfo_Teacher({ task_id: this.task_id }).then(({ live_room_sys_user_id, group_list }) => {
+        this.group_list = group_list;
+        this.live_room_sys_user_id = live_room_sys_user_id;
+
+        const list = [];
+        this.group_list.forEach(({ group_id, student_list }) => {
+          student_list.forEach(item => {
+            list.push({
+              group_id,
+              ...item
+            });
+          });
+        });
+        this.adjustGroupList = list;
+      });
+    },
+
+    adjustGroup() {
+      const student_list = this.adjustGroupList.map(({ group_id, student_id }) => {
+        return { group_id, student_id };
+      });
+      const loading = this.$loading({ text: '调整分组中...' });
+      AdjustGroup({ task_id: this.task_id, student_list })
+        .then(() => {
+          this.$message.success('调整分组成功');
+          this.getGroupInfo_Teacher();
+          this.visible = false;
+        })
+        .finally(() => {
+          loading.close();
+        });
     }
   }
 };
@@ -483,6 +552,10 @@ $live-bc: #3d3938;
         padding: 7px 12px;
         border-radius: 4px;
       }
+
+      ::v-deep .el-popover__reference-wrapper {
+        margin-right: 8px;
+      }
     }
 
     .live-course-name {
@@ -806,3 +879,51 @@ $live-bc: #3d3938;
   }
 }
 </style>
+
+<style lang="scss">
+.adjust {
+  padding: 0;
+  font-size: 16px;
+  border-width: 0;
+
+  &-title {
+    padding: 16px;
+    font-weight: bold;
+    background-color: #f4f4f4;
+  }
+
+  &-list {
+    height: 400px;
+    padding: 20px 16px;
+    overflow-y: auto;
+
+    &-item {
+      display: flex;
+      column-gap: 16px;
+      align-items: center;
+
+      .el-avatar {
+        min-width: 40px;
+      }
+
+      .student-name {
+        flex: 1;
+      }
+
+      .el-select {
+        width: 85px;
+      }
+    }
+  }
+
+  &-button {
+    padding: 9px 0;
+    font-weight: 700;
+    color: #fff;
+    text-align: center;
+    cursor: pointer;
+    background-color: $basic-color;
+    border-radius: 0 0 4px 4px;
+  }
+}
+</style>

+ 9 - 3
src/views/live/teacher/index.vue

@@ -491,9 +491,15 @@ export default {
     },
 
     closeLiveRoom() {
-      CloseLiveRoom({ task_id: this.task_id }).then(() => {
-        this.$router.push('/');
-        this.$message.success(this.$i18n.t('Key426'));
+      this.$confirm('是否关闭当前直播?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        CloseLiveRoom({ task_id: this.task_id }).then(() => {
+          this.$router.push('/');
+          this.$message.success(this.$i18n.t('Key426'));
+        });
       });
     },
 

+ 0 - 4
src/views/live/teacher/live.js

@@ -82,10 +82,6 @@ export function closeVideoTeacher(options) {
 export function initListener(vue) {
   rtc.on('login_success', data => {
     console.log('登录成功', data);
-    Message({
-      message: i18n.t('Key442'),
-      type: 'success'
-    });
     vue.roomData = data;
     // 初始化画板需要的数据
     const canvasInitData = {

+ 4 - 2
src/views/main/components/MonthlyCalendar.vue

@@ -172,7 +172,9 @@ export default {
       if (type === 'next' && this.time_unit === this.MONTH) {
         this.date = `${curYear + 1}-${curMonth + 1}`;
       }
-      this.$emit('changeDate');
+      this.$nextTick(() => {
+        this.$emit('changeDate');
+      });
       this.getDateArr();
     },
 
@@ -186,7 +188,7 @@ export default {
         this.focusDate = number;
       }
       if (!isCurMonth) {
-        this.changeDate(number < 7 ? 'next' : 'pre');
+        return this.changeDate(number < 7 ? 'next' : 'pre');
       }
       this.$emit('changeDate');
     },

+ 1 - 0
src/views/main/curricula_list/teacher.vue

@@ -142,6 +142,7 @@ export default {
         finish_status: this.finish_status,
         name: this.search,
         page_capacity: this.page_capacity,
+        release_status: -1,
         cur_page: this.cur_page
       }).then(({ course_list, total_count }) => {
         this.courseList = course_list;

+ 9 - 2
src/views/task_details/ShowCourseware.vue

@@ -17,10 +17,17 @@
         />
       </template>
       <template v-else-if="category === 'NPC'">
-        <booknpc v-if="context" :is-show-save="false" :context="context" :theme-color="themeColor" />
+        <booknpc v-if="context" task-model="" :is-show-save="false" :context="context" :theme-color="themeColor" />
       </template>
       <template v-if="category == 'NNPE'">
-        <booknnpe v-if="context" :context="context" :theme-color="themeColor" />
+        <booknnpe
+          v-if="context"
+          :context="context"
+          task-model=""
+          :theme-color="themeColor"
+          :is-show-title="true"
+          :is-show-save="false"
+        />
       </template>
     </div>
   </div>

+ 48 - 11
src/views/task_details/TaskTop.vue

@@ -2,12 +2,21 @@
   <div class="task-detail-top">
     <div class="title">
       <span class="title-name">{{ itemInfo.course_name }}</span>
-      <span class="title-time">{{ itemInfo.time_space_view_txt }}</span>
+      <span class="title-time">{{ itemInfo.cs_item_time }}</span>
     </div>
     <div class="cs_item_name">
       {{ itemInfo.cs_item_name }}
     </div>
     <div class="courseware-list">
+      <template v-if="type === 'teacher'">
+        <div class="task-name">{{ itemInfo.name }}</div>
+        <div class="task-time">{{ itemInfo.time_space_view_txt }}</div>
+        <div class="task-content">
+          <span>任务说明</span>
+          <!-- eslint-disable-next-line vue/no-v-html -->
+          <span v-html="contentUrl"></span>
+        </div>
+      </template>
       <div class="courseware-list-title">
         {{ $t('Key309') }}
       </div>
@@ -49,16 +58,22 @@
 export default {
   props: {
     itemInfo: {
-      default: () => {
-        return {
-          course_name: '',
-          time_space_view_txt: '',
-          cs_item_name: '',
-          cs_item_learning_material_list: [],
-          courseware_list: []
-        };
-      },
-      type: Object
+      type: Object,
+      required: true
+    },
+    type: {
+      type: String,
+      required: true
+    }
+  },
+  computed: {
+    contentUrl() {
+      const content = this.itemInfo.content;
+      if (!content) return '';
+      return content.replace(
+        new RegExp(/(https?:\/\/[\w-]+\.[\w-]+\.[\w-,@?^=%&:/~\\+#]+)/, 'g'),
+        '<a href="$1" target="_blank">$1</a>'
+      );
     }
   },
   methods: {
@@ -108,6 +123,28 @@ export default {
     margin-bottom: 16px;
     border-top: 1px solid #d9d9d9;
 
+    .task-name {
+      margin-bottom: 16px;
+      font-size: 20px;
+      font-weight: bold;
+    }
+
+    .task-time {
+      margin-bottom: 16px;
+      color: #969696;
+    }
+
+    .task-content {
+      display: flex;
+      column-gap: 40px;
+      margin-bottom: 16px;
+      white-space: pre-wrap;
+
+      ::v-deep a {
+        color: #18a0fb;
+      }
+    }
+
     &-title {
       margin-bottom: 16px;
       font-size: 18px;

+ 32 - 5
src/views/task_details/student/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div v-loading="loading" class="task-detail">
-    <task-top :item-info="itemInfo" @viewFile="viewFile" />
+    <task-top :item-info="itemInfo" type="student" @viewFile="viewFile" />
 
     <div class="task-detail-main">
       <div class="time-type">{{ $t(timeType) }} {{ name }}</div>
@@ -9,7 +9,7 @@
       </div>
       <div class="task-require">
         <span class="label">{{ $t('Key326') }}</span>
-        {{ content }}
+        <span v-html="contentUrl"></span>
       </div>
       <div class="task-courseware">
         <span class="label">{{ $t('Key312') }}</span>
@@ -176,6 +176,7 @@ import { GetTaskInfo, FillMyTaskExecuteInfo_Student } from '@/api/course';
 import { isAllowFileType, fileTypeSizeLimit } from '@/utils/validate';
 
 export default {
+  name: 'TaskDetailsStudent',
   components: {
     FinishCourseware,
     TaskTop,
@@ -230,6 +231,14 @@ export default {
         default:
           return '';
       }
+    },
+    contentUrl() {
+      const content = this.content;
+      if (!content) return '';
+      return content.replace(
+        new RegExp(/((https?:\/\/)?[\w-]+\.[\w-]+\.[\w-,@?^=%&:/~\\+#]+)/, 'g'),
+        '<a href="$1" target="_blank">$1</a>'
+      );
     }
   },
   created() {
@@ -255,13 +264,18 @@ export default {
           my_execute_info,
           is_enable_KHPJ,
           is_enable_homework,
-          is_enable_message
+          is_enable_message,
+          cs_item_begin_time,
+          cs_item_end_time
         }) => {
           this.itemInfo = {
+            name,
             time_space_view_txt,
             course_name,
             cs_item_name,
-            cs_item_learning_material_list
+            cs_item_learning_material_list,
+            content,
+            cs_item_time: `${cs_item_begin_time} ~ ${cs_item_end_time}`
           };
 
           this.name = name;
@@ -324,6 +338,11 @@ export default {
     },
 
     fillTaskExecuteInfo_Student() {
+      // 基础任务,必须提交作业
+      if (this.my_execute_info.is_finished === 'false' && this.teaching_type === 12 && this.file_list.length <= 0) {
+        return this.$message.warning('请先提交作业');
+      }
+
       const homework_file_id_list = [];
       this.file_list.forEach(item => {
         homework_file_id_list.push(item.file_id);
@@ -389,7 +408,7 @@ export default {
 };
 </script>
 
-<style lang="scss">
+<style lang="scss" scoped>
 @import '~@/styles/mixin';
 
 $bor-color: #d9d9d9;
@@ -524,6 +543,14 @@ $bor-color: #d9d9d9;
       }
     }
 
+    .task-require {
+      white-space: pre-wrap;
+
+      ::v-deep a {
+        color: #18a0fb;
+      }
+    }
+
     .submit-button {
       padding-left: 120px;
     }

+ 10 - 3
src/views/task_details/teacher/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div v-loading="loading" class="teacher-task-detail">
-    <task-top :item-info="itemInfo" @viewFile="viewFile" />
+    <task-top :item-info="itemInfo" type="teacher" @viewFile="viewFile" />
 
     <div class="teacher-task-detail-main">
       <div class="student-finish-situation">
@@ -132,6 +132,7 @@ import TaskTop from '../TaskTop.vue';
 import { GetTaskInfo, GetTaskStudentExecuteInfo, RemarkTaskStudentExecuteInfo_Teacher } from '@/api/course';
 
 export default {
+  name: 'TaskDetailsTeacher',
   components: { CompletionView, TaskTop, ShowFile },
   data() {
     return {
@@ -202,14 +203,20 @@ export default {
           custom_student_list,
           is_enable_KHPJ,
           is_enable_homework,
-          is_enable_message
+          is_enable_message,
+          content,
+          cs_item_begin_time,
+          cs_item_end_time
         }) => {
           this.itemInfo = {
+            name,
             time_space_view_txt,
             course_name,
             cs_item_name,
             cs_item_learning_material_list,
-            courseware_list
+            courseware_list,
+            content,
+            cs_item_time: `${cs_item_begin_time} ~ ${cs_item_end_time}`
           };
           this.name = name;
           this.teaching_type = teaching_type;

+ 1 - 0
src/views/teacher/create_course/index.vue

@@ -31,6 +31,7 @@
 import SelectTemplate from '@/components/select/SelectTemplate.vue';
 
 export default {
+  name: 'CreateCourse',
   components: { SelectTemplate },
   data() {
     return {

+ 18 - 4
src/views/teacher/create_course/step_table/CourseInfo.vue

@@ -59,8 +59,8 @@
           <el-date-picker v-model="form.end_date" type="date" value-format="yyyy-MM-dd" />
         </el-form-item>
 
-        <el-form-item :label="$t('Key254')">
-          <el-input v-model="form.student_count_max" class="student-count" @input="changeStudent" />
+        <el-form-item :label="$t('Key254')" prop="student_count_max">
+          <el-input v-model="form.student_count_max" class="student-count" maxlength="3" @input="changeStudent" />
           <el-checkbox v-model="form.is_auto_close">
             {{ $t('Key266') }}
           </el-checkbox>
@@ -139,6 +139,14 @@ export default {
         callback();
       }
     };
+    const validateCountMax = (rule, value, callback) => {
+      if (value < 1 || value > 999) {
+        callback(new Error('学生数量最小为1,最大为999'));
+      } else {
+        callback();
+      }
+    };
+
     const query = this.$route.query;
 
     return {
@@ -155,7 +163,7 @@ export default {
         teacher_id_list: [],
         begin_date: '',
         end_date: '',
-        student_count_max: '',
+        student_count_max: 1,
         is_auto_close: false,
         student_enter_control_type: 0,
         price: '0.00',
@@ -163,7 +171,8 @@ export default {
         is_enable_XYZP: false
       },
       formRules: {
-        name: { trigger: 'blur', validator: validateName }
+        name: { trigger: 'blur', validator: validateName },
+        student_count_max: { trigger: 'blur', validator: validateCountMax }
       },
       orgList: [],
       user_list: [],
@@ -180,6 +189,11 @@ export default {
     if (this.is_use_template) {
       this.getCourseInfo_ContainCSItem(this.template_id);
     }
+    // 创建课程时,默认授课教师是自己
+    if (!this.id && !this.is_template) {
+      this.form.teacher_id_list.push(this.$store.state.user.user_code);
+      this.getUserList();
+    }
   },
   methods: {
     nextStep() {

+ 6 - 8
src/views/teacher/create_course/step_table/SelectBook.vue

@@ -30,7 +30,7 @@
             <div v-for="item in book_list" :key="item.id" class="book-list-item">
               <div
                 :class="['book-list-item-img', { selected: item.is_selected === 'true' }]"
-                @click="addOrRemoveBookToCourse($event, item.id)"
+                @click="addOrRemoveBookToCourse(item.id)"
               >
                 <el-image fit="contain" :src="item.picture_url" />
               </div>
@@ -105,23 +105,21 @@ export default {
       this.cur_page = newPage;
       this.queryBookList();
     },
-    addOrRemoveBookToCourse(e, book_id) {
-      const classList = e.target.parentElement.parentElement.classList;
-
+    addOrRemoveBookToCourse(book_id) {
       const data = {
         course_id: this.id,
         book_id
       };
-
-      if (classList.contains('selected')) {
+      const bookIndex = this.book_list.findIndex(({ id }) => id === book_id);
+      if (this.book_list[bookIndex].is_selected === 'true') {
         RemoveBookFromCourse(data).then(() => {
           this.$message.success(this.$i18n.t('Key350'));
-          classList.remove('selected');
+          this.book_list[bookIndex].is_selected = 'false';
         });
       } else {
         AddBookToCourse(data).then(() => {
           this.$message.success(this.$i18n.t('Key351'));
-          classList.add('selected');
+          this.book_list[bookIndex].is_selected = 'true';
         });
       }
     },

+ 396 - 0
src/views/template_details/index.vue

@@ -0,0 +1,396 @@
+<template>
+  <div class="template_details">
+    <div
+      v-if="isData && CourseData.is_release === 'true' && CourseData.is_deleted === 'false'"
+      v-loading="loading"
+      class="details_container"
+    >
+      <header class="title">{{ CourseData.name }}</header>
+      <main class="main">
+        <div v-for="(item, i) in CourseData.cs_item_list" :key="i" class="course-list">
+          <div class="courseOne">
+            <div class="title">
+              <div>
+                <span>{{ i + 1 }}.</span>
+                <span>{{ item.name }}</span>
+              </div>
+              <div class="courseOne-time">{{ item.begin_time }} ~ {{ item.end_time }}</div>
+            </div>
+          </div>
+          <div class="course-content">
+            <el-collapse @change="handleChange(item.id)">
+              <el-collapse-item :name="item.id">
+                <div slot="title" class="Coursetasks">
+                  {{ $t('Key392') }}
+                  <img
+                    class="arrow"
+                    :src="
+                      openList.includes(item.id)
+                        ? require('../../assets/course_details/open1.png')
+                        : require('../../assets/course_details/open2.png')
+                    "
+                  />
+                </div>
+                <div class="courseContent">
+                  <template v-for="li in courseContentList">
+                    <div v-if="item[li.id].length > 0" :key="li.id" class="tasks">
+                      <div class="title">
+                        <span class="red-circle" /><span>{{ $t(li.name) }}</span>
+                      </div>
+                      <div class="content">
+                        <el-collapse-item v-for="(it, it_i) in item[li.id]" :key="it_i" :name="it.id">
+                          <div slot="title" :class="openList.includes(it.id) ? 'contenttitle2' : 'contenttitle'">
+                            <span :title="it.name">{{ it.name }}</span>
+                            <!-- <span class="content-image">
+                              <el-image :src="require('@/assets/course_details/people.png')" />
+                            </span> -->
+                            <span>
+                              <span class="gray">{{ it.begin_time }} ~ {{ it.end_time }}</span>
+                              <span class="gray">···</span>
+                            </span>
+                          </div>
+                          <div class="detail">
+                            <div>
+                              <span>{{ $t('Key393') }}</span>
+                              <p>
+                                {{ it.content }}
+                              </p>
+                            </div>
+                            <div v-if="it.courseware_list.length > 0">
+                              <span>{{ $t('Key309') }}</span>
+                              <div>
+                                <div
+                                  v-for="(courseware, courseware_i) in it.courseware_list"
+                                  :key="courseware_i"
+                                  class="btn"
+                                >
+                                  <img src="../../assets/course_details/file.png" alt="" />
+                                  <span> {{ courseware.courseware_name }} </span>
+                                </div>
+                              </div>
+                            </div>
+                            <div v-if="it.accessory_list.length > 0">
+                              <span>{{ $t('Key394') }}</span>
+                              <div>
+                                <div
+                                  v-for="(accessory, accessory_i) in it.accessory_list"
+                                  :key="accessory_i"
+                                  class="btn"
+                                >
+                                  <img src="../../assets/course_details/fileType1.png" alt="" />
+                                  <span> {{ accessory.file_name }} </span>
+                                </div>
+                              </div>
+                            </div>
+                          </div>
+                        </el-collapse-item>
+                      </div>
+                    </div>
+                  </template>
+                </div>
+              </el-collapse-item>
+            </el-collapse>
+          </div>
+        </div>
+      </main>
+    </div>
+
+    <div v-else-if="!loading" class="non-existent">
+      {{
+        CourseData.is_deleted === 'true'
+          ? '无法查看,课程已删除'
+          : CourseData.is_release === 'false'
+          ? '无法查看,课程已下架'
+          : '课程不存在'
+      }}
+    </div>
+  </div>
+</template>
+
+<script>
+import { GetCourseInfoBox } from '@/api/course';
+
+export default {
+  data() {
+    const query = this.$route.query;
+
+    return {
+      id: query.id,
+      CourseData: null,
+      isData: false,
+      loading: true,
+      openList: [],
+      courseContentList: [
+        { id: 'pre_task_list', name: 'Key353' },
+        { id: 'mid_task_list', name: 'Key354' },
+        { id: 'after_task_list', name: 'Key355' }
+      ]
+    };
+  },
+  created() {
+    GetCourseInfoBox({ id: this.id })
+      .then(res => {
+        this.CourseData = res;
+        this.isData = 'id' in res;
+        this.loading = false;
+      })
+      .catch(() => {
+        this.loading = false;
+      });
+  },
+  methods: {
+    // 课程任务的打开和关闭
+    handleChange(val) {
+      this.openList.includes(val) ? this.openList.splice(this.openList.indexOf(val), 1) : this.openList.push(val);
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.template_details {
+  min-height: 100%;
+  padding: 52px 0 20px;
+  background: #e5e5e5;
+
+  .details_container {
+    display: flex;
+    flex-direction: column;
+    row-gap: 24px;
+    width: 1200px;
+    min-height: calc(100vh - 168px);
+    margin: 0 auto;
+  }
+
+  header.title {
+    font-size: 20px;
+    font-weight: 700;
+
+    &::before {
+      display: inline-block;
+      width: 4px;
+      height: 13px;
+      margin-right: 16px;
+      content: '';
+      background-color: $basic-color;
+    }
+  }
+
+  .main {
+    flex: 1;
+    width: 100%;
+    padding: 40px;
+    background-color: #fff;
+    border-radius: 8px;
+
+    .course-list {
+      font-size: 18px;
+      background: #fff;
+
+      & + .course-list {
+        margin-top: 30px;
+      }
+
+      > :not(.courseOne) {
+        padding-left: 40px;
+        margin-top: 16px;
+      }
+
+      .courseOne {
+        .title {
+          display: flex;
+          justify-content: space-between;
+
+          > div:nth-child(1) {
+            font-size: 20px;
+            font-weight: bold;
+
+            span {
+              margin-right: 24px;
+            }
+          }
+        }
+
+        &-time {
+          color: #737373;
+        }
+      }
+
+      .course-content {
+        width: 670px;
+      }
+
+      .Coursetasks {
+        display: flex;
+        align-items: center;
+        font-size: 16px;
+        cursor: pointer;
+
+        .arrow {
+          width: 24px;
+          margin-left: 8px;
+        }
+      }
+
+      .courseContent {
+        .tasks {
+          padding-left: 10px;
+
+          .title {
+            font-size: 16px;
+            color: #2c2c2c;
+
+            .red-circle {
+              position: relative;
+              right: 3px;
+              display: inline-block;
+              width: 8px;
+              height: 8px;
+              margin-right: 16px;
+              background: #fe6d68;
+              border-radius: 50%;
+            }
+          }
+
+          .content {
+            padding-bottom: 10px;
+            padding-left: 20px;
+            margin-top: 16px;
+            border-left: 1px solid #fe6d68;
+
+            %contenttitle,
+            .contenttitle {
+              display: flex;
+              align-items: center;
+              justify-content: space-between;
+              width: 670px;
+              padding: 0 10px;
+              font-size: 16px;
+              cursor: pointer;
+              background: #fff;
+              border: 1px solid #e6e6e6;
+              border-radius: 8px;
+
+              img {
+                width: 16px;
+                margin-left: 8px;
+              }
+
+              .content-image {
+                .el-image {
+                  width: 16px;
+                  height: 16px;
+                  vertical-align: middle;
+                }
+              }
+
+              span {
+                margin-left: 16px;
+              }
+
+              > span:first-child {
+                width: 180px;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+              }
+
+              .gray {
+                opacity: 0.5;
+              }
+            }
+
+            .contenttitle2 {
+              @extend %contenttitle;
+
+              background: #f9f9f9;
+              border-bottom-right-radius: 0;
+              border-bottom-left-radius: 0;
+            }
+
+            .detail {
+              width: 100%;
+              padding-bottom: 24px;
+              background: #fff;
+              border: 1px solid #e7e7e7;
+              border-bottom-right-radius: 8px;
+              border-bottom-left-radius: 8px;
+              box-shadow: 0 2px 8px rgba(0, 0, 0, 10%);
+
+              > div {
+                display: flex;
+                align-items: center;
+                margin-top: 24px;
+
+                > :nth-child(1) {
+                  width: 78px;
+                  min-width: 78px;
+                  margin-left: 24px;
+                  font-size: 16px;
+                  color: #000;
+                }
+
+                > :nth-child(2) {
+                  margin-left: 16px;
+
+                  img {
+                    width: 24px;
+                  }
+
+                  .btn {
+                    display: flex;
+                    align-items: center;
+                    width: 270px;
+                    height: 40px;
+                    cursor: pointer;
+                    background: #fff;
+                    border: 1px solid #dbdbdb;
+                    border-radius: 4px;
+
+                    img {
+                      margin-right: 8px;
+                      margin-left: 16px;
+                    }
+                  }
+
+                  .btn:not(:nth-child(1)) {
+                    margin-top: 8px;
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+.el-collapse {
+  width: 670px;
+  border-style: none;
+
+  &-item + &-item {
+    margin-top: 12px;
+  }
+
+  .el-collapse-item__header {
+    border-style: none;
+
+    .l-collapse-item__arrow,
+    .el-icon-arrow-right {
+      display: none;
+    }
+  }
+
+  .el-collapse-item__wrap {
+    border-style: none;
+  }
+
+  .el-collapse-item__content {
+    padding-bottom: 0;
+  }
+}
+</style>

+ 2 - 1
stylelint.config.js

@@ -47,6 +47,7 @@ module.exports = {
     // 为类选择器指定一个模式
     'selector-class-pattern': null,
     'declaration-colon-newline-after': null,
-    'value-keyword-case': ['lower', { camelCaseSvgKeywords: true }]
+    'value-keyword-case': ['lower', { camelCaseSvgKeywords: true }],
+    'value-no-vendor-prefix': [true, { ignoreValues: ['box'] }]
   }
 };

Vissa filer visades inte eftersom för många filer har ändrats