Sfoglia il codice sorgente

合并了host-nju代码 解决了冲突

Jiang Pengyu 1 anno fa
parent
commit
9ef98362e5

+ 7 - 1
package-lock.json

@@ -21,6 +21,7 @@
         "mitt": "^3.0.1",
         "mitt": "^3.0.1",
         "pinia": "^2.1.7",
         "pinia": "^2.1.7",
         "pinia-plugin-persistedstate": "^3.2.1",
         "pinia-plugin-persistedstate": "^3.2.1",
+        "spark-md5": "^3.0.2",
         "vue": "^3.4.29",
         "vue": "^3.4.29",
         "vue-demi": "^0.14.10",
         "vue-demi": "^0.14.10",
         "vue-router": "^4.3.3",
         "vue-router": "^4.3.3",
@@ -2152,7 +2153,7 @@
     },
     },
     "node_modules/echarts": {
     "node_modules/echarts": {
       "version": "5.6.0",
       "version": "5.6.0",
-      "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
       "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
       "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
       "dependencies": {
       "dependencies": {
         "tslib": "2.3.0",
         "tslib": "2.3.0",
@@ -3952,6 +3953,11 @@
         "node": ">=0.10.0"
         "node": ">=0.10.0"
       }
       }
     },
     },
+    "node_modules/spark-md5": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/spark-md5/-/spark-md5-3.0.2.tgz",
+      "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw=="
+    },
     "node_modules/ssf": {
     "node_modules/ssf": {
       "version": "0.11.2",
       "version": "0.11.2",
       "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
       "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
     "mitt": "^3.0.1",
     "mitt": "^3.0.1",
     "pinia": "^2.1.7",
     "pinia": "^2.1.7",
     "pinia-plugin-persistedstate": "^3.2.1",
     "pinia-plugin-persistedstate": "^3.2.1",
+    "spark-md5": "^3.0.2",
     "vue": "^3.4.29",
     "vue": "^3.4.29",
     "vue-demi": "^0.14.10",
     "vue-demi": "^0.14.10",
     "vue-router": "^4.3.3",
     "vue-router": "^4.3.3",

+ 70 - 0
src/apis/class.ts

@@ -0,0 +1,70 @@
+import axiosInstance from "@/apis/axios.config";
+//提交的后端路径
+const ENGAGE_PREFIX = "/api/class"
+
+const classApis = {
+    getClassOfCourse:(courseId: number) => {
+        return axiosInstance.get(ENGAGE_PREFIX+"/select",{
+            params:{
+                courseId
+            }
+        })
+    },
+    createClassOfCourse:(courseId:number,className:string,classTime:string,teacherId:number) => {
+        return axiosInstance.post(ENGAGE_PREFIX+"/create",{
+            courseId,
+            className,
+            classTime,
+            teacherId
+        })
+    },
+    deleteClass:(classId:number) => {
+        return axiosInstance.delete(ENGAGE_PREFIX+"/delete",{
+            params:{
+                classId
+            }
+        })
+    },
+    getStudentsOfClass:(classId:number) => {
+        return axiosInstance.get(ENGAGE_PREFIX+`/${classId}`)
+    },
+    addStudentOfClasss:(classId:number,officialNumber:number) => {
+        return axiosInstance.post(ENGAGE_PREFIX+`/${classId}/students`,{},{
+            params:{
+                officialNumber
+            }
+        })
+    },
+    deleteStudentOfClass:(classId:number,officialNumbers) => {
+        return axiosInstance.delete(ENGAGE_PREFIX+`/${classId}/students/batch`,{
+            headers:{
+                'Content-Type': 'application/json'
+            },
+            data: officialNumbers
+        })
+    },
+    getClassStatesOfCourse:(classId:number,assignmentId:number) => {
+        return axiosInstance.get(`/api/assignment/class/${classId}/status`,{
+            params:{
+                assignmentId
+            }
+        })
+    },
+    getHomeworkOfStudent:(courseId:number,studentId:number) => {
+        return axiosInstance.get(`/api/course/student/assignments`,{
+            params:{
+                studentId,
+                courseId
+            }
+        })
+    },
+    getClassByClassId:(classId:number) => {
+        return axiosInstance.get(`/api/class/find/${classId}`)
+    },
+    getClassNameOfStudent:(courseId:number,studentId:number) => {
+        return axiosInstance.get(ENGAGE_PREFIX+`/student/${studentId}/course/${courseId}`)
+    }
+}
+
+
+export default classApis

+ 3 - 1
src/apis/file.ts

@@ -1,3 +1,4 @@
+
 import axiosInstance from "@/apis/axios.config";
 import axiosInstance from "@/apis/axios.config";
 
 
 const FILE_PREFIX = "/api/file"
 const FILE_PREFIX = "/api/file"
@@ -12,7 +13,8 @@ const fileApis = {
             },
             },
             timeout:300000 // 添加超时时间等待
             timeout:300000 // 添加超时时间等待
         });
         });
-    },
+    }
 }
 }
 
 
+
 export default fileApis
 export default fileApis

+ 2 - 1
src/apis/index.ts

@@ -11,7 +11,7 @@ import evaluationApis from "@/apis/evaluation";
 import emailApis from "@/apis/email";
 import emailApis from "@/apis/email";
 import authApis from "@/apis/auth";
 import authApis from "@/apis/auth";
 import bevaviorApis from "@/apis/behavior";
 import bevaviorApis from "@/apis/behavior";
-
+import classApis from "@/apis/class";
 
 
 const apis = {
 const apis = {
     ...authApis,
     ...authApis,
@@ -27,6 +27,7 @@ const apis = {
     ...evaluationApis,
     ...evaluationApis,
     ...emailApis,
     ...emailApis,
     ...bevaviorApis,
     ...bevaviorApis,
+    ...classApis,
 }
 }
 
 
 // hook
 // hook

+ 1 - 0
src/assets/add.svg

@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="addIconTitle" stroke="#4a90c4" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#4a90c4"> <title id="addIconTitle">Add</title> <path d="M17 12L7 12M12 17L12 7"/> <circle cx="12" cy="12" r="10"/> </svg>

+ 1 - 0
src/assets/chevron-right.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#b8b8b8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>

+ 1 - 0
src/assets/people.svg

@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" aria-labelledby="peopleIconTitle" stroke="#4a90c4" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#4a90c4"> <title id="peopleIconTitle">People</title> <path d="M1 18C1 15.75 4 15.75 5.5 14.25 6.25 13.5 4 13.5 4 9.75 4 7.25025 4.99975 6 7 6 9.00025 6 10 7.25025 10 9.75 10 13.5 7.75 13.5 8.5 14.25 10 15.75 13 15.75 13 18M12.7918114 15.7266684C13.2840551 15.548266 13.6874862 15.3832994 14.0021045 15.2317685 14.552776 14.9665463 15.0840574 14.6659426 15.5 14.25 16.25 13.5 14 13.5 14 9.75 14 7.25025 14.99975 6 17 6 19.00025 6 20 7.25025 20 9.75 20 13.5 17.75 13.5 18.5 14.25 20 15.75 23 15.75 23 18"/> <path stroke-linecap="round" d="M12,16 C12.3662741,15.8763472 12.6302112,15.7852366 12.7918114,15.7266684"/> </svg>

+ 1 - 0
src/assets/plus-square.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4a90c4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>

+ 1 - 0
src/assets/search.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#606266" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>

+ 1 - 0
src/assets/time.svg

@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" aria-labelledby="timeIconTitle" stroke="#4a90c4" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#4a90c4"> <title id="timeIconTitle">Time</title> <circle cx="12" cy="12" r="10"/> <polyline points="12 5 12 12 16 16"/> </svg>

+ 33 - 1
src/router/index.ts

@@ -137,6 +137,38 @@ const router = createRouter({
           name: 'myCorrectWithAssignmentId',
           name: 'myCorrectWithAssignmentId',
           component: () => import("@/views/CorrectPage/CorrectAssignmentPage.vue")
           component: () => import("@/views/CorrectPage/CorrectAssignmentPage.vue")
         },
         },
+        {
+          path: 'studentDetail',
+          name: 'studentDetail',
+          component: () => import("@/views/ClassPage/StudentDetail.vue")
+        },
+        {
+          path: 'courseClass/:courseId',
+          name: 'courseClass',
+          component: () => import("@/views/ClassPage/CourseClass.vue")
+        },
+        {
+          path: 'classDetail',
+          name: 'classDetail',
+          component: () => import("@/views/ClassPage/ClassDetail.vue"),
+          children:[
+            {
+              path: "",
+              name:"classDetail",
+              redirect: "/home/classDetail/student"
+            },
+            {
+              path: 'student',
+              name: 'classStudent',
+              component: () => import("@/views/ClassPage/ClassStudent.vue")
+            },
+            {
+              path: 'homework',
+              name: 'classHomework',
+              component: () => import("@/views/ClassPage/ClassHomework.vue")
+            },
+          ]
+        },
       ]
       ]
     },
     },
 
 
