You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/components/StepOneQuestionBankPicker.vue

137 lines
6.3 KiB
Vue

<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>