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

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

@ -51,9 +51,9 @@
</div>
<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>
<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>
<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>
</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)));
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 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); }
@ -403,38 +403,48 @@ function toBoolean(value, fallback) { if (value === undefined || value === null
display: flex;
align-items: center;
justify-content: center;
gap: var(--sp-4);
gap: var(--sp-3);
margin-top: var(--sp-5);
}
.fixed-step-button {
min-width: 164px;
height: 40px;
padding: 0 var(--sp-5);
border: 0;
min-width: 148px;
height: 38px;
padding: 0 var(--sp-4);
border: 1px solid var(--border-color);
border-radius: var(--r-control);
background: var(--edu-primary);
color: #ffffff;
background: #ffffff;
color: var(--text-body);
font: inherit;
font-size: var(--fs-body);
font-weight: 600;
font-weight: 500;
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 {
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);
transform: translateY(-1px);
&:hover { color: #ffffff; }
}
&:focus-visible { outline: 2px solid var(--edu-primary); outline-offset: 2px; }
}
.fixed-step-arrow {
color: var(--text-strong);
font-size: 20px;
font-weight: 700;
letter-spacing: -3px;
display: inline-flex;
align-items: center;
color: var(--text-muted);
font-size: 16px;
.el-icon { font-size: 16px; }
}
.form-row {

@ -10,7 +10,7 @@
<section v-loading="loading" class="assessment-list">
<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-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-actions">
<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 { ElMessage, ElMessageBox } from "element-plus";
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 * as examApi from "@/api/exam";
@ -470,6 +470,33 @@ function durationBetween(row) { if (!row.startTime || !row.endTime) return 0; re
color: var(--text-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 {

Loading…
Cancel
Save