|
|
@@ -1,13 +1,14 @@
|
|
|
<template>
|
|
|
- <ele-modal
|
|
|
- :visible.sync="visible"
|
|
|
- :title="dialogTitle"
|
|
|
- width="75vw"
|
|
|
- append-to-body
|
|
|
- :close-on-click-modal="false"
|
|
|
- :maxable="true"
|
|
|
- @close="cancel"
|
|
|
- >
|
|
|
+ <div>
|
|
|
+ <ele-modal
|
|
|
+ :visible.sync="visible"
|
|
|
+ :title="dialogTitle"
|
|
|
+ width="75vw"
|
|
|
+ append-to-body
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ :maxable="true"
|
|
|
+ @close="cancel"
|
|
|
+ >
|
|
|
<el-form
|
|
|
ref="form"
|
|
|
:model="form"
|
|
|
@@ -19,12 +20,12 @@
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="考试名称:" prop="name">
|
|
|
- <el-input v-model="form.name" placeholder="请输入考试名称" />
|
|
|
+ <el-input v-model="form.name" placeholder="请输入考试名称" :disabled="isView" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="考试地点:" prop="location">
|
|
|
- <el-input v-model="form.location" placeholder="请输入考试地点" />
|
|
|
+ <el-input v-model="form.location" placeholder="请输入考试地点" :disabled="isView" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
@@ -33,6 +34,8 @@
|
|
|
v-model="form.orgUnit"
|
|
|
placeholder="请选择组织单位"
|
|
|
style="width: 100%"
|
|
|
+ @changeGroup="handleOrgUnitChange"
|
|
|
+ :disabled="isView"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
@@ -47,6 +50,7 @@
|
|
|
placeholder="请选择开始时间"
|
|
|
style="width: 100%"
|
|
|
value-format="yyyy-MM-dd HH:mm:ss"
|
|
|
+ :disabled="isView"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
@@ -58,6 +62,7 @@
|
|
|
placeholder="请选择结束时间"
|
|
|
style="width: 100%"
|
|
|
value-format="yyyy-MM-dd HH:mm:ss"
|
|
|
+ :disabled="isView"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
@@ -67,6 +72,7 @@
|
|
|
v-model="form.examType"
|
|
|
placeholder="请选择考试类型"
|
|
|
style="width: 100%"
|
|
|
+ :disabled="isView"
|
|
|
>
|
|
|
<el-option
|
|
|
v-for="item in examTypeOptions"
|
|
|
@@ -82,17 +88,21 @@
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="总分数:" prop="totalScore">
|
|
|
- <el-input v-model="form.totalScore" placeholder="请输入总分数" />
|
|
|
+ <el-input
|
|
|
+ v-model="form.totalScore"
|
|
|
+ placeholder="选择试卷后自动计算"
|
|
|
+ :disabled="true"
|
|
|
+ />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="合格分数:" prop="passScore">
|
|
|
- <el-input v-model="form.passScore" placeholder="请输入合格分数" />
|
|
|
+ <el-input v-model="form.passScore" placeholder="请输入合格分数" :disabled="isView" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="考试时长:" prop="duration">
|
|
|
- <el-input v-model="form.duration" placeholder="请输入考试时长">
|
|
|
+ <el-input v-model="form.duration" placeholder="请输入考试时长" :disabled="isView">
|
|
|
<template slot="append">分钟</template>
|
|
|
</el-input>
|
|
|
</el-form-item>
|
|
|
@@ -106,17 +116,19 @@
|
|
|
v-model="form.examUnit"
|
|
|
placeholder="搜索或选择"
|
|
|
style="width: 100%"
|
|
|
+ @changeGroup="handleExamUnitChange"
|
|
|
+ :disabled="isView"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label-width="0">
|
|
|
- <el-checkbox v-model="form.needSignIn">是否需要签到</el-checkbox>
|
|
|
+ <el-checkbox v-model="form.needSignIn" :disabled="isView">是否需要签到</el-checkbox>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label-width="0">
|
|
|
- <el-checkbox v-model="form.randomSort">是否随机排序</el-checkbox>
|
|
|
+ <el-checkbox v-model="form.randomSort" :disabled="isView">是否随机排序</el-checkbox>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
@@ -125,6 +137,7 @@
|
|
|
<div class="section-title">试卷详情</div>
|
|
|
<div class="section-toolbar">
|
|
|
<el-button
|
|
|
+ v-if="isEdit"
|
|
|
type="primary"
|
|
|
size="small"
|
|
|
@click="handleSelectPaper"
|
|
|
@@ -162,22 +175,25 @@
|
|
|
<el-input v-model="scope.row.questionCount" placeholder="-" readonly />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="操作" width="80" align="center" fixed="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- <el-button
|
|
|
- type="text"
|
|
|
- class="danger-text"
|
|
|
- @click="handleDeletePaper(scope.$index)"
|
|
|
- >删除</el-button
|
|
|
- >
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
+ <template v-if="isEdit">
|
|
|
+ <el-table-column label="操作" width="80" align="center" fixed="right">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ class="danger-text"
|
|
|
+ @click="handleDeletePaper(scope.$index)"
|
|
|
+ >删除</el-button
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </template>
|
|
|
</el-table>
|
|
|
|
|
|
<!-- 考试人员 -->
|
|
|
<div class="section-title">考试人员</div>
|
|
|
<div class="section-toolbar">
|
|
|
<el-button
|
|
|
+ v-if="isEdit"
|
|
|
type="primary"
|
|
|
size="small"
|
|
|
@click="handleSelectPerson"
|
|
|
@@ -201,33 +217,106 @@
|
|
|
<el-input v-model="scope.row.deptName" placeholder="-" readonly />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="操作" width="80" align="center" fixed="right">
|
|
|
- <template slot-scope="scope">
|
|
|
- <el-button
|
|
|
- type="text"
|
|
|
- class="danger-text"
|
|
|
- @click="handleDeletePerson(scope.$index)"
|
|
|
- >删除</el-button
|
|
|
- >
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
+ <template v-if="dialogType === 'signIn'">
|
|
|
+ <el-table-column align="center" width="120">
|
|
|
+ <template slot="header" slot-scope="scope">
|
|
|
+ <el-checkbox
|
|
|
+ :value="isAllSignedIn"
|
|
|
+ :indeterminate="isIndeterminateSignedIn"
|
|
|
+ @change="handleSignInSelectAll"
|
|
|
+ >签到状态</el-checkbox
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <!-- 查看:显眼 tag -->
|
|
|
+ <el-tag
|
|
|
+ v-if="dialogType === 'view'"
|
|
|
+ :type="Number(scope.row.signIn) === 1 ? 'success' : 'danger'"
|
|
|
+ effect="dark"
|
|
|
+ >{{ Number(scope.row.signIn) === 1 ? '已签到' : '未签到' }}</el-tag
|
|
|
+ >
|
|
|
+ <!-- 发布 / 编辑:tag -->
|
|
|
+ <el-tag
|
|
|
+ v-else-if="dialogType === 'add' || dialogType === 'edit'"
|
|
|
+ :type="Number(scope.row.signIn) === 1 ? 'success' : 'info'"
|
|
|
+ >{{ Number(scope.row.signIn) === 1 ? '已签到' : '未签到' }}</el-tag
|
|
|
+ >
|
|
|
+ <!-- 监考人签到:可勾选 -->
|
|
|
+ <el-checkbox
|
|
|
+ v-else
|
|
|
+ v-model="scope.row.signIn"
|
|
|
+ :true-label="1"
|
|
|
+ :false-label="0"
|
|
|
+ @change="handlePersonSignInChange(scope.row, scope.$index)"
|
|
|
+ >签到</el-checkbox
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </template>
|
|
|
+ <template v-if="isEdit">
|
|
|
+ <el-table-column label="操作" width="80" align="center" fixed="right">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ class="danger-text"
|
|
|
+ @click="handleDeletePerson(scope.$index)"
|
|
|
+ >删除</el-button
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </template>
|
|
|
</el-table>
|
|
|
</el-form>
|
|
|
|
|
|
<template v-slot:footer>
|
|
|
- <el-button v-if="dialogType !== 'view'" type="primary" @click="save()" :loading="loading">保存</el-button>
|
|
|
- <el-button @click="cancel">{{ dialogType === 'view' ? '关闭' : '取消' }}</el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="form.needSignIn && dialogType === 'signIn'"
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ :loading="signInLoading"
|
|
|
+ @click="handleSaveSignIn"
|
|
|
+ >保存签到</el-button
|
|
|
+ >
|
|
|
+ <el-button v-if="isEdit" type="primary" @click="save()" :loading="loading">保存</el-button>
|
|
|
+ <el-button @click="cancel">{{ isReadOnly ? '关闭' : '取消' }}</el-button>
|
|
|
</template>
|
|
|
</ele-modal>
|
|
|
+
|
|
|
+ <!-- 选择试卷 -->
|
|
|
+ <select-paper-dialog
|
|
|
+ ref="selectPaperDialogRef"
|
|
|
+ @confirm="handleSelectPaperConfirm"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 选择考试人员 -->
|
|
|
+ <select-person-dialog
|
|
|
+ ref="selectPersonDialogRef"
|
|
|
+ @confirm="handleSelectPersonConfirm"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import DeptSelect from '@/components/CommomSelect/dept-select.vue';
|
|
|
+ import SelectPaperDialog from './selectPaperDialog.vue';
|
|
|
+ import SelectPersonDialog from './selectPersonDialog.vue';
|
|
|
+ import {
|
|
|
+ save as saveExam,
|
|
|
+ signIn as batchSignIn
|
|
|
+ } from '@/api/examManagement/examManagement.js';
|
|
|
+ import { getById as getPaperById } from '@/api/examPaper/examPaper.js';
|
|
|
+
|
|
|
+ // 试卷类型映射(后端数字 → 展示文本)
|
|
|
+ const PAPER_TYPE_MAP = {
|
|
|
+ 1: '定向',
|
|
|
+ 2: '随机'
|
|
|
+ };
|
|
|
|
|
|
const defForm = {
|
|
|
name: '',
|
|
|
location: '',
|
|
|
orgUnit: '',
|
|
|
+ orgUnitName: '',
|
|
|
startTime: '',
|
|
|
endTime: '',
|
|
|
examType: '',
|
|
|
@@ -235,6 +324,7 @@
|
|
|
passScore: '',
|
|
|
duration: '',
|
|
|
examUnit: '',
|
|
|
+ examUnitName: '',
|
|
|
needSignIn: false,
|
|
|
randomSort: false,
|
|
|
papers: [],
|
|
|
@@ -243,12 +333,15 @@
|
|
|
|
|
|
export default {
|
|
|
components: {
|
|
|
- DeptSelect
|
|
|
+ DeptSelect,
|
|
|
+ SelectPaperDialog,
|
|
|
+ SelectPersonDialog
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
visible: false,
|
|
|
loading: false,
|
|
|
+ signInLoading: false,
|
|
|
dialogType: 'add',
|
|
|
form: JSON.parse(JSON.stringify(defForm)),
|
|
|
examTypeOptions: [
|
|
|
@@ -276,7 +369,31 @@
|
|
|
{ required: true, message: '请输入总分数', trigger: 'blur' }
|
|
|
],
|
|
|
passScore: [
|
|
|
- { required: true, message: '请输入合格分数', trigger: 'blur' }
|
|
|
+ { required: true, message: '请输入合格分数', trigger: 'blur' },
|
|
|
+ {
|
|
|
+ validator: (rule, value, callback) => {
|
|
|
+ if (value === '' || value === null || value === undefined) {
|
|
|
+ callback();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const pass = Number(value);
|
|
|
+ const total = Number(this.form.totalScore);
|
|
|
+ if (Number.isNaN(pass)) {
|
|
|
+ callback(new Error('合格分数必须为数字'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (pass < 0) {
|
|
|
+ callback(new Error('合格分数不能小于 0'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (total && pass > total) {
|
|
|
+ callback(new Error('合格分数不能大于总分数'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ callback();
|
|
|
+ },
|
|
|
+ trigger: 'blur'
|
|
|
+ }
|
|
|
],
|
|
|
duration: [
|
|
|
{ required: true, message: '请输入考试时长', trigger: 'blur' }
|
|
|
@@ -287,22 +404,91 @@
|
|
|
}
|
|
|
};
|
|
|
},
|
|
|
+ watch: {
|
|
|
+ // 监听试卷列表:删除试卷时同步清空总分数
|
|
|
+ 'form.papers': {
|
|
|
+ handler(newVal) {
|
|
|
+ if (!newVal || newVal.length === 0) {
|
|
|
+ this.form.totalScore = '';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ deep: true
|
|
|
+ }
|
|
|
+ },
|
|
|
computed: {
|
|
|
dialogTitle() {
|
|
|
const titleMap = {
|
|
|
add: '发布考试',
|
|
|
edit: '编辑考试',
|
|
|
- view: '查看考试'
|
|
|
+ view: '查看考试',
|
|
|
+ signIn: '监考人签到'
|
|
|
};
|
|
|
return titleMap[this.dialogType] || '发布考试';
|
|
|
+ },
|
|
|
+ // 表单字段是否只读(view/signIn 都视为只读,避免改动基础信息)
|
|
|
+ isView() {
|
|
|
+ return this.dialogType === 'view' || this.dialogType === 'signIn';
|
|
|
+ },
|
|
|
+ // 是否可编辑(add/edit)
|
|
|
+ isEdit() {
|
|
|
+ return this.dialogType === 'add' || this.dialogType === 'edit';
|
|
|
+ },
|
|
|
+ // 是否完全只读(view/signIn)
|
|
|
+ isReadOnly() {
|
|
|
+ return this.dialogType === 'view' || this.dialogType === 'signIn';
|
|
|
+ },
|
|
|
+ // 是否全员已签到
|
|
|
+ isAllSignedIn() {
|
|
|
+ const persons = this.form.persons || [];
|
|
|
+ if (!persons.length) return false;
|
|
|
+ return persons.every((p) => Number(p.signIn) === 1);
|
|
|
+ },
|
|
|
+ // 是否部分签到(用于全选 checkbox 的 indeterminate 态)
|
|
|
+ isIndeterminateSignedIn() {
|
|
|
+ const persons = this.form.persons || [];
|
|
|
+ if (!persons.length) return false;
|
|
|
+ const signed = persons.filter((p) => Number(p.signIn) === 1).length;
|
|
|
+ return signed > 0 && signed < persons.length;
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
- open(row, type = 'add') {
|
|
|
+ async open(row, type = 'add') {
|
|
|
this.visible = true;
|
|
|
this.dialogType = type;
|
|
|
if (row && row.id) {
|
|
|
- this.form = JSON.parse(JSON.stringify({ ...defForm, ...row }));
|
|
|
+ this.form = this.mapDetailToForm(row);
|
|
|
+ // 编辑/查看时,若接口返回了 paperId,异步拉取试卷详情填入 papers
|
|
|
+ if (row.paperId) {
|
|
|
+ try {
|
|
|
+ const paperDetail = await getPaperById(row.paperId);
|
|
|
+ const totalScore = this.calcPaperTotalScore(paperDetail);
|
|
|
+ this.form.papers = [
|
|
|
+ {
|
|
|
+ id: paperDetail.id,
|
|
|
+ paperName: paperDetail.paperName,
|
|
|
+ paperType:
|
|
|
+ PAPER_TYPE_MAP[paperDetail.paperType] ||
|
|
|
+ String(paperDetail.paperType || ''),
|
|
|
+ departmentId: paperDetail.departmentId,
|
|
|
+ departmentName: paperDetail.departmentName,
|
|
|
+ deptName: paperDetail.departmentName,
|
|
|
+ questionCount: paperDetail.totalCount,
|
|
|
+ totalScore
|
|
|
+ }
|
|
|
+ ];
|
|
|
+ // 详情里带 totalScore 字段时,优先用它回填表单
|
|
|
+ if (
|
|
|
+ totalScore !== undefined &&
|
|
|
+ totalScore !== null &&
|
|
|
+ totalScore !== ''
|
|
|
+ ) {
|
|
|
+ this.form.totalScore = String(totalScore);
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 拉取失败时,保留接口原始 paperId 占位
|
|
|
+ this.form.papers = [{ id: row.paperId }];
|
|
|
+ }
|
|
|
+ }
|
|
|
} else {
|
|
|
this.form = JSON.parse(JSON.stringify(defForm));
|
|
|
}
|
|
|
@@ -310,6 +496,45 @@
|
|
|
this.$refs.form && this.$refs.form.clearValidate();
|
|
|
});
|
|
|
},
|
|
|
+ // 将后端详情映射为表单字段
|
|
|
+ mapDetailToForm(detail) {
|
|
|
+ const numToStr = (v) =>
|
|
|
+ v === undefined || v === null || v === '' ? '' : String(v);
|
|
|
+ return {
|
|
|
+ ...JSON.parse(JSON.stringify(defForm)),
|
|
|
+ id: detail.id,
|
|
|
+ name: detail.examName || '',
|
|
|
+ location: detail.examPlace || '',
|
|
|
+ orgUnit: numToStr(detail.organizationId),
|
|
|
+ orgUnitName: detail.organizationName || '',
|
|
|
+ startTime: detail.startTime || '',
|
|
|
+ endTime: detail.endTime || '',
|
|
|
+ examType:
|
|
|
+ detail.examType === undefined ||
|
|
|
+ detail.examType === null ||
|
|
|
+ detail.examType === ''
|
|
|
+ ? ''
|
|
|
+ : String(detail.examType),
|
|
|
+ totalScore: numToStr(detail.totalScore),
|
|
|
+ passScore: numToStr(detail.passScore),
|
|
|
+ duration: numToStr(detail.durationMinutes),
|
|
|
+ examUnit: numToStr(detail.examUnitId),
|
|
|
+ examUnitName: detail.examUnitName || '',
|
|
|
+ needSignIn: !!detail.needSignIn,
|
|
|
+ randomSort: !!detail.randomSort,
|
|
|
+ persons: (detail.personList || []).map((p) => ({
|
|
|
+ userId: p.userId,
|
|
|
+ userName: p.userName,
|
|
|
+ departmentId: p.departmentId,
|
|
|
+ departmentName: p.departmentName,
|
|
|
+ signIn: p.signIn === undefined || p.signIn === null ? 0 : Number(p.signIn),
|
|
|
+ // 兼容 publish 表单里的字段命名
|
|
|
+ id: p.userId,
|
|
|
+ deptId: p.departmentId,
|
|
|
+ deptName: p.departmentName
|
|
|
+ }))
|
|
|
+ };
|
|
|
+ },
|
|
|
cancel() {
|
|
|
this.form = JSON.parse(JSON.stringify(defForm));
|
|
|
this.visible = false;
|
|
|
@@ -317,8 +542,78 @@
|
|
|
},
|
|
|
// 选择试卷
|
|
|
handleSelectPaper() {
|
|
|
- // TODO: 选择试卷弹窗
|
|
|
- this.$message.info('选择试卷弹窗待接入');
|
|
|
+ if (!this.isEdit) return;
|
|
|
+ this.$refs.selectPaperDialogRef.open();
|
|
|
+ },
|
|
|
+ // 选择试卷确认
|
|
|
+ handleSelectPaperConfirm(paper) {
|
|
|
+ if (!paper) return;
|
|
|
+ // 接口仅支持单一 paperId,直接覆盖现有选择
|
|
|
+ this.form.papers = [paper];
|
|
|
+ // 若选中的 paper 已带 totalScore,直接同步;否则异步拉详情补算
|
|
|
+ if (
|
|
|
+ paper.totalScore !== undefined &&
|
|
|
+ paper.totalScore !== null &&
|
|
|
+ paper.totalScore !== ''
|
|
|
+ ) {
|
|
|
+ this.form.totalScore = String(paper.totalScore);
|
|
|
+ } else {
|
|
|
+ this.fetchPaperTotalScore(paper.id);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 异步拉试卷详情并计算总分
|
|
|
+ async fetchPaperTotalScore(paperId) {
|
|
|
+ if (!paperId) return;
|
|
|
+ try {
|
|
|
+ const detail = await getPaperById(paperId);
|
|
|
+ const totalScore = this.calcPaperTotalScore(detail);
|
|
|
+ if (
|
|
|
+ totalScore !== undefined &&
|
|
|
+ totalScore !== null &&
|
|
|
+ totalScore !== ''
|
|
|
+ ) {
|
|
|
+ this.form.totalScore = String(totalScore);
|
|
|
+ // 顺便回写到 papers[0],保证 watcher 与表单同步
|
|
|
+ if (this.form.papers && this.form.papers[0]) {
|
|
|
+ this.$set(this.form.papers[0], 'totalScore', totalScore);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 拉失败不阻断流程
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 计算试卷总分:优先接口字段,否则按题型 × 每题分数 推算
|
|
|
+ calcPaperTotalScore(detail) {
|
|
|
+ if (!detail) return '';
|
|
|
+ if (
|
|
|
+ detail.totalScore !== undefined &&
|
|
|
+ detail.totalScore !== null &&
|
|
|
+ detail.totalScore !== ''
|
|
|
+ ) {
|
|
|
+ return Number(detail.totalScore);
|
|
|
+ }
|
|
|
+ // 随机卷:typeCount × perScore
|
|
|
+ if (detail.paperType === 2 || detail.paperType === '2') {
|
|
|
+ const tc = detail.typeCount || {};
|
|
|
+ const ss = Number(detail.singleScore || 0);
|
|
|
+ const ms = Number(detail.multipleScore || 0);
|
|
|
+ const js = Number(detail.judgeScore || 0);
|
|
|
+ const qs = Number(detail.qaScore || 0);
|
|
|
+ return (
|
|
|
+ (Number(tc.single) || 0) * ss +
|
|
|
+ (Number(tc.multiple) || 0) * ms +
|
|
|
+ (Number(tc.judge) || 0) * js +
|
|
|
+ (Number(tc.qa) || 0) * qs
|
|
|
+ );
|
|
|
+ }
|
|
|
+ // 定向卷:questions 数组里每题 score 求和
|
|
|
+ if (Array.isArray(detail.questions) && detail.questions.length) {
|
|
|
+ return detail.questions.reduce(
|
|
|
+ (sum, q) => sum + (Number(q.score) || 0),
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return '';
|
|
|
},
|
|
|
// 删除试卷
|
|
|
handleDeletePaper(index) {
|
|
|
@@ -326,31 +621,153 @@
|
|
|
},
|
|
|
// 选择人员
|
|
|
handleSelectPerson() {
|
|
|
- // TODO: 选择人员弹窗
|
|
|
- this.$message.info('选择人员弹窗待接入');
|
|
|
+ if (!this.isEdit) return;
|
|
|
+ const excludeIds = (this.form.persons || []).map(
|
|
|
+ (p) => p.userId || p.id
|
|
|
+ );
|
|
|
+ this.$refs.selectPersonDialogRef.open(excludeIds);
|
|
|
+ },
|
|
|
+ // 选择人员确认
|
|
|
+ handleSelectPersonConfirm(persons) {
|
|
|
+ if (!Array.isArray(persons) || !persons.length) return;
|
|
|
+ // 追加到现有人员列表,并按 userId 去重
|
|
|
+ const existingIds = new Set(
|
|
|
+ (this.form.persons || [])
|
|
|
+ .map((p) => String(p.userId || p.id))
|
|
|
+ .filter((id) => id !== 'undefined' && id !== 'null' && id !== '')
|
|
|
+ );
|
|
|
+ const toAppend = persons
|
|
|
+ .filter((p) => !existingIds.has(String(p.userId || p.id)))
|
|
|
+ .map((p) => ({
|
|
|
+ ...p,
|
|
|
+ // 新选入的人员默认未签到
|
|
|
+ signIn: 0
|
|
|
+ }));
|
|
|
+ this.form.persons = [...(this.form.persons || []), ...toAppend];
|
|
|
},
|
|
|
// 删除人员
|
|
|
handleDeletePerson(index) {
|
|
|
this.form.persons.splice(index, 1);
|
|
|
},
|
|
|
+ // 考试人员勾选签到/取消签到
|
|
|
+ handlePersonSignInChange(row) {
|
|
|
+ // checkbox 双向绑定已自动更新 row.signIn,这里做强制规整
|
|
|
+ this.$set(row, 'signIn', Number(row.signIn) === 1 ? 1 : 0);
|
|
|
+ },
|
|
|
+ // 列头全选签到 / 全部取消
|
|
|
+ handleSignInSelectAll(checked) {
|
|
|
+ const value = checked ? 1 : 0;
|
|
|
+ (this.form.persons || []).forEach((p) => {
|
|
|
+ this.$set(p, 'signIn', value);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ // 批量保存签到状态
|
|
|
+ async handleSaveSignIn() {
|
|
|
+ if (!this.form.needSignIn) {
|
|
|
+ this.$message.warning('当前考试未启用签到');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.form.id) {
|
|
|
+ this.$message.warning('考试信息不完整,无法保存签到');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const personList = (this.form.persons || [])
|
|
|
+ .filter((p) => p.userId || p.id)
|
|
|
+ .map((p) => ({
|
|
|
+ userId: String(p.userId || p.id),
|
|
|
+ signIn: Number(p.signIn) === 1 ? 1 : 0
|
|
|
+ }));
|
|
|
+ if (!personList.length) {
|
|
|
+ this.$message.warning('暂无考试人员');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.signInLoading = true;
|
|
|
+ try {
|
|
|
+ const message = await batchSignIn({
|
|
|
+ examId: String(this.form.id),
|
|
|
+ personList
|
|
|
+ });
|
|
|
+ this.$message.success('签到状态已保存');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存签到状态失败:', error);
|
|
|
+ this.$message.error('保存签到状态失败');
|
|
|
+ } finally {
|
|
|
+ this.signInLoading = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 组织单位变更,捕获名称
|
|
|
+ handleOrgUnitChange(id, data) {
|
|
|
+ if (id && data) {
|
|
|
+ this.form.orgUnitName = data.name;
|
|
|
+ } else {
|
|
|
+ this.form.orgUnitName = '';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 考试单位变更,捕获名称
|
|
|
+ handleExamUnitChange(id, data) {
|
|
|
+ if (id && data) {
|
|
|
+ this.form.examUnitName = data.name;
|
|
|
+ } else {
|
|
|
+ this.form.examUnitName = '';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 构造提交参数
|
|
|
+ buildSavePayload() {
|
|
|
+ const f = this.form;
|
|
|
+ // 数值型字段转换
|
|
|
+ const toNumber = (v) => String(v);
|
|
|
+ // 选中的试卷(接口要求单一 paperId)
|
|
|
+ const firstPaper = (f.papers && f.papers[0]) || {};
|
|
|
+ return {
|
|
|
+ examName: f.name,
|
|
|
+ examPlace: f.location,
|
|
|
+ organizationId: toNumber(f.orgUnit),
|
|
|
+ organizationName: f.orgUnitName,
|
|
|
+ startTime: f.startTime,
|
|
|
+ endTime: f.endTime,
|
|
|
+ examType: f.examType,
|
|
|
+ totalScore: toNumber(f.totalScore),
|
|
|
+ passScore: toNumber(f.passScore),
|
|
|
+ durationMinutes: toNumber(f.duration),
|
|
|
+ examUnitId: toNumber(f.examUnit),
|
|
|
+ examUnitName: f.examUnitName,
|
|
|
+ needSignIn: !!f.needSignIn,
|
|
|
+ randomSort: !!f.randomSort,
|
|
|
+ paperId: toNumber(firstPaper.id),
|
|
|
+ personList: (f.persons || []).map((p) => ({
|
|
|
+ userId: toNumber(p.userId || p.id),
|
|
|
+ userName: p.userName,
|
|
|
+ departmentId: toNumber(p.deptId || p.departmentId),
|
|
|
+ departmentName: p.deptName || p.departmentName
|
|
|
+ }))
|
|
|
+ };
|
|
|
+ },
|
|
|
save() {
|
|
|
- if (this.dialogType === 'view') {
|
|
|
+ if (this.isReadOnly) {
|
|
|
this.cancel();
|
|
|
return;
|
|
|
}
|
|
|
- this.$refs.form.validate((valid) => {
|
|
|
- if (valid) {
|
|
|
- this.loading = true;
|
|
|
- setTimeout(() => {
|
|
|
- this.loading = false;
|
|
|
- const messageMap = {
|
|
|
- add: '发布成功',
|
|
|
- edit: '编辑成功'
|
|
|
- };
|
|
|
- this.$message.success(messageMap[this.dialogType] || '保存成功');
|
|
|
- this.cancel();
|
|
|
- this.$emit('reload');
|
|
|
- }, 500);
|
|
|
+ this.$refs.form.validate(async (valid) => {
|
|
|
+ if (!valid) return;
|
|
|
+ const payload = this.buildSavePayload();
|
|
|
+ // 必填校验补充
|
|
|
+ if (!payload.examName || !payload.startTime || !payload.endTime ||
|
|
|
+ payload.totalScore === null || payload.passScore === null ||
|
|
|
+ payload.durationMinutes === null || payload.paperId === null) {
|
|
|
+ this.$message.warning('请完善必填项');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.loading = true;
|
|
|
+ try {
|
|
|
+ const message = await saveExam(payload);
|
|
|
+ this.$message.success('发布成功');
|
|
|
+ this.cancel();
|
|
|
+ this.$emit('reload');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发布考试:', error);
|
|
|
+ this.$message.error('发布失败');
|
|
|
+ } finally {
|
|
|
+ this.loading = false;
|
|
|
}
|
|
|
});
|
|
|
}
|