Эх сурвалжийг харах

Merge branch 'master' of http://git.qmth.com.cn/themis/frontend-admin

zhangjie 4 жил өмнө
parent
commit
828fcb256a
44 өөрчлөгдсөн 2354 нэмэгдсэн , 285 устгасан
  1. 1 0
      package.json
  2. 5 3
      src/api/examwork-activity.js
  3. 20 0
      src/api/examwork-course.js
  4. 1 6
      src/api/examwork-exam.js
  5. 74 0
      src/api/examwork-examstudent.js
  6. 23 0
      src/api/examwork-invigilate.js
  7. 50 0
      src/api/examwork-student.js
  8. 16 0
      src/api/examwork-task.js
  9. 6 4
      src/api/system-info.js
  10. 38 0
      src/api/system-org.js
  11. 68 0
      src/components/ActivitySelect.vue
  12. 68 0
      src/components/CourseSelect.vue
  13. 67 0
      src/components/ExamSelect.vue
  14. 1 0
      src/components/ExamTypeSelect.vue
  15. 1 0
      src/components/OrgSelect.vue
  16. 1 0
      src/components/RoleSelect.vue
  17. 3 1
      src/components/StateSelect.vue
  18. 38 0
      src/components/registerComponents.js
  19. 2 1
      src/features/Login/Login.vue
  20. 8 7
      src/features/examwork/ActivityManagement/ActivityManagement.vue
  21. 4 2
      src/features/examwork/ActivityManagement/ActivityManagementDialog.vue
  22. 155 0
      src/features/examwork/CourseManagement/CourseManagement.vue
  23. 0 0
      src/features/examwork/CourseManagement/CoursePaperDialog.vue
  24. 0 0
      src/features/examwork/CourseManagement/PaperImportDialog.vue
  25. 85 0
      src/features/examwork/ExamManagement/CopyExamDialog.vue
  26. 308 193
      src/features/examwork/ExamManagement/ExamEdit.vue
  27. 35 18
      src/features/examwork/ExamManagement/ExamManagement.vue
  28. 122 0
      src/features/examwork/ExamStudentImport/ExamStudentImport.vue
  29. 209 0
      src/features/examwork/ExamStudentManagement/ExamStudentManagement.vue
  30. 141 0
      src/features/examwork/ExamStudentManagement/ExamStudentManagementDialog.vue
  31. 132 0
      src/features/examwork/InvigilateManagement/InvigilateManagement.vue
  32. 137 0
      src/features/examwork/InvigilateManagement/InvigilateManagementDialog.vue
  33. 154 0
      src/features/examwork/StudentManagement/StudentManagement.vue
  34. 40 8
      src/features/system/OrgManagement/OrgManagement.vue
  35. 164 0
      src/features/system/OrgManagement/OrgManagementDialog.vue
  36. 82 0
      src/features/system/OrgManagement/UploadFile.vue
  37. 19 16
      src/features/system/UserManagement/UserManagement.vue
  38. 5 0
      src/filters/index.js
  39. 1 0
      src/main.js
  40. 40 0
      src/router/index.js
  41. 12 0
      src/utils/utils.js
  42. 3 1
      src/views/Layout/components/AppMain.vue
  43. 10 25
      src/views/Layout/components/menu.js
  44. 5 0
      yarn.lock

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
     "element-ui": "^2.13.2",
     "flv.js": "^1.5.0",
     "js-cookie": "^2.2.1",
+    "js-md5": "^0.7.3",
     "lodash-es": "^4.17.15",
     "moment": "^2.27.0",
     "query-string": "^6.13.1",

+ 5 - 3
src/api/activity.js → src/api/examwork-activity.js

@@ -25,10 +25,10 @@ export function saveActivity({
 }) {
   const data = pickBy(
     {
+      examId,
       id,
       code,
       enable,
-      examId,
       finishTime,
       maxDurationSeconds,
       openingSeconds,
@@ -37,7 +37,9 @@ export function saveActivity({
     },
     (v) => v !== ""
   );
-  return httpApp.post("/api/admin/activity/save", data);
+  return httpApp.post("/api/admin/activity/save", [data]);
 }
 
-export function toggleEnableActivity() {}
+export function toggleEnableActivity({ examId, id, enable }) {
+  return httpApp.post("/api/admin/activity/save", [{ examId, id, enable }]);
+}

+ 20 - 0
src/api/examwork-course.js

@@ -0,0 +1,20 @@
+import { httpApp } from "@/plugins/axiosIndex";
+import { pickBy } from "lodash-es";
+import { object2QueryString } from "@/utils/utils";
+
+export function searchCourses({
+  examId = "",
+  courseName = "",
+  enable = "",
+  pageNumber = 1,
+  pageSize = 10,
+}) {
+  const data = pickBy(
+    { examId, courseName, enable, pageNumber, pageSize },
+    (v) => v !== ""
+  );
+  if (data.examId)
+    return httpApp.post(
+      "/api/admin/exam/course/query?" + object2QueryString(data)
+    );
+}

+ 1 - 6
src/api/exam.js → src/api/examwork-exam.js

@@ -3,17 +3,12 @@ import { pickBy } from "lodash-es";
 import { object2QueryString } from "@/utils/utils";
 
 export function searchExams({
-  role,
-  loginName = "",
   name = "",
   enable = "",
   pageNumber = 1,
   pageSize = 10,
 }) {
-  const data = pickBy(
-    { role, loginName, name, enable, pageNumber, pageSize },
-    (v) => v !== ""
-  );
+  const data = pickBy({ name, enable, pageNumber, pageSize }, (v) => v !== "");
   return httpApp.post("/api/admin/exam/query?" + object2QueryString(data));
 }
 

+ 74 - 0
src/api/examwork-examstudent.js

@@ -0,0 +1,74 @@
+import { httpApp } from "@/plugins/axiosIndex";
+import { pickBy } from "lodash-es";
+import { object2QueryString } from "@/utils/utils";
+
+export function searchExamStudents({
+  examId = "",
+  activityId = "",
+  roomCode = "",
+  courseCode = "",
+  name = "",
+  identity = "",
+  grade = "",
+  classNo = "",
+  pageNumber = 1,
+  pageSize = 10,
+}) {
+  const data = pickBy(
+    {
+      examId,
+      activityId,
+      roomCode,
+      name,
+      courseCode,
+      identity,
+      grade,
+      classNo,
+      pageNumber,
+      pageSize,
+    },
+    (v) => v !== ""
+  );
+  return httpApp.post(
+    "/api/admin/examStudent/query?" + object2QueryString(data)
+  );
+}
+
+export function saveExamStudent({
+  id = "",
+  examId = "",
+  activityId = "",
+  roomCode = "",
+  courseCode = "",
+  name = "",
+  identity = "",
+  grade = "",
+  classNo = "",
+  enable = 1,
+}) {
+  const data = pickBy(
+    {
+      id,
+      examId,
+      activityId,
+      roomCode,
+      name,
+      courseCode,
+      identity,
+      grade,
+      classNo,
+      enable,
+    },
+    (v) => v !== ""
+  );
+
+  return httpApp.post("/api/admin/examStudent/save", [data]);
+}
+
+export function toggleEnableExamStudent({ id, enable }) {
+  return httpApp.post("/api/admin/examStudent/save", [{ id, enable }]);
+}
+
+export function toggleEnableExamStudentArray(ary = []) {
+  return httpApp.post("/api/admin/examStudent/save", ary);
+}

+ 23 - 0
src/api/examwork-invigilate.js

@@ -0,0 +1,23 @@
+import { httpApp } from "@/plugins/axiosIndex";
+import { pickBy } from "lodash-es";
+import { object2QueryString } from "@/utils/utils";
+
+export function searchInvigilators({
+  roomCode = "",
+  userId = "",
+  pageNumber = 1,
+  pageSize = 10,
+}) {
+  const data = pickBy(
+    { roomCode, userId, pageNumber, pageSize },
+    (v) => v !== ""
+  );
+  return httpApp.post(
+    "/api/admin/invigilateUser/query?" + object2QueryString(data)
+  );
+}
+
+export function saveInvigilator({ roomCode = "", userIds = "" }) {
+  const data = pickBy({ roomCode, userIds }, (v) => v !== "");
+  return httpApp.post("/api/admin/invigilateUser/save", data);
+}

+ 50 - 0
src/api/examwork-student.js

@@ -0,0 +1,50 @@
+import { httpApp } from "@/plugins/axiosIndex";
+import { pickBy } from "lodash-es";
+import { object2QueryString, AESString } from "@/utils/utils";
+
+export function searchStudents({
+  orgId = "",
+  role,
+  loginName = "",
+  name = "",
+  enable = "",
+  pageNumber = 1,
+  pageSize = 10,
+}) {
+  const data = pickBy(
+    { orgId, role, loginName, name, enable, pageNumber, pageSize },
+    (v) => v !== ""
+  );
+  return httpApp.post("/api/admin/student/query?" + object2QueryString(data));
+}
+
+export function saveStudent({
+  orgId = "",
+  id = "",
+  roleCode,
+  loginName = "",
+  name = "",
+  enable = "",
+  password = "",
+  mobileNumber = "",
+}) {
+  const data = pickBy(
+    { orgId, id, roleCode, loginName, name, enable, password, mobileNumber },
+    (v) => v !== ""
+  );
+  return httpApp.post("/api/admin/student/save", {
+    ...data,
+    ...(password.length > 0 ? { password: AESString(password) } : {}),
+  });
+}
+
+export function toggleEnableStudent({ id, enable }) {
+  return httpApp.post("/api/admin/student/toggle", { id, enable });
+}
+
+export function resetStudentPassword({ id, password }) {
+  return httpApp.post("/api/admin/student/updatePwd", {
+    id,
+    password: AESString(password),
+  });
+}

+ 16 - 0
src/api/examwork-task.js

@@ -0,0 +1,16 @@
+import { httpApp } from "@/plugins/axiosIndex";
+import { pickBy } from "lodash-es";
+import { object2QueryString } from "@/utils/utils";
+
+export function searchTasks({ type = "", pageNumber = 1, pageSize = 10 }) {
+  const data = pickBy({ type, pageNumber, pageSize }, (v) => v !== "");
+  if (data.type)
+    return httpApp.post("/api/admin/task/query?" + object2QueryString(data));
+}
+
+export function downloadFile({ id = "", fileType = "" }) {
+  const data = pickBy({ id, fileType }, (v) => v !== "");
+  return httpApp.post(
+    "/api/admin/task/file/download?" + object2QueryString(data)
+  );
+}

+ 6 - 4
src/api/system-info.js

@@ -1,10 +1,12 @@
 import { httpApp } from "@/plugins/axiosIndex";
 import { object2QueryString } from "@/utils/utils";
 
-export function uploadFile({ file }) {
+export function uploadFile({ file, md5 }) {
+  const form = new FormData();
+  form.append("file", file);
   return httpApp.post(
-    "/api/admin/user/query?" + object2QueryString({ type: "frontend" }),
-    { file: file },
-    { headers: { "Content-Type": "multipart/form-data" } }
+    "/api/admin/sys/file/upload?" + object2QueryString({ type: "frontend" }),
+    form,
+    { headers: { "Content-Type": "multipart/form-data", md5: md5 } }
   );
 }

+ 38 - 0
src/api/system-org.js

@@ -15,3 +15,41 @@ export function searchOrgs({
   );
   return httpApp.post("/api/admin/org/query?" + object2QueryString(data));
 }
+
+export function toggleEnableOrg({ id, enable }) {
+  return httpApp.post("/api/admin/org/enable", { id, enable });
+}
+
+export function searchOrg(id) {
+  return httpApp.post("/api/admin/sys/org/query?" + object2QueryString({ id }));
+}
+
+export function saveOrg({
+  id = "",
+  name = "",
+  code = "",
+  contactName = "",
+  contactPhone = "",
+  logo = null,
+  enableSimulate = 0,
+  enableMonitorRecord = 0,
+  enableLiveness = 0,
+  enable = 0,
+}) {
+  const data = pickBy(
+    {
+      id,
+      name,
+      code,
+      contactName,
+      contactPhone,
+      // logo,
+      enableSimulate,
+      enableMonitorRecord,
+      enableLiveness,
+      enable,
+    },
+    (v) => v !== ""
+  );
+  return httpApp.post("/api/admin/org/save", { ...data, logo });
+}

+ 68 - 0
src/components/ActivitySelect.vue

@@ -0,0 +1,68 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="size-select"
+    placeholder="请选择"
+    @change="select"
+    style="width: 100px;"
+    filterable
+    remote
+    :remote-method="search"
+    clearable
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :label="item.code"
+      :value="item.id"
+    >
+      <span>{{ item.code }}</span>
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { searchActivities } from "@/api/examwork-activity";
+
+export default {
+  name: "ActivitySelect",
+  props: {
+    value: String,
+    examId: String,
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: "",
+    };
+  },
+  async created() {
+    this.search();
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      },
+    },
+  },
+  methods: {
+    async search(query) {
+      const res = await searchActivities({
+        examId: this.examId,
+        code: query,
+        pageNumber: 1,
+        pageSize: 30,
+      });
+      this.optionList = res?.data.data.records.records;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit("change", this.selected);
+    },
+  },
+};
+</script>
+
+<style></style>

