Parcourir la source

feat: 为数据看板添加PPT解析和实时数据加载功能

lsc il y a 2 jours
Parent
commit
61eb35b970

+ 101 - 30
src/components/pptEasyClass/dataBoard/teacher.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="teacher-dashboard" v-show="showDataBoard">
+  <div class="teacher-dashboard" v-show="showDataBoard" v-loading="loading">
     <!-- 顶部导航栏 -->
     <nav class="nav-bar">
       <div class="nav-container">
@@ -19,9 +19,9 @@
           >
             <el-option
               v-for="cls in classList"
-              :key="cls"
-              :label="cls"
-              :value="cls"
+              :key="cls.id"
+              :label="cls.name"
+              :value="cls.id"
             />
           </el-select>
         </div>
@@ -48,21 +48,21 @@
             <h2 class="section-heading">课程概况</h2>
           </div>
           <div class="course-info-block">
-            <h1 class="course-name">{{ courseInfo.title }}</h1>
-            <div class="course-meta-row">
-              <div class="course-meta-item">
+            <h1 class="course-name">{{ courseDetail.title }}</h1>
+            <div class="course-meta-row" v-if="grade || subject">
+              <div class="course-meta-item" v-if="grade">
                 <svg class="meta-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                   <path d="M22 10L12 5 2 10l10 5 10-5z" />
                   <path d="M6 12v5c0 1 2.5 3 6 3s6-2 6-3v-5" />
                 </svg>
-                <span class="meta-label">年级:</span>{{ courseInfo.grade }}
+                <span class="meta-label">年级:</span>{{ grade }}
               </div>
               <span class="meta-divider">|</span>
-              <div class="course-meta-item">
+              <div class="course-meta-item" v-if="subject">
                 <svg class="meta-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                   <path d="M4 19.5A2.5 2.5 0 016.5 17H20M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z" />
                 </svg>
-                <span class="meta-label">学科:</span>{{ courseInfo.subject }}
+                <span class="meta-label">学科:</span>{{ subject }}
               </div>
             </div>
             <div class="course-extra-row">
@@ -70,14 +70,14 @@
                 <svg class="extra-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                   <path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75M13 8a4 4 0 11-8 0 4 4 0 018 0z" />
                 </svg>
-                <span>班级: {{ courseInfo.class }}</span>
+                <span>班级: {{ getClassClassName }}</span>
               </div>
               <div class="course-extra-item">
                 <svg class="extra-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                   <circle cx="12" cy="12" r="10" />
                   <path d="M12 6v6l4 2" />
                 </svg>
-                <span>上课时间: {{ courseInfo.classTime }}</span>
+                <span>上课时间: {{ classstime }}</span>
               </div>
             </div>
           </div>
