Pārlūkot izejas kodu

feat: 命题任务改造

zhangjie 1 gadu atpakaļ
vecāks
revīzija
ce3f6138dd

+ 317 - 0
src/modules/exam/components/taskApply/ModifyTaskApply.vue

@@ -0,0 +1,317 @@
+<template>
+  <el-dialog
+    class="modify-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"
+    @close="closeHandle"
+  >
+    <template v-if="dataReady">
+      <task-info></task-info>
+      <task-paper ref="TaskPaper"></task-paper>
+      <task-flow ref="TaskFlow"></task-flow>
+    </template>
+
+    <div slot="footer">
+      <el-button
+        v-if="taskStatus.IS_APPLY"
+        type="primary"
+        :disabled="loading"
+        @click="submit"
+        >确认提交</el-button
+      >
+      <el-button
+        v-if="taskStatus.IS_AUDIT"
+        type="primary"
+        :disabled="loading"
+        @click="toAuditSubmit"
+        >确定</el-button
+      >
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { mapStates, mapMutations } from "vuex";
+import { examRuleDetail } from "../../base/api";
+import { taskApplyDetail, updateTaskApply } from "../../api";
+import { taskFlowApproverExchange, taskFlowApprover } from "../../base/api";
+import { COMMON_CARD_RULE_ID } from "@/constants/enumerate";
+
+import TaskInfo from "./TaskInfo.vue";
+import TaskPaper from "./TaskPaper.vue";
+import TaskFlow from "./TaskFlow.vue";
+
+const initExamTask = {
+  id: null,
+  examId: "",
+  examName: "",
+  examModel: "",
+  category: "",
+  semesterId: "",
+  semesterName: "",
+  courseId: "",
+  courseCode: "",
+  courseName: "",
+  specialty: "",
+  paperNumber: "",
+  startTime: "",
+  endTime: "",
+  cardRuleId: "",
+  cardRuleName: "",
+  flowId: "",
+  setup: null,
+  userId: "",
+  userName: "",
+  propositionName: "",
+  auditStatus: "",
+  reviewStatus: "",
+  source: "",
+  teachingRoomName: "",
+  teacherName: "",
+  lecturerName: "",
+};
+
+const initTaskApply = {
+  examId: "",
+  examTaskId: "",
+  category: "",
+  paperNumber: "",
+  paperType: "A",
+  paperAttachmentIds: "",
+  paperConfirmAttachmentIds: "",
+  cardId: "",
+  cardRuleId: "",
+  makeMethod: "",
+  remark: "",
+  courseId: "",
+  courseCode: "",
+  courseName: "",
+  drawCount: 1,
+  exposedPaperType: "",
+  auditContent: [],
+
+  // 流程
+  flowId: "",
+  flowStatus: "",
+  setup: null,
+  // 工作台任务id
+  flowTaskId: "",
+  // 题卡状态
+  status: "",
+  // 考务规则
+  review: false,
+  includePaper: false,
+  customCard: false,
+  // 作废
+  taskStatus: "",
+  cancelRemark: "",
+};
+
+export default {
+  name: "modify-task-apply",
+  components: { TaskInfo, TaskPaper, TaskFlow },
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    type: {
+      type: String,
+      validate(val) {
+        return ["APPLY", "PREVIEW", "AUDIT"].includes(val);
+      },
+    },
+  },
+  computed: {
+    ...mapStates("exam", [
+      "editType",
+      "examTask",
+      "curTaskApply",
+      "taskStatus",
+    ]),
+    title() {
+      // editType为主
+      if (this.editType) {
+        const names = {
+          APPLY: "提交入库申请",
+          PREVIEW: "入库申请详情",
+          AUDIT: "审核入库申请",
+        };
+        return names[this.editType];
+      }
+
+      if (this.examTask.setup === 1 || this.examTask.setup === null) {
+        return "提交入库申请";
+      } else if (this.examTask.setup <= 0) {
+        return "入库申请详情";
+      } else {
+        return "审核入库申请";
+      }
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      loading: false,
+      examRule: {},
+      dataReady: false,
+    };
+  },
+  created() {
+    this.getExamRule();
+  },
+  methods: {
+    ...mapMutations("exam", [
+      "setEditType",
+      "setExamTask",
+      "setCurTaskApply",
+      "setTaskStatus",
+    ]),
+    async getExamRule() {
+      const examRule = await examRuleDetail();
+      this.examRule = examRule || {};
+    },
+    async initData() {
+      this.setEditType(this.type);
+
+      const data = await taskApplyDetail(this.rowData.id, this.rowData.source);
+      const examTask = this.$objAssign(initExamTask, {
+        ...this.rowData,
+        review: this.examRule.review,
+        customCard: this.examRule.customCard,
+        setup: data.setup,
+        semesterName: data.semesterName,
+        examId: data.examId,
+        examName: data.examName,
+        examModel: data.examModel,
+      });
+      this.setExamTask(examTask);
+
+      // curTaskApply
+      const curTaskApply = this.$objAssign(initTaskApply, {
+        ...data,
+        examTaskId: examTask.id,
+        courseId: examTask.courseId,
+        courseName: examTask.courseName,
+        cardRuleId: examTask.cardRuleId,
+        customCard: examTask.customCard,
+        paperNumber: examTask.paperNumber,
+        auditContent: JSON.parse(data.auditContent || "[]"),
+        includePaper: data.printContent.indexOf("PAPER") !== -1,
+      });
+      this.setCurTaskApply(curTaskApply);
+      this.updateStatus();
+      this.dataReady = true;
+    },
+    updateStatus() {
+      const IS_APPLY = this.editType
+        ? this.editType === "APPLY"
+        : this.curTaskApply.setup === 1 || this.curTaskApply.setup === null;
+
+      const IS_PREVIEW = this.editType
+        ? this.editType === "PREVIEW"
+        : this.curTaskApply.setup !== null && this.curTaskApply.setup <= 0;
+
+      const IS_AUDIT = this.editType
+        ? this.editType === "AUDIT"
+        : this.curTaskApply.setup > 1;
+
+      const CAN_CREATE_CARD =
+        this.curTaskApply.courseId &&
+        this.curTaskApply.examId &&
+        this.curTaskApply.cardRuleId !== COMMON_CARD_RULE_ID;
+
+      const IS_EXPOSED_MODE = !!this.curTaskApply.exposedPaperType;
+
+      const IS_REBUILD = this.curTaskApply.category === "REBUILD";
+
+      this.setTaskStatus({
+        IS_APPLY,
+        IS_PREVIEW,
+        IS_AUDIT,
+        CAN_CREATE_CARD,
+        IS_EXPOSED_MODE,
+        IS_REBUILD,
+      });
+    },
+    visibleChange() {
+      this.initData();
+    },
+    closeHandle() {
+      this.dataReady = false;
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    modified() {
+      this.cancel();
+      this.$emit("modified");
+    },
+    async submit() {
+      if (!this.$refs.TaskPaper.checkData()) return;
+      if (!this.$refs.TaskFlow.checkData()) return;
+
+      const result = await this.$confirm(
+        "任务确定提交后,则不可更改试卷及答题卡内容,确定提交该任务?",
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (result !== "confirm") return;
+
+      const paperData = this.$refs.TaskPaper.getData();
+      const flowData = this.$refs.TaskFlow.getData();
+      const datas = {
+        ...this.curTaskApply,
+        ...paperData,
+        ...flowData,
+        operateType: "SUBMIT",
+      };
+      const data = await updateTaskApply(datas).catch(() => {});
+      if (!data) return;
+      this.$message.success("提交成功!");
+      this.modified();
+    },
+    async toAuditSubmit() {
+      const valid = await this.$refs.TaskFlow.checkAuditData();
+      if (!valid) return;
+
+      const actionName = this.TASK_AUDIT_RESULT[this.auditModal.approvePass];
+      const result = await this.$confirm(
+        `确定${actionName}该申请吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (result !== "confirm") return;
+
+      const datas = this.$refs.TaskFlow.getAuditData();
+
+      const apiFunc =
+        datas.approvePass === "EXCHANGE"
+          ? taskFlowApproverExchange
+          : taskFlowApprover;
+
+      const data = await apiFunc(datas).catch(() => {});
+      if (!data) return;
+
+      this.$message.success("审批成功!");
+      this.modified();
+    },
+  },
+};
+</script>

+ 536 - 0
src/modules/exam/components/taskApply/TaskFlow.vue

@@ -0,0 +1,536 @@
+<template>
+  <div>
+    <!-- 入库申请-审核阶段 -->
+    <!-- audit history -->
+    <div
+      v-if="flowHistoryList.length && curTaskApply.flowStatus !== 'START'"
+      class="task-audit-history flow-timeline"
+    >
+      <h4 class="mb-4">审核记录:</h4>
+      <el-timeline>
+        <el-timeline-item
+          v-for="flow in flowHistoryList"
+          :key="flow.stepKey"
+          :type="flow.type"
+          hide-timestamp
+          size="large"
+          placement="top"
+          :class="{ 'timeline-item-stop': flow.nextIsNewFlow }"
+        >
+          <div class="flow-item">
+            <div class="flow-item-content">
+              <p v-if="flow.createTime" class="flow-item-time">
+                {{ flow.createTime | timestampFilter }}
+              </p>
+              <h4 class="flow-item-title">{{ flow.approveUserName }}</h4>
+              <p class="flow-item-desc">
+                <span
+                  v-if="flow.approveOperation"
+                  :class="{
+                    'color-danger': flow.approveOperation === 'REJECT',
+                  }"
+                  >{{
+                    flow.approveOperation | flowApproveOperationTypeFilter
+                  }}</span
+                >
+                <span>{{ flow.approveRemark }}</span>
+              </p>
+              <div v-if="flow.attachments.length" class="flow-item-attachment">
+                <span>附件:</span>
+                <more-btn
+                  v-for="item in flow.attachments"
+                  :key="item.name"
+                  type="text"
+                  class="btn-primary"
+                  :data="`${item.name}卷:${item.filename}`"
+                  :show-count="20"
+                  @click="downloadPaper(item)"
+                ></more-btn>
+              </div>
+              <div
+                v-if="flow.isApproveSetFlowNextNode && approveUsers.length"
+                class="flow-item-users"
+              >
+                <span>审批人:</span>
+                <el-tag
+                  v-for="user in approveUsers"
+                  :key="user.id"
+                  size="small"
+                  :disable-transitions="false"
+                >
+                  {{ user.name }}
+                </el-tag>
+              </div>
+            </div>
+            <div
+              v-if="
+                flow.isApproveSetFlowNextNode &&
+                ((taskStatus.IS_AUDIT && auditModal.approvePass === 'PASS') ||
+                  curTaskApply.flowStatus === 'REJECT' ||
+                  curTaskApply.flowStatus === 'CANCEL')
+              "
+              class="flow-item-action"
+            >
+              <el-button
+                class="user-select"
+                icon="el-icon-plus"
+                @click="toSelectNextFlowUser"
+              ></el-button>
+              <p v-if="!approveUsers.length" class="tips-info tips-error">
+                请选择审核人
+              </p>
+            </div>
+          </div>
+        </el-timeline-item>
+      </el-timeline>
+    </div>
+    <!-- 入库申请-申请阶段 -->
+    <div
+      v-if="flowList.length && curTaskApply.flowStatus === 'START'"
+      class="task-audit-history flow-timeline"
+    >
+      <h4 class="mb-4">流程:</h4>
+      <el-timeline>
+        <el-timeline-item
+          v-for="flow in flowList"
+          :key="flow.taskKey"
+          :type="flow.type"
+        >
+          <div class="flow-item">
+            <div class="flow-item-content">
+              <h4 class="flow-item-title">{{ flow.taskName }}</h4>
+              <p v-if="flow.approveUserNames" class="flow-item-desc">
+                {{ flow.approveUserNames }}
+              </p>
+              <div
+                v-if="flow.isApproveSetFlowNextNode && approveUsers.length"
+                class="flow-item-users"
+              >
+                <span>审批人:</span>
+                <el-tag
+                  v-for="user in approveUsers"
+                  :key="user.id"
+                  size="small"
+                  :disable-transitions="false"
+                >
+                  {{ user.name }}
+                </el-tag>
+              </div>
+            </div>
+            <div v-if="flow.isApproveSetFlowNextNode" class="flow-item-action">
+              <el-button
+                class="user-select"
+                icon="el-icon-plus"
+                @click="toSelectNextFlowUser"
+              ></el-button>
+              <p v-if="!approveUsers.length" class="tips-info tips-error">
+                请选择审核人
+              </p>
+            </div>
+          </div>
+        </el-timeline-item>
+      </el-timeline>
+    </div>
+
+    <!-- audit -->
+    <div v-if="taskStatus.IS_AUDIT" class="task-audit">
+      <el-form
+        ref="auditModalComp"
+        :model="auditModal"
+        :rules="auditRules"
+        label-width="90px"
+      >
+        <el-form-item prop="approvePass" label="审批操作:">
+          <el-radio-group
+            v-model="auditModal.approvePass"
+            @change="approvePassChange"
+          >
+            <el-radio
+              v-for="(val, key) in TASK_AUDIT_RESULT"
+              :key="key"
+              :label="key"
+              >{{ val }}</el-radio
+            >
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item
+          v-if="auditModal.approvePass === 'REJECT'"
+          prop="setup"
+          label="驳回节点:"
+        >
+          <el-select
+            v-model="auditModal.setup"
+            placeholder="请选择"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in rejectSetupList"
+              :key="item.taskKey"
+              :value="item.setup"
+              :label="item.name"
+            >
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          v-if="auditModal.approvePass !== 'EXCHANGE'"
+          :key="auditModal.approvePass"
+          prop="remark"
+          label="审批意见:"
+        >
+          <el-input
+            class="mb-2"
+            v-model="auditModal.remark"
+            type="textarea"
+            resize="none"
+            :rows="5"
+            :maxlength="100"
+            clearable
+            show-word-limit
+            placeholder="建议不超过100个字"
+            ref="ReasonInput"
+          ></el-input>
+        </el-form-item>
+        <el-form-item
+          v-if="auditModal.approvePass === 'EXCHANGE'"
+          prop="userId"
+          label="审批人:"
+        >
+          <el-tag
+            v-for="user in exchangeUsers"
+            :key="user.id"
+            :disable-transitions="false"
+            size="medium"
+          >
+            {{ user.name }}
+          </el-tag>
+          <el-button
+            class="ml-2"
+            size="mini"
+            type="primary"
+            @click="toSelectExchangeUser"
+            >选择用户</el-button
+          >
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- select-user-dialog -->
+    <select-user-dialog
+      v-if="taskStatus.IS_AUDIT || IS_NEED_SELECT_APPROVE_USER"
+      ref="SelectUserDialog"
+      :user-limit-count="userLimitCount"
+      :filter-roles="userFilterRoles"
+      :users="curSelectedUsers"
+      @modified="userSelected"
+    ></select-user-dialog>
+  </div>
+</template>
+
+<script>
+import { mapStates, mapMutations } from "vuex";
+import {
+  taskFlowDetail,
+  taskFlowNodeInfo,
+  flowDetailByFlowId,
+} from "../../../base/api";
+import { attachmentPreview } from "../../../login/api";
+import { TASK_AUDIT_RESULT } from "@/constants/enumerate";
+
+import SelectUserDialog from "../../../base/components/SelectUserDialog";
+
+export default {
+  name: "task-flow",
+  components: { SelectUserDialog },
+  data() {
+    return {
+      flowList: [],
+      flowInfo: {},
+      flowHistoryList: [],
+      exchangeUsers: [],
+      selectUserType: "exchange", // exchange:转审,approve:下一节点审核
+      curSelectedUsers: [],
+      auditLogCache: { paper: {}, card: {} },
+      // 选择下一节点审批人
+      IS_NEED_SELECT_APPROVE_USER: false,
+      nextFlowTaskResult: {}, //下一节点信息
+      approveUsers: [],
+      userLimitCount: 1,
+      userFilterRoles: [],
+      // audit
+      TASK_AUDIT_RESULT: { ...TASK_AUDIT_RESULT, EXCHANGE: "转他人审批" },
+      rejectSetupList: [], // 可以驳回的节点
+      auditModal: {
+        approvePass: "PASS",
+        setup: "",
+        remark: "",
+        userId: "",
+      },
+      auditRules: {
+        approvePass: [
+          {
+            required: true,
+          },
+        ],
+        setup: [
+          {
+            required: true,
+            validator: (rule, value, callback) => {
+              if (this.auditModal.approvePass === "REJECT" && !value) {
+                callback(new Error(`请选择要驳回到的节点`));
+              } else {
+                callback();
+              }
+            },
+            trigger: "change",
+          },
+        ],
+        remark: [
+          {
+            required: false,
+            validator: (rule, value, callback) => {
+              if (this.auditModal.approvePass === "REJECT" && !value) {
+                callback(new Error(`请输入审批意见`));
+              } else {
+                callback();
+              }
+            },
+            trigger: "change",
+          },
+        ],
+        userId: [
+          {
+            required: true,
+            message: "请选择审批人",
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  computed: {
+    ...mapStates("exam", [
+      "editType",
+      "examTask",
+      "curTaskApply",
+      "taskStatus",
+    ]),
+  },
+  methods: {
+    ...mapMutations("exam", ["setEditType", "setExamTask", "setCurTaskApply"]),
+    async initData() {
+      const paperAttachments = this.curTaskApply.paperAttachmentIds
+        ? JSON.parse(this.curTaskApply.paperAttachmentIds)
+        : [];
+
+      const auditLogCache = { paper: {}, card: {} };
+      paperAttachments.forEach((paper) => {
+        if (
+          paper.attachmentId &&
+          this.curTaskApply.auditContent.includes("PAPER")
+        )
+          auditLogCache.paper[paper.attachmentId] = false;
+        if (paper.cardId && this.curTaskApply.auditContent.includes("CARD"))
+          auditLogCache.card[paper.cardId] = false;
+      });
+      this.auditLogCache = auditLogCache;
+
+      if (this.curTaskApply.flowStatus === "START") {
+        await this.getFlowList();
+      } else {
+        await this.getFlowHistory();
+        if (!this.taskStatus.IS_PREVIEW && this.curTaskApply.review)
+          this.getCurFlowNodeInfo();
+      }
+    },
+    async getFlowHistory() {
+      if (!this.curTaskApply.flowId) return;
+
+      const data = await taskFlowDetail(this.curTaskApply.flowId).catch(
+        () => {}
+      );
+      if (!data) return;
+
+      const FLOW_STATUS = {
+        SUBMIT: "success",
+        APPROVE: "success",
+        EXCHANGE: "success",
+        REJECT: "danger",
+        END: "success",
+      };
+      let nextFlowId = "";
+      this.flowHistoryList = data.tfFlowViewLogResultList.map((item, index) => {
+        const nextItem = data.tfFlowViewLogResultList[index + 1];
+        nextFlowId = nextItem ? nextItem.flowId : item.flowId;
+        item.nextIsNewFlow = nextFlowId !== item.flowId;
+        item.type = FLOW_STATUS[item.approveOperation];
+        item.attachments = item.paperAttachmentId
+          ? JSON.parse(item.paperAttachmentId)
+          : [];
+        item.isApproveSetFlowNextNode = false;
+        return item;
+      });
+      const flowIsEnd = data.currFlowTaskResult.taskKey === "end";
+      this.flowHistoryList.push({
+        type: flowIsEnd ? "success" : "current",
+        stepKey: this.$randomCode(),
+        approveSetup: data.currFlowTaskResult.setup, //审批人节点
+        approveRemark: data.currFlowTaskResult.taskName, //审批信息
+        approveOperation: flowIsEnd ? "END" : "",
+        approveUserName: data.currFlowTaskResult.approveUserNames, //审批人
+        createTime: null,
+        nextIsNewFlow: false,
+        isApproveSetFlowNextNode: false,
+        attachments: [],
+      });
+    },
+    async getFlowList() {
+      if (!this.curTaskApply.flowId) return;
+      const data = await flowDetailByFlowId(this.curTaskApply.flowId);
+      if (!data) return;
+
+      const modelType =
+        data.flowTaskResultList[0] && data.flowTaskResultList[0].modelType;
+      this.flowInfo = {
+        customFlowId: data.id,
+        version: data.version,
+      };
+      const nextFlowNodeIndex = 1;
+      this.IS_NEED_SELECT_APPROVE_USER = modelType === "APPROVE_SET";
+      const flowList = data.flowTaskResultList || [];
+      this.flowList = flowList.map((flow, index) => {
+        flow.isApproveSetFlowNextNode =
+          this.IS_NEED_SELECT_APPROVE_USER && index === nextFlowNodeIndex;
+        return flow;
+      });
+
+      if (this.flowList.length) {
+        this.flowList[0].type = "success";
+      }
+
+      this.nextFlowTaskResult = this.flowList[nextFlowNodeIndex];
+    },
+    async getCurFlowNodeInfo() {
+      const data = await taskFlowNodeInfo(this.curTaskApply.flowTaskId);
+      this.rejectSetupList = (data && data.rejectSetupList) || [];
+      this.rejectSetupList.forEach((item) => {
+        item.name = `【${item.taskName}】${item.approveUserNames}`;
+      });
+      this.nextFlowTaskResult = data.nextFlowTaskResult;
+      // 被打回给命题老师之后,命题老师只能通过。
+      if (data.setup === 1 && !this.rejectSetupList.length) {
+        this.TASK_AUDIT_RESULT = { PASS: "通过" };
+        this.auditModal.approvePass = "PASS";
+      }
+      // 判断发起人自选,不管approveUserNames有没有值,都可以选择下个审核人员
+      const { modelType, taskKey } = this.nextFlowTaskResult;
+      this.IS_NEED_SELECT_APPROVE_USER =
+        modelType === "APPROVE_SET" && taskKey.toLowerCase() !== "end";
+      this.flowHistoryList[
+        this.flowHistoryList.length - 1
+      ].isApproveSetFlowNextNode = this.IS_NEED_SELECT_APPROVE_USER;
+    },
+    approvePassChange() {
+      this.auditRules.remark[0].required =
+        this.auditModal.approvePass === "REJECT";
+    },
+    async downloadPaper(attachment) {
+      if (!attachment.attachmentId) return;
+      this.addPreviewLog(attachment, "paper");
+      const data = await attachmentPreview(attachment.attachmentId);
+      window.open(data.url);
+    },
+    toSelectNextFlowUser() {
+      if (!this.IS_NEED_SELECT_APPROVE_USER) return;
+
+      this.userLimitCount =
+        this.nextFlowTaskResult.approveUserCountType === "ONE" ? 1 : 0;
+      this.userFilterRoles =
+        this.nextFlowTaskResult.approveUserSelectRange === "ROLE"
+          ? this.nextFlowTaskResult.approveUserSelectRoles.map(
+              (item) => item.id
+            )
+          : [];
+      this.selectUserType = "approve";
+      this.curSelectedUsers = this.approveUsers;
+      this.$refs.SelectUserDialog.open();
+    },
+    toSelectExchangeUser() {
+      this.userLimitCount = 1;
+      this.userFilterRoles = [];
+      this.curSelectedUsers = this.exchangeUsers;
+      this.selectUserType = "exchange";
+      this.$refs.SelectUserDialog.open();
+    },
+    userSelected(users) {
+      if (this.selectUserType === "exchange") {
+        this.exchangeUsers = users;
+        this.auditModal.userId = users[0].id;
+      } else {
+        this.approveUsers = users;
+      }
+    },
+    // action
+    async checkAuditData() {
+      const res =
+        !Object.values(this.auditLogCache.paper).some((item) => !item) &&
+        !Object.values(this.auditLogCache.card).some((item) => !item);
+      if (!res) {
+        this.$message.error(
+          "还有未被查看过的题卡或试卷,请点击试卷及题卡进行查看审核"
+        );
+        return;
+      }
+
+      const valid = await this.$refs.auditModalComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (
+        this.auditModal.approvePass === "PASS" &&
+        this.IS_NEED_SELECT_APPROVE_USER &&
+        !this.approveUsers.length
+      ) {
+        this.$message.error("请设置审核人员!");
+        return;
+      }
+
+      return true;
+    },
+    getAuditData() {
+      let datas = {};
+      if (this.auditModal.approvePass === "EXCHANGE") {
+        datas = {
+          approvePass: this.auditModal.approvePass,
+          taskId: this.curTaskApply.flowTaskId,
+          userId: this.auditModal.userId,
+        };
+      } else {
+        datas = { ...this.auditModal };
+        datas.taskId = this.curTaskApply.flowTaskId;
+
+        if (
+          this.auditModal.approvePass === "PASS" &&
+          this.IS_NEED_SELECT_APPROVE_USER
+        )
+          datas.approveUserIds = this.approveUsers.map((item) => item.id);
+      }
+      return datas;
+    },
+    getData() {
+      if (this.IS_NEED_SELECT_APPROVE_USER) {
+        return {
+          approveUserIds: this.approveUsers.map((item) => item.id),
+        };
+      }
+      return {};
+    },
+    checkData() {
+      if (this.IS_NEED_SELECT_APPROVE_USER && !this.approveUsers.length) {
+        this.$message.error("请设置审核人员!");
+        return;
+      }
+      return true;
+    },
+  },
+};
+</script>

+ 154 - 0
src/modules/exam/components/taskApply/TaskInfo.vue

@@ -0,0 +1,154 @@
+<template>
+  <div>
+    <el-form class="form-info mb-4" label-width="120px">
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="课程(代码):">
+            <span>{{ examTask.courseName }}({{ examTask.courseCode }})</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="机构:">
+            <span>{{ examTask.teachingRoomName | defaultFieldFilter }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="使用学期:">
+            <span>{{ examTask.semesterName || "--" }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="考试:">
+            <span>{{ examTask.examName || "--" }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="拟卷教师:">
+            <span>{{ examTask.teacherName | defaultFieldFilter }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="命题老师:">
+            <span>{{ examTask.propositionName || examTask.userName }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="考试时间:">
+            <span v-if="examRoomInfo.examDate && examRoomInfo.examTime">
+              {{ examRoomInfo.examDate }} <i class="mr-1"></i>
+              {{ examRoomInfo.examTime }}
+            </span>
+            <span v-else>--</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="审核状态:">
+            <span>{{ examTask.auditStatus | auditStatusFilter }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <div v-if="!IS_MODEL2" class="task-exam-room">
+      <el-table
+        :data="examRoomClassList"
+        border
+        style="width: 100%"
+        max-height="300px"
+      >
+        <el-table-column
+          v-if="hasClassInfo"
+          prop="clazzNames"
+          label="使用班级"
+          min-width="400"
+        ></el-table-column>
+        <el-table-column
+          prop="studentCount"
+          label="人数"
+          min-width="100"
+        ></el-table-column>
+        <el-table-column
+          prop="printCount"
+          label="总印份数"
+          min-width="100"
+        ></el-table-column>
+      </el-table>
+    </div>
+
+    <div class="part-box part-box-pad task-exam-room" v-if="IS_MODEL2">
+      <p>
+        正式考生{{ examRoomClassInfo.studentCount }}个,总印份数{{
+          examRoomClassInfo.printCount
+        }}份(正式考生{{ examRoomClassInfo.studentCount }}份,备用卷{{
+          examRoomClassInfo.backupCount
+        }}份)
+      </p>
+      <p>考试对象:{{ examRoomClassInfo.clazzNames }}</p>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapStates } from "vuex";
+import { taskApplyExamObject } from "../api";
+import { parseTimeRangeDateAndTime } from "@/plugins/utils";
+
+export default {
+  name: "task-info",
+  data() {
+    return {
+      hasClassInfo: false,
+      examRoomClassList: [],
+      examRoomClassInfo: {},
+      examRoomInfo: {},
+    };
+  },
+  computed: {
+    ...mapStates("exam", ["editType", "examTask"]),
+    IS_MODEL2() {
+      return this.examTask.examModel === "MODEL2";
+    },
+  },
+  mounted() {
+    this.getExamRoomInfo();
+  },
+  methods: {
+    async getExamRoomInfo() {
+      const data = await taskApplyExamObject({
+        paperNumber: this.examTask.paperNumber,
+        examId: this.examTask.examId,
+      });
+
+      if (this.IS_MODEL2) {
+        const { printCount, clazzNames } = data.examRoomClassList[0];
+        const nums = printCount.split("+").map((item) => item * 1);
+        this.examRoomClassInfo = {
+          backupCount: nums[1],
+          studentCount: nums[0],
+          printCount: nums[0] + nums[1],
+          clazzNames,
+        };
+      } else {
+        this.examRoomClassList = data.examRoomClassList || [];
+        this.hasClassInfo = this.examRoomClassList.some(
+          (item) => item.clazzNames
+        );
+      }
+      const { date, time } = parseTimeRangeDateAndTime(
+        data.examStartTime,
+        data.examEndTime
+      );
+      this.examRoomInfo = {
+        examDate: date,
+        examTime: time,
+      };
+    },
+  },
+};
+</script>

+ 846 - 0
src/modules/exam/components/taskApply/TaskPaper.vue

@@ -0,0 +1,846 @@
+<template>
+  <div>
+    <!-- menu -->
+    <div
+      v-if="checkPrivilege('button', 'SelectTikuPaper', 'TaskApplyManage')"
+      class="mb-4 tab-btns"
+    >
+      <el-button
+        v-for="tab in tabs"
+        :key="tab.val"
+        size="medium"
+        :type="curTab == tab.val ? 'primary' : 'default'"
+        @click="selectMenu(tab.val)"
+        >{{ tab.name }}
+      </el-button>
+    </div>
+    <!-- table -->
+    <table class="table mb-2">
+      <colgroup>
+        <col width="100" />
+        <col width="240" />
+        <col />
+        <col v-if="taskStatus.IS_APPLY && !taskStatus.IS_REBUILD" width="80" />
+      </colgroup>
+      <tr>
+        <th>试卷类型</th>
+        <th>试卷文件</th>
+        <th>答题卡</th>
+        <th v-if="taskStatus.IS_APPLY && !taskStatus.IS_REBUILD">操作</th>
+      </tr>
+      <tr v-for="(attachment, index) in paperAttachments" :key="index">
+        <td>
+          <span>{{ attachment.name }}卷</span>
+          <span class="color-gray-2" v-if="attachment.isExposed">(已曝光)</span>
+        </td>
+        <template v-if="IS_TIKU_TAB">
+          <td>
+            <el-button
+              v-if="!attachment.isExposed && taskStatus.IS_APPLY"
+              type="text"
+              class="btn-primary"
+              @click="toSelect(attachment)"
+            >
+              <i
+                :class="[
+                  'icon',
+                  attachment.attachmentId ? 'icon-files-act' : 'icon-files',
+                ]"
+              ></i
+              >{{ attachment.filename || "选择试卷" }}
+            </el-button>
+            <el-button
+              v-else
+              type="text"
+              class="btn-primary"
+              @click="downloadPaper(attachment)"
+            >
+              <div
+                :class="{
+                  'color-primary': auditLogCache.paper[attachment.attachmentId],
+                }"
+              >
+                <i
+                  class="icon icon-download mr-1"
+                  v-if="attachment.attachmentId"
+                ></i>
+                {{ attachment.filename }}
+              </div>
+            </el-button>
+          </td>
+          <td>
+            <template v-if="taskStatus.IS_APPLY">
+              <el-select
+                v-model="attachment.cardId"
+                placeholder="请选择"
+                style="width: 260px; margin-right: 10px"
+              >
+                <el-option
+                  v-if="attachment.cardId"
+                  :value="attachment.cardId"
+                  :label="attachment.cardTitle"
+                >
+                </el-option>
+              </el-select>
+              <el-button
+                class="btn-primary"
+                type="text"
+                :disabled="!attachment.cardId"
+                @click="toViewCard(attachment)"
+                >预览</el-button
+              >
+              <el-button
+                v-if="!taskStatus.IS_REBUILD"
+                class="btn-primary"
+                type="text"
+                :disabled="!attachment.cardId"
+                @click="toEditCard(attachment)"
+                >编辑</el-button
+              >
+            </template>
+            <el-button
+              v-else
+              type="text"
+              class="btn-primary"
+              @click="toViewCard(attachment)"
+              ><i
+                :class="{
+                  'color-primary': auditLogCache.card[attachment.cardId],
+                }"
+                >{{ attachment.cardTitle || "预览" }}</i
+              ></el-button
+            >
+          </td>
+        </template>
+        <template v-else>
+          <td>
+            <el-button
+              v-if="!attachment.isExposed && taskStatus.IS_APPLY"
+              type="text"
+              class="btn-primary"
+              @click="toUpload(attachment)"
+            >
+              <i
+                :class="[
+                  'icon',
+                  attachment.attachmentId ? 'icon-files-act' : 'icon-files',
+                ]"
+              ></i
+              >{{
+                attachment.attachmentId
+                  ? attachment.filename
+                  : "点击上传试卷文件"
+              }}
+            </el-button>
+            <el-button
+              v-else
+              type="text"
+              class="btn-primary"
+              @click="downloadPaper(attachment)"
+            >
+              <div
+                :class="{
+                  'color-primary': auditLogCache.paper[attachment.attachmentId],
+                }"
+              >
+                <i
+                  class="icon icon-download mr-1"
+                  v-if="attachment.attachmentId"
+                ></i>
+                {{ attachment.filename }}
+              </div>
+            </el-button>
+          </td>
+          <td>
+            <template v-if="taskStatus.IS_APPLY">
+              <el-select
+                class="mr-2"
+                v-model="attachment.cardId"
+                placeholder="请选择"
+                style="width: 200px"
+                filterable
+                @visible-change="
+                  (visible) => cardOptionOpened(visible, attachment)
+                "
+                @change="cardChange(attachment)"
+              >
+                <el-option
+                  v-for="item in cards"
+                  :key="item.id"
+                  :value="item.id"
+                  :label="item.title"
+                  :disabled="item.disabled"
+                >
+                  <span
+                    :class="[
+                      item.type === 'GENERIC'
+                        ? 'color-success'
+                        : 'color-primary',
+                      'mr-1',
+                      {
+                        'color-danger': item.used,
+                      },
+                    ]"
+                    >[{{ item.type === "GENERIC" ? "通" : "专" }}]</span
+                  >
+                  {{ item.title }}
+                </el-option>
+              </el-select>
+              <span
+                v-if="attachment.cardId"
+                :class="[
+                  attachment.cardType === 'GENERIC'
+                    ? 'color-success'
+                    : 'color-primary',
+                  'mr-1',
+                  {
+                    'color-danger': attachment.used,
+                  },
+                ]"
+                >[{{ attachment.cardType === "GENERIC" ? "通" : "专" }}]</span
+              >
+              <el-button
+                class="btn-primary"
+                type="text"
+                :disabled="!attachment.cardId"
+                @click="toViewCard(attachment)"
+                >预览</el-button
+              >
+              <template v-if="!taskStatus.IS_REBUILD">
+                <el-button
+                  class="btn-primary"
+                  type="text"
+                  :disabled="
+                    !attachment.cardId ||
+                    (attachment.cardType === 'GENERIC' &&
+                      attachment.createMethod !== 'STANDARD')
+                  "
+                  @click="toCopyCard(attachment)"
+                  >复制</el-button
+                >
+                <el-button
+                  class="btn-primary"
+                  type="text"
+                  :disabled="
+                    !attachment.cardId ||
+                    attachment.cardType === 'GENERIC' ||
+                    !(!attachment.used && attachment.createId === user.id)
+                  "
+                  @click="toEditCard(attachment)"
+                  >编辑</el-button
+                >
+                <el-button
+                  class="btn-primary"
+                  type="text"
+                  :disabled="!taskStatus.CAN_CREATE_CARD"
+                  @click="toCreateCard(attachment)"
+                  >新建</el-button
+                >
+              </template>
+            </template>
+            <el-button
+              v-else
+              type="text"
+              class="btn-primary"
+              @click="toViewCard(attachment)"
+            >
+              <i
+                :class="{
+                  'color-primary': auditLogCache.card[attachment.cardId],
+                }"
+              >
+                {{ attachment.cardTitle || "预览" }}
+              </i>
+            </el-button>
+          </td>
+        </template>
+        <td
+          v-if="taskStatus.IS_APPLY && !taskStatus.IS_REBUILD"
+          class="text-right"
+        >
+          <el-button
+            v-if="index === paperAttachments.length - 1"
+            class="btn-primary btn-icon"
+            type="text"
+            icon="el-icon-circle-plus"
+            @click="addAtachment"
+          ></el-button>
+          <el-button
+            v-if="attachment.canDelete"
+            class="btn-danger btn-icon"
+            type="text"
+            icon="el-icon-remove"
+            @click="deleteAttachment(index)"
+          ></el-button>
+        </td>
+      </tr>
+      <tr v-if="!paperAttachments.length">
+        <td colspan="5">
+          <p class="tips-info text-center">暂无数据</p>
+        </td>
+      </tr>
+    </table>
+
+    <el-form v-if="!taskStatus.IS_REBUILD">
+      <el-form-item label="单次抽卷卷型数量:" label-width="150">
+        <el-input-number
+          v-model="curTaskApply.drawCount"
+          :min="1"
+          :max="maxFetchCount"
+          :step="1"
+          :controls="false"
+          step-strictly
+          :disabled="!taskStatus.IS_APPLY || taskStatus.IS_EXPOSED_MODE"
+        ></el-input-number>
+        <!-- :disabled="!taskStatus.IS_APPLY || taskStatus.IS_EXPOSED_MODE" -->
+      </el-form-item>
+    </el-form>
+
+    <!-- 附件 -->
+    <h4 class="mb-2">
+      附件<span v-if="taskStatus.IS_APPLY">(最多4张)</span>:
+    </h4>
+    <div class="image-list">
+      <div
+        class="image-item"
+        v-for="(img, index) in paperConfirmAttachments"
+        :key="index"
+      >
+        <img
+          :src="img.url"
+          :alt="img.filename"
+          title="点击查看大图"
+          @click="toPreview(index)"
+        />
+        <div v-if="taskStatus.IS_APPLY" class="image-delete">
+          <i
+            class="el-icon-delete-solid"
+            @click="deletePaperConfirmAttachment(index)"
+          ></i>
+        </div>
+      </div>
+      <div
+        v-if="paperConfirmAttachments.length < 4 && taskStatus.IS_APPLY"
+        class="image-item image-add"
+        title="上传附件"
+        @click="toUploadPaperConfirm"
+      >
+        <i class="el-icon-plus"></i>
+      </div>
+    </div>
+    <div
+      v-if="!taskStatus.IS_APPLY && !paperConfirmAttachments.length"
+      class="image-list-none"
+    >
+      暂无数据
+    </div>
+
+    <!-- 附件说明 -->
+    <h4 class="mb-2">附件说明:</h4>
+    <el-input
+      v-if="taskStatus.IS_APPLY"
+      class="mb-2"
+      v-model="curTaskApply.remark"
+      type="textarea"
+      resize="none"
+      :rows="2"
+      :maxlength="100"
+      clearable
+      show-word-limit
+      placeholder="建议不超过100个字"
+    ></el-input>
+    <div class="color-gray-2" v-else>
+      <p v-if="curTaskApply.remark">{{ curTaskApply.remark }}</p>
+      <p v-else>暂无</p>
+    </div>
+
+    <!-- upload-paper-dialog -->
+    <upload-paper-dialog
+      :paper-attachment="curAttachment"
+      :upload-type="curUploadType"
+      @confirm="uploadConfirm"
+      ref="UploadPaperDialog"
+    ></upload-paper-dialog>
+    <!-- image-preview -->
+    <simple-image-preview
+      :cur-image="curImage"
+      @on-prev="toPrevImage"
+      @on-next="toNextImage"
+      ref="SimpleImagePreview"
+    ></simple-image-preview>
+    <!-- ModifyCard -->
+    <modify-card ref="ModifyCard" @modified="cardModified"></modify-card>
+
+    <!-- SelectTikuPaperDialog -->
+    <select-tiku-paper-dialog
+      ref="SelectTikuPaperDialog"
+      :row="curAttachment"
+      @confirm="tikuPaperSelected"
+    ></select-tiku-paper-dialog>
+    <!-- CardBuildDialog -->
+    <card-build-dialog
+      ref="CardBuildDialog"
+      :presetData="cardBuildPresetData"
+      @confirm="cardBuildConfirm"
+    ></card-build-dialog>
+  </div>
+</template>
+
+<script>
+import { mapStates, mapMutations } from "vuex";
+
+import { cardForSelectList } from "../../api";
+import { attachmentPreview } from "../../../login/api";
+import { copyCard } from "../../../card/api";
+
+import UploadPaperDialog from "../UploadPaperDialog.vue";
+import SelectTikuPaperDialog from "../createExamAndPrintTask/SelectTikuPaperDialog.vue";
+import CardBuildDialog from "../../../card/components/CardBuildDialog.vue";
+import SimpleImagePreview from "../../../../components/SimpleImagePreview.vue";
+import ModifyCard from "../../../card/components/ModifyCard.vue";
+
+export default {
+  name: "task-paper",
+  components: {
+    UploadPaperDialog,
+    SimpleImagePreview,
+    ModifyCard,
+    CardBuildDialog,
+    SelectTikuPaperDialog,
+  },
+  data() {
+    return {
+      tabs: [
+        {
+          name: "上传本地试卷",
+          val: "upload",
+        },
+        {
+          name: "从题库选择试卷",
+          val: "tiku",
+        },
+      ],
+      curTab: "upload",
+      uuid: "",
+      user: {},
+      paperConfirmAttachmentId: { attachmentId: "", filename: "", url: "" },
+      paperAttachments: [],
+      paperConfirmAttachments: [],
+      curAttachment: {},
+      curUploadType: "paper",
+      attachmentLimitCount: 26,
+      abc: "abcdefghijklmnopqrstuvwxyz".toUpperCase(),
+      cards: [],
+      // card-build
+      cardBuildPresetData: {},
+      // image-preview
+      curImage: {},
+      curImageIndex: 0,
+    };
+  },
+  computed: {
+    ...mapStates("exam", [
+      "editType",
+      "examTask",
+      "curTaskApply",
+      "taskStatus",
+    ]),
+    IS_TIKU_TAB() {
+      return this.curTab === "tiku";
+    },
+    maxFetchCount() {
+      return this.paperAttachments.length < 1
+        ? 1
+        : this.paperAttachments.length;
+    },
+  },
+  mounted() {
+    this.initData();
+  },
+  methods: {
+    ...mapMutations("exam", ["setEditType", "setExamTask", "setCurTaskApply"]),
+    initData() {
+      this.paperAttachments = this.curTaskApply.paperAttachmentIds
+        ? JSON.parse(this.curTaskApply.paperAttachmentIds)
+        : [];
+
+      if (!this.paperAttachments.length) {
+        this.addAtachment();
+      }
+
+      const pAttachment = this.paperAttachments.some((item) => !!item.paperId);
+      if (pAttachment) {
+        this.curTab = "tiku";
+        this.uuid = pAttachment.uuid || this.$randomCode(32);
+      } else {
+        this.curTab = "upload";
+        this.uuid = this.$randomCode(32);
+      }
+
+      const exposedPaperType = this.curTaskApply.exposedPaperType || "";
+      let exposedPaperTypes = exposedPaperType.split(",");
+      exposedPaperTypes.sort((a, b) => (a > b ? -1 : 1));
+      const maxExposedPaperType = exposedPaperTypes[0];
+      this.paperAttachments.forEach((paper) => {
+        paper.canDelete = maxExposedPaperType
+          ? paper.name > maxExposedPaperType
+          : true;
+        paper.isExposed = exposedPaperTypes.includes(paper.name);
+      });
+
+      this.paperConfirmAttachments = this.curTaskApply.paperConfirmAttachmentIds
+        ? JSON.parse(this.curTaskApply.paperConfirmAttachmentIds)
+        : [];
+
+      if (this.taskStatus.IS_APPLY) this.getCardList();
+    },
+    async selectMenu(tab) {
+      if (!this.taskStatus.IS_APPLY) return;
+
+      const attachment = this.paperAttachments[0];
+      if (attachment.cardId || attachment.attachmentId) {
+        const result = await this.$confirm(
+          "更改类型会清空已设置数据,确定要更改类型?",
+          "提示",
+          {
+            type: "warning",
+          }
+        ).catch(() => {});
+        if (result !== "confirm") return;
+
+        this.paperAttachments = [];
+        this.addAtachment();
+      }
+
+      this.curTab = tab;
+    },
+    async getCardList() {
+      if (!this.curTaskApply.courseId || !this.curTaskApply.examId) return;
+      const data = await cardForSelectList({
+        courseId: this.curTaskApply.courseId,
+        examId: this.curTaskApply.examId,
+        paperNumber: this.curTaskApply.paperNumber,
+      });
+      this.cards = data || [];
+      if (this.taskStatus.IS_REBUILD) {
+        this.cards = this.cards.filter((item) => item.type === "GENERIC");
+      }
+    },
+    addAtachment() {
+      if (this.paperAttachments.length >= this.attachmentLimitCount) return;
+
+      const name = this.abc[this.paperAttachments.length];
+      const newAttachment = {
+        name,
+        attachmentId: "",
+        filename: "",
+        paperId: null,
+        uuid: null,
+        cardId: "",
+        cardType: "",
+        createMethod: "",
+        cardTitle: "",
+        pages: 0,
+        canDelete: true,
+        isExposed: false,
+        used: false,
+        createId: null,
+      };
+      this.paperAttachments.push(newAttachment);
+    },
+    deleteAttachment(index) {
+      if (this.paperAttachments.length <= 1) {
+        this.$message.error("试卷类型数量不得少于1");
+        return;
+      }
+      this.paperAttachments.splice(index, 1);
+      this.paperAttachments.forEach((item, itemIndex) => {
+        item.name = this.abc[itemIndex];
+      });
+
+      if (
+        this.curTaskApply.drawCount &&
+        this.curTaskApply.drawCount > this.paperAttachments.length
+      ) {
+        this.curTaskApply.drawCount = this.paperAttachments.length;
+      }
+    },
+    toUpload(attachment) {
+      this.curUploadType = "paper";
+      this.curAttachment = {
+        ...attachment,
+      };
+      this.$refs.UploadPaperDialog.open();
+    },
+    toUploadPaperConfirm() {
+      if (this.paperConfirmAttachments.length >= 4) return;
+      this.curUploadType = "paperConfirm";
+      this.curAttachment = {
+        ...this.paperConfirmAttachmentId,
+      };
+      this.$refs.UploadPaperDialog.open();
+    },
+    uploadConfirm(attachment, uploadType) {
+      if (uploadType === "paper") {
+        const index = this.paperAttachments.findIndex(
+          (item) => item.name === attachment.name
+        );
+        this.paperAttachments.splice(index, 1, { ...attachment });
+      } else {
+        this.paperConfirmAttachments.push(attachment);
+      }
+    },
+    deletePaperConfirmAttachment(index) {
+      this.paperConfirmAttachments.splice(index, 1);
+    },
+    toViewCard(attachment) {
+      this.addPreviewLog(attachment, "card");
+      window.open(
+        this.getRouterPath({
+          name: "CardPreview",
+          params: {
+            cardId: attachment.cardId,
+          },
+        })
+      );
+    },
+    async toCopyCard(attachment) {
+      this.curAttachment = { ...attachment };
+      const newCardId = await copyCard(
+        attachment.cardId,
+        this.curTaskApply.courseId
+      );
+      this.cardModified({ id: newCardId });
+    },
+    toEditCard(attachment) {
+      this.curAttachment = { ...attachment };
+      this.$ls.set("prepareTcPCard", {
+        id: attachment.cardId,
+        examTaskId: this.curTaskApply.examTaskId,
+        courseId: this.curTaskApply.courseId,
+        courseName: this.curTaskApply.courseName,
+        makeMethod: this.curTaskApply.makeMethod,
+        cardRuleId: this.curTaskApply.cardRuleId,
+        type: attachment.cardType,
+        createMethod: attachment.createMethod,
+      });
+      this.$refs.ModifyCard.open();
+    },
+    async cardModified(data) {
+      // data: {id,title}
+      if (!data.id) return;
+
+      if (this.IS_TIKU_TAB) {
+        const aind = this.paperAttachments.findIndex(
+          (item) => item.name === this.curAttachment.name
+        );
+        this.paperAttachments[aind].cardTitle = data.title;
+        return;
+      }
+
+      await this.getCardList();
+      let card = this.cards.find((item) => item.id === data.id);
+
+      const aind = this.paperAttachments.findIndex(
+        (item) => item.name === this.curAttachment.name
+      );
+      if (aind !== -1 && card) {
+        this.paperAttachments[aind].cardId = card.id;
+        this.paperAttachments[aind].cardType = card.type;
+        this.paperAttachments[aind].createMethod = card.createMethod;
+        this.paperAttachments[aind].cardTitle = card.title;
+        this.paperAttachments[aind].used = card.used;
+        this.paperAttachments[aind].createId = card.createId;
+      }
+    },
+    cardChange(attachment) {
+      const card = this.cards.find((item) => item.id === attachment.cardId);
+      if (card) {
+        attachment.cardType = card.type;
+        attachment.createMethod = card.createMethod;
+        attachment.cardTitle = card.title;
+        attachment.used = card.used;
+        attachment.createId = card.createId;
+      }
+    },
+    cardOptionOpened(visible, attachment) {
+      if (!visible) return;
+
+      //   const selectedCardIds = this.paperAttachments.map((item) => item.cardId);
+      //   this.cards = this.cards.map((card) => {
+      //     card.disabled =
+      //       card.id !== attachment.cardId &&
+      //       selectedCardIds.includes(card.id) &&
+      //       card.type !== "GENERIC";
+      //     return card;
+      //   });
+    },
+    async toCreateCard(attachment) {
+      if (!this.examTask.cardRuleId) {
+        this.$message.error("题卡规则缺失!");
+        return;
+      }
+      const res = await this.$prompt("确定要提交当前题卡吗?", "提示", {
+        type: "warning",
+        showInput: true,
+        inputPlaceholder: "请输入题卡名称",
+        inputValidator: (val) => {
+          if (!val) return "请输入题卡名称!";
+          if (val.length > 50) return "题卡名称不得超过50个字符!";
+          return true;
+        },
+      }).catch(() => {});
+      if (!res || res.action !== "confirm") return;
+
+      this.curAttachment = { ...attachment };
+      // 这里只允许新建标准专卡
+      this.$ls.set("prepareTcPCard", {
+        courseId: this.examTask.courseId,
+        courseName: this.examTask.courseName,
+        schoolName: this.$ls.get("schoolName"),
+        makeMethod: "SELF",
+        cardName: res.value,
+        cardRuleId: this.examTask.cardRuleId,
+        type: "CUSTOM",
+        createMethod: "STANDARD",
+      });
+      this.$refs.ModifyCard.open();
+    },
+    async downloadPaper(attachment) {
+      if (!attachment.attachmentId) return;
+      this.addPreviewLog(attachment, "paper");
+      const data = await attachmentPreview(attachment.attachmentId);
+      window.open(data.url);
+    },
+    // select-paper
+    toSelect(attachment) {
+      this.curAttachment = {
+        ...attachment,
+        courseId: this.examTask.courseId,
+      };
+      this.$refs.SelectTikuPaperDialog.open();
+    },
+    async tikuPaperSelected(data) {
+      this.cardBuildPresetData = {
+        examId: this.examTask.examId,
+        courseId: this.examTask.courseId,
+        courseName: this.examTask.courseName,
+        schoolName: this.$ls.get("schoolName"),
+        makeMethod: "SELF",
+        cardName: "",
+        cardRuleId: this.examTask.cardRuleId,
+        type: "CUSTOM",
+        createMethod: "STANDARD",
+        paperId: data.id,
+        paperName: data.name,
+        uuid: this.uuid,
+      };
+      this.$refs.CardBuildDialog.open();
+    },
+    cardBuildConfirm(data) {
+      if (!data.success) {
+        this.$message.error(data.message);
+        return;
+      }
+      const ind = this.paperAttachments.findIndex(
+        (item) => item.name === this.curAttachment.name
+      );
+      if (ind === -1) return;
+
+      const info = data.data;
+      this.curAttachment = { ...this.paperAttachments[ind] };
+      this.paperAttachments[ind] = Object.assign(this.paperAttachments[ind], {
+        paperId: this.cardBuildPresetData.paperId,
+        cardType: this.cardBuildPresetData.type,
+        createMethod: this.cardBuildPresetData.createMethod,
+        filename: this.cardBuildPresetData.paperName,
+        cardId: info.id,
+        cardTitle: info.title,
+        uuid: info.uuid,
+        attachmentId: info.attachmentId,
+      });
+    },
+    // image-preview
+    toPreview(index) {
+      this.curImageIndex = index;
+      this.selectImage(index);
+      this.$refs.SimpleImagePreview.open();
+    },
+    selectImage(index) {
+      this.curImage = this.paperConfirmAttachments[index];
+    },
+    toPrevImage() {
+      if (this.curImageIndex === 0) {
+        this.curImageIndex = this.paperConfirmAttachments.length - 1;
+      } else {
+        this.curImageIndex--;
+      }
+
+      this.selectImage(this.curImageIndex);
+    },
+    toNextImage() {
+      if (this.curImageIndex === this.paperConfirmAttachments.length - 1) {
+        this.curImageIndex = 0;
+      } else {
+        this.curImageIndex++;
+      }
+
+      this.selectImage(this.curImageIndex);
+    },
+    // action
+    getData() {
+      let data = { ...this.curTaskApply };
+      data.paperType = this.paperAttachments.map((item) => item.name).join(",");
+      data.paperAttachmentIds = JSON.stringify(this.paperAttachments, (k, v) =>
+        k === "url" ? undefined : v
+      );
+      data.paperConfirmAttachmentIds = JSON.stringify(
+        this.paperConfirmAttachments
+      );
+      return data;
+    },
+    checkData() {
+      if (this.IS_TIKU_TAB) {
+        const attachmentValid = !this.paperAttachments.some(
+          (item) => !item.cardId
+        );
+        if (!attachmentValid) {
+          this.$message.error("请完成试卷选择!");
+          return;
+        }
+      } else {
+        // 设置了入库强制包含试卷时,校验试卷是否上传。
+        // 未设置入库强制包含试卷时,若有试卷上传,则需要上传全部。若无试卷上传,则通过。
+        if (this.curTaskApply.includePaper) {
+          const attachmentValid = !this.paperAttachments.some(
+            (item) => !item.attachmentId
+          );
+          if (!attachmentValid) {
+            this.$message.error("请完成试卷文件上传!");
+            return;
+          }
+        } else {
+          const hasUploadPaperAttachments = this.paperAttachments.filter(
+            (item) => item.attachmentId
+          );
+          if (
+            hasUploadPaperAttachments.length > 0 &&
+            hasUploadPaperAttachments.length !== this.paperAttachments.length
+          ) {
+            this.$message.error("有试卷文件未完成上传!");
+            return;
+          }
+        }
+
+        const cardValid = !this.paperAttachments.some((item) => !item.cardId);
+        if (!cardValid) {
+          this.$message.error("有试卷类型未选择题卡!");
+          return;
+        }
+      }
+
+      return true;
+    },
+  },
+};
+</script>

+ 22 - 0
src/modules/exam/store.js

@@ -14,6 +14,11 @@ const state = {
   infoExamTaskDetail: {},
   infoExamPrintPlan: {},
   infoPrintTask: {},
+  // apply-task
+  editType: "",
+  examTask: {},
+  curTaskApply: {},
+  taskStatus: {},
 };
 
 const mutations = {
@@ -44,6 +49,23 @@ const mutations = {
     state.infoExamPrintPlan = {};
     state.infoPrintTask = {};
   },
+  // apply-task
+  setTaskStatus(state, taskStatus) {
+    state.taskStatus = taskStatus;
+  },
+  setEditType(state, editType) {
+    state.editType = editType;
+  },
+  setExamTask(state, examTaskPartial) {
+    state.examTask = Object.assign({}, state.examTask, examTaskPartial);
+  },
+  setCurTaskApply(state, curTaskApplyPartial) {
+    state.curTaskApply = Object.assign(
+      {},
+      state.curTaskApply,
+      curTaskApplyPartial
+    );
+  },
 };
 
 const actions = {

+ 1 - 0
src/modules/mark/components/markParam/MarkParamGroup.vue

@@ -12,6 +12,7 @@
       </div>
       <div>
         <el-switch
+          v-if="checkPrivilege('button', 'OpenClassReading', 'MarkSetting')"
           v-model="markClassIsOpen"
           active-text="分班阅卷"
           @change="markClassChange"