+ 68 - 0
src/components/CourseSelect.vue

@@ -0,0 +1,68 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="size-select"
+    placeholder="请选择"
+    @change="select"
+    style="width: 100px;"
+    filterable
+    remote
+    :remote-method="search"
+    clearable
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.courseCode"
+      :label="item.courseName"
+      :value="item.courseCode"
+    >
+      <span>{{ item.courseName }}</span>
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { searchCourses } from "@/api/examwork-course";
+
+export default {
+  name: "CourseSelect",
+  props: {
+    value: String,
+    examId: String,
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: "",
+    };
+  },
+  async created() {
+    this.search();
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      },
+    },
+  },
+  methods: {
+    async search(query) {
+      const res = await searchCourses({
+        examId: this.examId,
+        courseName: query,
+        pageNumber: 1,
+        pageSize: 30,
+      });
+      this.optionList = res?.data.data.records.records;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit("change", this.selected);
+    },
+  },
+};
+</script>
+
+<style></style>

+ 67 - 0
src/components/ExamSelect.vue

@@ -0,0 +1,67 @@
+<template>
+  <el-select
+    v-model="selected"
+    class="size-select"
+    placeholder="请选择"
+    @change="select"
+    style="width: 100px;"
+    filterable
+    remote
+    :remote-method="search"
+    clearable
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :label="item.name"
+      :value="item.id"
+    >
+      <span>{{ item.name }}</span>
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { searchExams } from "@/api/examwork-exam";
+
+export default {
+  name: "ExamSelect",
+  props: {
+    value: String,
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: "",
+    };
+  },
+  async created() {
+    this.search();
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      },
+    },
+  },
+  methods: {
+    async search(query) {
+      const res = await searchExams({
+        name: query,
+        pageNumber: 1,
+        pageSize: 30,
+      });
+      // console.log(res.data);
+      this.optionList = res.data.data.records.records;
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit("change", this.selected);
+    },
+  },
+};
+</script>
+
+<style></style>

+ 1 - 0
src/components/ExamTypeSelect.vue

@@ -5,6 +5,7 @@
     placeholder="请选择"
     @change="select"
     style="width: 100px;"
+    clearable
   >
     <el-option
       v-for="item in optionList"

+ 1 - 0
src/components/OrgSelect.vue

@@ -5,6 +5,7 @@
     placeholder="请选择"
     @change="select"
     style="width: 100px;"
+    clearable
   >
     <el-option
       v-for="item in optionList"

+ 1 - 0
src/components/RoleSelect.vue

@@ -6,6 +6,7 @@
     @change="select"
     style="width: 100px;"
     :multiple="multiple"
+    clearable
   >
     <el-option
       v-for="item in optionList"

+ 3 - 1
src/components/StateSelect.vue

@@ -5,6 +5,7 @@
     placeholder="请选择"
     @change="select"
     style="width: 100px;"
+    clearable
   >
     <el-option
       v-for="item in optionList"
@@ -25,10 +26,11 @@ export default {
       type: Number,
       default: 1,
     },
