dusenyao hai 1 ano
pai
achega
29189b92a4

+ 1 - 4
.vscode/settings.json

@@ -13,8 +13,5 @@
   "i18n-ally.keystyle": "nested",
   "i18n-ally.sourceLanguage": "ZH",
   "i18n-ally.displayLanguage": "ZH",
-  "svg.preview.background": "transparent",
-  "path-intellisense.mappings": {
-    "@": "${workspaceFolder}/src"
-  }
+  "svg.preview.background": "transparent"
 }

+ 2 - 2
src/utils/course.js

@@ -1,4 +1,4 @@
-import { timeZeroFill } from '@/utils/index';
+import { zeroFill } from '@/utils/index';
 
 /**
  * @description 开始时间转换
@@ -11,7 +11,7 @@ export function previewDateTransform(date, duration) {
   let hour = parseInt(date.slice(11, 13));
   let minute = date.slice(14, 16);
 
-  return `${date.slice(5, 7)} 月 ${date.slice(8, 10)} 日 ${timeZeroFill(hour)}:${minute} 开始 ${timeZeroFill(
+  return `${date.slice(5, 7)} 月 ${date.slice(8, 10)} 日 ${zeroFill(hour)}:${minute} 开始 ${zeroFill(
     hour + duration / 60 / 60
   )}:${minute} 结束`;
 }

+ 25 - 13
src/utils/index.js

@@ -57,25 +57,37 @@ export function getConfigInformation() {
 }
 
 // 对小于 10 的补零
-export function timeZeroFill(val) {
+export function zeroFill(val) {
   if (val < 10) return `0${val}`;
   return val;
 }
 
 /**
- * 将秒转为 时:分:秒 格式
- * @param {Number} val 秒
- * @returns {String} hh:MM:ss
+ * 将秒转为时:分:秒格式
+ * @param {Number|String} val 秒
+ * @param {'normal'|'chinese'} type 格式类型
+ * @returns {String} hh:MM:ss 小于1小时返回 MM:ss
  */
