dedsudiyu 1 week ago
parent
commit
85514b9007
4 changed files with 560 additions and 5 deletions
  1. 4 4
      .env.development
  2. 25 0
      src/api/index.js
  3. 8 1
      src/views/home.vue
  4. 523 0
      src/views/learning/learningMaterials.vue

+ 4 - 4
.env.development

@@ -36,13 +36,13 @@ VUE_APP_VERSION_DIFFERENCE_FIELD = 'kuangYeDaXue_nanHu'
 # VUE_APP_BASE_API = 'lab.zjznai.com/zhlabSystem'
 # VUE_APP_BASE_API = 'lab.zjznai.com/zhlabSystem'
 
 
 # 西农
 # 西农
-VUE_APP_BASE_API = 'labcontrol.nwafu.edu.cn/api'
+# VUE_APP_BASE_API = 'labcontrol.nwafu.edu.cn/api'
 
 
 # 山西农大
 # 山西农大
 # VUE_APP_BASE_API = 'lasfl.sxau.edu.cn/api'
 # VUE_APP_BASE_API = 'lasfl.sxau.edu.cn/api'
 
 
 # 1.8外网地址
 # 1.8外网地址
-# VUE_APP_BASE_API = 'lab.zjznai.com/labTest'
+VUE_APP_BASE_API = 'lab.zjznai.com/labTest'
 # VUE_APP_BASE_API = '192.168.1.8/api'
 # VUE_APP_BASE_API = '192.168.1.8/api'
 
 
 # 1.88外网地址
 # 1.88外网地址
@@ -70,13 +70,13 @@ VUE_APP_BASE_API = 'labcontrol.nwafu.edu.cn/api'
 # VUE_APP_BASE_LOCAL_API = '192.168.1.12/api'
 # VUE_APP_BASE_LOCAL_API = '192.168.1.12/api'
 
 
 # 西农
 # 西农
-VUE_APP_BASE_LOCAL_API = '172.16.0.65/api'
+# VUE_APP_BASE_LOCAL_API = '172.16.0.65/api'
 
 
 # 山西农大
 # 山西农大
 # VUE_APP_BASE_LOCAL_API = 'lasfl.sxau.edu.cn/api'
 # VUE_APP_BASE_LOCAL_API = 'lasfl.sxau.edu.cn/api'
 
 
 # 1.8内网地址
 # 1.8内网地址
-# VUE_APP_BASE_LOCAL_API = '192.168.1.8/api'
+VUE_APP_BASE_LOCAL_API = '192.168.1.8/api'
 
 
 # 1.88内网地址
 # 1.88内网地址
 # VUE_APP_BASE_LOCAL_API = '192.168.1.88/labSystem'
 # VUE_APP_BASE_LOCAL_API = '192.168.1.88/labSystem'

+ 25 - 0
src/api/index.js

@@ -125,3 +125,28 @@ export function systemFileUpload(data) {
     data: data
     data: data
   })
   })
 }
 }
+
+//根据课件ID查询课件信息
+export function examUserCoursewareLearningDetail(query) {
+  return request({
+    url: '/exam/user/courseware/learning/detail',
+    method: 'get',
+    params: query
+  })
+}
+//上报课件学习进度
+export function examUserCoursewareLearningReportProgress(data) {
+  return request({
+    url: '/exam/user/courseware/learning/reportProgress',
+    method: 'post',
+    data: data
+  })
+}
+//课件学习完成确认
+export function examUserCoursewareLearningConfirmCompletion(data) {
+  return request({
+    url: '/exam/user/courseware/learning/confirmCompletion',
+    method: 'post',
+    data: data
+  })
+}

+ 8 - 1
src/views/home.vue

@@ -9,6 +9,9 @@
       <miniProgramVideoHls v-if="pageType == 'miniProgramVideoHls'"></miniProgramVideoHls>
       <miniProgramVideoHls v-if="pageType == 'miniProgramVideoHls'"></miniProgramVideoHls>
       <chemicalsInstructionsVideo v-if="pageType == 'chemicalsInstructionsVideo'"></chemicalsInstructionsVideo>
       <chemicalsInstructionsVideo v-if="pageType == 'chemicalsInstructionsVideo'"></chemicalsInstructionsVideo>
       <alarmPage v-if="pageType == 'alarm'"></alarmPage>
       <alarmPage v-if="pageType == 'alarm'"></alarmPage>
