feat: 更新侧边栏和二级菜单逻辑,新增题库选择功能

This commit is contained in:
yuk255 2025-08-22 21:17:10 +08:00
parent 2d8339ed4e
commit 59faaa25cb
6 changed files with 641 additions and 37 deletions

View File

@ -278,7 +278,7 @@ const confirmBatchSet = () => {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 12px;
margin-top: 20px;
}
/* 滚动条样式 */

View File

@ -1,6 +1,10 @@
<template>
<n-modal v-model:show="showModal" class="exam-settings-modal" preset="dialog" title="试卷设置" :mask-closable="false"
:closable="true" :style="{ width: '1000px' }">
<n-modal v-model:show="showModal" class="exam-settings-modal" preset="card" :mask-closable="false"
:closable="false" :style="{ width: '1000px' }">
<div class="header">
<span class="header-title">试卷设置</span>
</div>
<n-divider />
<div class="exam-settings-content">
<!-- 试卷名称 -->
@ -258,12 +262,10 @@
</div>
<!-- 底部按钮 -->
<template #action>
<div class="modal-actions">
<n-button @click="cancelSettings">取消</n-button>
<n-button type="primary" @click="confirmSettings">确定</n-button>
</div>
</template>
<div class="modal-actions">
<n-button @click="cancelSettings">取消</n-button>
<n-button type="primary" @click="confirmSettings">确定</n-button>
</div>
</n-modal>
</template>
@ -454,6 +456,12 @@ const confirmSettings = () => {
--n-color: #ffffff;
}
.header-title{
color: #000;
font-weight: 400;
font-size: 20px;
}
.exam-settings-content {
max-height: 800px;
overflow-y: auto;
@ -713,6 +721,7 @@ const confirmSettings = () => {
}
.modal-actions {
margin-top: 20px;
display: flex;
justify-content: flex-end;
gap: 12px;

View File

@ -0,0 +1,459 @@
<template>
<n-modal
v-model:show="showModal"
class="question-bank-modal"
preset="card"
:mask-closable="false"
:closable="false"
:style="{ width: '1200px' }">
<div class="header">
<span class="header-title">题库</span>
</div>
<n-divider />
<div class="question-bank-content">
<!-- 筛选条件 -->
<div class="filter-section">
<div class="filter-row">
<div class="filter-item">
<label>试题分类</label>
<n-select
v-model:value="filters.category"
placeholder="全部"
:options="categoryOptions"
style="width: 150px"
/>
</div>
<div class="filter-item">
<label>试题难度</label>
<n-select
v-model:value="filters.difficulty"
placeholder="全部"
:options="difficultyOptions"
style="width: 150px"
/>
</div>
<div class="filter-item">
<label>试题题型</label>
<n-select
v-model:value="filters.type"
placeholder="全部"
:options="typeOptions"
style="width: 150px"
/>
</div>
<div class="filter-actions">
<span class="tip">已全部加载{{ pagination.itemCount }}试题</span>
<n-button type="primary" @click="addNewQuestion">
<template #icon>
<n-icon>
<Add />
</n-icon>
</template>
导入试题
</n-button>
</div>
</div>
</div>
<!-- 题目列表 -->
<div class="question-list-section">
<n-data-table
ref="tableRef"
:columns="columns"
:data="questionList"
:pagination="pagination"
:loading="loading"
:row-key="(row: any) => row.id"
:checked-row-keys="selectedRowKeys"
@update:checked-row-keys="handleCheck"
striped
/>
</div>
<!-- 已选择题目统计 -->
<div class="selected-info">
<span>已选择{{ selectedRowKeys.length }}道题目</span>
</div>
</div>
<!-- 底部按钮 -->
<div class="modal-actions">
<n-button @click="cancelSelection">取消</n-button>
<n-button type="primary" @click="confirmSelection" :disabled="selectedRowKeys.length === 0">
确定
</n-button>
</div>
</n-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
import { createDiscreteApi } from 'naive-ui';
import { Add } from '@vicons/ionicons5';
// message API
const { message } = createDiscreteApi(['message']);
//
interface QuestionItem {
id: string;
number: number;
title: string;
type: string;
difficulty: string;
category: string;
score: number;
creator: string;
createTime: string;
//
content?: any;
}
// Props
interface Props {
visible: boolean;
questionType?: string; //
}
// Emits
interface Emits {
(e: 'update:visible', value: boolean): void;
(e: 'confirm', questions: QuestionItem[]): void;
(e: 'cancel'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
//
const showModal = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
//
const filters = ref({
category: '',
difficulty: '',
type: props.questionType || '',
keyword: ''
});
//
const categoryOptions = ref([
{ label: '全部', value: '' },
{ label: '计算机基础', value: 'computer' },
{ label: '数学', value: 'math' },
{ label: '英语', value: 'english' }
]);
const difficultyOptions = ref([
{ label: '全部', value: '' },
{ label: '易', value: 'easy' },
{ label: '中', value: 'medium' },
{ label: '难', value: 'hard' }
]);
const typeOptions = ref([
{ label: '全部', value: '' },
{ label: '单选题', value: 'single_choice' },
{ label: '多选题', value: 'multiple_choice' },
{ label: '判断题', value: 'true_false' },
{ label: '填空题', value: 'fill_blank' },
{ label: '简答题', value: 'short_answer' }
]);
//
const loading = ref(false);
const selectedRowKeys = ref<string[]>([]);
// ()
const questionList = ref<QuestionItem[]>([]);
//
const generateMockData = () => {
const mockData: QuestionItem[] = [];
const types = ['单选题', '多选题', '判断题', '填空题', '简答题'];
const difficulties = ['易', '中', '难'];
const categories = ['计算机基础', '数学', '英语'];
for (let i = 1; i <= 15; i++) {
mockData.push({
id: `question_${i}`,
number: i,
title: `在数据库的三级模式结构中,内模式有...`,
type: types[Math.floor(Math.random() * types.length)],
difficulty: difficulties[Math.floor(Math.random() * difficulties.length)],
category: categories[Math.floor(Math.random() * categories.length)],
score: 10,
creator: '王建国',
createTime: '2025.08.20 09:20'
});
}
return mockData;
};
//
const columns = [
{
type: 'selection'
},
{
title: '序号',
key: 'number',
width: 80,
align: 'center' as const
},
{
title: '试题内容',
key: 'title',
width: 300,
ellipsis: {
tooltip: true
}
},
{
title: '题型',
key: 'type',
width: 100,
align: 'center' as const
},
{
title: '分数',
key: 'score',
width: 80,
align: 'center' as const
},
{
title: '难度',
key: 'difficulty',
width: 80,
align: 'center' as const
},
{
title: '分值',
key: 'score',
width: 80,
align: 'center' as const
},
{
title: '创建人',
key: 'creator',
width: 100,
align: 'center' as const
},
{
title: '创建时间',
key: 'createTime',
width: 150,
align: 'center' as const
}
];
//
const pagination = ref({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
itemCount: 0,
onChange: (page: number) => {
pagination.value.page = page;
loadQuestions();
},
onUpdatePageSize: (pageSize: number) => {
pagination.value.pageSize = pageSize;
pagination.value.page = 1;
loadQuestions();
},
prefix: ({ itemCount }: { itemCount: number }) => `${itemCount}试题`
});
//
const handleCheck = (rowKeys: string[]) => {
selectedRowKeys.value = rowKeys;
};
//
const loadQuestions = async () => {
loading.value = true;
try {
// API
await new Promise(resolve => setTimeout(resolve, 500));
questionList.value = generateMockData();
pagination.value.itemCount = questionList.value.length;
} catch (error) {
message.error('加载题目失败');
} finally {
loading.value = false;
}
};
//
const addNewQuestion = () => {
message.info('导入试题功能待开发');
};
//
const cancelSelection = () => {
selectedRowKeys.value = [];
emit('cancel');
};
//
const confirmSelection = () => {
const selectedQuestions = questionList.value.filter(q => selectedRowKeys.value.includes(q.id));
emit('confirm', selectedQuestions);
selectedRowKeys.value = [];
};
// visible
watch(() => props.visible, (visible) => {
if (visible) {
loadQuestions();
selectedRowKeys.value = [];
//
if (props.questionType) {
filters.value.type = props.questionType;
}
}
});
//
onMounted(() => {
if (props.visible) {
loadQuestions();
}
});
</script>
<style scoped>
.question-bank-modal {
position: relative;
}
.header-title{
color: #000;
font-weight: 400;
font-size: 20px;
}
.question-bank-content {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 60vh;
overflow: hidden;
}
.filter-section {
border-bottom: 1px solid #f0f0f0;
padding-bottom: 16px;
}
.filter-row {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.filter-item {
display: flex;
align-items: center;
gap: 8px;
}
.filter-item label {
font-size: 14px;
color: #333;
white-space: nowrap;
}
.filter-actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.tip{
font-size: 12px;
color: #999;
}
.question-list-section {
flex: 1;
overflow: hidden;
}
.selected-info {
padding: 12px 0;
border-top: 1px solid #f0f0f0;
color: #666;
font-size: 14px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
/* 表格样式优化 */
:deep(.n-data-table-th) {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
:deep(.n-data-table-td) {
border-bottom: 1px solid #f0f0f0;
padding: 12px 16px;
}
:deep(.n-data-table-tr:hover .n-data-table-td) {
background-color: #f8f9fa;
}
:deep(.n-data-table-tbody .n-data-table-tr--checked .n-data-table-td) {
background-color: #e6f7ff;
}
:deep(.n-data-table .n-data-table-th) {
font-size: 14px;
}
:deep(.n-data-table .n-data-table-td) {
font-size: 14px;
}
/* 筛选区域样式 */
.filter-section {
background-color: #fafafa;
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.question-bank-content {
max-height: 50vh;
}
.filter-row {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.filter-actions {
margin-left: 0;
width: 100%;
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="course-editor">
<!-- 左侧导航菜单 -->
<div class="sidebar">
<div class="sidebar" v-if="showSidebar">
<router-link :to="`/teacher/course-editor/${courseId}/courseware`" class="menu-item"
:class="{ active: $route.path.includes('courseware') }">
<img :src="$route.path.includes('courseware') ? '/images/teacher/课件-选中.png' : '/images/teacher/课件.png'"
@ -16,11 +16,11 @@
</router-link>
<!-- 作业二级导航 -->
<div class="menu-group">
<div class="menu-header" @click="toggleHomework">
<div class="menu-header" @click="toggleHomework('homework')">
<img :src="$route.path.includes('homework') ? '/images/teacher/作业-选中.png' : '/images/teacher/作业.png'"
alt="作业" />
<span>作业</span>
<i class="n-base-icon" :class="{ 'expanded': homeworkExpanded }">
<i class="n-base-icon" :class="{ 'expanded': subMenuArr.homework }">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
@ -28,7 +28,7 @@
</svg>
</i>
</div>
<div class="submenu" v-show="homeworkExpanded">
<div class="submenu" v-show="subMenuArr.homework">
<router-link :to="`/teacher/course-editor/${courseId}/homework/library`" class="submenu-item"
:class="{ active: $route.path.includes('homework/library') }">
<span>作业库</span>
@ -39,12 +39,32 @@
</router-link>
</div>
</div>
<router-link :to="`/teacher/course-editor/${courseId}/practice`" class="menu-item"
:class="{ active: $route.path.includes('practice') }">
<img :src="$route.path.includes('practice') ? '/images/teacher/练考通-选中.png' : '/images/teacher/练考通.png'"
alt="练考通" />
<span>练考通</span>
</router-link>
<!-- 练考通父菜单 -->
<div class="menu-group">
<div class="menu-header" @click="toggleHomework('practice')">
<img :src="$route.path.includes('practice') ? '/images/teacher/练考通-选中.png' : '/images/teacher/练考通.png'"
alt="练考通" />
<span>练考通</span>
<i class="n-base-icon" :class="{ 'expanded': subMenuArr.practice }">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
fill="#C2C2C2"></path>
</svg>
</i>
</div>
<div class="submenu" v-show="subMenuArr.practice">
<router-link :to="`/teacher/course-editor/${courseId}/practice/exam-library`" class="submenu-item"
:class="{ active: $route.path.includes('exam-library') }">
<span>试卷库</span>
</router-link>
<router-link :to="`/teacher/course-editor/${courseId}/practice/marking-center`" class="submenu-item"
:class="{ active: $route.path.includes('marking-center') }">
<span>阅卷中心</span>
</router-link>
</div>
</div>
<router-link :to="`/teacher/course-editor/${courseId}/question-bank`" class="menu-item"
:class="{ active: $route.path.includes('question-bank') }">
<img :src="$route.path.includes('question-bank') ? '/images/teacher/题库-选中.png' : '/images/teacher/题库.png'"
@ -92,20 +112,38 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ref } from 'vue'
import { computed, ref, watch } from 'vue'
const route = useRoute()
// ID
const courseId = route.params.id
//
const homeworkExpanded = ref(false)
//
const subMenuArr = ref({
homework: false,
practice: false
})
// /
const toggleHomework = () => {
homeworkExpanded.value = !homeworkExpanded.value
const toggleHomework = (e: 'homework' | 'practice') => {
subMenuArr.value[e] = !subMenuArr.value[e]
}
//
watch(() => route.path, (newPath) => {
if (newPath.includes('practice')) {
subMenuArr.value.practice = true
}else if (newPath.includes('homework')) {
subMenuArr.value.homework = true
}
}, { immediate: true })
//
const showSidebar = computed(() => {
return route.meta.hideSidebar !== true
})
</script>
<style scoped>
@ -258,6 +296,9 @@ const toggleHomework = () => {
font-size: 16px;
color: #666;
}
/* 右侧内容区域 */
.content-area {
flex: 1;

View File

@ -364,13 +364,13 @@
</n-button>
</n-popselect>
<div class="mr-10"></div>
<n-button type="primary" ghost>
<n-button type="primary" ghost @click="openQuestionBankModal(index)">
<template #icon>
<n-icon>
<BookSharp />
</n-icon>
</template>
题库题库选择
题库选择题目
</n-button>
</n-row>
</n-card>
@ -400,6 +400,11 @@
{{ examForm.useAIGrading ? '已启用AI阅卷' : '使用AI阅卷功能' }}
</n-button>
<n-button type="primary" ghost size="large" @click="openExamSettingsModal">
<template #icon>
<n-icon>
<SettingsOutline />
</n-icon>
</template>
试卷设置
</n-button>
<n-button type="primary" ghost size="large">
@ -420,7 +425,7 @@
<n-button strong type="primary" secondary size="large">
取消
</n-button>
<n-button strong type="primary" secondary size="large">
<n-button strong type="primary" secondary size="large" @click="previewSubQuestion">
预览
</n-button>
<n-button strong type="primary" size="large" @click="saveExam">
@ -447,6 +452,13 @@
@confirm="handleExamSettingsConfirm"
@cancel="handleExamSettingsCancel"
/>
<!-- 题库选择模态框 -->
<QuestionBankModal
v-model:visible="showQuestionBankModal"
@confirm="handleQuestionBankConfirm"
@cancel="handleQuestionBankCancel"
/>
</div>
</n-config-provider>
</template>
@ -458,6 +470,7 @@ import type { GlobalThemeOverrides } from 'naive-ui';
import { AddCircle, SettingsOutline, TrashOutline, ChevronUpSharp, BookSharp } from '@vicons/ionicons5'
import BatchSetScoreModal from '@/components/admin/ExamComponents/BatchSetScoreModal.vue';
import ExamSettingsModal from '@/components/admin/ExamComponents/ExamSettingsModal.vue';
import QuestionBankModal from '@/components/admin/ExamComponents/QuestionBankModal.vue';
//
const themeOverrides: GlobalThemeOverrides = {
@ -955,6 +968,10 @@ const handleBatchScoreCancel = () => {
//
const showExamSettingsModal = ref(false);
//
const showQuestionBankModal = ref(false);
const currentBigQuestionIndex = ref(0);
//
const examSettingsData = computed(() => ({
title: examForm.title,
@ -1020,7 +1037,7 @@ const handleExamSettingsConfirm = (settings: any) => {
examForm.instructions = settings.instructions;
examForm.duration = settings.timerDuration || examForm.duration;
//
// TODO
console.log('试卷设置数据:', settings);
};
@ -1029,6 +1046,87 @@ const handleExamSettingsCancel = () => {
//
};
//
const openQuestionBankModal = (bigQuestionIndex: number) => {
currentBigQuestionIndex.value = bigQuestionIndex;
showQuestionBankModal.value = true;
};
//
const handleQuestionBankConfirm = (selectedQuestions: any[]) => {
const bigQuestionIndex = currentBigQuestionIndex.value;
//
selectedQuestions.forEach(question => {
const questionType = getQuestionTypeFromString(question.type);
const newSubQuestion: SubQuestion = {
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type: questionType as QuestionType,
title: question.title,
score: question.score,
difficulty: question.difficulty,
required: true,
createTime: new Date().toISOString()
};
//
if (questionType === 'single_choice') {
newSubQuestion.options = [
{ id: '1', content: '选项A', isCorrect: false },
{ id: '2', content: '选项B', isCorrect: false },
{ id: '3', content: '选项C', isCorrect: false },
{ id: '4', content: '选项D', isCorrect: false }
];
newSubQuestion.correctAnswer = '';
} else if (questionType === 'multiple_choice') {
newSubQuestion.options = [
{ id: '1', content: '选项A', isCorrect: false },
{ id: '2', content: '选项B', isCorrect: false },
{ id: '3', content: '选项C', isCorrect: false },
{ id: '4', content: '选项D', isCorrect: false }
];
newSubQuestion.correctAnswer = [];
} else if (questionType === 'true_false') {
newSubQuestion.trueFalseAnswer = undefined;
} else if (questionType === 'fill_blank') {
newSubQuestion.fillBlanks = [
{ id: '1', content: '', position: 1 }
];
} else if (questionType === 'short_answer') {
newSubQuestion.textAnswer = '';
}
examForm.questions[bigQuestionIndex].subQuestions.push(newSubQuestion);
});
//
updateBigQuestionScore(bigQuestionIndex);
dialog.success({
title: '成功',
content: `成功导入${selectedQuestions.length}道题目`,
positiveText: '确定'
});
showQuestionBankModal.value = false;
};
//
const handleQuestionBankCancel = () => {
showQuestionBankModal.value = false;
};
//
const getQuestionTypeFromString = (typeString: string) => {
const typeMap: { [key: string]: string } = {
'单选题': 'single_choice',
'多选题': 'multiple_choice',
'判断题': 'true_false',
'填空题': 'fill_blank',
'简答题': 'short_answer'
};
return typeMap[typeString] || 'single_choice';
};
//
const saveExam = () => {
//
@ -1293,11 +1391,11 @@ const changeCompositeSubQuestionType = (bigQuestionIndex: number, subQuestionInd
// }
//
// const previewSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number) => {
// const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
// console.log(':', subQuestion);
// //
// }
const previewSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number) => {
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
console.log('预览题目:', subQuestion);
//
}
</script>
<style scoped>

View File

@ -1,9 +1,6 @@
<template>
<div class="practice-management">
<div class="content-placeholder">
<h2>练考通管理</h2>
<p>练考通管理功能正在开发中...</p>
</div>
<router-view></router-view>
</div>
</template>