Procházet zdrojové kódy

Merge remote-tracking branch 'origin/seec' into seec

181250114 před 3 roky
rodič
revize
d5b6b5eb73

+ 1 - 0
package.json

@@ -19,6 +19,7 @@
     "pdfmake": "^0.2.6",
     "react": "^18.2.0",
     "react-pdf": "^6.0.3",
+    "marked": "^4.2.3",
     "resize-detector": "^0.3.0",
     "vue": "^3.1.0",
     "vue-pdf-embed": "^1.1.5",

+ 1 - 1
src/App.vue

@@ -38,6 +38,6 @@ export default {
 <style lang="scss" scoped>
 /*Zhurui修改,消除底部对主体元素的覆盖*/
 .main-body {
-  margin-bottom: 250px;
+  //margin-bottom: 250px;
 }
 </style>

+ 0 - 29
src/api/courseWare.js

@@ -25,32 +25,6 @@ function getCourseWareById(id) {
     return axios.get(`${prefix}/getById`, {params: {id}});
 }
 
-function downloadCourseWare(id) {
-    return axios.get(`${prefix}/download`, {params: {id}}, {responseType: "blob"}
-        //{headers: {Authorization: localStorage.getItem("token") }}
-    );
-}
-
-// const host = process.env.NODE_ENV === "development" ? "http://localhost:8080" : "";
-// const slideUrl = host + "/api/slide";
-// const useSlideServer = postfix => postfix ? slideUrl + postfix : slideUrl;
-
-// // 获取课件的文件链接
-// function getSlideUrl (slideId) {
-//     // return axios
-//     //     .get(useSlideServer("/" + slideId + "/url"))
-//     //     .then(res => res.data);
-//     return axios.get(`${prefix}/getSlideUrl`, {params: {slideId}});
-// }
-//
-// // 获取某个具体课件的信息
-// function getSlideInfo (slideId) {
-//     // return axios
-//     //     .get(useSlideServer("/" + slideId))
-//     //     .then(res => res.data);
-//     return axios.get(`${prefix}/getSlideInfo`, {params: {slideId}});
-// }
-
 function checkCourseWareNameUnique(courseId, name) {
     return axios.get(`${prefix}/checkName`, {params: {courseId, name}});
 }
@@ -70,10 +44,7 @@ const courseWareAPIs = {
     getCourseWaresByCourseId,
     getCourseWareById,
     uploadCourseWareFile,
-    downloadCourseWare,
     checkCourseWareNameUnique,
-    // getSlideUrl,
-    // getSlideInfo,
     changeCourseWareName
 };
 

+ 76 - 0
src/api/eval.js

@@ -32,6 +32,22 @@ function getTeacherEvalExamsByCourseId(courseId) {
   });
 }
 
