|
|
|
|
@ -0,0 +1,136 @@
|
|
|
|
|
<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>
|