@@ -195,7 +227,7 @@ const router = createRouter({
  */
  */
   router.beforeEach(async (to) => {
   router.beforeEach(async (to) => {
     const store = useStore();
     const store = useStore();
-    const publicPaths = [ '/register', '/passwordChange', '/admin/login'];
+    const publicPaths = [ '/register', '/passwordChange', '/admin/login','];
     // 1. 检查URL中的token
     // 1. 检查URL中的token
     const urlToken = new URLSearchParams(window.location.search).get('token');
     const urlToken = new URLSearchParams(window.location.search).get('token');
     if (urlToken) {
     if (urlToken) {

+ 1 - 1
src/types/vo.ts

@@ -144,7 +144,7 @@ export interface CourseDetailVO {
 export interface StudentAssignmentStatus {
 export interface StudentAssignmentStatus {
     assignmentId: number;
     assignmentId: number;
     assignmentName: string;
     assignmentName: string;
-    status: 'CORRECTED' | 'SUBMITTED' | 'NOT_SUBMITTED';
+    status: 'CORRECTED' | 'SUBMITTED' | 'NOT_SUBMITTED'|'LATE_SUBMITTED_CORRECTED'|'LATE_SUBMITTED';
     score: number;
     score: number;
 }
 }
 
 

+ 44 - 10
src/views/AiSpeakingPage/SpeakingPage.vue

@@ -13,12 +13,14 @@
           <div class="end-time">截止时间:{{ formatDate(aiSpeakingAssignmentInfo.endTime) }}</div>
           <div class="end-time">截止时间:{{ formatDate(aiSpeakingAssignmentInfo.endTime) }}</div>
 
 
         </div>
         </div>
-            <div class="data-item">
-              <span class="data-label">作业描述 </span>
-              <span class="data-value">{{ aiSpeakingAssignmentInfo.description !== null ? aiSpeakingAssignmentInfo.description : '未设置' }}</span>
-            </div>
-            <div class="data-item">
-              <span v-if="aiSpeakingAssignmentInfo.descriptionFile" class="data-label">作业附件</span>
+        <div class="data-item">
+          <span class="data-label">作业描述</span>
+          <div class="scrollable-description">
+            {{ aiSpeakingAssignmentInfo.description !== null ? aiSpeakingAssignmentInfo.description : '未设置' }}
+          </div>
+        </div>
+            <div class="data-item" v-if="aiSpeakingAssignmentInfo.descriptionFile">
+              <span class="data-label">作业附件</span>
               <el-icon  @click="openEditor(aiSpeakingAssignmentInfo.descriptionFile, aiSpeakingAssignmentInfo.assignmentName)"><Document /></el-icon>
               <el-icon  @click="openEditor(aiSpeakingAssignmentInfo.descriptionFile, aiSpeakingAssignmentInfo.assignmentName)"><Document /></el-icon>
             </div>
             </div>
             <div class="data-item">
             <div class="data-item">
@@ -620,6 +622,9 @@ let homeworkStartTime = null;
 const cameraPreview = ref(null);
 const cameraPreview = ref(null);
 const recordingDuration = ref(0); // 录制时长(秒)
 const recordingDuration = ref(0); // 录制时长(秒)
 let recordingTimer = null; // 录制计时器
 let recordingTimer = null; // 录制计时器
+
+const lastSavePromptTime = ref(0); // 记录上次提示保存的时间点
+const SAVE_REMINDER_INTERVAL = 180; // 3分钟(180秒)提醒一次
 // 开始作业
 // 开始作业
 const startHomework = async () => {
 const startHomework = async () => {
   scrollToBottom();
   scrollToBottom();
@@ -652,6 +657,13 @@ const startHomework = async () => {
       // 启动录制时长计时器
       // 启动录制时长计时器
     recordingTimer = setInterval(() => {
     recordingTimer = setInterval(() => {
       recordingDuration.value += 1;
       recordingDuration.value += 1;
+
+      // 每3分钟提醒保存一次(180秒)
+      if (recordingDuration.value % SAVE_REMINDER_INTERVAL === 0 && 
+          recordingDuration.value !== lastSavePromptTime.value) {
+        lastSavePromptTime.value = recordingDuration.value;
+        ElMessage.warning('录制时长已超过3分钟,建议暂存当前进度');
+  }
     }, 1000);
     }, 1000);
      // 将摄像头画面绑定到 video 元素
      // 将摄像头画面绑定到 video 元素
      if (cameraPreview.value) {
      if (cameraPreview.value) {
@@ -702,6 +714,7 @@ const saveHomework = async () => {
         const videoBlob = new Blob([...recordedChunks.value], { type: "video/webm" });
         const videoBlob = new Blob([...recordedChunks.value], { type: "video/webm" });
         const fileName = `user_${userId}_${assignmentId}_${Date.now()}`;
         const fileName = `user_${userId}_${assignmentId}_${Date.now()}`;
         let renameFile =new File([videoBlob], fileName +'.webm', { type: 'video/webm' })
         let renameFile =new File([videoBlob], fileName +'.webm', { type: 'video/webm' })
+
         try {
         try {
           doubaoApis
           doubaoApis
           .saveSigment(userId, assignmentId,renameFile, duration)
           .saveSigment(userId, assignmentId,renameFile, duration)
@@ -712,6 +725,7 @@ const saveHomework = async () => {
             isHomeworkStarted.value = false;
             isHomeworkStarted.value = false;
             ElMessage.success("视频片段暂存成功");
             ElMessage.success("视频片段暂存成功");
             loadingInstance.close();
             loadingInstance.close();
+            lastSavePromptTime.value = 0; // 重置提示计时
           })
           })
         } catch (error) {
         } catch (error) {
           console.error("视频片段上传失败:", error);
           console.error("视频片段上传失败:", error);
@@ -795,18 +809,20 @@ const resetHomework = async() => {
     return;
     return;
   }
   }
   if (isHomeworkStarted.value) {
   if (isHomeworkStarted.value) {
-    if (mediaRecorder.value && mediaRecorder.value.state === "recording") {
-      mediaRecorder.value.stop();
-    }
     // 停止录制时长计时器
     // 停止录制时长计时器
     if (recordingTimer) {
     if (recordingTimer) {
       clearInterval(recordingTimer);
       clearInterval(recordingTimer);
       recordingTimer = null;
       recordingTimer = null;
+
     }
     }
     // 清除摄像头画面
     // 清除摄像头画面
     if (cameraPreview.value) {
     if (cameraPreview.value) {
       cameraPreview.value.srcObject = null;
       cameraPreview.value.srcObject = null;
     }
     }
+     // 关闭摄像头和麦克风
+     if (mediaRecorder.value && mediaRecorder.value.stream) {
+              mediaRecorder.value.stream.getTracks().forEach((track) => track.stop());
+      }
   }
   }
 
 
    // 显示加载状态
    // 显示加载状态
@@ -823,8 +839,10 @@ const resetHomework = async() => {
       isHomeworkSaved.value = false;
       isHomeworkSaved.value = false;
       isHomeworkStarted.value = false;
       isHomeworkStarted.value = false;
       initRecognitionValue()
       initRecognitionValue()
+      recordingDuration.value = 0;
       ElMessage.success("作业内容已清空");
       ElMessage.success("作业内容已清空");
       getHistory()
       getHistory()
+      lastSavePromptTime.value = 0; // 重置提示计时
 
 
     } else {
     } else {
       ElMessage.error("清除失败,请稍后尝试")
       ElMessage.error("清除失败,请稍后尝试")
@@ -1005,6 +1023,7 @@ onBeforeUnmount(() => {
   if (cameraPreview.value) {
   if (cameraPreview.value) {
     cameraPreview.value.srcObject = null;
     cameraPreview.value.srcObject = null;
   }
   }
+  lastSavePromptTime.value = 0; // 清理计时
 });
 });
 
 
 
 
@@ -1124,6 +1143,20 @@ onMounted(() => {
     color: #777;
     color: #777;
   }
   }
 
 
+
+  .scrollable-description {
+  max-height: 12em; /* 10行高度(假设1em=1行) */
+  line-height: 1.3em; /* 行高设定 */
+  overflow-y: auto; /* 垂直滚动 */
+  white-space: pre-wrap; /* 保留换行符 */
+  color: #777;
+  margin-top: 10px;
+  scrollbar-width: none; /* Firefox */
+  -ms-overflow-style: none; /* IE/Edge */
+}
+
+
+
 .homework-button-group {
 .homework-button-group {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
@@ -1369,7 +1402,7 @@ progress {
 }
 }
 
 
 .wechat-audio-button {
 .wechat-audio-button {
-  margin-left: 20px;
+  margin-left: 25px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   background-color: #90caf9;
   background-color: #90caf9;
@@ -1378,6 +1411,7 @@ progress {
   padding: 8px 15px;
   padding: 8px 15px;
   cursor: pointer;
   cursor: pointer;
   transition: background-color 0.3s;
   transition: background-color 0.3s;
+  margin-bottom:8px;
 }
 }
 
 
 .wechat-audio-button:hover {
 .wechat-audio-button:hover {

+ 198 - 0
src/views/ClassPage/ClassDetail.vue

@@ -0,0 +1,198 @@
+<template>
+    <div class="course-class" v-show="DataLoading">
+        <classHeader>{{Coursedata.courseName}}:{{ Classdata.className }}</classHeader>
+        <div class="content">
+            <router-view></router-view>
+            <!-- <classTable></classTable> -->
+        </div>
+        <!-- <classDetailFooter :classId=classId :courseId=courseId :flag=true></classDetailFooter> -->
+        <div class="class-footer">
+            <button 
+                :class="{'active':choose,'unactive':!choose,'inhover':(!choose && hoverid===1)}"
+                :disabled="choose"
+                @click="{choose = true;$router.replace(`/home/classDetail/student?classId=${classId}&courseId=${courseId}`);}"
+                @mouseenter="hoverid = 1"
+                @mouseleave="hoverid = 0"
+            >
+            班级学生管理
+            </button>
+            <button 
+                :class="{'active':!choose,'unactive':choose,'inhover':(choose && hoverid===2)}"
+                :disabled="!choose"
+                @click="{choose = false;$router.replace(`/home/classDetail/homework?classId=${classId}&courseId=${courseId}`);}"
+                @mouseenter="hoverid = 2"
+                @mouseleave="hoverid = 0"
+            >
+            班级作业管理
+            </button>
+        </div>
+    </div>
+    <div class="loading" v-show="!DataLoading"> Loading...</div>
+</template>
+
+<script setup lang="ts">
+import {onMounted, reactive, ref, computed} from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import classHeader from './components/classHeader.vue';
+import type {CourseDetailVO, ClassVO} from "@/types/vo";
+import { useStore } from "@/store"
+import useApis from "@/apis";
+
+const store = useStore()
+const route = useRoute()
+const router = useRouter()
+const apis = useApis()
+const classId = +route.query.classId;
+const courseId = +route.query.courseId;
+
+const choose = ref(true)
+let hoverid = ref(0) // 0 没有 1 学生 2 作业 
+
+const CourseDataLoading = ref(false)
+const ClassDataLoading = ref(false)
+
+const Coursedata = reactive<CourseDetailVO>({
+  courseId: null,
+  courseName: '',
+  teacherIds: [],
+  teacherNames: [],
+  courseImages: [''],
+  targetGrade: '',
+  startTime: '',
+  endTime: '',
+  createTime: '',
+  description: '',
+})
+
+const Classdata = reactive<ClassVO>({
+  classId: null,
+  className: '',
+  courseId: null,
+  stuNumber: null,
+  classTime: '',
+  teacherId: null,
+})
+
+const identify = () => {
+    if(store.user.role === "STUDENT"){
+        router.push('/404')
+    }
+}
+const getClass = () => {
+    console.log("classId:",classId);
+    Classdata.classId = classId
+    Classdata.courseId = courseId
+    Classdata.className = "读写1班"
+    Classdata.stuNumber = 30
+    Classdata.classTime = "Mon 10:10"
+    Classdata.teacherId = 4
+
+    ClassDataLoading.value = true
+}
+const getCourse = () => {
+  // 获取课程信息
+  apis.getCourseByCourseId(courseId)
+      .then((res: any) => {
+        let data = res.data.data.records[0]
+        Object.assign(Coursedata, {...data})
+        CourseDataLoading.value = true
+      })
+      .catch((err: any) => {
+        console.error(err);
+      })
+}
+const setchoose = () => {
+    const currentPath = route.path
+    const parts = currentPath.split('/')
+    if(parts[parts.length-1]=="homework")
+        choose.value=false
+    else
+        choose.value=true
+}
+const DataLoading = computed(() => {
+  return CourseDataLoading.value && ClassDataLoading.value
+})
+
+
+onMounted(() => {
+    identify()
+    setchoose()
+    getClass()
+    getCourse()
+})
+</script>
+
+<script lang="ts">
+export default {
+  name: "ClassDetail"
+}
+</script>
+
+<style scoped>
+.course-class {
+    width: 66vw;
+    height: 65vh;
+    background-color: #fff;
+    border-radius: 15px;
+    margin: 0 auto;
+    margin-top: 4.6vh;
+    border-bottom: 1px solid #70bbdb;
+    padding:  7vh 5.5vw;
+    .content {
+        height: 55vh;
+        width: 100%;
+        /* border:1px solid; */
+        margin: 0 auto;
+        display: flex;
+        align-items: center;
+        flex-direction: row;
+        flex-wrap: wrap;
+        justify-content: start;
+        padding: 10px;
+        gap: 3.6vw;
+    }
+    .class-footer {
+        width: '100%';
+        height: 7.5vh;
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        justify-content: center;
+        gap: 250px;
+        .active {
+            font-size: 24px;
+            font-weight: 600;
+            color: #fff;
+            border-radius: 50px;
+            padding: 2px 10px;
+            background-color: #70bbdb;
+            border:none;
+            border-bottom: 5px solid #4FA3C5;
+        }
+        .unactive {
+            font-size: 24px;
+            font-weight: 600;
+            color: #fff;
+            border-radius: 50px;
+            padding: 2px 10px;
+            background-color: rgb(112, 187, 219, 0.5);
+            border:none;
+            border-bottom: 5px solid #4FA3C5;
+            box-shadow: -5px 7px 7px 0 rgba(0, 0, 0, 0.25);
+        }
+        .inhover{
+            box-shadow: -10px 7px 20px #70bbdb;
+            border-bottom: 5px solid rgb(112, 187, 219, 0.5);
+        }
+    }
+}
+.loading{
+    height: 100%;
+    width: 100%;
+    text-align: center;
+    line-height: 100vh;
+    font-size: 44px;
+    font-weight: 700;
+    color: #70bbdb;
+}
+</style>

+ 574 - 0
src/views/ClassPage/ClassHomework.vue

@@ -0,0 +1,574 @@
+<template>
+    <div class="main">
+        <div class="search">
+            <img src="@/assets/search.svg" width="20px" height="20px"></img> 
+            <div class="searchInput">
+                <input type="text" v-model.trim="searchInput" placeholder="输入姓名或学号" :disabled="!isTable">
+                <div class="fix">
+                    <span>搜索</span>
+                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#b8b8b8" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
+                </div>
+            </div>
+            <div class="Myselect">
+                <select id="selectHomework" v-model="TheHomework" @change="changeHomework" :disabled="!isTable"> 
+                    <option value=-1>选择作业</option>
+                    <option v-for="item in Homeworksdata" :value="item.assignmentId">{{item.assignmentName}}</option>
+                </select>
+                <div class="show">
+                        {{ ShowTheHomework }}
+                </div>
+            </div>
+            <div class="Myselect">
+                <select id="selectSort" v-model="TheSort" :disabled="!isTable"> 
+                    <option value="无">排序方式</option>
+                    <option v-for="item in sortMethods" :value="item">{{item}}</option>
+                </select>
+                <div class="show">
+                        {{ ShowTheSort }}
+                </div>
+            </div>
+            <classToggle v-model:isLeft="isTable">
+                <template #left>
+                    table
+                </template>
+                <template #right>
+                    chart
+                </template>
+            </classToggle>
+        </div>
+        <div class="content">
+            <div class="Loading" v-show="!DataLoading">Loading...</div>
+            <div class="half-table" v-show="DataLoading">
+                <classTable v-if="isTable">
+                <template #head>
+                    <tr>
+                        <th>序号</th>
+                        <th>学生姓名</th>
+                        <th>学号</th>
+                        <th>成绩</th>
+                    </tr>
+                </template>
+                <template #body>
+                    <tr v-for="(item,index) in showData.slice(0,5)" :key="index+item.officialNumber">
+                        <template v-if="item.officialNumber!==0">
+                            <td>{{ (currentPage-1)*10+index+1 }}</td>
+                            <td
+                                @click="$router.push(`/home/studentDetail?classId=${classId}&courseId=${courseId}&stuId=${item.studentId}`)"
+                                @mouseenter="hoverStuId=item.studentId"
+                                @mouseleave="hoverStuId=-1"
+                                :class="{Gethover:hoverStuId===item.studentId,Blue:true}"
+                                >{{ item.stuName }}</td>
+                            <td>{{ item.officialNumber }}</td>
+                            <td  @click="$router.push(`/home/assignment/${ShowTheHomeworkId}/correct?studentId=${item.studentId}`)">
+                                <div class="highScore" v-if="item.score>=80"> {{ item.score }}</div>
+                                <div class="midScore" v-else-if="item.score>=60"> {{ item.score }}</div>
+                                <div class="lowScore" v-else-if="item.score>0"> {{ item.score }}</div>
+                                <div class="uncorrected" v-else-if="item.status==='SUBMITTED'"> 未批改</div>
+                                <div class="unsubmitted" v-else-if="item.status==='NOT_SUBMITTED'"> 未提交</div>             
+                            </td>
+                        </template>
+                        <template v-else>
+                            <td>&nbsp;</td>
+                            <td>&nbsp;</td>
+                            <td>&nbsp;</td>
+                            <td>&nbsp;</td>
+                        </template>
+                    </tr>
+                </template>
+                </classTable>
+                <div ref="chartRef1" class="chart" v-else></div>
+            </div>
+            <div class="half-table" v-show="DataLoading">
+                <classTable v-if="isTable">
+                <template #head>
+                    <tr>
+                        <th>序号</th>
+                        <th>学生姓名</th>
+                        <th>学号</th>
+                        <th>成绩</th>
+                    </tr>
+                </template>
+                <template #body>
+                    <tr v-for="(item,index) in showData.slice(5,10)" :key="index+item.officialNumber">
+                        <template v-if="item.officialNumber!==0">
+                            <td>{{ (currentPage-1)*10+index+6 }}</td>
+                            <td
+                                @click="$router.push(`/home/studentDetail?classId=${classId}&courseId=${courseId}&stuId=${item.studentId}`)"
+                                @mouseenter="hoverStuId=item.studentId"
+                                @mouseleave="hoverStuId=-1"
+                                :class="{Gethover:hoverStuId===item.studentId,Blue:true}"
+                                >{{ item.stuName }}</td>
+                            <td>{{ item.officialNumber }}</td>
+                            <td @click="$router.push(`/home/assignment/${ShowTheHomeworkId}/correct?studentId=${item.studentId}`)">
+                                <div class="highScore" v-if="item.score>=80"> {{ item.score }}</div>
+                                <div class="midScore" v-else-if="item.score>=60"> {{ item.score }}</div>
+                                <div class="lowScore" v-else-if="item.score>0"> {{ item.score }}</div>
+                                <div class="uncorrected" v-else-if="item.status==='SUBMITTED'"> 未批改</div>
+                                <div class="unsubmitted" v-else-if="item.status==='NOT_SUBMITTED'"> 未提交</div>                
+                            </td>
+                        </template>
+                        <template v-else>
+                            <td>&nbsp;</td>
+                            <td>&nbsp;</td>
+                            <td>&nbsp;</td>
+                            <td>&nbsp;</td>
+                        </template>
+                    </tr>
+                </template>
+                </classTable>
+                <div ref="chartRef2" class="chart chart2" v-else></div>
+            </div>
+        </div>
+        <div class="pages" v-show="DataLoading && isTable">
+            <el-pagination
+                v-model:current-page="currentPage"
+                :page-size="10"
+                :pager-count="5"
+                layout="total, prev, pager, next"
+                :total="searchData.length"
+            />
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import classTable from './components/classTable.vue';
+import classToggle from './components/classToggle.vue'
+import {onMounted, ref, computed, watch, nextTick } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useStore } from "@/store"
+import * as echarts from 'echarts'
+import useApis from "@/apis";
+
+const apis = useApis()
+const store = useStore()
+const route = useRoute()
+const router = useRouter()
+const classId = +route.query.classId;
+const courseId = +route.query.courseId;
+
+const currentPage = ref(1)
+const searchInput = ref("")
+const TheHomework = ref(-1)
+const ShowTheHomework = ref("")
+const ShowTheHomeworkId = ref(-1)
+const TheSort = ref("无")
+const ShowTheSort = ref("默认")
+const hoverStuId = ref(-1)
+
+const StudentsDataLoading = ref(false)
+const HomeworkDataLoading = ref(false)
+const isTable = ref(true)
+
+const Studentsdata = ref([])
+const Homeworksdata = ref([])
+
+
+const sortMethods = ["默认","成绩从低到高","成绩从高到低","未批改优先","未提交优先"]
+
+
+const chartRef1 = ref(null)
+const chartRef2 = ref(null)
+const setChart = () => {
+
+    const chartInstance = echarts.init(chartRef1.value)
+    const chartInstance2 = echarts.init(chartRef2.value)
+    let excellentnum=0, goodnum=0 , failnum=0 , uncorrectednum=0 , unsubmittednum=0
+    Studentsdata.value.forEach((item)=>{
+        if(item.status === "NOT_SUBMITTED"){
+            unsubmittednum++
+        }
+        else if(item.status === "SUBMITTED"){
+            uncorrectednum++
+        }
+        else if(item.score<60){
+            failnum++
+        }
+        else if(item.score<80){
+            goodnum++
+        }
+        else{
+            excellentnum++
+        }
+    })
+    
+    const option = {
+        title: {
+        text: ShowTheHomework.value+'详情',
+        // subtext: '纯属虚构',
+        left: 'center'
+        },
+        tooltip: {
+        trigger: 'item',
+        formatter: '{b}: {d}%'
+        },
+        legend: {
+        orient: 'vertical',
+        left: 'left'
+        },
+        series: [
+        {
+            name: '作业组成',
+            type: 'pie',
+            radius: '50%',
+            data: [
+                { value: excellentnum, name: '优秀', itemStyle: { color: '#7ADBB0' } },
+                { value: goodnum, name: '良好', itemStyle: { color: '#F0C05A' } },
+                { value: failnum, name: '不及格', itemStyle: { color: '#F4A261' } },
+                { value: unsubmittednum, name: '未提交', itemStyle: { color: '#C45C4A' } },
+                { value: uncorrectednum, name: '未批改', itemStyle: { color: '#D9D9D9' } }
+            ],
+            emphasis: {
+            itemStyle: {
+                shadowBlur: 10,
+                shadowOffsetX: 0,
+                shadowColor: 'rgba(0, 0, 0, 0.5)'
+            }
+            }
+        }
+        ]
+    }
+    const option2 = {
+        title: {
+        text: ShowTheHomework.value+'详情',
+        // subtext: '纯属虚构',
+        left: 'center'
+        },
+        tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+            type: 'shadow' // 悬停时显示阴影效果
+        },
+        formatter: '{b}: {c}'
+        },
+        xAxis: {
+            type: 'category',
+            data: ['优秀', '良好', '不及格', '未提交', '未批改']
+        },
+        yAxis: {
+            type: 'value',
+            name: '人数'
+        },
+        series: [
+        {
+            name: '作业组成',
+            type: 'bar',
+            radius: '50%',
+            data: [
+                { value: excellentnum, name: '优秀', itemStyle: { color: '#7ADBB0' } },
+                { value: goodnum, name: '良好', itemStyle: { color: '#F0C05A' } },
+                { value: failnum, name: '不及格', itemStyle: { color: '#F4A261' } },
+                { value: unsubmittednum, name: '未提交', itemStyle: { color: '#C45C4A' } },
+                { value: uncorrectednum, name: '未批改', itemStyle: { color: '#D9D9D9' } }
+            ],
+            barWidth: '40%',
+            emphasis: {
+            itemStyle: {
+                shadowBlur: 10,
+                shadowOffsetX: 0,
+                shadowColor: 'rgba(0, 0, 0, 0.5)'
+            }
+            }
+        }
+        ]
+    }
+
+    chartInstance.setOption(option)
+    chartInstance2.setOption(option2)
+}
+
+
+const identify = () => {
+    if(store.user.role === "STUDENT"){
+        router.push('/404')
+    }
+}
+const getHomeworks = async () => {
+    // console.log(courseId,classId);
+    console.log(ShowTheHomeworkId.value);
+    await apis.getAssignmentByCourseId(courseId)
+        .then((res:any)=>{
+            // console.log('----------------------');
+            // console.log(res.data.data.records);
+            res.data.data.records.forEach(item => Homeworksdata.value.push(item))
+            ShowTheHomework.value = res.data.data.records[0].assignmentName
+            ShowTheHomeworkId.value = res.data.data.records[0].assignmentId
+        })
+    // t.forEach(item => Homeworksdata.value.push(item))
+    HomeworkDataLoading.value = true
+    console.log(ShowTheHomeworkId.value);
+}
+const getStudents = () => {
+    StudentsDataLoading.value=false
+    console.log('+++++++++++++++++++++++');
+    console.log(classId,ShowTheHomeworkId.value);
+    apis.getClassStatesOfCourse(classId,ShowTheHomeworkId.value)
+        .then((res:any)=>{
+            console.log(res.data.data);
+            Studentsdata.value = res.data.data
+            StudentsDataLoading.value = true
+        })
+        .catch((err:any)=>{
+            console.log('-------------------------');
+            console.log(err);
+        })
+}
+const changeHomework = () => {
+    console.log(ShowTheHomework.value)
+    getStudents()
+}
+
+const searchData = computed(()=>{
+    if(searchInput.value===""){
+        return Studentsdata.value
+    }
+    else{
+        return Studentsdata.value.filter(student => {
+          return (
+            student.stuName.includes(searchInput.value) ||
+            student.officialNumber.toString().includes(searchInput.value)
+          )
+        })
+    }
+})
+
+
+const showData = computed(() => {
+    const showdata = searchData.value.slice((currentPage.value-1)*10,currentPage.value*10)
+    while(showdata.length<10){
+        showdata.push(    {
+        "officialNumber": 0,
+        "stuName": "",
+        "status": "",
+        "score": 0,
+        "studentId": -1
+        })
+    }
+    return showdata
+})
+
+const DataLoading = computed(() => {
+  return StudentsDataLoading.value && HomeworkDataLoading.value
+})
+
+watch(TheHomework, () => {
+    nextTick(() => {
+        // console.log(ShowTheHomework.value,TheHomework.value);
+        if(TheHomework.value != -1){
+            let t = Homeworksdata.value.find(item=>item.assignmentId==TheHomework.value)
+            ShowTheHomework.value = t.assignmentName
+            ShowTheHomeworkId.value = t.assignmentId
+            TheHomework.value = -1
+        }
+        // console.log(ShowTheHomework.value,TheHomework.value);
+    });
+});
+watch(TheSort, () => {
+    nextTick(() => {
+        // console.log(ShowTheSort.value,TheSort.value);
+        if(TheSort.value != "无"){
+            ShowTheSort.value = TheSort.value
+            TheSort.value = "无"
+        }
+        // console.log(ShowTheSort.value,TheSort.value);
+    });
+});
+watch(ShowTheSort,(newData) =>{
+    if(newData==="未提交优先"){
+        Studentsdata.value = Studentsdata.value.sort((a,b)=>{
+            return a.status === "NOT_SUBMITTED" ? -1 : b.status === "NOT_SUBMITTED" ? 1 : 0;
+        })
+        console.log("未批改已排序");
+        console.log(Studentsdata.value);
+        
+    }
+    else if(newData==="未批改优先"){
+        Studentsdata.value = Studentsdata.value.sort((a,b)=>{
+            return a.status === "SUBMITTED" ? -1 : b.status === "SUBMITTED" ? 1 : 0;
+        })
+    }
+    else if(newData==="成绩从低到高"){
+        Studentsdata.value = Studentsdata.value.sort((a,b)=>{
+            if (a.status === "CORRECTED" && b.status !== "CORRECTED") return -1;
+            if (a.status !== "CORRECTED" && b.status === "CORRECTED") return 1;
+            return a.score - b.score;
+        })
+    }
+    else if(newData==="成绩从高到低"){
+        Studentsdata.value = Studentsdata.value.sort((a,b)=>{
+            if (a.status === "CORRECTED" && b.status !== "CORRECTED") return -1;
+            if (a.status !== "CORRECTED" && b.status === "CORRECTED") return 1;
+            return b.score - a.score;
+        })
+    }
+})
+watch(isTable, (newVal) => {
+    if(newVal === false){
+        nextTick(()=>{
+            console.log('chartRef1 dom:', chartRef1.value)
+            setChart()
+        })
+    }
+})
+onMounted(async () => {
+    await getHomeworks(); 
+    if (ShowTheHomeworkId.value) {
+        await getStudents();
+    } else {
+        console.warn('未获取到作业 ID,无法加载学生数据');
+    }
+});
+</script>
+
+<style scoped>
+.main{
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    height: 100%;
+    .search{
+    height: 50px;
+    width: 100%;
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-start;
+    align-items: center;
+    margin-top: 20px;
+    .searchInput{
+        height: 100%;
+        display: flex;
+        flex-direction: row;
+        justify-content: flex-start;
+        align-items: center;
+        margin-left: 20px;
+        input{
+            border: 2px solid #e4e7ed;
+            border-right: none;
+            height: 27px;
+            border-radius: 7px 0 0 7px;
+            padding: 0 10px;
+            color: #606266;
+        }
+        input::placeholder {
+            color:#c2c4c9;
+        }
+        .fix{
+            background-color: #f5f7fa;
+            padding: 2px 10px;
+            color:#b8b8b8;
+            font-weight: 600;
+            border: 2px solid #e4e7ed;
+            border-radius: 0 7px 7px 0;
+        }
+    }
+    .Myselect{
+        margin-left: 15px;
+        display: flex;
+        flex-direction: row;
+        select {
+            border: none;
+            background-color: #4a90c4;
+            font-size: 14px;
+            font-weight: 600;
+            color:#fff;
+            padding: 3px 10px;
+            padding-right: 0;
+            border-radius: 5px 0 0 5px;
+            max-width: 100px;
+        }
+        select:focus {
+            outline: none;
+            border: none;
+            background-color: #FFF;
+            border: 3px solid #4a90c4;
+            border-right: 0;
+            color:#4a90c4;
+        }
+        .show{
+            width: fit-content;
+            color: #4a90c4;
+            border: 3px solid #4a90c4;
+            font-weight: 600;
+            padding-left: 6px;
+            padding-right: 5px;
+            border-radius: 0 5px 5px 0;
+        }
+    }
+}
+.content{
+    width: 100%;
+    flex-grow: 1;
+    display: flex;
+    flex-direction: row;
+    gap: 40px;
+    .half-table{
+        width: 50%;
+        height: 100%;
+        .highScore{
+            padding: 2px 10px;
+            background-color: #7ADBB0;   
+            color: #fff;
+            border-radius: 20px;
+            width: fit-content;
+            margin: auto;
+        }
+        .midScore{
+            padding: 2px 10px;
+            background-color: #F0C05A;   
+            color: #fff;
+            border-radius: 20px;
+            width: fit-content;
+            margin: auto;
+        }
+        .lowScore{
+            padding: 2px 10px;
+            background-color: #F4A261;   
+            color: #fff;
+            border-radius: 20px;
+            width: fit-content;
+            margin: auto;
+        }
+        .unsubmitted{
+            padding: 2px 10px;
+            background-color: #C45C4A;   
+            color: #fff;
+            border-radius: 20px;
+            width: fit-content;
+            margin: auto;
+        }
+        .uncorrected{
+            padding: 2px 10px;
+            background-color: #D9D9D9;   
+            color: #fff;
+            border-radius: 20px;
+            width: fit-content;
+            margin: auto;
+        }
+        .chart{
+            margin-top: 70px;
+            margin-left: 30px;
+            height: 300px;
+            width: 100%;
+        }
+        .chart2{
+            margin-left: 0;
+            margin-right: 30px;
+            padding-bottom: 50px;
+        }
+    }
+    .Loading{
+        font-size: 40px;
+        color: #70bbdb;
+        font-weight: 700;
+        height: 100%;
+        width: 100%;
+        text-align: center;
+        align-content: center;
+    }
+}
+.pages{
+    height: 30px;
+    margin-top: 10px;
+}
+}
+</style>

+ 480 - 0
src/views/ClassPage/ClassStudent.vue

@@ -0,0 +1,480 @@
+<template>
+    <div class="main">
+        <div class="search">
+            <img src="@/assets/search.svg" width="20px" height="20px"></img> 
+            <div class="searchInput">
+                <input type="text" v-model.trim="searchInput" placeholder="输入姓名或学号">
+                <div class="fix">
+                    <span>搜索</span>
+                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#b8b8b8" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
+                </div>
+            </div>
+
+            <!-- <button @click="visible_add=true"> 添加学生 </button> -->
+
+            <classToggle v-model:isLeft="isSingleDelete">
+                <template #left>
+                    单个删除
+                </template>
+                <template #right>
+                    批量删除
+                </template>
+            </classToggle>
+
+            <button v-show="!isSingleDelete" class="Del" @click="DeleteStudents()">确认删除</button>
+        </div>
+        <div class="content">
+            <div class="Loading" v-show="!DataLoading">Loading...</div>
+            <classTable v-show="DataLoading">
+                <template #head>
+                    <tr>
+                        <th>序号</th>
+                        <th>学生姓名</th>
+                        <th>学号</th>
+                        <!-- <th>是否加入班级</th> -->
+                        <th>
+                                <span v-show="!isSingleDelete" style="margin-right: 5px;">全选</span>
+                                <input 
+                                    type="checkbox"
+                                    v-show="!isSingleDelete"
+                                    v-model="allDeleted"
+                                >
+                        </th>
+                    </tr>
+                </template>
+                <template #body>
+                    <tr v-for="(item,index) in showData" :key="index+item.officialNumber">
+                        <template v-if="item.officialNumber!==0">
+                            <td>{{ (currentPage-1)*5+index+1 }}</td>
+                            <td  
+                                @click="$router.push(`/home/studentDetail?classId=${classId}&courseId=${courseId}&stuId=${item.studentId}`)"
+                                @mouseenter="hoverStuId=item.officialNumber" 
+                                @mouseleave="hoverStuId=-1"
+                                :class="{Gethover:hoverStuId===item.officialNumber,Blue:true}"
+                                >{{ item.stuName }}</td>
+                            <td>{{ item.officialNumber }}</td>
+                            <!-- <td>
+                                <div class="added" v-if="item.stateCode===1">已加入 </div>
+                                <div class="notadd" v-else> 未加入</div>                        
+                            </td> -->
+                            <td>
+                                <button v-show="isSingleDelete" class="Del" @click="handleDelete(item.officialNumber)">DEL</button>
+                                <input 
+                                    type="checkbox"
+                                    v-show="!isSingleDelete" 
+                                    :value="item.officialNumber" 
+                                    v-model="deleteIds"
+                                    @change="console.log(deleteIds)"
+                                >
+                            </td>
+                        </template>
+                        <template v-else>
+                            <td>&nbsp;</td>
+                            <td>&nbsp;</td>
+                            <td>&nbsp;</td>
+                            <td>&nbsp;</td>
+                            <td>&nbsp;</td>
+                        </template>
+                    </tr>
+                </template>
+            </classTable>
+        </div>
+        <div class="pages">
+            <el-pagination
+                v-model:current-page="currentPage"
+                :page-size="5"
+                :pager-count="5"
+                layout="total, prev, pager, next"
+                :total="searchData.length"
+            />
+        </div>
+    </div>
+
+    <div v-if="visible_del" class="modal-mask" @click.self="isloading || (visible_del=false)" >
+        <div class="modal-container">
+            <div class="modal-header">
+                <h2>{{isloading?"删除中...":"你确定要删除 "+deleteName+":"+deleteId+" 吗?"}}</h2>
+            </div>
+            <div class="modal-footer">
+                <button class="yes-button" @click.stop="DeleteStudent" :disabled="isloading">确定</button>
+                <button class="no-button" @click.stop="visible_del=false;" :disabled="isloading">取消</button>
+            </div>
+        </div>
+    </div>
+
+    <div v-if="visible_add" class="modal-mask" @click.self="isloading_add || (visible_add=false)" >
+        <div class="modal-container">
+            <div class="add1" v-show="!isloading_add">
+                <h2>学生姓名:</h2> <input type="text" v-model.trim="addStudentName" ></input>
+            </div>
+            <div class="add1" v-show="!isloading_add">
+                <h2>学生学号:</h2> <input type="text" v-model.trim="addOfficalNumber" ></input>
+            </div>
+            <div class="add1" v-show="isloading_add" 
+            :style="{'height': '66%', 'margin-left':'210px'}">
+                <h2>上传中...</h2>
+            </div>
+            <div class="modal-footer add2">
+                <button class="yes-button" @click.stop="AddStudent" :disabled="isloading_add">确定</button>
+                <button class="no-button" @click.stop="visible_add=false" :disabled="isloading_add">取消</button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import classTable from './components/classTable.vue';
+import classToggle from './components/classToggle.vue'
+import {onMounted, reactive, ref, computed} from 'vue';
+import { useRoute } from 'vue-router';
+import type {ClassStudentVO} from "@/types/vo";
+import useApis from "@/apis";
+import { get } from 'http';
+
+const route = useRoute()
+const apis = useApis()
+const classId = +route.query.classId;
+const courseId = +route.query.courseId;
+
+const currentPage = ref(1)
+const searchInput = ref("")
+const visible_add = ref(false)
+const visible_del = ref(false)
+const isloading = ref(false)
+const isloading_add = ref(false)
+const addStudentName = ref("")
+const addOfficalNumber = ref("")
+const deleteId = ref(-1)
+const deleteIds = ref<number[]>([])
+const deleteName = ref("")
+const hoverStuId = ref(-1)
+const isSingleDelete = ref(true)
+
+const StudentsDataLoading = ref(false)
+
+const Studentsdata = reactive<ClassStudentVO[]>([])
+
+const handleDelete = (sid) => {
+    visible_del.value = true
+    deleteId.value = sid
+    deleteName.value = Studentsdata.find((item)=>{
+        return item.officialNumber === sid
+    }).stuName
+}
+const DeleteStudent = async () =>{
+    isloading.value = true
+    await apis.deleteStudentOfClass(classId,[deleteId.value])
+        .then((res:any)=>{console.log(res);
+        })
+    visible_del.value=false
+    isloading.value = false
+    getStudents()
+}
+const DeleteStudents = async () => {
+    isloading.value = true
+    visible_del.value = true
+    await apis.deleteStudentOfClass(classId,deleteIds.value)
+        .then((res:any)=>{console.log(res);
+        })
+    deleteIds.value=[]
+    visible_del.value=false
+    isloading.value = false
+    getStudents()
+}
+const AddStudent = async () => {
+    isloading_add.value = true
+    console.log(addStudentName.value)
+    console.log(+addOfficalNumber.value)
+    console.log(classId);
+    await apis.addStudentOfClasss(classId,+addOfficalNumber.value)
+    isloading_add.value = false
+    visible_add.value=false
+    addStudentName.value = ""
+    addOfficalNumber.value = "" 
+    console.log("添加成功");
+    getStudents()
+    // setTimeout(()=>{
+    //     isloading_add.value = false
+    //     visible_add.value=false
+    //     addStudentName.value = ""
+    //     addOfficalNumber.value = "" 
+    // }, 1000)
+}
+
+const getStudents = () => {
+    apis.getStudentsOfClass(classId)
+        .then((res:any) => {
+            Studentsdata.length=0
+            console.log(res.data.data);
+            res.data.data.forEach(item => Studentsdata.push(item))
+            console.log(res.data);
+            StudentsDataLoading.value = true
+        })
+    
+}
+
+const searchData = computed(()=>{
+    if(searchInput.value===""){
+        return Studentsdata
+    }
+    else{
+        return Studentsdata.filter(student => {
+          return (
+            student.stuName.includes(searchInput.value) ||
+            student.officialNumber.toString().includes(searchInput.value)
+          )
+        })
+    }
+})
+
+
+const showData = computed(() => {
+    const showdata = searchData.value.slice((currentPage.value-1)*5,currentPage.value*5)
+    while(showdata.length<5){
+        showdata.push(    {
+        "officialNumber": 0,
+        "stuName": "",
+        "stateCode": -1,
+        "stateDesc": "",
+        "studentId": -1
+        })
+    }
+    return showdata
+})
+const DataLoading = computed(() => {
+  return StudentsDataLoading.value
+})
+
+const allDeleted = computed({
+    get() {
+        return deleteIds.value.length === Studentsdata.length
+    },
+    set(newValue) {
+        if(newValue === true){
+            deleteIds.value = <number[]>[]
+            Studentsdata.forEach((item)=>{
+                deleteIds.value.push(+item.officialNumber)
+            })
+        }
+        else{
+            deleteIds.value = <number[]>[]
+        }
+    }
+})
+
+onMounted(() => {
+    getStudents()
+})
+</script>
+
+<style scoped>
+.main{
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    height: 100%;
+}
+.search{
+    height: 50px;
+    width: 100%;
+    margin-top: 20px;
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-start;
+    align-items: center;
+    .searchInput{
+        height: 100%;
+        display: flex;
+        flex-direction: row;
+        justify-content: flex-start;
+        align-items: center;
+        margin-left: 20px;
+        input{
+            border: 2px solid #e4e7ed;
+            border-right: none;
+            height: 27px;
+            border-radius: 7px 0 0 7px;
+            padding: 0 10px;
+            color: #606266;
+        }
+        input::placeholder {
+            color:#c2c4c9;
+        }
+        .fix{
+            background-color: #f5f7fa;
+            padding: 2px 10px;
+            color:#b8b8b8;
+            font-weight: 600;
+            border: 2px solid #e4e7ed;
+            border-radius: 0 7px 7px 0;
+        }
+        
+    }
+    button{
+        background-color: #4a90c4;
+        font-size: 14px;
+        font-weight: 700;
+        border: none;
+        padding: 4px 10px;
+        color: #fff;
+        margin-left: 30px;
+        border-radius: 5px;
+        cursor: pointer;
+    }
+}
+.content{
+    width: 100%;
+    flex-grow: 1;
+    .Loading{
+        font-size: 40px;
+        color: #70bbdb;
+        font-weight: 700;
+        height: 100%;
+        text-align: center;
+        align-content: center;
+    }
+}
+.Del{
+    font-size: 14px;
+    padding: 1px 12px;
+    background-color: #c45c4a;
+    color: #fff;
+    border: none;
+    border-radius: 20px;
+    cursor: pointer;
+}
+.pages{
+    height: 30px;
+    margin-top: 10px;
+}
+.added{
+    color: #fff;
+    background-color: #7adbb0;
+    width: 62px;
+    height: 20px;
+    margin: auto;
+    border-radius: 20px;
+    font-weight: 600;
+}
+.notadd{
+    color: #fff;
+    background-color: #d9d9d9;
+    width: 62px;
+    height: 20px;
+    margin: auto;
+    border-radius: 20px;
+    font-weight: 600;
+}
+input[type="checkbox"]{
+    margin-bottom: -2px;
+    appearance: none; /* 隐藏默认样式 */
+    -webkit-appearance: none;
+    width: 15px;
+    height: 15px;
+    border: 2px solid #4a90c4; /* 边框 */
+    border-radius: 4px;    
+}
+input[type="checkbox"]:checked {
+  background-color: #4a90c4; /* 绿色 */
+  border-color: #4a90c4;
+}
+.modal-mask {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 9999;
+    .modal-container {
+        border-radius: 30px;
+        border: 7px solid #70BBDB;
+        background: #FFF;
+        box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);
+        width: 35vw;
+        height: 25vh;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+        .modal-header {
+            padding: 16px 20px;
+            height: 50%;
+            display: flex;
+            flex-direction: row;
+            justify-content: center;
+            align-items: center;
+            color:#70bbdb
+        }
+        .modal-footer {
+            padding: 12px 20px;
+            height: 50%;
+            display: flex;
+            flex-direction: row;
+            justify-content: center;
+            align-items: center;
+            gap: 50px;
+            .yes-button{
+                background-color: #c45c4a;
+                color: #fff;
+                border: 0;
+                padding: 3px 25px;
+                font-size: 22px;
+                font-weight: 600;
+                border-radius: 20px;
+                margin-right: 20px;
+            }
+            .no-button{
+                background-color: #4a90c4;
+                color: #fff;
+                border: 0;
+                padding: 3px 25px;
+                font-size: 22px;
+                font-weight: 600;
+                border-radius: 20px;
+                margin-right: 20px;
+            }
+            button{
+                cursor: pointer;
+            }
+            button:disabled{
+                background-color: #d9d9d9;
+            }
+        }
+        .add1 {
+            height: 33%;
+            display: flex;
+            flex-direction: row;
+            justify-content: start;
+            align-items: center;
+            color:#70bbdb;
+            margin-left: 80px;
+            input,select {
+                border: none;
+                border-bottom: 3px solid #70bbdb;
+                font-size: 16px;
+                font-weight: 600;
+                color:#70bbdb
+            }
+            input::placeholder {
+                color:#b2d7e7
+            }
+            input:focus,select:focus {
+                outline: none;
+                border: none;
+                outline: 5px solid #70bbdb;
+            }
+            span{
+                font-size: 16px;
+                font-weight: 600;
+                margin: 0 15px;
+            }
+        }
+        .add2 {
+            height: 33%;
+        }
+    }   
+}
+</style>

+ 421 - 0
src/views/ClassPage/CourseClass.vue

@@ -0,0 +1,421 @@
+<template>
+    <div class="course-class" v-show="DataLoading">
+        <classHeader>{{Coursedata.courseName}}</classHeader>
+        <div class="content scroll">
+            <a v-for="(item) in data" :key="item.classId" 
+                class="content-item" 
+                @mouseenter="activeId=item.classId" 
+                @mouseleave="activeId=-1" 
+                :class="{'shadow_unactive':item.classId!==activeId,'shadow_active':item.classId===activeId}"
+                @click="{$event.preventDefault();$router.push(`/home/classDetail?classId=${item.classId}&courseId=${item.courseId}`)}"
+            >
+                <h2>{{ item.className }}</h2>
+                <div class="svg-text">
+                    <img src="@/assets/people.svg" width="24px" height="24px"></img> 
+                    <span>:</span>
+                    <span>{{ item.stuNumber }}</span>
+                </div>
+                <div class="svg-text">
+                    <img src="@/assets/time.svg" width="24px" height="24px"></img> 
+                    <span>:</span>
+                    <span>{{ item.classTime }}</span>
+                </div>
+                <div class="svg-text">
+                    <img src="@/assets/plus-square.svg" width="24px" height="24px"></img> 
+                    <span>:</span>
+                    <span>{{item.classCode}}</span>
+                </div>
+                <div class="del">
+                    <button :disabled="item.stuNumber!==0"
+                            @click.stop="handleDelete(item.classId)"
+                            :title="item.stuNumber!==0 ? '仍有学生剩余无法直接删除班级' : ''"
+                    >
+                     Del
+                    </button>
+                </div>
+            </a>
+            <a class="plus"
+                @mouseenter="activeId=-2" 
+                @mouseleave="activeId=-1" 
+                @click="visible_add=true"
+                :class="{'shadow_unactive':-2!==activeId,'shadow_active':-2===activeId}">
+                <img src="@/assets/add.svg"></img> 
+            </a>
+        </div>
+    </div>
+
+    <div v-if="visible_del" class="modal-mask" @click.self="isloading || (visible_del=false)" >
+        <div class="modal-container">
+            <div class="modal-header">
+                <h2>{{isloading?"删除中...":"你确定要删除"+deleteName+"吗?"}}</h2>
+            </div>
+            <div class="modal-footer">
+                <button class="yes-button" @click.stop="DeleteClass" :disabled="isloading">确定</button>
+                <button class="no-button" @click.stop="visible_del=false;" :disabled="isloading">取消</button>
+            </div>
+        </div>
+    </div>
+
+    <div v-if="visible_add" class="modal-mask" @click.self="isloading_add || (visible_add=false)" >
+        <div class="modal-container">
+            <div class="add1" v-show="!isloading_add">
+                <h2>班级名称:</h2> <input type="text" v-model.trim="addClassName" placeholder="班级名称建议小于6个字"></input>
+            </div>
+            <div class="add1" v-show="isloading_add" 
+            :style="{'height': '66%', 'margin-left':'210px'}">
+                <h2>上传中...</h2>
+            </div>
+            <div class="add1" v-show="!isloading_add">
+                <h2>上课时间:</h2>
+                <select id="addWeekdays" v-model="addClassDay">
+                    <option value="">Weekdays</option>
+                    <option value="Mon">Mon</option>
+                    <option value="Tue">Tue</option>
+                    <option value="Wed">Wed</option>
+                    <option value="Thu">Thu</option>
+                    <option value="Tue">Fri</option>
+                    <option value="Wed">Sat</option>
+                    <option value="Thu">Sun</option>
+                </select>
+                <span>-</span>
+                <select id="addTime" v-model="addClassClock">
+                    <option value="">Time</option>
+                    <option value="08:00">08:00</option>
+                    <option value="10:10">10:10</option>
+                    <option value="14:00">14:00</option>
+                    <option value="16:10">16:10</option> 
+                </select>
+            </div>
+            <div class="modal-footer add2">
+                <button class="yes-button" @click.stop="AddClass" :disabled="isloading_add">确定</button>
+                <button class="no-button" @click.stop="visible_add=false" :disabled="isloading_add">取消</button>
+            </div>
+        </div>
+    </div>
+
+
+</template>
+
+<script setup lang="ts">
+import {onMounted, reactive, ref, computed } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useStore } from "@/store"
+import classHeader from './components/classHeader.vue';
+import type {CourseDetailVO, ClassVO} from "@/types/vo";
+import useApis from "@/apis";
+
+const route = useRoute()
+const store = useStore()
+const router = useRouter()
+const apis = useApis()
+const courseId = +route.params.courseId;
+
+const data = reactive<ClassVO[]>([])
+const Coursedata = reactive<CourseDetailVO>({
+  courseId: null,
+  courseName: '',
+  teacherIds: [],
+  teacherNames: [],
+  courseImages: [''],
+  targetGrade: '',
+  startTime: '',
+  endTime: '',
+  createTime: '',
+  description: '',
+})
+let activeId = ref(-1)
+let isloading = ref(false)
+let isloading_add = ref(false)
+let visible_del = ref(false)
+let visible_add = ref(false)
+let deleteName = ref("")
+let deleteId = ref(-1)
+let addClassName = ref("")
+let addClassDay = ref("")
+let addClassClock = ref("")
+
+let CourseDataLoading = ref(false)
+let ClassDataLoading = ref(false)
+
+const handleDelete = (cid) => {
+    visible_del.value = true
+    deleteId.value = cid
+    deleteName.value = data.find((item)=>{
+        return item.classId === cid
+    }).className
+}
+const DeleteClass = async () =>{
+    isloading.value = true
+    console.log(deleteId.value);
+    await apis.deleteClass(deleteId.value)
+    visible_del.value=false
+    isloading.value = false
+    getClass()
+}
+const AddClass = async () =>{
+    if(addClassName.value!==""&&addClassDay.value!=""&&addClassClock.value!=""){
+        isloading_add.value = true
+        await apis.createClassOfCourse(courseId,addClassName.value,addClassDay.value+" "+addClassClock.value,Coursedata.teacherIds[0])
+        isloading_add.value = false
+        visible_add.value=false
+        addClassName.value = ""
+        addClassDay.value = ""
+        addClassClock.value = ""    
+        getClass()
+    }
+    else{
+        alert("名称或时间为空,添加失败")
+    }
+}
+const identify = () => {
+    if(store.user.role === "STUDENT"){
+        router.push('/404')
+    }
+}
+const getClass = () => {
+    // console.log(courseId);
+    data.length=0
+    apis.getClassOfCourse(courseId)
+        .then((res: any)=>{
+            console.log("getclass:",res.data.data)
+            res.data.data.forEach(item => data.push(item))
+            ClassDataLoading.value = true
+        })
+        .catch((err:any) => {
+            console.error(err)
+        })
+}
+const getCourse = () => {
+  // 获取课程信息
+  apis.getCourseByCourseId(courseId)
+      .then((res: any) => {
+        let data = res.data.data.records[0]
+        Object.assign(Coursedata, {...data})
+        CourseDataLoading.value = true
+      })
+      .catch((err: any) => {
+        console.error(err);
+      })
+}
+const DataLoading = computed(() => {
+  return CourseDataLoading.value && ClassDataLoading.value
+})
+
+onMounted(() => {
+    identify()
+    getClass()
+    getCourse()
+})
+</script>
+
+<script lang="ts">
+export default {
+  name: "CourseClass"
+}
+</script>
+
+<style scoped>
+.course-class {
+    width: 66vw;
+    height: 65vh;
+    background-color: #fff;
+    border-radius: 15px;
+    flex-shrink: 0;
+    margin: 0 auto;
+    margin-top: 4.6vh;
+    border-bottom: 1px solid #70bbdb;
+    padding:  7vh 5.5vw;
+}
+.content {
+    height: 50vh;
+    width: 100%;
+    /* border:1px solid; */
+    margin: 0 auto;
+    margin-top: 30px;
+    margin-bottom: 10vh;
+    display: flex;
+    align-items: center;
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: start;
+    padding: 10px;
+    gap: 30px;
+    overflow: auto;
+    .content-item{
+        width: 160px;
+        height: 260px;
+        border-radius: 25px;
+        border: 20px solid #70bbdb; 
+        cursor: pointer;
+        transition: all 0.3s ease;
+        margin-left: 20px;
+        margin-bottom: 20px;
+        h2 {
+            color: #4a90c4;
+            margin-left: 15px;
+            margin-right: 15px;
+            margin-top: 25px;
+            white-space: nowrap;      /* 强制不换行 */
+            overflow: hidden;        /* 隐藏超出部分 */
+            text-overflow: ellipsis;
+        }
+        .svg-text{
+            display: flex;
+            align-items: center;
+            font-size: 16px;
+            color:#4a90c4;
+            font-weight: 600;
+            margin:20px 10px;
+            margin-left: 15px;
+            span {
+                margin-left: 10px;
+            }
+        }
+        .del{
+            display: flex;
+            flex-direction: row;
+            justify-content: end;
+            button{
+                background-color: #c45c4a;
+                color: #fff;
+                border: 0;
+                padding: 3px 20px;
+                font-size: 18px;
+                font-weight: 600;
+                border-radius: 20px;
+                margin-right: 20px;
+            }
+            button:disabled{
+                background-color: #d9d9d9;
+            }
+        }
+    }
+    a:active{
+        box-shadow: none;
+        border: 25px solid #4a90c4; 
+    }
+    .plus{
+        width: 160px;
+        height: 260px;
+        border-radius: 25px;
+        border: 20px solid #70bbdb; 
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        cursor: pointer;
+        transition: all 0.3s ease;
+        margin-left: 30px;
+        margin-bottom: 20px;
+    }
+}
+.shadow_unactive{
+    box-shadow: -20px 20px 20px 0 rgba(0, 0, 0, 0.25);
+}
+.shadow_active{
+    box-shadow: -20px 20px 20px 0 #B9DDF5;
+}
+.scroll{
+    scrollbar-width: thin;
+    scrollbar-color: rgba(0, 123, 255, 0.2) transparent;
+}
+
+.modal-mask {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 9999;
+    .modal-container {
+        border-radius: 30px;
+        border: 7px solid #70BBDB;
+        background: #FFF;
+        box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);
+        width: 35vw;
+        height: 25vh;
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+        .modal-header {
+            padding: 16px 20px;
+            height: 50%;
+            display: flex;
+            flex-direction: row;
+            justify-content: center;
+            align-items: center;
+            color:#70bbdb
+        }
+        .modal-footer {
+            padding: 12px 20px;
+            height: 50%;
+            display: flex;
+            flex-direction: row;
+            justify-content: center;
+            align-items: center;
+            gap: 50px;
+            .yes-button{
+                background-color: #c45c4a;
+                color: #fff;
+                border: 0;
+                padding: 3px 25px;
+                font-size: 22px;
+                font-weight: 600;
+                border-radius: 20px;
+                margin-right: 20px;
+            }
+            .no-button{
+                background-color: #4a90c4;
+                color: #fff;
+                border: 0;
+                padding: 3px 25px;
+                font-size: 22px;
+                font-weight: 600;
+                border-radius: 20px;
+                margin-right: 20px;
+            }
+            button{
+                cursor: pointer;
+            }
+            button:disabled{
+                background-color: #d9d9d9;
+            }
+        }
+        .add1 {
+            height: 33%;
+            display: flex;
+            flex-direction: row;
+            justify-content: start;
+            align-items: center;
+            color:#70bbdb;
+            margin-left: 80px;
+            input,select {
+                border: none;
+                border-bottom: 3px solid #70bbdb;
+                font-size: 16px;
+                font-weight: 600;
+                color:#70bbdb
+            }
+            input::placeholder {
+                color:#b2d7e7
+            }
+            input:focus,select:focus {
+                outline: none;
+                border: none;
+                outline: 5px solid #70bbdb;
+            }
+            span{
+                font-size: 16px;
+                font-weight: 600;
+                margin: 0 15px;
+            }
+        }
+        .add2 {
+            height: 33%;
+        }
+    }   
+}
+</style>

+ 12 - 0
src/views/ClassPage/StudentClass.vue

@@ -0,0 +1,12 @@
+<template>
+    <div>
+        <h1>学生班级</h1>
+    </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style scoped>
+</style>

+ 531 - 0
src/views/ClassPage/StudentDetail.vue

@@ -0,0 +1,531 @@
+<template>
+    <div class="course-class" v-show="DataLoading">
+        <classHeader>{{store.user.role === 'STUDENT'?Coursedata.courseName:Studentdata.stuName}}:{{store.user.role === 'STUDENT'?Classdata.className: Studentdata.officialNumber }} 
+            <template #TeacherName v-if="store.user.role === 'STUDENT'">{{ Coursedata.teacherNames[0] }}</template>
+        </classHeader>
+        <div class="content">
+            <div class="search">
+                <img src="@/assets/search.svg" width="20px" height="20px"></img> 
+                <div class="searchInput">
+                    <input type="text" v-model.trim="searchInput" placeholder="输入作业序号或作业名称">
+                    <div class="fix">
+                        <span>搜索</span>
+                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#b8b8b8" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
+                    </div>
+                </div>
+                <div class="Myselect">
+                    <select id="selectSort" v-model="TheSort" > 
+                        <option value="无">排序方式</option>
+                        <option v-for="item in sortMethods" :value="item">{{item}}</option>
+                    </select>
+                    <div class="show">
+                            {{ ShowTheSort }}
+                    </div>
+                </div>
+            </div>
+            <div class="table">
+                <div class="table-half left-half">
+                    <classTable>
+                        <template #head>
+                            <tr>
+                                <th>序号</th>
+                                <th>作业列表</th>
+                                <th>成绩</th>
+                            </tr>
+                        </template>
+                        <template #body>
+                            <tr v-for="(item,index) in showData.slice(0,5)" :key="index+item.assignmentName">
+                                <template v-if="item.status!==''">
+                                    <td>{{ (currentPage-1)*10+index+1 }}</td>
+                                    <td>{{ item.assignmentName }}</td>
+                                    <td @click="$router.push(`/home/assignment/${item.assignmentId}/correct?studentId=${studentId}`)">
+                                        <div class="highScore" v-if="item.score>=80"> {{ item.score }}</div>
+                                        <div class="midScore" v-else-if="item.score>=60"> {{ item.score }}</div>
+                                        <div class="lowScore" v-else-if="item.score>0"> {{ item.score }}</div>
+                                        <div class="uncorrected" v-else-if="item.status==='SUBMITTED'"> 未批改</div>
+                                        <div class="unsubmitted" v-else-if="item.status==='NOT_SUBMITTED'"> 未提交</div>      
+                                    </td>
+                                </template>
+                                <template v-else>
+                                    <td>&nbsp;</td>
+                                    <td>&nbsp;</td>
+                                    <td>&nbsp;</td>
+                                </template>
+                            </tr>
+                        </template>
+                    </classTable>
+                </div>
+                <div class="table-half right-half">
+                    <classTable>
+                        <template #head>
+                            <tr>
+                                <th>序号</th>
+                                <th>作业列表</th>
+                                <th>成绩</th>
+                            </tr>
+                        </template>
+                        <template #body>
+                            <tr v-for="(item,index) in showData.slice(5,10)" :key="index+item.assignmentName">
+                                <template v-if="item.status!==''">
+                                    <td>{{ (currentPage-1)*10+index+6 }}</td>
+                                    <td>{{ item.assignmentName }}</td>
+                                    <td @click="$router.push(`/home/assignment/${item.assignmentId}/correct?studentId=${studentId}`)">
+                                        <div class="highScore" v-if="item.score>=80"> {{ item.score }}</div>
+                                        <div class="midScore" v-else-if="item.score>=60"> {{ item.score }}</div>
+                                        <div class="lowScore" v-else-if="item.score>0"> {{ item.score }}</div>
+                                        <div class="uncorrected" v-else-if="item.status==='SUBMITTED'"> 未批改</div>
+                                        <div class="unsubmitted" v-else-if="item.status==='NOT_SUBMITTED'"> 未提交</div>      
+                                    </td>
+                                </template>
+                                <template v-else>
+                                    <td>&nbsp;</td>
+                                    <td>&nbsp;</td>
+                                    <td>&nbsp;</td>
+                                </template>
+                            </tr>
+                        </template>
+                    </classTable>
+                </div>
+            </div>
+            <div class="pages">
+                <el-pagination
+                    v-model:current-page="currentPage"
+                    :page-size="10"
+                    :pager-count="5"
+                    layout="total, prev, pager, next"
+                    :total="searchData.length"
+                />
+            </div>
+        </div>
+    </div>
+    <div class="loading" v-show="!DataLoading"> Loading...</div>
+</template>
+
+<script setup lang="ts">
+import classTable from './components/classTable.vue';
+import {onMounted, reactive, ref, computed, watch, nextTick} from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useStore } from "@/store"
+import classHeader from './components/classHeader.vue';
+import type {CourseDetailVO, ClassVO, ClassStudentVO} from "@/types/vo";
+import useApis from "@/apis";
+
+const route = useRoute()
+const store = useStore()
+const router = useRouter()
+const apis = useApis()
+const classId = +route.query.classId
+const courseId = +route.query.courseId
+const studentId = +route.query.stuId
+
+const currentPage = ref(1)
+const searchInput = ref("")
+const TheSort = ref("无")
+const ShowTheSort = ref("默认")
+
+const StudentdataLoading = ref(false)
+const CourseDataLoading = ref(false)
+const ClassDataLoading = ref(false)
+
+const Coursedata = reactive<CourseDetailVO>({
+  courseId: null,
+  courseName: '',
+  teacherIds: [],
+  teacherNames: [],
+  courseImages: [''],
+  targetGrade: '',
+  startTime: '',
+  endTime: '',
+  createTime: '',
+  description: '',
+})
+
+const Classdata = ref<ClassVO>({
+  classId: null,
+  className: '',
+  courseId: null,
+  stuNumber: null,
+  classTime: '',
+  teacherId: null,
+})
+const sortMethods = ["默认","成绩从低到高","成绩从高到低","未批改优先","未提交优先"]
+// const Studentdata = ref({
+//     "officialNumber": 522025312092,
+//     "stuName": "张伟",
+//     "studentId": studentId,
+//     "assignments": [
+//         {
+//             "assignmentId": 73,
+//             "assignmentName":"第一次作业",
+//             "status": "CORRECTED",
+//             "score": 88
+//         },
+//         {
+//             "assignmentId": 73,
+//             "assignmentName":"第二次作业",
+//             "status": "CORRECTED",
+//             "score": 91
+//         },
+//         {
+//             "assignmentId": 73,
+//             "assignmentName":"第三次作业",
+//             "status": "CORRECTED",
+//             "score": 94
+//         },
+//         {
+//             "assignmentId": 73,
+//             "assignmentName":"期中作业",
+//             "status": "CORRECTED",
+//             "score": 59
+//         },
+//         {
+//             "assignmentId": 73,
+//             "assignmentName":"第五次作业",
+//             "status": "CORRECTED",
+//             "score": 79
+//         },
+//         {
+//             "assignmentId": 73,
+//             "assignmentName":"第六次作业",
+//             "status": "NOT_SUBMITTED",
+//             "score": 0
+//         },
+//         {
+//             "assignmentId": 73,
+//             "assignmentName":"期末考试",
+//             "status": "SUBMITTED",
+//             "score": 0
+//         }
+//     ]
+// })
+
+// const identify = () => {
+//     if(store.user.role === "STUDENT"){
+//         router.push('/404')
+//     }
+// }
+const Studentdata = ref({
+    "officialNumber": 0,
+    "stuName": "",
+    "studentId": 0,
+    "assignments":[]
+})
+const getStudent = () => {
+    StudentdataLoading.value = false
+    console.log(courseId)
+    console.log(studentId)
+    apis.getHomeworkOfStudent(courseId,studentId)
+        .then((res:any)=>{
+            Studentdata.value = res.data.data
+            console.log(res.data.data);
+            StudentdataLoading.value = true
+        })
+}
+const getClass = () => {
+    console.log("classId:",classId)
+    apis.getClassByClassId(classId)
+        .then((res:any)=>{
+            console.log(res.data.data);
+            Classdata.value = res.data.data
+            console.log("Classdata",Classdata.value);
+            
+        })
+
+    ClassDataLoading.value = true
+}
+
+const getCourse = () => {
+  // 获取课程信息
+  apis.getCourseByCourseId(courseId)
+      .then((res: any) => {
+        let data = res.data.data.records[0]
+        Object.assign(Coursedata, {...data})
+        CourseDataLoading.value = true
+      })
+      .catch((err: any) => {
+        console.error(err);
+      })
+}
+
+const DataLoading = computed(() => {
+//   return CourseDataLoading.value && ClassDataLoading.value
+    if(store.user.role === "STUDENT"){
+        return StudentdataLoading.value && CourseDataLoading.value && ClassDataLoading.value
+    }
+    else{
+        return StudentdataLoading.value
+    }
+})
+const searchData = computed(()=>{
+    if(searchInput.value===""){
+        return Studentdata.value.assignments
+    }
+    else{
+        return Studentdata.value.assignments.filter(student => {
+          return (
+            student.assignmentName.includes(searchInput.value)
+          )
+        })
+    }
+})
+const showData = computed(() => {
+    const showdata = searchData.value.slice((currentPage.value-1)*10,currentPage.value*10)
+    while(showdata.length<10){
+        showdata.push(    {
+        "assignmentId": -1,
+        "assignmentName": "",
+        "status": "",
+        "score": 0
+        })
+    }
+    return showdata
+})
+watch(TheSort, () => {
+    nextTick(() => {
+        // console.log(ShowTheSort.value,TheSort.value);
+        if(TheSort.value != "无"){
+            ShowTheSort.value = TheSort.value
+            TheSort.value = "无"
+        }
+        // console.log(ShowTheSort.value,TheSort.value);
+    });
+});
+watch(ShowTheSort,(newData) =>{
+    if(newData==="未批改优先"){
+        Studentdata.value.assignments = Studentdata.value.assignments.sort((a,b)=>{
+            return a.status === "SUBMITTED" ? -1 : b.status === "SUBMITTED" ? 1 : 0;
+        })
+    }
+    else if(newData==="未提交优先"){
+        Studentdata.value.assignments = Studentdata.value.assignments.sort((a,b)=>{
+            return a.status === "NOT_SUBMITTED" ? -1 : b.status === "NOT_SUBMITTED" ? 1 : 0;
+        })
+    }
+    else if(newData==="成绩从低到高"){
+        Studentdata.value.assignments = Studentdata.value.assignments.sort((a,b)=>{
+            if (a.status === "CORRECTED" && b.status !== "CORRECTED") return -1;
+            if (a.status !== "CORRECTED" && b.status === "CORRECTED") return 1;
+            return a.score - b.score;
+        })
+    }
+    else if(newData==="成绩从高到低"){
+        Studentdata.value.assignments = Studentdata.value.assignments.sort((a,b)=>{
+            if (a.status === "CORRECTED" && b.status !== "CORRECTED") return -1;
+            if (a.status !== "CORRECTED" && b.status === "CORRECTED") return 1;
+            return b.score - a.score;
+        })
+    }
+})
+
+onMounted(() => {
+    // identify()
+    if(store.user.role === "STUDENT"){
+        getCourse()
+        getClass()
+    }
+    getStudent()
+    
+})
+</script>
+
+<script lang="ts">
+export default {
+  name: "ClassDetail"
+}
+</script>
+
+<style scoped>
+.course-class {
+    width: 66vw;
+    height: 65vh;
+    background-color: #fff;
+    border-radius: 15px;
+    margin: 0 auto;
+    margin-top: 4.6vh;
+    border-bottom: 1px solid #70bbdb;
+    padding:  7vh 5.5vw;
+    .content {
+        height: 55vh;
+        width: 100%;
+        /* border:1px solid; */
+        margin: 0 auto;
+        display: flex;
+        flex-direction: column;
+        justify-content: start;
+        padding: 10px;
+        .search{
+            height: 50px;
+            width: 100%;
+            display: flex;
+            flex-direction: row;
+            justify-content: flex-start;
+            align-items: center;
+            margin-top: 20px;
+            .searchInput{
+                height: 100%;
+                display: flex;
+                flex-direction: row;
+                justify-content: flex-start;
+                align-items: center;
+                margin-left: 20px;
+                input{
+                    border: 2px solid #e4e7ed;
+                    border-right: none;
+                    height: 27px;
+                    border-radius: 7px 0 0 7px;
+                    padding: 0 10px;
+                    color: #606266;
+                }
+                input::placeholder {
+                    color:#c2c4c9;
+                }
+                .fix{
+                    background-color: #f5f7fa;
+                    padding: 2px 10px;
+                    color:#b8b8b8;
+                    font-weight: 600;
+                    border: 2px solid #e4e7ed;
+                    border-radius: 0 7px 7px 0;
+                }
+            }
+            .Myselect{
+                margin-left: 15px;
+                display: flex;
+                flex-direction: row;
+                select {
+                    border: none;
+                    background-color: #4a90c4;
+                    font-size: 14px;
+                    font-weight: 600;
+                    color:#fff;
+                    padding: 3px 10px;
+                    padding-right: 0;
+                    border-radius: 5px 0 0 5px;
+                    max-width: 100px;
+                }
+                select:focus {
+                    outline: none;
+                    border: none;
+                    background-color: #FFF;
+                    border: 3px solid #4a90c4;
+                    border-right: 0;
+                    color:#4a90c4;
+                }
+                .show{
+                    width: fit-content;
+                    color: #4a90c4;
+                    border: 3px solid #4a90c4;
+                    font-weight: 600;
+                    padding-left: 6px;
+                    padding-right: 5px;
+                    border-radius: 0 5px 5px 0;
+                }
+            }
+            .toggle{
+                margin-left: 20px;
+                .left{
+                    border: 3px solid #4A90C4;
+                    border-radius: 20px 0 0 20px;
+                    border-right: 0;
+                    padding: 2px 7px;
+                    font-weight: 700;
+                    cursor: pointer;
+                }
+                .right{
+                    border: 3px solid #4A90C4;
+                    border-radius: 0 20px 20px 0;
+                    padding: 2px 7px;
+                    font-weight: 700;
+                    cursor: pointer;
+                }
+                .unactive{
+                    color: #70BBDB;
+                    background-color: #fff;
+                }
+                .active{
+                    color: #fff;
+                    background-color:#4A90C4;
+                }
+            }
+        }
+        .table{
+            flex-grow: 1;
+            width: 100%;
+            display: flex;
+            flex-direction: row;
+            margin-top: 20px;
+            .table-half{
+                width: 50%;
+                height: 100%;
+                border: 5px solid #70bbdb;
+                padding: 15px;
+                padding-top: 0;
+                td{
+                    height:45px;
+                }
+                .highScore{
+                    padding: 2px 10px;
+                    background-color: #7ADBB0;   
+                    color: #fff;
+                    border-radius: 20px;
+                    width: fit-content;
+                    margin: auto;
+                }
+                .midScore{
+                    padding: 2px 10px;
+                    background-color: #F0C05A;   
+                    color: #fff;
+                    border-radius: 20px;
+                    width: fit-content;
+                    margin: auto;
+                }
+                .lowScore{
+                    padding: 2px 10px;
+                    background-color: #F4A261;   
+                    color: #fff;
+                    border-radius: 20px;
+                    width: fit-content;
+                    margin: auto;
+                }
+                .unsubmitted{
+                    padding: 2px 10px;
+                    background-color: #C45C4A;   
+                    color: #fff;
+                    border-radius: 20px;
+                    width: fit-content;
+                    margin: auto;
+                }
+                .uncorrected{
+                    padding: 2px 10px;
+                    background-color: #D9D9D9;   
+                    color: #fff;
+                    border-radius: 20px;
+                    width: fit-content;
+                    margin: auto;
+                }
+            }
+            .left-half{
+                border-right: 0;
+                border-radius: 10px 0 0 10px;
+            }
+            .right-half{
+                border-left: 5px solid #B9DDF5;
+                border-radius: 0 10px 10px 0;
+            }
+        }
+        .pages{
+            height: 30px;
+            margin-top: 40px;
+        }
+    }
+}
+.loading{
+    height: 100%;
+    width: 100%;
+    text-align: center;
+    line-height: 100vh;
+    font-size: 44px;
+    font-weight: 700;
+    color: #70bbdb;
+}
+</style>

+ 54 - 0
src/views/ClassPage/components/classHeader.vue

@@ -0,0 +1,54 @@
+<template>
+    <div class="class-header">
+        <div class="title title_border">
+            <slot></slot>
+        </div>
+        <div class="title title_border"style="margin-left:-200px;">
+            <slot name="TeacherName"></slot>
+        </div>
+        <div class="title" @click="$router.go(-1)" style="padding-right: 4.1vw;cursor: pointer;">
+            <el-icon style="vertical-align: bottom; margin-right: 6px;"><ArrowLeftBold/></el-icon>
+            返回
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ArrowLeftBold } from '@element-plus/icons-vue'
+</script>
+
+<script lang="ts">
+export default {
+  name: "classHeader"
+}
+</script>
+
+<style scoped>
+.class-header {
+    width: '100%';
+    height: 7.5vh;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    padding-left: 0.5vw;
+    margin-top: -15px;
+}
+.title{
+    /* 字体设置 一级标题 */
+    color:#70BBDB;
+    font-family: Inter;
+    font-size: 28px;
+    font-style: normal;
+    font-weight: 700;
+    line-height: 24px;
+    height:28px;
+    padding-bottom: 10px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+}
+.title_border{
+    border-bottom: 5px solid #4FA3C5;
+}
+</style>

+ 50 - 0
src/views/ClassPage/components/classTable.vue

@@ -0,0 +1,50 @@
+<template>
+    <div class="class-table-zcx">
+        <table>
+            <slot name="head"></slot>
+            <slot name="body"></slot>
+        </table>
+    </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<script lang="ts">
+export default {
+  name: "classTable"
+}
+</script>
+
+<style>
+.class-table-zcx {
+    width: 100%;
+    height: 100%;
+    background-color: #fff;
+    table{
+      width: 100%;
+      height: 100%;
+      th{
+        font-size: 16px;
+        font-weight: 700;
+        color: #70bbdb;
+        border: none;
+        border-bottom: 3px solid #70bbdb;
+      }
+      td{
+        font-size: 14px;
+        color: #606266;
+        border: none;
+        border-bottom: 2px solid #e4e7ed;
+        text-align: center;
+      }
+      .Gethover {
+        background-color: #f0f2f5;
+      }
+      .Blue {
+        color: #4a90c4;
+      }
+    }
+}
+</style>

+ 62 - 0
src/views/ClassPage/components/classToggle.vue

@@ -0,0 +1,62 @@
+<template>
+    <div class="toggle">
+        <button class="left"
+                :class="{'active':isLeft,'unactive':!isLeft}"
+                @click="toggle(true)"
+        ><slot name="left"></slot></button>
+        <button class="right"
+                :class="{'active':!isLeft,'unactive':isLeft}"
+                @click="toggle(false)"
+        ><slot name="right"></slot></button>
+    </div>
+</template>
+
+<script setup lang="ts">
+const props = defineProps({
+  isLeft: {
+    type: Boolean,
+    required: true
+  }
+})
+
+const emit = defineEmits(['update:isLeft'])
+
+const toggle = (value) => {
+    emit('update:isLeft', value)
+}
+</script>
+
+<script lang="ts">
+export default {
+  name: "classToggle"
+}
+</script>
+
+<style>
+.toggle{
+    margin-left: 20px;
+    .left{
+        border: 3px solid #4A90C4;
+        border-radius: 20px 0 0 20px;
+        border-right: 0;
+        padding: 2px 7px;
+        font-weight: 700;
+        cursor: pointer;
+    }
+    .right{
+        border: 3px solid #4A90C4;
+        border-radius: 0 20px 20px 0;
+        padding: 2px 7px;
+        font-weight: 700;
+        cursor: pointer;
+    }
+    .unactive{
+        color: #70BBDB;
+        background-color: #fff;
+    }
+    .active{
+        color: #fff;
+        background-color:#4A90C4;
+    }
+}
+</style>

+ 30 - 2
src/views/CourseSquare/component/CourseDetails/index.vue

@@ -32,6 +32,7 @@
           </template>
           </template>
           <template v-if="showEdit && role == 'teachers'">
           <template v-if="showEdit && role == 'teachers'">
             <el-button color="#597d3b" @click="visible = true; fileList[0] = {name: '壁纸', url: newCourseInfo.courseImages[0]}">编辑课程</el-button>
             <el-button color="#597d3b" @click="visible = true; fileList[0] = {name: '壁纸', url: newCourseInfo.courseImages[0]}">编辑课程</el-button>
+            <el-button color="#4a90c4" style="color: #fff" @click="$router.push(`/home/courseClass/${courseId}`)">班级管理</el-button>
           </template>
           </template>
         </div>
         </div>
       </div>
       </div>
@@ -44,6 +45,15 @@
               {{dataForm.description}}
               {{dataForm.description}}
             </div>
             </div>
           </template>
           </template>
+          <template v-else-if="activeTab == '班级列表'">
+            <div>
+                <button 
+                style="color: #fff;background-color: #4a90c4;border: none;padding: 5px 10px;border-radius: 5px;cursor: pointer;" 
+                @click="$router.push(`/home/studentDetail?classId=${classId}&courseId=${courseId}&stuId=${store.user.id}`)">
+                查看班级列表
+              </button>
+            </div>
+          </template>
           <template v-else>
           <template v-else>
             <CourseHomeworkTable :disabled="isDisabled()" :columns="columns" :tableData="homeworkList" :role="role" :courseId="dataForm.courseId" :isShowNewHomework="showEdit && role == 'teachers'"/>
             <CourseHomeworkTable :disabled="isDisabled()" :columns="columns" :tableData="homeworkList" :role="role" :courseId="dataForm.courseId" :isShowNewHomework="showEdit && role == 'teachers'"/>
           </template>
           </template>
@@ -123,7 +133,7 @@ import {
   type UploadFiles, type UploadFile, type FormRules, type FormInstance
   type UploadFiles, type UploadFile, type FormRules, type FormInstance
 } from 'element-plus';
 } from 'element-plus';
 import './index.scss'
 import './index.scss'
-import type {CourseDetailVO, CourseVO, IAssignment, IAssignmentTableColumn, IAssignmentTableItem} from "@/types/vo";
+import type {CourseDetailVO, CourseVO, IAssignment, IAssignmentTableColumn, IAssignmentTableItem, ClassVO} from "@/types/vo";
 import {useRoute} from "vue-router";
 import {useRoute} from "vue-router";
 import useApis from "@/apis";
 import useApis from "@/apis";
 import CourseHomeworkTable from "@/views/CourseSquare/component/CourseDetails/component/CourseHomeworkTable.vue";
 import CourseHomeworkTable from "@/views/CourseSquare/component/CourseDetails/component/CourseHomeworkTable.vue";
@@ -139,6 +149,7 @@ const store = useStore()
 const route = useRoute()
 const route = useRoute()
 const role:'teachers' | 'student' = store.user.role === 'STUDENT' ?   'student':'teachers';
 const role:'teachers' | 'student' = store.user.role === 'STUDENT' ?   'student':'teachers';
 
 
+let classId
 const courseId = +route.params.courseId
 const courseId = +route.params.courseId
 
 
 let showCode = ref(true)
 let showCode = ref(true)
@@ -147,6 +158,8 @@ let showEdit = ref(false)
 let visible = ref(false)
 let visible = ref(false)
 let uploadRef = ref()
 let uploadRef = ref()
 
 
+// classId = 2
+
 // 用于展示课程信息
 // 用于展示课程信息
 let dataForm = reactive<CourseDetailVO>({
 let dataForm = reactive<CourseDetailVO>({
   courseId: null,
   courseId: null,
@@ -317,7 +330,14 @@ const handleChange = (file,fileList) => {
 const processCourses = () => {
 const processCourses = () => {
   dataForm.courseImages[0] = dataForm.courseImages[0].split("?")[0]
   dataForm.courseImages[0] = dataForm.courseImages[0].split("?")[0]
 }
 }
-
+const getClassId = () => {
+  
+  apis.getClassNameOfStudent(courseId,store.user.id)
+    .then((res:any)=>{
+      console.log('NEWRES',res);
+      classId = res.data.data.classId
+    })
+}
 const getCourse = () => {
 const getCourse = () => {
   // 获取课程信息
   // 获取课程信息
   apis.getCourseByCourseId(courseId)
   apis.getCourseByCourseId(courseId)
@@ -558,9 +578,17 @@ function isDisabled(){
   if(role == "teachers") return !showEdit.value;
   if(role == "teachers") return !showEdit.value;
 }
 }
 
 
+const ShowStudentClass = () => {
+  if(role == "student"){
+    items.value.push({label: "班级列表", key: "班级列表"})
+  }
+}
+
 onMounted(() => {
 onMounted(() => {
+  ShowStudentClass()
   getCourse()
   getCourse()
   getHomeworkList()
   getHomeworkList()
+  getClassId()
 
 
   if(role == 'student') isShowCode()
   if(role == 'student') isShowCode()
   if(role == 'teachers') isShowEdit()
   if(role == 'teachers') isShowEdit()

+ 1 - 1
src/views/HomeworkPage/index.vue

@@ -133,7 +133,7 @@
                    class="result-btn" 
                    class="result-btn" 
                    size="small"
                    size="small"
                    type="primary"
                    type="primary"
-                   :disabled="getSecondStatusText(assignment) !== '已批改'"
+                   :disabled="getSecondStatusText(assignment) !== '已批改' && getSecondStatusText(assignment) !== '(补交)已批改'"
                    @click="viewCorrection(assignment.assignmentId, store.user.id, assignment.type, assignment.assignmentName)"
                    @click="viewCorrection(assignment.assignmentId, store.user.id, assignment.type, assignment.assignmentName)"
                  >
                  >
                    批改结果
                    批改结果