-export function secondFormatConversion(val) {
-  let second = parseInt(val); // 秒
-  if (second < 60) {
-    return `00:${second < 10 ? `0${second}` : second}`;
+export function secondFormatConversion(val, type = 'normal') {
+  const seconds = parseInt(val); // 输入的秒数
+  const hours = Math.floor(seconds / 3600); // 小时部分
+  const minutes = Math.floor((seconds % 3600) / 60); // 分钟部分
+  const remainingSeconds = seconds % 60; // 剩余的秒数
+
+  // 使用零填充函数来格式化小时、分钟和秒
+  const formattedHours = zeroFill(hours);
+  const formattedMinutes = zeroFill(minutes);
+  const formattedSeconds = zeroFill(remainingSeconds);
+
+  // 根据时间范围返回不同的格式
+  if (hours > 0) {
+    if (type === 'chinese') {
+      return `${hours}时${minutes}分${remainingSeconds}秒`;
+    }
+    return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
   }
-  if (second < 3600) {
-    return `00:${timeZeroFill(parseInt(second / 60))}:${timeZeroFill(parseInt(second % 60))}`;
+  if (type === 'chinese') {
+    return `${minutes}分${remainingSeconds}秒`;
   }
-  return `${timeZeroFill(parseInt(second / 60 / 60))}:${timeZeroFill(parseInt((second / 60) % 60))}:${timeZeroFill(
-    parseInt(second % 60)
-  )}`;
+  return `${formattedMinutes}:${formattedSeconds}`;
 }

+ 64 - 24
src/views/task_details/student/index.vue

@@ -8,32 +8,38 @@
         {{ time_space_view_txt }}
       </div>
       <div class="task-require">
-        <span class="label">{{ $t('Key326') }}</span>
+        <span class="label">{{ isExercises ? '任务说明' : $t('Key326') }}</span>
         <span v-html="contentUrl"></span>
       </div>
-      <div class="task-courseware">
-        <span class="label">{{ $t('Key312') }}</span>
-        <div>
-          <el-tag
-            v-for="{ courseware_id, courseware_name, is_finished, group_id_selected_info } in courseware_list"
-            :key="courseware_id"
-            color="#fff"
-            :title="courseware_name"
-            @click="finishTask(courseware_id, is_finished, group_id_selected_info)"
-          >
-            <svg-icon icon-class="courseware" /> <span>{{ courseware_name }}</span>
-            <svg-icon v-if="is_finished === 'true'" class="check-mark" icon-class="check-mark" />
-          </el-tag>
-        </div>
+      <div v-if="isExercises" class="exercise-task">
+        <span class="label">练习任务</span>
+        <span class="exercise-link" @click="exerciseLink"> <SvgIcon icon-class="courseware" />{{ name }} </span>
       </div>
-      <div class="accessory-list">
-        <span class="label">{{ $t('Key313') }}</span>
-        <div>
-          <el-tag v-for="item in accessory_list" :key="item.file_id" color="#fff" :title="item.file_name">
-            <span @click="showFileVisible(item.file_name, item.file_id)">{{ item.file_name }}</span>
-          </el-tag>
+      <template v-if="!isExercises">
+        <div class="task-courseware">
+          <span class="label">{{ $t('Key312') }}</span>
+          <div>
+            <el-tag
+              v-for="{ courseware_id, courseware_name, is_finished, group_id_selected_info } in courseware_list"
+              :key="courseware_id"
+              color="#fff"
+              :title="courseware_name"
+              @click="finishTask(courseware_id, is_finished, group_id_selected_info)"
+            >
+              <svg-icon icon-class="courseware" /> <span>{{ courseware_name }}</span>
+              <svg-icon v-if="is_finished === 'true'" class="check-mark" icon-class="check-mark" />
+            </el-tag>
+          </div>
         </div>
-      </div>
+        <div class="accessory-list">
+          <span class="label">{{ $t('Key313') }}</span>
+          <div>
+            <el-tag v-for="item in accessory_list" :key="item.file_id" color="#fff" :title="item.file_name">
+              <span @click="showFileVisible(item.file_name, item.file_id)">{{ item.file_name }}</span>
+            </el-tag>
+          </div>
+        </div>
+      </template>
       <!-- 完成评价 -->
       <template v-if="my_execute_info.is_finished === 'true'">
         <div class="teacher-commenting">
@@ -170,7 +176,7 @@ export default {
 
 <script setup>
 import { ref, computed, inject } from 'vue';
-import { fileUpload } from '@/api/app';
+import { fileUpload, GetShareConfig } from '@/api/app';
 import { CreateEnterLiveRoomSession } from '@/api/live';
 import { GetTaskInfo, FillMyTaskExecuteInfo_Student } from '@/api/course';
 import { isAllowFileType, fileTypeSizeLimit } from '@/utils/validate';
@@ -249,6 +255,8 @@ let contentUrl = computed(() => {
   );
 });
 
+let exercise_share_record_id = ref('');
+
 GetTaskInfo({
   id,
   is_contain_cs_item_learning_material: true,
@@ -271,7 +279,8 @@ GetTaskInfo({
       is_enable_homework: isHomework,
       is_enable_message: isMessage,
       cs_item_begin_time,
-      cs_item_end_time
+      cs_item_end_time,
+      exercise_share_record_id: shareID
     }) => {
       itemInfo.value = {
         name,
@@ -294,6 +303,7 @@ GetTaskInfo({
       is_enable_KHPJ.value = isKHPJ === 'true';
       is_enable_homework.value = isHomework === 'true';
       is_enable_message.value = isMessage === 'true';
+      exercise_share_record_id.value = shareID;
     }
   )
   .finally(() => {
@@ -401,6 +411,15 @@ function dialogClose_completion() {
   dialogVisible_completion.value = false;
   curCoursewareId.value = '';
 }
+
+let exercise_share_url_path = ref(''); // 练习任务分享链接
+GetShareConfig().then(({ exercise_share_url_path: path }) => {
+  exercise_share_url_path.value = path;
+});
+
+function exerciseLink() {
+  window.open(`${exercise_share_url_path.value}?share_record_id=${exercise_share_record_id.value}`, '_blank');
+}
 </script>
 
 <style lang="scss" scoped>
@@ -521,12 +540,33 @@ $bor-color: #d9d9d9;
       }
     }
 
+    .exercise-task {
+      display: flex;
+      align-items: center;
+
+      .exercise-link {
+        display: flex;
+        column-gap: 8px;
+        align-items: center;
+        padding: 8px 16px;
+        cursor: pointer;
+        border: 1px solid #e0e0e0;
+        border-radius: 4px;
+
+        .svg-icon {
+          width: 24px;
+          height: 24px;
+        }
+      }
+    }
+
     .task-require,
     .task-courseware,
     .accessory-list,
     .submit-homework,
     .leave-message,
     .live-info,
+    .exercise-task,
     .teacher-commenting {
       display: flex;
       margin-bottom: 16px;

+ 35 - 0
src/views/task_details/teacher/exercise.js

@@ -0,0 +1,35 @@
+import { ref } from 'vue';
+import { GetShareConfig } from '@/api/app';
+
+export const questionColorList = [
+  { label: '正确', color: '#D9D9D9' },
+  { label: '错题', color: '#F2555A' },
+  { label: '主观题', color: '#FEF2A4' }
+];
+
+export function useExerciseTeacher() {
+  let exercise_share_url_path = ref('');
+  GetShareConfig().then(({ exercise_share_url_path: path }) => {
+    exercise_share_url_path.value = path;
+  });
+
+  /**
+   * 跳转到答题页面
+   * @param {string} exercise_share_record_id 分享记录id
+   * @param {string} exercise_answer_record_id 答题记录id
+   * @param {number} index 题目索引
+   */
+  function exerciseLink(exercise_share_record_id, exercise_answer_record_id, index) {
+    console.log(
+      `${exercise_share_url_path.value}?share_record_id=${exercise_share_record_id}&answer_record_id=${exercise_answer_record_id}&question_index=${index}`
+    );
+    window.open(
+      `${exercise_share_url_path.value}?share_record_id=${exercise_share_record_id}&answer_record_id=${exercise_answer_record_id}&question_index=${index}`,
+      '_blank'
+    );
+  }
+
+  return {
+    exerciseLink
+  };
+}

+ 182 - 8
src/views/task_details/teacher/index.vue

@@ -14,15 +14,78 @@
               @click="getTaskStudentExecuteInfo(item.student_id)"
             >
               <span>{{ item.student_name }}</span>
-              <svg-icon v-if="item.is_finished === 'true'" icon-class="check-mark" />
+              <svg-icon v-if="!isExercises && item.is_finished === 'true'" icon-class="check-mark" />
+              <span v-if="isExercises && item.exercise_info.is_finish === 'true'" class="submitted">已提交</span>
             </li>
           </ul>
         </div>
       </div>
       <div class="finish-detail">
         <template v-if="isExercises">
-          <div ref="situation" class="exercise"></div>
+          <div ref="situation" class="exercise">
+            <div class="title">测试报告</div>
+            <div class="color-list">
+              <div v-for="{ label, color } in questionColorList" :key="label" class="color-item">
+                <span class="color" :style="{ backgroundColor: color }"></span>
+                <span>{{ label }}</span>
+              </div>
+            </div>
+            <div class="exercise-details">
+              <div class="info-item">
+                <span class="label">完成时间</span>
+                <span class="exercise-info">{{ exercise_info.answer_record?.finish_time }}</span>
+              </div>
+              <div class="info-item">
+                <span class="label">耗时</span>
+                <span class="exercise-info">{{
+                  secondFormatConversion(exercise_info.answer_record?.answer_duration, 'chinese')
+                }}</span>
+              </div>
+              <div class="info-item">
+                <span class="label">正确</span>
+                <span class="exercise-info">{{ exercise_info.answer_record?.right_count }}</span>
+              </div>
+              <div class="info-item">
+                <span class="label">错误</span>
+                <span class="exercise-info">{{ exercise_info.answer_record?.error_count }}</span>
+              </div>
+              <div class="info-item">
+                <span class="label">正确率</span>
+                <span class="exercise-info">{{ exercise_info.answer_record?.right_percent }}%</span>
+              </div>
+            </div>
+            <!-- 问题列表 -->
+            <div class="question-list">
+              <span
+                v-for="({ question_id, is_objective, answer_status }, i) in exercise_info.question_list"
+                :key="question_id"
+                :class="['question-list-item', { subjectivity: is_objective === 'false', error: answer_status === 2 }]"
+                @click="
+                  exerciseLink(
+                    exercise_info.answer_record.exercise_share_record_id,
+                    exercise_info.answer_record.exercise_answer_record_id,
+                    i
+                  )
+                "
+              >
+                {{ i + 1 }}
+              </span>
+            </div>
+            <div class="total-score">
+              <div>总得分</div>
+              <div>{{ exercise_info.answer_record?.total_score }}</div>
+            </div>
+
+            <div class="footer">
+              <el-button type="primary" @click="remarkTaskStudentExecuteInfo_Teacher">批改完成</el-button>
+              <el-button>重发</el-button>
+              <el-button v-if="student_list.length > 1" @click="next">
+                {{ buttonName }} <i class="el-icon-right"></i>
+              </el-button>
+            </div>
+          </div>
         </template>
+
         <template v-else>
           <div class="student-info">
             <div>
@@ -138,6 +201,8 @@ import { GetTaskInfo, GetTaskStudentExecuteInfo, RemarkTaskStudentExecuteInfo_Te
 import { useRoute } from 'vue-router/composables';
 import { Message } from 'element-ui';
 import { useShowFile } from '@/common/show_file/index';
+import { useExerciseTeacher, questionColorList } from './exercise';
+import { secondFormatConversion } from '@/utils';
 
 import CompletionView from '@/components/course/CompletionView.vue';
 import ShowFile from '@/common/show_file/index.vue';
@@ -147,6 +212,8 @@ const $t = inject('$t');
 const route = useRoute();
 let id = route.params.id;
 
+const { exerciseLink } = useExerciseTeacher();
+
 // 任务详情
 let itemInfo = ref({});
 let teachingType = ref('');
@@ -177,8 +244,6 @@ GetTaskInfo({
       cs_item_learning_material_list,
       time_space_view_txt,
       student_list: stuList,
-      is_custom_student,
-      custom_student_list,
       is_enable_KHPJ: isKHPJ,
       is_enable_homework: isHomework,
       is_enable_message: isMessage,
@@ -201,7 +266,7 @@ GetTaskInfo({
       is_enable_KHPJ.value = isKHPJ === 'true';
       is_enable_homework.value = isHomework === 'true';
       is_enable_message.value = isMessage === 'true';
-      student_list.value = is_custom_student === 'true' ? custom_student_list : stuList;
+      student_list.value = stuList;
       if (student_list.value.length > 0) getTaskStudentExecuteInfo(student_list.value[0].student_id);
     }
   )
@@ -225,6 +290,7 @@ let teacher_score = ref(0);
 let student_remark = ref('');
 let student_score = ref(0);
 let student_list_height = ref(490);
+let exercise_info = ref({}); // 练习题信息
 function getTaskStudentExecuteInfo(student_id) {
   GetTaskStudentExecuteInfo({
     task_id: id,
@@ -241,7 +307,7 @@ function getTaskStudentExecuteInfo(student_id) {
       teacher_score: tScore,
       student_remark: sRemake,
       student_score: stuScore,
-      exercise_info
+      exercise_info: exerciseInfo
     }) => {
       curStudentId.value = student_id;
       teacher_remark.value = tRemake;
@@ -256,7 +322,7 @@ function getTaskStudentExecuteInfo(student_id) {
         student_image_url,
         finish_time_view_txt
       };
-      console.log(exercise_info);
+      exercise_info.value = exerciseInfo;
       nextTick(() => {
         student_list_height.value = situation.value.clientHeight;
       });
@@ -288,7 +354,7 @@ function remarkTaskStudentExecuteInfo_Teacher() {
     teacher_score: teacher_score.value,
     teacher_remark: teacher_remark.value
   }).then(() => {
-    Message.success($t('Key324'));
+    Message.success(isExercises.value ? '批改成功' : $t('Key324'));
   });
 }
 
@@ -363,6 +429,12 @@ $bor-color: #d9d9d9;
             &.active {
               background-color: #f2f2f2;
             }
+
+            .submitted {
+              position: relative;
+              left: 16px;
+              color: #00c264;
+            }
           }
         }
       }
@@ -373,6 +445,108 @@ $bor-color: #d9d9d9;
       flex: 7;
       border-left: 1px solid #dbdbdb;
 
+      // 练习题
+      .exercise {
+        display: flex;
+        flex-direction: column;
+        row-gap: 24px;
+        padding: 24px 40px;
+
+        .color-list {
+          display: flex;
+          column-gap: 24px;
+
+          .color-item {
+            display: flex;
+            column-gap: 8px;
+            align-items: center;
+
+            .color {
+              width: 16px;
+              height: 16px;
+              border-radius: 50%;
+            }
+          }
+        }
+
+        // 练习题详情
+        .exercise-details {
+          display: flex;
+          column-gap: 80px;
+          padding: 16px 40px;
+          background-color: #f7f7f7;
+
+          .info-item {
+            display: flex;
+            flex-direction: column;
+            row-gap: 12px;
+
+            .label {
+              font-size: 14px;
+              color: #949494;
+              white-space: nowrap;
+            }
+
+            .exercise-info {
+              font-size: 20px;
+              font-weight: bold;
+              color: #333;
+            }
+          }
+        }
+
+        // 题目列表
+        .question-list {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 24px;
+          width: 770px;
+          margin-bottom: 40px;
+
+          &-item {
+            width: 56px;
+            height: 40px;
+            padding: 8px;
+            line-height: 24px;
+            text-align: center;
+            cursor: pointer;
+            background-color: #f0f0f0;
+            border-radius: 20px;
+
+            &.error {
+              color: #fff;
+              background-color: #f2555a;
+            }
+
+            &.subjectivity {
+              background-color: #fef2a4;
+            }
+          }
+        }
+
+        .total-score {
+          display: flex;
+          flex-direction: column;
+          row-gap: 8px;
+          font-size: 14px;
+
+          :first-child {
+            color: #949494;
+          }
+
+          :last-child {
+            font-weight: bold;
+            color: #333;
+          }
+        }
+
+        .footer {
+          .el-button {
+            font-weight: bold;
+          }
+        }
+      }
+
       .student-info {
         display: flex;
         justify-content: space-between;