zhangjie 3 gadi atpakaļ
vecāks
revīzija
c669620ae4

+ 0 - 0
src/plugins/mixins.js → src/mixins/common.js


+ 10 - 0
src/mixins/privilege.js

@@ -0,0 +1,10 @@
+export default {
+  methods: {
+    checkPrivilege(type, field) {
+      const key = `${type}_${field}`.toLowerCase();
+      const routerPrivileges =
+        this.$store.state.privilegeMap[this.$route.name] || [];
+      return routerPrivileges.includes(key);
+    }
+  }
+};

+ 11 - 0
src/modules/base/api.js

@@ -150,6 +150,17 @@ export const updateStudent = datas => {
   return $post("/api/admin/basic/student/save", datas);
 };
 
+// print-plan-push-manage
+export const printPlanPushListQuery = datas => {
+  return $postParam("/api/admin/exam/print_sync/list_sync", datas);
+};
+export const printPlanBatchPush = datas => {
+  return $postParam("/api/admin/exam/print_sync/sync_data_cloud", datas);
+};
+export const printPlanMergePush = datas => {
+  return $postParam("/api/admin/exam/print_sync/sync_data_merge", datas);
+};
+
 // common
 export const uploadFile = datas => {
   return $post("/api/admin/common/file/upload", datas);

+ 132 - 0
src/modules/base/components/MergePushDialog.vue

@@ -0,0 +1,132 @@
+<template>
+  <el-dialog
+    class="merge-push-dialog"
+    :visible.sync="modalIsShow"
+    title="批量合并印刷任务"
+    top="10vh"
+    width="500px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <div>
+      <p class="tips-infos">
+        1、此操作会将多个印刷计划合并,合并操作不可还原,是否继续合并操作?
+      </p>
+      <p class="tips-infos">
+        2、如果确定需要合并,请填写合并后考试名称及对应考试ID,然后点击确定按钮。
+      </p>
+    </div>
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      label-width="100px"
+    >
+      <el-form-item prop="name" label="考试名称:">
+        <el-input
+          v-model.trim="modalForm.name"
+          placeholder="请输入考试名称"
+          clearable
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="code" label="考试ID:">
+        <el-input
+          v-model.trim="modalForm.code"
+          placeholder="请输入考试ID"
+          clearable
+        ></el-input>
+      </el-form-item>
+    </el-form>
+    <div slot="footer">
+      <el-button type="primary" :disabled="isSubmit" @click="submit"
+        >确认</el-button
+      >
+      <el-button type="danger" @click="cancel" plain>取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { printPlanMergePush } from "../api";
+
+const initModalForm = {
+  name: "",
+  code: ""
+};
+
+export default {
+  name: "merge-push-dialog",
+  props: {
+    ids: {
+      type: Array,
+      default() {
+        return [];
+      }
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+      rules: {
+        name: [
+          {
+            required: true,
+            message: "考试名称不能超过50个字",
+            max: 50,
+            trigger: "change"
+          }
+        ],
+        code: [
+          {
+            required: true,
+            pattern: /^[0-9a-zA-Z_-]{3,30}$/,
+            message: "考试ID只能由数字字母短横线组成,长度在3-30之间",
+            trigger: "change"
+          }
+        ]
+      }
+    };
+  },
+  methods: {
+    initData(val) {
+      this.modalForm = { ...initModalForm };
+      this.$nextTick(() => {
+        this.$refs.modalFormComp.clearValidate();
+      });
+    },
+    visibleChange() {
+      this.initData(this.instance);
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const data = await printPlanMergePush({
+        ...this.modalForm,
+        ids: this.ids
+      }).catch(() => {
+        this.isSubmit = false;
+      });
+
+      if (!data) return;
+
+      this.isSubmit = false;
+      this.$message.success("提交成功!");
+      this.$emit("modified");
+      this.cancel();
+    }
+  }
+};
+</script>

+ 6 - 0
src/modules/base/router.js

@@ -9,6 +9,7 @@ import CommonCardTemplate from "./views/CommonCardTemplate.vue";
 import ParamPrintTemplate from "./views/ParamPrintTemplate.vue";
 import CommonPrintTemplate from "./views/CommonPrintTemplate.vue";
 import CourseManage from "./views/CourseManage.vue";
+import PrintPlanPushManage from "./views/PrintPlanPushManage.vue";
 
 export default [
   {
@@ -55,5 +56,10 @@ export default [
     path: "/base/course-manage",
     name: "CourseManage",
     component: CourseManage
+  },
+  {
+    path: "/base/print-plan-push-manage",
+    name: "PrintPlanPushManage",
+    component: PrintPlanPushManage
   }
 ];

+ 2 - 2
src/modules/base/views/CampusManage.vue

@@ -13,8 +13,8 @@
     </div>
     <div class="part-box">
       <el-table ref="TableList" :data="dataList" border stripe>
-        <el-table-column prop="name" label="校区名称"></el-table-column>
-        <el-table-column prop="code" label="校区编码"></el-table-column>
+        <el-table-column prop="campusName" label="校区名称"></el-table-column>
+        <el-table-column prop="campusCode" label="校区编码"></el-table-column>
         <el-table-column label="操作" align="center" width="120px">
           <template slot-scope="scope">
             <el-button

+ 229 - 0
src/modules/base/views/PrintPlanPushManage.vue

@@ -0,0 +1,229 @@
+<template>
+  <div class="print-plan-push-manage">
+    <div class="part-box part-box-filter part-box-flex">
+      <el-form ref="FilterForm" label-position="left" label-width="85px" inline>
+        <el-form-item label="印刷计划:">
+          <print-plan-select
+            v-model.trim="filter.printPlanId"
+            placeholder="请选择"
+            clearable
+          ></print-plan-select>
+        </el-form-item>
+        <el-form-item label="考试计划:"> </el-form-item>
+        <el-form-item label-width="0px">
+          <el-button type="primary" icon="el-icon-search" @click="search"
+            >查询</el-button
+          >
+        </el-form-item>
+      </el-form>
+      <div class="part-box-action">
+        <el-button
+          icon="el-icon-circle-plus-outline"
+          type="primary"
+          :disabled="loading"
+          @click="toBatchPush"
+        >
+          批量推送
+        </el-button>
+        <el-button
+          icon="el-icon-circle-plus-outline"
+          type="primary"
+          @click="toMergePush"
+        >
+          合并推送印刷计划
+        </el-button>
+      </div>
+    </div>
+
+    <div class="part-box">
+      <el-table
+        ref="TableList"
+        :data="dataList"
+        border
+        stripe
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column type="selection" width="55"></el-table-column>
+        <el-table-column
+          prop="printPlanName"
+          label="印刷计划"
+        ></el-table-column>
+        <el-table-column prop="examStartTime" label="考试开始时间">
+          <span slot-scope="scope">{{
+            scope.row.examStartTime | timestampFilter
+          }}</span>
+        </el-table-column>
+        <el-table-column prop="examEndTime" label="考试结束时间">
+          <span slot-scope="scope">{{
+            scope.row.examEndTime | timestampFilter
+          }}</span>
+        </el-table-column>
+        <el-table-column prop="status" label="计划状态" width="100">
+          <span slot-scope="scope">
+            {{ scope.row.status | printPlanStatusFilter }}
+          </span>
+        </el-table-column>
+        <el-table-column prop="totalGates" label="总门次" width="80">
+        </el-table-column>
+        <el-table-column prop="totalSubjects" label="总科次" width="80">
+        </el-table-column>
+        <el-table-column prop="totalPackages" label="总卷袋数" width="100">
+        </el-table-column>
+        <el-table-column prop="createTime" label="创建时间">
+          <span slot-scope="scope">{{
+            scope.row.createTime | timestampFilter
+          }}</span>
+        </el-table-column>
+        <el-table-column prop="createName" label="创建人"></el-table-column>
+        <el-table-column prop="examName" label="考试名称"></el-table-column>
+        <el-table-column prop="examId" label="考试ID"></el-table-column>
+        <el-table-column prop="syncStatus" label="同步状态"></el-table-column>
+        <el-table-column
+          class-name="action-column"
+          label="操作"
+          align="center"
+          width="120px"
+        >
+          <template slot-scope="scope">
+            <el-button
+              class="btn-table-icon"
+              type="text"
+              icon="icon icon-circle-right"
+              @click="toPreview(scope.row)"
+              title="查看"
+            ></el-button>
+            <el-button
+              v-if="
+                scope.row.createId === curUserId && scope.row.status === 'NEW'
+              "
+              class="btn-table-icon"
+              type="text"
+              icon="icon icon-edit"
+              title="推送"
+              :disabled="loading"
+              @click="toPush(scope.row)"
+            ></el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="part-page">
+        <el-pagination
+          background
+          layout="total,prev, pager, next"
+          :current-page="current"
+          :total="total"
+          :page-size="size"
+          @current-change="toPage"
+        >
+        </el-pagination>
+      </div>
+    </div>
+
+    <!-- MergePushDialog -->
+    <merge-push-dialog
+      ref="MergePushDialog"
+      :ids="multipleSelection"
+      @modified="mergePushed"
+    ></merge-push-dialog>
+  </div>
+</template>
+
+<script>
+import { printPlanPushListQuery, printPlanBatchPush } from "../api";
+import MergePushDialog from "../components/MergePushDialog";
+
+export default {
+  name: "print-plan-push-manage",
+  components: { MergePushDialog },
+  data() {
+    return {
+      filter: {
+        printPlanId: "",
+        examId: ""
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      loading: false,
+      dataList: [],
+      multipleSelection: []
+    };
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size
+      };
+      const data = await printPlanPushListQuery(datas);
+      this.dataList = data.records;
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.multipleSelection = [];
+      this.current = page;
+      this.getList();
+    },
+    search() {
+      this.toPage(1);
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val.map(item => item.cardId);
+    },
+    async toBatchPush() {
+      if (this.loading) return;
+      if (!this.multipleSelection.length) {
+        this.$message.error("请选择要批量推送的数据");
+        return;
+      }
+      const result = await this.$confirm("确定要推送选中的计划吗?", "提示", {
+        cancelButtonClass: "el-button--danger is-plain",
+        confirmButtonClass: "el-button--primary",
+        type: "warning"
+      }).catch(() => {});
+      if (result !== "confirm") return;
+
+      this.loading = true;
+      const data = await printPlanBatchPush(
+        this.multipleSelection
+      ).catch(() => {});
+      this.loading = false;
+      if (!data) return;
+
+      this.$message.success("提交成功!");
+      this.toPage(this.current);
+    },
+    toMergePush() {
+      if (!this.multipleSelection.length) {
+        this.$message.error("请选择要合并推送的数据");
+        return;
+      }
+      this.$refs.MergePushDialog.open();
+    },
+    mergePushed() {
+      this.toPage(this.current);
+    },
+    toPreview(row) {
+      console.log(row);
+    },
+    async toPush(row) {
+      if (this.loading) return;
+      const result = await this.$confirm("确定要推送当前计划吗?", "提示", {
+        cancelButtonClass: "el-button--danger is-plain",
+        confirmButtonClass: "el-button--primary",
+        type: "warning"
+      }).catch(() => {});
+      if (result !== "confirm") return;
+
+      this.loading = true;
+      const data = await printPlanBatchPush([row.id]).catch(() => {});
+      this.loading = true;
+      if (!data) return;
+
+      this.$message.success("提交成功!");
+      this.toPage(this.current);
+    }
+  }
+};
+</script>

+ 17 - 0
src/modules/exam/api.js

@@ -106,6 +106,23 @@ export const downloadPaper = examTaskId => {
 export const paperAndCardBatchExport = datas => {
   return $postParam("/api/admin/exam/task/paper_card_download_pdf", datas);
 };
+// publish-print-task
+export const listTaskPrint = datas => {
+  return $post("/api/admin/exam/task/list_task_print", datas);
+};
+export const createTaskPrint = datas => {
+  return $post("/api/admin/exam/task/create_task_print", datas);
+};
+export const removeTaskPrint = examTaskPrintId => {
+  return $post("/api/admin/exam/task/remove_task_print", { examTaskPrintId });
+};
+export const listTaskPrintStudent = datas => {
+  return $post("/api/admin/exam/task/list_task_print_student", datas);
+};
+// 班级查询
+export const listTaskPrintClass = datas => {
+  return $postParam("/api/admin/exam/task/list_task_print_class", datas);
+};
 
 // card
 export const cardForSelectList = datas => {

+ 65 - 0
src/modules/exam/components/ApplyContent.vue

@@ -16,6 +16,7 @@
         <tr>
           <th>试卷类型</th>
           <th>试卷文件</th>
+          <th>答题卡创建方式</th>
           <th>答题卡</th>
           <th v-if="IS_APPLY">操作</th>
         </tr>
@@ -51,6 +52,7 @@
               <i>{{ attachment.filename }}</i>
             </span>
           </td>
+          <td>{{ createCardTypeName }}</td>
           <td
             class="td-link"
             :rowspan="paperAttachments.length"
@@ -63,6 +65,13 @@
               <i class="icon icon-circle-right"></i>
               <i>查看题卡</i>
             </span>
+            <el-button
+              v-if="curTaskApply.makeMethod"
+              size="mini"
+              type="primary"
+              @click="changeCreateCardType"
+              >切换题卡创建方式</el-button
+            >
           </td>
           <td v-if="IS_APPLY">
             <el-button
@@ -76,6 +85,17 @@
         </tr>
       </table>
 
+      <el-form-item label="单次抽卷卷型数量:" label-width="150">
+        <el-input-number
+          v-model="curTaskApply.fetchCount"
+          :min="1"
+          :max="maxFetchCount"
+          :step="1"
+          step-strictly
+          :controls="false"
+        ></el-input-number>
+      </el-form-item>
+
       <h4 class="mb-2">备注说明:</h4>
       <el-input
         class="mb-2"
@@ -200,6 +220,7 @@ import CardOptionDialog from "./CardOptionDialog";
 import { taskApplyDetail, updateTaskApply, updateTaskReview } from "../api";
 import { attachmentPreview } from "../../login/api";
 import SimpleImagePreview from "@/components/SimpleImagePreview";
+import { CARD_SOURCE_TYPE } from "@/constants/enumerate";
 
 const initTaskApply = {
   examTaskId: "",
@@ -212,6 +233,7 @@ const initTaskApply = {
   remark: "",
   courseCode: "",
   courseName: "",
+  fetchCount: 1,
   // 题卡状态
   status: "",
   // 考务规则
@@ -278,6 +300,14 @@ export default {
         }
       }
       return name;
+    },
+    createCardTypeName() {
+      return CARD_SOURCE_TYPE[this.curTaskApply.makeMethod] || "";
+    },
+    maxFetchCount() {
+      return this.paperAttachments.length < 1
+        ? 1
+        : this.paperAttachments.length;
     }
   },
   mounted() {
@@ -285,6 +315,16 @@ export default {
   },
   methods: {
     async initData() {
+      if (!this.examTask.id) {
+        this.curTaskApply = this.$objAssign(initTaskApply, {
+          courseCode: this.examTask.courseCode,
+          courseName: this.examTask.courseName,
+          cardRuleId: this.examTask.cardRuleId,
+          customCard: this.examTask.customCard
+        });
+        return;
+      }
+
       const data = await taskApplyDetail(
         this.examTask.id,
         this.examTask.source
@@ -413,6 +453,31 @@ export default {
     cardConfirm(data) {
       this.curTaskApply = this.$objAssign(this.curTaskApply, data);
     },
+    async changeCreateCardType() {
+      const h = this.$createElement;
+      const result = await this.$confirm({
+        title: "切换题卡操作说明",
+        message: h("div", null, [
+          h("p", null, "1、切换题卡会将之前选择题卡数据删除。"),
+          h(
+            "p",
+            null,
+            "2、之前选择专卡进行绘制,切换题卡后再次选择专卡,需要重新开始绘制。"
+          )
+        ]),
+        cancelButtonClass: "el-button--danger is-plain",
+        confirmButtonClass: "el-button--primary",
+        type: "warning"
+      }).catch(() => {});
+      if (result !== "confirm") return;
+      this.clearMakeMethod();
+      this.toCreateOrViewCard();
+    },
+    clearMakeMethod() {
+      this.curTaskApply.makeMethod = "";
+      this.curTaskApply.cardId = "";
+      // TODO:清除后台记录的题卡
+    },
     getTaskData() {
       let data = { ...this.curTaskApply };
       data.paperType = this.paperAttachments.map(item => item.name).join(",");

+ 264 - 0
src/modules/exam/components/CreatePrintTask.vue

@@ -0,0 +1,264 @@
+<template>
+  <el-dialog
+    class="create-print-task"
+    :visible.sync="modalIsShow"
+    :title="title"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    fullscreen
+    @opened="visibleChange"
+  >
+    <div class="part-box part-box-pad part-box-border">
+      <el-form
+        ref="modalFormComp"
+        label-width="100px"
+        :rules="rules"
+        :model="modalForm"
+      >
+        <el-form-item label="印刷计划:">
+          <span>{{ printPlanName }}</span>
+        </el-form-item>
+        <el-form-item label="考试时间:">{{ createTime }} </el-form-item>
+        <el-form-item label="考场:">
+          <el-input
+            v-model="modalForm.examRoom"
+            placeholder="请填写考场名称"
+            clearable
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="考点:">
+          <el-input
+            v-model="modalForm.examPlace"
+            placeholder="请填写考点名称"
+            clearable
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="监考老师:">
+          <el-select
+            v-model="modalForm.invigilatorTeacher"
+            style="width: 142px;"
+            placeholder="请选择"
+            clearable
+          >
+            <el-option
+              v-for="teacher in teachers"
+              :key="teacher.id"
+              :value="teacher.id"
+              :label="teacher.name"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item prop="classId" label="考试对象:">
+          <el-select
+            v-model="classIds"
+            style="width: 142px;"
+            placeholder="请选择"
+            multiple
+            clearable
+            @change="classChange"
+          >
+            <el-option
+              v-for="clazz in clazzs"
+              :key="clazz.classId"
+              :value="clazz.classId"
+              :label="clazz.className"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item prop="studentCount" label="人数:">
+          <el-input-number
+            v-model="modalForm.studentCount"
+            :min="1"
+            :max="10000"
+            :step="1"
+            step-strictly
+            disabled
+            :controls="false"
+          ></el-input-number>
+        </el-form-item>
+        <el-form-item prop="printHouseId" label="印刷室:">
+          <el-select
+            v-model="modalForm.printHouseId"
+            style="width: 142px;"
+            placeholder="请选择"
+            clearable
+          >
+            <el-option
+              v-for="room in printRooms"
+              :key="room.id"
+              :value="room.id"
+              :label="room.name"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          v-for="item in extendFields"
+          :key="item.code"
+          :label="item.name"
+          :prop="item.code"
+        >
+          <el-input
+            v-model="modalForm[item.code]"
+            :placeholder="`请填写${item.name}`"
+            clearable
+          ></el-input>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div slot="footer">
+      <el-button type="primary" :disabled="isSubmit" @click="submit"
+        >确认</el-button
+      >
+      <el-button type="danger" @click="cancel" plain>取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { calcSum } from "@/plugins/utils";
+import { createTaskPrint } from "../api";
+
+export default {
+  name: "create-print-task",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    clazzs: {
+      type: Array,
+      default() {
+        return [];
+      }
+    },
+    extendFields: {
+      type: Array,
+      default() {
+        return [];
+      }
+    }
+  },
+  computed: {
+    isEdit() {
+      return !!this.instance.id;
+    },
+    title() {
+      return (this.isEdit ? "编辑" : "新增") + "印刷任务";
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: {},
+      tableData: [],
+      printRooms: [],
+      teachers: [],
+      printPlanName: "",
+      createTime: "",
+      classIds: [],
+      rules: {
+        examRoom: [
+          {
+            required: true,
+            message: "请输入考场名称",
+            trigger: "change"
+          }
+        ],
+        examPlace: [
+          {
+            required: true,
+            message: "请输入考点名称",
+            trigger: "change"
+          }
+        ],
+        invigilatorTeacher: [
+          {
+            required: true,
+            message: "请选择监考老师",
+            trigger: "change"
+          }
+        ],
+        classId: [
+          {
+            required: true,
+            message: "请选择考试对象",
+            trigger: "change"
+          }
+        ],
+        studentCount: [
+          {
+            required: true,
+            message: "请输入人数",
+            trigger: "change"
+          }
+        ],
+        printHouseId: [
+          {
+            required: true,
+            message: "请选择印刷室",
+            trigger: "change"
+          }
+        ]
+      }
+    };
+  },
+  created() {
+    this.extendFields.forEach(field => {
+      this.rules[field.code] = [
+        {
+          required: true,
+          message: `请输入${field.name}`,
+          trigger: "change"
+        }
+      ];
+    });
+  },
+  methods: {
+    visibleChange() {
+      this.modalForm = { ...this.instance };
+      this.classIds = this.modalForm.classId.split(",");
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    classChange(vals) {
+      console.log(vals);
+      this.modalForm.className = vals.map(item => item.className);
+      this.modalForm.classId = this.classIds.join(",");
+      this.modalForm.studentCount = calcSum(
+        vals.map(item => item.studentCount)
+      );
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+
+      let datas = { ...this.modalForm };
+      const extendFields = this.extendFields.map(field => {
+        let info = { ...field };
+        info.value = datas[field.code];
+        return info;
+      });
+      datas.extendFields = extendFields;
+
+      const data = await createTaskPrint(datas).catch(() => {});
+      this.isSubmit = false;
+      if (!data) return;
+
+      this.$emit("modified", this.modalForm);
+      this.cancel();
+    }
+  }
+};
+</script>

+ 260 - 0
src/modules/exam/components/CreateTaskApply.vue

@@ -0,0 +1,260 @@
+<template>
+  <el-dialog
+    class="create-task-apply"
+    :visible.sync="modalIsShow"
+    :title="title"
+    top="10px"
+    width="900px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <div class="part-box part-box-pad part-box-border">
+      <el-form
+        ref="modalFormComp"
+        :model="modalForm"
+        :rules="rules"
+        label-width="100px"
+      >
+        <el-row>
+          <el-col :span="12">
+            <el-form-item prop="teachingRoomId" label="教研室:">
+              <staff-room-select
+                v-model="modalForm.teachingRoomId"
+              ></staff-room-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item prop="courseCode" label="课程(代码):">
+              <course-select v-model="modalForm.courseCode"></course-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item prop="cardRuleId" label="题卡规则:">
+              <card-rule-select
+                ref="CardRuleSelect"
+                v-model.trim="modalForm.cardRuleId"
+                placeholder="请选择"
+                clearable
+              ></card-rule-select>
+              <p class="tips-info">
+                说明:若选择全部通卡,则命题老师只能选择通卡,若选择题卡规则,则专卡和通卡均可选择
+              </p>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="试卷编号:">
+              <el-input
+                v-model.trim="modalForm.paperNumber"
+                placeholder="请输入试卷编号"
+                clearable
+              ></el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="拟卷教师:">
+              <el-input
+                v-model.trim="modalForm.paperNumber"
+                placeholder="请输入拟卷教师"
+                clearable
+              ></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="主讲教师/电话:">
+              <el-input
+                v-model.trim="modalForm.paperNumber"
+                placeholder="请输入主讲教师/电话"
+                clearable
+              ></el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </div>
+
+    <div class="mb-4">
+      <el-steps align-center>
+        <el-step
+          v-for="(step, index) in steps"
+          :key="index"
+          :title="step.title"
+          :description="step.desc"
+          :status="step.status"
+        ></el-step>
+      </el-steps>
+    </div>
+
+    <div class="part-box part-box-pad part-box-border" v-if="modalIsShow">
+      <apply-content
+        ref="ApplyContent"
+        :exam-task="modalForm"
+        :edit-type="editType"
+        @cancel="cancel"
+        @modified="modified"
+      ></apply-content>
+    </div>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import ApplyContent from "./ApplyContent";
+import { examRuleDetail } from "../../base/api";
+
+const initModalForm = {
+  id: null,
+  courseCode: "",
+  courseName: "",
+  paperNumber: "",
+  cardRuleId: "",
+  userId: "",
+  userName: ""
+};
+
+const STEPS = [
+  {
+    title: "提交申请",
+    status: {
+      wait: {
+        desc: "待进行",
+        range: []
+      },
+      process: {
+        desc: "进行中",
+        range: ["NEW", "READY", "STAGE"]
+      },
+      success: {
+        desc: "已完成",
+        range: ["SUBMIT", "FINISH"]
+      }
+    }
+  },
+  {
+    title: "审核",
+    status: {
+      wait: {
+        desc: "待进行",
+        range: ["NEW", "READY", "STAGE"]
+      },
+      process: {
+        desc: "进行中",
+        range: ["SUBMIT"]
+      },
+      success: {
+        desc: "已完成",
+        range: ["FINISH"]
+      }
+    }
+  },
+  {
+    title: "入库",
+    status: {
+      wait: {
+        desc: "待进行",
+        range: ["NEW", "READY", "STAGE", "SUBMIT"]
+      },
+      process: {
+        desc: "进行中",
+        range: []
+      },
+      success: {
+        desc: "已完成",
+        range: ["FINISH"]
+      }
+    }
+  }
+];
+
+export default {
+  name: "create-task-apply",
+  components: { ApplyContent },
+  data() {
+    return {
+      modalIsShow: false,
+      modalForm: {},
+      needReview: false,
+      examRule: {},
+      steps: [],
+      actStep: "",
+      editType: "APPLY",
+      rules: {
+        teachingRoomId: [
+          {
+            required: true,
+            message: "请选择教研室",
+            trigger: "change"
+          }
+        ],
+        courseCode: [
+          {
+            required: true,
+            message: "请选择课程",
+            trigger: "change"
+          }
+        ],
+        cardRuleId: [
+          {
+            required: true,
+            message: "请选择题卡规则",
+            trigger: "change"
+          }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getExamRule();
+  },
+  methods: {
+    async getExamRule() {
+      const examRule = await examRuleDetail();
+      this.examRule = examRule || {};
+      this.needReview = examRule && examRule.review;
+    },
+    initData() {
+      this.modalForm = { ...initModalForm };
+      this.modalForm.includePaper = this.examRule.includePaper;
+      this.modalForm.review = this.examRule.review;
+      this.modalForm.customCard = this.examRule.customCard;
+      this.buildSteps();
+    },
+    buildSteps() {
+      // TODO:
+      const curStatus = "";
+      // 构建steps
+      const stepList = this.needReview ? STEPS : [STEPS[0], STEPS[2]];
+      this.steps = stepList.map(step => {
+        const validStepStatus = Object.keys(step.status).find(key =>
+          step.status[key].range.includes(curStatus)
+        );
+
+        return {
+          title: step.title,
+          status: validStepStatus,
+          desc: step.status[validStepStatus].desc
+        };
+      });
+    },
+    visibleChange() {
+      this.initData();
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    modified() {
+      this.cancel();
+      this.$emit("modified");
+    }
+  }
+};
+</script>

+ 104 - 0
src/modules/exam/components/PrintTaskStudents.vue

@@ -0,0 +1,104 @@
+<template>
+  <el-dialog
+    class="print-task-students"
+    :visible.sync="modalIsShow"
+    title="考生名细"
+    top="10vh"
+    width="700px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <div class="part-box part-box-pad part-box-border">
+      <el-form ref="FilterForm" label-position="left" label-width="85px" inline>
+        <el-form-item>
+          <el-input v-model="params" placeholder="请输入" clearable></el-input>
+        </el-form-item>
+        <el-form-item label-width="0px">
+          <el-button type="primary" icon="el-icon-search" @click="toPage(1)"
+            >查询</el-button
+          >
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div class="part-box">
+      <el-table ref="TableList" :data="dataList" border stripe>
+        <el-table-column
+          type="index"
+          label="序号"
+          width="70"
+          align="center"
+          :index="indexMethod"
+        ></el-table-column>
+        <el-table-column prop="studentName" label="姓名"></el-table-column>
+        <el-table-column prop="studentCode" label="学号"></el-table-column>
+        <el-table-column prop="clazz" label="班级"></el-table-column>
+      </el-table>
+      <div class="part-page">
+        <el-pagination
+          v-if="dataList.length"
+          background
+          layout="total,prev, pager, next"
+          :current-page="current"
+          :total="total"
+          :page-size="size"
+          @current-change="toPage"
+        >
+        </el-pagination>
+      </div>
+    </div>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import { listTaskPrintStudent } from "../api";
+
+export default {
+  name: "print-task-students",
+  props: {
+    classId: {
+      type: String,
+      default: ""
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      params: "",
+      dataList: [],
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0
+    };
+  },
+  methods: {
+    visibleChange() {
+      this.toPage(1);
+    },
+    async getList() {
+      const data = await listTaskPrintStudent({
+        classId: this.classId,
+        pageNumber: this.current,
+        pageSize: this.size
+      });
+      this.dataList = data.records;
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    }
+  }
+};
+</script>

+ 275 - 0
src/modules/exam/components/PublishPrintTask.vue

@@ -0,0 +1,275 @@
+<template>
+  <el-dialog
+    class="publish-print-task"
+    :visible.sync="modalIsShow"
+    title="发布印刷任务"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    fullscreen
+    @opened="visibleChange"
+  >
+    <div class="part-box part-box-pad part-box-border">
+      <el-form ref="FilterForm" label-position="left" label-width="85px" inline>
+        <el-form-item label="印刷计划:">
+          <print-plan-select
+            v-model.trim="filter.printPlanId"
+            placeholder="请选择"
+            clearable
+            @change="printPlanChange"
+          ></print-plan-select>
+        </el-form-item>
+        <el-form-item label="考试时间:">
+          <el-date-picker
+            v-model="createTime"
+            type="datetimerange"
+            :picker-options="pickerOptions"
+            :disabled="timeDisabled"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="timestamp"
+            align="right"
+            unlink-panels
+          >
+          </el-date-picker>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="part-box">
+      <el-button type="primary" @click="toAdd">增加考试对象</el-button>
+      <el-table ref="TableList" :data="tableData" border stripe>
+        <el-table-column
+          type="index"
+          width="70"
+          align="center"
+          label="卷袋序号"
+        ></el-table-column>
+        <el-table-column prop="examRoom" label="考场"> </el-table-column>
+        <el-table-column prop="examPlace" label="考点"> </el-table-column>
+        <el-table-column prop="invigilatorTeacher" label="监考老师">
+        </el-table-column>
+        <el-table-column prop="clazzName" label="考试对象"> </el-table-column>
+        <el-table-column prop="studentCount" label="人数"> </el-table-column>
+        <el-table-column prop="printHouseName" label="印刷室">
+        </el-table-column>
+        <el-table-column
+          v-for="item in extendFields"
+          :key="item.code"
+          :label="item.name"
+        >
+          <div slot-scope="scope">
+            {{ scope.row[item.code] }}
+          </div>
+        </el-table-column>
+        <el-table-column label="操作">
+          <template slot-scope="scope">
+            <el-button
+              class="btn-table-icon"
+              type="text"
+              icon="icon icon-circle-right"
+              @click="toPreview(scope.row)"
+              title="考生明细"
+            ></el-button>
+            <el-button
+              class="btn-table-icon"
+              type="text"
+              icon="icon icon-circle-right"
+              @click="toEdit(scope.row)"
+              title="编辑"
+            ></el-button>
+            <el-button
+              class="btn-table-icon"
+              type="text"
+              icon="icon icon-circle-right"
+              @click="toDelete(scope.row)"
+              title="删除"
+            ></el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="part-page">
+        <el-pagination
+          v-if="papers.length"
+          background
+          layout="total,prev, pager, next"
+          :current-page="current"
+          :total="total"
+          :page-size="size"
+          @current-change="toPage"
+        >
+        </el-pagination>
+      </div>
+    </div>
+
+    <div slot="footer"></div>
+
+    <!-- CreatePrintTask -->
+    <create-print-task
+      ref="CreatePrintTask"
+      :instance="curRow"
+      :clazz="validClazzes"
+      :extend-fields="extendFields"
+      @modified="getList"
+    ></create-print-task>
+    <!-- PrintTaskStudents -->
+    <print-task-students
+      ref="PrintTaskStudents"
+      :class-id="curClassId"
+    ></print-task-students>
+  </el-dialog>
+</template>
+
+<script>
+import { listTaskPrint, removeTaskPrint, listTaskPrintClass } from "../api";
+import { examRuleDetail } from "../../base/api";
+import pickerOptions from "@/constants/datePickerOptions";
+import CreatePrintTask from "./CreatePrintTask";
+import PrintTaskStudents from "./PrintTaskStudents";
+
+export default {
+  name: "publish-print-task",
+  components: { CreatePrintTask, PrintTaskStudents },
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      filter: {
+        printPlanId: "",
+        examStartTime: "",
+        examEndTime: "",
+        paperNumber: "",
+        courseName: "",
+        courseCode: ""
+      },
+      timeDisabled: false,
+      tableData: [],
+      curRow: {},
+      printRooms: [],
+      teachers: [],
+      clazzs: [],
+      curClassId: "",
+      validClazzes: [],
+      extendFields: [],
+      // date-picker
+      createTime: [],
+      pickerOptions
+    };
+  },
+  created() {
+    this.getExtendFields();
+  },
+  methods: {
+    visibleChange() {
+      this.filter = this.$objAssign(this.filter, this.instance);
+      this.getClazzs();
+    },
+    async getExtendFields() {
+      const examRule = await examRuleDetail();
+      this.extendFields = JSON.parse(examRule.extendFields);
+    },
+    async getClazzs() {
+      this.clazzs = await listTaskPrintClass({
+        printPlanId: this.filter.printPlanId,
+        courseCode: this.instance.courseCode,
+        paperNumber: this.instance.paperNumber
+      });
+    },
+    async getList() {
+      const datas = {
+        printPlanId: this.filter.printPlanId,
+        courseCode: this.instance.courseCode,
+        paperNumber: this.instance.paperNumber,
+        pageNumber: this.current,
+        pageSize: this.size
+      };
+      const data = await listTaskPrint(datas);
+      if (data.iPage) {
+        this.tableData = data.iPage.records.map(item => {
+          item.extendFields.forEach(field => {
+            item[field.code] = field.value;
+          });
+          return item;
+        });
+        this.total = data.iPage.total;
+      }
+      if (data.examStartTime && data.examEndTime) {
+        this.createTime = [data.examStartTime, data.examEndTime];
+        this.filter.examStartTime = this.createTime[0];
+        this.filter.examEndTime = this.createTime[1];
+        this.timeDisabled = true;
+      }
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    printPlanChange(val) {
+      this.filter.printPlanName = val.name;
+      this.getList();
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    toAdd() {
+      let item = {
+        examPlace: "",
+        examRoom: "",
+        invigilatorTeacher: "",
+        classId: "",
+        className: "",
+        studentCount: 1,
+        printHouseId: null,
+        ...this.filter
+      };
+      this.extendFields.forEach(field => {
+        item[field.code] = "";
+      });
+      this.curRow = item;
+      let unvalidClazzs = [];
+      this.tableData.forEach(item => {
+        unvalidClazzs = [...unvalidClazzs, ...item.classId.split(",")];
+      });
+      this.validClazzes = this.clazzs.filter(
+        item => !unvalidClazzs.includes(item.classId)
+      );
+      this.$refs.CreatePrintTask.open();
+    },
+    toEdit(row) {
+      this.curRow = { ...row };
+      let unvalidClazzs = [];
+      this.tableData
+        .filter(item => item.id !== row.id)
+        .forEach(item => {
+          unvalidClazzs = [...unvalidClazzs, ...item.classId.split(",")];
+        });
+      this.validClazzes = this.clazzs.filter(
+        item => !unvalidClazzs.includes(item.classId)
+      );
+      this.$refs.CreatePrintTask.open();
+    },
+    toPreview(row) {
+      this.curClassId = row.classId;
+      this.$refs.PrintTaskStudents.open();
+    },
+    async toDelete(row) {
+      await removeTaskPrint(row.id);
+      this.$message.success("删除成功!");
+
+      const pos = this.tableData.findIndex(item => item.id === row.id);
+      this.tableData.splice(pos, 1);
+    }
+  }
+};
+</script>

+ 8 - 8
src/modules/print/components/ModifyPrintPlan.vue

@@ -323,13 +323,13 @@ export default {
     }
   },
   data() {
-    const printContentValidator = (rule, value, callback) => {
-      if (value.includes("PAPER") && !value.includes("CARD")) {
-        callback(new Error("选择了试卷,同时必须选择题卡"));
-      } else {
-        callback();
-      }
-    };
+    // const printContentValidator = (rule, value, callback) => {
+    //   if (value.includes("PAPER") && !value.includes("CARD")) {
+    //     callback(new Error("选择了试卷,同时必须选择题卡"));
+    //   } else {
+    //     callback();
+    //   }
+    // };
     const backupMethodValidator = (rule, value, callback) => {
       if (!this.modalForm.printContent.length) {
         return callback();
@@ -389,7 +389,7 @@ export default {
         printContent: [
           {
             required: false,
-            validator: printContentValidator,
+            // validator: printContentValidator,
             trigger: "change"
           }
         ],

+ 4 - 2
src/plugins/globalVuePlugins.js

@@ -1,6 +1,7 @@
 import { objAssign, randomCode, tableAction } from "@/plugins/utils";
 // mixins
-import globalMixins from "./mixins";
+import commonMixins from "../mixins/common";
+import privilegeMixins from "../mixins/privilege";
 // components
 import ViewFooter from "@/components/ViewFooter.vue";
 import RoomSelect from "../components/base/RoomSelect.vue";
@@ -45,6 +46,7 @@ export default {
     });
 
     //全局 mixins
-    Vue.mixin(globalMixins);
+    Vue.mixin(commonMixins);
+    Vue.mixin(privilegeMixins);
   }
 };

+ 1 - 0
src/plugins/utils.js

@@ -373,6 +373,7 @@ export function removeHtmlTag(str) {
  * @param {Array} dataList 需要统计的数组
  */
 export function calcSum(dataList) {
+  if (!dataList.length) return 0;
   return dataList.reduce(function(total, item) {
     return total + item;
   }, 0);

+ 5 - 1
src/store.js

@@ -9,11 +9,15 @@ import exam from "./modules/exam/store";
 
 export default new Vuex.Store({
   state: {
-    user: {}
+    user: {},
+    privilegeMap: {}
   },
   mutations: {
     setUser(state, user) {
       state.user = user;
+    },
+    setPrivilegeMap(state, privilegeMap) {
+      state.privilegeMap = privilegeMap;
     }
   },
   actions: {},

+ 20 - 0
src/views/Home.vue

@@ -236,6 +236,7 @@ export default {
     },
     async getMenus() {
       const data = await sysMenu();
+      this.initPrivilegeMap(data);
       const { menus, firstRouter } = this.menusToTree(data);
       this.menus = menus;
 
@@ -255,6 +256,25 @@ export default {
         this.actCurNav();
       }
     },
+    initPrivilegeMap(data) {
+      let privilegeMap = {};
+      const pageSetTypes = ["querys", "buttons", "columns", "links"];
+      data.forEach(item => {
+        const isPage = pageSetTypes.some(
+          type => item[type] && item[type].length
+        );
+        if (!isPage) return;
+        privilegeMap[item.url] = [];
+        pageSetTypes.forEach((type, index) => {
+          if (item[type] && item[type].length) {
+            item[type].forEach(elem => {
+              privilegeMap[item.url].push(`${type}_${elem.url}`.toLowerCase());
+            });
+          }
+        });
+      });
+      this.$store.dispatch("setPrivilegeMap", privilegeMap);
+    },
     menusToTree(menus) {
       let navTree = deepCopy(localNavs);
       let validRoutes = [];