@@ -335,23 +335,29 @@
 <script>
 export default {
   name: 'TeacherDataBoard',
+  props: {
+    courseDetail: {
+      type: Object,
+      default: () => ({})
+    },
+    classList: {
+      type: Array,
+      default: () => []
+    },
+    pptJSON: {
+      type: Object,
+      default: () => ({})
+    },
+  },
   data() {
     return {
-      selectedClass: '测试1班',
-      classList: ['测试1班', '高一(3)班'],
+      selectedClass: '',
       searchKeyword: '',
       chartInstance: null,
-      courseInfo: {
-        title: '人工智能普及课程',
-        grade: '高中一年级',
-        subject: '信息技术',
-        class: '一班',
-        classTime: '2026-05-13 17:08'
-      },
       kpis: [
         { value: '90%', suffix: '', label: '出勤率', sub: '(45/50人)', color: '#F97316' },
         { value: '89%', suffix: '', label: '平均提交率', sub: '(313次提交)', color: '#10B981' },
-        { value: '64%', suffix: '', label: '平均正确率', sub: '(最高94%·最低25%)', color: '#10B981' },
+        // { value: '64%', suffix: '', label: '平均正确率', sub: '(最高94%·最低25%)', color: '#10B981' },
         { value: '4', suffix: '轮', label: 'AI 轮次', sub: '(最多7·最少0)', color: '#3B82F6' },
         { value: '47', suffix: '', label: '总互动量', sub: '(总点赞数)', color: '#EC4899' }
       ],
@@ -435,18 +441,24 @@ export default {
         }
       ],
       students: [
-        { id: 1, name: '张伟', attention: false, tasksDone: 7, tasksTotal: 7, correctRate: '87%', aiRounds: 6, likes: '5/3' },
-        { id: 2, name: '李子涵', attention: false, tasksDone: 7, tasksTotal: 7, correctRate: '92%', aiRounds: 7, likes: '8/5' },
-        { id: 3, name: '王芳', attention: false, tasksDone: 7, tasksTotal: 7, correctRate: '86%', aiRounds: 5, likes: '4/2' },
-        { id: 4, name: '陈嘉怡', attention: false, tasksDone: 7, tasksTotal: 7, correctRate: '91%', aiRounds: 5, likes: '6/4' },
-        { id: 5, name: '杨浩宇', attention: true, tasksDone: 7, tasksTotal: 7, correctRate: '76%', aiRounds: 7, likes: '2/1' }
+        // { id: 1, name: '张伟', attention: false, tasksDone: 7, tasksTotal: 7, correctRate: '87%', aiRounds: 6, likes: '5/3' },
+        // { id: 2, name: '李子涵', attention: false, tasksDone: 7, tasksTotal: 7, correctRate: '92%', aiRounds: 7, likes: '8/5' },
+        // { id: 3, name: '王芳', attention: false, tasksDone: 7, tasksTotal: 7, correctRate: '86%', aiRounds: 5, likes: '4/2' },
+        // { id: 4, name: '陈嘉怡', attention: false, tasksDone: 7, tasksTotal: 7, correctRate: '91%', aiRounds: 5, likes: '6/4' },
+        // { id: 5, name: '杨浩宇', attention: true, tasksDone: 7, tasksTotal: 7, correctRate: '76%', aiRounds: 7, likes: '2/1' }
       ],
       chartData: {
         labels: ['高频参与', '中等频率', '极少参与'],
         values: [65, 25, 10],
         colors: ['#10B981', '#60A5FA', '#F43F5E']
       },
-      showDataBoard: false
+      showDataBoard: false,
+      classstime: '',
+      loading: false,
+      grade: '',
+      subject: '',
+      works: [],
+      likesArray: []
     };
   },
   computed: {
@@ -454,6 +466,10 @@ export default {
       if (!this.searchKeyword) return this.students;
       const kw = this.searchKeyword.trim().toLowerCase();
       return this.students.filter(s => s.name.toLowerCase().includes(kw));
+    },
+    getClassClassName() {
+      let name = this.classList.length ? this.classList.filter(item => item.id == this.selectedClass)[0].name || '' : '';
+      return name;  
     }
   },
   mounted() {
@@ -531,11 +547,13 @@ export default {
     handleResize() {
       if (this.chartInstance) this.chartInstance.resize();
     },
-    open(){
-
+    open(tcid2){
+      this.selectedClass = tcid2;
       this.showDataBoard = true;
+      console.log(this.selectedClass);
       this.$nextTick(() => {
         this.initChart();
+        this.getDataBoard();
       });
     },
     handleBack() {
@@ -552,6 +570,59 @@ export default {
     },
     handleStudentReportRow(row) {
       this.$emit('student-row-report', row);
+    },
+    getDataBoard() {
+      this.loading = true;
+      const params = {
+        cid: this.courseDetail.courseId,
+        classid: this.selectedClass
+      }
+      this.ajax.get(this.$store.state.api + "getPPTDataBoard", params).then(res => {
+        this.classstime = res.data[0][0].time;
+        this.grade = res.data[1].map(item => item.name).join('、');
+        this.subject = res.data[2].map(item => item.name).join('、');  
+        this.students = res.data[3];
+        this.works = res.data[4];
+        this.likesArray = res.data[5];
+        // 出勤率计算
+        // 计算works中不重复的userid数量,再除以学生总数得到出勤率
+        const uniqueUserIds = new Set(this.works.map(work => work.userid));
+        const attendanceCount = uniqueUserIds.size;
+        const totalStudents = this.students.length || 1;
+        const attendanceRate = attendanceCount / totalStudents;
+        this.kpis[0].value = Math.round(attendanceRate * 100) + '%';
+        this.kpis[0].sub = `(${attendanceCount}/${this.students.length}人)`;
+
+        // 平均提交率
+        let pptSlides = this.pptJSON.slides || [];
+        let tools = 0;
+        const notTool = [74, 75, 76, 82];
+        for (let i = 0; i < pptSlides.length; i++) {
+          const hasSubmissionTool = pptSlides[i].elements.some((element) => {
+            return element.type === 'frame' && !notTool.includes(element.toolType);
+          });
+          if (hasSubmissionTool) {
+            tools++;
+          }
+        }
+        // 平均提交率 = 作业总数 / (工具数量 × 参与学生数量)
+        const totalExpectedSubmissions = tools * attendanceCount;
+        const totalWorks = this.works.length;
+        const submitRate = totalExpectedSubmissions > 0 ? Math.round((totalWorks / totalExpectedSubmissions) * 100) : 0;
+        this.kpis[1].value = submitRate + '%';
+        this.kpis[1].sub = `(${totalWorks}次提交)`;
+        
+        // AI 轮次
+
+        // 总点赞数
+        const totalLikes = this.likesArray.length;
+        this.kpis[3].value = totalLikes;
+        this.kpis[3].sub = `(${totalLikes}人点赞)`;
+        
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      })
     }
   }
 };

+ 79 - 2
src/components/pptEasyClass/index.vue

@@ -129,7 +129,7 @@
     <shareDialog ref="shareDialogRef"></shareDialog>
     
     <!-- 数据看板 -->
-    <dataBoard ref="dataBoardRef"></dataBoard>
+    <dataBoard ref="dataBoardRef" :courseDetail="courseDetail" :classList="classList" :pptJSON="pptJSON"></dataBoard>
   </div>
 </template>
 
@@ -141,6 +141,56 @@ import messageInstruction from '../components/messageInstruction.vue';
 import confirmInstruction from '../components/confirmInstruction.vue';
 import shareDialog from './shareDialog.vue';
 import dataBoard from './dataBoard/teacher.vue';
+
+const getFile = (url) => {
+  return new Promise((resolve, reject) => {
+    var credentials = {
+        accessKeyId: "AKIATLPEDU37QV5CHLMH",
+        secretAccessKey: "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",
+      }; //秘钥形式的登录上传
+      window.AWS.config.update(credentials);
+      window.AWS.config.region = "cn-northwest-1"; //设置区域
+      let url2 = url;
+      let _url2 = "";
+      if (
+        url2.indexOf("https://view.officeapps.live.com/op/view.aspx?src=") != -1
+      ) {
+        _url2 = url2.split(
+          "https://view.officeapps.live.com/op/view.aspx?src="
+        )[1];
+      } else {
+        _url2 = url2;
+      }
+      var s3 = new window.AWS.S3({ params: { Bucket: "ccrb" } });
+      let name = decodeURIComponent(_url2.split("https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/")[1])
+      var params = {
+        Bucket: "ccrb",
+        Key: name
+      };
+      s3.getObject(params, function (err, data) {
+        if (err) {
+          console.log(err, err.stack)
+          resolve({ data: 1 });
+        }else {
+          resolve({ data: data.Body });
+          console.log(data);
+        }          // sxuccessful response
+
+      });
+    // axios({
+    //   method: "get",
+    //   url,
+    //   responseType: "blob",
+    // })
+    //   .then((data) => {
+    //     resolve({ data: data.data });
+    //   })
+    //   .catch((error) => {
+    //     resolve({ data: 1 });
+    //     // reject(error.toString());
+    //   });
+  });
+};
 export default {
   mixins: [myMixin],
   components: {
@@ -206,7 +256,9 @@ export default {
       isResultArray: [],
       isCan: false,
       timeoutPan: null,
+      dataBoardDebounce: null,
       isTimeOutPan: false,
+      pptJSON: {},
     };
   },
   computed: {
@@ -654,6 +706,23 @@ export default {
           console.log("getCourseDetail", res);
           this.courseDetail = res.data[0][0];
           this.courseDetail.chapters = JSON.parse(this.courseDetail.chapters);
+          getFile(this.courseDetail.chapters.pptData).then(res => {
+            let pptdata = res
+            // pptdata.data 是 ArrayBuffer,需要先转成字符串再解析为 JSON
+            let jsonStr = ''
+            if (pptdata && pptdata.data) {
+              // 先将 ArrayBuffer 转为字符串
+              const uint8Array = new Uint8Array(pptdata.data)
+              jsonStr = new TextDecoder('utf-8').decode(uint8Array)
+              try {
+                const jsonObj = JSON.parse(jsonStr)
+                this.pptJSON = jsonObj
+                console.log(this.pptJSON)
+              } catch (error) {
+                console.error('JSON 解析错误:', error);
+              }
+            }
+          })
           this.tcid = this.arrayToArray(
             this.courseDetail.juri ? this.courseDetail.juri.split(",") : [],
             this.tcid2 ? this.tcid2.split(",") : []
@@ -1011,7 +1080,13 @@ export default {
       // console.log('resetIdleOp','重置空闲定时器')
     },
     openDataBoard(){
-      this.$refs.dataBoardRef.open()
+      if (this.dataBoardDebounce) clearTimeout(this.dataBoardDebounce);
+      this.dataBoardDebounce = setTimeout(async () => {
+        let sec = Math.ceil((new Date(await this.getServerTime()) - new Date(this.startTime)) / (1000 * 60))
+        this.setoTime(sec);
+        this.setOperationTime()
+        this.$refs.dataBoardRef.open(this.tcid2)
+      }, 500);
     }
   },
   destroyed() {
@@ -1019,6 +1094,8 @@ export default {
     this.opertimer = null;
     clearTimeout(this.timeoutPan);
     this.timeoutPan = null;
+    clearTimeout(this.dataBoardDebounce);
+    this.dataBoardDebounce = null;
     this.doSyncClassData();
     this.stopIdleDetection(this.resetIdleOp);
   },