+    options: { type: Array, default: () => null },
   },
   data() {
     return {
-      optionList: [
+      optionList: this.options || [
         { code: 0, name: "禁用" },
         { code: 1, name: "启用" },
       ],

+ 38 - 0
src/components/registerComponents.js

@@ -0,0 +1,38 @@
+import Vue from "vue";
+// import upperFirst from "lodash/upperFirst";
+// import camelCase from "lodash/camelCase";
+
+const requireComponent = require.context(
+  // The relative path of the components folder
+  "./",
+  // Whether or not to look in subfolders
+  false,
+  // The regular expression used to match base component filenames
+  /[A-Z]\w+\.(vue|js)$/
+);
+
+requireComponent.keys().forEach((fileName) => {
+  // Get component config
+  const componentConfig = requireComponent(fileName);
+
+  // Get PascalCase name of component
+  const componentName =
+    // upperFirst(
+    //   camelCase(
+    // Gets the file name regardless of folder depth
+    fileName
+      .split("/")
+      .pop()
+      .replace(/\.\w+$/, "");
+  //   )
+  // );
+
+  // Register component globally
+  Vue.component(
+    componentName,
+    // Look for the component options on `.default`, which will
+    // exist if the component was exported with `export default`,
+    // otherwise fall back to module's root.
+    componentConfig.default || componentConfig
+  );
+});

+ 2 - 1
src/features/Login/Login.vue

@@ -7,7 +7,7 @@
         在线考试管理系统
       </h2> -->
       <div class="form-container">
-        <div>
+        <div class="text-center">
           <img :src="schoolLogo" class="school-logo" alt="学校logo" />
           <div class="text-center title">在线考试平台后台管理系统</div>
         </div>
@@ -258,6 +258,7 @@ export default {
   text-align: center;
   height: 60px;
   width: 200px;
+  object-fit: cover;
 }
 .title {
   color: #626a82;

+ 8 - 7
src/features/examwork/ActivityManagement/ActivityManagement.vue

@@ -10,8 +10,8 @@
     </el-form>
 
     <el-table :data="tableData" stripe style="width: 100%;">
-      <el-table-column type="selection" width="40" />
-      <el-table-column width="55" label="ID">
+      <el-table-column type="selection" width="42" />
+      <el-table-column width="100" label="ID">
         <span slot-scope="scope">{{ scope.row.id }}</span>
       </el-table-column>
       <el-table-column width="200" label="场次代码">
@@ -79,7 +79,10 @@
 </template>
 
 <script>
-import { searchActivities, toggleEnableActivity } from "@/api/activity";
+import {
+  searchActivities,
+  toggleEnableActivity,
+} from "@/api/examwork-activity";
 import ActivityManagementDialog from "./ActivityManagementDialog";
 
 export default {
@@ -95,10 +98,7 @@ export default {
   data() {
     return {
       form: {
-        orgId: "",
-        roleCode: "",
-        loginName: "",
-        name: "",
+        code: "",
         enableState: null,
       },
       tableData: [],
@@ -139,6 +139,7 @@ export default {
     },
     async toggleEnableActivity(activity) {
       await toggleEnableActivity({
+        examId: this.examId,
         id: activity.id,
         enable: activity.enable === 0 ? 1 : 0,
       });

+ 4 - 2
src/features/examwork/ActivityManagement/ActivityManagementDialog.vue

@@ -66,7 +66,7 @@
 
 <script>
 import MinuteInput from "@/components/MinuteInput";
-import { saveActivity } from "@/api/activity";
+import { saveActivity } from "@/api/examwork-activity";
 export default {
   name: "ActivityManagementDialog",
   components: { MinuteInput },
@@ -89,12 +89,13 @@ export default {
         prepareSeconds: 0,
         openingSeconds: 0,
         maxDurationSeconds: 0,
+        enable: 0,
       },
       rules: {},
     };
   },
   watch: {
-    user(val) {
+    activity(val) {
       let tmp = { ...val };
       if (!tmp.id) {
         tmp = {
@@ -104,6 +105,7 @@ export default {
           prepareSeconds: 0,
           openingSeconds: 0,
           maxDurationSeconds: 0,
+          enable: 0,
         };
       }
       this.form = tmp;

+ 155 - 0
src/features/examwork/CourseManagement/CourseManagement.vue

@@ -0,0 +1,155 @@
+<template>
+  <div>
+    <el-form ref="form" :model="form" :rules="rules" inline>
+      <el-form-item label="批次名称" prop="examId">
+        <ExamSelect v-model="form.examId" />
+      </el-form-item>
+      <el-form-item label="科目名称">
+        <CourseSelect :examId="form.examId" v-model="form.code" />
+      </el-form-item>
+      <el-form-item label="科目名称">
+        <StateSelect
+          :options="[
+            { code: 0, name: '未绑卷' },
+            { code: 1, name: '已绑卷' },
+          ]"
+          v-model="form.hasPaper"
+        />
+      </el-form-item>
+      <el-button @click="searchForm">查询</el-button>
+      <el-button>导入</el-button>
+    </el-form>
+
+    <el-table :data="tableData" stripe style="width: 100%;">
+      <el-table-column type="selection" width="42" />
+      <el-table-column width="100" label="ID">
+        <span slot-scope="scope">{{ scope.row.id }}</span>
+      </el-table-column>
+      <el-table-column width="200" label="场次代码">
+        <span slot-scope="scope">{{ scope.row.code }}</span>
+      </el-table-column>
+      <el-table-column label="候考时间">
+        <span slot-scope="scope">{{ scope.row.prepareSeconds / 60 }}</span>
+      </el-table-column>
+      <el-table-column width="120" label="状态">
+        <span slot-scope="scope">{{
+          scope.row.enable | zeroOneEnableDisableFilter
+        }}</span>
+      </el-table-column>
+      <el-table-column width="100" label="开始时间">
+        <span slot-scope="scope">{{
+          scope.row.startTime | datetimeFilter
+        }}</span>
+      </el-table-column>
+      <el-table-column width="100" label="结束时间">
+        <span slot-scope="scope">{{ scope.row.endTime | datetimeFilter }}</span>
+      </el-table-column>
+      <el-table-column width="120" label="更新人">
+        <span slot-scope="scope">{{ scope.row.updateName }}</span>
+      </el-table-column>
+      <el-table-column sortable width="170" label="更新时间">
+        <span slot-scope="scope">{{
+          scope.row.updateTime | datetimeFilter
+        }}</span>
+      </el-table-column>
+      <el-table-column :context="_self" label="操作" width="210">
+        <div slot-scope="scope">
+          <el-button size="mini" type="primary" plain @click="edit(scope.row)">
+            编辑
+          </el-button>
+          <el-button
+            size="mini"
+            type="primary"
+            plain
+            @click="toggleEnableActivity(scope.row)"
+          >
+            {{ scope.row.enable ? "禁用" : "启用" }}
+          </el-button>
+        </div>
+      </el-table-column>
+    </el-table>
+    <div class="page float-right">
+      <el-pagination
+        @current-change="handleCurrentChange"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        :page-sizes="[10, 20, 50, 100, 200, 300]"
+        @size-change="handleSizeChange"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="total"
+      />
+    </div>
+
+    <!-- <ActivityManagementDialog
+      ref="theDialog"
+      :examId="examId"
+      :activity="selectedActivity"
+      @reload="searchForm"
+    /> -->
+  </div>
+</template>
+
+<script>
+import { searchCourses } from "@/api/examwork-course";
+// import ActivityManagementDialog from "./ActivityManagementDialog";
+
+export default {
+  name: "CourseManagement",
+  // components: {
+  //   ActivityManagementDialog,
+  // },
+  computed: {},
+  data() {
+    return {
+      form: {
+        examId: "",
+        code: "",
+        hasPaper: null,
+      },
+      rules: {
+        examId: [{ required: true, message: "批次必选" }],
+      },
+      tableData: [],
+      currentPage: 1,
+      pageSize: 10,
+      total: 10,
+      selectedActivity: {},
+    };
+  },
+  async created() {},
+  methods: {
+    async searchForm() {
+      const valid = await this.$refs.form.validate();
+      if (!valid) return;
+      const res = await searchCourses({
+        examId: this.form.examId,
+        code: this.form.code,
+        hasPaper: this.form.hasPaper,
+        pageNumber: this.currentPage,
+        pageSize: this.pageSize,
+      });
+      this.tableData = res.data.data.records.records;
+      this.total = res.data.data.records.total;
+    },
+    handleCurrentChange(val) {
+      this.currentPage = val;
+      this.searchForm();
+    },
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.currentPage = 1;
+      this.searchForm();
+    },
+    add() {
+      this.selectedActivity = {};
+      this.$refs.theDialog.openDialog();
+    },
+    edit(activity) {
+      this.selectedActivity = activity;
+      this.$refs.theDialog.openDialog();
+    },
+  },
+};
+</script>
+
+<style></style>

+ 0 - 0
src/features/examwork/CourseManagement/CoursePaperDialog.vue


+ 0 - 0
src/features/examwork/CourseManagement/PaperImportDialog.vue


+ 85 - 0
src/features/examwork/ExamManagement/CopyExamDialog.vue

@@ -0,0 +1,85 @@
+<template>
+  <el-dialog
+    ref="dialog"
+    title="复制考试批次"
+    width="450px"
+    :visible.sync="visible"
+    @close="closeDialog"
+  >
+    <el-form
+      :model="form"
+      ref="form"
+      :rules="rules"
+      label-position="right"
+      label-width="120px"
+    >
+      <el-row>
+        <el-form-item label="批次名称" prop="name">
+          <el-input
+            class="pull_length"
+            v-model="form.name"
+            placeholder="批次名称"
+          />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="批次编码" prop="code">
+          <el-input
+            class="pull_length"
+            v-model="form.code"
+            placeholder="批次编码"
+            disabled
+          />
+        </el-form-item>
+      </el-row>
+      <el-row class="d-flex justify-content-center">
+        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button @click="closeDialog">取 消</el-button>
+      </el-row>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script>
+import { copyExam } from "@/api/examwork-exam";
+export default {
+  name: "CopyExamDialog",
+  props: {
+    exam: Object,
+  },
+  data() {
+    return {
+      visible: false,
+      form: {
+        name: "",
+        code: "",
+      },
+      rules: {},
+    };
+  },
+  watch: {
+    "form.name"(val) {
+      this.form.code = val;
+    },
+  },
+  methods: {
+    openDialog() {
+      this.visible = true;
+    },
+    closeDialog() {
+      this.visible = false;
+    },
+    async submitForm() {
+      await copyExam({
+        sourceId: this.exam.id,
+        code: this.form.code,
+        name: this.form.name,
+      });
+      this.$emit("reload");
+      this.closeDialog();
+    },
+  },
+};
+</script>
+
+<style></style>

+ 308 - 193
src/features/examwork/ExamManagement/ExamEdit.vue

@@ -2,210 +2,322 @@
   <div>
     <el-tabs v-model="activeName" type="card">
       <el-tab-pane label="考试规则设置" name="first">
-        <el-form :model="form" inline>
-          <el-form-item label="考试模式">
-            <ExamTypeSelect v-model="form.mode"></ExamTypeSelect>
-          </el-form-item>
-          <el-form-item label="批次编码">
-            <el-input v-model.trim="form.code"></el-input>
-          </el-form-item>
-          <el-form-item label="批次名称">
-            <el-input v-model.trim="form.name"></el-input>
-          </el-form-item>
-          <el-form-item label="考试时间">
-            <el-date-picker
-              v-model="form.startEndTimeProxy"
-              type="datetimerange"
-              range-separator="至"
-              start-placeholder="开始日期"
-              end-placeholder="结束日期"
-            >
-            </el-date-picker>
-          </el-form-item>
-          <el-form-item label="候考时长(分钟)">
-            <MinuteInput v-model.trim="form.prepareSeconds"> </MinuteInput>
-          </el-form-item>
-          <el-form-item label="考试次数限制">
-            <el-input v-model.number.trim="form.examCount"></el-input>
-          </el-form-item>
-          <el-form-item label="考试时长(分钟)">
-            <el-input v-model.trim="form.maxDurationSeconds"></el-input>
-          </el-form-item>
-          <el-form-item label="迟到时长(分钟)">
-            <el-input v-model.trim="form.openingSeconds"></el-input>
-          </el-form-item>
-          <el-form-item label="冻结时间(分钟)">
-            <el-input v-model.trim="form.minDurationSeconds"></el-input>
-          </el-form-item>
-          <el-form-item label="启用集中收卷">
-            <el-radio v-model="form.forceFinish" :label="1">是</el-radio>
-            <el-radio v-model="form.forceFinish" :label="0">否</el-radio>
-          </el-form-item>
-          <el-form-item label="启用开考口令">
-            <el-radio v-model="form.enableShortCode" :label="1">是</el-radio>
-            <el-radio v-model="form.enableShortCode" :label="0">否</el-radio>
-            <el-input v-model.trim="form.shortCode"></el-input>
-          </el-form-item>
-          <el-form-item label="是否允许断点续考">
-            <el-radio v-model="form.enableBreak" :label="1">是</el-radio>
-            <el-radio v-model="form.enableBreak" :label="0">否</el-radio>
-          </el-form-item>
-          <el-form-item label="断点次数">
-            <el-input v-model.trim="form.breakResumeCount"></el-input>
-          </el-form-item>
-          <el-form-item label="断点时长(分钟)">
-            <el-input v-model.trim="form.breakExpireSeconds"></el-input>
-          </el-form-item>
-          <el-form-item label="重考是否需要审核">
-            <el-radio v-model="form.reexamAuditing" :label="1">是</el-radio>
-            <el-radio v-model="form.reexamAuditing" :label="0">否</el-radio>
-          </el-form-item>
-          <el-form-item label="取分策略">
-            <el-radio
-              v-model="form.recordSelectStrategy"
-              label="HIGHEST_TOTAL_SCORE"
-            >
-              全部阅卷后取最高分
-            </el-radio>
-            <el-radio
-              v-model="form.recordSelectStrategy"
-              label="HIGHEST_OBJECTIVE_SCORE"
-            >
-              客观分最高
-            </el-radio>
-            <el-radio v-model="form.recordSelectStrategy" label="LATEST">
-              最后一次提交
-            </el-radio>
-          </el-form-item>
-          <el-form-item label="多选题给分规则">
-            <el-radio v-model="form.objectiveScorePolicy" label="EQUAL">
-              全对给分
-            </el-radio>
-            <el-radio v-model="form.objectiveScorePolicy" label="PARTIAL">
-              漏选给一半分
-            </el-radio>
-          </el-form-item>
-          <el-form-item label="是否显示客观分">
-            <el-radio v-model="form.showObjectiveScore" :label="1">是</el-radio>
-            <el-radio v-model="form.showObjectiveScore" :label="0">否</el-radio>
-          </el-form-item>
-          <el-form-item label="是否允许摄像头拍照作答">
-            <el-radio v-model="form.cameraPhotoUpload" :label="1">是</el-radio>
-            <el-radio v-model="form.cameraPhotoUpload" :label="0">否</el-radio>
-          </el-form-item>
-          <el-form-item label="是否允许小程序作答">
-            <el-radio v-model="form.mobilePhotoUpload" :label="1">是</el-radio>
-            <el-radio v-model="form.mobilePhotoUpload" :label="0">否</el-radio>
-          </el-form-item>
+        <el-form :model="form" label-width="170px" inline>
+          <el-row>
+            <el-form-item label="考试模式">
+              <ExamTypeSelect v-model="form.mode"></ExamTypeSelect>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="批次编码">
+              <el-input v-model.trim="form.code"></el-input>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="批次名称">
+              <el-input v-model.trim="form.name"></el-input>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="考试时间">
+              <el-date-picker
+                v-model="form.startEndTimeProxy"
+                type="datetimerange"
+                range-separator="至"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+              >
+              </el-date-picker>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="候考时长(分钟)">
+              <MinuteInput v-model.trim="form.prepareSeconds"> </MinuteInput>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="考试次数限制">
+              <el-input v-model.number.trim="form.examCount"></el-input>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="考试时长(分钟)">
+              <MinuteInput v-model.trim="form.maxDurationSeconds">
+              </MinuteInput>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="迟到时长(分钟)">
+              <MinuteInput v-model.trim="form.openingSeconds"> </MinuteInput>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="冻结时间(分钟)">
+              <MinuteInput v-model.trim="form.minDurationSeconds">
+              </MinuteInput>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="启用集中收卷">
+              <el-radio v-model="form.forceFinish" :label="1">是</el-radio>
+              <el-radio v-model="form.forceFinish" :label="0">否</el-radio>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="启用开考口令">
+              <el-radio v-model="form.enableShortCode" :label="1">是</el-radio>
+              <el-radio v-model="form.enableShortCode" :label="0">否</el-radio>
+              <el-input v-model.trim="form.shortCode"></el-input>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="是否允许断点续考">
+              <el-radio v-model="form.enableBreak" :label="1">是</el-radio>
+              <el-radio v-model="form.enableBreak" :label="0">否</el-radio>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item v-if="form.enableBreak" label="断点次数">
+              <el-input v-model.trim="form.breakResumeCount"></el-input>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item v-if="form.enableBreak" label="断点时长(分钟)">
+              <MinuteInput v-model.trim="form.breakExpireSeconds">
+              </MinuteInput>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="重考是否需要审核">
+              <el-radio v-model="form.reexamAuditing" :label="1">是</el-radio>
+              <el-radio v-model="form.reexamAuditing" :label="0">否</el-radio>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="取分策略">
+              <el-radio
+                v-model="form.recordSelectStrategy"
+                label="HIGHEST_TOTAL_SCORE"
+              >
+                全部阅卷后取最高分
+              </el-radio>
+              <el-radio
+                v-model="form.recordSelectStrategy"
+                label="HIGHEST_OBJECTIVE_SCORE"
+              >
+                客观分最高
+              </el-radio>
+              <el-radio v-model="form.recordSelectStrategy" label="LATEST">
+                最后一次提交
+              </el-radio>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="多选题给分规则">
+              <el-radio v-model="form.objectiveScorePolicy" label="EQUAL">
+                全对给分
+              </el-radio>
+              <el-radio v-model="form.objectiveScorePolicy" label="PARTIAL">
+                漏选给一半分
+              </el-radio>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="是否显示客观分">
+              <el-radio v-model="form.showObjectiveScore" :label="1">
+                是
+              </el-radio>
+              <el-radio v-model="form.showObjectiveScore" :label="0">
+                否
+              </el-radio>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="是否允许摄像头拍照作答">
+              <el-radio v-model="form.cameraPhotoUpload" :label="1">
+                是
+              </el-radio>
+              <el-radio v-model="form.cameraPhotoUpload" :label="0">
+                否
+              </el-radio>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="是否允许小程序作答">
+              <el-radio v-model="form.mobilePhotoUpload" :label="1">
+                是
+              </el-radio>
+              <el-radio v-model="form.mobilePhotoUpload" :label="0">
+                否
+              </el-radio>
+            </el-form-item>
+          </el-row>
         </el-form>
       </el-tab-pane>
 
       <el-tab-pane label="监考设置" name="second">
-        <el-form :model="form" inline>
-          <el-form-item label="开考检测">
-            <el-radio v-model="form.entryAuthenticationPolicy" label="OFF"
-              >安全级别:无</el-radio
-            >
-            <el-radio
-              v-model="form.entryAuthenticationPolicy"
-              label="FACE_VERIFY_OPTIONAL"
-              >安全级别:低</el-radio
-            >
-            <el-radio
-              v-model="form.entryAuthenticationPolicy"
-              label="FACE_VERIFY_FORCE"
-              >安全级别:中</el-radio
-            >
-            <el-radio v-model="form.entryAuthenticationPolicy" label="LIVENESS"
-              >安全级别:高</el-radio
-            >
-          </el-form-item>
+        <el-form :model="form" label-width="180px" inline>
+          <el-row>
+            <h2>开考检测</h2>
+            <el-form-item label="">
+              <el-radio v-model="form.entryAuthenticationPolicy" label="OFF">
+                安全级别:无
+              </el-radio>
+              <el-radio
+                v-model="form.entryAuthenticationPolicy"
+                label="FACE_VERIFY_OPTIONAL"
+              >
+                安全级别:低
+              </el-radio>
+              <el-radio
+                v-model="form.entryAuthenticationPolicy"
+                label="FACE_VERIFY_FORCE"
+              >
+                安全级别:中
+              </el-radio>
+              <el-radio
+                v-model="form.entryAuthenticationPolicy"
+                label="LIVENESS"
+              >
+                安全级别:高
+              </el-radio>
+            </el-form-item>
+          </el-row>
           <h2>过程监控</h2>
-          <el-form-item label="是否考中人脸识别">
-            <el-radio v-model="form.inProcessFaceVerify" :label="1"
-              >是</el-radio
-            >
-            <el-radio v-model="form.inProcessFaceVerify" :label="0"
-              >否</el-radio
-            >
-          </el-form-item>
-          <el-form-item label="是否考中陌生人脸识别">
-            <el-radio v-model="form.inProcessFaceStrangerIgnore" :label="0"
-              >是</el-radio
-            >
-            <el-radio v-model="form.inProcessFaceStrangerIgnore" :label="1"
-              >否</el-radio
-            >
-          </el-form-item>
-          <el-form-item label="是否考中活体检测">
-            <el-radio v-model="form.inProcessLivenessVerify" :label="0"
-              >是</el-radio
-            >
-            <el-radio v-model="form.inProcessLivenessVerify" :label="1"
-              >否</el-radio
-            >
-          </el-form-item>
-          <el-form-item label="活体验证弹出时间段">
-            <el-input
-              v-model.number.trim="form.inProcessLivenessFixedRange[0]"
-            ></el-input>
-            ~
-            <el-input
-              v-model.number.trim="form.inProcessLivenessFixedRange[1]"
-            ></el-input>
-            分钟
-          </el-form-item>
-          <el-form-item label="活体验证结果的判定方案">
-            <el-radio v-model="form.inProcessLivenessJudgePolicy" label="ANY">
-              单条成功则通过</el-radio
-            >
-            <el-radio v-model="form.inProcessLivenessJudgePolicy" label="ALL"
-              >所有验证成功则通过</el-radio
-            >
-            <el-radio v-model="form.inProcessLivenessJudgePolicy" label="MORE"
-              >成功次数大于失败则通过</el-radio
-            >
-            <h2>监考直播</h2>
-            <el-form-item label="是否开启考生端监考直播">
-              <el-radio v-model="form.monitorProxy" :label="true">是</el-radio>
-              <el-radio v-model="form.monitorProxy" :label="false">否</el-radio>
+          <el-row>
+            <el-form-item label="是否考中人脸识别">
+              <el-radio v-model="form.inProcessFaceVerify" :label="1"
+                >是
+              </el-radio>
+              <el-radio v-model="form.inProcessFaceVerify" :label="0"
+                >否
+              </el-radio>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="是否考中陌生人脸识别">
+              <el-radio v-model="form.inProcessFaceStrangerIgnore" :label="0"
+                >是
+              </el-radio>
+              <el-radio v-model="form.inProcessFaceStrangerIgnore" :label="1"
+                >否
+              </el-radio>
             </el-form-item>
-            <el-form-item v-if="form.monitorProxy" label="是否需要视频转录">
-              <el-radio v-model="form.monitorRecord" :label="1">是</el-radio>
-              <el-radio v-model="form.monitorRecord" :label="0">否</el-radio>
+          </el-row>
+          <el-row>
+            <el-form-item label="是否考中活体检测">
+              <el-radio v-model="form.inProcessLivenessVerify" :label="0"
+                >是
+              </el-radio>
+              <el-radio v-model="form.inProcessLivenessVerify" :label="1"
+                >否
+              </el-radio>
             </el-form-item>
-            <el-form-item v-if="form.monitorProxy" label="电脑&手机监控方案">
-              <el-checkbox-group v-model="form.monitorVideoSource">
-                <el-checkbox label="client_camera"
-                  >电脑摄像头为主机位</el-checkbox
+          </el-row>
+          <el-row>
+            <el-form-item label="活体验证弹出时间段">
+              <MinuteInput
+                v-model.trim="form.inProcessLivenessFixedRange[0]"
+                style="width: 150px;"
+              />
+              ~
+              <MinuteInput
+                v-model.trim="form.inProcessLivenessFixedRange[1]"
+                style="width: 150px;"
+              />
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="活体验证结果的判定方案">
+              <el-radio v-model="form.inProcessLivenessJudgePolicy" label="ANY">
+                单条成功则通过
+              </el-radio>
+              <el-radio v-model="form.inProcessLivenessJudgePolicy" label="ALL">
+                所有验证成功则通过
+              </el-radio>
+              <el-radio
+                v-model="form.inProcessLivenessJudgePolicy"
+                label="MORE"
+              >
+                成功次数大于失败则通过
+              </el-radio>
+              <h2>监考直播</h2>
+              <el-row>
+                <el-form-item label="是否开启考生端监考直播">
+                  <el-radio v-model="form.monitorProxy" :label="true"
+                    >是
+                  </el-radio>
+                  <el-radio v-model="form.monitorProxy" :label="false"
+                    >否
+                  </el-radio>
+                </el-form-item>
+              </el-row>
+              <el-row>
+                <el-form-item v-if="form.monitorProxy" label="是否需要视频转录">
+                  <el-radio v-model="form.monitorRecord" :label="1"
+                    >是
+                  </el-radio>
+                  <el-radio v-model="form.monitorRecord" :label="0"
+                    >否
+                  </el-radio>
+                </el-form-item>
+              </el-row>
+              <el-row>
+                <el-form-item
+                  v-if="form.monitorProxy"
+                  label="电脑&手机监控方案"
                 >
-                <el-checkbox label="client_screen">电脑开启录频</el-checkbox>
-                <el-checkbox label="mobile_first">手机监考机位1</el-checkbox>
-                <el-checkbox label="mobile_second">手机监考机位2</el-checkbox>
-              </el-checkbox-group>
+                  <el-checkbox-group v-model="form.monitorVideoSource">
+                    <el-checkbox label="client_camera"
+                      >电脑摄像头为主机位</el-checkbox
+                    >
+                    <el-checkbox label="client_screen"
+                      >电脑开启录频</el-checkbox
+                    >
+                    <el-checkbox label="mobile_first"
+                      >手机监考机位1</el-checkbox
+                    >
+                    <el-checkbox label="mobile_second"
+                      >手机监考机位2</el-checkbox
+                    >
+                  </el-checkbox-group>
+                </el-form-item>
+              </el-row>
             </el-form-item>
-          </el-form-item>
+          </el-row>
         </el-form>
       </el-tab-pane>
 
       <el-tab-pane label="其他设置" name="third">
-        <el-form :model="form" inline>
-          <el-form-item label="考试须知">
-            <el-input v-model.trim="form.preNotice"></el-input>
-          </el-form-item>
-          <el-form-item label="须知强制阅读时长(秒)">
-            <el-input v-model.trim="form.preNoticeStaySeconds"></el-input>
-          </el-form-item>
-          <el-form-item label="考后说明">
-            <el-input v-model.trim="form.postNotice"></el-input>
-          </el-form-item>
-          <el-form-item label="IP限制">
-            <el-radio v-model="form.enableIpLimit" :label="1">是</el-radio>
-            <el-radio v-model="form.enableIpLimit" :label="0">否</el-radio>
-          </el-form-item>
-          <el-form-item label="IP段(*表示任意):">
-            <el-input v-model.trim="form.ipAllow"></el-input>
-          </el-form-item>
+        <el-form :model="form" label-width="170px" inline>
+          <el-row>
+            <el-form-item label="考试须知">
+              <el-input v-model.trim="form.preNotice"></el-input>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="须知强制阅读时长">
+              <el-input v-model.trim="form.preNoticeStaySeconds">
+                <template slot="append">秒</template>
+              </el-input>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="考后说明">
+              <el-input v-model.trim="form.postNotice"></el-input>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="IP限制">
+              <el-radio v-model="form.enableIpLimit" :label="1">是</el-radio>
+              <el-radio v-model="form.enableIpLimit" :label="0">否</el-radio>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="IP段(*表示任意):">
+              <el-input v-model.trim="form.ipAllow"></el-input>
+            </el-form-item>
+          </el-row>
         </el-form>
       </el-tab-pane>
     </el-tabs>
@@ -218,7 +330,7 @@
 <script>
 import ExamTypeSelect from "@/components/ExamTypeSelect";
 import MinuteInput from "@/components/MinuteInput";
-import { saveExam, getExamDetail } from "@/api/exam";
+import { saveExam, getExamDetail } from "@/api/examwork-exam";
 
 export default {
   name: "ExamEdit",
@@ -303,8 +415,11 @@ export default {
     async save() {
       await saveExam(this.form);
       this.$notify({ title: "保存成功", type: "success" });
+      this.$router.back();
+    },
+    cancel() {
+      this.$router.back();
     },
-    cancel() {},
   },
 };
 </script>

+ 35 - 18
src/features/examwork/ExamManagement/ExamManagement.vue

@@ -16,8 +16,8 @@
     </el-form>
 
     <el-table ref="table" :data="tableData" stripe style="width: 100%;">
-      <el-table-column type="selection" width="40" />
-      <el-table-column width="55" label="ID">
+      <el-table-column type="selection" width="42" />
+      <el-table-column width="100" label="ID">
         <span slot-scope="scope">{{ scope.row.id }}</span>
       </el-table-column>
       <el-table-column width="200" label="批次编码">
@@ -27,7 +27,7 @@
         <span slot-scope="scope">{{ scope.row.name }}</span>
       </el-table-column>
       <el-table-column label="模式">
-        <span slot-scope="scope">{{ scope.row.mode }}</span>
+        <span slot-scope="scope">{{ scope.row.mode | modeFilter }}</span>
       </el-table-column>
       <el-table-column width="120" label="状态">
         <span slot-scope="scope">{{
@@ -68,6 +68,7 @@
           <el-button size="mini" type="primary" plain @click="edit(scope.row)">
             编辑
           </el-button>
+          <div class="mb-2"></div>
           <el-button
             size="mini"
             type="primary"
@@ -93,19 +94,22 @@
         :total="total"
       />
     </div>
+    <CopyExamDialog ref="theDialog" :exam="selected" @reload="searchForm" />
   </div>
 </template>
 
 <script>
 import StateSelect from "@/components/StateSelect";
 import ExamTypeSelect from "@/components/ExamTypeSelect";
-import { searchExams, toggleEnableExam, copyExam } from "@/api/exam";
+import { searchExams, toggleEnableExam } from "@/api/examwork-exam";
+import CopyExamDialog from "./CopyExamDialog";
 
 export default {
   name: "ExamManagement",
   components: {
     StateSelect,
     ExamTypeSelect,
+    CopyExamDialog,
   },
   data() {
     return {
@@ -123,6 +127,26 @@ export default {
       selected: null,
     };
   },
+  // activated() {
+  //   console.log("im activated");
+  // },
+  // deactivated() {
+  //   console.log("im deactivated");
+  // },
+  beforeRouteEnter(to, from, next) {
+    if (from.name === "ExamEdit") {
+      next((vm) => vm.$nextTick(() => vm.searchForm()));
+    } else {
+      next();
+    }
+  },
+  beforeRouteLeave(to, from, next) {
+    if (to.name !== "ExamEdit") {
+      // 仅仅在编辑考试时保持alive
+      this.$destroy();
+    }
+    next();
+  },
   methods: {
     async searchForm() {
       const res = await searchExams({
@@ -157,23 +181,16 @@ export default {
       this.searchForm();
     },
     async copy() {
-      console.log(this.$refs.table.selection);
       let rows = this.$refs.table.selection;
-      if (rows.length === 0) {
-        this.$notify({ type: "warning", title: "请选择一个批次" });
-        return;
-      }
-      if (rows.length > 1) {
-        this.$notify({ type: "warning", title: "请仅选择一个批次" });
+      if (rows.length !== 1) {
+        this.$notify({
+          type: "warning",
+          title: rows.length === 0 ? "请选择一个批次" : "请仅选择一个批次",
+        });
         return;
       }
-      const exam = rows[0];
-      await copyExam({
-        sourceId: exam.id,
-        code: exam.code,
-        name: exam.name,
-      });
-      this.searchForm();
+      this.selected = rows[0];
+      this.$refs.theDialog.openDialog();
     },
     editActivities(exam) {
       this.$router.push({

+ 122 - 0
src/features/examwork/ExamStudentImport/ExamStudentImport.vue

@@ -0,0 +1,122 @@
+<template>
+  <div>
+    <!-- 下载链接 -->
+    <el-form :model="form" inline>
+      <el-form-item label="批次名称" prop="examId">
+        <ExamSelect v-model="form.examId" />
+      </el-form-item>
+      <el-button @click="add">导入</el-button>
+      <!-- <el-button>导入</el-button> -->
+    </el-form>
+
+    <el-table :data="tableData" stripe style="width: 100%;">
+      <el-table-column type="selection" width="42" />
+      <el-table-column width="100" label="ID">
+        <span slot-scope="scope">{{ scope.row.id }}</span>
+      </el-table-column>
+      <el-table-column width="200" label="批次名称">
+        <span slot-scope="scope">{{ scope.row.entityName }}</span>
+      </el-table-column>
+      <el-table-column label="上传文件名">
+        <span slot-scope="scope">{{ scope.row.importFileName }}</span>
+      </el-table-column>
+      <el-table-column width="120" label="状态">
+        <span slot-scope="scope">{{ scope.row.status }}</span>
+      </el-table-column>
+      <el-table-column width="100" label="异常">
+        <span slot-scope="scope">{{ scope.row.summary }}</span>
+      </el-table-column>
+      <el-table-column width="100" label="上传时间">
+        <span slot-scope="scope">{{
+          scope.row.createTime | datetimeFilter
+        }}</span>
+      </el-table-column>
+      <el-table-column :context="_self" label="操作" width="210">
+        <div slot-scope="scope">
+          <el-button
+            size="mini"
+            type="primary"
+            plain
+            @click="download({ id: scope.row.id, fileType: 'IMPORTFILE' })"
+          >
+            下载文件
+          </el-button>
+          <el-button
+            size="mini"
+            type="primary"
+            plain
+            @click="download({ id: scope.row.id, fileType: 'ERROR' })"
+          >
+            导出报告
+          </el-button>
+        </div>
+      </el-table-column>
+    </el-table>
+    <div class="page float-right">
+      <el-pagination
+        @current-change="handleCurrentChange"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        :page-sizes="[10, 20, 50, 100, 200, 300]"
+        @size-change="handleSizeChange"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="total"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import { searchTasks, downloadFile } from "@/api/examwork-task";
+import { downloadFileURL } from "@/utils/utils";
+
+export default {
+  name: "ExamStudentImport",
+  data() {
+    return {
+      form: {
+        examId: "",
+      },
+      rules: {
+        examId: [{ required: true, message: "批次必选" }],
+      },
+      tableData: [],
+      currentPage: 1,
+      pageSize: 10,
+      total: 10,
+      selectedActivity: {},
+    };
+  },
+  created() {
+    this.searchForm();
+  },
+  methods: {
+    async searchForm() {
+      const res = await searchTasks({
+        type: "IMPORT_EXAM_STUDENT",
+        pageNumber: this.currentPage,
+        pageSize: this.pageSize,
+      });
+      this.tableData = res.data.data.records.records;
+      this.total = res.data.data.records.total;
+    },
+    handleCurrentChange(val) {
+      this.currentPage = val;
+      this.searchForm();
+    },
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.currentPage = 1;
+      this.searchForm();
+    },
+    add() {},
+    async download({ id, fileType }) {
+      const res = await downloadFile({ id, fileType });
+      const url = res.data.data.url;
+      downloadFileURL(url);
+    },
+  },
+};
+</script>
+
+<style></style>

+ 209 - 0
src/features/examwork/ExamStudentManagement/ExamStudentManagement.vue

@@ -0,0 +1,209 @@
+<template>
+  <div>
+    <el-form ref="form" :model="form" :rules="rules" inline>
+      <el-form-item label="批次名称" prop="examId">
+        <ExamSelect v-model="form.examId" />
+      </el-form-item>
+      <el-form-item label="场次代码">
+        <ActivitySelect :examId="form.examId" v-model="form.activityId" />
+      </el-form-item>
+      <el-form-item label="考场名称">
+        <el-input v-model.trim="form.roomCode"></el-input>
+      </el-form-item>
+      <el-form-item label="科目">
+        <CourseSelect :examId="form.examId" v-model="form.courseCode" />
+      </el-form-item>
+      <el-form-item label="姓名">
+        <el-input v-model.trim="form.name"></el-input>
+      </el-form-item>
+      <el-form-item label="证件号">
+        <el-input v-model.trim="form.identity"></el-input>
+      </el-form-item>
+      <el-form-item label="年级">
+        <el-input v-model.trim="form.grade"></el-input>
+      </el-form-item>
+      <el-form-item label="教学班级">
+        <el-input v-model.trim="form.classNo"></el-input>
+      </el-form-item>
+      <el-button @click="searchForm">查询</el-button>
+      <el-button @click="add">新增</el-button>
+      <el-button @click="toggleEnableExamStudentArray({ enable: 1 })"
+        >启用</el-button
+      >
+      <el-button @click="toggleEnableExamStudentArray({ enable: 0 })"
+        >禁用</el-button
+      >
+      <!-- <el-button>导入</el-button> -->
+    </el-form>
+
+    <el-table ref="table" :data="tableData" stripe style="width: 100%;">
+      <el-table-column type="selection" width="42" />
+      <el-table-column width="100" label="ID">
+        <span slot-scope="scope">{{ scope.row.id }}</span>
+      </el-table-column>
+      <el-table-column width="200" label="场次代码">
+        <span slot-scope="scope">{{ scope.row.activityCode }}</span>
+      </el-table-column>
+      <el-table-column label="姓名">
+        <span slot-scope="scope">{{ scope.row.name }}</span>
+      </el-table-column>
+      <el-table-column width="200" label="证件号">
+        <span slot-scope="scope">{{ scope.row.identity }}</span>
+      </el-table-column>
+      <el-table-column width="200" label="科目名称">
+        <span slot-scope="scope">{{ scope.row.courseName }}</span>
+      </el-table-column>
+      <el-table-column width="200" label="科目代码">
+        <span slot-scope="scope">{{ scope.row.courseCode }}</span>
+      </el-table-column>
+      <el-table-column width="120" label="状态">
+        <span slot-scope="scope">{{
+          scope.row.enable | zeroOneEnableDisableFilter
+        }}</span>
+      </el-table-column>
+      <el-table-column width="100" label="考场代码">
+        <span slot-scope="scope">{{ scope.row.roomCode }}</span>
+      </el-table-column>
+      <el-table-column width="100" label="考场名称">
+        <span slot-scope="scope">{{ scope.row.roomName }}</span>
+      </el-table-column>
+      <el-table-column width="120" label="年级">
+        <span slot-scope="scope">{{ scope.row.grade }}</span>
+      </el-table-column>
+      <el-table-column width="170" label="教学班级">
+        <span slot-scope="scope">{{ scope.row.classNo }}</span>
+      </el-table-column>
+      <el-table-column :context="_self" label="操作" width="210">
+        <div slot-scope="scope">
+          <el-button size="mini" type="primary" plain @click="edit(scope.row)">
+            编辑
+          </el-button>
+          <el-button
+            size="mini"
+            type="primary"
+            plain
+            @click="toggleEnableExamStudent(scope.row)"
+          >
+            {{ scope.row.enable ? "禁用" : "启用" }}
+          </el-button>
+        </div>
+      </el-table-column>
+    </el-table>
+    <div class="page float-right">
+      <el-pagination
+        @current-change="handleCurrentChange"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        :page-sizes="[10, 20, 50, 100, 200, 300]"
+        @size-change="handleSizeChange"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="total"
+      />
+    </div>
+
+    <ExamStudentManagementDialog
+      ref="theDialog"
+      :examId="examId"
+      :examStudent="selected"
+      @reload="searchForm"
+    />
+  </div>
+</template>
+
+<script>
+import {
+  searchExamStudents,
+  toggleEnableExamStudent,
+  toggleEnableExamStudentArray,
+} from "@/api/examwork-examstudent";
+import ExamStudentManagementDialog from "./ExamStudentManagementDialog";
+
+export default {
+  name: "ExamStudentManagement",
+  components: {
+    ExamStudentManagementDialog,
+  },
+  computed: {
+    examId() {
+      return this.$route.params.examId;
+    },
+  },
+  data() {
+    return {
+      form: {
+        examId: "",
+        activityId: "",
+        roomCode: "",
+        courseCode: "",
+        name: "",
+        identity: "",
+        grade: "",
+        classNo: "",
+      },
+      rules: {
+        examId: [{ required: true, message: "批次必选" }],
+      },
+      tableData: [],
+      currentPage: 1,
+      pageSize: 10,
+      total: 10,
+      selected: {},
+    };
+  },
+  async created() {},
+  methods: {
+    async searchForm() {
+      // console.log(this.form);
+      const valid = await this.$refs.form.validate();
+      if (!valid) return;
+      const res = await searchExamStudents({
+        ...this.form,
+        pageNumber: this.currentPage,
+        pageSize: this.pageSize,
+      });
+      this.tableData = res.data.data.records.records;
+      this.total = res.data.data.records.total;
+    },
+    handleCurrentChange(val) {
+      this.currentPage = val;
+      this.searchForm();
+    },
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.currentPage = 1;
+      this.searchForm();
+    },
+    add() {
+      this.selected = {};
+      this.$refs.theDialog.openDialog();
+    },
+    edit(examStudent) {
+      this.selected = examStudent;
+      this.$refs.theDialog.openDialog();
+    },
+    async toggleEnableExamStudent(examStudent) {
+      await toggleEnableExamStudent({
+        id: examStudent.id,
+        enable: examStudent.enable === 0 ? 1 : 0,
+      });
+      this.searchForm();
+    },
+    async toggleEnableExamStudentArray({ enable }) {
+      let rows = this.$refs.table.selection;
+      if (rows.length < 1) {
+        this.$notify({
+          type: "warning",
+          title: "请至少选择一个考生",
+        });
+        return;
+      }
+      await toggleEnableExamStudentArray(
+        rows.map((v) => ({ id: v.id, enable }))
+      );
+      this.searchForm();
+    },
+  },
+};
+</script>
+
+<style></style>

+ 141 - 0
src/features/examwork/ExamStudentManagement/ExamStudentManagementDialog.vue

@@ -0,0 +1,141 @@
+<template>
+  <el-dialog
+    ref="dialog"
+    :title="(isEdit ? '编辑' : '新增') + '考生'"
+    width="450px"
+    :visible.sync="visible"
+    @close="closeDialog"
+  >
+    <el-form
+      :model="form"
+      ref="form"
+      :rules="rules"
+      label-position="right"
+      label-width="120px"
+    >
+      <el-row>
+        <el-form-item label="批次名称" prop="examId">
+          <ExamSelect v-model="form.examId" />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="场次代码">
+          <ActivitySelect :examId="form.examId" v-model="form.activityId" />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="考场名称">
+          <el-input v-model.trim="form.roomCode"></el-input>
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="科目">
+          <CourseSelect :examId="form.examId" v-model="form.courseCode" />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="姓名">
+          <el-input v-model.trim="form.name"></el-input>
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="证件号">
+          <el-input v-model.trim="form.identity"></el-input>
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="年级">
+          <el-input v-model.trim="form.grade"></el-input>
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="教学班级">
+          <el-input v-model.trim="form.classNo"></el-input>
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="状态" prop="enable">
+          <el-radio-group class="pull_right_sm" v-model="form.enable">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-row>
+      <el-row class="d-flex justify-content-center">
+        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button @click="closeDialog">取 消</el-button>
+      </el-row>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script>
+import { saveExamStudent } from "@/api/examwork-examstudent";
+export default {
+  name: "ExamStudentManagementDialog",
+  props: {
+    examId: String,
+    examStudent: Object,
+  },
+  computed: {
+    isEdit() {
+      return this.examStudent.id;
+    },
+  },
+  data() {
+    return {
+      visible: false,
+      form: {
+        id: "",
+        examId: "",
+        activityId: "",
+        roomCode: "",
+        courseCode: "",
+        name: "",
+        identity: "",
+        grade: "",
+        classNo: "",
+      },
+      rules: {},
+    };
+  },
+  watch: {
+    examStudent(val) {
+      let tmp = { ...val };
+      if (!tmp.id) {
+        tmp = {
+          id: "",
+          examId: "",
+          activityId: "",
+          roomCode: "",
+          courseCode: "",
+          name: "",
+          identity: "",
+          grade: "",
+          classNo: "",
+        };
+      }
+      this.form = tmp;
+    },
+  },
+  methods: {
+    openDialog() {
+      this.visible = true;
+    },
+    closeDialog() {
+      this.visible = false;
+    },
+    async submitForm() {
+      let data = this.form;
+      if (this.isEdit) {
+        data = { ...data, id: this.examStudent.id };
+      }
+      await saveExamStudent(data);
+      this.$emit("reload");
+      this.closeDialog();
+    },
+  },
+};
+</script>
+
+<style></style>

+ 132 - 0
src/features/examwork/InvigilateManagement/InvigilateManagement.vue

@@ -0,0 +1,132 @@
+<template>
+  <div>
+    <el-form :model="form" inline>
+      <!-- <el-form-item v-if="$store.state.user.orgId === null" label="机构">
+        <OrgSelect v-model="form.orgId"></OrgSelect>
+      </el-form-item> -->
+      <el-form-item label="角色">
+        <RoleSelect v-model="form.roleCode"></RoleSelect>
+      </el-form-item>
+      <el-form-item label="登录名">
+        <el-input v-model.trim="form.loginName"></el-input>
+      </el-form-item>
+      <el-form-item label="姓名">
+        <el-input v-model.trim="form.name"></el-input>
+      </el-form-item>
+      <el-form-item label="状态">
+        <StateSelect v-model="form.enableState"></StateSelect>
+      </el-form-item>
+      <el-button @click="searchForm">查询</el-button>
+      <el-button @click="add">新增</el-button>
+      <!-- <el-button>导入</el-button> -->
+    </el-form>
+
+    <el-table :data="tableData" stripe style="width: 100%;">
+      <!-- <el-table-column type="selection" width="42" />
+      <el-table-column width="100" label="ID">
+        <span slot-scope="scope">{{ scope.row.id }}</span>
+      </el-table-column> -->
+      <el-table-column width="200" label="考场编码">
+        <span slot-scope="scope">{{ scope.row.roomCode }}</span>
+      </el-table-column>
+      <el-table-column label="考场名称">
+        <span slot-scope="scope">{{ scope.row.roomName }}</span>
+      </el-table-column>
+      <el-table-column width="220" label="监考老师">
+        <span slot-scope="scope">{{ scope.row.name }}</span>
+      </el-table-column>
+      <el-table-column :context="_self" label="操作" width="210">
+        <div slot-scope="scope">
+          <el-button size="mini" type="primary" plain @click="edit(scope.row)">
+            编辑
+          </el-button>
+        </div>
+      </el-table-column>
+    </el-table>
+    <div class="page float-right">
+      <el-pagination
+        @current-change="handleCurrentChange"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        :page-sizes="[10, 20, 50, 100, 200, 300]"
+        @size-change="handleSizeChange"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="total"
+      />
+    </div>
+
+    <InvigilateManagementDialog
+      ref="theDialog"
+      :user="selectedUser"
+      @reload="searchForm"
+    />
+  </div>
+</template>
+
+<script>
+import RoleSelect from "@/components/RoleSelect.vue";
+import StateSelect from "@/components/StateSelect";
+import InvigilateManagementDialog from "./InvigilateManagementDialog";
+import { searchInvigilators } from "@/api/examwork-invigilate";
+
+export default {
+  name: "InvigilateManagement",
+  components: {
+    RoleSelect,
+    StateSelect,
+    // OrgSelect,
+    InvigilateManagementDialog,
+  },
+  data() {
+    return {
+      form: {
+        orgId: "",
+        roleCode: "",
+        loginName: "",
+        name: "",
+        enableState: null,
+      },
+      tableData: [],
+      currentPage: 1,
+      pageSize: 10,
+      total: 10,
+      selectedUser: {},
+    };
+  },
+  async created() {},
+  methods: {
+    async searchForm() {
+      const res = await searchInvigilators({
+        orgId: this.form.orgId,
+        role: this.form.roleCode,
+        enable: this.form.enableState,
+        loginName: this.form.loginName,
+        name: this.form.name,
+        pageNumber: this.currentPage,
+        pageSize: this.pageSize,
+      });
+      this.tableData = res.data.data.records.records;
+      this.total = res.data.data.records.total;
+    },
+    handleCurrentChange(val) {
+      this.currentPage = val;
+      this.searchForm();
+    },
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.currentPage = 1;
+      this.searchForm();
+    },
+    add() {
+      this.selectedUser = {};
+      this.$refs.theDialog.openDialog();
+    },
+    edit(user) {
+      this.selectedUser = user;
+      this.$refs.theDialog.openDialog();
+    },
+  },
+};
+</script>
+
+<style></style>

+ 137 - 0
src/features/examwork/InvigilateManagement/InvigilateManagementDialog.vue

@@ -0,0 +1,137 @@
+<template>
+  <el-dialog
+    ref="dialog"
+    :title="(isEdit ? '编辑' : '新增') + '用户'"
+    width="450px"
+    :visible.sync="visible"
+    @close="closeDialog"
+  >
+    <el-form
+      :model="form"
+      ref="form"
+      :rules="rules"
+      label-position="right"
+      label-width="120px"
+    >
+      <el-row>
+        <el-form-item v-if="$store.state.user.orgId === null" label="机构">
+          <OrgSelect v-model="form.orgId"></OrgSelect>
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="登录名" prop="loginName">
+          <el-input
+            class="pull_length"
+            v-model="form.loginName"
+            placeholder="登录名"
+          />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="姓名" prop="name">
+          <el-input
+            class="pull_length"
+            v-model="form.name"
+            placeholder="姓名"
+          />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="密码" prop="password">
+          <el-input
+            class="pull_length"
+            v-model="form.password"
+            placeholder="密码"
+          />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="角色" prop="roleCode">
+          <RoleSelect v-model="form.roleCode" multiple />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="手机号" prop="mobileNumber">
+          <el-input
+            class="pull_length"
+            v-model="form.mobileNumber"
+            placeholder="联系方式"
+          />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="状态" prop="enable">
+          <el-radio-group class="pull_right_sm" v-model="form.enable">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-row>
+      <el-row class="d-flex justify-content-center">
+        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button @click="closeDialog">取 消</el-button>
+      </el-row>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script>
+import RoleSelect from "@/components/RoleSelect";
+import { saveUser } from "@/api/system-user";
+import OrgSelect from "@/components/OrgSelect";
+export default {
+  name: "InvigilateManagementDialog",
+  components: { RoleSelect, OrgSelect },
+  props: {
+    user: Object,
+  },
+  computed: {
+    isEdit() {
+      return this.user.id;
+    },
+  },
+  data() {
+    return {
+      visible: false,
+      form: {},
+      rules: {},
+    };
+  },
+  watch: {
+    user(val) {
+      let tmp = { ...val };
+      if (!tmp.id) {
+        tmp = {
+          orgId: this.$store.state.user.orgId,
+          name: "",
+          loginName: "",
+          password: "",
+          mobileNumber: "",
+          roleCode: [],
+          enable: 1,
+        };
+      }
+      this.form = tmp;
+    },
+  },
+  methods: {
+    openDialog() {
+      this.visible = true;
+    },
+    closeDialog() {
+      this.visible = false;
+    },
+    async submitForm() {
+      let data = this.form;
+      if (this.isEdit) {
+        data = { ...data, id: this.user.id };
+      }
+      await saveUser(data);
+      this.$emit("reload");
+      this.closeDialog();
+    },
+  },
+};
+</script>
+
+<style></style>

+ 154 - 0
src/features/examwork/StudentManagement/StudentManagement.vue

@@ -0,0 +1,154 @@
+<template>
+  <div>
+    <el-form :model="form" inline>
+      <el-form-item label="姓名">
+        <el-input v-model.trim="form.name"></el-input>
+      </el-form-item>
+      <el-form-item label="证件号">
+        <el-input v-model.trim="form.identity"></el-input>
+      </el-form-item>
+      <el-form-item label="状态">
+        <StateSelect v-model="form.enable"></StateSelect>
+      </el-form-item>
+      <el-button @click="searchForm">查询</el-button>
+    </el-form>
+
+    <el-table :data="tableData" stripe style="width: 100%;">
+      <el-table-column type="selection" width="42" />
+      <el-table-column width="100" label="ID">
+        <span slot-scope="scope">{{ scope.row.id }}</span>
+      </el-table-column>
+      <el-table-column width="100" label="姓名">
+        <span slot-scope="scope">{{ scope.row.name }}</span>
+      </el-table-column>
+      <el-table-column width="200" label="证件号">
+        <span slot-scope="scope">{{ scope.row.identity }}</span>
+      </el-table-column>
+      <el-table-column width="100" label="机构">
+        <span slot-scope="scope">{{ scope.row.orgName }}</span>
+      </el-table-column>
+      <el-table-column width="120" label="状态">
+        <span slot-scope="scope">{{
+          scope.row.enable | zeroOneEnableDisableFilter
+        }}</span>
+      </el-table-column>
+      <el-table-column width="120" label="照片">
+        <span slot-scope="scope">{{ scope.row.basePhotoPath }}</span>
+      </el-table-column>
+      <el-table-column width="120" label="更新人">
+        <span slot-scope="scope">{{ scope.row.updateName }}</span>
+      </el-table-column>
+      <el-table-column sortable width="170" label="更新时间">
+        <span slot-scope="scope">{{
+          scope.row.updateTime | datetimeFilter
+        }}</span>
+      </el-table-column>
+      <el-table-column :context="_self" label="操作" width="260">
+        <div slot-scope="scope">
+          <el-button size="mini" type="primary" plain @click="edit(scope.row)">
+            考试记录
+          </el-button>
+          <el-button
+            size="mini"
+            type="primary"
+            plain
+            @click="resetStudentPassword(scope.row)"
+          >
+            重置密码
+          </el-button>
+          <el-button
+            size="mini"
+            type="primary"
+            plain
+            @click="toggleEnableStudent(scope.row)"
+          >
+            {{ scope.row.enable ? "禁用" : "启用" }}
+          </el-button>
+        </div>
+      </el-table-column>
+    </el-table>
+    <div class="page float-right">
+      <el-pagination
+        @current-change="handleCurrentChange"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        :page-sizes="[10, 20, 50, 100, 200, 300]"
+        @size-change="handleSizeChange"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="total"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  searchStudents,
+  toggleEnableStudent,
+  resetStudentPassword,
+} from "@/api/examwork-student";
+
+export default {
+  name: "StudentManagement",
+  data() {
+    return {
+      form: {
+        name: "",
+        identity: "",
+        enable: null,
+      },
+      tableData: [],
+      currentPage: 1,
+      pageSize: 10,
+      total: 10,
+      selectedUser: {},
+    };
+  },
+  async created() {},
+  methods: {
+    async searchForm() {
+      const res = await searchStudents({
+        enable: this.form.enable,
+        name: this.form.name,
+        pageNumber: this.currentPage,
+        pageSize: this.pageSize,
+      });
+      this.tableData = res.data.data.records.records;
+      this.total = res.data.data.records.total;
+    },
+    handleCurrentChange(val) {
+      this.currentPage = val;
+      this.searchForm();
+    },
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.currentPage = 1;
+      this.searchForm();
+    },
+    add() {
+      this.selectedUser = {};
+      this.$refs.theDialog.openDialog();
+    },
+    edit(user) {
+      this.selectedUser = user;
+      this.$refs.theDialog.openDialog();
+    },
+    async toggleEnableStudent(user) {
+      await toggleEnableStudent({
+        id: user.id,
+        enable: user.enable === 0 ? 1 : 0,
+      });
+      this.searchForm();
+    },
+    async resetStudentPassword(user) {
+      await resetStudentPassword({
+        id: user.id,
+        password: "123456",
+      });
+      this.searchForm();
+    },
+  },
+};
+</script>
+
+<style></style>

+ 40 - 8
src/features/system/OrgManagement/OrgManagement.vue

@@ -11,19 +11,19 @@
         <StateSelect v-model="form.enableState"></StateSelect>
       </el-form-item>
       <el-button @click="searchForm">查询</el-button>
-      <el-button>新增</el-button>
+      <el-button @click="add">新增</el-button>
       <!-- <el-button>导入</el-button> -->
     </el-form>
 
     <el-table :data="tableData" stripe style="width: 100%;">
-      <el-table-column type="selection" width="40" />
-      <el-table-column width="55" label="ID">
+      <el-table-column type="selection" width="42" />
+      <el-table-column width="100" label="ID">
         <span slot-scope="scope">{{ scope.row.id }}</span>
       </el-table-column>
       <el-table-column width="200" label="机构名称">
         <span slot-scope="scope">{{ scope.row.name }}</span>
       </el-table-column>
-      <el-table-column width="200" label="机构代码">
+      <el-table-column width="100" label="机构代码">
         <span slot-scope="scope">{{ scope.row.code }}</span>
       </el-table-column>
       <el-table-column label="状态">
@@ -40,13 +40,24 @@
       <el-table-column width="120" label="更新人">
         <span slot-scope="scope">{{ scope.row.updateName }}</span>
       </el-table-column>
-      <el-table-column sortable width="170" label="更新时间" prop="updateTime">
+      <el-table-column width="100" label="更新时间">
+        <span slot-scope="scope">{{
+          scope.row.updateTime | datetimeFilter
+        }}</span>
       </el-table-column>
       <el-table-column :context="_self" label="操作" width="210">
         <div slot-scope="scope">
           <el-button size="mini" type="primary" plain @click="edit(scope.row)">
             <i class="el-icon-edit"></i> 编辑
           </el-button>
+          <el-button
+            size="mini"
+            type="primary"
+            plain
+            @click="toggleEnableOrg(scope.row)"
+          >
+            {{ scope.row.enable ? "禁用" : "启用" }}
+          </el-button>
         </div>
       </el-table-column>
     </el-table>
@@ -61,17 +72,22 @@
         :total="total"
       />
     </div>
+    <OrgManagementDialog
+      ref="theDialog"
+      :org="selectedOrg"
+      @reload="searchForm"
+    />
   </div>
 </template>
 
 <script>
-import StateSelect from "@/components/StateSelect";
-import { searchOrgs } from "../../../api/system-org";
+import { searchOrgs, toggleEnableOrg } from "../../../api/system-org";
+import OrgManagementDialog from "./OrgManagementDialog";
 
 export default {
   name: "OrgManagement",
   components: {
-    StateSelect,
+    OrgManagementDialog,
   },
   data() {
     return {
@@ -84,6 +100,7 @@ export default {
       currentPage: 1,
       pageSize: 10,
       total: 10,
+      selectedOrg: {},
     };
   },
   async created() {},
@@ -108,6 +125,21 @@ export default {
       this.currentPage = 1;
       this.searchForm();
     },
+    add() {
+      this.selectedOrg = {};
+      this.$refs.theDialog.openDialog();
+    },
+    edit(org) {
+      this.selectedOrg = org;
+      this.$refs.theDialog.openDialog();
+    },
+    async toggleEnableOrg(org) {
+      await toggleEnableOrg({
+        id: org.id,
+        enable: org.enable === 0 ? 1 : 0,
+      });
+      this.searchForm();
+    },
   },
 };
 </script>

+ 164 - 0
src/features/system/OrgManagement/OrgManagementDialog.vue

@@ -0,0 +1,164 @@
+<template>
+  <el-dialog
+    ref="dialog"
+    :title="(isEdit ? '编辑' : '新增') + '机构'"
+    width="480px"
+    :visible.sync="visible"
+    @close="closeDialog"
+  >
+    <el-form
+      :model="form"
+      ref="form"
+      :rules="rules"
+      label-position="right"
+      label-width="140px"
+    >
+      <el-row>
+        <el-form-item label="机构名称" prop="name">
+          <el-input
+            class="pull_length"
+            v-model="form.name"
+            placeholder="机构名称"
+          />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="机构编码" prop="code">
+          <el-input
+            class="pull_length"
+            v-model="form.code"
+            placeholder="机构编码"
+          />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="负责人" prop="contactName">
+          <el-input
+            class="pull_length"
+            v-model="form.contactName"
+            placeholder="负责人"
+          />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="联系方式" prop="contactPhone">
+          <el-input
+            class="pull_length"
+            v-model="form.contactPhone"
+            placeholder="联系方式"
+          />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="logo" prop="logo">
+          <UploadFile v-model="form.logo" />
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="是否启用模考" prop="enableSimulate">
+          <el-radio-group class="pull_right_sm" v-model="form.enableSimulate">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="是否视频转录" prop="enableMonitorRecord">
+          <el-radio-group
+            class="pull_right_sm"
+            v-model="form.enableMonitorRecord"
+          >
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="是否使用活体检测" prop="enableLiveness">
+          <el-radio-group class="pull_right_sm" v-model="form.enableLiveness">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-row>
+      <el-row>
+        <el-form-item label="状态" prop="enable">
+          <el-radio-group class="pull_right_sm" v-model="form.enable">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-row>
+      <el-row class="d-flex justify-content-center">
+        <el-button type="primary" @click="submitForm">保 存</el-button>
+        <el-button @click="closeDialog">取 消</el-button>
+      </el-row>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script>
+import { searchOrg, saveOrg } from "@/api/system-org";
+import UploadFile from "./UploadFile";
+export default {
+  name: "OrgManagementDialog",
+  props: {
+    org: Object,
+  },
+  components: { UploadFile },
+  computed: {
+    isEdit() {
+      return this.org.id;
+    },
+  },
+  data() {
+    return {
+      visible: false,
+      form: {},
+      rules: {},
+    };
+  },
+  watch: {
+    org: {
+      immediate: true,
+      handler: async function () {
+        if (this.isEdit) {
+          const res = await searchOrg(this.org.id);
+          this.form = res.data.data.records[0];
+        } else {
+          this.form = {
+            name: "",
+            code: "",
+            contactName: "",
+            contactPhone: "",
+            logo: null,
+            enableSimulate: 0,
+            enableMonitorRecord: 0,
+            enableLiveness: 0,
+            enable: 0,
+          };
+        }
+      },
+    },
+  },
+  methods: {
+    openDialog() {
+      this.visible = true;
+    },
+    closeDialog() {
+      this.visible = false;
+    },
+    async submitForm() {
+      let data = this.form;
+      if (this.isEdit) {
+        data = { ...data, id: this.org.id };
+      }
+      await saveOrg(data);
+      this.$emit("reload");
+      this.closeDialog();
+    },
+  },
+};
+</script>
+
+<style></style>

+ 82 - 0
src/features/system/OrgManagement/UploadFile.vue

@@ -0,0 +1,82 @@
+<template>
+  <el-upload
+    ref="upload"
+    class="upload-demo"
+    action="/api/placeholder"
+    accept=".jpg,.png"
+    :on-preview="handlePreview"
+    :on-remove="handleRemove"
+    :file-list="fileList"
+    :before-upload="beforeUpload"
+    list-type="picture"
+  >
+    <el-button size="small" type="primary">点击上传</el-button>
+    <div slot="tip" class="el-upload__tip">
+      只能上传jpg/png文件,且不超过500kb
+    </div>
+  </el-upload>
+</template>
+
+<script>
+import MD5 from "js-md5";
+import { uploadFile } from "@/api/system-info";
+
+export default {
+  props: { value: String },
+  data() {
+    return {
+      fileList: [
+        {
+          name: "",
+          url: this.value || "",
+        },
+      ],
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(v) {
+        this.fileList[0].url = v;
+      },
+    },
+  },
+  methods: {
+    handleRemove(file, fileList) {
+      console.log(file, fileList);
+      const url = "";
+      this.fileList[0].url = url;
+      this.$emit("input", url);
+      this.$emit("change", url);
+    },
+    handlePreview(file) {
+      console.log(file);
+    },
+    async beforeUpload(file) {
+      console.log(file);
+      async function blobToArray(blob) {
+        return new Promise((resolve) => {
+          var reader = new FileReader();
+          reader.addEventListener("loadend", function () {
+            // reader.result contains the contents of blob as a typed array
+            resolve(reader.result);
+          });
+          reader.readAsArrayBuffer(blob);
+        });
+      }
+      const ab = await blobToArray(file);
+      const md5 = MD5(ab);
+
+      const res = await uploadFile({ file, md5 });
+      console.log(res);
+      const url = res.data.data.url;
+      this.fileList[0].url = url;
+      this.$emit("input", url);
+      this.$emit("change", url);
+      // return Promise.reject("stop upload");
+    },
+  },
+};
+</script>
+
+<style></style>

+ 19 - 16
src/features/system/UserManagement/UserManagement.vue

@@ -22,8 +22,8 @@
     </el-form>
 
     <el-table :data="tableData" stripe style="width: 100%;">
-      <el-table-column type="selection" width="40" />
-      <el-table-column width="55" label="ID">
+      <el-table-column type="selection" width="42" />
+      <el-table-column width="100" label="ID">
         <span slot-scope="scope">{{ scope.row.id }}</span>
       </el-table-column>
       <el-table-column
@@ -33,7 +33,7 @@
       >
         <span slot-scope="scope">{{ scope.row.orgName }}</span>
       </el-table-column>
-      <el-table-column width="200" label="姓名">
+      <el-table-column width="100" label="姓名">
         <span slot-scope="scope">{{ scope.row.name }}</span>
       </el-table-column>
       <el-table-column label="登录名">
@@ -50,21 +50,24 @@
       <el-table-column width="120" label="更新人">
         <span slot-scope="scope">{{ scope.row.updateName }}</span>
       </el-table-column>
-      <el-table-column sortable width="170" label="更新时间" prop="updateTime">
+      <el-table-column width="100" label="更新时间">
+        <span slot-scope="scope">{{
+          scope.row.updateTime | datetimeFilter
+        }}</span>
       </el-table-column>
-      <el-table-column :context="_self" label="操作" width="210">
+      <el-table-column :context="_self" label="操作" width="250">
         <div slot-scope="scope">
           <el-button size="mini" type="primary" plain @click="edit(scope.row)">
             编辑
           </el-button>
-          <el-button
-            size="mini"
-            type="primary"
-            plain
-            @click="resetUserPassword(scope.row)"
+          <el-popconfirm
+            title="确定将此用户密码重置为 123456 吗?"
+            @onConfirm="resetUserPassword(scope.row)"
           >
-            重置密码
-          </el-button>
+            <el-button slot="reference" size="mini" type="primary" plain>
+              重置密码
+            </el-button>
+          </el-popconfirm>
           <el-button
             size="mini"
             type="primary"
@@ -89,7 +92,7 @@
     </div>
 
     <UserManagementDialog
-      ref="userDialog"
+      ref="theDialog"
       :user="selectedUser"
       @reload="searchForm"
     />
@@ -157,11 +160,11 @@ export default {
     },
     add() {
       this.selectedUser = {};
-      this.$refs.userDialog.openDialog();
+      this.$refs.theDialog.openDialog();
     },
     edit(user) {
       this.selectedUser = user;
-      this.$refs.userDialog.openDialog();
+      this.$refs.theDialog.openDialog();
     },
     async toggleEnableUser(user) {
       await toggleEnableUser({
@@ -175,7 +178,7 @@ export default {
         id: user.id,
         password: "123456",
       });
-      this.searchForm();
+      this.$notify({ type: "success", title: "重置成功" });
     },
   },
 };

+ 5 - 0
src/filters/index.js

@@ -44,3 +44,8 @@ Vue.filter("scoreStatusFilter", function (val) {
     FINISH: "算分完成",
   }[val];
 });
+
+Vue.filter("modeFilter", function (val) {
+  if (val === null) return "无";
+  return { TOGETHER: "集中统一", ANYTIME: "随到随考" }[val];
+});

+ 1 - 0
src/main.js

@@ -6,6 +6,7 @@ import store from "./store";
 // import "./registerServiceWorker";
 // 27KB non-zip
 import "./plugins/axiosIndex";
+import "./components/registerComponents";
 import "./plugins/customComponents";
 import "./filters";
 import "./mixins/logout";

+ 40 - 0
src/router/index.js

@@ -98,6 +98,46 @@ const routes = [
             /* webpackChunkName: "exam" */ "../features/examwork/ActivityManagement/ActivityManagement.vue"
           ),
       },
+      {
+        path: "examstudent",
+        name: "ExamStudentManagement",
+        component: () =>
+          import(
+            /* webpackChunkName: "exam" */ "../features/examwork/ExamStudentManagement/ExamStudentManagement.vue"
+          ),
+      },
+      {
+        path: "examstudent/import",
+        name: "ExamStudentImport",
+        component: () =>
+          import(
+            /* webpackChunkName: "exam" */ "../features/examwork/ExamStudentImport/ExamStudentImport.vue"
+          ),
+      },
+      {
+        path: "course",
+        name: "CourseManagement",
+        component: () =>
+          import(
+            /* webpackChunkName: "exam" */ "../features/examwork/CourseManagement/CourseManagement.vue"
+          ),
+      },
+      {
+        path: "student",
+        name: "StudentManagement",
+        component: () =>
+          import(
+            /* webpackChunkName: "exam" */ "../features/examwork/StudentManagement/StudentManagement.vue"
+          ),
+      },
+      {
+        path: "invigilate",
+        name: "InvigilateManagement",
+        component: () =>
+          import(
+            /* webpackChunkName: "exam" */ "../features/examwork/InvigilateManagement/InvigilateManagement.vue"
+          ),
+      },
     ],
   },
   {

+ 12 - 0
src/utils/utils.js

@@ -40,3 +40,15 @@ export function AESString(content) {
 export function object2QueryString(obj) {
   return queryString.stringify(obj);
 }
+
+// 下载文件
+export function downloadFileURL(url, name) {
+  const link = document.createElement("a");
+  link.style.display = "none";
+  link.href = url;
+  const fileName = name || url.split("/").pop();
+  link.setAttribute("download", fileName);
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+}

+ 3 - 1
src/views/Layout/components/AppMain.vue

@@ -2,7 +2,9 @@
   <section class="app-main">
     <transition name="fade" mode="out-in">
       <!-- <router-view :key="key"></router-view> -->
-      <router-view />
+      <keep-alive include="ExamManagement">
+        <router-view />
+      </keep-alive>
     </transition>
   </section>
 </template>

+ 10 - 25
src/views/Layout/components/menu.js

@@ -49,39 +49,24 @@ const businessMenuConfig = [
         name: "ExamManagement",
       },
       {
-        title: "场次管理",
-        name: "ActivityManagement",
+        title: "考生管理",
+        name: "ExamStudentManagement",
       },
       {
-        title: "调卷规则",
-        name: "Base",
-      },
-      {
-        title: "学生档案设置",
-        name: "Base",
-      },
-      {
-        title: "考场监考设置",
-        name: "Base",
+        title: "考生导入",
+        name: "ExamStudentImport",
       },
-    ],
-  },
-  {
-    title: "系统设置",
-    name: "System",
-    icon: "icon-business",
-    children: [
       {
-        title: "用户管理",
-        name: "Base",
+        title: "调卷规则",
+        name: "CourseManagement",
       },
       {
-        title: "角色权限",
-        name: "Base",
+        title: "学生档案",
+        name: "StudentManagement",
       },
       {
-        title: "机构管理",
-        name: "Base",
+        title: "考场监考设置",
+        name: "InvigilateManagement",
       },
     ],
   },

+ 5 - 0
yarn.lock

@@ -6807,6 +6807,11 @@ js-cookie@^2.2.1:
   resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
   integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
 
+js-md5@^0.7.3:
+  version "0.7.3"
+  resolved "https://registry.npm.taobao.org/js-md5/download/js-md5-0.7.3.tgz#b4f2fbb0b327455f598d6727e38ec272cd09c3f2"
+  integrity sha1-tPL7sLMnRV9ZjWcn447Ccs0Jw/I=
+
 js-message@1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/js-message/-/js-message-1.0.5.tgz#2300d24b1af08e89dd095bc1a4c9c9cfcb892d15"