|
|
<template>
|
|
|
<main class="exam-editor" v-loading="loading">
|
|
|
<header class="editor-head">
|
|
|
<nav class="head-breadcrumb"><span @click="$router.push('/exam/index')">综合考核</span><span
|
|
|
class="bc-sep">/</span><span @click="$router.push('/exam/index')">考评管理</span><span
|
|
|
class="bc-sep">/</span><span class="bc-current">{{ isNew ? '新建考试' : (form.name || '编辑考试') }}</span>
|
|
|
</nav>
|
|
|
<div class="head-title-row">
|
|
|
<h1>{{ isNew ? '新建考试' : (form.name || '编辑考试') }}</h1>
|
|
|
<el-tag v-if="!isNew" :type="statusMeta[status]?.tone" effect="light">{{ statusMeta[status]?.label
|
|
|
}}</el-tag>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
<section class="editor-panel prototype-base-panel">
|
|
|
<div class="section-card">
|
|
|
<header class="card-head">
|
|
|
<div>
|
|
|
<h2>基础设置</h2>
|
|
|
<p>填写考试名称、时间与班级,分值矩阵自动汇总</p>
|
|
|
</div>
|
|
|
</header>
|
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="base-form">
|
|
|
<el-form-item label="考试名称" prop="name"><el-input v-model="form.name" maxlength="20" show-word-limit
|
|
|
:disabled="inProgress" /></el-form-item>
|
|
|
<div class="form-row">
|
|
|
<el-form-item label="开始时间" prop="startTime"><el-date-picker v-model="form.startTime"
|
|
|
type="datetime" value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
:disabled="inProgress" /></el-form-item>
|
|
|
<el-form-item label="结束时间" prop="endTime"><el-date-picker v-model="form.endTime" type="datetime"
|
|
|
value-format="YYYY-MM-DD HH:mm:ss" @change="syncDurationFromEnd" /></el-form-item>
|
|
|
</div>
|
|
|
<div class="form-row">
|
|
|
<el-form-item label="考试时长"><el-input-number v-model="duration" :min="1" :max="10080"
|
|
|
@change="syncEndTime" /><span class="unit">分钟</span></el-form-item>
|
|
|
<el-form-item label="考试班级" prop="classIds">
|
|
|
<el-select v-model="form.classIds" multiple collapse-tags placeholder="选择教师名下的教学班"
|
|
|
:disabled="inProgress"><el-option v-for="item in classes" :key="item.id"
|
|
|
:label="item.name" :value="item.id" /></el-select>
|
|
|
</el-form-item>
|
|
|
</div>
|
|
|
</el-form>
|
|
|
<div class="score-table-wrap">
|
|
|
<table class="score-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>题型</th>
|
|
|
<th>题库题目数量</th>
|
|
|
<th>选题数量</th>
|
|
|
<th>每题分值</th>
|
|
|
<th>小计</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="item in typeOptions" :key="item.value">
|
|
|
<td>{{ item.label }}</td>
|
|
|
<td>{{ questionCount(item.value) }}</td>
|
|
|
<td>{{ selectedByType(item.value) }}</td>
|
|
|
<td><el-input-number v-model="typeScores[item.value]" :min="0" :max="100" size="small"
|
|
|
controls-position="right" :disabled="inProgress"
|
|
|
@change="onTypeScoreChange(item.value)" /></td>
|
|
|
<td>{{ scoreByType(item.value) }}</td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</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"><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"><el-icon>
|
|
|
<ArrowRight />
|
|
|
</el-icon></span>
|
|
|
<button type="button" class="fixed-step-button" :class="{ 'is-active': activeStep === 0 }"
|
|
|
@click="activeStep = 0">考试设置</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section v-if="!inProgress && activeStep === 0" class="editor-panel settings-stage">
|
|
|
<div class="section-card">
|
|
|
<header class="card-head">
|
|
|
<div>
|
|
|
<h2>考试设置</h2>
|
|
|
<p>设置试题展示、作答行为和多选题计分方式</p>
|
|
|
</div>
|
|
|
</header>
|
|
|
<div class="settings-grid">
|
|
|
<div class="setting-card">
|
|
|
<div class="setting-card-body">
|
|
|
<span class="setting-card-label">题目乱序</span>
|
|
|
<span class="setting-card-desc">同一题型内打乱题目顺序</span>
|
|
|
</div>
|
|
|
<el-radio-group v-model="form.questionOrderMode"><el-radio-button
|
|
|
label="FIXED">固定顺序</el-radio-button><el-radio-button
|
|
|
label="RANDOM">随机打乱</el-radio-button></el-radio-group>
|
|
|
</div>
|
|
|
<div class="setting-card">
|
|
|
<div class="setting-card-body">
|
|
|
<span class="setting-card-label">选项乱序</span>
|
|
|
<span class="setting-card-desc">同一题型内打乱选项顺序</span>
|
|
|
</div>
|
|
|
<el-radio-group v-model="form.optionOrderMode"><el-radio-button
|
|
|
label="FIXED">固定顺序</el-radio-button><el-radio-button
|
|
|
label="RANDOM">随机打乱</el-radio-button></el-radio-group>
|
|
|
</div>
|
|
|
<div class="setting-card">
|
|
|
<div class="setting-card-body">
|
|
|
<span class="setting-card-label">允许复制试题</span>
|
|
|
<span class="setting-card-desc">考试过程中考生可复制题干</span>
|
|
|
</div>
|
|
|
<el-radio-group v-model="form.allowCopyQuestion"><el-radio-button
|
|
|
:label="false">不允许</el-radio-button><el-radio-button
|
|
|
:label="true">允许</el-radio-button></el-radio-group>
|
|
|
</div>
|
|
|
<div class="setting-card">
|
|
|
<div class="setting-card-body">
|
|
|
<span class="setting-card-label">允许粘贴答案</span>
|
|
|
<span class="setting-card-desc">考试过程中考生可粘贴内容</span>
|
|
|
</div>
|
|
|
<el-radio-group v-model="form.allowPasteAnswer"><el-radio-button
|
|
|
:label="false">不允许</el-radio-button><el-radio-button
|
|
|
:label="true">允许</el-radio-button></el-radio-group>
|
|
|
</div>
|
|
|
<div class="setting-card">
|
|
|
<div class="setting-card-body">
|
|
|
<span class="setting-card-label">考试过程可以回退</span>
|
|
|
<span class="setting-card-desc">答完一题后可返回修改</span>
|
|
|
</div>
|
|
|
<el-radio-group v-model="form.allowRollback"><el-radio-button
|
|
|
:label="false">不允许</el-radio-button><el-radio-button
|
|
|
:label="true">允许</el-radio-button></el-radio-group>
|
|
|
</div>
|
|
|
<div class="setting-card setting-card-wide">
|
|
|
<div class="setting-card-body">
|
|
|
<span class="setting-card-label">多选题计分方式</span>
|
|
|
<span class="setting-card-desc">少选、多选、错选的得分规则</span>
|
|
|
</div>
|
|
|
<el-radio-group v-model="form.multipleScoreMode"><el-radio-button
|
|
|
label="ALL_OR_NOTHING">全对才得分</el-radio-button><el-radio-button
|
|
|
label="PARTIAL_BY_COUNT">按选项个数计分</el-radio-button><el-radio-button
|
|
|
label="HALF_FOR_PARTIAL">少选得一半分</el-radio-button></el-radio-group>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section v-if="!inProgress && activeStep === 1" class="editor-panel question-stage">
|
|
|
<div class="section-card">
|
|
|
<header class="section-head">
|
|
|
<div>
|
|
|
<h2>选择试题</h2>
|
|
|
<p>勾选题目并设置该题分值,选择结果实时同步到试卷概览</p>
|
|
|
</div>
|
|
|
<div class="import-tools">
|
|
|
<div class="import-actions"><el-button class="template-button" :icon="Download"
|
|
|
@click="downloadTemplate">下载 Word 模板</el-button><el-upload class="word-upload"
|
|
|
:show-file-list="false" accept=".docx" :http-request="importPaperQuestions"><el-button
|
|
|
:icon="Upload">导入 Word 试题</el-button></el-upload></div>
|
|
|
<span class="import-hint">按模板填写后上传,系统会解析题干、选项、答案和解析,并自动选中题目</span>
|
|
|
</div>
|
|
|
</header>
|
|
|
<div class="type-tabs">
|
|
|
<button v-for="item in typeOptions" :key="item.value"
|
|
|
:class="['type-tab', { active: activeType === item.value }]" @click="activeType = item.value">
|
|
|
<span class="type-tab-label">{{ item.label }}</span>
|
|
|
<span class="type-tab-count"><b>{{ selectedByType(item.value) }}</b>/<em>{{
|
|
|
questionCount(item.value) }}</em></span>
|
|
|
</button>
|
|
|
</div>
|
|
|
<div class="question-list">
|
|
|
<article v-for="(question, idx) in filteredQuestions" :key="question.id"
|
|
|
:class="['q-card', { 'is-picked': isSelected(question.id) }]"
|
|
|
@click="toggleQuestion(question, !isSelected(question.id))">
|
|
|
<div class="q-card-stripe"></div>
|
|
|
<div class="q-card-body">
|
|
|
<div class="q-card-head">
|
|
|
<span class="q-card-no">{{ idx + 1 }}</span>
|
|
|
<span class="q-card-type">{{typeOptions.find(t => t.value === question.type)?.label ||
|
|
|
question.type}}</span>
|
|
|
</div>
|
|
|
<h3 class="q-card-stem">{{ question.stem }}</h3>
|
|
|
<div v-if="question.options?.length" class="q-options">
|
|
|
<div v-for="option in question.options" :key="option.label" class="q-option"><span
|
|
|
class="q-option-label">{{ option.label }}</span><span>{{ option.content
|
|
|
}}</span></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="q-card-check">
|
|
|
<div v-if="isSelected(question.id)" class="q-check-on"><el-icon>
|
|
|
<Check />
|
|
|
</el-icon></div>
|
|
|
<div v-else class="q-check-off">+</div>
|
|
|
</div>
|
|
|
</article>
|
|
|
<div v-if="!filteredQuestions.length" class="table-empty">
|
|
|
<el-icon class="empty-icon">
|
|
|
<FolderOpened />
|
|
|
</el-icon>
|
|
|
<p class="empty-title">该题型暂无题目</p>
|
|
|
<p class="empty-tip">可先到"题库管理"新增题目</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section v-if="!inProgress && activeStep === 2" class="editor-panel preview-stage">
|
|
|
<div class="section-card preview-summary-card">
|
|
|
<header class="section-head">
|
|
|
<div>
|
|
|
<h2>试卷概览</h2>
|
|
|
<p>{{ form.questions.length }} 道题,共 {{ totalScore }} 分</p>
|
|
|
</div>
|
|
|
<div class="preview-actions"><el-upload :show-file-list="false" accept=".docx"
|
|
|
:http-request="importPaperQuestions"><el-button :icon="Upload">导入 Word
|
|
|
试卷</el-button></el-upload><el-button :icon="Download" @click="exportPaper"
|
|
|
:disabled="isNew">导出 Word 试卷</el-button></div>
|
|
|
</header>
|
|
|
<div class="score-cards preview-score-cards">
|
|
|
<div v-for="item in typeOptions" :key="item.value" class="score-card"
|
|
|
:class="{ 'score-card-zero': !selectedByType(item.value) }">
|
|
|
<div class="score-card-bar"></div>
|
|
|
<div class="score-card-body">
|
|
|
<div class="score-card-label">{{ item.label }}</div>
|
|
|
<div class="score-card-nums"><span class="num-picked">{{ selectedByType(item.value)
|
|
|
}}</span><span class="num-sep">题</span></div>
|
|
|
<div class="score-card-sub">{{ scoreByType(item.value) }} 分</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="score-card score-card-total">
|
|
|
<div class="score-card-bar bar-total"></div>
|
|
|
<div class="score-card-body">
|
|
|
<div class="score-card-label">总分</div>
|
|
|
<div class="score-card-nums"><span class="num-picked">{{ totalScore }}</span><span
|
|
|
class="num-sep">分</span></div>
|
|
|
<div class="score-card-sub">{{ form.questions.length }} 道题</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div v-if="form.questions.length" class="preview-body">
|
|
|
<nav class="type-tabs">
|
|
|
<button v-for="item in selectedTypes" :key="item.value" class="type-tab type-tab-nav"
|
|
|
@click="scrollToType(item.value)">{{ item.label }}<span class="type-tab-count"><b>{{
|
|
|
selectedByType(item.value) }}</b></span></button>
|
|
|
</nav>
|
|
|
<section v-for="item in selectedTypes" :id="`paper-${item.value}`" :key="item.value"
|
|
|
class="paper-section preview-paper-section">
|
|
|
<header class="paper-section-head">
|
|
|
<div class="paper-section-stripe"></div>
|
|
|
<h3>{{ item.label }}</h3>
|
|
|
<span class="paper-section-meta">{{ selectedByType(item.value) }} 题 · {{ scoreByType(item.value)
|
|
|
}} 分</span>
|
|
|
</header>
|
|
|
<div class="paper-questions">
|
|
|
<article v-for="(question, index) in selectedQuestions(item.value)" :key="question.id"
|
|
|
class="pq-card">
|
|
|
<span class="pq-no">{{ index + 1 }}</span>
|
|
|
<div class="pq-body">
|
|
|
<h4 class="pq-stem">{{ question.stem }}</h4>
|
|
|
<div v-if="question.options?.length" class="pq-options">
|
|
|
<span v-for="option in question.options" :key="option.label" class="pq-option"><b>{{
|
|
|
option.label }}</b> {{ option.content }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<span class="pq-score">{{ selectedScore(question.id) }} 分</span>
|
|
|
<el-button class="pq-remove" text type="danger"
|
|
|
@click.stop="removeQuestionFromPreview(question)">取消选择</el-button>
|
|
|
</article>
|
|
|
</div>
|
|
|
</section>
|
|
|
</div>
|
|
|
<div v-if="!form.questions.length" class="table-empty">
|
|
|
<el-icon class="empty-icon">
|
|
|
<FolderOpened />
|
|
|
</el-icon>
|
|
|
<p class="empty-title">尚未选择试题</p>
|
|
|
<p class="empty-tip">请返回上一步勾选题目</p>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<footer class="editor-footer"><el-button :icon="Close"
|
|
|
@click="$router.push('/exam/index')">取消</el-button><el-button :icon="Check"
|
|
|
@click="save(false)">保存考试</el-button><el-button v-if="status === 'DRAFT'" type="primary"
|
|
|
:icon="Promotion" @click="save(true)">立即发布</el-button></footer>
|
|
|
</main>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { computed, onActivated, onMounted, reactive, ref } from "vue"
|
|
|
import { useRoute, useRouter } from "vue-router"
|
|
|
import { ElMessage } from "element-plus"
|
|
|
import { ArrowLeft, ArrowRight, Check, Close, Download, FolderOpened, Promotion, Upload } from "@element-plus/icons-vue"
|
|
|
import { saveAs } from "file-saver"
|
|
|
import * as examApi from "@/api/exam"
|
|
|
|
|
|
const route = useRoute(); const router = useRouter(); const id = computed(() => route.params.id); const isNew = computed(() => id.value === "new")
|
|
|
const loading = ref(false); const activeStep = ref(0); const activeType = ref("SINGLE"); const status = ref("DRAFT"); const classes = ref([]); const bank = ref([]); const formRef = ref(); const duration = ref(60)
|
|
|
const typeScores = reactive({ SINGLE: 1, MULTIPLE: 1, TRUE_FALSE: 1, SHORT_ANSWER: 1 })
|
|
|
const typeOptions = [{ label: "单选题", value: "SINGLE" }, { label: "多选题", value: "MULTIPLE" }, { label: "判断题", value: "TRUE_FALSE" }, { label: "简答题", value: "SHORT_ANSWER" }]
|
|
|
const statusMeta = { DRAFT: { label: "未发布", tone: "info" }, NOT_STARTED: { label: "未开始", tone: "" }, IN_PROGRESS: { label: "进行中", tone: "success" }, ENDED: { label: "已结束", tone: "warning" } }
|
|
|
const form = reactive({ name: "", startTime: "", endTime: "", classIds: [], questionOrderMode: "FIXED", optionOrderMode: "FIXED", allowCopyQuestion: false, allowPasteAnswer: false, allowRollback: true, multipleScoreMode: "ALL_OR_NOTHING", questions: [] })
|
|
|
const rules = { name: [{ required: true, message: "请输入考试名称" }, { pattern: /^[\p{L}\p{N}]+$/u, message: "仅支持汉字、字母和数字" }], startTime: [{ required: true, message: "请选择开始时间" }], endTime: [{ required: true, message: "请选择结束时间" }], classIds: [{ type: "array", required: true, min: 1, message: "至少选择一个班级" }] }
|
|
|
const inProgress = computed(() => status.value === "IN_PROGRESS")
|
|
|
const filteredQuestions = computed(() => bank.value.filter((item) => item.type === activeType.value))
|
|
|
const totalScore = computed(() => form.questions.reduce((sum, item) => sum + Number(item.score || 0), 0))
|
|
|
const selectedTypes = computed(() => typeOptions.filter((item) => selectedByType(item.value)))
|
|
|
|
|
|
let editorMounted = false
|
|
|
onMounted(load)
|
|
|
onActivated(() => { if (editorMounted) load(); editorMounted = true })
|
|
|
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 } const startMs = Number(data.startTime); const endMs = Number(data.endTime); if (endMs > startMs) duration.value = Math.round((endMs - startMs) / 60000) } 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) }
|
|
|
function selectedScore(questionId) { return form.questions.find((item) => item.questionId === questionId)?.score || 1 }
|
|
|
function toggleQuestion(question, checked) { if (checked) form.questions.push({ questionId: question.id, score: typeScores[question.type] || 1 }); else form.questions = form.questions.filter((item) => item.questionId !== question.id) }
|
|
|
function removeQuestionFromPreview(question) { toggleQuestion(question, false) }
|
|
|
|
|
|
function selectedByType(type) { const ids = new Set(bank.value.filter((item) => item.type === type).map((item) => item.id)); return form.questions.filter((item) => ids.has(item.questionId)).length }
|
|
|
function questionCount(type) { return bank.value.filter((item) => item.type === type).length }
|
|
|
function scoreByType(type) { return selectedQuestions(type).reduce((sum, item) => sum + Number(selectedScore(item.id)), 0) }
|
|
|
function scorePerQuestion(type) { return Number(typeScores[type] || 1) }
|
|
|
function onTypeScoreChange(type) { const score = Number(typeScores[type] || 0); for (const item of form.questions) { const bankItem = bank.value.find((q) => q.id === item.questionId); if (bankItem && bankItem.type === type) item.score = score } }
|
|
|
function selectedQuestions(type) { const selected = new Set(form.questions.map((item) => item.questionId)); return bank.value.filter((item) => item.type === type && selected.has(item.id)) }
|
|
|
async function nextStep() { if (activeStep.value === 0) { try { await formRef.value.validate(); const start = new Date(form.startTime.replace(" ", "T")); const end = new Date(form.endTime.replace(" ", "T")); if (end <= start) return ElMessage.warning("结束时间必须晚于开始时间"); duration.value = Math.round((end - start) / 60000) } catch { return } } if (activeStep.value === 1 && !form.questions.length) return ElMessage.warning("至少选择一道试题"); activeStep.value++ }
|
|
|
async function save(publish) { if (inProgress.value) { await examApi.updateExam(id.value, form); ElMessage.success("结束时间已更新"); return router.push("/exam/index") } try { await formRef.value?.validate() } catch { activeStep.value = 0; return } if (!form.questions.length) { activeStep.value = 1; return ElMessage.warning("至少选择一道试题") } const result = isNew.value ? await examApi.createExam(form) : await examApi.updateExam(id.value, form); const examId = result.data.id; if (publish) await examApi.publishExam(examId); ElMessage.success(publish ? "考试已保存并发布" : "考试已保存"); router.push("/exam/index") }
|
|
|
async function importPaperQuestions({ file }) { const formData = new FormData(); formData.append("file", file); const { data } = await examApi.importQuestions(formData); bank.value = [...data, ...bank.value]; for (const q of data) if (!isSelected(q.id)) form.questions.push({ questionId: q.id, score: typeScores[q.type] || 1 }); ElMessage.success(`导入并选中 ${data.length} 道题`) }
|
|
|
async function downloadTemplate() { saveAs(await examApi.downloadQuestionTemplate(), "考试题库导入模板.docx") }
|
|
|
async function exportPaper() { saveAs(await examApi.exportExamPaper(id.value), `${form.name}试卷.docx`) }
|
|
|
function scrollToType(type) { document.getElementById(`paper-${type}`)?.scrollIntoView({ behavior: "smooth", block: "start" }) }
|
|
|
function formatValue(value) { const date = value instanceof Date ? value : new Date(value); const pad = (n) => String(n).padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` }
|
|
|
function toBoolean(value, fallback) { if (value === undefined || value === null || value === "") return fallback; return value === true || value === 1 || value === "1" || value === "true" }
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
.exam-editor {
|
|
|
color: var(--text-strong);
|
|
|
padding-bottom: 92px;
|
|
|
|
|
|
/* ===== 标题行(左组:返回+标题;右侧:操作按钮;扁平落地画布)===== */
|
|
|
.editor-head {
|
|
|
display: grid;
|
|
|
gap: var(--sp-2);
|
|
|
margin-bottom: var(--sp-4);
|
|
|
padding-bottom: var(--sp-3);
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
}
|
|
|
|
|
|
.head-breadcrumb {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-1);
|
|
|
font-size: var(--fs-tag);
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
span {
|
|
|
cursor: pointer;
|
|
|
transition: color 0.14s ease;
|
|
|
}
|
|
|
|
|
|
span:hover {
|
|
|
color: var(--edu-primary);
|
|
|
}
|
|
|
|
|
|
.bc-sep {
|
|
|
cursor: default;
|
|
|
color: var(--text-muted);
|
|
|
opacity: .5;
|
|
|
}
|
|
|
|
|
|
.bc-sep:hover {
|
|
|
color: var(--text-muted);
|
|
|
}
|
|
|
|
|
|
.bc-current {
|
|
|
cursor: default;
|
|
|
color: var(--text-body);
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.bc-current:hover {
|
|
|
color: var(--text-body);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.head-title-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-3);
|
|
|
|
|
|
h1 {
|
|
|
margin: 0;
|
|
|
color: var(--text-strong);
|
|
|
font-size: var(--fs-page);
|
|
|
font-weight: 700;
|
|
|
letter-spacing: -0.01em;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 步骤条 ===== */
|
|
|
.steps {
|
|
|
max-width: 820px;
|
|
|
margin: var(--sp-5) auto;
|
|
|
|
|
|
:deep(.el-step__title) {
|
|
|
color: var(--text-body);
|
|
|
font-size: var(--fs-body);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
:deep(.el-step__title.is-process) {
|
|
|
color: var(--text-strong);
|
|
|
}
|
|
|
|
|
|
:deep(.el-step__title.is-finish) {
|
|
|
color: var(--text-body);
|
|
|
}
|
|
|
|
|
|
:deep(.el-step__head.is-process) {
|
|
|
color: var(--edu-primary);
|
|
|
border-color: var(--edu-primary);
|
|
|
}
|
|
|
|
|
|
:deep(.el-step__head.is-finish) {
|
|
|
color: var(--edu-primary);
|
|
|
border-color: var(--edu-primary);
|
|
|
}
|
|
|
|
|
|
:deep(.el-step__head.is-wait) {
|
|
|
color: var(--text-muted);
|
|
|
border-color: var(--border-color);
|
|
|
}
|
|
|
|
|
|
:deep(.el-step__icon) {
|
|
|
background: #ffffff;
|
|
|
border-color: var(--border-color);
|
|
|
}
|
|
|
|
|
|
:deep(.el-step__icon-inner) {
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
:deep(.el-step__line) {
|
|
|
background-color: var(--border-color);
|
|
|
}
|
|
|
|
|
|
:deep(.el-step__line-inner) {
|
|
|
border-color: var(--edu-primary);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 面板(左对齐文档流,不居中悬浮)===== */
|
|
|
.editor-panel {
|
|
|
width: 100%;
|
|
|
max-width: 1280px;
|
|
|
margin: 0;
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
h2 {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-2);
|
|
|
margin: 0;
|
|
|
color: var(--text-strong);
|
|
|
font-size: var(--fs-page);
|
|
|
font-weight: 700;
|
|
|
|
|
|
&::before {
|
|
|
width: 4px;
|
|
|
height: 20px;
|
|
|
border-radius: 4px;
|
|
|
background: var(--edu-primary);
|
|
|
content: '';
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 分组白卡(调研结论:逻辑分组 + 呼吸感)===== */
|
|
|
.section-card {
|
|
|
padding: var(--sp-5);
|
|
|
border: 1px solid var(--border-color);
|
|
|
border-radius: var(--r-card);
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.card-head {
|
|
|
margin-bottom: var(--sp-4);
|
|
|
|
|
|
p {
|
|
|
margin: var(--sp-1) 0 0;
|
|
|
color: var(--text-body);
|
|
|
font-size: var(--fs-body);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 基础设置表单(单列 + 成对字段组,呼吸感间距)===== */
|
|
|
.base-form {
|
|
|
:deep(.el-form-item) {
|
|
|
margin-bottom: var(--sp-4);
|
|
|
}
|
|
|
|
|
|
:deep(.el-form-item:first-child) {
|
|
|
margin-bottom: var(--sp-3);
|
|
|
}
|
|
|
|
|
|
:deep(.el-form-item__label) {
|
|
|
color: var(--text-body);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
:deep(.el-input__count-inner) {
|
|
|
color: var(--text-muted);
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
:deep(.el-input),
|
|
|
:deep(.el-select),
|
|
|
:deep(.el-date-editor) {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
:deep(.el-input-number) {
|
|
|
width: 180px;
|
|
|
}
|
|
|
|
|
|
:deep(.el-input-number .el-input__wrapper) {
|
|
|
padding-right: 58px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 原型中的固定信息区:基础字段、题型汇总和步骤入口始终保持在页面顶部。 */
|
|
|
.score-table-wrap {
|
|
|
overflow-x: auto;
|
|
|
margin-top: var(--sp-5);
|
|
|
border: 1px solid var(--border-color);
|
|
|
border-radius: var(--r-control);
|
|
|
}
|
|
|
|
|
|
.score-table {
|
|
|
width: 100%;
|
|
|
min-width: 860px;
|
|
|
border-collapse: collapse;
|
|
|
table-layout: fixed;
|
|
|
background: #ffffff;
|
|
|
|
|
|
th,
|
|
|
td {
|
|
|
height: 42px;
|
|
|
padding: 0 var(--sp-3);
|
|
|
border-right: 1px solid var(--border-color);
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
color: var(--text-body);
|
|
|
font-size: var(--fs-body);
|
|
|
text-align: center;
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
|
|
|
th {
|
|
|
color: var(--text-strong);
|
|
|
background: var(--edu-primary-soft);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
th:last-child,
|
|
|
td:last-child {
|
|
|
border-right: 0;
|
|
|
}
|
|
|
|
|
|
tbody tr:last-child td {
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
|
|
|
tbody tr:hover td {
|
|
|
background: #fafcff;
|
|
|
}
|
|
|
|
|
|
:deep(.el-input-number) {
|
|
|
width: 100px;
|
|
|
}
|
|
|
|
|
|
:deep(.el-input-number .el-input__wrapper) {
|
|
|
padding: 0 8px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.fixed-step-actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: var(--sp-3);
|
|
|
margin-top: var(--sp-5);
|
|
|
}
|
|
|
|
|
|
.fixed-step-button {
|
|
|
min-width: 148px;
|
|
|
height: 38px;
|
|
|
padding: 0 var(--sp-4);
|
|
|
border: 1px solid var(--border-color);
|
|
|
border-radius: var(--r-control);
|
|
|
background: #ffffff;
|
|
|
color: var(--text-body);
|
|
|
font: inherit;
|
|
|
font-size: var(--fs-body);
|
|
|
font-weight: 500;
|
|
|
cursor: pointer;
|
|
|
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);
|
|
|
}
|
|
|
|
|
|
&.is-active {
|
|
|
border-color: var(--edu-primary);
|
|
|
background: var(--edu-primary);
|
|
|
color: #ffffff;
|
|
|
box-shadow: 0 4px 10px rgba(25, 137, 204, 0.18);
|
|
|
|
|
|
&:hover {
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&:focus-visible {
|
|
|
outline: 2px solid var(--edu-primary);
|
|
|
outline-offset: 2px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.fixed-step-arrow {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
color: var(--text-muted);
|
|
|
font-size: 16px;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 16px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.form-row {
|
|
|
display: grid;
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
gap: var(--sp-4) var(--sp-5);
|
|
|
}
|
|
|
|
|
|
.unit {
|
|
|
margin-left: var(--sp-2);
|
|
|
color: var(--text-body);
|
|
|
}
|
|
|
|
|
|
/* ===== 分值卡片组(替代旧表格,彩色顶部条)===== */
|
|
|
.score-cards {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(5, 1fr);
|
|
|
gap: var(--sp-3);
|
|
|
margin-top: var(--sp-5);
|
|
|
padding-top: var(--sp-4);
|
|
|
border-top: 1px solid var(--border-color);
|
|
|
}
|
|
|
|
|
|
.score-card {
|
|
|
position: relative;
|
|
|
border: 1px solid var(--border-color);
|
|
|
border-radius: var(--r-control);
|
|
|
background: #ffffff;
|
|
|
overflow: hidden;
|
|
|
transition: border-color 0.16s ease;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: var(--edu-primary);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.score-card-bar {
|
|
|
height: 3px;
|
|
|
background: var(--border-color);
|
|
|
}
|
|
|
|
|
|
.score-card-bar,
|
|
|
.bar-total {
|
|
|
background: var(--edu-primary);
|
|
|
}
|
|
|
|
|
|
.score-card-body {
|
|
|
padding: var(--sp-3);
|
|
|
display: grid;
|
|
|
gap: var(--sp-1);
|
|
|
}
|
|
|
|
|
|
.score-card-label {
|
|
|
font-size: var(--fs-tag);
|
|
|
color: var(--text-muted);
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.score-card-nums {
|
|
|
display: flex;
|
|
|
align-items: baseline;
|
|
|
gap: var(--sp-1);
|
|
|
}
|
|
|
|
|
|
.num-picked {
|
|
|
font-size: var(--fs-num);
|
|
|
font-weight: 700;
|
|
|
color: var(--text-strong);
|
|
|
line-height: 1;
|
|
|
}
|
|
|
|
|
|
.num-sep {
|
|
|
color: var(--text-muted);
|
|
|
font-size: var(--fs-card);
|
|
|
}
|
|
|
|
|
|
.num-total {
|
|
|
font-size: var(--fs-card);
|
|
|
color: var(--text-body);
|
|
|
}
|
|
|
|
|
|
.score-card-sub {
|
|
|
font-size: var(--fs-tag);
|
|
|
color: var(--text-body);
|
|
|
}
|
|
|
|
|
|
.score-card-total {
|
|
|
background: var(--edu-primary-soft);
|
|
|
|
|
|
.score-card-label {
|
|
|
color: var(--edu-primary-deep);
|
|
|
}
|
|
|
|
|
|
.num-picked {
|
|
|
color: var(--edu-primary);
|
|
|
}
|
|
|
|
|
|
.score-card-sub {
|
|
|
color: var(--edu-primary-deep);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.section-head {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: var(--sp-4);
|
|
|
margin-bottom: var(--sp-4);
|
|
|
|
|
|
h2 {
|
|
|
margin: 0;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: var(--sp-1) 0 0;
|
|
|
color: var(--text-body);
|
|
|
font-size: var(--fs-body);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.preview-actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-2);
|
|
|
}
|
|
|
|
|
|
.import-tools {
|
|
|
display: grid;
|
|
|
justify-items: end;
|
|
|
gap: var(--sp-2);
|
|
|
}
|
|
|
|
|
|
/* 下载模板/导入试题按钮外观归 teacher-light.scss 全局接管(ghost) */
|
|
|
.import-actions {
|
|
|
display: flex;
|
|
|
gap: var(--sp-2);
|
|
|
}
|
|
|
|
|
|
.import-hint {
|
|
|
max-width: 420px;
|
|
|
color: var(--text-muted);
|
|
|
font-size: var(--fs-tag);
|
|
|
line-height: 1.45;
|
|
|
text-align: right;
|
|
|
}
|
|
|
|
|
|
/* ===== 题型 Tab 栏(步骤02选择题型 + 步骤03试卷快速跳转共用)===== */
|
|
|
.type-tabs {
|
|
|
display: flex;
|
|
|
gap: var(--sp-2);
|
|
|
margin-bottom: var(--sp-4);
|
|
|
padding-bottom: var(--sp-3);
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
overflow-x: auto;
|
|
|
}
|
|
|
|
|
|
.type-tab {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-2);
|
|
|
padding: 9px var(--sp-4);
|
|
|
border: 1px solid var(--border-color);
|
|
|
border-radius: var(--r-control);
|
|
|
background: #ffffff;
|
|
|
cursor: pointer;
|
|
|
font: inherit;
|
|
|
font-size: var(--fs-body);
|
|
|
font-weight: 500;
|
|
|
color: var(--text-body);
|
|
|
white-space: nowrap;
|
|
|
transition: all 0.16s ease;
|
|
|
height: 36px;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: var(--edu-primary);
|
|
|
color: var(--edu-primary);
|
|
|
}
|
|
|
|
|
|
&.active {
|
|
|
background: var(--edu-primary);
|
|
|
border-color: var(--edu-primary);
|
|
|
color: #ffffff;
|
|
|
|
|
|
.type-tab-count {
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
}
|
|
|
|
|
|
.type-tab-count b {
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
|
|
|
.type-tab-count em {
|
|
|
color: rgba(255, 255, 255, 0.7);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.type-tab-label {
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.type-tab-count {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-1);
|
|
|
padding: 2px var(--sp-2);
|
|
|
border-radius: var(--r-control);
|
|
|
background: var(--border-color);
|
|
|
font-size: var(--fs-tag);
|
|
|
|
|
|
b {
|
|
|
font-weight: 600;
|
|
|
color: var(--edu-primary);
|
|
|
font-style: normal;
|
|
|
}
|
|
|
|
|
|
em {
|
|
|
color: var(--text-muted);
|
|
|
font-style: normal;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.type-tab-nav.type-tab {
|
|
|
border-color: var(--border-color);
|
|
|
background: #ffffff;
|
|
|
color: var(--text-body);
|
|
|
}
|
|
|
|
|
|
.question-list {
|
|
|
display: grid;
|
|
|
gap: var(--sp-3);
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
/* 题目卡片 */
|
|
|
.q-card {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
gap: var(--sp-3);
|
|
|
padding: var(--sp-4);
|
|
|
border: 1px solid var(--border-color);
|
|
|
border-radius: var(--r-control);
|
|
|
background: #ffffff;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.18s ease;
|
|
|
overflow: hidden;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: var(--edu-primary);
|
|
|
}
|
|
|
|
|
|
&.is-picked {
|
|
|
border-color: var(--edu-primary);
|
|
|
background: var(--edu-primary-soft);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.q-card-stripe {
|
|
|
position: absolute;
|
|
|
left: 0;
|
|
|
top: 0;
|
|
|
bottom: 0;
|
|
|
width: 3px;
|
|
|
background: var(--border-color);
|
|
|
transition: background 0.18s ease;
|
|
|
}
|
|
|
|
|
|
.q-card.is-picked .q-card-stripe {
|
|
|
background: var(--edu-primary);
|
|
|
}
|
|
|
|
|
|
.q-card-body {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
padding-left: var(--sp-2);
|
|
|
}
|
|
|
|
|
|
.q-card-head {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-2);
|
|
|
margin-bottom: var(--sp-2);
|
|
|
}
|
|
|
|
|
|
.q-card-no {
|
|
|
display: inline-grid;
|
|
|
place-items: center;
|
|
|
width: 22px;
|
|
|
height: 22px;
|
|
|
border-radius: 6px;
|
|
|
background: var(--border-color);
|
|
|
color: var(--text-muted);
|
|
|
font-size: var(--fs-tag);
|
|
|
font-weight: 600;
|
|
|
line-height: 1;
|
|
|
}
|
|
|
|
|
|
.q-card.is-picked .q-card-no {
|
|
|
background: var(--edu-primary);
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
|
|
|
.q-card-type {
|
|
|
font-size: var(--fs-body);
|
|
|
color: var(--text-muted);
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.q-card-stem {
|
|
|
margin: 0 0 var(--sp-2);
|
|
|
color: var(--text-strong);
|
|
|
font-size: var(--fs-body);
|
|
|
line-height: 1.6;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.q-options {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
gap: var(--sp-1) var(--sp-3);
|
|
|
}
|
|
|
|
|
|
.q-option {
|
|
|
display: flex;
|
|
|
gap: 8px;
|
|
|
font-size: var(--fs-tag);
|
|
|
color: var(--text-body);
|
|
|
line-height: 1.5;
|
|
|
}
|
|
|
|
|
|
.q-option-label {
|
|
|
font-weight: 600;
|
|
|
color: var(--text-muted);
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.q-card-check {
|
|
|
flex-shrink: 0;
|
|
|
align-self: flex-start;
|
|
|
margin-top: 2px;
|
|
|
}
|
|
|
|
|
|
.q-check-off {
|
|
|
width: 24px;
|
|
|
height: 24px;
|
|
|
border-radius: 50%;
|
|
|
border: 2px solid var(--text-muted);
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
color: var(--text-muted);
|
|
|
font-size: var(--fs-card);
|
|
|
font-weight: 400;
|
|
|
transition: all 0.18s ease;
|
|
|
}
|
|
|
|
|
|
.q-card:hover .q-check-off {
|
|
|
border-color: var(--edu-primary);
|
|
|
color: var(--edu-primary);
|
|
|
}
|
|
|
|
|
|
.q-check-on {
|
|
|
width: 24px;
|
|
|
height: 24px;
|
|
|
border-radius: 50%;
|
|
|
background: var(--edu-primary);
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
color: #ffffff;
|
|
|
transition: all 0.18s ease;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: var(--fs-card);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 试卷概览(复用 score-cards + 新预览结构)===== */
|
|
|
.preview-stage {
|
|
|
max-width: 1280px;
|
|
|
}
|
|
|
|
|
|
.preview-score-cards {
|
|
|
margin-top: var(--sp-4);
|
|
|
}
|
|
|
|
|
|
.score-card-zero {
|
|
|
opacity: 0.5;
|
|
|
|
|
|
.num-picked {
|
|
|
color: var(--text-muted);
|
|
|
}
|
|
|
|
|
|
.score-card-sub {
|
|
|
color: var(--text-muted);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.preview-body {
|
|
|
margin-top: var(--sp-5);
|
|
|
}
|
|
|
|
|
|
.paper-section {
|
|
|
scroll-margin-top: 80px;
|
|
|
margin-bottom: var(--sp-5);
|
|
|
border: 1px solid var(--border-color);
|
|
|
border-radius: var(--r-control);
|
|
|
background: #ffffff;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.paper-section-head {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-3);
|
|
|
padding: var(--sp-3) var(--sp-4);
|
|
|
background: #ffffff;
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
.paper-section-stripe {
|
|
|
width: 4px;
|
|
|
height: 20px;
|
|
|
border-radius: 2px;
|
|
|
background: var(--border-color);
|
|
|
}
|
|
|
|
|
|
.paper-section-head h3 {
|
|
|
margin: 0;
|
|
|
font-size: var(--fs-card);
|
|
|
font-weight: 600;
|
|
|
color: var(--text-strong);
|
|
|
}
|
|
|
|
|
|
.paper-section-meta {
|
|
|
margin-left: auto;
|
|
|
font-size: var(--fs-tag);
|
|
|
color: var(--text-muted);
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.paper-questions {
|
|
|
display: grid;
|
|
|
gap: var(--sp-1);
|
|
|
}
|
|
|
|
|
|
.pq-card {
|
|
|
display: grid;
|
|
|
grid-template-columns: 36px 1fr auto;
|
|
|
gap: var(--sp-3);
|
|
|
align-items: start;
|
|
|
padding: var(--sp-3) var(--sp-4);
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
transition: background 0.14s ease;
|
|
|
|
|
|
&:last-child {
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
|
|
|
&:hover {
|
|
|
background: var(--edu-primary-soft);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.pq-no {
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
width: 28px;
|
|
|
height: 28px;
|
|
|
border-radius: var(--r-control);
|
|
|
background: var(--border-color);
|
|
|
color: var(--text-muted);
|
|
|
font-size: var(--fs-card);
|
|
|
font-weight: 600;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.pq-body {
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
.pq-stem {
|
|
|
margin: 0 0 var(--sp-2);
|
|
|
color: var(--text-strong);
|
|
|
font-size: var(--fs-body);
|
|
|
line-height: 1.65;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.pq-options {
|
|
|
display: grid;
|
|
|
gap: var(--sp-1);
|
|
|
}
|
|
|
|
|
|
.pq-option {
|
|
|
font-size: var(--fs-tag);
|
|
|
color: var(--text-body);
|
|
|
line-height: 1.55;
|
|
|
|
|
|
b {
|
|
|
color: var(--text-muted);
|
|
|
font-weight: 600;
|
|
|
margin-right: var(--sp-1);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.pq-score {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
padding: 2px var(--sp-3);
|
|
|
border-radius: var(--r-control);
|
|
|
background: var(--edu-primary-soft);
|
|
|
color: var(--edu-primary-deep);
|
|
|
font-size: var(--fs-tag);
|
|
|
font-weight: 600;
|
|
|
flex-shrink: 0;
|
|
|
margin-top: var(--sp-1);
|
|
|
}
|
|
|
|
|
|
.pq-remove {
|
|
|
flex: none;
|
|
|
margin-left: var(--sp-2);
|
|
|
}
|
|
|
|
|
|
/* ===== 自定义空状态 ===== */
|
|
|
.table-empty {
|
|
|
padding: var(--sp-5) 0;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-1);
|
|
|
|
|
|
.empty-icon {
|
|
|
font-size: 28px;
|
|
|
color: var(--text-muted);
|
|
|
}
|
|
|
|
|
|
.empty-title {
|
|
|
margin: var(--sp-1) 0 0;
|
|
|
font-size: var(--fs-body);
|
|
|
font-weight: 600;
|
|
|
color: var(--text-body);
|
|
|
}
|
|
|
|
|
|
.empty-tip {
|
|
|
margin: 0;
|
|
|
font-size: var(--fs-tag);
|
|
|
color: var(--text-muted);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 底部固定操作条(白底 + 灰上边;按钮外观归 teacher-light.scss 全局接管)===== */
|
|
|
.editor-footer {
|
|
|
position: fixed;
|
|
|
bottom: 0;
|
|
|
left: 0;
|
|
|
right: 0;
|
|
|
height: 76px;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-3);
|
|
|
background: #ffffff;
|
|
|
border-top: 1px solid var(--border-color);
|
|
|
box-shadow: 0 -2px 12px rgba(17, 24, 39, 0.06);
|
|
|
z-index: 10;
|
|
|
|
|
|
:deep(.el-button) {
|
|
|
min-width: 112px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 编辑器步骤条(极简文字式:数字徽标 + 文字 + 箭头,左对齐跟随内容流)===== */
|
|
|
.prototype-nav {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: flex-start;
|
|
|
gap: var(--sp-4);
|
|
|
margin-bottom: var(--sp-4);
|
|
|
|
|
|
.step-node {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-2);
|
|
|
padding: 6px 2px;
|
|
|
border: 0;
|
|
|
background: transparent;
|
|
|
color: var(--text-body);
|
|
|
font-size: var(--fs-body);
|
|
|
font-weight: 400;
|
|
|
cursor: pointer;
|
|
|
transition: color 0.16s ease;
|
|
|
|
|
|
.step-num {
|
|
|
display: inline-grid;
|
|
|
place-items: center;
|
|
|
width: 22px;
|
|
|
height: 22px;
|
|
|
border-radius: 50%;
|
|
|
background: var(--border-color);
|
|
|
color: var(--text-body);
|
|
|
font-size: var(--fs-tag);
|
|
|
font-weight: 600;
|
|
|
transition: all 0.16s ease;
|
|
|
}
|
|
|
|
|
|
&:hover {
|
|
|
color: var(--text-strong);
|
|
|
|
|
|
.step-num {
|
|
|
background: var(--edu-primary-soft);
|
|
|
color: var(--edu-primary);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.is-active {
|
|
|
color: var(--edu-primary);
|
|
|
font-weight: 600;
|
|
|
|
|
|
.step-num {
|
|
|
background: var(--edu-primary);
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.step-arrow {
|
|
|
color: var(--text-muted);
|
|
|
font-size: var(--fs-card);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.prototype-base-panel {
|
|
|
max-width: 1280px;
|
|
|
padding: 0 0 var(--sp-4);
|
|
|
}
|
|
|
|
|
|
.settings-stage {
|
|
|
max-width: 1280px;
|
|
|
}
|
|
|
|
|
|
/* ===== 考试设置:独立设置卡片(替代旧 setting-row 列表)===== */
|
|
|
.settings-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
gap: var(--sp-3);
|
|
|
}
|
|
|
|
|
|
.setting-card {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: var(--sp-4);
|
|
|
padding: var(--sp-4) var(--sp-4);
|
|
|
border: 1px solid var(--border-color);
|
|
|
border-radius: var(--r-control);
|
|
|
background: #f9fafb;
|
|
|
transition: all 0.16s ease;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: var(--edu-primary);
|
|
|
background: #ffffff;
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.setting-card-wide {
|
|
|
grid-column: 1 / -1;
|
|
|
}
|
|
|
|
|
|
.setting-card-body {
|
|
|
display: grid;
|
|
|
gap: 4px;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
.setting-card-label {
|
|
|
font-size: var(--fs-body);
|
|
|
font-weight: 600;
|
|
|
color: var(--text-strong);
|
|
|
}
|
|
|
|
|
|
.setting-card-desc {
|
|
|
font-size: var(--fs-tag);
|
|
|
color: var(--text-muted);
|
|
|
}
|
|
|
|
|
|
@media (max-width: 760px) {
|
|
|
.editor-head {
|
|
|
display: grid;
|
|
|
gap: var(--sp-2);
|
|
|
}
|
|
|
|
|
|
.section-card {
|
|
|
padding: var(--sp-4);
|
|
|
}
|
|
|
|
|
|
.base-form {
|
|
|
:deep(.el-form-item) {
|
|
|
margin-bottom: var(--sp-3);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.form-row {
|
|
|
grid-template-columns: 1fr;
|
|
|
gap: var(--sp-3);
|
|
|
}
|
|
|
|
|
|
.score-cards {
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
}
|
|
|
|
|
|
.fixed-step-actions {
|
|
|
flex-wrap: wrap;
|
|
|
gap: var(--sp-2);
|
|
|
}
|
|
|
|
|
|
.fixed-step-arrow {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.fixed-step-button {
|
|
|
min-width: 138px;
|
|
|
}
|
|
|
|
|
|
.settings-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.type-tabs {
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.pq-card {
|
|
|
grid-template-columns: 28px 1fr;
|
|
|
}
|
|
|
|
|
|
.pq-score {
|
|
|
grid-column: 1 / -1;
|
|
|
justify-self: start;
|
|
|
}
|
|
|
|
|
|
.section-head {
|
|
|
align-items: flex-start;
|
|
|
flex-direction: column;
|
|
|
gap: var(--sp-3);
|
|
|
}
|
|
|
|
|
|
.import-tools {
|
|
|
justify-items: start;
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.import-hint {
|
|
|
max-width: none;
|
|
|
text-align: left;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<style lang="scss">
|
|
|
#app .teacherLayout .editor-footer {
|
|
|
left: calc(var(--teacher-sidebar-width, 180px) + 1px);
|
|
|
}
|
|
|
|
|
|
#app .teacherLayout.hideSidebar .editor-footer {
|
|
|
left: 64px;
|
|
|
}
|
|
|
|
|
|
#app .mobile.teacherLayout .editor-footer {
|
|
|
left: 0;
|
|
|
}
|
|
|
</style>
|