+      <!--课程学习-->
+      <!--课件学习-->
+      <learningMaterials v-if="pageType == 'learningMaterials'"></learningMaterials>
     </div>
     </div>
 </template>
 </template>
 <script>
 <script>
@@ -20,6 +23,7 @@
     import miniProgramVideoHls from '@/views/miniProgramVideoHls/index.vue';
     import miniProgramVideoHls from '@/views/miniProgramVideoHls/index.vue';
     import chemicalsInstructionsVideo from '@/views/chemicalsInstructionsVideo/index.vue';
     import chemicalsInstructionsVideo from '@/views/chemicalsInstructionsVideo/index.vue';
     import alarmPage from '@/views/alarmPage/index.vue';
     import alarmPage from '@/views/alarmPage/index.vue';
+    import learningMaterials from '@/views/learning/learningMaterials.vue';
     import {
     import {
       getConfigByType,
       getConfigByType,
     } from '@/api/index.js'
     } from '@/api/index.js'
@@ -35,6 +39,7 @@
           miniProgramVideoHls,
           miniProgramVideoHls,
           chemicalsInstructionsVideo,
           chemicalsInstructionsVideo,
           alarmPage,
           alarmPage,
+          learningMaterials,
         },
         },
         data() {
         data() {
             return {
             return {
@@ -54,7 +59,9 @@
               this.getConfigByType(),
               this.getConfigByType(),
             ]).then((result) => {
             ]).then((result) => {
               //页面展示
               //页面展示
-              if (text.indexOf('videoBroadcastHk') != -1){
+              if(text.indexOf('learningMaterials') != -1){
+                this.$set(this,'pageType','learningMaterials');
+              }else if (text.indexOf('videoBroadcastHk') != -1){
                 this.$set(this,'pageType','videoBroadcastHk');
                 this.$set(this,'pageType','videoBroadcastHk');
               }else if(text.indexOf('videoBroadcastHls') != -1){
               }else if(text.indexOf('videoBroadcastHls') != -1){
                 this.$set(this,'pageType','videoBroadcastHls');
                 this.$set(this,'pageType','videoBroadcastHls');

+ 523 - 0
src/views/learning/learningMaterials.vue

@@ -0,0 +1,523 @@
+<!-- 学习课件 - 小程序webview版 -->
+<template>
+  <div class="learningMaterials">
+    <!-- 顶部导航栏 -->
+    <div class="nav-bar">
+      <p class="nav-title">{{ newData.coursewareName }}</p>
+    </div>
+
+    <!-- 课件展示区 -->
+    <div class="courseware-box">
+      <!-- 视频 -->
+      <video
+        v-if="officeType === 'video'"
+        class="video-player"
+        :src="lookUrl"
+        controls
+        playsinline
+        webkit-playsinline
+        x5-playsinline
+      ></video>
+      <!-- 富文本/文章 -->
+      <div v-else-if="officeType === 'text'" class="rich-text-box" v-html="lookUrl"></div>
+      <!-- 文档(docx/excel/pdf) -->
+      <div v-else-if="officeType === 'docx' || officeType === 'excel' || officeType === 'pdf'" class="office-box">
+        <vue-office-docx
+          v-if="officeType === 'docx' && !officeNullType"
+          :src="lookUrl"
+          @rendered="renderedHandler"
+          @error="errorHandler"
+        />
+        <vue-office-excel
+          v-if="officeType === 'excel' && !officeNullType"
+          :src="lookUrl"
+          @rendered="renderedHandler"
+          @error="errorHandler"
+        />
+        <vue-office-pdf
+          v-if="officeType === 'pdf' && !officeNullType"
+          :src="lookUrl"
+          @rendered="renderedHandler"
+          @error="errorHandler"
+        />
+        <p v-if="officeNullType" class="load-fail-text">加载失败...</p>
+      </div>
+      <!-- 空态 -->
+      <div v-else class="empty-box">
+        <p>课件加载中...</p>
+      </div>
+    </div>
+
+    <!-- 底部学习信息 -->
+    <div class="info-box">
+      <div class="info-meta">
+        <span>{{ secondsToMinutes(newData.minLearningDuration) }}分钟</span>
+        <span class="dot">·</span>
+        <span>{{ newData.creditHours }}学时</span>
+        <span class="dot">·</span>
+        <span>{{ newData.points }}积分</span>
+      </div>
+      <!--<div class="progress-row">-->
+        <!--<span class="progress-label">学习进度</span>-->
+        <!--<div class="progress-bar-wrap">-->
+          <!--<div class="progress-bar-track">-->
+            <!--<div class="progress-bar-fill" :style="{ width: (newData.progress || 0) + '%' }"></div>-->
+          <!--</div>-->
+          <!--<span class="progress-pct">{{ newData.progress || 0 }}%</span>-->
+        <!--</div>-->
+      <!--</div>-->
+      <div class="time-row">
+        <span class="time-text">
+          已学:{{ formatTime(currentLearnedDuration) }} / {{ formatTime(newData.minLearningDuration) }}
+        </span>
+        <span
+          :class="['status-tag',
+            learnStatus === 2 ? 'status-done' :
+            learnStatus === 1 ? 'status-ing' : 'status-none'
+          ]"
+        >
+          {{ learnStatus === 2 ? '已学完' : (learnStatus === 1 ? '学习中' : '未学完') }}
+        </span>
+      </div>
+      <!-- 确认完成按钮 -->
+      <div
+        class="confirm-btn"
+        v-if="learnStatus !== 2 && currentLearnedDuration >= newData.minLearningDuration && newData.minLearningDuration > 0"
+        @click="finishStudying"
+      >
+        若已完成学习,请点此确认
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+  import {judgmentNetworkReturnAddress} from "@/utils/ruoyi";
+  import VueOfficeDocx from '@vue-office/docx'
+  import '@vue-office/docx/lib/index.css'
+  import VueOfficeExcel from '@vue-office/excel'
+  import '@vue-office/excel/lib/index.css'
+  import VueOfficePdf from '@vue-office/pdf'
+  import { Toast, Dialog } from 'vant'
+  import {
+    examUserCoursewareLearningDetail,
+    examUserCoursewareLearningReportProgress,
+    examUserCoursewareLearningConfirmCompletion,
+  } from '@/api/index.js'
+
+  export default {
+    name: 'learningMaterials',
+    props:{
+      learningMaterialsData:{},
+    },
+    components: {
+      VueOfficeDocx,
+      VueOfficeExcel,
+      VueOfficePdf,
+    },
+    data() {
+      return {
+        coursewareId: null,
+        newData: {
+          coursewareName: '',
+          minLearningDuration: 0,
+          creditHours: 0,
+          points: 0,
+          progress: 0,
+          attachmentPath: '',
+          attachmentType: '',
+          materialType: null,
+          articleContent: '',
+          learnedDuration: 0,
+          learnStatus: 0,
+        },
+        // 课件展示
+        officeType: null,
+        lookUrl: '',
+        officeNullType: false,
+        // 学习状态
+        learnStatus: 0,
+        currentLearnedDuration: 0,
+        // 定时器
+        studyTimer: null,
+        heartbeatValue: 30,
+      }
+    },
+    mounted() {
+      document.title = "";
+      this.initialization()
+    },
+    methods: {
+      // 初始化
+      initialization() {
+        // 从 URL 参数获取 coursewareId(小程序 webview 传参方式)
+        let text = decodeURIComponent(window.location.href)
+        let search = text.split('?')[1] || ''
+        let params = {}
+        search.split('&').forEach(item => {
+          let kv = item.split('=')
+          if (kv[0]) params[kv[0]] = kv[1]
+        })
+        this.$set(this, 'coursewareId', params.coursewareId)
+        this.$nextTick(() => {
+          this.loadDetail()
+        })
+      },
+      // 获取课件详情
+      loadDetail() {
+        examUserCoursewareLearningDetail({ coursewareId: this.coursewareId }).then(response => {
+          let data = response.data
+          this.$set(this, 'newData', data)
+          this.$set(this, 'learnStatus', data.learnStatus)
+          this.$set(this, 'currentLearnedDuration', data.learnedDuration || 0)
+          this.renderCourseware(data)
+          this.initTimer()
+        })
+      },
+      // 课件渲染
+      renderCourseware(data) {
+        this.$set(this, 'officeNullType', false)
+        if (data.materialType === 1) {
+          this.$set(this, 'officeType', 'video')
+          this.$set(this, 'lookUrl', localStorage.getItem('fileBrowseEnvironment')+'/'+data.attachmentPath)
+        } else if (data.materialType === 3) {
+          let ext = data.attachmentType
+          if (ext === '.docx') {
+            this.$set(this, 'officeType', 'docx')
+          } else if (ext === '.xlsx') {
+            this.$set(this, 'officeType', 'excel')
+          } else if (ext === '.pdf') {
+            this.$set(this, 'officeType', 'pdf')
+          }
+          this.$set(this, 'lookUrl', localStorage.getItem('fileBrowseEnvironment')+'/'+data.attachmentPath)
+        } else if (data.materialType === 4) {
+          this.$set(this, 'officeType', 'text')
+          this.$set(this, 'lookUrl', localStorage.getItem('fileBrowseEnvironment')+'/'+data.articleContent)
+        }
+        // 更新学习状态为"学习中"
+        if (this.learnStatus === 0) {
+          this.$set(this, 'learnStatus', 1)
+        }
+      },
+      renderedHandler() {},
+      errorHandler() {
+        this.$set(this, 'officeNullType', true)
+      },
+      // 返回 - 小程序 navigateBack
+      backPage() {
+        if (window.wx && window.wx.miniProgram) {
+          window.wx.miniProgram.navigateBack({ delta: 1 })
+        } else {
+          this.$router.back()
+        }
+      },
+      // 点击确认完成
+      finishStudying() {
+        Dialog.confirm({
+          title: '提示',
+          message: '确认完成课件学习?',
+          confirmButtonText: '确认',
+          cancelButtonText: '取消',
+        }).then(() => {
+          this.confirmCompletion()
+        }).catch(() => {})
+      },
+      // 确认完成接口
+      confirmCompletion() {
+        examUserCoursewareLearningConfirmCompletion({
+          coursewareId: this.coursewareId,
+          courseId: 0,
+          examId: 0,
+        }).then(response => {
+          this.$set(this, 'learnStatus', 2)
+          this.$set(this.newData, 'progress', 100)
+          Toast(response.message || '学习完成')
+        })
+      },
+      // 心跳上报
+      reportProgress() {
+        examUserCoursewareLearningReportProgress({
+          coursewareId: this.coursewareId,
+          courseId: 0,
+          examId: 0,
+          duration: this.heartbeatValue,
+        }).then(() => {})
+      },
+      // 初始化定时器
+      initTimer() {
+        if (this.newData.learnedDuration >= this.newData.minLearningDuration) return
+        let self = this
+        let initialSeconds = this.newData.learnedDuration || 0
+        let maxSeconds = this.newData.minLearningDuration
+        let elapsedTime = initialSeconds
+        let isRunning = false
+        let isFinished = false
+        let timerId = null
+        let startTimeStamp = 0
+
+        const handleVisibilityChange = () => {
+          if (isFinished) return
+          if (document.hidden) {
+            pause()
+          } else {
+            resume()
+          }
+        }
+        document.addEventListener('visibilitychange', handleVisibilityChange)
+
+        const resume = () => {
+          if (isRunning || isFinished) return
+          isRunning = true
+          startTimeStamp = Date.now() - elapsedTime * 1000
+          timerId = setInterval(() => {
+            let now = Date.now()
+            let rawElapsed = (now - startTimeStamp) / 1000
+            if (rawElapsed >= maxSeconds) {
+              elapsedTime = maxSeconds
+              self.$set(self, 'currentLearnedDuration', elapsedTime)
+              self.reportProgress()
+              pause()
+              isFinished = true
+              document.removeEventListener('visibilitychange', handleVisibilityChange)
+            } else {
+              elapsedTime = Math.floor(rawElapsed)
+              self.$set(self, 'currentLearnedDuration', elapsedTime)
+              if (elapsedTime % self.heartbeatValue === 0 && elapsedTime > 0) {
+                self.reportProgress()
+              }
+            }
+          }, 1000)
+        }
+
+        const pause = () => {
+          if (!isRunning) return
+          isRunning = false
+          clearInterval(timerId)
+          timerId = null
+        }
+
+        this.studyTimer = { pause, resume, destroy: () => {
+          pause()
+          document.removeEventListener('visibilitychange', handleVisibilityChange)
+        }}
+
+        resume()
+      },
+      // 时间格式化
+      formatTime(totalSeconds) {
+        if (typeof totalSeconds !== 'number' || totalSeconds <= 0) return '0秒'
+        let minutes = Math.floor(totalSeconds / 60)
+        let seconds = totalSeconds % 60
+        if (minutes > 0 && seconds > 0) return `${minutes}分${seconds}秒`
+        if (minutes > 0) return `${minutes}分`
+        return `${seconds}秒`
+      },
+      secondsToMinutes(totalSeconds) {
+        if (typeof totalSeconds !== 'number' || totalSeconds <= 0) return 1
+        return Math.ceil(totalSeconds / 60)
+      },
+    },
+    beforeDestroy() {
+      if (this.studyTimer) {
+        this.studyTimer.destroy()
+        this.studyTimer = null
+      }
+    },
+  }
+</script>
+<style scoped lang="scss">
+  .learningMaterials {
+    display: flex;
+    flex-direction: column;
+    height: 100vh;
+    background: #f5f5f5;
+    overflow: hidden;
+
+    .nav-bar {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      height: 44px;
+      padding: 0 12px;
+      background: #fff;
+      border-bottom: 1px solid #eee;
+      flex-shrink: 0;
+      .nav-back {
+        display: flex;
+        align-items: center;
+        width: 60px;
+        font-size: 14px;
+        color: #0183fa;
+        .back-arrow {
+          font-size: 24px;
+          line-height: 1;
+          margin-right: 2px;
+        }
+      }
+      .nav-title {
+        flex: 1;
+        text-align: center;
+        font-size: 24px;
+        font-weight: 700;
+        color: #333;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        padding: 0 8px;
+      }
+    }
+
+    .courseware-box {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+      background: #000;
+
+      .video-player {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+        background: #000;
+      }
+
+      .rich-text-box {
+        flex: 1;
+        overflow-y: auto;
+        padding: 16px;
+        background: #fff;
+        font-size: 14px;
+        line-height: 1.7;
+        word-break: break-word;
+        ::v-deep img {
+          max-width: 100% !important;
+          height: auto !important;
+          display: block;
+          margin: 8px auto;
+        }
+        ::v-deep table {
+          width: 100%;
+          border-collapse: collapse;
+        }
+        ::v-deep p {
+          margin-bottom: 8px;
+        }
+      }
+
+      .office-box {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+        background: #fff;
+        .load-fail-text {
+          flex: 1;
+          text-align: center;
+          line-height: 200px;
+          color: #ccc;
+          font-size: 14px;
+        }
+      }
+
+      .empty-box {
+        flex: 1;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        background: #fff;
+        color: #999;
+        font-size: 14px;
+      }
+    }
+
+    .info-box {
+      background: #fff;
+      padding: 12px 16px 40px 16px;
+      flex-shrink: 0;
+      border-top: 1px solid #eee;
+
+      .info-meta {
+        font-size: 24px;
+        color: #999;
+        margin-bottom: 8px;
+        .dot {
+          margin: 0 6px;
+        }
+      }
+
+      .progress-row {
+        display: flex;
+        align-items: center;
+        margin-bottom: 8px;
+        .progress-label {
+          font-size: 22px;
+          color: #333;
+          font-weight: 700;
+          white-space: nowrap;
+          margin-right: 10px;
+        }
+        .progress-bar-wrap {
+          flex: 1;
+          display: flex;
+          align-items: center;
+          .progress-bar-track {
+            flex: 1;
+            height: 8px;
+            background: #eee;
+            border-radius: 4px;
+            overflow: hidden;
+            .progress-bar-fill {
+              height: 100%;
+              background: #0183fa;
+              border-radius: 4px;
+              transition: width 0.3s;
+            }
+          }
+          .progress-pct {
+            font-size: 24px;
+            color: #0183fa;
+            margin-left: 8px;
+            min-width: 32px;
+            text-align: right;
+          }
+        }
+      }
+
+      .time-row {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin-bottom: 8px;
+        .time-text {
+          font-size: 26px;
+          color: #666;
+        }
+        .status-tag {
+          font-size: 24px;
+          padding: 2px 8px;
+          border-radius: 4px;
+          background: #999;
+          color: #fff;
+          &.status-done {
+            background: #0183fa;
+          }
+          &.status-ing {
+            background: #fff;
+            border: 1px solid #0183fa;
+            color: #0183fa;
+          }
+        }
+      }
+
+      .confirm-btn {
+        background: #13ce66;
+        color: #fff;
+        text-align: center;
+        height: 60px;
+        line-height: 64px;
+        border-radius: 6px;
+        font-size: 28px;
+        margin-top: 10px;
+      }
+    }
+  }
+</style>