feat: 修复

This commit is contained in:
QDKF 2025-09-15 11:56:14 +08:00
parent ed8e688422
commit 760290bfb0
8 changed files with 367 additions and 134 deletions

View File

@ -289,7 +289,8 @@ import {
NCheckbox,
NButton,
NDivider,
NSwitch
NSwitch,
NFlex
} from 'naive-ui';
// message API

View File

@ -33,7 +33,7 @@
</div>
<div class="filter-actions">
<span class="tip">
{{ selectedRepo ? `已加载题库题目,共${pagination.itemCount}试题` : '请先选择题库' }}
{{ selectedRepo ? `已加载题库题目,共${paginationItemCount}试题` : '请先选择题库' }}
</span>
<n-button type="default" @click="resetFilters" style="margin-right: 8px;">
<template #icon>
@ -93,8 +93,8 @@
<!-- 题目列表 -->
<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"
<n-data-table ref="tableRef" :columns="columns" :data="questionList" :pagination="false"
:loading="loading" :row-key="(row) => row.id" :checked-row-keys="selectedRowKeys"
@update:checked-row-keys="handleCheck" striped>
<template #empty>
<div class="empty-state">
@ -119,6 +119,14 @@
</div>
</template>
</n-data-table>
<!-- 独立分页器 -->
<div v-if="paginationItemCount > 0" class="pagination-wrapper">
<n-pagination v-model:page="paginationPage" v-model:page-size="paginationPageSize"
:item-count="paginationItemCount" :page-sizes="[10, 20, 50]" show-size-picker show-quick-jumper
:prefix="({ itemCount }) => `共${itemCount}题`" @update:page="handlePageChange"
@update:page-size="handlePageSizeChange" />
</div>
</div>
<!-- 已选择题目统计 -->
@ -248,7 +256,7 @@ const filteredQuestions = computed(() => {
//
if (filters.value.category) {
filtered = filtered.filter(q => q.category === filters.value.category);
filtered = filtered.filter((q: QuestionItem) => q.category === filters.value.category);
}
//
@ -259,7 +267,7 @@ const filteredQuestions = computed(() => {
'3': '困难'
};
const targetDifficulty = difficultyMap[filters.value.difficulty];
filtered = filtered.filter(q => q.difficulty === targetDifficulty);
filtered = filtered.filter((q: QuestionItem) => q.difficulty === targetDifficulty);
}
//
@ -273,13 +281,13 @@ const filteredQuestions = computed(() => {
'5': '复合题'
};
const targetType = typeMap[filters.value.type];
filtered = filtered.filter(q => q.type === targetType);
filtered = filtered.filter((q: QuestionItem) => q.type === targetType);
}
//
if (filters.value.keyword) {
const keyword = filters.value.keyword.toLowerCase();
filtered = filtered.filter(q =>
filtered = filtered.filter((q: QuestionItem) =>
q.title.toLowerCase().includes(keyword) ||
q.creator.toLowerCase().includes(keyword)
);
@ -369,60 +377,54 @@ const columns = [
}
];
//
const pagination = ref({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
itemCount: 0,
showQuickJumper: true,
displayOrder: ['size-picker', 'pages', 'quick-jumper'],
onChange: (page: number) => {
pagination.value.page = page;
updateCurrentPageQuestions();
},
onUpdatePageSize: (pageSize: number) => {
pagination.value.pageSize = pageSize;
pagination.value.page = 1;
updateCurrentPageQuestions();
},
prefix: ({ itemCount }: { itemCount: number }) => `${itemCount}`
});
//
const paginationPage = ref(1);
const paginationPageSize = ref(10);
const paginationItemCount = ref(0);
//
const handleCheck = (rowKeys: string[]) => {
selectedRowKeys.value = rowKeys;
};
//
const handlePageChange = (page: number) => {
paginationPage.value = page;
updateCurrentPageQuestions();
};
const handlePageSizeChange = (pageSize: number) => {
paginationPageSize.value = pageSize;
paginationPage.value = 1;
updateCurrentPageQuestions();
};
//
const updateCurrentPageQuestions = () => {
const filtered = filteredQuestions.value;
const startIndex = (pagination.value.page - 1) * pagination.value.pageSize;
const endIndex = startIndex + pagination.value.pageSize;
const startIndex = (paginationPage.value - 1) * paginationPageSize.value;
const endIndex = startIndex + paginationPageSize.value;
questionList.value = filtered.slice(startIndex, endIndex);
console.log('📄 更新当前页题目 - 页码:', pagination.value.page, '每页:', pagination.value.pageSize, '显示题目数:', questionList.value.length);
//
paginationItemCount.value = filtered.length;
//
loadCurrentPageDetails();
};
//
const handleRepoChange = (repoId: string) => {
selectedRepo.value = repoId;
filters.value.repoId = repoId;
paginationPage.value = 1; //
loadQuestions();
};
//
const handleFilterChange = () => {
const filtered = filteredQuestions.value;
pagination.value.itemCount = filtered.length;
pagination.value.page = 1; //
//
const startIndex = (pagination.value.page - 1) * pagination.value.pageSize;
const endIndex = startIndex + pagination.value.pageSize;
questionList.value = filtered.slice(startIndex, endIndex);
console.log('📊 筛选后分页器更新 - 题目总数:', pagination.value.itemCount);
paginationPage.value = 1; //
updateCurrentPageQuestions();
};
//
@ -450,7 +452,6 @@ const loadCategories = async () => {
value: category.name
}))
];
console.log('✅ 加载分类选项成功:', categoryOptions.value);
}
} catch (error) {
console.error('加载分类选项失败:', error);
@ -476,7 +477,6 @@ const loadDifficulties = async () => {
value: difficulty.id
}))
];
console.log('✅ 加载难度选项成功:', difficultyOptions.value);
}
} catch (error) {
console.error('加载难度选项失败:', error);
@ -496,12 +496,10 @@ const loadRepos = async () => {
const response = await ExamApi.getCourseRepoList();
if (response.data && response.data.result) {
repoList.value = response.data.result;
console.log('✅ 加载题库列表成功:', repoList.value);
//
if (!selectedRepo.value && repoList.value.length > 0) {
selectedRepo.value = repoList.value[0].id;
console.log('🔄 自动选择第一个题库:', selectedRepo.value);
//
await loadQuestions();
}
@ -518,12 +516,11 @@ const loadQuestions = async () => {
try {
if (selectedRepo.value) {
//
console.log('🔍 正在加载题库题目:', selectedRepo.value);
const response = await ExamApi.getQuestionsByRepo(selectedRepo.value);
console.log('✅ 题库题目响应:', response);
if (response.data && response.data.result && response.data.result.length > 0) {
const questions = response.data.result.map((q: Question, index: number) => ({
//
const questionsWithBasicInfo = response.data.result.map((q: Question, index: number) => ({
id: q.id,
number: index + 1,
title: q.content || '无标题',
@ -533,43 +530,133 @@ const loadQuestions = async () => {
score: q.score || 5,
creator: q.createBy || '未知',
createTime: q.createTime ? new Date(q.createTime).toLocaleString() : '未知时间',
originalData: q
originalData: {
...q,
options: [], //
answers: [] //
}
}));
allQuestions.value = questions;
allQuestions.value = questionsWithBasicInfo;
//
const filtered = filteredQuestions.value;
pagination.value.itemCount = filtered.length;
updateCurrentPageQuestions();
//
const startIndex = (pagination.value.page - 1) * pagination.value.pageSize;
const endIndex = startIndex + pagination.value.pageSize;
questionList.value = filtered.slice(startIndex, endIndex);
// UI
//
loadCurrentPageDetails();
console.log('✅ 题目列表加载成功:', questionList.value);
console.log('📊 筛选后题目总数:', filtered.length);
} else {
//
allQuestions.value = [];
questionList.value = [];
console.log('⚠️ 该题库暂无题目');
paginationItemCount.value = 0; //
}
} else {
//
allQuestions.value = [];
questionList.value = [];
console.log(' 请先选择题库');
paginationItemCount.value = 0; //
}
console.log('📊 分页器更新 - 题目总数:', pagination.value.itemCount);
} catch (error) {
} catch (error: any) {
// ""
if (error?.message?.includes('该题库下没有题目') || error?.message?.includes('该题库不存在')) {
//
allQuestions.value = [];
questionList.value = [];
paginationItemCount.value = 0;
} else {
//
console.error('加载题目失败:', error);
message.error('加载题目失败');
allQuestions.value = [];
questionList.value = [];
paginationItemCount.value = 0;
}
} finally {
loading.value = false;
}
};
//
const loadCurrentPageDetails = async () => {
if (!questionList.value || questionList.value.length === 0) return;
try {
//
const currentPageQuestions = questionList.value;
// 使 Promise.allSettled
const detailPromises = currentPageQuestions.map(async (question: QuestionItem) => {
const q = question.originalData;
if (!q || !q.id) return question;
let options: any[] = [];
let answers: any[] = [];
//
if (q.type === 0 || q.type === 1 || q.type === 2) {
try {
const optionsResponse = await ExamApi.getQuestionOptions(q.id);
options = processApiResponse(optionsResponse.data);
} catch (error) {
console.warn(`获取题目 ${q.id} 选项失败:`, error);
}
}
//
if (q.type === 3 || q.type === 4) {
try {
const answersResponse = await ExamApi.getQuestionAnswers(q.id);
answers = processApiResponse(answersResponse.data);
} catch (error) {
console.warn(`获取题目 ${q.id} 答案失败:`, error);
}
}
//
question.originalData = {
...q,
options: options,
answers: answers
} as Question;
return question;
});
//
await Promise.allSettled(detailPromises);
//
allQuestions.value = [...allQuestions.value];
} catch (error) {
console.warn('加载题目详情失败:', error);
}
};
// API
const processApiResponse = (data: any): any[] => {
if (!data) return [];
if (Array.isArray(data)) {
return data;
} else if (data && Array.isArray(data.result)) {
return data.result;
} else if (data && data.success && data.result) {
if (Array.isArray(data.result)) {
return data.result;
} else if (data.result && data.result.records) {
return data.result.records || [];
} else {
return [data.result];
}
}
return [];
};
//
const addNewQuestion = () => {
//
@ -685,7 +772,7 @@ const exportQuestions = async () => {
link.href = url;
//
const repo = repoList.value.find(r => r.id === selectedRepo.value);
const repo = repoList.value.find((r: Repo) => r.id === selectedRepo.value);
const fileName = repo ? `${repo.title}_题目模板.xlsx` : '题目模板.xlsx';
link.download = fileName;
@ -765,8 +852,8 @@ onMounted(() => {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 60vh;
overflow: hidden;
max-height: 80vh;
overflow-y: auto;
}
.filter-section {
@ -807,7 +894,9 @@ onMounted(() => {
.question-list-section {
flex: 1;
overflow: hidden;
overflow: visible;
display: flex;
flex-direction: column;
}
.selected-info {
@ -817,6 +906,11 @@ onMounted(() => {
font-size: 14px;
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.modal-actions {
display: flex;
justify-content: flex-end;

View File

@ -84,7 +84,7 @@
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
// defineProps defineEmits
interface FillBlankAnswer {
content: string;

View File

@ -96,7 +96,7 @@
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
// defineProps defineEmits
interface Option {
content: string;

View File

@ -67,7 +67,7 @@
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
// defineProps defineEmits
interface Props {
modelValue: string;

View File

@ -73,7 +73,7 @@
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
// defineProps defineEmits
interface Option {
content: string;

View File

@ -81,7 +81,7 @@
</template>
<script setup lang="ts">
import { defineProps, defineEmits, computed } from 'vue';
import { computed } from 'vue';
interface Props {
answer: boolean | null;

View File

@ -138,9 +138,11 @@
<div class="question-component-wrapper">
<!-- 单选题 -->
<SingleChoiceQuestion v-if="subQuestion.type === 'single_choice'"
v-model="subQuestion.options!" v-model:correctAnswer="subQuestion.correctAnswer!"
v-model="subQuestion.options!" :correctAnswer="subQuestion.correctAnswer"
@update:correctAnswer="(val: number | null) => subQuestion.correctAnswer = val"
v-model:title="subQuestion.title" v-model:explanation="subQuestion.explanation" />
<!-- 多选题 -->
<MultipleChoiceQuestion v-else-if="subQuestion.type === 'multiple_choice'"
v-model="subQuestion.options!" v-model:correctAnswers="subQuestion.correctAnswers!"
@ -263,8 +265,20 @@
<n-button strong type="primary" secondary size="large">
取消
</n-button>
<n-button strong type="primary" size="large" @click="saveExam">
保存试卷
<n-button strong type="primary" size="large" @click="saveExam" :loading="saving"
:disabled="saving">
<template #icon v-if="saving">
<n-icon>
<svg viewBox="0 0 24 24" fill="currentColor" class="loading-spinner">
<path d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z">
<animateTransform attributeName="transform" attributeType="XML"
type="rotate" dur="1s" from="0 12 12" to="360 12 12"
repeatCount="indefinite" />
</path>
</svg>
</n-icon>
</template>
{{ saving ? '保存中...' : '保存试卷' }}
</n-button>
</n-space>
</div>
@ -364,6 +378,7 @@ enum QuestionType {
//
interface ChoiceOption {
content: string;
isCorrect?: boolean; //
}
//
@ -704,7 +719,7 @@ const ensureSubQuestionFields = (subQuestion: SubQuestion): void => {
if (!subQuestion.correctAnswers) subQuestion.correctAnswers = [];
if (!subQuestion.fillBlanks) subQuestion.fillBlanks = [];
if (!subQuestion.subQuestions) subQuestion.subQuestions = [];
if (subQuestion.correctAnswer === undefined) subQuestion.correctAnswer = null;
if (subQuestion.correctAnswer === undefined || subQuestion.correctAnswer === '' || subQuestion.correctAnswer === null) subQuestion.correctAnswer = null;
if (subQuestion.trueFalseAnswer === undefined) subQuestion.trueFalseAnswer = null;
if (!subQuestion.textAnswer) subQuestion.textAnswer = '';
if (!subQuestion.explanation) subQuestion.explanation = '';
@ -1062,11 +1077,12 @@ const openQuestionBankModal = (bigQuestionIndex: number) => {
};
//
const handleQuestionBankConfirm = (selectedQuestions: any[]) => {
const handleQuestionBankConfirm = async (selectedQuestions: any[]) => {
const bigQuestionIndex = currentBigQuestionIndex.value;
//
selectedQuestions.forEach(question => {
for (const question of selectedQuestions) {
const questionType = getQuestionTypeFromString(question.type);
const newSubQuestion: SubQuestion = {
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
@ -1075,26 +1091,95 @@ const handleQuestionBankConfirm = (selectedQuestions: any[]) => {
score: question.score,
difficulty: question.difficulty,
required: 'true',
explanation: '', //
explanation: question.originalData?.analysis || '', // 使
textAnswer: '', //
createTime: new Date().toISOString()
};
//
//
if (question.originalData) {
//
newSubQuestion.explanation = question.originalData.analysis || '';
//
if (questionType === 'single_choice' || questionType === 'multiple_choice') {
if (question.originalData.options && question.originalData.options.length > 0) {
//
newSubQuestion.options = question.originalData.options.map((option: any) => ({
content: option.content || '',
isCorrect: option.izCorrent === 1
}));
//
if (questionType === 'single_choice') {
const correctOption = question.originalData.options.find((opt: any) => opt.izCorrent === 1);
newSubQuestion.correctAnswer = correctOption ? question.originalData.options.indexOf(correctOption) : null;
} else if (questionType === 'multiple_choice') {
newSubQuestion.correctAnswers = question.originalData.options
.map((opt: any, index: number) => opt.izCorrent === 1 ? index : -1)
.filter((index: number) => index !== -1);
}
} else {
// 使
newSubQuestion.options = [
{ content: '选项A', isCorrect: false },
{ content: '选项B', isCorrect: false },
{ content: '选项C', isCorrect: false },
{ content: '选项D', isCorrect: false }
];
newSubQuestion.correctAnswer = null;
newSubQuestion.correctAnswers = [];
}
} else if (questionType === 'true_false') {
//
if (question.originalData.options && question.originalData.options.length > 0) {
const correctOption = question.originalData.options.find((opt: any) => opt.izCorrent === 1);
if (correctOption) {
newSubQuestion.trueFalseAnswer = correctOption.content === '正确' || correctOption.content === 'true';
}
} else {
newSubQuestion.trueFalseAnswer = null;
}
} else if (questionType === 'fill_blank') {
//
if (question.originalData.answers && question.originalData.answers.length > 0) {
newSubQuestion.fillBlanks = question.originalData.answers.map((answer: any) => ({
content: answer.answerText || answer.content || '',
score: 1,
caseSensitive: false
}));
} else {
newSubQuestion.fillBlanks = [
{ content: '', score: 1, caseSensitive: false }
];
}
} else if (questionType === 'short_answer') {
//
if (question.originalData.answers && question.originalData.answers.length > 0) {
newSubQuestion.textAnswer = question.originalData.answers[0]?.answerText || question.originalData.answers[0]?.content || '';
} else {
newSubQuestion.textAnswer = '';
}
}
} else {
// 使
if (questionType === 'single_choice') {
newSubQuestion.options = [
{ content: '选项A' },
{ content: '选项B' },
{ content: '选项C' },
{ content: '选项D' }
{ content: '选项A', isCorrect: false },
{ content: '选项B', isCorrect: false },
{ content: '选项C', isCorrect: false },
{ content: '选项D', isCorrect: false }
];
newSubQuestion.correctAnswer = null;
} else if (questionType === 'multiple_choice') {
newSubQuestion.options = [
{ content: '选项A' },
{ content: '选项B' },
{ content: '选项C' },
{ content: '选项D' }
{ content: '选项A', isCorrect: false },
{ content: '选项B', isCorrect: false },
{ content: '选项C', isCorrect: false },
{ content: '选项D', isCorrect: false }
];
newSubQuestion.correctAnswers = [];
} else if (questionType === 'true_false') {
@ -1106,9 +1191,10 @@ const handleQuestionBankConfirm = (selectedQuestions: any[]) => {
} else if (questionType === 'short_answer') {
newSubQuestion.textAnswer = '';
}
}
examForm.questions[bigQuestionIndex].subQuestions.push(newSubQuestion);
});
}
//
updateBigQuestionScore(bigQuestionIndex);
@ -1152,10 +1238,15 @@ const clearExamForm = () => {
}
//
const dialogTitle = '确认清除试卷内容';
const dialogContent = isEditMode.value
? '您正在编辑已保存的试卷,确定要清空当前编辑的所有试卷内容吗?\n\n⚠ 清空后将删除所有题目内容,但试卷基本信息会保留\n⚠ 此操作不可撤销,请谨慎操作!'
: '您已创建了试卷内容,确定要清空当前编辑的所有试卷内容吗?\n\n⚠ 清空后将删除所有题目内容,但试卷基本信息会保留\n⚠ 此操作不可撤销,请谨慎操作!';
dialog.warning({
title: '确认清除',
content: '确定要清除所有试卷内容吗?此操作不可撤销!',
positiveText: '确定清除',
title: dialogTitle,
content: dialogContent,
positiveText: '确定清',
negativeText: '取消',
onPositiveClick: () => {
//
@ -1190,6 +1281,9 @@ const clearExamForm = () => {
});
};
//
const saving = ref(false);
//
const saveExam = async () => {
//
@ -1228,6 +1322,9 @@ const saveExam = async () => {
return;
}
//
saving.value = true;
try {
// API - /aiol/aiolPaper/add
const apiData = {
@ -1297,11 +1394,11 @@ const saveExam = async () => {
console.error('❌ 无法获取试卷ID跳过题目保存');
}
dialog.success({
title: '保存成功',
content: isEditMode.value ? '试卷更新成功!' : '试卷保存成功!',
positiveText: '确定',
onPositiveClick: () => {
//
message.success(isEditMode.value ? '试卷更新成功!' : '试卷保存成功!');
//
setTimeout(() => {
if (isEditMode.value) {
//
loadExamDetail(examId.value);
@ -1311,15 +1408,14 @@ const saveExam = async () => {
//
router.back();
}
}
});
}, 1000);
} catch (error) {
console.error('创建试卷失败:', error);
dialog.error({
title: '保存失败',
content: '试卷保存失败,请重试',
positiveText: '确定'
});
message.error('试卷保存失败,请重试');
} finally {
//
saving.value = false;
}
}
@ -1694,6 +1790,7 @@ const isAutoSaved = ref(false);
padding: 12px;
margin-bottom: 15px 0;
}
.q-title {
font-size: 16px;
font-weight: 600;
@ -1923,6 +2020,47 @@ const isAutoSaved = ref(false);
z-index: 1;
}
/* 加载动画样式 */
.loading-spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 保存按钮加载状态样式 */
.n-button[loading] {
position: relative;
overflow: hidden;
}
.n-button[loading]::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
/* 响应式布局 */
@media (max-width: 1200px) {