feat: 修复
This commit is contained in:
parent
ed8e688422
commit
760290bfb0
@ -289,7 +289,8 @@ import {
|
|||||||
NCheckbox,
|
NCheckbox,
|
||||||
NButton,
|
NButton,
|
||||||
NDivider,
|
NDivider,
|
||||||
NSwitch
|
NSwitch,
|
||||||
|
NFlex
|
||||||
} from 'naive-ui';
|
} from 'naive-ui';
|
||||||
|
|
||||||
// 创建独立的 message API
|
// 创建独立的 message API
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
<span class="tip">
|
<span class="tip">
|
||||||
{{ selectedRepo ? `已加载题库题目,共${pagination.itemCount}试题` : '请先选择题库' }}
|
{{ selectedRepo ? `已加载题库题目,共${paginationItemCount}试题` : '请先选择题库' }}
|
||||||
</span>
|
</span>
|
||||||
<n-button type="default" @click="resetFilters" style="margin-right: 8px;">
|
<n-button type="default" @click="resetFilters" style="margin-right: 8px;">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@ -93,8 +93,8 @@
|
|||||||
|
|
||||||
<!-- 题目列表 -->
|
<!-- 题目列表 -->
|
||||||
<div class="question-list-section">
|
<div class="question-list-section">
|
||||||
<n-data-table ref="tableRef" :columns="columns" :data="questionList" :pagination="pagination"
|
<n-data-table ref="tableRef" :columns="columns" :data="questionList" :pagination="false"
|
||||||
:loading="loading" :row-key="(row: any) => row.id" :checked-row-keys="selectedRowKeys"
|
:loading="loading" :row-key="(row) => row.id" :checked-row-keys="selectedRowKeys"
|
||||||
@update:checked-row-keys="handleCheck" striped>
|
@update:checked-row-keys="handleCheck" striped>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@ -119,6 +119,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-data-table>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- 已选择题目统计 -->
|
<!-- 已选择题目统计 -->
|
||||||
@ -248,7 +256,7 @@ const filteredQuestions = computed(() => {
|
|||||||
|
|
||||||
// 按分类筛选
|
// 按分类筛选
|
||||||
if (filters.value.category) {
|
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': '困难'
|
'3': '困难'
|
||||||
};
|
};
|
||||||
const targetDifficulty = difficultyMap[filters.value.difficulty];
|
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': '复合题'
|
'5': '复合题'
|
||||||
};
|
};
|
||||||
const targetType = typeMap[filters.value.type];
|
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) {
|
if (filters.value.keyword) {
|
||||||
const keyword = filters.value.keyword.toLowerCase();
|
const keyword = filters.value.keyword.toLowerCase();
|
||||||
filtered = filtered.filter(q =>
|
filtered = filtered.filter((q: QuestionItem) =>
|
||||||
q.title.toLowerCase().includes(keyword) ||
|
q.title.toLowerCase().includes(keyword) ||
|
||||||
q.creator.toLowerCase().includes(keyword)
|
q.creator.toLowerCase().includes(keyword)
|
||||||
);
|
);
|
||||||
@ -369,60 +377,54 @@ const columns = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// 分页配置
|
// 分页器状态
|
||||||
const pagination = ref({
|
const paginationPage = ref(1);
|
||||||
page: 1,
|
const paginationPageSize = ref(10);
|
||||||
pageSize: 10,
|
const paginationItemCount = ref(0);
|
||||||
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 handleCheck = (rowKeys: string[]) => {
|
const handleCheck = (rowKeys: string[]) => {
|
||||||
selectedRowKeys.value = rowKeys;
|
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 updateCurrentPageQuestions = () => {
|
||||||
const filtered = filteredQuestions.value;
|
const filtered = filteredQuestions.value;
|
||||||
const startIndex = (pagination.value.page - 1) * pagination.value.pageSize;
|
const startIndex = (paginationPage.value - 1) * paginationPageSize.value;
|
||||||
const endIndex = startIndex + pagination.value.pageSize;
|
const endIndex = startIndex + paginationPageSize.value;
|
||||||
questionList.value = filtered.slice(startIndex, endIndex);
|
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) => {
|
const handleRepoChange = (repoId: string) => {
|
||||||
selectedRepo.value = repoId;
|
selectedRepo.value = repoId;
|
||||||
filters.value.repoId = repoId;
|
filters.value.repoId = repoId;
|
||||||
|
paginationPage.value = 1; // 重置到第一页
|
||||||
loadQuestions();
|
loadQuestions();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 筛选条件变化处理
|
// 筛选条件变化处理
|
||||||
const handleFilterChange = () => {
|
const handleFilterChange = () => {
|
||||||
const filtered = filteredQuestions.value;
|
paginationPage.value = 1; // 重置到第一页
|
||||||
pagination.value.itemCount = filtered.length;
|
updateCurrentPageQuestions();
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重置筛选条件
|
// 重置筛选条件
|
||||||
@ -450,7 +452,6 @@ const loadCategories = async () => {
|
|||||||
value: category.name
|
value: category.name
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
console.log('✅ 加载分类选项成功:', categoryOptions.value);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载分类选项失败:', error);
|
console.error('加载分类选项失败:', error);
|
||||||
@ -476,7 +477,6 @@ const loadDifficulties = async () => {
|
|||||||
value: difficulty.id
|
value: difficulty.id
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
console.log('✅ 加载难度选项成功:', difficultyOptions.value);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载难度选项失败:', error);
|
console.error('加载难度选项失败:', error);
|
||||||
@ -496,12 +496,10 @@ const loadRepos = async () => {
|
|||||||
const response = await ExamApi.getCourseRepoList();
|
const response = await ExamApi.getCourseRepoList();
|
||||||
if (response.data && response.data.result) {
|
if (response.data && response.data.result) {
|
||||||
repoList.value = response.data.result;
|
repoList.value = response.data.result;
|
||||||
console.log('✅ 加载题库列表成功:', repoList.value);
|
|
||||||
|
|
||||||
// 如果还没有选择题库,自动选择第一个题库
|
// 如果还没有选择题库,自动选择第一个题库
|
||||||
if (!selectedRepo.value && repoList.value.length > 0) {
|
if (!selectedRepo.value && repoList.value.length > 0) {
|
||||||
selectedRepo.value = repoList.value[0].id;
|
selectedRepo.value = repoList.value[0].id;
|
||||||
console.log('🔄 自动选择第一个题库:', selectedRepo.value);
|
|
||||||
// 自动加载该题库的题目
|
// 自动加载该题库的题目
|
||||||
await loadQuestions();
|
await loadQuestions();
|
||||||
}
|
}
|
||||||
@ -518,12 +516,11 @@ const loadQuestions = async () => {
|
|||||||
try {
|
try {
|
||||||
if (selectedRepo.value) {
|
if (selectedRepo.value) {
|
||||||
// 根据选择的题库加载题目
|
// 根据选择的题库加载题目
|
||||||
console.log('🔍 正在加载题库题目:', selectedRepo.value);
|
|
||||||
const response = await ExamApi.getQuestionsByRepo(selectedRepo.value);
|
const response = await ExamApi.getQuestionsByRepo(selectedRepo.value);
|
||||||
console.log('✅ 题库题目响应:', response);
|
|
||||||
|
|
||||||
if (response.data && response.data.result && response.data.result.length > 0) {
|
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,
|
id: q.id,
|
||||||
number: index + 1,
|
number: index + 1,
|
||||||
title: q.content || '无标题',
|
title: q.content || '无标题',
|
||||||
@ -533,43 +530,133 @@ const loadQuestions = async () => {
|
|||||||
score: q.score || 5,
|
score: q.score || 5,
|
||||||
creator: q.createBy || '未知',
|
creator: q.createBy || '未知',
|
||||||
createTime: q.createTime ? new Date(q.createTime).toLocaleString() : '未知时间',
|
createTime: q.createTime ? new Date(q.createTime).toLocaleString() : '未知时间',
|
||||||
originalData: q
|
originalData: {
|
||||||
|
...q,
|
||||||
|
options: [], // 初始化为空数组
|
||||||
|
answers: [] // 初始化为空数组
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
allQuestions.value = questions;
|
|
||||||
|
allQuestions.value = questionsWithBasicInfo;
|
||||||
|
|
||||||
// 应用筛选条件并更新当前页
|
// 应用筛选条件并更新当前页
|
||||||
const filtered = filteredQuestions.value;
|
updateCurrentPageQuestions();
|
||||||
pagination.value.itemCount = filtered.length;
|
|
||||||
|
|
||||||
// 计算当前页的题目
|
// 优化:延迟加载选项和答案数据,避免阻塞UI
|
||||||
const startIndex = (pagination.value.page - 1) * pagination.value.pageSize;
|
// 只对当前页的题目加载详细数据
|
||||||
const endIndex = startIndex + pagination.value.pageSize;
|
loadCurrentPageDetails();
|
||||||
questionList.value = filtered.slice(startIndex, endIndex);
|
|
||||||
|
|
||||||
console.log('✅ 题目列表加载成功:', questionList.value);
|
|
||||||
console.log('📊 筛选后题目总数:', filtered.length);
|
|
||||||
} else {
|
} else {
|
||||||
// 如果题库没有题目,显示空列表
|
// 如果题库没有题目,显示空列表
|
||||||
allQuestions.value = [];
|
allQuestions.value = [];
|
||||||
questionList.value = [];
|
questionList.value = [];
|
||||||
console.log('⚠️ 该题库暂无题目');
|
paginationItemCount.value = 0; // 重置分页器总数
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果没有选择题库,显示提示信息
|
// 如果没有选择题库,显示提示信息
|
||||||
|
allQuestions.value = [];
|
||||||
questionList.value = [];
|
questionList.value = [];
|
||||||
console.log('ℹ️ 请先选择题库');
|
paginationItemCount.value = 0; // 重置分页器总数
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📊 分页器更新 - 题目总数:', pagination.value.itemCount);
|
} catch (error: any) {
|
||||||
} catch (error) {
|
// 检查是否是"没有题目"的错误
|
||||||
console.error('加载题目失败:', error);
|
if (error?.message?.includes('该题库下没有题目') || error?.message?.includes('该题库不存在')) {
|
||||||
message.error('加载题目失败');
|
// 没有题目不是错误,正常处理
|
||||||
questionList.value = [];
|
allQuestions.value = [];
|
||||||
|
questionList.value = [];
|
||||||
|
paginationItemCount.value = 0;
|
||||||
|
} else {
|
||||||
|
// 真正的错误才显示错误信息
|
||||||
|
console.error('加载题目失败:', error);
|
||||||
|
message.error('加载题目失败');
|
||||||
|
allQuestions.value = [];
|
||||||
|
questionList.value = [];
|
||||||
|
paginationItemCount.value = 0;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
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 = () => {
|
const addNewQuestion = () => {
|
||||||
// 创建文件输入元素
|
// 创建文件输入元素
|
||||||
@ -685,7 +772,7 @@ const exportQuestions = async () => {
|
|||||||
link.href = url;
|
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';
|
const fileName = repo ? `${repo.title}_题目模板.xlsx` : '题目模板.xlsx';
|
||||||
link.download = fileName;
|
link.download = fileName;
|
||||||
|
|
||||||
@ -765,8 +852,8 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
max-height: 60vh;
|
max-height: 80vh;
|
||||||
overflow: hidden;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-section {
|
.filter-section {
|
||||||
@ -807,7 +894,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
.question-list-section {
|
.question-list-section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-info {
|
.selected-info {
|
||||||
@ -817,6 +906,11 @@ onMounted(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
.modal-actions {
|
.modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
@ -84,7 +84,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, defineEmits } from 'vue';
|
// defineProps 和 defineEmits 是编译器宏,不需要导入
|
||||||
|
|
||||||
interface FillBlankAnswer {
|
interface FillBlankAnswer {
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -96,7 +96,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, defineEmits } from 'vue';
|
// defineProps 和 defineEmits 是编译器宏,不需要导入
|
||||||
|
|
||||||
interface Option {
|
interface Option {
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, defineEmits } from 'vue';
|
// defineProps 和 defineEmits 是编译器宏,不需要导入
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: string;
|
modelValue: string;
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, defineEmits } from 'vue';
|
// defineProps 和 defineEmits 是编译器宏,不需要导入
|
||||||
|
|
||||||
interface Option {
|
interface Option {
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, defineEmits, computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
answer: boolean | null;
|
answer: boolean | null;
|
||||||
|
@ -138,9 +138,11 @@
|
|||||||
<div class="question-component-wrapper">
|
<div class="question-component-wrapper">
|
||||||
<!-- 单选题 -->
|
<!-- 单选题 -->
|
||||||
<SingleChoiceQuestion v-if="subQuestion.type === 'single_choice'"
|
<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" />
|
v-model:title="subQuestion.title" v-model:explanation="subQuestion.explanation" />
|
||||||
|
|
||||||
|
|
||||||
<!-- 多选题 -->
|
<!-- 多选题 -->
|
||||||
<MultipleChoiceQuestion v-else-if="subQuestion.type === 'multiple_choice'"
|
<MultipleChoiceQuestion v-else-if="subQuestion.type === 'multiple_choice'"
|
||||||
v-model="subQuestion.options!" v-model:correctAnswers="subQuestion.correctAnswers!"
|
v-model="subQuestion.options!" v-model:correctAnswers="subQuestion.correctAnswers!"
|
||||||
@ -242,7 +244,7 @@
|
|||||||
<div class="footer-center">
|
<div class="footer-center">
|
||||||
<span>大题数量:{{ examForm.questions.length }} 道 - 小题数量:{{examForm.questions.reduce((total, q) =>
|
<span>大题数量:{{ examForm.questions.length }} 道 - 小题数量:{{examForm.questions.reduce((total, q) =>
|
||||||
total +
|
total +
|
||||||
q.subQuestions.length, 0) }} 道</span>
|
q.subQuestions.length, 0)}} 道</span>
|
||||||
<span>总分:{{examForm.questions.reduce((total, q) => total + q.totalScore, 0)}} 分</span>
|
<span>总分:{{examForm.questions.reduce((total, q) => total + q.totalScore, 0)}} 分</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -263,8 +265,20 @@
|
|||||||
<n-button strong type="primary" secondary size="large">
|
<n-button strong type="primary" secondary size="large">
|
||||||
取消
|
取消
|
||||||
</n-button>
|
</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-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
@ -364,6 +378,7 @@ enum QuestionType {
|
|||||||
// 选择题选项接口(适配题型组件)
|
// 选择题选项接口(适配题型组件)
|
||||||
interface ChoiceOption {
|
interface ChoiceOption {
|
||||||
content: string;
|
content: string;
|
||||||
|
isCorrect?: boolean; // 添加可选的正确性标识
|
||||||
}
|
}
|
||||||
|
|
||||||
// 填空题答案接口(适配题型组件)
|
// 填空题答案接口(适配题型组件)
|
||||||
@ -704,7 +719,7 @@ const ensureSubQuestionFields = (subQuestion: SubQuestion): void => {
|
|||||||
if (!subQuestion.correctAnswers) subQuestion.correctAnswers = [];
|
if (!subQuestion.correctAnswers) subQuestion.correctAnswers = [];
|
||||||
if (!subQuestion.fillBlanks) subQuestion.fillBlanks = [];
|
if (!subQuestion.fillBlanks) subQuestion.fillBlanks = [];
|
||||||
if (!subQuestion.subQuestions) subQuestion.subQuestions = [];
|
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.trueFalseAnswer === undefined) subQuestion.trueFalseAnswer = null;
|
||||||
if (!subQuestion.textAnswer) subQuestion.textAnswer = '';
|
if (!subQuestion.textAnswer) subQuestion.textAnswer = '';
|
||||||
if (!subQuestion.explanation) subQuestion.explanation = '';
|
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;
|
const bigQuestionIndex = currentBigQuestionIndex.value;
|
||||||
|
|
||||||
// 将选择的题目转换为当前系统的题目格式并添加到对应大题
|
// 将选择的题目转换为当前系统的题目格式并添加到对应大题
|
||||||
selectedQuestions.forEach(question => {
|
for (const question of selectedQuestions) {
|
||||||
|
|
||||||
const questionType = getQuestionTypeFromString(question.type);
|
const questionType = getQuestionTypeFromString(question.type);
|
||||||
const newSubQuestion: SubQuestion = {
|
const newSubQuestion: SubQuestion = {
|
||||||
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
@ -1075,40 +1091,110 @@ const handleQuestionBankConfirm = (selectedQuestions: any[]) => {
|
|||||||
score: question.score,
|
score: question.score,
|
||||||
difficulty: question.difficulty,
|
difficulty: question.difficulty,
|
||||||
required: 'true',
|
required: 'true',
|
||||||
explanation: '', // 添加默认的解析字段
|
explanation: question.originalData?.analysis || '', // 使用原始数据中的答案解析
|
||||||
textAnswer: '', // 添加默认的文本答案字段
|
textAnswer: '', // 添加默认的文本答案字段
|
||||||
createTime: new Date().toISOString()
|
createTime: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
// 根据题型初始化不同的字段
|
// 如果有原始数据,尝试加载完整的题目信息
|
||||||
if (questionType === 'single_choice') {
|
if (question.originalData) {
|
||||||
newSubQuestion.options = [
|
// 设置答案解析
|
||||||
{ content: '选项A' },
|
newSubQuestion.explanation = question.originalData.analysis || '';
|
||||||
{ content: '选项B' },
|
|
||||||
{ content: '选项C' },
|
// 根据题型加载选项和答案
|
||||||
{ content: '选项D' }
|
if (questionType === 'single_choice' || questionType === 'multiple_choice') {
|
||||||
];
|
|
||||||
newSubQuestion.correctAnswer = null;
|
if (question.originalData.options && question.originalData.options.length > 0) {
|
||||||
} else if (questionType === 'multiple_choice') {
|
// 加载选项数据
|
||||||
newSubQuestion.options = [
|
newSubQuestion.options = question.originalData.options.map((option: any) => ({
|
||||||
{ content: '选项A' },
|
content: option.content || '',
|
||||||
{ content: '选项B' },
|
isCorrect: option.izCorrent === 1
|
||||||
{ content: '选项C' },
|
}));
|
||||||
{ content: '选项D' }
|
|
||||||
];
|
// 设置正确答案
|
||||||
newSubQuestion.correctAnswers = [];
|
if (questionType === 'single_choice') {
|
||||||
} else if (questionType === 'true_false') {
|
const correctOption = question.originalData.options.find((opt: any) => opt.izCorrent === 1);
|
||||||
newSubQuestion.trueFalseAnswer = null;
|
newSubQuestion.correctAnswer = correctOption ? question.originalData.options.indexOf(correctOption) : null;
|
||||||
} else if (questionType === 'fill_blank') {
|
} else if (questionType === 'multiple_choice') {
|
||||||
newSubQuestion.fillBlanks = [
|
newSubQuestion.correctAnswers = question.originalData.options
|
||||||
{ content: '', score: 1, caseSensitive: false }
|
.map((opt: any, index: number) => opt.izCorrent === 1 ? index : -1)
|
||||||
];
|
.filter((index: number) => index !== -1);
|
||||||
} else if (questionType === 'short_answer') {
|
}
|
||||||
newSubQuestion.textAnswer = '';
|
|
||||||
|
} 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', 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', isCorrect: false },
|
||||||
|
{ content: '选项B', isCorrect: false },
|
||||||
|
{ content: '选项C', isCorrect: false },
|
||||||
|
{ content: '选项D', isCorrect: false }
|
||||||
|
];
|
||||||
|
newSubQuestion.correctAnswers = [];
|
||||||
|
} else if (questionType === 'true_false') {
|
||||||
|
newSubQuestion.trueFalseAnswer = null;
|
||||||
|
} else if (questionType === 'fill_blank') {
|
||||||
|
newSubQuestion.fillBlanks = [
|
||||||
|
{ content: '', score: 1, caseSensitive: false }
|
||||||
|
];
|
||||||
|
} else if (questionType === 'short_answer') {
|
||||||
|
newSubQuestion.textAnswer = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
examForm.questions[bigQuestionIndex].subQuestions.push(newSubQuestion);
|
examForm.questions[bigQuestionIndex].subQuestions.push(newSubQuestion);
|
||||||
});
|
}
|
||||||
|
|
||||||
// 重新计算总分
|
// 重新计算总分
|
||||||
updateBigQuestionScore(bigQuestionIndex);
|
updateBigQuestionScore(bigQuestionIndex);
|
||||||
@ -1152,10 +1238,15 @@ const clearExamForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 显示确认对话框
|
// 显示确认对话框
|
||||||
|
const dialogTitle = '确认清除试卷内容';
|
||||||
|
const dialogContent = isEditMode.value
|
||||||
|
? '您正在编辑已保存的试卷,确定要清空当前编辑的所有试卷内容吗?\n\n⚠️ 清空后将删除所有题目内容,但试卷基本信息会保留\n⚠️ 此操作不可撤销,请谨慎操作!'
|
||||||
|
: '您已创建了试卷内容,确定要清空当前编辑的所有试卷内容吗?\n\n⚠️ 清空后将删除所有题目内容,但试卷基本信息会保留\n⚠️ 此操作不可撤销,请谨慎操作!';
|
||||||
|
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
title: '确认清除',
|
title: dialogTitle,
|
||||||
content: '确定要清除所有试卷内容吗?此操作不可撤销!',
|
content: dialogContent,
|
||||||
positiveText: '确定清除',
|
positiveText: '确定清空',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: () => {
|
onPositiveClick: () => {
|
||||||
// 重置试卷基本信息
|
// 重置试卷基本信息
|
||||||
@ -1190,6 +1281,9 @@ const clearExamForm = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 保存状态
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
// 保存试卷
|
// 保存试卷
|
||||||
const saveExam = async () => {
|
const saveExam = async () => {
|
||||||
// 验证数据
|
// 验证数据
|
||||||
@ -1228,6 +1322,9 @@ const saveExam = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置保存状态
|
||||||
|
saving.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 准备API数据 - 匹配 /aiol/aiolPaper/add 接口
|
// 准备API数据 - 匹配 /aiol/aiolPaper/add 接口
|
||||||
const apiData = {
|
const apiData = {
|
||||||
@ -1297,29 +1394,28 @@ const saveExam = async () => {
|
|||||||
console.error('❌ 无法获取试卷ID,跳过题目保存');
|
console.error('❌ 无法获取试卷ID,跳过题目保存');
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.success({
|
// 显示保存成功消息
|
||||||
title: '保存成功',
|
message.success(isEditMode.value ? '试卷更新成功!' : '试卷保存成功!');
|
||||||
content: isEditMode.value ? '试卷更新成功!' : '试卷保存成功!',
|
|
||||||
positiveText: '确定',
|
// 延迟一下让用户看到成功消息
|
||||||
onPositiveClick: () => {
|
setTimeout(() => {
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
// 编辑模式:保存成功后重新加载数据
|
// 编辑模式:保存成功后重新加载数据
|
||||||
loadExamDetail(examId.value);
|
loadExamDetail(examId.value);
|
||||||
} else {
|
} else {
|
||||||
// 新建模式:保存成功后清除表单内容
|
// 新建模式:保存成功后清除表单内容
|
||||||
clearExamForm();
|
clearExamForm();
|
||||||
// 返回试卷列表页面
|
// 返回试卷列表页面
|
||||||
router.back();
|
router.back();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}, 1000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建试卷失败:', error);
|
console.error('创建试卷失败:', error);
|
||||||
dialog.error({
|
message.error('试卷保存失败,请重试');
|
||||||
title: '保存失败',
|
} finally {
|
||||||
content: '试卷保存失败,请重试',
|
// 无论成功还是失败,都要重置保存状态
|
||||||
positiveText: '确定'
|
saving.value = false;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1694,19 +1790,20 @@ const isAutoSaved = ref(false);
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-bottom: 15px 0;
|
margin-bottom: 15px 0;
|
||||||
}
|
}
|
||||||
.q-title{
|
|
||||||
|
.q-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
/* margin-bottom: 10px; */
|
/* margin-bottom: 10px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-question-header{
|
.sub-question-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-question-number{
|
.sub-question-number {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@ -1923,6 +2020,47 @@ const isAutoSaved = ref(false);
|
|||||||
z-index: 1;
|
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) {
|
@media (max-width: 1200px) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user