Browse Source

feat: Zhurui 创建Coder考试

Leone 4 years ago
parent
commit
43b3e0a4aa

+ 10 - 1
src/App.vue

@@ -1,6 +1,8 @@
 <template>
   <GlobalHeader v-if="this.$route.path!=='/auth'"/>
-  <router-view/>
+  <div class="main-body">
+    <router-view/>
+  </div>
   <GlobalFooter v-if="this.$route.path!=='/auth'"/>
 </template>
 
@@ -25,3 +27,10 @@ export default {
   },
 };
 </script>
+
+<style lang="scss" scoped>
+/*Zhurui修改,消除底部对主体元素的覆盖*/
+.main-body {
+  margin-bottom: 250px;
+}
+</style>

+ 18 - 0
src/api/coder.js

@@ -40,6 +40,22 @@ function getTeacherCoderExamsGroupByCourse(){
   return axios.get(`${prefix}/exam/teacher/groupByCourse`);
 }
 
+function retrievePageProblem(page, limit, typeStr, name){
+  return axios.get(`${prefix}/problem`, {
+    params: {
+      page,
+      limit,
+      typeStr,
+      name
+    }
+  });
+}
+
+function createCoderExam(data){
+  return axios.post(`${prefix}/exam/create`, data);
+}
+
+
 const coderAPIs = {
   getStudentCoderExams,
   getStudentCoderExamsGroupByCourse,
@@ -49,6 +65,8 @@ const coderAPIs = {
   getRecommendCoderExamsGroupByCourse,
   getTeacherCoderExamsByCourseId,
   getStudentCoderExamsByCourseId,
+  retrievePageProblem,
+  createCoderExam
 };
 
 export default coderAPIs;

+ 6 - 1
src/api/course/course.js

@@ -11,6 +11,10 @@ function getAllCourses() {
   return axios.get(`${prefix}/all`);
 }
 
+function getAllCoderCourses() {
+  return axios.get(`${prefix}/coder_all`);
+}
+
 function getExampleCourses() {
   return axios.get(`${prefix}/example`);
 }
@@ -74,6 +78,7 @@ function checkCourseCodeUnique(id, code) {
 const courseAPIs = {
   createCourse,
   getAllCourses,
+  getAllCoderCourses,
   getTeacherCourses,
   getStudentCourses,
   getCourseById,
@@ -87,4 +92,4 @@ const courseAPIs = {
   getCourseByEvalId,
 };
 
-export default courseAPIs;
+export default courseAPIs;

+ 58 - 47
src/components/GlobalFooter.vue

@@ -1,56 +1,60 @@
 <template>
-  <div id="global-footer-body">
-    <div id="global-footer-main">
-      <div id="global-footer-logo">
-        <img id="global-footer-logo-img" alt="err" src="@/assets/seec-logo--full.png">
-        <div id="global-footer-logo-text">软件工程教育云平台 Software Engineering Education Cloud 2022.07.16</div>
-      </div>
-      <div style="width: 10%"></div>
-      <div id="global-footer-about">
-        <div>关于我们</div>
-        <div id="global-footer-about-main">
-          <a href="javascript:void(0)">
-            <div>常见问题</div>
-          </a>
-          <a href="javascript:void(0)">
-            <div>意见反馈</div>
-          </a>
-          <a href="javascript:void(0)">
-            <div>联系我们</div>
-          </a>
-          <a href="javascript:void(0)">
-            <div>法律条款</div>
-          </a>
+  <div class="global-footer-wrapper">
+    <div id="global-footer-body">
+      <div id="global-footer-main">
+        <div id="global-footer-logo">
+          <img id="global-footer-logo-img" alt="err" src="@/assets/seec-logo--full.png">
+          <div id="global-footer-logo-text">软件工程教育云平台 Software Engineering Education Cloud 2022.07.16</div>
         </div>
-      </div>
-      <div id="global-footer-follow">
-        <div>关注我们</div>
-        <div id="global-footer-follow-main">
-          <a href="https://zhuanlan.zhihu.com/c_1220396784981487616" target="_blank">
-            <img alt="err" src="@/assets/icon-zhihu.jpg">
-          </a>
-          <a href="https://space.bilibili.com/507030405" target="_blank">
-            <img alt="err" src="@/assets/icon-bilibili.jpg">
-          </a>
+        <div style="width: 10%"></div>
+        <div id="global-footer-about">
+          <div>关于我们</div>
+          <div id="global-footer-about-main">
+            <a href="javascript:void(0)">
+              <div>常见问题</div>
+            </a>
+            <a href="javascript:void(0)">
+              <div>意见反馈</div>
+            </a>
+            <a href="javascript:void(0)">
+              <div>联系我们</div>
+            </a>
+            <a href="javascript:void(0)">
+              <div>法律条款</div>
+            </a>
+          </div>
         </div>
-      </div>
-      <div id="global-footer-collaborator">
-        <div>合作机构</div>
-        <div id="global-footer-collaborator-main">
-          <a href="https://www.nju.edu.cn" target="_blank">
-            <div>南京大学</div>
-          </a>
-
-          <a href="javascript:void(0)">
-            <div>阿里巴巴</div>
-          </a>
+        <div id="global-footer-follow">
+          <div>关注我们</div>
+          <div id="global-footer-follow-main">
+            <a href="https://zhuanlan.zhihu.com/c_1220396784981487616" target="_blank">
+              <img alt="err" src="@/assets/icon-zhihu.jpg">
+            </a>
+            <a href="https://space.bilibili.com/507030405" target="_blank">
+              <img alt="err" src="@/assets/icon-bilibili.jpg">
+            </a>
+          </div>
+        </div>
+        <div id="global-footer-collaborator">
+          <div>合作机构</div>
+          <div id="global-footer-collaborator-main">
+            <a href="https://www.nju.edu.cn" target="_blank">
+              <div>南京大学</div>
+            </a>
+
+            <a href="javascript:void(0)">
+              <div>阿里巴巴</div>
+            </a>
+          </div>
         </div>
       </div>
-    </div>
-    <div id="global-footer-bottom">
-      <div>©2020 NJU SEECODER TEAM<br/>苏ICP备19073881号-1</div>
+      <div id="global-footer-bottom">
+        <div>©2020 NJU SEECODER TEAM<br/>苏ICP备19073881号-1</div>
+      </div>
     </div>
   </div>
+
+
 </template>
 
 <script>
@@ -60,6 +64,13 @@ export default {
 </script>
 
 <style scoped>
+/*Zhurui修改,footer固定在视口的最下方*/
+.global-footer-wrapper {
+  position: fixed;
+  bottom: 0;
+  width: 100%;
+}
+
 #global-footer-body {
   display: flex;
   flex-direction: column;
@@ -149,4 +160,4 @@ export default {
   height: 50px;
 }
 
-</style>
+</style>

+ 2 - 0
src/utils/coder-utils.js

@@ -41,6 +41,8 @@ export function problemTypeToTagContent(type) {
     switch (type) {
         case "JAVA":
             return "Java";
+        case "JAVA_JVM":
+            return "Java虚拟机";
         case "JAVA_COVERAGE":
             return "Java覆盖测试";
         case "PYTHON":

+ 33 - 0
src/utils/debounce.js

@@ -0,0 +1,33 @@
+/**
+ * 函数防抖: 当事件被触发 n 秒后再执行回调,如果在 n 秒内又被触发,则重新计时。
+ * @returns {(function(): void)|*}
+ * @param func
+ * @param wait
+ */
+// export const debounce = (func, wait) => {
+//     let delay = wait || 500;
+//     let timer = null;
+//     return () => {
+//         const _this = this;
+//         let args = arguments;
+//         if(timer) clearTimeout(timer);
+//         timer = setTimeout(() => {
+//             timer = null;
+//             func.apply(_this, args);
+//         }, delay);
+//     };
+// };
+
+export function debounce(fn, timer){
+    // console.log("fn", fn);
+    let timer_out = null;
+
+    return function(...args){
+        // 清除计时器
+        if(timer_out) clearTimeout(timer_out);
+
+        timer_out = setTimeout(()=>{
+            fn.apply(this,args);
+        },timer);
+    };
+}

+ 290 - 13
src/views/ManagePage/ManagePage.vue

@@ -60,16 +60,10 @@
           <span style="font-weight: bold; font-size: larger">
             全部测试
           </span>
-<!--            <div class="course-type-selector">-->
-<!--              <el-select v-model="examType" class="m-2" placeholder="测试类型">-->
-<!--                <el-option-->
-<!--                  v-for="item in examTypeOptions"-->
-<!--                  :key="item.value"-->
-<!--                  :label="item.label"-->
-<!--                  :value="item.value"-->
-<!--                />-->
-<!--              </el-select>-->
-<!--            </div>-->
+            <el-button class="ml-16" plain type="primary"
+                       @click="dialogExamTitle='新增测试';dialogExamVisible=true;switchExamDisabled=false;">
+              新建测试
+            </el-button>
           </div>
         </template>
         <el-tabs v-model="activeTestPaneName">
@@ -96,6 +90,8 @@
         </el-tabs>
       </el-card>
     </el-col>
+
+<!--    创建门户课程-->
     <el-dialog v-model="dialogVisible" :before-close="handleFormCancel" :width="700" :title="dialogTitle">
       <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
         <el-form-item label="课程名称" prop="name">
@@ -171,6 +167,140 @@
         <el-button @click="handleFormCancel">取消</el-button>
       </div>
     </el-dialog>
+
+
+<!--    创建Coder考试-->
+    <el-dialog v-model="dialogExamVisible" :before-close="handleExamFormCancel" :width="1200" :title="dialogExamTitle">
+      <el-form ref="examFormRef" :model="examForm" :rules="examFormRules" label-width="150px">
+        <el-form-item label="测试种类" prop="type">
+          <el-select v-model="examForm.examType" class="m-2" placeholder="请选择种类">
+            <el-option
+                v-for="item in examCategoryOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-if="isCoder" label="所属课程" prop="course">
+          <el-select v-model="examForm.course" class="m-2" placeholder="请选择课程">
+            <el-option
+                v-for="item in courseOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item v-if="isCoder" label="测试名称" prop="name">
+          <el-input class="course-form-item" v-model="examForm.name" placeholder="请输入课程名称"></el-input>
+        </el-form-item>
+        <el-form-item v-if="isCoder" label="测试时间" prop="range">
+          <el-date-picker v-model="examForm.range" range-separator="至"
+                          start-placeholder="开始时间" end-placeholder="结束时间" type="datetimerange">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item v-if="isCoder" label="允许学生查看控制台">
+          <el-switch v-model="examForm.isConsoleOutputShownForStu" :disabled="switchExamDisabled" active-text="开启" inactive-text="不开启"/>
+        </el-form-item>
+        <el-form-item v-if="isCoder" label="启用代码分析">
+          <el-switch v-model="examForm.isCodeAnalysisNeeded" :disabled="switchExamDisabled" active-text="开启" inactive-text="不开启"/>
+        </el-form-item>
+        <el-form-item v-if="isCoder" label="题目列表">
+          <el-button type="text" @click="showProblemDialog">选择题目</el-button>
+        </el-form-item>
+        <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"
+            >
+              <template #title>
+                <span>{{ problem.name }}</span>
+                <el-tag class="problem-type">{{ problem.type }}</el-tag>
+              </template>
+              <b>题目描述:</b>
+              <span class="problem-description">{{ problem.description }}</span>
+              <div class="remove-button">
+                <el-button
+                    type="danger"
+                    size="mini"
+                    @click="$store.commit('removeProblem', problem)"
+                >移除该题</el-button
+                >
+              </div>
+            </el-collapse-item>
+          </el-collapse>
+        </el-form-item>
+      </el-form>
+      <div style="margin: auto;width: fit-content">
+        <el-button type="primary" @click="handleExamCreate">确定</el-button>
+        <el-button @click="handleExamFormCancel">取消</el-button>
+      </div>
+    </el-dialog>
+
+
+<!--    展示Coder题目列表-->
+    <el-dialog v-model="dialogProblemVisible" :width="1200">
+      <el-input v-model="dialogProblemName"
+                style="width:200px;"
+                placeholder="搜索"
+      />
+      <el-select v-model="dialogProblemTypeStr" style="width: 300px" @change="fetchProblemData(1)">
+        <el-option v-for="item in problemTypeOptions" :key="item.value" :value="item.value">
+          {{item.label}}
+        </el-option>
+      </el-select>
+      <el-table :data="problemList" >
+        <el-table-column type="expand">
+          <template #default="props">
+            <div>{{ props.row.description }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="name" label="名称"></el-table-column>
+        <el-table-column label="类型">
+          <template #default="scope">
+            <span>{{ problemTypeToTagContent(scope.row.type) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="上传时间">
+          <template #default="scope">
+            <span>{{ formatTimestamp(scope.row.createdAt)}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="200">
+          <template #default="scope">
+            <el-button
+                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)"
+            >移出试题清单</el-button
+            >
+            <el-button
+                v-else-if="addEnable(scope.row)"
+                type="success"
+                size="mini"
+                @click="$store.commit('addProblem', scope.row)"
+            >加入试题清单</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="problemPageInfo.total"
+          @current-change="changePage"
+      />
+    </el-dialog>
   </el-row>
 </template>
 
@@ -180,6 +310,8 @@ import { ElMessage, ElMessageBox } from "element-plus";
 import client from "@/utils/oss";
 import { Back, Document } from "@element-plus/icons-vue";
 import TestCard from "@/components/TestCard";
+import {debounce} from "@/utils/debounce";
+import {formatTimestamp, problemTypeOptions, problemTypeToTagContent} from "@/utils/coder-utils";
 
 export default {
   name: "ManagePage",
@@ -231,7 +363,9 @@ export default {
       activeIndex: "1",
       activeTestPaneName: "eval",
       dialogVisible: false,
+      dialogExamVisible: false,
       dialogTitle: "新增课程",
+      dialogExamTitle: "新增测试",
       form: {
         id: null,
         name: "",
@@ -250,6 +384,14 @@ export default {
         coderLink: false,
         evalLink: false,
       },
+      examForm: {
+        examType: "coder",
+        course: "",
+        name: "",
+        range: [],
+        isCodeAnalysisNeeded: false,
+        isConsoleOutputShownForStu: false
+      },
       formRules: {
         name: {
           required: true, validator: validateName, trigger: "blur",
@@ -273,7 +415,25 @@ export default {
           required: true, validator: validatePrice, trigger: "blur",
         },
       },
+      examFormRules: {
+        // type: {
+        //   required: true, message: "请选择测试种类",
+        // },
+        name: {
+          required: true, message: "请输入测试名称", trigger: "blur",
+        },
+        course: {
+          required: true, message: "请选择测试名称",
+        },
+        range: {
+          required: true, message: "请选择测试时间段", trigger: "blur",
+        }
+      },
+      coderCourses: [],
+      courseOptions: [],
+
       switchDisabled: false,
+      switchExamDisabled: false,
       //
       myCourseList: [],
       courseType: "all",
@@ -292,6 +452,17 @@ export default {
         { value: "start", label: "正在进行" },
         { value: "end", label: "已结束" },
       ],
+      examCategoryOptions: [
+        { value: "eval", label: "评测题"},
+        { value: "coder", label: "代码题"},
+      ],
+      dialogProblemVisible: false,
+      dialogProblemPage: 1,
+      dialogProblemLimit: 10,
+      dialogProblemTypeStr: "",
+      dialogProblemName: "",
+      problemList: [],
+      problemPageInfo: {}
     };
   },
   computed: {
@@ -302,6 +473,12 @@ export default {
       if (this.courseType === "end") return this.myCourseList.filter(i => this.state(i) === "FINISHED");
       else return this.myCourseList;
     },
+    isCoder() {
+      return this.examForm.examType === "coder";
+    },
+    problemTypeOptions() {
+      return problemTypeOptions(true);
+    }
   },
   mounted() {
     this.$apis.getTeacherCourses().then(res => {
@@ -313,8 +490,26 @@ export default {
     this.$apis.getTeacherCoderExamsGroupByCourse().then(res => {
       this.coderExamList = res.data.data;
     });
+    this.$apis.getAllCoderCourses().then(_res => {
+      this.coderCourses = _res.data.res;
+      this.courseOptions = this.coderCourses.map(i => {
+        return {
+          value: i.id,
+          label: i.name
+        };
+      });
+    });
+  },
+  watch: {
+    //TODO 防抖!
+    dialogProblemName() {
+      debounce(this.fetchProblemData, 500);
+    }
   },
+
   methods: {
+    formatTimestamp,
+    problemTypeToTagContent,
     state(courseVO) {
       const now = new Date();
       if (new Date(courseVO.endTime) < now) {
@@ -366,7 +561,8 @@ export default {
       function createOrUpdateCourse() {
         console.log(_this.form);
         if (_this.form.id === null) {
-          _this.$apis.createCourse({
+          console.log("进入创建");
+          let data = {
             name: _this.form.name,
             teacher: {
               id: _this.$store.state.user.id,
@@ -382,8 +578,11 @@ export default {
             price: Number(_this.form.price),
             coderId: _this.form.coderLink ? -1 : null,
             evalId: _this.form.evalLink ? -1 : null,
-          })
+          };
+          console.log(data);
+          _this.$apis.createCourse(data)
             .then((res) => {
+              console.log(res);
               _this.myCourseList.push(res.data.data);
               _this.dialogVisible = false;
               _this.clearForm();
@@ -423,7 +622,7 @@ export default {
         }
       }
     },
-    handleFormCancel() {
+    handleFormCancel(done) {
       ElMessageBox.confirm("是否确认退出?退出后将不保存表格信息。", "提示", {
         confirmButtonText: "确认退出",
         cancelButtonText: "我再想想",
@@ -433,11 +632,89 @@ export default {
         .then(() => {
           this.dialogVisible = false;
           this.clearForm();
+          // Zhurui修改,关闭dialog
+          done();
         })
         .catch((err) => {
           console.log(err);
         });
     },
+    handleExamCreate(){
+      const form = this.$refs.examFormRef;
+      form.validate((valid) => {
+        if (valid) {
+          console.log("validate 成功");
+          this.createCoderExam();
+        } else {
+          console.log("validate 失败了");
+          return false;
+        }
+      });
+    },
+    createCoderExam(){
+      //创建Coder考试
+      this.$apis.createCoderExam({
+        course: this.examForm.course,
+        name: this.examForm.name,
+        startAt: this.examForm.range[0].getTime()/1000,
+        endAt: this.examForm.range[1].getTime()/1000,
+        problems: this.$store.state.problem.cart.map((item) => item.id),
+        isConsoleOutputShownForStu: this.examForm.isConsoleOutputShownForStu
+      }).then(_res => {
+        this.$store.commit("clearProblem");
+        ElMessage.success("创建测试成功!");
+        this.dialogExamVisible = false;
+      }).catch(_err => {
+        console.log(_err);
+        ElMessage.warning("创建测试失败!");
+      });
+    },
+    handleExamFormCancel(done) {
+      //关闭dialog的提示
+      ElMessageBox.confirm("是否确认退出?退出后将不保存表格信息。", "提示", {
+        confirmButtonText: "确认退出",
+        cancelButtonText: "我再想想",
+        type: "warning",
+        center: true,
+      })
+          .then(() => {
+            this.dialogExamVisible = false;
+            // this.clearForm();
+            //TODO 清除表格信息
+            done();
+          })
+          .catch((err) => {
+            console.log(err);
+          });
+    },
+    showProblemDialog(){ //打开弹框,获取初始数据
+      this.fetchProblemData();
+      this.dialogProblemVisible = true;
+    },
+    // handleProblemChange(){
+    //   debounce(fetchProblemData,1000);
+    // },
+    checkDetails(){
+      ElMessage.warning("暂不支持查看详情!");
+    },
+    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;
+          });
+    },
+    removeEnable(problem) { //是否可以移除题目
+      return this.$store.state.problem.cart.length && this.$store.state.problem.cart
+          .map((item) => item.id)
+          .includes(problem.id);
+    },
+    addEnable(problem) { //是否可以添加题目
+      return this.$store.getters.isTeacher && problem.modifiedAt;
+    },
+    changePage(val){
+      this.fetchProblemData(val);
+    },
     clearForm() {
       this.form = {
         id: null,

+ 14 - 0
src/views/ManagePage/component/selectProblemModal.vue

@@ -0,0 +1,14 @@
+<template>
+<!--TODO 封装成组件-->
+
+</template>
+
+<script>
+export default {
+  name: "selectProblemModal"
+}
+</script>
+
+<style scoped>
+
+</style>