Compare commits

..

No commits in common. 'c23d623f5eeb8b07985d78181c76c02aba602d25' and '27dabb1d1c4dd68b0c7a450d6eb255bb71ae0e31' have entirely different histories.

@ -12,14 +12,9 @@
<span>STEP 01 · KNOWLEDGE CHECK</span> <span>STEP 01 · KNOWLEDGE CHECK</span>
<p>每次新增一题;填写题干、至少两个选项,并标记正确答案。</p> <p>每次新增一题;填写题干、至少两个选项,并标记正确答案。</p>
</div> </div>
<div class="choice-editor-actions"> <el-button type="primary" @click="addQuestion">新增题目</el-button>
<el-button v-if="loadQuestions" @click="bankPickerVisible = true">从题库选题</el-button>
<el-button type="primary" @click="addQuestion">新增题目</el-button>
</div>
</div> </div>
<StepOneQuestionBankPicker v-if="loadQuestions" v-model:visible="bankPickerVisible" :load-questions="loadQuestions" :existing="draft" @confirm="mergeFromBank" />
<el-empty v-if="!draft.length" description="暂未设置选择题,可点击“新增题目”开始配置。" /> <el-empty v-if="!draft.length" description="暂未设置选择题,可点击“新增题目”开始配置。" />
<div v-else class="choice-question-list"> <div v-else class="choice-question-list">
<article v-for="(question, questionIndex) in draft" :key="question.id" class="choice-question-card"> <article v-for="(question, questionIndex) in draft" :key="question.id" class="choice-question-card">
@ -54,19 +49,15 @@
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { QUESTION_OPTION_KEYS, createStepOneChoiceQuestion, parseStepOneChoiceQuestions } from "@/utils/stepOneChoiceQuestions"; import { QUESTION_OPTION_KEYS, createStepOneChoiceQuestion, parseStepOneChoiceQuestions } from "@/utils/stepOneChoiceQuestions";
import StepOneQuestionBankPicker from "@/views/components/StepOneQuestionBankPicker.vue";
const props = defineProps({ const props = defineProps({
visible: { type: Boolean, default: false }, visible: { type: Boolean, default: false },
modelValue: { type: Array, default: () => [] }, modelValue: { type: Array, default: () => [] },
taskName: { type: String, default: "" }, taskName: { type: String, default: "" },
// 传入则在弹窗内提供"从题库选题"入口(教师端/平台端各自注入接口)
loadQuestions: { type: Function, default: null },
}); });
const emit = defineEmits(["update:visible", "update:modelValue"]); const emit = defineEmits(["update:visible", "update:modelValue"]);
const draft = ref([]); const draft = ref([]);
const optionKeys = QUESTION_OPTION_KEYS; const optionKeys = QUESTION_OPTION_KEYS;
const bankPickerVisible = ref(false);
watch(() => props.visible, (opened) => { watch(() => props.visible, (opened) => {
if (opened) draft.value = structuredClone(parseStepOneChoiceQuestions(props.modelValue)); if (opened) draft.value = structuredClone(parseStepOneChoiceQuestions(props.modelValue));
@ -76,13 +67,6 @@ function addQuestion() {
draft.value.push(createStepOneChoiceQuestion(draft.value.length)); draft.value.push(createStepOneChoiceQuestion(draft.value.length));
} }
function mergeFromBank(questions) {
const existingTitles = new Set(draft.value.map((question) => String(question.title || "").trim()));
const added = questions.filter((question) => !existingTitles.has(question.title));
draft.value.push(...added);
if (added.length) ElMessage.success(`已从题库添加 ${added.length} 道题`);
}
function addOption(question) { function addOption(question) {
const key = optionKeys[question.options.length]; const key = optionKeys[question.options.length];
if (key) question.options.push({ key, content: "" }); if (key) question.options.push({ key, content: "" });
@ -130,16 +114,14 @@ function close(value = false) {
<style lang="scss"> <style lang="scss">
.step-one-question-editor { .step-one-question-editor {
.el-dialog { overflow: hidden; border: 1px solid #dbeafe; background: #ffffff; box-shadow: 0 24px 80px rgba(17, 24, 39, 0.18), inset 0 1px rgba(255, 255, 255, 0.14); } .el-dialog { overflow: hidden; border: 1px solid #cfe2f5; background: #ffffff; box-shadow: 0 24px 80px rgba(30, 58, 95, 0.46), inset 0 1px rgba(255, 255, 255, 0.14); }
.el-dialog__header { margin: 0; padding: 20px 24px; border-bottom: 1px solid #dbeafe; } .el-dialog__header { margin: 0; padding: 20px 24px; border-bottom: 1px solid #cfe2f5; }
.el-dialog__title { color: #1f2937; font-size: 18px; font-weight: 800; } .el-dialog__title { color: #1e3a5f; font-size: 18px; font-weight: 800; }
.el-dialog__body { max-height: min(66vh, 720px); overflow-y: auto; padding: 20px 24px; } .el-dialog__body { max-height: min(66vh, 720px); overflow-y: auto; padding: 20px 24px; }
.el-dialog__footer { padding: 16px 24px; border-top: 1px solid #dbeafe; } .el-dialog__footer { padding: 16px 24px; border-top: 1px solid #cfe2f5; }
} }
.choice-editor-intro { display:flex; align-items:center; justify-content:space-between; gap:20px; margin-bottom:18px; padding:15px 17px; border-left:3px solid #2563eb; background:linear-gradient(90deg,rgba(219, 234, 254, 0.6),transparent); } .choice-editor-intro { display:flex; align-items:center; justify-content:space-between; gap:20px; margin-bottom:18px; padding:15px 17px; border-left:3px solid #1e88e5; background:linear-gradient(90deg,rgba(187, 222, 251, 0.5),transparent); }
.choice-editor-actions { display:flex; flex-shrink:0; gap:10px; } .choice-editor-intro span { color:#1e88e5; font-size:12px; font-weight:800; letter-spacing:.12em; }.choice-editor-intro p { margin:5px 0 0; color:#5a7a9a; line-height:1.6; }
/* 只命中左上角的 STEP 01 标签,避免染蓝按钮内文字(el-button 文字渲染在 span 中) */ .choice-question-list { display:grid; gap:16px; }.choice-question-card { padding:16px; border:1px solid #cfe2f5; border-radius:10px; background:#f0f6fd; }.choice-question-card header { display:flex; align-items:center; gap:9px; margin-bottom:13px; color:#2c4a6a; }.choice-question-number { display:grid; place-items:center; width:28px; height:28px; border-radius:50%; color:#ffffff; background:#1e88e5; font-size:12px; font-weight:900; }.choice-question-remove { margin-left:auto; color:#ff92a5; cursor:pointer; border:0; background:transparent; }.choice-options { display:grid; gap:9px; margin:13px 0 7px; }.choice-option-row { display:grid; grid-template-columns:94px 28px minmax(0,1fr) 52px; align-items:center; gap:8px; }.choice-option-row .el-radio { margin-right:0; color:#2c4a6a; }.choice-option-key { display:grid; place-items:center; width:25px; height:25px; color:#1e88e5; font-weight:900; border:1px solid #cfe2f5; border-radius:5px; }
.choice-editor-intro > div > span { color:#2563eb; font-size:12px; font-weight:800; letter-spacing:.12em; }.choice-editor-intro p { margin:5px 0 0; color:#6b7280; line-height:1.6; }
.choice-question-list { display:grid; gap:16px; }.choice-question-card { padding:16px; border:1px solid #dbeafe; border-radius:10px; background:#eff6ff; }.choice-question-card header { display:flex; align-items:center; gap:9px; margin-bottom:13px; color:#374151; }.choice-question-number { display:grid; place-items:center; width:28px; height:28px; border-radius:50%; color:#ffffff; background:#2563eb; font-size:12px; font-weight:900; }.choice-question-remove { margin-left:auto; color:#ff92a5; cursor:pointer; border:0; background:transparent; }.choice-options { display:grid; gap:9px; margin:13px 0 7px; }.choice-option-row { display:grid; grid-template-columns:94px 28px minmax(0,1fr) 52px; align-items:center; gap:8px; }.choice-option-row .el-radio { margin-right:0; color:#374151; }.choice-option-key { display:grid; place-items:center; width:25px; height:25px; color:#2563eb; font-weight:900; border:1px solid #dbeafe; border-radius:5px; }
@media (max-width:640px) { .choice-editor-intro { align-items:flex-start; flex-direction:column; }.choice-option-row { grid-template-columns:78px 25px minmax(0,1fr); }.choice-option-row > .el-button { grid-column:3; justify-self:end; } } @media (max-width:640px) { .choice-editor-intro { align-items:flex-start; flex-direction:column; }.choice-option-row { grid-template-columns:78px 25px minmax(0,1fr); }.choice-option-row > .el-button { grid-column:3; justify-self:end; } }
</style> </style>

@ -1,136 +0,0 @@
<template>
<el-dialog :model-value="visible" title="从题库选择单选题" width="min(880px, 94vw)" append-to-body
class="step-one-bank-picker" @update:model-value="close">
<div class="bank-picker-intro">
<div>
<span>QUESTION BANK</span>
<p>勾选后确认,题目会复制为当前任务的第一步选择题;仅列出题库中的单选题,与现有题目重复的不再列出。</p>
</div>
<span class="bank-picker-count">已选 {{ checkedIds.size }} 题</span>
</div>
<div v-loading="loading" class="bank-question-list">
<article v-for="(question, index) in selectable" :key="question.id" role="button" tabindex="0"
:class="['bank-question-card', { 'is-checked': checkedIds.has(question.id) }]"
@click="toggle(question)" @keydown.enter.prevent="toggle(question)">
<span class="bank-check">{{ checkedIds.has(question.id) ? '✓' : String(index + 1).padStart(2, '0') }}</span>
<div class="bank-body">
<h4>{{ question.stem }}</h4>
<div class="bank-options">
<span v-for="option in question.options" :key="option.label"><b>{{ option.label }}.</b>{{
option.content }}</span>
</div>
</div>
</article>
<el-empty v-if="!loading && !selectable.length" description="题库暂无可添加的单选题" />
</div>
<template #footer>
<el-button @click="close(false)">取消</el-button>
<el-button type="primary" :disabled="!checkedIds.size" @click="confirmSelection">
添加 {{ checkedIds.size }} 道题
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { computed, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { QUESTION_OPTION_KEYS } from "@/utils/stepOneChoiceQuestions";
const props = defineProps({
visible: { type: Boolean, default: false },
// 返回题库单选题列表的异步函数(教师端/平台端各自注入接口)
loadQuestions: { type: Function, required: true },
// 当前已配置的题目,用于按题干去重
existing: { type: Array, default: () => [] },
});
const emit = defineEmits(["update:visible", "confirm"]);
const loading = ref(false);
const questions = ref([]);
const checkedIds = ref(new Set());
const existingTitles = computed(() => new Set((props.existing || []).map((q) => String(q.title || "").trim())));
// 已存在于任务配置中的题不再展示,避免重复添加
const selectable = computed(() => questions.value.filter((q) => !existingTitles.value.has(String(q.stem || "").trim())));
watch(() => props.visible, (opened) => {
if (opened) load();
}, { immediate: true });
async function load() {
checkedIds.value = new Set();
loading.value = true;
try {
questions.value = (await props.loadQuestions()) || [];
} catch {
questions.value = [];
ElMessage.error("题库加载失败,请稍后重试");
} finally {
loading.value = false;
}
}
function toggle(question) {
const next = new Set(checkedIds.value);
next.has(question.id) ? next.delete(question.id) : next.add(question.id);
checkedIds.value = next;
}
function convert(question, index) {
const options = (question.options || [])
.map((option) => ({ key: option.label, content: String(option.content || "").trim() }))
.filter((option) => option.key && option.content);
const keys = options.map((option) => option.key);
const rawAnswer = Array.isArray(question.answer) ? question.answer[0] : question.answer;
const answer = keys.includes(rawAnswer) ? rawAnswer : keys[0] || QUESTION_OPTION_KEYS[0];
return {
id: `step-one-question-${Date.now()}-${index + 1}`,
title: String(question.stem || "").trim(),
options,
answer,
};
}
function confirmSelection() {
const picked = questions.value.filter((q) => checkedIds.value.has(q.id));
if (!picked.length) return;
emit("confirm", picked.map(convert));
close();
}
function close(value = false) {
emit("update:visible", value);
}
</script>
<style lang="scss">
.step-one-bank-picker {
.el-dialog { overflow: hidden; border: 1px solid #dbeafe; background: #ffffff; box-shadow: 0 24px 80px rgba(17, 24, 39, 0.18); }
.el-dialog__header { margin: 0; padding: 20px 24px; border-bottom: 1px solid #dbeafe; }
.el-dialog__title { color: #1f2937; font-size: 18px; font-weight: 800; }
.el-dialog__body { max-height: min(64vh, 700px); overflow-y: auto; padding: 20px 24px; }
.el-dialog__footer { padding: 16px 24px; border-top: 1px solid #dbeafe; }
}
.bank-picker-intro { display:flex; align-items:center; justify-content:space-between; gap:20px; margin-bottom:18px; padding:15px 17px; border-left:3px solid #2563eb; background:linear-gradient(90deg,rgba(219,234,254,.6),transparent); }
/* 只命中左上角标签,避免染蓝按钮内文字 */
.bank-picker-intro > div > span { color:#2563eb; font-size:12px; font-weight:800; letter-spacing:.12em; }
.bank-picker-intro p { margin:5px 0 0; color:#6b7280; line-height:1.6; }
.bank-picker-count { flex-shrink:0; padding:6px 14px; color:#2563eb; font-size:13px; font-weight:800; border:1px solid #dbeafe; border-radius:16px; background:#eff6ff; }
.bank-question-list { display:grid; gap:12px; min-height:120px; }
.bank-question-card { display:flex; gap:12px; padding:14px 16px; border:1px solid #dbeafe; border-radius:10px; background:#ffffff; cursor:pointer; transition:all .16s ease;
&:hover { border-color:#2563eb; background:#f6fafe; }
&.is-checked { border-color:#2563eb; background:#eaf3fd; }
}
.bank-check { flex:0 0 auto; display:grid; place-items:center; width:28px; height:28px; margin-top:2px; color:#2563eb; font-size:12px; font-weight:900; border:1px solid #dbeafe; border-radius:50%; background:#eff6ff; }
.bank-question-card.is-checked .bank-check { color:#ffffff; background:#2563eb; border-color:#2563eb; }
.bank-body { min-width:0;
h4 { margin:0 0 8px; color:#1f2937; font-size:14px; line-height:1.6; }
}
.bank-options { display:grid; gap:4px;
span { color:#6b7280; font-size:13px; line-height:1.55; }
b { margin-right:6px; color:#2563eb; font-weight:700; }
}
</style>

@ -3,7 +3,7 @@
<header class="page-head"><div><h1>题库管理</h1><p>维护平台公共题库,供教师组卷使用</p></div><div class="actions"><el-button type="primary" :icon="Plus" @click="openEditor()">添加题目</el-button><el-button type="success" :icon="Upload" :loading="loading" @click="pickImport">批量导入</el-button><el-dropdown trigger="click" @command="downloadTemplate"><el-button type="success" :icon="Download">下载模板<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="word">Word 导入模板</el-dropdown-item><el-dropdown-item command="excel">Excel 导入模板</el-dropdown-item></el-dropdown-menu></template></el-dropdown><el-button type="primary" @click="openAi">AI出题</el-button></div></header> <header class="page-head"><div><h1>题库管理</h1><p>维护平台公共题库,供教师组卷使用</p></div><div class="actions"><el-button type="primary" :icon="Plus" @click="openEditor()">添加题目</el-button><el-button type="success" :icon="Upload" :loading="loading" @click="pickImport">批量导入</el-button><el-dropdown trigger="click" @command="downloadTemplate"><el-button type="success" :icon="Download">下载模板<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="word">Word 导入模板</el-dropdown-item><el-dropdown-item command="excel">Excel 导入模板</el-dropdown-item></el-dropdown-menu></template></el-dropdown><el-button type="primary" @click="openAi">AI出题</el-button></div></header>
<section class="toolbar"><el-select v-model="query.type" clearable placeholder="题型:请选择" @change="load"><el-option v-for="item in types" :key="item.value" :label="item.label" :value="item.value" /></el-select><el-select v-model="query.taskKey" clearable placeholder="归属项目:请选择" @change="load"><el-option v-for="item in projects" :key="item.taskKey" :label="projectLabel(item)" :value="item.taskKey" /></el-select><el-input v-model="query.keyword" placeholder="请输入题干关键词" clearable @keyup.enter="load" /><el-button type="primary" :icon="Search" @click="load">搜索</el-button><input ref="fileInput" type="file" accept=".docx,.xlsx,.xls" hidden @change="importFile" /></section> <section class="toolbar"><el-select v-model="query.type" clearable placeholder="题型:请选择" @change="load"><el-option v-for="item in types" :key="item.value" :label="item.label" :value="item.value" /></el-select><el-select v-model="query.taskKey" clearable placeholder="归属项目:请选择" @change="load"><el-option v-for="item in projects" :key="item.taskKey" :label="projectLabel(item)" :value="item.taskKey" /></el-select><el-input v-model="query.keyword" placeholder="请输入题干关键词" clearable @keyup.enter="load" /><el-button type="primary" :icon="Search" @click="load">搜索</el-button><input ref="fileInput" type="file" accept=".docx,.xlsx,.xls" hidden @change="importFile" /></section>
<el-table v-loading="loading" :data="questions" class="data-table"><el-table-column type="index" label="序号" width="78" /><el-table-column label="题干" min-width="420" show-overflow-tooltip><template #default="{ row }"><span>{{ plainHtml(row.stem) }}</span></template></el-table-column><el-table-column label="题型" width="120"><template #default="{ row }"><el-tag>{{ typeName[row.type] }}</el-tag></template></el-table-column><el-table-column label="题目来源" width="140"><template #default><el-tag type="success">平台题库</el-tag></template></el-table-column><el-table-column label="操作" width="190" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openEditor(row)">编辑</el-button><el-button link type="primary" @click="openEditor(row)">查看</el-button><el-button link type="danger" @click="remove(row)">删除</el-button></template></el-table-column><template #empty><el-empty description="暂无题目" /></template></el-table> <el-table v-loading="loading" :data="questions" class="data-table"><el-table-column type="index" label="序号" width="78" /><el-table-column label="题干" min-width="420" show-overflow-tooltip><template #default="{ row }"><span>{{ plainHtml(row.stem) }}</span></template></el-table-column><el-table-column label="题型" width="120"><template #default="{ row }"><el-tag>{{ typeName[row.type] }}</el-tag></template></el-table-column><el-table-column label="题目来源" width="140"><template #default><el-tag type="success">平台题库</el-tag></template></el-table-column><el-table-column label="操作" width="190" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openEditor(row)">编辑</el-button><el-button link type="primary" @click="openEditor(row)">查看</el-button><el-button link type="danger" @click="remove(row)">删除</el-button></template></el-table-column><template #empty><el-empty description="暂无题目" /></template></el-table>
<p class="bank-note">说明:平台题库为系统默认题库,所有教师可见;教师仅可查看平台题目,编辑删除自己新增的题目。</p> <p class="prototype-note">说明:简答题批量导入不支持文件上传,只导入题干和参考答案。管理端进行内置题库管理,题库管理功能在管理端也放一个页面。老师端只能查看管理端的题目,只能编辑删除自己新增的题目。智云平台上有AI出题功能,可以复用过来,点击AI出题按钮跳转到AI出题界面,支持导出试题</p>
<el-dialog v-model="editorVisible" :title="form.id ? '编辑题目' : '新增题目'" width="min(1180px, 94vw)" top="4vh" class="question-editor-dialog" destroy-on-close> <el-dialog v-model="editorVisible" :title="form.id ? '编辑题目' : '新增题目'" width="min(1180px, 94vw)" top="4vh" class="question-editor-dialog" destroy-on-close>
<div class="editor-shell"><div class="editor-project"><span>归属项目</span><el-select v-model="form.taskKey" placeholder="请选择归属项目"><el-option v-for="item in projects" :key="item.taskKey" :label="projectLabel(item)" :value="item.taskKey" /></el-select></div> <div class="editor-shell"><div class="editor-project"><span>归属项目</span><el-select v-model="form.taskKey" placeholder="请选择归属项目"><el-option v-for="item in projects" :key="item.taskKey" :label="projectLabel(item)" :value="item.taskKey" /></el-select></div>
@ -46,10 +46,10 @@ function displayAnswer(value) { return Array.isArray(value) ? value.join("、")
</script> </script>
<style scoped> <style scoped>
.question-bank { min-height: 100%; color: #1f2937; background: transparent; } .question-bank { min-height: 100%; color: #eaf8ff; background: linear-gradient(145deg, #061b2b, #07111c); }
.page-head { display: flex; align-items: center; justify-content: space-between; gap: 24px; margin-bottom: 18px; } .page-head { display: flex; align-items: center; justify-content: space-between; gap: 24px; margin-bottom: 18px; }
.page-head h1 { margin: 0; font-size: 22px; font-weight: 700; color: #1f2937; }.page-head p { margin: 7px 0 0; color: #6b7280; font-size: 13px; }.actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; } .page-head h1 { margin: 0; font-size: 26px; }.page-head p { margin: 7px 0 0; color: #9fc2d8; }.actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; }
.toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding: 14px; margin-bottom: 16px; border: 1px solid #e5e7eb; border-radius: 10px; background: #ffffff; }.toolbar .el-select { width: 210px; }.toolbar .el-input { width: min(300px, 30vw); } .toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding: 14px; margin-bottom: 16px; border: 1px solid rgba(69, 185, 236, .3); border-radius: 10px; background: rgba(2, 18, 31, .6); }.toolbar .el-select { width: 210px; }.toolbar .el-input { width: min(300px, 30vw); }
.data-table { background: #ffffff; border-radius: 10px; }.bank-note { margin: 14px auto 0; max-width: 1180px; color: #6b7280; font-size: 13px; line-height: 1.8; text-align: center; } .data-table { background: rgba(2, 18, 31, .72); }.prototype-note { margin: 14px auto 0; max-width: 1180px; color: #ff5e67; font-size: 15px; line-height: 1.8; text-align: center; }
.editor-shell { padding: 4px 8px 12px; color: #243746; }.editor-project { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; }.editor-project > span { width: 62px; color: #526979; }.editor-project .el-select { width: 420px; }.type-switch { display: flex; align-items: center; gap: 10px; margin: 4px 0 22px; font-size: 16px; }.rich-row, .option-block { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 12px; margin-bottom: 22px; }.rich-row > label, .option-block > label, .tf-block > label { padding-top: 10px; color: #526979; font-size: 15px; text-align: right; }.option-list { display: grid; gap: 12px; }.option-row { display: grid; grid-template-columns: 68px minmax(0, 1fr); align-items: start; gap: 8px; }.answer-control { padding-top: 12px; }.tf-block { display: flex; align-items: center; gap: 24px; margin: 18px 0 30px; }.tf-block .el-radio-group { display: flex; gap: 28px; }.material-row { display: flex; align-items: center; gap: 14px; margin: 6px 0 22px 102px; }.material-name { color: #5c7585; }.preview-alert { margin-bottom: 12px; }.preview-item { padding: 10px 4px; border-bottom: 1px solid #24465a; line-height: 1.7; }.editor-shell :deep(.ql-container) { min-height: 88px; font-size: 15px; }.editor-shell :deep(.ql-editor) { min-height: 74px; }.editor-shell :deep(.ql-toolbar) { background: #fff; }.editor-shell :deep(.ql-editor.ql-blank::before) { color: #a6b0b8; font-style: normal; } .editor-shell { padding: 4px 8px 12px; color: #243746; }.editor-project { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; }.editor-project > span { width: 62px; color: #526979; }.editor-project .el-select { width: 420px; }.type-switch { display: flex; align-items: center; gap: 10px; margin: 4px 0 22px; font-size: 16px; }.rich-row, .option-block { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 12px; margin-bottom: 22px; }.rich-row > label, .option-block > label, .tf-block > label { padding-top: 10px; color: #526979; font-size: 15px; text-align: right; }.option-list { display: grid; gap: 12px; }.option-row { display: grid; grid-template-columns: 68px minmax(0, 1fr); align-items: start; gap: 8px; }.answer-control { padding-top: 12px; }.tf-block { display: flex; align-items: center; gap: 24px; margin: 18px 0 30px; }.tf-block .el-radio-group { display: flex; gap: 28px; }.material-row { display: flex; align-items: center; gap: 14px; margin: 6px 0 22px 102px; }.material-name { color: #5c7585; }.preview-alert { margin-bottom: 12px; }.preview-item { padding: 10px 4px; border-bottom: 1px solid #24465a; line-height: 1.7; }.editor-shell :deep(.ql-container) { min-height: 88px; font-size: 15px; }.editor-shell :deep(.ql-editor) { min-height: 74px; }.editor-shell :deep(.ql-toolbar) { background: #fff; }.editor-shell :deep(.ql-editor.ql-blank::before) { color: #a6b0b8; font-style: normal; }
</style> </style>

@ -394,10 +394,6 @@
</div> </div>
</div> </div>
<!-- 第一步选择题:编辑入口固定在第一个步骤行旁,仅三个白名单任务显示 -->
<StepOneQuestionBankPicker v-model:visible="bankPickerVisible" :load-questions="loadBankQuestions" :existing="taskForm.stepOneQuestions" @confirm="handleBankConfirm" />
<StepOneChoiceQuestionEditor v-model:visible="choiceEditorVisible" v-model="taskForm.stepOneQuestions" :task-name="taskForm.title" :load-questions="loadBankQuestions" />
<el-form-item label="步骤名称:"> <el-form-item label="步骤名称:">
<div class="step-list"> <div class="step-list">
<div v-for="(step, index) in taskForm.steps" :key="step.id" class="step-item"> <div v-for="(step, index) in taskForm.steps" :key="step.id" class="step-item">
@ -409,12 +405,6 @@
<div class="step-content"> <div class="step-content">
<span class="step-number">{{ index + 1 }}</span> <span class="step-number">{{ index + 1 }}</span>
<el-input v-model="step.name" placeholder="步骤名称" /> <el-input v-model="step.name" placeholder="步骤名称" />
<template v-if="index === 0 && supportsStepOneChoice(taskForm.taskKey)">
<el-button v-if="!isReadonly" type="primary" class="step-question-btn" @click="choiceEditorVisible = true">
编辑题目{{ stepOneQuestionCount ? `(${stepOneQuestionCount}题)` : "" }}
</el-button>
<span v-else class="step-question-tag">已配置 {{ stepOneQuestionCount }} 道题</span>
</template>
<button <button
v-if="!isReadonly && taskForm.steps.length > 1" v-if="!isReadonly && taskForm.steps.length > 1"
type="button" type="button"
@ -466,10 +456,6 @@ import {
updateTrainingTask, updateTrainingTask,
uploadTrainingTaskMaterial, uploadTrainingTaskMaterial,
} from "@/api/trainingTask"; } from "@/api/trainingTask";
import StepOneChoiceQuestionEditor from "@/views/components/StepOneChoiceQuestionEditor.vue";
import StepOneQuestionBankPicker from "@/views/components/StepOneQuestionBankPicker.vue";
import { parseStepOneChoiceQuestions, supportsStepOneChoiceQuestions } from "@/utils/stepOneChoiceQuestions";
import { getPlatformQuestions } from "@/api/exam";
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
@ -478,8 +464,6 @@ const keyword = ref("");
const loading = ref(false); const loading = ref(false);
const saving = ref(false); const saving = ref(false);
const uploadingField = ref(""); const uploadingField = ref("");
const choiceEditorVisible = ref(false);
const bankPickerVisible = ref(false);
// 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段 // 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段
const materialSlots = [ const materialSlots = [
{ key: "material", label: "案例资料" }, { key: "material", label: "案例资料" },
@ -493,7 +477,6 @@ const dialogMode = ref("view");
const currentTaskId = ref(""); const currentTaskId = ref("");
const taskForm = reactive(createEmptyTask()); const taskForm = reactive(createEmptyTask());
const isReadonly = computed(() => dialogMode.value === "view"); const isReadonly = computed(() => dialogMode.value === "view");
const stepOneQuestionCount = computed(() => taskForm.stepOneQuestions?.length || 0);
const enabledCount = computed(() => tasks.value.filter(t => t.enabled).length); const enabledCount = computed(() => tasks.value.filter(t => t.enabled).length);
@ -581,28 +564,12 @@ function createEmptyTask() {
guideManualUrl: "", guideManualUrl: "",
aiManualName: "", aiManualName: "",
aiManualUrl: "", aiManualUrl: "",
stepOneQuestions: [],
steps: createBuiltinSteps(), steps: createBuiltinSteps(),
sort: 0, sort: 0,
enabled: true, enabled: true,
}; };
} }
function supportsStepOneChoice(taskKey) {
return supportsStepOneChoiceQuestions(taskKey);
}
// 平台题库单选题(平台管理员维护默认任务,题源为平台题库)
async function loadBankQuestions() {
const res = await getPlatformQuestions({ type: "SINGLE" });
return res?.data || [];
}
function handleBankConfirm(questions) {
taskForm.stepOneQuestions = [...(taskForm.stepOneQuestions || []), ...questions];
proxy?.$modal?.msgSuccess(`已从题库添加 ${questions.length} 道题,点击“保存配置”后生效`);
}
function hasMaterial(slotKey) { function hasMaterial(slotKey) {
return isRealMaterial(taskForm[`${slotKey}Name`], taskForm[`${slotKey}Url`]); return isRealMaterial(taskForm[`${slotKey}Name`], taskForm[`${slotKey}Url`]);
} }
@ -632,7 +599,6 @@ function normalizeTask(task) {
guideManualUrl: guideManual.url, guideManualUrl: guideManual.url,
aiManualName: aiManual.name, aiManualName: aiManual.name,
aiManualUrl: aiManual.url, aiManualUrl: aiManual.url,
stepOneQuestions: parseStepOneChoiceQuestions(task.stepOneQuestions),
steps: normalizeSteps(parseArray(task.steps, createBuiltinSteps())), steps: normalizeSteps(parseArray(task.steps, createBuiltinSteps())),
sort: task.sort || 0, sort: task.sort || 0,
enabled: task.enabled !== false, enabled: task.enabled !== false,
@ -805,7 +771,6 @@ function buildPayload() {
guideManualUrl: taskForm.guideManualUrl, guideManualUrl: taskForm.guideManualUrl,
aiManualName: taskForm.aiManualName, aiManualName: taskForm.aiManualName,
aiManualUrl: taskForm.aiManualUrl, aiManualUrl: taskForm.aiManualUrl,
stepOneQuestions: JSON.stringify(taskForm.stepOneQuestions || []),
sort: taskForm.sort, sort: taskForm.sort,
enabled: taskForm.enabled, enabled: taskForm.enabled,
}; };
@ -1811,19 +1776,6 @@ function buildPayload() {
border-radius: 8px; border-radius: 8px;
} }
.step-question-btn {
flex-shrink: 0;
height: 32px;
font-weight: 600;
}
.step-question-tag {
flex-shrink: 0;
color: rgba(200, 230, 255, 0.6);
font-size: 12px;
white-space: nowrap;
}
.el-input { .el-input {
flex: 1; flex: 1;

@ -344,10 +344,6 @@
</div> </div>
</div> </div>
<!-- 第一步选择题:编辑入口固定在第一个步骤行旁,仅三个白名单任务显示 -->
<StepOneQuestionBankPicker v-model:visible="bankPickerVisible" :load-questions="loadBankQuestions" :existing="taskForm.stepOneQuestions" @confirm="handleBankConfirm" />
<StepOneChoiceQuestionEditor v-model:visible="choiceEditorVisible" v-model="taskForm.stepOneQuestions" :task-name="taskForm.title" :load-questions="loadBankQuestions" />
<!-- 步骤数量与顺序固定,仅允许修改名称 --> <!-- 步骤数量与顺序固定,仅允许修改名称 -->
<el-form-item label="步骤名称:"> <el-form-item label="步骤名称:">
<div class="step-list"> <div class="step-list">
@ -361,13 +357,6 @@
<div class="step-content"> <div class="step-content">
<span class="step-number">{{ index + 1 }}</span> <span class="step-number">{{ index + 1 }}</span>
<el-input v-model="step.name" placeholder="步骤名称" /> <el-input v-model="step.name" placeholder="步骤名称" />
<template v-if="index === 0 && supportsStepOneChoice(taskForm.taskKey)">
<el-button v-if="!isReadonly" type="primary" class="step-question-btn"
@click="choiceEditorVisible = true">
编辑题目{{ stepOneQuestionCount ? `(${stepOneQuestionCount}题)` : "" }}
</el-button>
<span v-else class="step-question-tag">已配置 {{ stepOneQuestionCount }} 道题</span>
</template>
</div> </div>
</div> </div>
</div> </div>
@ -408,10 +397,6 @@ import { listTrainingTasks, listClassTrainingTasks, saveClassTrainingTask, resto
import { createStudentDemoSession } from "@/api/studentDemo" import { createStudentDemoSession } from "@/api/studentDemo"
import * as indexApi from "@/api/teacher" import * as indexApi from "@/api/teacher"
import useUserStore from "@/store/modules/user" import useUserStore from "@/store/modules/user"
import StepOneChoiceQuestionEditor from "@/views/components/StepOneChoiceQuestionEditor.vue"
import StepOneQuestionBankPicker from "@/views/components/StepOneQuestionBankPicker.vue"
import { parseStepOneChoiceQuestions, supportsStepOneChoiceQuestions } from "@/utils/stepOneChoiceQuestions"
import { getQuestions } from "@/api/exam"
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const userStore = useUserStore() const userStore = useUserStore()
@ -423,8 +408,6 @@ const classesLoading = ref(false)
const saving = ref(false) const saving = ref(false)
const restoring = ref(false) const restoring = ref(false)
const uploadingField = ref("") const uploadingField = ref("")
const choiceEditorVisible = ref(false)
const bankPickerVisible = ref(false)
// 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段 // 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段
const materialSlots = [ const materialSlots = [
{ key: "material", label: "案例资料" }, { key: "material", label: "案例资料" },
@ -440,7 +423,6 @@ const dialogMode = ref("view")
const taskForm = reactive(createEmptyTask()) const taskForm = reactive(createEmptyTask())
const currentUserId = computed(() => userStore.userInfo.userId) const currentUserId = computed(() => userStore.userInfo.userId)
const isReadonly = computed(() => dialogMode.value === "view") const isReadonly = computed(() => dialogMode.value === "view")
const stepOneQuestionCount = computed(() => taskForm.stepOneQuestions?.length || 0)
const projects = computed(() => [...new Set(tasks.value.map((task) => task.project).filter(Boolean))]) const projects = computed(() => [...new Set(tasks.value.map((task) => task.project).filter(Boolean))])
const projectOptions = computed(() => { const projectOptions = computed(() => {
return projects.value.map((project) => ({ return projects.value.map((project) => ({
@ -584,28 +566,12 @@ function createEmptyTask() {
guideManualUrl: "", guideManualUrl: "",
aiManualName: "", aiManualName: "",
aiManualUrl: "", aiManualUrl: "",
stepOneQuestions: [],
steps: createBuiltinSteps(), steps: createBuiltinSteps(),
sort: 0, sort: 0,
enabled: true, enabled: true,
} }
} }
function supportsStepOneChoice(taskKey) {
return supportsStepOneChoiceQuestions(taskKey)
}
// 本校题库单选题(平台题 + 本校教师题,按学校过滤)
async function loadBankQuestions() {
const res = await getQuestions({ scope: "school", type: "SINGLE" })
return res?.data || []
}
function handleBankConfirm(questions) {
taskForm.stepOneQuestions = [...(taskForm.stepOneQuestions || []), ...questions]
proxy?.$modal?.msgSuccess(`已从题库添加 ${questions.length} 道题,点击“保存配置”后生效`)
}
function hasMaterial(slotKey) { function hasMaterial(slotKey) {
return isRealMaterial(taskForm[`${slotKey}Name`], taskForm[`${slotKey}Url`]) return isRealMaterial(taskForm[`${slotKey}Name`], taskForm[`${slotKey}Url`])
} }
@ -635,7 +601,6 @@ function normalizeTask(task) {
guideManualUrl: guideManual.url, guideManualUrl: guideManual.url,
aiManualName: aiManual.name, aiManualName: aiManual.name,
aiManualUrl: aiManual.url, aiManualUrl: aiManual.url,
stepOneQuestions: parseStepOneChoiceQuestions(task.stepOneQuestions),
steps: normalizeSteps(parseArray(task.steps, createBuiltinSteps())), steps: normalizeSteps(parseArray(task.steps, createBuiltinSteps())),
sort: task.sort || 0, sort: task.sort || 0,
enabled: task.enabled !== false, enabled: task.enabled !== false,
@ -868,7 +833,6 @@ function buildPayload() {
guideManualUrl: taskForm.guideManualUrl, guideManualUrl: taskForm.guideManualUrl,
aiManualName: taskForm.aiManualName, aiManualName: taskForm.aiManualName,
aiManualUrl: taskForm.aiManualUrl, aiManualUrl: taskForm.aiManualUrl,
stepOneQuestions: JSON.stringify(taskForm.stepOneQuestions || []),
sort: taskForm.sort, sort: taskForm.sort,
enabled: taskForm.enabled, enabled: taskForm.enabled,
} }
@ -1617,20 +1581,6 @@ function buildPayload() {
.el-input { .el-input {
flex: 1; flex: 1;
} }
.step-question-btn {
flex-shrink: 0;
height: 32px;
font-size: var(--fs-body);
font-weight: 600;
}
.step-question-tag {
flex-shrink: 0;
color: var(--text-muted);
font-size: var(--fs-tag);
white-space: nowrap;
}
} }
.drawer-footer { .drawer-footer {

Loading…
Cancel
Save