+function createEvalExam(exam) {
+  return axios.post(`${prefix}/exam/create`,exam);
+}
+
+function getExamQuestions(examId) {
+  return axios.get(`${prefix}/exam/${examId}/questions`);
+}
+
+function getSamplePaper() {
+  return axios.get(`${prefix}/exam/sample`);
+}
+
+function getQuestionInfo(id) {
+  return axios.get(`${prefix}/questions/${id}`);
+}
+
 function getStudentEvalExamsByCourseId(courseId){
   return axios.get(`${prefix}/exam/student/course`, {
     params: {
@@ -44,6 +60,53 @@ function getTeacherEvalExamsGroupByCourse() {
   return axios.get(`${prefix}/exam/teacher/groupByCourse`);
 }
 
+function setExamPaperFromSample(examId, sampleExamPaperId){
+  return axios.post(`${prefix}/exam/question/sample/${examId}/?sampleExamPaperId=${sampleExamPaperId}`);
+}
+
+function getExamSubmitRecordsByUserId(examId) {
+  return axios.get(`${prefix}/exam/record/all?examId=${examId}`);
+}
+
+function getQuestionList(page,stem,tags,knowledgeName) {
+  let params = "";
+  if (stem) {
+    params += `stem=${stem}&`;
+  }
+  if (tags) {
+    params += `tags=${tags}&`;
+  }
+  if (knowledgeName) {
+    params += `knowledgeName=${knowledgeName}&`;
+  }
+
+  return axios.get(`${prefix}/exam/question?${params}pageSize=${page.pageSize}&pageNum=${page.pageNum}`);
+}
+
+function setExamPaperQuestions(examId,questions){
+  return axios.put(`${prefix}/exam/${examId}/setQuestions`,{questionScoreMap: questions});
+}
+
+function getExamById(examId){
+  return axios.get(`${prefix}/exam/${examId}`);
+}
+
+function getExamJoinedUsers(examId){
+  return axios.get(`${prefix}/exam/user/${examId}`);
+}
+
+function getAllReportByExamId(examId){
+  return axios.get(`${prefix}/exam/report/all?examId=${examId}`);
+}
+
+function joinPublicExam(examId){
+  return axios.put(`${prefix}/exam/invite/withoutCode?examId=${examId}`);
+}
+
+function joinExamByInviteCode(inviteCode,examId){
+  return axios.put(`${prefix}/exam/invite/withCode?inviteCode=${inviteCode}&examId=${examId}`);
+}
+
 const evalAPIs = {
   getTeacherEvalExams,
   getTeacherEvalExamsGroupByCourse,
@@ -53,6 +116,19 @@ const evalAPIs = {
   getRecommendEvalExamsGroupByCourse,
   getTeacherEvalExamsByCourseId,
   getStudentEvalExamsByCourseId,
+  createEvalExam,
+  getExamQuestions,
+  getSamplePaper,
+  getQuestionInfo,
+  setExamPaperFromSample,
+  getExamSubmitRecordsByUserId,
+  getQuestionList,
+  setExamPaperQuestions,
+  getExamById,
+  getExamJoinedUsers,
+  getAllReportByExamId,
+  joinPublicExam,
+  joinExamByInviteCode
 };
 
 export default evalAPIs;

+ 2 - 2
src/components/CourseCard.vue

@@ -2,7 +2,7 @@
   <el-card class="course-card" shadow="hover" @click="toCourseInfo">
     <img :src="imageUrl" alt="" class="image">
     <el-row>
-      <el-col :span="16" class="name">{{ title }}</el-col>
+      <el-col :span="16" class="name" :title="this.title">{{ title.length>12?title.substring(0,12)+"...":title }}</el-col>
       <el-col :span="8" class="status">
         <el-tag v-if="state==='NOT_STARTED'" size="small" type="info">未开始</el-tag>
         <el-tag v-else-if="state==='RUNNING'" size="small">进行中</el-tag>
@@ -16,7 +16,7 @@
       季度: {{ semester }}
     </el-row>
     <el-row class="time">
-      时间: {{ startTime }}  {{ endTime }}
+      时间: {{ startTime.replace(/-/g,".") }}至{{ endTime.replace(/-/g,".") }}
     </el-row>
     <el-row class="description">
       {{ description }}

+ 2 - 2
src/components/GlobalHeader.vue

@@ -13,10 +13,10 @@
         课程
       </el-menu-item>
       <el-menu-item index="4">
-        项目广场
+        项目
       </el-menu-item>
       <el-menu-item index="5">
-        测试广场
+        测试
       </el-menu-item>
       <el-menu-item index="6">
         个人中心

+ 63 - 25
src/components/ProjectCard.vue

@@ -1,33 +1,33 @@
 <template>
   <el-card class="project" shadow="hover" @click="toProjectInfo">
-
-    <div :style="{borderBottom: type==='courseProject' ? '30px solid #f5b86e' : '30px solid #8dc9e2'}"
-         style="float: right; width: 0; height: 0; position: absolute;right: 0;bottom: 0;border-left: 30px solid transparent;">
-      <div v-if="type==='courseProject'" style="padding: 0 4px 0 4px; position: absolute; right: 0; bottom: -30px; color: white">
-        D
+    <template #header>
+      <div class="project-header">
+        <span :title="this.projectVO['name']">
+          {{ this.projectVO["name"] }}
+        </span>
       </div>
-      <div v-else style="padding: 0 3px 0 3px; position: absolute; right: 0; bottom: -30px; color: white">
-        P
+    </template>
+    <div class="project-logo">
+      <div class="project-logo-text">
+        D
       </div>
     </div>
     <el-row class="projectId">
       {{ projectId }}
     </el-row>
-    <el-row>
-      {{ name }}
+    <el-row class="project-description">
+      {{ description }}
     </el-row>
-    <el-row>
-      {{description}}
-    </el-row>
-    <el-row>
-      {{ link }}
+    <el-row class="project-link">
+      <el-button type="text" style="padding: 0" @click.stop="copyLink(projectVO['gitRemoteUrl'])">复制链接</el-button>
     </el-row>
   </el-card>
 </template>
 
 <script>
-import {ElMessage, ElMessageBox} from "element-plus";
+import {ElMessage} from "element-plus";
 import store from "@/store";
+import colorUtil from "@/utils/color";
 
 export default {
   name: "ProjectCard",
@@ -38,6 +38,7 @@ export default {
   data() {
     return {
       teacher: Object,
+      color: colorUtil.SYSTEM_THEME.developerColor,
     };
   },
   computed: {
@@ -51,17 +52,11 @@ export default {
       return this.$store.getters.isStudent;
     },
     projectId() {
-      return "Id: " + this.projectVO.id;
-    },
-    name() {
-      return "名称: " + this.projectVO.name;
+      return "项目ID: " + this.projectVO.id;
     },
     description() {
       return "描述: " + this.projectVO.description;
     },
-    link() {
-      return "链接: " + this.projectVO.gitRemoteUrl;
-    },
   },
 
   methods: {
@@ -74,13 +69,16 @@ export default {
           duration: 1000,
         });
         await this.$router.push({name: "Auth", params: {login_to: "/portal"}});
-        return;
       } else {
         if (this.projectVO.name) {
           window.open("https://devcloud.seec.seecoder.cn/project/" + this.projectVO.id + "/schedule?id_token=" + this.$store.state.user.token);
         }
-        return;
       }
+    },
+
+    async copyLink(link) {
+      await navigator.clipboard.writeText(link);
+      this.$message.success("复制成功!");
     }
   }
 };
@@ -101,9 +99,49 @@ export default {
 }
 
 .projectId {
-  font-weight: bold;
+  /*font-weight: bold;*/
+  font-size: medium;
+  color: #777777;
+}
+
+.project-header {
+  width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.project-description {
+  width: 100%;
+  display: -webkit-box; /*必须结合的属性,将对象作为弹性伸缩盒子模型显示*/
+  -webkit-line-clamp: 2; /*设置多少行*/
+  -webkit-box-orient: vertical; /*必须结合的属性,设置或检索伸缩盒对象的子元素的排列方式*/
+  overflow: hidden; /*文本会被修剪,并且剩余的部分不可见*/
+  text-overflow: ellipsis;
   font-size: medium;
+  color: #777777;
 }
 
+.project-link {
+  font-size: medium;
+}
+
+.project-logo {
+  float: right;
+  width: 0;
+  height: 0;
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  border-left: 30px solid transparent;
+  border-bottom: 30px solid v-bind(color)
+}
 
+.project-logo-text {
+  padding: 0 3px 0 0;
+  position: absolute;
+  right: 0;
+  bottom: -32px;
+  color: white
+}
 </style>

+ 49 - 32
src/components/TestCard.vue

@@ -34,11 +34,14 @@
     </el-row>
 
     <el-row class="time">
-      {{ timePeriod }}
+      {{ startTime }}
+    </el-row>
+    <el-row class="time">
+      {{ endTime }}
     </el-row>
 
-    <el-row v-if="type==='eval'" class="comment">
-      {{ testVO.comment }}
+    <el-row v-if="type==='eval'&&testVO['comment']!==''" class="comment">
+      {{ testVO["comment"] }}
     </el-row>
   </el-card>
 </template>
@@ -46,6 +49,7 @@
 <script>
 import {ElMessage, ElMessageBox} from "element-plus";
 import store from "@/store";
+import timeUtil from "@/utils/time";
 
 export default {
   name: "TestCard",
@@ -68,7 +72,7 @@ export default {
     isCreator() {
       if (this.testVO.examId) {
         return Number(this.testVO.course.teacherId) === Number(this.$store.state.user.id);
-      }else{
+      } else {
         return this.testVO.creator.username === this.$store.state.user.username;
       }
     },
@@ -94,24 +98,41 @@ export default {
     },
     course() {
       if (this.type === "eval") {
-        return "";
+        return "课程: " + this.testVO.course.name;
       } else {
-        return "所属课程: " + this.testVO.course;
+        return "课程: " + this.testVO.course;
       }
     },
     teacherName() {
-      if (this.type === "eval") {
-        return "教师: " + this.teacher.name;
-      } else {
-        return "教师: " + this.teacher;
+      return "教师: " + this.teacher;
+    },
+    startTime() {
+      let start = "开始: ";
+      switch (this.type) {
+        case "eval":
+          start += this.testVO.startTime.substring(0, 16);
+          break;
+        case "coder":
+          start += timeUtil.timestampToTime(this.testVO.startAt);
+          break;
+        default:
+          break;
       }
+      return start;
     },
-    timePeriod() {
-      if (this.type === "eval") {
-        return "时间: " + this.testVO.startTime.substring(0, 16) + "至" + this.testVO.endTime.substring(0, 16);
-      } else {
-        return "时间: " + this.timestampToTime(this.testVO.startAt) + "至" + this.timestampToTime(this.testVO.endAt);
+    endTime() {
+      let end = "结束: ";
+      switch (this.type) {
+        case "eval":
+          end += this.testVO.endTime.substring(0, 16);
+          break;
+        case "coder":
+          end += timeUtil.timestampToTime(this.testVO.endAt);
+          break;
+        default:
+          break;
       }
+      return end;
     },
     state() {
       const now = new Date();
@@ -140,23 +161,19 @@ export default {
       }
     },
   },
-  async created() {
-    if (this.type === "eval") {
-      this.teacher = (await this.$apis.getUserAPI(this.testVO.course["teacherId"])).data.data;
-    } else {
-      this.teacher = (await this.$apis.getNameByUsernameAPI(this.testVO.creator.username)).data.data;
+  async mounted() {
+    switch (this.type) {
+      case "eval":
+        this.teacher = (await this.$apis.getUserAPI(this.testVO.course["teacherId"])).data.data.name;
+        break;
+      case "coder":
+        this.teacher = (await this.$apis.getNameByUsernameAPI(this.testVO.creator.username)).data.data;
+        break;
+      default:
+        break;
     }
   },
   methods: {
-    timestampToTime(timestamp) {
-      const date = new Date(timestamp * 1000);
-      const Y = date.getFullYear() + "-";
-      const M = (date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1) + "-";
-      const D = (date.getDate() < 10 ? "0" + date.getDate() : date.getDate()) + " ";
-      const h = (date.getHours() < 10 ? "0" + date.getHours() : date.getHours()) + ":";
-      const m = (date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes());
-      return Y + M + D + h + m;
-    },
     // TODO 分情况处理跳转
     async toTestInfo() {
       if (!this.isLogin) {
@@ -226,7 +243,7 @@ export default {
               if (isCourseSelected) {
                 messageBoxContent = "是否立即查看测试详情";
                 confirmButtonText = "立即查看";
-                this.$router.push("/exam/"+this.testVO.id);
+                this.$router.push("/exam/" + this.testVO.id);
                 // nextURL = "https://coder.seec.seecoder.cn/exam?id_token=" + this.$store.state.user.token;
               } else {
                 messageBoxContent = "尚未选择课程,是否立即参加?";
@@ -288,14 +305,14 @@ export default {
 
 .teacher {
   font-size: 10px;
-  margin-top: 10px;
+  margin-top: 4px;
   color: #999999;
 }
 
 .time {
   margin-top: 4px;
   font-size: 10px;
-  margin-bottom: 16px;
+  /*margin-bottom: 16px;*/
   color: #999999;
 }
 

+ 6 - 0
src/global/register-element.js

@@ -37,7 +37,9 @@ import {
   ElMenuItem,
   ElSubmenu, ElCollapse, ElCollapseItem,
   ElSwitch,
+  ElContainer,ElHeader,ElMain,
   ElBadge, ElOptionGroup, ElTooltip,
+  ElAlert,
 } from "element-plus";
 
 // NOTE 使用到的组件列表,请确保 import 和 component 的一致性
@@ -85,6 +87,10 @@ const components = [
   ElBadge,
   ElOptionGroup,
   ElTooltip,
+  ElContainer,
+  ElHeader,
+  ElMain,
+  ElAlert
 ];
 
 export default function (app) {

+ 1 - 19
src/requests/axios.config.js

@@ -1,32 +1,18 @@
 import axios from "axios";
 import store from "@/store";
-import userAPIs from "@/api/user";
 
 const httpPrefix = "http";
 
 const instance = axios.create({
   baseURL: "/api"
 });
-//
-// const specialUrls = [
-//   "/eval",
-// ];
 
 instance.interceptors.request.use(
   async (config) => {
-    if(config.url.startsWith(httpPrefix)){
+    if (config.url.startsWith(httpPrefix)) {
       config.baseURL = "";
     }
 
-    // if(specialUrls.indexOf(config.url) !== -1){
-    //   const res = await userAPIs.loginAPI({
-    //     phone: "17301585267",
-    //     password: "SEEC2020",
-    //   });
-    //   config.headers.authorization = res.data.data.token;
-    //   return config;
-    // }
-
     if (store.state.user.token) {
       config.headers.authorization = store.state.user.token;
     } else {
@@ -39,10 +25,6 @@ instance.interceptors.request.use(
   }
 );
 
-// instance.interceptors.response.use((response) => {
-//
-// });
-
 export {
   instance as axios
 };

+ 16 - 1
src/router/index.js

@@ -115,7 +115,22 @@ const routes = [
     path: "/exam/:examId/result/:resultId/commit/:commitId/record/:recordId",
     name: "RecordDetail-Second",
     component: () => import("../views/ExamPage/ResultPage/CommitPage/RecordPage/RecordDetail")
-  }
+  },
+  {
+    path: "/evalExam/:examId/edit",
+    name: "edit-exams",
+    component: () =>import("../views/ExamPage/evalExamPage/EditExam")
+  },
+  {
+    path: "/evalExam/:examId/teacher/details",
+    name: "teacher-examDetails",
+    component: () =>import("../views/ExamPage/evalExamPage/teacherExamDetails")
+  },
+  {
+    path: "/evalExam/:examId/student/details",
+    name: "student-examDetails",
+    component: () =>import("../views/ExamPage/evalExamPage/studentExamDetails")
+  },
 ];
 
 const router = createRouter({

+ 13 - 0
src/utils/color.js

@@ -0,0 +1,13 @@
+const coderColor = "#8dc9e2";
+const evalColor = "#f5b86e";
+const developerColor = "#3a85bb";
+
+const colorUtil = {
+  SYSTEM_THEME: {
+    coderColor,
+    evalColor,
+    developerColor,
+  }
+};
+
+export default colorUtil;

+ 9 - 0
src/utils/markdown.js

@@ -0,0 +1,9 @@
+import {marked} from "marked";
+
+export function markdownToHTML(markdownData) {
+    return marked(markdownData, {
+        pedantic: false,
+        breaks: false,
+        gfm: true
+    });
+}

+ 15 - 0
src/utils/time.js

@@ -0,0 +1,15 @@
+function timestampToTime(timestamp, delimiter = "-") {
+  const date = new Date(timestamp * 1000);
+  const Y = date.getFullYear() + delimiter;
+  const M = (date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1) + delimiter;
+  const D = (date.getDate() < 10 ? "0" + date.getDate() : date.getDate()) + " ";
+  const h = (date.getHours() < 10 ? "0" + date.getHours() : date.getHours()) + ":";
+  const m = (date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes());
+  return Y + M + D + h + m;
+}
+
+const timeUtil = {
+  timestampToTime,
+};
+
+export default timeUtil;

+ 2 - 0
src/views/CoursePage/style/CourseDetail.css

@@ -37,6 +37,7 @@
 .course-img {
     width: 400px;
     height: 230px;
+    margin-left: -80px
 }
 
 .course-img > img {
@@ -50,6 +51,7 @@
     font-size: 16px;
     color: #333333;
     margin-left: 32px;
+    margin-right: -80px;
 }
 
 .course-info-main {

+ 101 - 87
src/views/CoursePage/tabs/CoursewareTab.vue

@@ -5,22 +5,20 @@
           <el-button
                   @click="uploadDialogVisible = true"
                   type="primary"
-                  icon="el-icon-circle-plus-outline"
                   plain>新建</el-button>
       </el-col>
       <el-col :span="15">
-          <span style="font-size: smaller; position: absolute; bottom: 25%">(支持拓展名:.pdf .ppt .pptx .doc .docx .xls .jpg .jpeg .png,单次只可上传一个文件)</span>
+          <span style="font-size: smaller; position: absolute; bottom: 25%">(支持拓展名:.pdf .ppt .pptx .doc .docx .xls .jpg .jpeg .png .exe .zip仅支持单次上传一个文件)</span>
       </el-col>
     </el-row>
     <el-dialog v-model="uploadDialogVisible" :before-close="handleCancelUpload" :width="700">
-        <el-form ref="formRef" :model="form"  label-width="100px">
+        <el-form ref="formRef" :model="form" label-width="100px" style="margin-top: 10px">
           <el-form-item label="使用类型" required>
               <el-select v-model="form.useMethod" placeholder="类型">
-                  <el-option key="1" label="浏览" value="SCAN"></el-option>
-                  <el-option key="2" label="附件" value="ATTACHMENT"></el-option>
+                  <el-option key="1" label="浏览(支持拓展名:.pdf .ppt .pptx .doc .docx .xls .jpg .jpeg .png)" value="SCAN"></el-option>
+                  <el-option key="2" label="附件(支持拓展名:.exe .zip)" value="ATTACHMENT"></el-option>
               </el-select>
           </el-form-item>
-
           <el-form-item label="文件" required>
               <el-upload
                   :file-list="fileList"
@@ -30,12 +28,7 @@
                   :auto-upload="false"
                   :show-file-list="false"
               >
-                  <el-button
-                      plain
-                      type="primary"
-                      class="tabs-function-button"
-                      icon="el-icon-upload"
-                        >上传文件</el-button>(仅支持上传单个文件)
+                  <el-button plain type="primary" class="tabs-function-button">上传文件</el-button>(仅支持上传单个文件)
               </el-upload>
               <el-tag
                       v-if="isNameUnique"
@@ -62,8 +55,6 @@
         </div>
     </el-dialog>
 
-
-
       <el-table :data="fileEntryList">
         <el-table-column
             type="index"
@@ -72,14 +63,15 @@
       <el-table-column
         prop="name"
         label="文件名"
-        width="240">
+        width="260">
           <template #default="scope">
             <el-icon v-if="scope.row['courseWareVO'].useMethod === 'SCAN'" style="color: #337ecc" class="myicon"><Reading /></el-icon>
             <el-icon v-if="scope.row['courseWareVO'].useMethod === 'ATTACHMENT'" style="color: green" class="myicon"><Paperclip /></el-icon>
               {{scope.row['courseWareVO'].name}}
-              <el-icon :size="20" style="color: #337ecc; position: absolute; bottom: 35%; margin-left: 5px" @click="handleChangeName(scope.row['courseWareVO'].id, scope.row['courseWareVO'].name)">
-                  <Edit />
-              </el-icon>
+              <el-button v-if="Number(scope.row['courseWareVO'].senderId) === Number(this.$store.state.user.id)"
+                         @click="handleChangeName(scope.row['courseWareVO'].id, scope.row['courseWareVO'].name)"
+                         type="text" icon="el-icon-edit-outline" style="color: #337ecc; font-size: 18px">
+              </el-button>
           </template>
       </el-table-column>
       <el-table-column
@@ -95,33 +87,39 @@
         label="上传时间"
         width="160"
         :sortable="true"
-        :sort-method="sortByDate2"
         >
           <template #default="scope">
-<!--            {{timestampToTime(scope.row["courseWareVO"].createdTime)}}-->
               {{timestampToTime(scope.row["courseWareVO"].createdTime)}}
           </template>
       </el-table-column>
       <el-table-column
         prop="operation"
         label="操作"
-        width="220">
+        width="200">
           <template #default="scope">
             <a :href="scope.row['courseWareVO'].fileUrl" v-if="isPdfOrPicture(scope.row['courseWareVO'].name) === 'OTHER'">
               <el-button v-if="scope.row['courseWareVO'].useMethod === 'SCAN'"
-                       size="mini" type="primary" plain style="margin-right: 5px">预览</el-button>
+                       style="color: #409EFF; margin-right: 10px" type="text">预览</el-button>
             </a>
             <el-button v-if="scope.row['courseWareVO'].useMethod === 'SCAN' && isPdfOrPicture(scope.row['courseWareVO'].name) === 'PDF'"
                  @click="handlePreviewFile(scope.row['courseWareVO'].fileUrl)"
-                   size="mini" type="primary" plain style="margin-right: 5px">预览</el-button>
+                       style="color: #409EFF" type="text">预览</el-button>
             <el-button v-if="scope.row['courseWareVO'].useMethod === 'SCAN' && isPdfOrPicture(scope.row['courseWareVO'].name) === 'PICTURE'"
                  @click="handlePreviewPicture(scope.row['courseWareVO'])"
-                 size="mini" type="primary" plain style="margin-right: 5px">预览</el-button>
-            <a :href="scope.row['courseWareVO'].fileUrl">
-              <el-button size="mini" type="success" plain style="margin-right: 5px">下载</el-button>
+                       style="color: #409EFF; margin-right: 10px" type="text">预览</el-button>
+            <a :href="scope.row['courseWareVO'].fileUrl" v-if="isPdfOrPicture(scope.row['courseWareVO'].name) === 'OTHER'">
+              <el-button style="color: #67C23A" type="text">下载</el-button>
+            </a>
+            <el-button v-if="isPdfOrPicture(scope.row['courseWareVO'].name) === 'PDF'"
+                     @click="handlePreviewFile(scope.row['courseWareVO'].fileUrl)"
+                    style="color: #67C23A" type="text">下载</el-button>
+            <a :href="scope.row['courseWareVO'].fileUrl"
+               v-if="isPdfOrPicture(scope.row['courseWareVO'].name) === 'PICTURE'">
+              <el-button style="color: #67C23A" type="text">下载</el-button>
             </a>
             <el-button v-if="Number(scope.row['courseWareVO'].senderId) === Number(this.$store.state.user.id)"
-                       size="mini" type="danger" @click="handleRemoveCourseWare(scope.row['courseWareVO'].id)" plain>删除</el-button>
+                       style="color: #F56C6C; margin-left: 10px" type="text"
+                       @click="handleRemoveCourseWare(scope.row['courseWareVO'].id)">删除</el-button>
           </template>
       </el-table-column>
     </el-table>
@@ -206,25 +204,6 @@ export default {
       this.init();
   },
   methods: {
-      sortByDate(obj1, obj2) {
-          console.log(obj1, obj2);
-          let val1 = obj1.deadline;
-          let val2 = obj2.deadline;
-          console.log(val1, val2);
-          console.log(val1 - val2);
-          return val1 - val2;
-      },
-
-      sortByDate2(obj1, obj2) {
-          console.log(obj1.courseWareVO, obj2.courseWareVO);
-          let val1 = obj1.courseWareVO.createdTime;
-          let val2 = obj2.courseWareVO.createdTime;
-          console.log(val1, val2);
-          console.log(val2 > val1 ? 1 : -1);
-          return val2 > val1 ? 1 : -1;
-      },
-
-
       async init() {
           this.fileEntryList = (await this.$apis.getCourseWaresByCourseId(this.courseId)).data.data;
           console.log(this.fileEntryList);
@@ -242,7 +221,12 @@ export default {
       },
 
       handleCancelChangeName() {
-          ElMessageBox.confirm("确定取消修改课件名称?")
+          ElMessageBox.confirm("是否退出修改课件名称?推出后将不保存已填写信息。", "提示", {
+              confirmButtonText: "确认退出",
+              cancelButtonText: "我再想想",
+              type: "warning",
+              center: true,
+          })
               .then(() => {
                   this.newName = null;
                   this.nameAppend = null;
@@ -251,7 +235,19 @@ export default {
       },
 
       handleConfirmChangeName() {
-          ElMessageBox.confirm("确定修改课件名称?")
+          if (this.newName === null  && this.nameAppend !== null) {
+              this.$message({
+                  message: "课件名称不可为空,请重新修改课件名称",
+                  type: "error"
+              });
+              return;
+          }
+          ElMessageBox.confirm("是否确定修改课件名称?", "提示", {
+              confirmButtonText: "确认修改",
+              cancelButtonText: "我再想想",
+              type: "warning",
+              center: true,
+          })
               .then(async () => {
                   const isNameUnique = (await this.$apis.checkCourseWareNameUnique(this.courseId, this.newName + this.nameAppend)).data.data;
                   if (!isNameUnique) {
@@ -278,13 +274,23 @@ export default {
       async handleUploadFile(file, fileList) {
           const fileType = file.name.substring(file.name.lastIndexOf(".") + 1);
           console.log(this.form.useMethod + " + " + file.name.substring(file.name.lastIndexOf(".") + 1));
-          if (!(fileType === "pdf" || fileType === "ppt" || fileType === "pptx" || fileType === "xls"
-                || fileType === "doc" || fileType === "docx" || fileType === "png" || fileType === "jpeg" || fileType === "jpg")) {
-              this.$message({
-                  message: "该文件类型不属于支持的课件类型",
-                  type: "error"
-              });
-              return;
+          if (this.form.useMethod === "SCAN") {
+              if (!(fileType === "pdf" || fileType === "ppt" || fileType === "pptx" || fileType === "xls"
+                  || fileType === "doc" || fileType === "docx" || fileType === "png" || fileType === "jpeg" || fileType === "jpg")) {
+                  this.$message({
+                      message: "该文件类型不属于支持的可浏览课件类型",
+                      type: "error"
+                  });
+                  return;
+              }
+          } else {
+              if (!(fileType === "exe" || fileType === "zip")) {
+                  this.$message({
+                      message: "该文件类型不属于支持的附件类型",
+                      type: "error"
+                  });
+                  return;
+              }
           }
           if (this.fileNum === 1) {
               this.$message({
@@ -308,6 +314,20 @@ export default {
       },
 
       async handleConfirmUpload() {
+        if (this.file === null) {
+            this.$message({
+              message: "尚未上传课件",
+              type: "error"
+            });
+            return;
+        }
+        if (this.newName === null && this.nameAppend !== null) {
+            this.$message({
+                message: "课件名称不可为空,请重新修改课件名称",
+                type: "error"
+            });
+            return;
+        }
         if (this.newName !== null) {
           const isNameUnique = (await this.$apis.checkCourseWareNameUnique(this.courseId, this.newName + this.nameAppend)).data.data;
           if (!isNameUnique) {
@@ -319,36 +339,30 @@ export default {
           }
         }
 
-        if (this.file === null) {
-            this.$message({
-                message: "尚未上传课件",
-                type: "error"
-            });
-        } else {
-            const fileUrl = (await this.$apis.uploadCourseWareFile(this.courseId + "/" + (this.newName == null ? this.file.name : (this.newName + this.nameAppend)), this.fileList[0].raw)).data.data;
-            this.$message({
-                message: "上传课件成功",
-                type: "success"
-            });
-            this.$apis.createCourseWare({
-                name: this.newName === null ? this.file.name : (this.newName + this.nameAppend),
-                size: this.file.size,
-                senderId: this.$store.state.user.id,
-                courseId: this.courseId,
-                createdTime: new Date(),
-                useTime: "BEFORE_CLASS",
-                useMethod: this.form.useMethod,
-                fileUrl: fileUrl,
-            }).then(async () => {
-                this.fileEntryList = (await this.$apis.getCourseWaresByCourseId(this.courseId)).data.data;
-            });
-            this.uploadDialogVisible = false;
-            this.isChangeNameWhenUpload = false;
-            this.isNameUnique = false;
-            this.fileNum = 0;
-            this.newName = null;
-            this.nameAppend = null;
-        }
+        const fileUrl = (await this.$apis.uploadCourseWareFile(this.courseId + "/" + (this.newName == null ? this.file.name : (this.newName + this.nameAppend)), this.file.raw)).data.data;
+        this.$apis.createCourseWare({
+            name: this.newName === null ? this.file.name : (this.newName + this.nameAppend),
+            size: this.file.size,
+            senderId: this.$store.state.user.id,
+            courseId: this.courseId,
+            createdTime: new Date(),
+            useTime: "BEFORE_CLASS",
+            useMethod: this.form.useMethod,
+            fileUrl: fileUrl,
+        }).then(async () => {
+            this.fileEntryList = (await this.$apis.getCourseWaresByCourseId(this.courseId)).data.data;
+        });
+        this.$message({
+          message: "上传课件成功",
+          type: "success"
+        });
+        this.uploadDialogVisible = false;
+        this.isChangeNameWhenUpload = false;
+        this.isNameUnique = false;
+        this.fileNum = 0;
+        this.newName = null;
+        this.nameAppend = null;
+        this.file = null;
       },
 
       handleCancelUpload(done) {
@@ -375,8 +389,8 @@ export default {
       },
 
       handleCloseFileTag() {
-        ElMessageBox.confirm("是否确认取消选择该课件?", "提示", {
-            confirmButtonText: "确认取消",
+        ElMessageBox.confirm("是否取消上传该课件?", "提示", {
+            confirmButtonText: "确",
             cancelButtonText: "我再想想",
             type: "warning",
             center: true,

+ 305 - 76
src/views/CoursePage/tabs/TaskTab.vue

@@ -11,7 +11,7 @@
                  icon="el-icon-circle-plus-outline"
                  style="color: #f5b86e;">Eval测试
       </el-button>
-      <el-timeline >
+      <el-timeline>
         <el-timeline-item
             v-for="task in courseTaskList"
             :key="task['id']"
@@ -47,45 +47,45 @@
       </el-timeline>
     </div>
     <div v-if="isStudent">
-    <el-timeline >
-      <el-timeline-item
-        v-for="task in courseTaskList"
-        :key="task['id']"
-        :timestamp="task['examId'] ? task.startTime.substring(0, 16) + ' — ' + task.endTime.substring(0, 16) : timestampToTime(task['startAt'] * 1000) + ' — ' + timestampToTime(task['endAt'] * 1000)"
-        :color="courseTaskStatusColor(task)" @click="toTask(task)">
-        <el-row>
-          <el-tag v-if="task['examId']" class="timeline-content" size="small" color="#f5b86e" effect="dark"
-                  style="border: none">E
-          </el-tag>
-          <el-tag v-else class="timeline-content" size="small" color="#8dc9e2" effect="dark"
-                  style="border: none">C
-          </el-tag>
-          <el-row v-if="task['examId']" class="timeline-content">
-            <el-tag v-if="this.state(task)==='NOT_STARTED'" size="small" >未开始</el-tag>
-            <el-tag v-else-if="this.state(task)==='RUNNING' && task['isJoined']" size="small" type="success">
-              已参加
+      <el-timeline>
+        <el-timeline-item
+            v-for="task in courseTaskList"
+            :key="task['id']"
+            :timestamp="task['examId'] ? task.startTime.substring(0, 16) + ' — ' + task.endTime.substring(0, 16) : timestampToTime(task['startAt'] * 1000) + ' — ' + timestampToTime(task['endAt'] * 1000)"
+            :color="courseTaskStatusColor(task)" @click="toTask(task)">
+          <el-row>
+            <el-tag v-if="task['examId']" class="timeline-content" size="small" color="#f5b86e" effect="dark"
+                    style="border: none">E
             </el-tag>
-            <el-tag v-else-if="this.state(task)==='RUNNING' && !task['isJoined']" size="small">未参加</el-tag>
-            <el-tag v-else-if="this.state(task)==='FINISHED'" size="small" type="info">已结束</el-tag>
-            <el-tag v-else-if="this.state(task)==='CLOSED'" size="small" type="info">已关闭</el-tag>
-          </el-row>
-          <el-row v-else class="timeline-content">
-            <el-tag v-if="this.state(task)==='NOT_STARTED'" size="small">未开始</el-tag>
-            <el-tag v-else-if="this.state(task)==='RUNNING' && task['joined']" size="small" type="success">已参加
+            <el-tag v-else class="timeline-content" size="small" color="#8dc9e2" effect="dark"
+                    style="border: none">C
             </el-tag>
-            <el-tag v-else-if="this.state(task)==='RUNNING' && !task['joined']" size="small">未参加</el-tag>
-            <el-tag v-else-if="this.state(task)==='CLOSED'" size="small" type="info">已关闭</el-tag>
-            <el-tag v-else-if="this.state(task)==='FINISHED'" size="small" type="danger">已结束</el-tag>
-          </el-row>
-          <el-row v-if="task['examId']" class="task-title timeline-content">
-            {{ task["examName"] }}
-          </el-row>
-          <el-row v-else class="task-title timeline-content">
-            {{ task.name }}
+            <el-row v-if="task['examId']" class="timeline-content">
+              <el-tag v-if="this.state(task)==='NOT_STARTED'" size="small">未开始</el-tag>
+              <el-tag v-else-if="this.state(task)==='RUNNING' && task['isJoined']" size="small" type="success">
+                已参加
+              </el-tag>
+              <el-tag v-else-if="this.state(task)==='RUNNING' && !task['isJoined']" size="small">未参加</el-tag>
+              <el-tag v-else-if="this.state(task)==='FINISHED'" size="small" type="info">已结束</el-tag>
+              <el-tag v-else-if="this.state(task)==='CLOSED'" size="small" type="info">已关闭</el-tag>
+            </el-row>
+            <el-row v-else class="timeline-content">
+              <el-tag v-if="this.state(task)==='NOT_STARTED'" size="small">未开始</el-tag>
+              <el-tag v-else-if="this.state(task)==='RUNNING' && task['joined']" size="small" type="success">已参加
+              </el-tag>
+              <el-tag v-else-if="this.state(task)==='RUNNING' && !task['joined']" size="small">未参加</el-tag>
+              <el-tag v-else-if="this.state(task)==='CLOSED'" size="small" type="info">已关闭</el-tag>
+              <el-tag v-else-if="this.state(task)==='FINISHED'" size="small" type="danger">已结束</el-tag>
+            </el-row>
+            <el-row v-if="task['examId']" class="task-title timeline-content">
+              {{ task["examName"] }}
+            </el-row>
+            <el-row v-else class="task-title timeline-content">
+              {{ task.name }}
+            </el-row>
           </el-row>
-        </el-row>
-      </el-timeline-item>
-    </el-timeline>
+        </el-timeline-item>
+      </el-timeline>
     </div>
   </div>
   <!-- 创建Coder考试-->
@@ -117,9 +117,9 @@
       <el-form-item v-if="$store.state.problem.cart.length">
         <el-collapse accordion>
           <el-collapse-item
-            v-for="problem in $store.state.problem.cart"
-            :key="problem.id"
-            :name="problem.id"
+              v-for="problem in $store.state.problem.cart"
+              :key="problem.id"
+              :name="problem.id"
           >
             <template #title>
               <span>{{ problem.name }}</span>
@@ -129,9 +129,9 @@
             <span class="problem-description">{{ problem.description }}</span>
             <div class="remove-button">
               <el-button
-                type="danger"
-                size="mini"
-                @click="$store.commit('removeProblem', problem)"
+                  type="danger"
+                  size="mini"
+                  @click="$store.commit('removeProblem', problem)"
               >移除该题
               </el-button
               >
@@ -176,25 +176,25 @@
       <el-table-column label="操作" width="200">
         <template #default="scope">
           <el-button
-            type="primary"
-            size="mini"
-            @click="checkDetails"
+              type="primary"
+              size="mini"
+              @click="checkDetails"
           >详情
           </el-button
           >
           <el-button
-            v-if="removeEnable(scope.row)"
-            type="danger"
-            size="mini"
-            @click="$store.commit('removeProblem', scope.row)"
+              v-if="removeEnable(scope.row)"
+              type="danger"
+              size="mini"
+              @click="$store.commit('removeProblem', scope.row)"
           >移出试题清单
           </el-button
           >
           <el-button
-            v-else-if="addEnable(scope.row)"
-            type="success"
-            size="mini"
-            @click="$store.commit('addProblem', scope.row)"
+              v-else-if="addEnable(scope.row)"
+              type="success"
+              size="mini"
+              @click="$store.commit('addProblem', scope.row)"
           >加入试题清单
           </el-button
           >
@@ -202,18 +202,66 @@
       </el-table-column>
     </el-table>
     <el-pagination
-      background
-      layout="prev, pager, next"
-      :total="problemPageInfo.total"
-      @current-change="changePage"
+        background
+        layout="prev, pager, next"
+        :total="problemPageInfo.total"
+        @current-change="changePage"
     />
   </el-dialog>
+
+  <!-- 创建Eval考试-->
+  <el-dialog v-model="evalExamDialog" :before-close="handleEvalExamFormCancel" :width="1200"
+             :title="evalExamDialogTitle">
+    <el-form ref="evalExamFormRef" :model="evalExamForm" :rules="evalExamFormRules" label-width="150px">
+      <el-form-item label="所属课程" prop="course">
+        <el-input disabled :placeholder="courseName"/>
+      </el-form-item>
+      <el-form-item label="测试名称" prop="examName">
+        <el-input class="course-form-item" v-model="evalExamForm.examName" placeholder="请输入测试名称"></el-input>
+      </el-form-item>
+      <el-form-item label="职位" prop="position">
+        <el-input class="course-form-item" v-model="evalExamForm.position" placeholder="请输入职位名称"></el-input>
+      </el-form-item>
+      <el-form-item label="技能点" prop="skills">
+        <el-select v-model="evalExamForm.skills" multiple placeholder="请选择技能点">
+          <el-option v-for="item in skillList"
+                     :key="item.value"
+                     :label="item.label"
+                     :value="item.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="预期难度" prop="difficulty">
+        <el-input v-model.number="evalExamForm.difficulty"/>
+      </el-form-item>
+      <el-form-item label="测试时间" prop="range">
+        <el-date-picker v-model="evalExamForm.range" range-separator="至"
+                        start-placeholder="开始时间" end-placeholder="结束时间" type="datetimerange">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="在线监考">
+        <el-switch v-model="evalExamForm.antiCheating" active-text="开启"
+                   inactive-text="不开启"/>
+      </el-form-item>
+      <el-form-item label="需要邀请码加入">
+        <el-switch v-model="evalExamForm.isPrivate" active-text="是"
+                   inactive-text="否"/>
+      </el-form-item>
+      <el-form-item label="备注">
+        <el-input class="course-form-item" v-model="evalExamForm.comment" placeholder="请输入备注"></el-input>
+      </el-form-item>
+    </el-form>
+    <div style="margin: auto;width: fit-content">
+      <el-button type="primary" :disabled="!valid" @click="createEvalExam">创建评测</el-button>
+      <el-button @click="handleEvalExamFormCancel">取消</el-button>
+    </div>
+  </el-dialog>
 </template>
 
 <script>
 import portal from "@/utils/portal";
 import {ElMessage, ElMessageBox} from "element-plus";
 import {formatTimestamp, problemTypeOptions, problemTypeToTagContent} from "@/utils/coder-utils";
+import {toArray} from "core-js/internals/async-iterator-iteration";
 
 export default {
   name: "TaskTab",
@@ -263,7 +311,7 @@ export default {
           required: true, message: "请输入测试名称", trigger: "blur",
         },
         course: {
-          required: true, message: "请选择测试名称",
+          required: true, message: "请选择课程名称",
         },
         range: {
           required: true, message: "请选择测试时间段", trigger: "blur",
@@ -278,7 +326,90 @@ export default {
       dialogProblemTypeStr: "",
       dialogProblemName: "",
       problemList: [],
-      problemPageInfo: {}
+      problemPageInfo: {},
+
+      //Create Eval exam
+      evalExamDialog: false,
+      evalExamDialogTitle: "新建Eval测试",
+      evalExamForm: {
+        course: "",
+        examName: "",
+        position: "",
+        skills: [],
+        difficulty: 50,
+        range: [],
+        startDate: "",
+        startTime: "",
+        endDate: "",
+        endTime: "",
+        comment: "",
+        antiCheating: false,
+        isPrivate: false,
+        valid: true,//todo 将form表单的校验情况和确定按钮是否可用绑定
+      },
+      evalExamFormRules: {
+        examName: [
+          {required: true, message: "该项为必填项", trigger: "blur"},
+          {min: 1, max: 25, message: "长度应在1到25之间", trigger: "change"}
+        ],
+        course: {
+          required: true, message: "该项为必填项", trigger: "blur"
+        },
+        position: [
+          {required: true, message: "该项为必填项", trigger: "blur"},
+          {min: 1, max: 25, message: "长度应在1到25之间", trigger: "change"}
+        ],
+        skills: [
+          {required: true, message: "至少选择一项", trigger: "change"},
+        ],
+        difficulty: [
+          {required: true, message: "该项为必填项", trigger: "blur"},
+          {type: "number", min: 1, max: 100, message: "1到100之间的数字", trigger: "change"}
+        ],
+        range: {
+          required: true, message: "该项为必填项", trigger: "blur"
+        },
+      },
+      skillList: [
+        {
+          value: "Java",
+          label: "Java",
+        },
+        {
+          value: "C++",
+          label: "C++",
+        },
+        {
+          value: "Python",
+          label: "Python",
+        },
+        {
+          value: "JavaScript",
+          label: "JavaScript",
+        },
+        {
+          value: "GO",
+          label: "Go",
+        },
+        {
+          value: "MySQL",
+          label: "MySQL",
+        },
+        {
+          value: "Redis",
+          label: "Redis",
+        },
+        {
+          value: "React",
+          label: "React",
+        },
+        {
+          value: "Vue",
+          label: "Vue",
+        },
+      ],
+      isEvalExamCreated: false,
+      valid: true,
     };
   },
   computed: {
@@ -287,13 +418,13 @@ export default {
     },
     courseTaskList() {
       return []
-        .concat(this.evalExamList)
-        .concat(this.coderExamList)
-        .sort((a, b) => {
-          let a_time = a["startAt"] * 1000 || new Date(a.startTime).getTime();
-          let b_time = b["startAt"] * 1000 || new Date(b.startTime).getTime();
-          return b_time - a_time;
-        });
+          .concat(this.evalExamList)
+          .concat(this.coderExamList)
+          .sort((a, b) => {
+            let a_time = a["startAt"] * 1000 || new Date(a.startTime).getTime();
+            let b_time = b["startAt"] * 1000 || new Date(b.startTime).getTime();
+            return b_time - a_time;
+          });
     },
     isTeacher() {
       return this.$store.getters.isTeacher;
@@ -402,8 +533,23 @@ export default {
       if (type === "coder") {
         _this.$router.push(`/exam/${task.id}`);
 
-      }else { //只有eval直接跳转
-        window.open(url + "?id_token=" + this.$store.state.user.token);
+      } else { //只有eval直接跳转
+        if (_this.isTeacher()) {
+          _this.$router.push({
+            name: "teacher-examDetails",
+            params: {
+              examId: task["examId"],
+            }
+          });
+        } else {
+          _this.$router.push({
+            name: "student-examDetails",
+            params: {
+              examId: task["examId"],
+            }
+          });
+        }
+        // window.open(url + "?id_token=" + this.$store.state.user.token);
       }
     },
     // 创建Coder考试的Dialog
@@ -418,10 +564,10 @@ export default {
     },
     fetchProblemData(page = 1) { // 按需获得Coder题目
       this.$apis.retrievePageProblem(page, this.dialogProblemLimit, this.dialogProblemTypeStr, this.dialogProblemName)
-        .then(_res => {
-          this.problemPageInfo = _res.data.page;
-          this.problemList = _res.data.res;
-        }).catch(err => {
+          .then(_res => {
+            this.problemPageInfo = _res.data.page;
+            this.problemList = _res.data.res;
+          }).catch(err => {
         console.log(err);
       });
     },
@@ -466,8 +612,8 @@ export default {
     //题目购物车cart
     removeEnable(problem) { //是否可以移除题目
       return this.$store.state.problem.cart.length && this.$store.state.problem.cart
-        .map((item) => item.id)
-        .includes(problem.id);
+          .map((item) => item.id)
+          .includes(problem.id);
     },
 
     addEnable(problem) { //是否可以添加题目
@@ -494,12 +640,85 @@ export default {
     problemTypeToTagContent,
 
     handleAddEvalClick() {
-      window.open("https://eval.seec.seecoder.cn/teacher/created-exams?id_token=" + this.$store.state.user.token);
+      // window.open("https://eval.seec.seecoder.cn/teacher/created-exams?id_token=" + this.$store.state.user.token);
+      this.evalExamDialog = true;
     },
 
     handleAddCoderClick() {
       this.coderExamDialog = true;
     },
+
+    // 创建Eval考试的Dialog
+    handleEvalExamFormCancel() {
+      this.evalExamDialog = false;
+      this.$refs.evalExamFormRef.resetFields();
+    },
+
+    //格式化日期函数
+    formatDate(date, fmt) {
+      if (typeof date == "string") {
+        return date;
+      }
+
+      if (!fmt) fmt = "yyyy-MM-dd hh:mm:ss";
+
+      if (!date || date == null) return null;
+      var o = {
+        "M+": date.getMonth() + 1, // 月份
+        "d+": date.getDate(), // 日
+        "h+": date.getHours(), // 小时
+        "m+": date.getMinutes(), // 分
+        "s+": date.getSeconds(), // 秒
+        "q+": Math.floor((date.getMonth() + 3) / 3), // 季度
+        "S": date.getMilliseconds() // 毫秒
+      };
+      if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
+      for (var k in o) {
+        if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
+      }
+      return fmt;
+    },
+
+
+    // 创建Eval评测
+    createEvalExam() {
+      const obj = {
+        creatorId: this.$store.state.user.id,
+        examName: this.evalExamForm.examName,
+        position: this.evalExamForm.position,
+        skills: Object.values(this.evalExamForm.skills),
+        difficulty: this.evalExamForm.difficulty,
+        startTime: this.formatDate(this.evalExamForm.range[0], "yyyy-MM-dd hh:mm:ss"),
+        endTime: this.formatDate(this.evalExamForm.range[1], "yyyy-MM-dd hh:mm:ss"),
+        comment: this.evalExamForm.comment,
+        antiCheating: this.evalExamForm.antiCheating,
+        isPublic: !this.evalExamForm.isPrivate,
+        isHidden: false,
+        courseId: this.courseId,
+        courseName: this.courseName
+      };
+      console.log("创建eval测试", obj);
+      this.$apis.createEvalExam(obj).then(
+          _res => {
+            ElMessage.success("创建Eval评测成功");
+            this.init();
+            console.log(_res.data.data.result);
+            this.evalExamDialog = false;
+            this.isEvalExamCreated = !!_res.data;
+            console.log(this.isEvalExamCreated);
+            const examId = _res.data.data.result.examId;
+            this.$router.push({
+              name: "edit-exams",
+              params: {
+                examId: examId
+              }
+            });
+          }
+      ).catch(_err => {
+        ElMessage.error("创建Eval评测失败");
+      });
+
+    }
   },
 };
 </script>
@@ -511,4 +730,14 @@ export default {
   margin-top: 10px;
   padding-left: 10px;
 }
+
+.slider-demo-block {
+  display: flex;
+  align-items: center;
+}
+
+.slider-demo-block .el-slider {
+  margin-top: 0;
+  margin-left: 12px;
+}
 </style>

+ 299 - 0
src/views/ExamPage/evalExamPage/EditExam.vue

@@ -0,0 +1,299 @@
+<template>
+  <el-row v-if="!showQuestionList" :gutter="20" type="flex" justify="space-between">
+    <el-col :span="24" class="box">
+      <el-card class="box-card" v-if="examInfo">
+        <template #header>
+          <div class="card-header" v-if="paperQuestionsSet" style="font-size: 20px">
+            评测「{{ examInfo.examName }}」
+          </div>
+          <div class="card-header" v-else style="font-size: 20px">
+            评测「{{ examInfo.examName }}」创建成功 请设置试卷内容
+          </div>
+        </template>
+        <el-row>评测ID: {{ examInfo.examId }}</el-row>
+        <el-row>
+          <el-col :span="12">
+            评测邀请码:{{
+              examInfo.inviteCode ? examInfo.inviteCode : '无'
+            }}
+          </el-col>
+          <el-col :span="12">
+            当前难度值:{{ examInfo.difficulty }}
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            开始时间:{{ examInfo.startTime }}
+          </el-col>
+          <el-col :span="12">
+            结束时间:{{ examInfo.endTime }}
+          </el-col>
+        </el-row>
+      </el-card>
+    </el-col>
+    <el-col v-if="!paperQuestionsSet" :span="24" class="box">
+      <el-card class="box-card">
+        <span style="font-size: 20px">样卷选择</span>
+        <el-divider></el-divider>
+        <el-tabs v-model="tab" v-if="examSamples[0]" class="demo-tabs">
+          <el-tab-pane v-for="sample in examSamples" :key="sample.id" :name="sample.id"
+                       :label="'样卷'+sample.id">
+            <div v-if="!fetching">
+              <exam-questions-info
+                  :exam-questions="sampleQuestions[tab-1]"
+                  :examId="this.examInfo.examId"
+              ></exam-questions-info>
+            </div>
+            <el-row justify="center">
+              <el-button
+                  type="primary"
+                  style="float:right"
+                  @click="onSelectSample(sample.id)"
+              >
+                选择此试卷
+              </el-button>
+              <el-button
+                  style="float: left"
+                  @click="showQuestionList = true"
+              >
+                基于样卷调整试题
+              </el-button>
+            </el-row>
+          </el-tab-pane>
+        </el-tabs>
+      </el-card>
+    </el-col>
+    <el-col v-else :span="24" class="box">
+      <el-card class="box-card">
+        <template #header>
+          <div class="card-header">
+            <span style="font-size: 20px">试卷内容</span>
+            <el-button
+                v-if="fetching & hide"
+                @click="onClickHideButton()"
+                round
+                style="float: right"
+            >
+              <el-icon class="el-icon--right">
+                <ArrowUp/>
+              </el-icon>
+            </el-button>
+            <el-button
+                v-if="fetching & !hide"
+                @click="onClickShowButton()"
+                round
+                style="float: right"
+            >
+              <el-icon class="el-icon--right">
+                <ArrowDown/>
+              </el-icon>
+            </el-button>
+            <!--          响应事件是?-->
+            <el-button
+                v-if="!fetching"
+                type="primary"
+                style="float: right"
+                @click="showQuestionList = true"
+            >
+              调整试题
+            </el-button>
+          </div>
+        </template>
+
+        <exam-questions-info
+            v-show="showQuestionInfo"
+            :exam-questions="examQuestions"
+            :examId="this.examInfo.examId"
+        ></exam-questions-info>
+      </el-card>
+    </el-col>
+  </el-row>
+  <question-list
+      v-if="showQuestionList"
+      :sample-questions="curQuestions"
+      :question-and-score="curQuestionAndScore"
+      :exam-id="this.examInfo.examId"
+      :from-edit-page="true"
+      @leave="showQuestionList = false"
+  ></question-list>
+</template>
+
+<script>
+
+import ExamQuestionsInfo from "@/views/ExamPage/evalExamPage/ExamQuestionsInfo";
+import QuestionList from "@/views/ExamPage/evalExamPage/QuestionList";
+import {ArrowDown, ArrowUp} from "@element-plus/icons-vue";
+import {ElMessage} from "element-plus";
+
+export default {
+  name: "EditExam",
+  components: {
+    ArrowDown,
+    ArrowUp,
+    ExamQuestionsInfo,
+    QuestionList
+  },
+  data() {
+    return {
+      tab: 1,
+      showQuestionList: false,
+      paperQuestionsSet: false,
+      examSamples: [],
+      examQuestionAndScore: {},
+      fetching: false,
+      examQuestions: [],
+      examInfo: [],
+      sampleQuestions: [],
+      showQuestionInfo: true,
+      hide: true
+    };
+  },
+
+  computed: {
+    curQuestionAndScore() {
+      if (this.paperQuestionsSet) {
+        return this.examQuestionAndScore;
+      }
+      return this.examSamples[this.tab-1].questionAndScore;
+    },
+
+    curQuestions() {
+      if (this.paperQuestionsSet) {
+        return this.examQuestions;
+      }
+      return this.sampleQuestions[this.tab-1];
+    },
+
+  },
+
+  created() {
+    this.getExamInfo();
+    this.$apis.getExamQuestions(this.examInfo.examId ?? "").then(
+        _res => {
+          if (_res.data.data.result) {
+            this.paperQuestionsSet = true;
+          }
+          this.examQuestionAndScore = _res.data.data.result.questionAndScore;
+          const questionsIds = Object.keys(_res.data.data.result.questionAndScore);
+          const questionsPromises = [];
+          this.fetching = true;
+          questionsIds.forEach((value, index) => {
+            questionsPromises[index] = this.$apis.getQuestionInfo(value);
+          });
+          Promise.all(questionsPromises)
+              .then(reps => {
+                this.examQuestions = reps.map((value) => {
+                  return value.data.data.result;
+                });
+              })
+              .catch((error) => {
+                console.log("题目获取失败");
+              })
+              .finally(() => {
+                this.fetching = false;
+              });
+        }
+    ).catch(() => {
+      this.$apis.getSamplePaper().then(
+          _res => {
+            this.examSamples = _res.data.data.result;
+            this.fetchQuestionInfo(0);
+          });
+    });
+  },
+
+  watch: {
+    tab(val) {
+      val = val - 1;
+      this.fetchQuestionInfo(val);
+    }
+  },
+  methods: {
+
+    getExamInfo() {
+      this.examId = this.$route.params.examId;
+      this.$apis.getExamById(this.examId).then(_res => {
+        this.examInfo = _res.data.data.result;
+        this.examInfo.skills = this.examInfo.skills.toString();
+      });
+    },
+
+    fetchQuestionInfo(val) {
+      if (this.sampleQuestions[val]) {
+        return this.sampleQuestions[val];
+      } else {
+        this.fetching = true;
+        let questionsPromises = [];
+        let questionsIds = Object.entries(
+            this.examSamples[val].questionAndScore
+        );
+        questionsPromises = questionsIds.map((val) => {
+          return this.$apis.getQuestionInfo(val[0]);
+        });
+        Promise.all(questionsPromises).then((reps) => {
+          this.sampleQuestions[val] = reps.map((value) => {
+            return value.data.data.result;
+          });
+          this.fetching = false;
+        });
+      }
+    },
+
+    onSelectSample(sampleId) {
+      this.$apis.setExamPaperFromSample(this.examInfo.examId ?? "", sampleId).then(
+          () => {
+            ElMessage({
+              message: "设置样卷成功",
+              type: "success",
+            });
+            setTimeout(() => {
+              this.$router.push({
+                name: "teacher-examDetails",
+                params: {
+                  examId: this.examInfo.examId,
+                }
+              });
+            }, 1000);
+          }
+      ).catch(() => {
+        ElMessage.error("设置样卷失败");
+      });
+
+    },
+
+    onClickHideButton() {
+      this.showQuestionInfo = false;
+      this.hide = false;
+    },
+
+    onClickShowButton() {
+      this.showQuestionInfo = true;
+      this.hide = true;
+    }
+
+
+  }
+
+};
+</script>
+
+<style scoped>
+.el-alert {
+  margin: 20px 0 0;
+}
+
+.el-alert:first-child {
+  margin: 0;
+}
+
+.box {
+  height: 80%;
+  /*overflow-y: auto;*/
+  display: flex;
+  flex-direction: column;
+}
+
+.el-card {
+  margin-bottom: 20px;
+}
+</style>

+ 134 - 0
src/views/ExamPage/evalExamPage/ExamQuestionsInfo.vue

@@ -0,0 +1,134 @@
+<template>
+  <div>
+    <div v-for="question in examQuestions" :key="question.id">
+      <el-divider/>
+      <el-row>
+        <el-col :span="2">
+          <el-tag v-if="!analysis">
+            {{ examQuestions.indexOf(question) + 1 }}
+          </el-tag>
+          <el-tag v-else :type="submitAnswers.get(question.id) === question.answer.replace(/[;]/gi, '') ? 'success' : 'danger' ">
+            {{ examQuestions.indexOf(question) + 1 }}
+          </el-tag>
+        </el-col>
+        <el-col :span="18">
+          <markdown-text
+              :raw="question.stem + mergeOptions(question.options)"
+              style="height: 100%;width: 100%"
+          ></markdown-text>
+        </el-col>
+        <el-col :span="4">
+          <el-card v-if="question.keyPoints">
+            关键词: {{ question.keyPoints }}
+          </el-card>
+        </el-col>
+      </el-row>
+      <el-row v-if="analysis && question.type !== 'code'">
+        <el-col>
+          <markdown-text
+              :raw="question.analysis"
+              style="height: 100%;width: 100%"
+          ></markdown-text>
+        </el-col>
+        <el-col
+            v-if="question.type !== 'short_answer'"
+        >
+          我的答案:
+          <el-tag
+              :type="
+              submitAnswers.get(question.id) ===
+              question.answer.replace(/[;]/gi, '')
+                ? 'success'
+                : 'danger'
+            "
+              v-if="submitAnswers.size > 0"
+          >
+            {{ submitAnswers.get(question.id) }}
+          </el-tag>
+          正确答案:
+          <el-tag type="success">
+            {{ question.answer.replace(/[;]/gi, '') }}
+          </el-tag>
+          <!--  简答题解析即为答案        -->
+        </el-col>
+      </el-row>
+    </div>
+  </div>
+</template>
+
+<script>
+import MarkdownText from "@/views/ExamPage/evalExamPage/MarkdownText";
+export default {
+  name: "ExamQuestionsInfo",
+  components: {MarkdownText},
+  props: {
+    examQuestions: {
+      type: Array,
+      required: true
+    },
+    analysis: {
+      type: Boolean,
+      default: false
+    },
+    examId: null,
+  },
+  data() {
+    return {
+      submitAnswers: {},
+    };
+  },
+
+  methods: {
+    mergeOptions(options){
+      if (!options) {
+        return "";
+      }
+
+      let keys = Object.keys(options);
+      let values = Object.values(options);
+      let entries = [];
+
+      for (let i = 0; i < keys.length; i++) {
+        entries.push(keys[i] + " : " + values[i]);
+      }
+      return "\n" + entries.join(" ");
+    },
+
+    async fetchAnswers() {
+      this.$apis.getExamSubmitRecordsByUserId(this.examId).then(_res => {
+        let records = _res.data.data.result;
+        if (records && records.length > 0) {
+          this.submitAnswers = new Map();
+          records.forEach((val) => {
+            this.submitAnswers.set(
+                val.questionId,
+                val.submitAnswer === ""
+                    ? "无"
+                    : val.submitAnswer === "t"
+                        ? "true"
+                        : val.submitAnswer === "f"
+                            ? "false"
+                            : val.submitAnswer.replace(/[;]/gi, "")
+            );
+          });
+        } else {
+          this.submitAnswers = new Map();
+          this.examQuestions.forEach((val) => {
+            this.submitAnswers.set(val.id, "无");
+          });
+        }
+      });
+    },
+  },
+
+  mounted() {
+    if(this.analysis){
+      this.fetchAnswers();
+    }
+  }
+};
+</script>
+
+<style scoped>
+
+</style>

+ 38 - 0
src/views/ExamPage/evalExamPage/MarkdownText.vue

@@ -0,0 +1,38 @@
+<template>
+  <div v-html="markdownToHTML(trimmedRaw)" class="markdown-content"></div>
+</template>
+
+<script>
+import { markdownToHTML } from "@/utils/markdown";
+export default {
+  name: "MarkdownText",
+  props: {
+    raw: String
+  },
+  data() {
+    return {
+      trimmedRaw: ""
+    };
+  },
+  methods: {
+    markdownToHTML
+  },
+  mounted() {
+    this.trimmedRaw = this.raw.trim();
+  },
+};
+</script>
+
+<style lang="scss">
+.markdown-content {
+  word-break: break-all;
+  white-space: pre-wrap;
+  margin-top: 16px;
+  p {
+    margin-bottom: 0;
+  }
+  img {
+    max-width: 100%;
+  }
+}
+</style>

+ 248 - 0
src/views/ExamPage/evalExamPage/QuestionList.vue

@@ -0,0 +1,248 @@
+<template>
+  <el-row :gutter="20" type="flex" justify="space-between">
+    <el-col :span="24" class="box">
+      <el-card class="box-card">
+        <span style="font-size: 20px">已选题目</span>
+        <el-divider></el-divider>
+        <el-table ref="selectedQuestionsTable" :data="selectedQuestions" style="width: 100%" empty-text="空" :row-key="getRowKeys">
+          <el-table-column label="分值" width="100">
+            <template #default="scope">
+              <span>{{ scope.row.score }}</span>
+              <el-icon @click="increasePoints(scope.row)">
+                <ArrowUp/>
+              </el-icon>
+              <el-icon @click="decreasePoints(scope.row)">
+                <ArrowDown/>
+              </el-icon>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="100">
+            <template #default="scope">
+              <el-icon @click="deleteItem(scope.$index,scope.row)">
+                <DeleteFilled/>
+              </el-icon>
+            </template>
+          </el-table-column>
+          <el-table-column label="题干">
+            <template #default="scope">
+              <markdown-text :raw="scope.row.stem"></markdown-text>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-row justify="center">
+          <el-button type="primary" @click="onSubmitManualPaper">确认调整</el-button>
+          <el-button @click="onLeave">返回</el-button>
+        </el-row>
+      </el-card>
+    </el-col>
+    <el-col :span="24" class="box">
+      <el-card class="box-card">
+        <span style="font-size: 20px">试题列表</span>
+        <el-divider></el-divider>
+        <el-table ref="questionsTable" :data="questions" :row-key="getRowKeys" style="width: 100%"
+                  @select="handleSelect" @select-all="handleSelectAll">
+          <el-table-column type="selection" align="center" :reserve-selection="true" width="55"/>
+          <el-table-column label="题目类型" prop="typeText" width="100"/>
+          <el-table-column label="标签" prop="tags" width="100"/>
+          <el-table-column label="关键词" prop="keyPoints" width="200"/>
+          <el-table-column label="题干" prop="stem">
+            <template #default="scope">
+              <markdown-text :raw="scope.row.stem"></markdown-text>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-row justify="center">
+          <el-pagination
+              v-model:current-page="pageNum"
+              layout="total, prev, pager, next, jumper"
+              background
+              :total="totalNum"
+              @current-change="changePage"
+          ></el-pagination>
+
+        </el-row>
+      </el-card>
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+import markdownText from "@/views/ExamPage/evalExamPage/MarkdownText";
+import {toRaw} from "vue";
+import {ElMessage} from "element-plus";
+
+export default {
+  name: "QuestionList",
+  props: {
+    sampleQuestions: {
+      type: Array
+    },
+    questionAndScore: {},
+    examId: null,
+    fromEditPage: Boolean //是否从编辑页面跳过来
+  },
+  components: {
+    markdownText
+  },
+  data() {
+    return {
+      questions: [],
+      selectedQuestions: [],
+      tags: "",
+      knowledgeName: "",
+      stem: "",
+      pageNum: 1,
+      pageSize: 10,
+      totalNum: 0,
+      loading: true,
+      selectQuestionsSuccess: false,
+      expanded: [],
+    };
+  },
+  created() {
+    this.fetchData();
+  },
+  mounted() {
+    this.selectedQuestions = this.sampleQuestions.map((val) => {
+      return {
+        ...val,
+        score: this.questionAndScore[val.id] ?? 3,
+        typeText: this.questionTypeToText(val.type)
+      };
+    });
+  },
+  methods: {
+    fetchData() {
+      this.$apis.getQuestionList(
+          {pageSize: this.pageSize, pageNum: this.pageNum},
+          this.stem,
+          this.tags,
+          this.knowledgeName
+      ).then(res => {
+        this.questions = res.data.data.result.questionStemList.map((val) => {
+          return {...val, typeText: this.questionTypeToText(val.type), score: 5};
+        });
+        this.totalNum = res.data.data.result.totalNum;
+        this.expanded = this.questions;
+        this.$nextTick(() => {
+          this.loading = false;
+        });
+      });
+    },
+    deleteItem(index,item) {
+      console.log(item);
+      console.log(this.selectedQuestions);
+      // this.selectedQuestions = this.selectedQuestions.filter(
+      //     (val) => val.id !== item.id
+      // );
+      this.selectedQuestions.splice(index,1);
+      this.$refs.questionsTable.toggleRowSelection(item, false);
+      console.log(this.selectedQuestions);
+    },
+    increasePoints(item) {
+      let selectedIndex = this.selectedQuestions.indexOf(item);
+      this.selectedQuestions[selectedIndex].score++;
+    },
+    decreasePoints(item) {
+      let selectedIndex = this.selectedQuestions.indexOf(item);
+      if (this.selectedQuestions[selectedIndex].score > 0)
+        this.selectedQuestions[selectedIndex].score--;
+    },
+
+    questionTypeToText(type) {
+      switch (type) {
+        case "choice":
+          return "单选题";
+        case "multiple_choice":
+          return "多选题";
+        case "true_false":
+          return "判断题";
+        case "blank":
+          return "填空题";
+        case "code":
+          return "编程题";
+        case "short_answer":
+          return "简答题";
+      }
+    },
+
+    getRowKeys(row) {
+      return row.id;
+      //todo 列表的唯一id值
+    },
+    handleSelect(rows, row) {
+      if (this.selectedQuestions.find((item) => item.id === row.id)) {
+        this.selectedQuestions = this.selectedQuestions.filter(
+            (item) => item.id !== row.id
+        );
+      } else {
+        this.selectedQuestions.push(row);
+      }
+    },
+
+    handleSelectAll(rows) {
+      if (rows.length && rows.indexOf(this.expanded.at(0)) !== -1) {
+        rows.forEach((row) => {
+          if (!this.selectedQuestions.find((item) => item.id === row.id)) {
+            this.selectedQuestions.push(row);
+          }
+        });
+      } else {
+        this.expanded.forEach((row) => {
+          this.selectedQuestions = this.selectedQuestions.filter(
+              (item) => item.id !== row.id
+          );
+        });
+      }
+    },
+
+    onSubmitManualPaper() {
+      let idAndScore = new Map();
+      this.selectedQuestions.forEach((val) => {
+        idAndScore.set(val.id, val.score);
+      });
+      console.log("submit");
+      console.log(idAndScore);
+      this.$apis.setExamPaperQuestions(this.examId, Object.fromEntries(idAndScore)).then(() => {
+        this.selectQuestionsSuccess = true;
+        ElMessage({
+          message: "修改试卷成功",
+          type: "success",
+        });
+        if (this.fromEditPage) {
+          setTimeout(() => {
+            this.$router.push({
+              name: "teacher-examDetails",
+              params: {
+                examId: this.examId,
+              }
+            });
+          }, 1000);
+        } else {
+          this.$emit("updatePaper");
+        }
+      });
+    },
+    changePage() {
+      this.loading = true;
+      this.fetchData();
+    },
+    onLeave() {
+      this.$emit("leave");
+    }
+  }
+};
+</script>
+
+<style scoped>
+.box {
+  height: 80%;
+  /*overflow-y: auto;*/
+  display: flex;
+  flex-direction: column;
+}
+
+.el-card {
+  margin-bottom: 20px;
+}
+</style>

+ 234 - 0
src/views/ExamPage/evalExamPage/studentExamDetails.vue

@@ -0,0 +1,234 @@
+<template>
+  <el-row>
+    <el-col :span="24" class="box">
+      <el-card class="box-card" v-if="this.examInfo">
+        <span>评测详情</span>
+        <el-divider></el-divider>
+        <template #header>
+          <div class="card-header">
+            <el-tooltip v-if="this.examInfo.isPublic" class="box-item" content="无需邀请码即可加入" placement="top"
+                        effect="dark">
+              <el-icon>
+                <Unlock/>
+              </el-icon>
+            </el-tooltip>
+            <el-tooltip v-else class="box-item" content="需要邀请码加入" placement="top" effect="dark">
+              <el-icon>
+                <Lock/>
+              </el-icon>
+            </el-tooltip>
+            <span style="font-size: 20px">评测「{{ this.examInfo.examName }}」</span>
+            <el-tag class="ml-2" :type="statusToType(state)">{{ state }}</el-tag>
+            <el-tag v-if="this.examInfo.isFinished" class="ml-2" type="info">已完成</el-tag>
+            <el-button
+                v-if="!this.examInfo.isJoined && (state === '未开始' || state === '进行中')"
+                :disabled="this.examInfo.isJoined"
+                type="primary"
+                style="float: right"
+                @click="onJoinExam">
+              报名参加
+            </el-button>
+            <el-button
+                v-if="this.examInfo.isJoined && state === '未开始'"
+                :disabled="this.examInfo.isJoined"
+                type="primary"
+                style="float: right">
+              等待加入
+            </el-button>
+            <el-button
+                v-if="this.examInfo.isJoined && state === '进行中' && !this.examInfo.isFinished"
+                type="success"
+                style="float: right"
+                @click="onStartExam">
+              开始评测
+            </el-button>
+            <el-button
+                v-if="this.examInfo.isJoined && (state === '已结束' || this.examInfo.isFinished)"
+                style="float: right"
+                @click="onOpenResult">
+              结果报告
+            </el-button>
+          </div>
+        </template>
+        <el-row>评测ID: {{ this.examInfo.examId }}</el-row>
+        <el-row>
+          <el-col :span="12">
+            所属课程: {{ this.courseInfo.name }}
+          </el-col>
+          <el-col :span="12">
+            监考:{{ this.examInfo.antiCheating ? '是' : '否' }}
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            职位: {{ this.examInfo.position }}
+          </el-col>
+          <el-col :span="12">
+            技能点: {{ this.examInfo.skills }}
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            开始时间:{{ this.examInfo.startTime }}
+          </el-col>
+          <el-col :span="12">
+            结束时间:{{ this.examInfo.endTime }}
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            客观题: {{ examQuestionsNum - examCodeQuestionsNum }} 道
+          </el-col>
+          <el-col :span="12">
+            编程题: {{ examCodeQuestionsNum }} 道
+          </el-col>
+        </el-row>
+      </el-card>
+      <el-dialog v-model="inputInviteCode" :width="500">
+        <el-form label-width="150px">
+          <el-form-item label="请输入邀请码">
+            <el-input v-model="inviteCode"></el-input>
+          </el-form-item>
+        </el-form>
+        <div style="margin: auto;width: fit-content">
+          <el-button type="success" @click="joinExamByInviteCode">确定</el-button>
+        </div>
+
+      </el-dialog>
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+import dayjs from "dayjs";
+import {ElMessage} from "element-plus";
+
+export default {
+  name: "studentExamDetails",
+  data() {
+    return {
+      examInfo: [],
+      courseInfo: [],
+      examId: 0,
+      state: "",
+      inputInviteCode: false,
+      examQuestionsNum: 0,
+      examCodeQuestionsNum: 0,
+      inviteCode: "",
+    };
+  },
+  created() {
+    this.examId=this.$route.params.examId;
+    // this.examId = 84;
+    this.$apis.getExamById(this.examId).then(_res => {
+      this.examInfo = _res.data.data.result;
+      this.examInfo.skills = this.examInfo.skills.toString();
+      this.courseInfo = this.examInfo.course;
+      this.state = this.computeExamStateText(
+          this.examInfo.startTime,
+          this.examInfo.endTime
+      );
+      this.$apis.getExamSubmitRecordsByUserId(this.examId).then(({ data }) => {
+        console.log(data.data.result);
+        this.examInfo.isFinished = !!data.data.result.find((val) => !val.message);
+      });
+      console.log(this.examInfo);
+    });
+  },
+
+  mounted() {
+  },
+
+  updated() {
+    this.state = this.computeExamStateText(
+        this.examInfo.startTime,
+        this.examInfo.endTime
+    );
+    this.$apis.getExamSubmitRecordsByUserId(this.examId).then(({ data }) => {
+      console.log(data.data.result);
+      this.examInfo.isFinished = !!data.data.result.find((val) => !val.message);
+    });
+  },
+
+  methods: {
+    statusToType(state) {
+      switch (state) {
+        case "未开始":
+          return "info";
+        case "进行中":
+          return "success";
+        case "已结束":
+          return "danger";
+      }
+    },
+
+    computeExamStateText(startTime, endTime) {
+      const TIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
+      const now = dayjs();
+      const formattedStartTime = dayjs(startTime, TIME_FORMAT);
+      const formattedEndTime = dayjs(endTime, TIME_FORMAT);
+
+      if (formattedEndTime < now) {
+        return "已结束";
+      }
+      if (formattedStartTime > now) {
+        return "未开始";
+      }
+      return "进行中";
+    },
+
+    onJoinExam() {
+      if (this.examInfo.isPublic) {
+        const examId = this.examInfo.examId ?? 0;
+        this.$apis.joinPublicExam(examId).then(() => {
+          ElMessage({
+            message: "加入考试成功",
+            type: "success",
+          });
+          this.examInfo.isJoined = true;
+          console.log(this.examInfo);
+        });
+      } else {
+        this.inputInviteCode = true;
+
+      }
+    },
+
+    joinExamByInviteCode() {
+      const examId = this.examInfo.examId ?? 0;
+      const inviteCode = this.inviteCode;
+      this.$apis.joinExamByInviteCode(inviteCode, examId)
+          .then(() => {
+            this.inputInviteCode = false;
+            ElMessage({
+              message: "加入考试成功",
+              type: "success",
+            });
+            this.examInfo.isJoined = true;
+            console.log(this.examInfo);
+          })
+          .catch(() => {
+          })
+          .finally(() => {
+            this.inviteCode = "";
+          });
+    },
+
+    onStartExam() {
+      const examId = this.examInfo.examId?.toString();
+      const url = "https://eval.seec.seecoder.cn/exam/" + examId + "/before-exam";
+      window.open(url + "?id_token=" + this.$store.state.user.token);
+    },
+    onOpenResult() {
+      const examId = this.examInfo.examId?.toString() ?? "";
+      const url = "https://eval.seec.seecoder.cn/student/result/" + examId;
+      window.open(url + "?id_token=" + this.$store.state.user.token);
+    },
+
+  }
+};
+</script>
+
+<style scoped>
+
+</style>

+ 238 - 0
src/views/ExamPage/evalExamPage/teacherExamDetails.vue

@@ -0,0 +1,238 @@
+<template>
+  <el-row v-if="!showQuestionList" :gutter="20" type="flex" justify="space-between">
+    <el-col :span="24" class="box">
+      <el-card class="box-card" v-if="this.examInfo">
+        <template #header>
+          <div class="card-header" style="font-size: 20px">
+            评测「{{ this.examInfo.examName }}」
+          </div>
+        </template>
+        <el-row>评测ID: {{ this.examInfo.examId }}</el-row>
+        <el-row>
+          <el-col :span="12">
+            评测邀请码:{{
+              this.examInfo.inviteCode ? this.examInfo.inviteCode : '无'
+            }}
+          </el-col>
+          <el-col :span="12">
+            当前难度值:{{ this.examInfo.difficulty }}
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            开始时间:{{ this.examInfo.startTime }}
+          </el-col>
+          <el-col :span="12">
+            结束时间:{{ this.examInfo.endTime }}
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            职位:{{ this.examInfo.position }}
+          </el-col>
+          <el-col :span="12">
+            技能点:{{ this.examInfo.skills }}
+          </el-col>
+        </el-row>
+        <el-row>备注:{{
+            this.examInfo.comment ? this.examInfo.comment : '无'
+          }}
+        </el-row>
+      </el-card>
+    </el-col>
+    <el-col :span="24" class="box">
+      <el-card class="box-card">
+        <template #header>
+          <div class="card-header">
+            <span style="font-size: 20px">试卷题目</span>
+            <el-button
+                v-if="hide"
+                @click="onClickHideButton()"
+                round
+                style="float: right"
+            >
+              <el-icon>
+                <ArrowUp/>
+              </el-icon>
+            </el-button>
+            <el-button
+                v-else
+                @click="onClickShowButton()"
+                round
+                style="float: right"
+            >
+              <el-icon>
+                <ArrowDown/>
+              </el-icon>
+            </el-button>
+            <!--          响应事件是?-->
+            <el-button
+                v-if="!fetching"
+                type="primary"
+                style="float: right"
+                @click="showQuestionList = true"
+            >
+              调整试题
+            </el-button>
+          </div>
+        </template>
+        <div v-if="!fetching">
+          <exam-questions-info
+              v-show="showQuestionInfo"
+              :exam-questions="examQuestions"
+          ></exam-questions-info>
+        </div>
+        <div v-else v-loading="loading"></div>
+      </el-card>
+    </el-col>
+    <el-col :span="24" class="box">
+      <el-card class="box-card">
+        <span style="font-size: 20px">评测名单</span>
+        <el-table :data="joinedUsers" style="width: 100%">
+          <el-table-column type="index" width="50"/>
+          <el-table-column prop="name" label="用户名" width="180"/>
+          <el-table-column prop="email" label="邮箱" width="180"/>
+          <el-table-column prop="score" label="成绩"/>
+        </el-table>
+      </el-card>
+    </el-col>
+  </el-row>
+  <question-list
+      v-if="showQuestionList"
+      :sample-questions="this.examQuestions"
+      :question-and-score="this.examQuestionAndScore"
+      :exam-id="this.examInfo.examId"
+      :from-edit-page="false"
+      @updatePaper="updatePaper"
+      @leave="showQuestionList = false"
+  ></question-list>
+</template>
+
+<script>
+import examQuestionsInfo from "@/views/ExamPage/evalExamPage/ExamQuestionsInfo";
+import questionList from "@/views/ExamPage/evalExamPage/QuestionList";
+
+export default {
+  name: "teacherExamDetails",
+  components: {
+    examQuestionsInfo,
+    questionList
+  },
+  data() {
+    return {
+      examId: 0,
+      fetching: false,
+      examInfo: [],
+      examQuestions: [],
+      joinedUsers: [],
+      loading: true,
+      hide: true,
+      showQuestionInfo: true,
+      showQuestionList: false,
+      examQuestionAndScore: {},
+    };
+  },
+  computed: {
+
+  },
+
+  methods: {
+    onClickHideButton() {
+      this.showQuestionInfo = false;
+      this.hide = false;
+    },
+
+    onClickShowButton() {
+      this.showQuestionInfo = true;
+      this.hide = true;
+    },
+
+    onClickChangePaperButton() {
+      this.showQuestionList=true;
+    },
+
+    updatePaper(){
+      this.showQuestionList = false;
+
+      this.getExamQuestions();
+    },
+
+    getExamQuestions() {
+      this.$apis.getExamQuestions(this.examId)
+          .then(_res => {
+            this.fetching = true;
+            this.examQuestionAndScore = _res.data.data.result.questionAndScore;
+            let questionsPromises = [];
+            let questionsIds = Object.entries(_res.data.data.result.questionAndScore);
+            questionsIds.forEach((v, ind) => {
+              questionsPromises[ind] = this.$apis.getQuestionInfo(v[0]);
+            });
+
+            Promise.all(questionsPromises)
+                .then((reps) => {
+                  this.examQuestions = reps.map((value) => {
+                    return value.data.data.result;
+                  });
+                })
+                .catch()
+                .finally(() => {
+                  this.fetching = false;
+                });
+          })
+          .catch(() => {
+            this.$router.push({
+              name: "edit-exams",
+              params: {
+                examId: this.examId
+              }
+            });
+          });
+    }
+  },
+
+  created() {
+    this.examId = this.$route.params.examId;
+    // const examId = 47;
+    console.log(this.examId);
+    this.$apis.getExamById(this.examId).then(_res => {
+      this.examInfo = _res.data.data.result;
+      console.log(this.examInfo);
+      this.examInfo.skills = this.examInfo.skills.toString();
+    });
+  },
+  mounted() {
+    this.getExamQuestions();
+
+    this.$apis.getExamJoinedUsers(this.examId).then(_res => {
+      this.joinedUsers = _res.data.data.result;
+      this.$apis.getAllReportByExamId(this.examId).then(res => {
+        const that = this;
+        this.joinedUsers.forEach((user) => {
+          res.data.data.result.forEach((report) => {
+            if (report.userId === user.id) {
+              that.$set(user, "score", report.score);
+              //todo 注意是否有用
+            }
+          });
+        });
+      });
+    });
+  },
+
+
+
+};
+</script>
+
+<style scoped>
+.box {
+  height: 80%;
+  /*overflow-y: auto;*/
+  display: flex;
+  flex-direction: column;
+}
+
+.el-card {
+  margin-bottom: 20px;
+}
+</style>

+ 1 - 0
src/views/PortalPage/components/CarouselCard.vue

@@ -22,6 +22,7 @@ export default {
   },
   async created() {
     const rootPoint = (await this.$apis.queryCarouselChart()).data.data;
+    console.log(rootPoint);
     for (let i = 0; i < rootPoint.length; i++) {
       this.contents[i] = rootPoint[i].imageUrl;
       this.link[i] = rootPoint[i].id;

+ 2 - 2
src/views/PortalPage/components/MyCourseCard.vue

@@ -7,7 +7,7 @@
       <el-col :span="2"/>
       <el-col :span="16">
         <el-row>
-          <el-col :span="18" class="title">{{ title }}</el-col>
+          <el-col :span="18" class="title" :title="this.title">{{ title.length>6?title.substring(0,6)+"...":title }}</el-col>
           <el-col :span="4">
             <el-tag v-if="state==='NOT_STARTED'" size="small" type="info">未开始</el-tag>
             <el-tag v-else-if="state==='RUNNING'" size="small">进行中</el-tag>
@@ -17,7 +17,7 @@
         <el-row class="info">
           <el-col>
             <el-row class="teacher">教师: {{ teacher }}</el-row>
-            <el-row class="time">开课时间: {{ startTime }}</el-row>
+            <el-row class="time">开课时间: {{ startTime.replace(/-/g,".") }}</el-row>
           </el-col>
         </el-row>
       </el-col>

+ 5 - 4
src/views/PortalPage/components/MyTestCard.vue

@@ -35,7 +35,7 @@
     </el-row>
 
     <el-row class="test-period">
-      {{ timePeriod }}
+      {{ timePeriod.replace(/-/g,".") }}
     </el-row>
   </el-card>
 </template>
@@ -70,9 +70,9 @@ export default {
     },
     course() {
       if (this.type === "eval") {
-        return "";
+        return "课程: " + this.testVO.course.name;
       } else {
-        return "所属课程: " + this.testVO.course;
+        return "课程: " + this.testVO.course;
       }
     },
     teacherName() {
@@ -200,11 +200,12 @@ export default {
 
 .course {
   font-size: x-small;
+  color: #999999;
 }
 
 .teacher {
   font-size: x-small;
-  color: #888888;
+  color: #999999;
 }
 
 .test-period {

+ 3 - 11
src/views/ProjectPage/ProjectPage.vue

@@ -49,6 +49,9 @@
           </span>
         </template>
         <div v-if="this.activeIndex === '1'">
+        </div>
+
+        <div v-if="this.activeIndex === '2' && isStudent">
           <el-tabs v-model="activeProjectPaneName" style="width: 100%">
             <el-tab-pane label="课程下项目" name="courseProject">
               <div v-for="(item, key) in courseProjectList" :key="key">
@@ -70,17 +73,6 @@
           </el-tabs>
         </div>
 
-        <div v-if="this.activeIndex === '2' && isStudent">
-              <div v-for="(item, key) in ProjectList_my" :key="key">
-                <div>
-                  <el-divider style="margin: 10px 0 20px 0" content-position="left">{{ key }}</el-divider>
-                </div>
-                <div class="project-page-main">
-                  <ProjectCard v-for="project in item" :key="project.id" :projectVO="project" type="myProject"/>
-                </div>
-              </div>
-        </div>
-
         <div v-if="this.activeIndex === '2' && isTeacher">
           教师请前往
           <span style="cursor: pointer;color: #409eff"

+ 10 - 10
vue.config.js

@@ -5,12 +5,12 @@ module.exports = {
   publicPath: process.env.BASE_URL,
   chainWebpack: config => {
     config
-        .plugin("html")
-        .tap(args => {
-          // NOTE 确定系统正式名称
-          args[0].title = "SEEC门户系统";
-          return args;
-        });
+      .plugin("html")
+      .tap(args => {
+        // NOTE 确定系统正式名称
+        args[0].title = "SEEC门户系统";
+        return args;
+      });
   },
   css: {
     loaderOptions: {
@@ -27,14 +27,14 @@ module.exports = {
           target: "http://localhost:8080/api",
           pathRewrite: {
             "^/api": ""
-          }
+          },
         }
       },
       port: 8000,
     },
     plugins: process.env.NODE_ENV === "development" ?
-        [
-          new WebpackBar(),
-        ] : []
+      [
+        new WebpackBar(),
+      ] : []
   },
 };