新建考试选题改为按校题库(scope=school);步骤条与考试列表起止时间样式优化

fix/exam-question-access-material-excel-delete-draft
陈沅 2 weeks ago
parent f8174e5a0c
commit f120701ebe

@ -51,9 +51,9 @@
</div> </div>
<div v-if="!inProgress" class="fixed-step-actions" aria-label="考试编辑步骤"> <div v-if="!inProgress" class="fixed-step-actions" aria-label="考试编辑步骤">
<button type="button" class="fixed-step-button" :class="{ 'is-active': activeStep === 1 }" @click="activeStep = 1">添加试题</button> <button type="button" class="fixed-step-button" :class="{ 'is-active': activeStep === 1 }" @click="activeStep = 1">添加试题</button>
<span class="fixed-step-arrow">&gt;&gt;&gt;</span> <span class="fixed-step-arrow"><el-icon><ArrowRight /></el-icon></span>
<button type="button" class="fixed-step-button" :class="{ 'is-active': activeStep === 2 }" @click="activeStep = 2">试卷概览</button> <button type="button" class="fixed-step-button" :class="{ 'is-active': activeStep === 2 }" @click="activeStep = 2">试卷概览</button>
<span class="fixed-step-arrow">&gt;&gt;&gt;</span> <span class="fixed-step-arrow"><el-icon><ArrowRight /></el-icon></span>
<button type="button" class="fixed-step-button" :class="{ 'is-active': activeStep === 0 }" @click="activeStep = 0">考试设置</button> <button type="button" class="fixed-step-button" :class="{ 'is-active': activeStep === 0 }" @click="activeStep = 0">考试设置</button>
</div> </div>
</div> </div>
@ -233,7 +233,7 @@ const totalScore = computed(() => form.questions.reduce((sum, item) => sum + Num
const selectedTypes = computed(() => typeOptions.filter((item) => selectedByType(item.value))); const selectedTypes = computed(() => typeOptions.filter((item) => selectedByType(item.value)));
onMounted(load); onMounted(load);
async function load() { loading.value = true; try { const [classRes, questionRes] = await Promise.all([examApi.getExamClasses(), examApi.getQuestions({})]); classes.value = classRes.data || []; bank.value = questionRes.data || []; if (!isNew.value) { const { data } = await examApi.getTeacherExam(id.value); status.value = data.status; const bankIds = new Set(bank.value.map((item) => item.id)); bank.value = [...bank.value, ...data.questions.filter((item) => !bankIds.has(item.id))]; Object.assign(form, { name: data.name, startTime: formatValue(data.startTime), endTime: formatValue(data.endTime), classIds: data.classes.map((item) => item.id), questionOrderMode: data.questionOrderMode || "FIXED", optionOrderMode: data.optionOrderMode || "FIXED", allowCopyQuestion: toBoolean(data.allowCopyQuestion, false), allowPasteAnswer: toBoolean(data.allowPasteAnswer, false), allowRollback: toBoolean(data.allowRollback, true), multipleScoreMode: data.multipleScoreMode || "ALL_OR_NOTHING", questions: data.questions.map((item) => ({ questionId: item.id, score: Number(item.score) })) }); for (const type of typeOptions.map((item) => item.value)) { const first = data.questions.find((item) => item.type === type); if (first) typeScores[type] = Number(first.score) || 1; } duration.value = data.durationMinutes; } else { const start = new Date(Date.now() + 3600000); form.startTime = formatValue(start); form.endTime = formatValue(new Date(start.getTime() + 3600000)); } } finally { loading.value = false; } } async function load() { loading.value = true; try { const [classRes, questionRes] = await Promise.all([examApi.getExamClasses(), examApi.getQuestions({ scope: "school" })]); classes.value = classRes.data || []; bank.value = questionRes.data || []; if (!isNew.value) { const { data } = await examApi.getTeacherExam(id.value); status.value = data.status; const bankIds = new Set(bank.value.map((item) => item.id)); bank.value = [...bank.value, ...data.questions.filter((item) => !bankIds.has(item.id))]; Object.assign(form, { name: data.name, startTime: formatValue(data.startTime), endTime: formatValue(data.endTime), classIds: data.classes.map((item) => item.id), questionOrderMode: data.questionOrderMode || "FIXED", optionOrderMode: data.optionOrderMode || "FIXED", allowCopyQuestion: toBoolean(data.allowCopyQuestion, false), allowPasteAnswer: toBoolean(data.allowPasteAnswer, false), allowRollback: toBoolean(data.allowRollback, true), multipleScoreMode: data.multipleScoreMode || "ALL_OR_NOTHING", questions: data.questions.map((item) => ({ questionId: item.id, score: Number(item.score) })) }); for (const type of typeOptions.map((item) => item.value)) { const first = data.questions.find((item) => item.type === type); if (first) typeScores[type] = Number(first.score) || 1; } duration.value = data.durationMinutes; } else { const start = new Date(Date.now() + 3600000); form.startTime = formatValue(start); form.endTime = formatValue(new Date(start.getTime() + 3600000)); } } finally { loading.value = false; } }
function syncEndTime() { if (!form.startTime) return; form.endTime = formatValue(new Date(new Date(form.startTime.replace(" ", "T")).getTime() + duration.value * 60000)); } function syncEndTime() { if (!form.startTime) return; form.endTime = formatValue(new Date(new Date(form.startTime.replace(" ", "T")).getTime() + duration.value * 60000)); }
function syncDurationFromEnd() { if (!form.startTime || !form.endTime) return; const start = new Date(form.startTime.replace(" ", "T")); const end = new Date(form.endTime.replace(" ", "T")); if (end > start) duration.value = Math.round((end - start) / 60000); } function syncDurationFromEnd() { if (!form.startTime || !form.endTime) return; const start = new Date(form.startTime.replace(" ", "T")); const end = new Date(form.endTime.replace(" ", "T")); if (end > start) duration.value = Math.round((end - start) / 60000); }
function isSelected(questionId) { return form.questions.some((item) => item.questionId === questionId); } function isSelected(questionId) { return form.questions.some((item) => item.questionId === questionId); }
@ -403,38 +403,48 @@ function toBoolean(value, fallback) { if (value === undefined || value === null
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: var(--sp-4); gap: var(--sp-3);
margin-top: var(--sp-5); margin-top: var(--sp-5);
} }
.fixed-step-button { .fixed-step-button {
min-width: 164px; min-width: 148px;
height: 40px; height: 38px;
padding: 0 var(--sp-5); padding: 0 var(--sp-4);
border: 0; border: 1px solid var(--border-color);
border-radius: var(--r-control); border-radius: var(--r-control);
background: var(--edu-primary); background: #ffffff;
color: #ffffff; color: var(--text-body);
font: inherit;
font-size: var(--fs-body); font-size: var(--fs-body);
font-weight: 600; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: background 0.16s ease, transform 0.16s ease, box-shadow 0.16s ease; transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease;
&:hover {
border-color: var(--edu-primary);
color: var(--edu-primary);
}
&:hover,
&.is-active { &.is-active {
background: var(--edu-primary-deep); border-color: var(--edu-primary);
background: var(--edu-primary);
color: #ffffff;
box-shadow: 0 4px 10px rgba(25, 137, 204, 0.18); box-shadow: 0 4px 10px rgba(25, 137, 204, 0.18);
transform: translateY(-1px);
&:hover { color: #ffffff; }
} }
&:focus-visible { outline: 2px solid var(--edu-primary); outline-offset: 2px; } &:focus-visible { outline: 2px solid var(--edu-primary); outline-offset: 2px; }
} }
.fixed-step-arrow { .fixed-step-arrow {
color: var(--text-strong); display: inline-flex;
font-size: 20px; align-items: center;
font-weight: 700; color: var(--text-muted);
letter-spacing: -3px; font-size: 16px;
.el-icon { font-size: 16px; }
} }
.form-row { .form-row {

@ -10,7 +10,7 @@
<section v-loading="loading" class="assessment-list"> <section v-loading="loading" class="assessment-list">
<article v-for="row in exams" :key="row.id" class="assessment-card"> <article v-for="row in exams" :key="row.id" class="assessment-card">
<div class="assessment-cover"><el-icon><PictureFilled /></el-icon></div> <div class="assessment-cover"><el-icon><PictureFilled /></el-icon></div>
<div class="assessment-info"><h3>{{ row.name }}</h3><p>起止时间:{{ formatTime(row.startTime) }} - {{ formatTime(row.endTime) }}</p><p>考试时长:{{ row.durationMinutes || durationBetween(row) }}分钟</p></div> <div class="assessment-info"><h3>{{ row.name }}</h3><p class="time-line"><el-icon><Clock /></el-icon><span class="time-label">起止时间</span><span class="time-value">{{ formatTime(row.startTime) }} ~ {{ formatTime(row.endTime) }}</span></p><p>考试时长:{{ row.durationMinutes || durationBetween(row) }}分钟</p></div>
<div class="assessment-status"><el-tag :type="statusMeta[row.status]?.tone || 'info'" effect="light">{{ statusMeta[row.status]?.label || row.status }}</el-tag></div> <div class="assessment-status"><el-tag :type="statusMeta[row.status]?.tone || 'info'" effect="light">{{ statusMeta[row.status]?.label || row.status }}</el-tag></div>
<div class="assessment-actions"> <div class="assessment-actions">
<el-button v-if="row.status === 'NOT_STARTED'" text type="primary" :icon="RefreshLeft" @click="withdraw(row)">撤回</el-button> <el-button v-if="row.status === 'NOT_STARTED'" text type="primary" :icon="RefreshLeft" @click="withdraw(row)">撤回</el-button>
@ -115,7 +115,7 @@
import { computed, onActivated, onMounted, reactive, ref } from "vue"; import { computed, onActivated, onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ArrowDown, Check, Close, DataAnalysis, Delete, Download, Edit, EditPen, FolderOpened, MagicStick, Paperclip, PictureFilled, Plus, RefreshLeft, Search, Upload } from "@element-plus/icons-vue"; import { ArrowDown, Check, Clock, Close, DataAnalysis, Delete, Download, Edit, EditPen, FolderOpened, MagicStick, Paperclip, PictureFilled, Plus, RefreshLeft, Search, Upload } from "@element-plus/icons-vue";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import * as examApi from "@/api/exam"; import * as examApi from "@/api/exam";
@ -470,6 +470,33 @@ function durationBetween(row) { if (!row.startTime || !row.endTime) return 0; re
color: var(--text-body); color: var(--text-body);
font-size: var(--fs-body); font-size: var(--fs-body);
} }
.time-line {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 4px 0;
padding: 4px 10px;
border-radius: 6px;
background: var(--edu-primary-soft);
.el-icon {
font-size: 15px;
color: var(--edu-primary);
}
.time-label {
color: var(--edu-primary-deep);
font-size: var(--fs-body);
}
.time-value {
color: var(--text-strong);
font-size: var(--fs-body);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
}
} }
.assessment-status { .assessment-status {

Loading…
Cancel
Save