123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711 |
- <template>
- <div class="mark-param-structure">
- <div class="part-box part-box-pad box-justify">
- <el-breadcrumb separator="|">
- <el-breadcrumb-item
- >本试卷大题:{{ paperStat.mainQuestionCount }}道</el-breadcrumb-item
- >
- <el-breadcrumb-item
- >小题:{{ paperStat.questionCount }}道</el-breadcrumb-item
- >
- <el-breadcrumb-item
- >客观题:{{ paperStat.objectiveQuestionCount }}道</el-breadcrumb-item
- >
- <el-breadcrumb-item
- >主观题:{{ paperStat.subjectiveQuestionCount }}道</el-breadcrumb-item
- >
- <el-breadcrumb-item
- >总分:<span class="color-danger mlr-1">{{
- paperStat.paperTotalScore
- }}</span>
- 分
- </el-breadcrumb-item>
- </el-breadcrumb>
- <div>
- <el-button type="primary" @click="toNext(1)">下一步</el-button>
- </div>
- </div>
- <div class="part-box part-box-pad structure-body">
- <div class="box-justify mb-2">
- <div>
- <el-button v-if="editOpen" type="primary" @click="toAddMain"
- >新增大题</el-button
- >
- </div>
- <div v-if="checkPrivilege('link', 'EditPaperStruct', 'MarkSetting')">
- <el-switch v-model="editOpen" active-text="开启编辑"></el-switch>
- </div>
- </div>
- <el-table
- ref="TableList"
- :data="tableData"
- border
- :row-class-name="getRowClassName"
- :key="tableKey"
- :height="tableHeight"
- >
- <el-table-column width="50" align="center">
- <template slot-scope="scope" v-if="scope.row.mainFirstSub">
- <div
- :class="[
- 'expand-btn',
- { 'expand-btn-unexpand': !scope.row.expandSub },
- ]"
- @click="switchExpandSub(scope.row)"
- >
- <i
- :class="scope.row.expandSub ? 'el-icon-minus' : 'el-icon-plus'"
- ></i>
- </div>
- </template>
- </el-table-column>
- <template v-if="editOpen">
- <el-table-column prop="mainTitle" label="大题名称">
- <span slot-scope="scope" v-if="scope.row.mainFirstSub">
- <el-input
- v-model.trim="scope.row.mainTitle"
- size="small"
- :maxlength="32"
- clearable
- @change="mainTitleChange(scope.row)"
- ></el-input>
- </span>
- </el-table-column>
- <el-table-column prop="questionType" label="题型" width="120">
- <template slot-scope="scope" v-if="scope.row.mainFirstSub">
- <el-select
- v-model="scope.row.questionType"
- placeholder="请选择"
- class="width-full"
- :disabled="checkMainQuestionHasMarker(scope.row.mainId)"
- @visible-change="(val) => qTypeVisibleChange(val, scope.row)"
- @change="qTypeChange(scope.row)"
- >
- <el-option
- v-for="item in QUESTION_TYPE_LIST"
- :key="item.code"
- :value="item.code"
- :label="item.name"
- >
- </el-option>
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="选项个数" width="100">
- <template
- slot-scope="scope"
- v-if="scope.row.questionType <= 2 && scope.row.mainFirstSub"
- >
- <el-input-number
- v-model="scope.row.optionCount"
- class="width-full"
- size="small"
- :min="2"
- :max="26"
- :step="1"
- step-strictly
- :controls="false"
- @change="optionCountChange(scope.row)"
- ></el-input-number>
- </template>
- </el-table-column>
- </template>
- <template v-else>
- <el-table-column prop="mainTitle" label="大题名称">
- <span slot-scope="scope" v-if="scope.row.mainFirstSub">
- {{ scope.row.mainTitle }}
- </span>
- </el-table-column>
- <el-table-column label="题型" width="120">
- <template slot-scope="scope" v-if="scope.row.mainFirstSub">
- {{ questionTypeDict[scope.row.questionType] }}
- </template>
- </el-table-column>
- </template>
- <el-table-column prop="mainNumber" label="大题号" width="80">
- <template slot-scope="scope" v-if="scope.row.mainFirstSub">
- <span>{{ scope.row.mainNumber }}</span>
- </template>
- </el-table-column>
- <el-table-column
- prop="subNumber"
- label="小题号"
- width="80"
- ></el-table-column>
- <el-table-column label="每题分值" width="120">
- <template slot="header">
- <span>每题分值</span>
- <el-tooltip effect="dark" placement="top">
- <div slot="content" class="tooltip-area">
- <p>每题分值,属于批量设置</p>
- <p>
- 输入每题分值后,比如2按回车键或鼠标点击输入框外其他地方,系统会自动设置该大题下每个小题的分值为2。
- </p>
- </div>
- <i class="el-icon-info ml-1 tooltip-info-icon"></i>
- </el-tooltip>
- </template>
- <template slot-scope="scope" v-if="scope.row.mainFirstSub">
- <el-input-number
- v-model="scoresPerTopic[scope.row.mainId]"
- class="width-full"
- size="small"
- :min="0"
- :max="500"
- :step="0.1"
- step-strictly
- :controls="false"
- placeholder="每题分值"
- @change="(val) => scorePerTopicChange(val, scope.row)"
- ></el-input-number>
- </template>
- </el-table-column>
- <el-table-column prop="totalScore" label="小题满分" width="120">
- <template slot-scope="scope">
- <el-input-number
- v-model="scope.row.totalScore"
- class="width-full"
- size="small"
- :min="0"
- :max="500"
- :step="0.1"
- step-strictly
- :controls="false"
- placeholder="小题分值"
- @change="totalScoreChange(scope.row)"
- ></el-input-number>
- </template>
- </el-table-column>
- <el-table-column label="每题最小分" width="120">
- <template
- slot-scope="scope"
- v-if="scope.row.mainFirstSub && !scope.row.objective"
- >
- <el-input-number
- v-model="intervalScorePerTopic[scope.row.mainId]"
- class="width-full"
- size="small"
- :min="0.1"
- :max="500"
- :step="0.1"
- step-strictly
- :controls="false"
- placeholder="每题最小分"
- @change="(val) => intervalScorePerTopicChange(val, scope.row)"
- ></el-input-number>
- </template>
- </el-table-column>
- <el-table-column prop="intervalScore" label="最小分" width="120">
- <template slot="header">
- <span>最小分</span>
- <el-tooltip effect="dark" placement="top">
- <div slot="content" class="tooltip-area">
- <p>最小分是评卷中每题最小给分间隔,详见样例。</p>
- <p>
- 如小题总分为5分的题目,最小分设置为1,则可以给0,1,2,3,4,5分。
- </p>
- <p>
- 如最小分设置为0.5分,则可以给0.5,1,1.5,2,2.5,3,3.5,4,4.5,5分。
- </p>
- <p>如果最小分设置为5分,则只能给0分和5分。</p>
- <p>每题最小分是只用设置1次,系统会自动设置每个小题的最小分</p>
- </div>
- <i class="el-icon-info ml-1 tooltip-info-icon"></i>
- </el-tooltip>
- </template>
- <template slot-scope="scope" v-if="!scope.row.objective">
- <el-input-number
- v-model="scope.row.intervalScore"
- class="width-full"
- size="small"
- :min="0.1"
- :max="scope.row.totalScore"
- :step="0.1"
- step-strictly
- :controls="false"
- placeholder="最小分"
- ></el-input-number>
- </template>
- </el-table-column>
- <el-table-column
- v-if="editOpen"
- class-name="action-column"
- label="操作"
- width="140px"
- >
- <template slot-scope="scope">
- <el-button
- class="btn-primary"
- type="text"
- @click="toAddSub(scope.row)"
- >新增小题</el-button
- >
- <el-button
- :disabled="
- tableData.length <= 1 || checkSubQuestionHasMarker(scope.row)
- "
- class="btn-danger"
- type="text"
- @click="toDeleteSub(scope.row)"
- >删除</el-button
- >
- </template>
- </el-table-column>
- </el-table>
- <!-- tips -->
- <div class="mt-2">
- <p class="tips-info">
- 1.请确认展示的试卷结构与提交的试卷、答题卡是否一致?
- </p>
- <p class="tips-info">
- 2.请补充所有题目的小题分值,并确认试卷总分。主观题设置间隔分,间隔分的具体说明见列表帮助提示。
- </p>
- <p class="tips-info tips-error">
- 3.开始阅卷后不允许修改试卷结构,请确认清楚后再提交!
- </p>
- </div>
- </div>
- </div>
- </template>
- <script>
- import { calcSum, maxNum, toPrecision } from "@/plugins/utils";
- import { QUESTION_TYPE_LIST } from "@/constants/enumerate";
- import { mapState, mapMutations } from "vuex";
- import { markStructureSave } from "../../api";
- import { omit } from "lodash";
- import { MD5 } from "@/plugins/md5";
- export default {
- name: "mark-paper-structure",
- data() {
- return {
- tableData: [],
- curRow: {},
- cacheDataMd5: "",
- QUESTION_TYPE_LIST,
- questionTypeDict: {},
- scoresPerTopic: {},
- intervalScorePerTopic: {},
- loading: false,
- editOpen: false,
- tableKey: "",
- hasMarkerQuestions: [],
- tableHeight: 200,
- };
- },
- computed: {
- ...mapState("markParam", [
- "basicInfo",
- "paperStructureInfo",
- "subjectiveTaskList",
- ]),
- paperStat() {
- const questionCount = this.tableData.length;
- const mainQuestionCount = this.tableData.filter(
- (item) => item.mainFirstSub
- ).length;
- const subjectiveQuestionCount = this.tableData.filter(
- (item) => !item.objective
- ).length;
- const paperTotalScore = toPrecision(
- calcSum(this.tableData.map((item) => item.totalScore || 0)),
- 1
- );
- return {
- questionCount,
- mainQuestionCount,
- paperTotalScore,
- subjectiveQuestionCount,
- objectiveQuestionCount: questionCount - subjectiveQuestionCount,
- };
- },
- },
- watch: {
- editOpen() {
- this.tableKey = this.$randomCode();
- this.updateTableHeight();
- },
- },
- mounted() {
- this.initData();
- this.registResize();
- },
- methods: {
- ...mapMutations("markParam", ["setPaperStructureInfo"]),
- initData() {
- this.tableKey = this.$randomCode();
- this.hasMarkerQuestions = this.subjectiveTaskList
- .filter((item) => item.markers.length)
- .map((item) => `${item.mainNumber}-${item.subNumber}`);
- let questionTypeDict = {};
- QUESTION_TYPE_LIST.forEach((item) => {
- questionTypeDict[item.code] = item.name;
- });
- this.questionTypeDict = questionTypeDict;
- let curMainNumber = null;
- let curMainId = null;
- let scoresPerTopic = {},
- intervalScorePerTopic = {};
- this.tableData = this.paperStructureInfo.map((item) => {
- let nitem = {
- ...item,
- key: this.$randomCode(),
- mainFirstSub: false,
- expandSub: true,
- };
- if (curMainNumber !== item.mainNumber) {
- curMainNumber = item.mainNumber;
- curMainId = this.$randomCode();
- scoresPerTopic[curMainNumber] = undefined;
- intervalScorePerTopic[curMainNumber] = undefined;
- nitem.mainFirstSub = true;
- }
- nitem.totalScore =
- nitem.totalScore || nitem.totalScore === 0
- ? nitem.totalScore
- : undefined;
- nitem.intervalScore = nitem.intervalScore || undefined;
- nitem.mainId = curMainId;
- return nitem;
- });
- this.scoresPerTopic = scoresPerTopic;
- this.intervalScorePerTopic = intervalScorePerTopic;
- if (!this.tableData.length && this.editOpen) {
- this.createMain();
- }
- this.cacheDataMd5 = this.getSubmitDataMd5();
- this.updateTableHeight();
- },
- getSubmitDataMd5() {
- return MD5(JSON.stringify(this.getData()));
- },
- getNewRow(val) {
- return this.$objAssign(
- {
- id: null,
- key: this.$randomCode(),
- mainId: this.$randomCode(),
- objective: true,
- mainNumber: 1,
- subNumber: 1,
- mainTitle: "",
- answer: "",
- totalScore: undefined,
- intervalScore: undefined,
- objectivePolicy: null,
- questionType: null,
- optionCount: undefined,
- mainFirstSub: true,
- expandSub: true,
- },
- val
- );
- },
- createMain() {
- this.tableData.push(this.getNewRow({}));
- },
- getRowClassName({ row }) {
- let classNames = [];
- if (row.mainFirstSub) {
- classNames.push("row-main-first-sub");
- }
- if (!row.mainFirstSub && !row.expandSub) {
- classNames.push("row-unexpand-sub");
- }
- return classNames.join(" ");
- },
- getNextMainStartPos(startPos, curMainId) {
- let nextMainStartPos = null;
- for (let i = startPos, len = this.tableData.length; i < len; i++) {
- const element = this.tableData[i];
- if (element.mainId !== curMainId) {
- nextMainStartPos = i;
- return nextMainStartPos;
- }
- }
- if (nextMainStartPos === null) return this.tableData.length;
- },
- getNewMainNumber() {
- return maxNum(this.tableData.map((item) => item.mainNumber)) + 1;
- },
- toAddMain() {
- const newMainData = this.getNewRow({
- mainId: this.$randomCode(),
- mainNumber: this.getNewMainNumber(),
- totalScore: undefined,
- questionType: null,
- objective: true,
- });
- this.tableData.push(newMainData);
- this.$set(this.intervalScorePerTopic, newMainData.mainId, undefined);
- },
- updateMainData() {
- let curMainNumber = 0,
- curMainId = null;
- this.tableData.forEach((item) => {
- if (item.mainId !== curMainId) {
- curMainId = item.mainId;
- curMainNumber++;
- }
- item.mainNumber = curMainNumber;
- });
- },
- toAddSub(row) {
- const subPos = this.tableData.findIndex((item) => item.key === row.key);
- this.tableData.splice(
- subPos + 1,
- 0,
- this.getNewRow({
- ...row,
- mainFirstSub: false,
- answer: "",
- key: this.$randomCode(),
- })
- );
- this.updateSubData(row.mainId);
- },
- updateSubData(mainId) {
- this.tableData
- .filter((item) => item.mainId === mainId)
- .forEach((item, index) => {
- item.subNumber = index + 1;
- });
- },
- async toDeleteSub(row) {
- if (!row.objective && this.checkSubQuestionHasMarker(row)) {
- this.$message.error("当前小题已设置评卷员,不可删除!");
- return;
- }
- const confirm = await this.$confirm(`确定要删除小题吗?`, "提示", {
- type: "warning",
- }).catch(() => {});
- if (confirm !== "confirm") return;
- const subPos = this.tableData.findIndex((item) => item.key === row.key);
- this.tableData.splice(subPos, 1);
- this.tableData
- .filter((item) => item.mainId === row.mainId)
- .forEach((item, index) => {
- item.mainFirstSub = !index;
- });
- this.updateSubData(row.mainId);
- },
- switchExpandSub(row) {
- row.expandSub = !row.expandSub;
- this.tableData
- .filter((item) => item.mainId === row.mainId && !item.mainFirstSub)
- .forEach((item) => (item.expandSub = row.expandSub));
- },
- mainTitleChange(row) {
- this.tableData
- .filter((item) => item.mainId === row.mainId && !item.mainFirstSub)
- .forEach((item) => (item.mainTitle = row.mainTitle));
- },
- qTypeVisibleChange(val, row) {
- if (!val) return;
- this.curRow = { ...row };
- },
- checkMainQuestionHasMarker(mainId) {
- return this.tableData
- .filter((item) => item.mainId === mainId)
- .some((item) => this.checkSubQuestionHasMarker(item));
- },
- checkSubQuestionHasMarker(item) {
- return this.hasMarkerQuestions.includes(
- `${item.mainNumber}-${item.subNumber}`
- );
- },
- async qTypeChange(row) {
- if (row.objective) {
- const confirm = await this.$confirm(`确定要更改题型吗?`, "提示", {
- type: "warning",
- }).catch(() => {});
- if (confirm !== "confirm") {
- row.questionType = this.curRow.questionType;
- return;
- }
- } else {
- if (this.checkMainQuestionHasMarker(row.mainId)) {
- row.questionType = this.curRow.questionType;
- this.$message.error("当前大题已设置评卷员,不可更改");
- return;
- }
- }
- const curQt = this.QUESTION_TYPE_LIST.find(
- (item) => item.code === row.questionType
- );
- if (!curQt) return;
- this.tableData
- .filter((item) => item.mainId === row.mainId)
- .forEach((item) => {
- item.questionType = curQt.code;
- item.objective = curQt.qType === "objective";
- item.optionCount = curQt.optionCount;
- if (item.objective) {
- item.intervalScore = undefined;
- this.intervalScorePerTopic[row.mainId] = undefined;
- }
- });
- },
- optionCountChange(row) {
- if (!row.optionCount) return;
- this.tableData
- .filter((item) => item.mainId === row.mainId && !item.mainFirstSub)
- .forEach((item) => {
- item.optionCount = row.optionCount;
- });
- },
- scorePerTopicChange(val, row) {
- if (!val && val !== 0) return;
- this.tableData
- .filter((item) => item.mainId === row.mainId)
- .forEach((item) => {
- item.totalScore = val;
- item.intervalScore = Math.min(item.totalScore, item.intervalScore);
- });
- },
- intervalScorePerTopicChange(val, row) {
- if (!val) return;
- this.tableData
- .filter((item) => item.mainId === row.mainId)
- .forEach((item) => {
- item.intervalScore = Math.min(item.totalScore, val);
- });
- },
- totalScoreChange(row) {
- const isInit = (num) => !(num % 1);
- if (!row.intervalScore) {
- row.intervalScore = isInit(row.totalScore) ? 1 : 0.5;
- return;
- }
- if (
- (isInit(row.totalScore) && !isInit(row.intervalScore)) ||
- (!isInit(row.totalScore) && isInit(row.intervalScore))
- ) {
- row.intervalScore = isInit(row.totalScore) ? 1 : 0.5;
- return;
- }
- row.intervalScore = Math.min(row.totalScore, row.intervalScore);
- },
- checkData() {
- let errorMessages = [];
- this.tableData.forEach((item) => {
- let errorMsg = ``;
- if (item.mainFirstSub) {
- let errorFields = [];
- if (!item.mainTitle) {
- errorFields.push("大题名称");
- }
- if (!item.questionType) {
- errorFields.push("题型");
- }
- if (!item.mainNumber) {
- errorFields.push("大题号");
- }
- if (!item.optionCount && item.questionType <= 2) {
- errorFields.push("选项个数");
- }
- if (errorFields.length) {
- errorMsg += `${errorFields.join("、")}不能为空,`;
- }
- }
- let errorFields = [];
- if (!item.subNumber) {
- errorFields.push("小题号");
- }
- if (!item.totalScore && item.totalScore !== 0) {
- errorFields.push("小题满分");
- }
- if (!item.intervalScore && !item.objective) {
- errorFields.push("评卷最小分");
- }
- if (errorFields.length) {
- errorMsg += `第${item.subNumber}小题,${errorFields.join(
- "、"
- )}不能为空,`;
- }
- if (errorMsg) {
- errorMsg = `第${item.mainNumber}大题,${errorMsg}`;
- errorMessages.push(errorMsg);
- }
- });
- if (errorMessages.length) {
- this.$message.error(errorMessages.join("。"));
- return;
- }
- return true;
- },
- getData() {
- return this.tableData.map((item) => {
- return omit(item, ["key", "mainId", "expandSub"]);
- });
- },
- async submit() {
- if (this.loading) return;
- if (!this.checkData()) return;
- this.loading = true;
- const questions = this.getData();
- const res = await markStructureSave({
- examId: this.basicInfo.examId,
- paperNumber: this.basicInfo.paperNumber,
- questions,
- }).catch(() => {});
- this.loading = false;
- if (!res) return;
- this.$message.success("保存成功!");
- this.setPaperStructureInfo(questions);
- return true;
- },
- async toNext(step = 1) {
- if (!this.checkData()) return;
- // 如果试卷结构有变动,先保存
- if (this.cacheDataMd5 !== this.getSubmitDataMd5()) {
- const res = await this.submit();
- if (!res) return;
- }
- this.$emit("next", step);
- },
- // table height
- updateTableHeight() {
- this.$nextTick(() => {
- if (!this.$refs.TableList) return;
- const tableOffsetTop = this.$refs.TableList.$el.offsetTop;
- this.tableHeight = window.innerHeight - tableOffsetTop - 115;
- // console.log(this.tableHeight);
- });
- },
- registResize() {
- window.addEventListener("resize", this.updateTableHeight);
- },
- },
- beforeDestroy() {
- window.removeEventListener("resize", this.updateTableHeight);
- },
- };
- </script>
|