feat:修复

This commit is contained in:
小张 2025-09-13 15:52:55 +08:00
parent 155db7a1e4
commit c35f54fdd9
4 changed files with 749 additions and 182 deletions

View File

@ -100,6 +100,65 @@ export class ExamApi {
return response
}
/**
*
*/
static async exportRepo(repoId: string): Promise<Blob> {
console.log('🚀 导出题库:', { repoId })
try {
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/aiol/aiolRepo/exportXls?repoId=${repoId}`, {
method: 'GET',
headers: {
'X-Access-Token': localStorage.getItem('token') || '',
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`导出失败: ${response.status} ${response.statusText}`)
}
const blob = await response.blob()
console.log('✅ 题库导出成功:', blob)
return blob
} catch (error) {
console.error('❌ 导出题库失败:', error)
throw error
}
}
/**
*
*/
static async importRepo(repoId: string, file: File): Promise<ApiResponse<any>> {
console.log('🚀 导入题库:', { repoId, fileName: file.name })
try {
const formData = new FormData()
formData.append('file', file)
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/aiol/aiolRepo/importXls?repoId=${repoId}`, {
method: 'POST',
headers: {
'X-Access-Token': localStorage.getItem('token') || ''
},
body: formData
})
if (!response.ok) {
throw new Error(`导入失败: ${response.status} ${response.statusText}`)
}
const result = await response.json()
console.log('✅ 题库导入成功:', result)
return result
} catch (error) {
console.error('❌ 导入题库失败:', error)
throw error
}
}
// ========== 题目管理 ==========
/**
@ -184,7 +243,7 @@ export class ExamApi {
*/
static async updateQuestionOption(data: UpdateQuestionOptionRequest): Promise<ApiResponse<string>> {
console.log('🚀 编辑题目选项:', data)
const response = await ApiRequest.put<string>('/gen/questionoption/questionOption/edit', data)
const response = await ApiRequest.put<string>('/aiol/aiolQuestionOption/edit', data)
console.log('✅ 编辑题目选项成功:', response)
return response
}
@ -204,11 +263,11 @@ export class ExamApi {
// ========== 题目答案管理 ==========
/**
*
* - 使API接口
*/
static async createQuestionAnswer(data: CreateQuestionAnswerRequest): Promise<ApiResponse<string>> {
console.log('🚀 添加题目答案:', data)
const response = await ApiRequest.post<string>('/gen/questionanswer/questionAnswer/add', data)
const response = await ApiRequest.post<string>('/aiol/aiolQuestionAnswer/add', data)
console.log('✅ 添加题目答案成功:', response)
return response
}
@ -218,7 +277,7 @@ export class ExamApi {
*/
static async updateQuestionAnswer(data: UpdateQuestionAnswerRequest): Promise<ApiResponse<string>> {
console.log('🚀 编辑题目答案:', data)
const response = await ApiRequest.put<string>('/gen/questionanswer/questionAnswer/edit', data)
const response = await ApiRequest.put<string>('/aiol/aiolQuestionAnswer/edit', data)
console.log('✅ 编辑题目答案成功:', response)
return response
}

View File

@ -16,54 +16,32 @@
</div>
</div>
<!-- 视频控制工具栏 -->
<div v-if="playerInitialized" class="video-controls-overlay">
<!-- 功能按钮组 -->
<div class="video-function-buttons">
<!-- 清晰度选择器 -->
<div v-if="videoQualities.length > 1" class="dplayer-quality-selector">
<button class="dplayer-control-btn dplayer-quality-btn" @click="showQualityMenu = !showQualityMenu" title="清晰度">
</div>
<!-- 播放器下方的清晰度选择器 -->
<div v-show="props.videoQualities.length > 1" class="video-controls-bottom">
<div class="quality-selector-bottom">
<span class="quality-label">清晰度</span>
<div class="quality-dropdown-bottom">
<button class="quality-btn-bottom" @click="showQualityMenu = !showQualityMenu">
{{ getCurrentQualityLabel() }}
<svg width="12" height="12" viewBox="0 0 12 12" class="quality-dropdown-icon">
<svg width="12" height="12" viewBox="0 0 12 12" class="quality-dropdown-icon" :class="{ 'rotated': showQualityMenu }">
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" fill="none" />
</svg>
</button>
<div v-if="showQualityMenu" class="dplayer-quality-menu">
<div v-for="quality in videoQualities" :key="quality.value"
class="dplayer-quality-option"
:class="{ active: quality.value === currentQuality }"
<div v-if="showQualityMenu" class="quality-menu-bottom">
<div v-for="quality in props.videoQualities" :key="quality.value"
class="quality-option-bottom"
:class="{ active: quality.value === props.currentQuality }"
@click="switchQuality(quality); showQualityMenu = false">
{{ quality.label }}
</div>
</div>
</div>
<!-- 截屏按钮 -->
<button class="dplayer-control-btn" @click="takeScreenshot" title="截屏">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M15 5v6c0 .55-.45 1-1 1H2c-.55 0-1-.45-1-1V5c0-.55.45-1 1-1h1.5l.5-1h7l.5 1H14c.55 0 1 .45 1 1zM8 10.5c1.38 0 2.5-1.12 2.5-2.5S9.38 5.5 8 5.5 5.5 6.62 5.5 8 6.62 10.5 8 10.5z"/>
</svg>
</button>
<!-- 画中画按钮 -->
<button class="dplayer-control-btn" @click="togglePictureInPicture" title="画中画">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 1a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1zm1 0v14h14V1H1zm5.5 4a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V5z"/>
</svg>
</button>
<!-- 弹幕开关 -->
<button class="dplayer-control-btn" @click="toggleDanmaku" :class="{ active: danmakuEnabled }" title="弹幕">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M2.5 3A1.5 1.5 0 0 0 1 4.5v7A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 13.5 3h-11zM2 4.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-7z"/>
<path d="M3.5 6h9v1h-9V6zm0 2h7v1h-7V8zm0 2h5v1h-5v-1z"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</template>
@ -102,7 +80,6 @@ const emit = defineEmits<{
error: [error: any]
qualityChange: [quality: string]
screenshot: [dataUrl: string]
danmakuSend: [text: string]
}>()
const dplayerContainer = ref<HTMLDivElement>()
@ -110,9 +87,7 @@ let player: any = null
const playerInitialized = ref(false)
const isPlaying = ref(false)
//
const danmakuEnabled = ref(true)
const danmakuText = ref('')
//
const isPictureInPicture = ref(false)
const showQualityMenu = ref(false)
@ -224,17 +199,6 @@ const initializePlayer = async (videoUrl?: string) => {
playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2],
loop: false,
screenshot: true, //
danmaku: {
id: 'course-video-' + Date.now(),
api: '/api/danmaku/', // API
token: 'demo-token',
maximum: 1000,
// addition API
// addition: ['https://api.prprpr.me/dplayer/'],
user: 'student',
bottom: '15%',
unlimited: true
},
contextmenu: [
{
text: '截屏',
@ -420,32 +384,7 @@ const togglePictureInPicture = async () => {
}
}
//
const toggleDanmaku = () => {
danmakuEnabled.value = !danmakuEnabled.value
if (player && player.danmaku) {
if (danmakuEnabled.value) {
player.danmaku.show()
} else {
player.danmaku.hide()
}
}
}
const sendDanmaku = () => {
if (danmakuText.value.trim() && player && player.danmaku) {
const danmaku = {
text: danmakuText.value.trim(),
color: '#ffffff',
type: 'right'
}
player.danmaku.send(danmaku)
emit('danmakuSend', danmakuText.value.trim())
danmakuText.value = ''
console.log('发送弹幕:', danmaku.text)
}
}
//
const getCurrentQualityLabel = () => {
@ -540,8 +479,6 @@ defineExpose({
initializePlayer,
takeScreenshot,
togglePictureInPicture,
toggleDanmaku,
sendDanmaku,
switchQuality
})
@ -742,64 +679,98 @@ onUnmounted(() => {
color: #fff;
}
/* 弹幕输入框 */
.danmaku-input-container {
position: absolute;
bottom: 60px;
left: 16px;
right: 16px;
z-index: 15;
}
.danmaku-input-wrapper {
display: flex;
gap: 8px;
background: rgba(0, 0, 0, 0.8);
padding: 8px;
/* 播放器下方的清晰度选择器 */
.video-controls-bottom {
margin-top: 12px;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 8px;
backdrop-filter: blur(4px);
border: 1px solid #e9ecef;
}
.danmaku-input {
flex: 1;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
.quality-selector-bottom {
display: flex;
align-items: center;
gap: 12px;
}
.quality-label {
font-size: 14px;
outline: none;
transition: all 0.2s;
color: #495057;
font-weight: 500;
}
.danmaku-input::placeholder {
color: rgba(255, 255, 255, 0.6);
.quality-dropdown-bottom {
position: relative;
}
.danmaku-input:focus {
background: rgba(255, 255, 255, 0.15);
border-color: #007bff;
}
.danmaku-send-btn {
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
.quality-btn-bottom {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
min-width: 80px;
background: #fff;
color: #495057;
border: 1px solid #ced4da;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
white-space: nowrap;
transition: all 0.2s ease;
justify-content: space-between;
}
.danmaku-send-btn:hover:not(:disabled) {
background: #0056b3;
.quality-btn-bottom:hover {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.danmaku-send-btn:disabled {
background: rgba(255, 255, 255, 0.2);
cursor: not-allowed;
.quality-dropdown-icon {
transition: transform 0.2s ease;
}
.quality-dropdown-icon.rotated {
transform: rotate(180deg);
}
.quality-menu-bottom {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
background: #fff;
border: 1px solid #ced4da;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
overflow: hidden;
}
.quality-option-bottom {
padding: 10px 12px;
font-size: 14px;
color: #495057;
cursor: pointer;
transition: background-color 0.2s ease;
border-bottom: 1px solid #f8f9fa;
}
.quality-option-bottom:last-child {
border-bottom: none;
}
.quality-option-bottom:hover {
background-color: #f8f9fa;
}
.quality-option-bottom.active {
background-color: #007bff;
color: #fff;
}
.quality-option-bottom.active:hover {
background-color: #0056b3;
}
/* 响应式设计 */

View File

@ -328,6 +328,10 @@ const questionForm = reactive({
explanation: '' //
});
// ID
const existingOptionsIds = ref<string[]>([]);
const existingAnswersIds = ref<string[]>([]);
//
const formRules = {
type: {
@ -471,9 +475,8 @@ const saveQuestion = async () => {
//
if (isEditMode.value && questionId) {
// TODO:
message.info('编辑模式暂未实现');
return;
// -
await updateExistingQuestion(questionId);
} else {
// -
await createNewQuestion(bankId);
@ -550,10 +553,14 @@ const createNewQuestion = async (bankId: string) => {
console.log('✅ 题目创建成功题目ID:', questionId);
//
//
const questionType = getQuestionTypeNumber(questionForm.type);
if (questionType === 0 || questionType === 1 || questionType === 2) { //
if (questionType === 0 || questionType === 1 || questionType === 2) {
//
await createQuestionOptions(questionId, questionType);
} else if (questionType === 3 || questionType === 4) {
//
await createQuestionAnswers(questionId, questionType);
}
message.success('题目创建成功!');
@ -582,9 +589,15 @@ const createQuestionOptions = async (questionId: string, questionType: number) =
const options = questionForm.options || [];
const correctAnswer = questionForm.correctAnswer;
console.log('🔍 单选题选项创建逻辑:', {
options,
correctAnswer,
correctAnswerType: typeof correctAnswer
});
optionsToCreate = options.map((option: any, index: number) => ({
content: option.content || option.text || '',
izCorrent: option.letter === correctAnswer ? 1 : 0,
izCorrent: index === correctAnswer ? 1 : 0,
orderNo: index
}));
@ -592,16 +605,29 @@ const createQuestionOptions = async (questionId: string, questionType: number) =
const options = questionForm.options || [];
const correctAnswers = questionForm.correctAnswers || [];
console.log('🔍 多选题选项创建逻辑:', {
options,
correctAnswers,
correctAnswersType: typeof correctAnswers
});
optionsToCreate = options.map((option: any, index: number) => ({
content: option.content || option.text || '',
izCorrent: correctAnswers.includes(option.letter) ? 1 : 0,
izCorrent: correctAnswers.includes(index) ? 1 : 0,
orderNo: index
}));
} else if (questionType === 2) { //
//
const correctAnswer = questionForm.correctAnswer;
const isCorrectTrue = String(correctAnswer) === 'true' || correctAnswer === 1;
const trueFalseAnswer = questionForm.trueFalseAnswer;
const isCorrectTrue = trueFalseAnswer === true;
console.log('🔍 判断题选项创建逻辑:', {
trueFalseAnswer,
isCorrectTrue,
trueFalseAnswerType: typeof trueFalseAnswer
});
optionsToCreate = [
{
content: '正确',
@ -640,6 +666,231 @@ const createQuestionOptions = async (questionId: string, questionType: number) =
}
};
//
const createQuestionAnswers = async (questionId: string, questionType: number) => {
try {
console.log('🚀 开始创建题目答案题目ID:', questionId, '题目类型:', questionType);
let answersToCreate: Array<{
answerText: string;
orderNo: number;
}> = [];
if (questionType === 3) { //
const fillBlankAnswers = questionForm.fillBlankAnswers || [];
answersToCreate = fillBlankAnswers.map((answer: any, index: number) => ({
answerText: answer.content || '',
orderNo: index
}));
} else if (questionType === 4) { //
const shortAnswer = questionForm.shortAnswer || '';
if (shortAnswer.trim()) {
answersToCreate = [{
answerText: shortAnswer,
orderNo: 0
}];
}
}
console.log('📊 准备创建的答案:', answersToCreate);
//
for (const answer of answersToCreate) {
const answerData = {
questionId: questionId,
answerText: answer.answerText,
orderNo: answer.orderNo
};
console.log('🚀 创建答案:', answerData);
const answerResponse = await ExamApi.createQuestionAnswer(answerData);
console.log('✅ 答案创建成功:', answerResponse);
}
console.log('✅ 所有答案创建完成');
} catch (error: any) {
console.error('❌ 创建题目答案失败:', error);
throw new Error('创建题目答案失败:' + (error.message || '未知错误'));
}
};
//
const updateExistingQuestion = async (questionId: string) => {
try {
console.log('🚀 开始更新题目题目ID:', questionId);
// 1.
const questionData = {
id: questionId,
parentId: undefined,
type: getQuestionTypeNumber(questionForm.type),
content: questionForm.title,
analysis: questionForm.explanation || '',
difficulty: getDifficultyNumber(questionForm.difficulty),
score: questionForm.score
};
console.log('🚀 更新题目基本信息:', questionData);
const questionResponse = await ExamApi.updateQuestion(questionData);
console.log('✅ 题目基本信息更新成功:', questionResponse);
// 2.
const questionType = getQuestionTypeNumber(questionForm.type);
if (questionType === 0 || questionType === 1 || questionType === 2) {
//
await updateQuestionOptions(questionId, questionType);
} else if (questionType === 3 || questionType === 4) {
//
await updateQuestionAnswers(questionId, questionType);
}
message.success('题目更新成功!');
router.back();
} catch (error: any) {
console.error('❌ 更新题目失败:', error);
message.error(error.message || '更新题目失败');
throw error;
}
};
//
const updateQuestionAnswers = async (questionId: string, questionType: number) => {
try {
console.log('🚀 开始更新题目答案题目ID:', questionId, '题目类型:', questionType);
let answersToUpdate: Array<{
id: string;
answerText: string;
orderNo: number;
}> = [];
if (questionType === 3) { //
const fillBlankAnswers = questionForm.fillBlankAnswers || [];
answersToUpdate = fillBlankAnswers.map((answer: any, index: number) => ({
id: existingAnswersIds.value[index] || '',
answerText: answer.content || '',
orderNo: index
})).filter(answer => answer.id); // ID
} else if (questionType === 4) { //
const shortAnswer = questionForm.shortAnswer || '';
if (shortAnswer.trim() && existingAnswersIds.value.length > 0) {
answersToUpdate = [{
id: existingAnswersIds.value[0],
answerText: shortAnswer,
orderNo: 0
}];
}
}
console.log('📊 准备更新的答案:', answersToUpdate);
//
for (const answer of answersToUpdate) {
const answerData = {
id: answer.id,
questionId: questionId,
answerText: answer.answerText,
orderNo: answer.orderNo
};
console.log('🚀 更新答案:', answerData);
const answerResponse = await ExamApi.updateQuestionAnswer(answerData);
console.log('✅ 答案更新成功:', answerResponse);
}
console.log('✅ 所有答案更新完成');
} catch (error: any) {
console.error('❌ 更新题目答案失败:', error);
throw new Error('更新题目答案失败:' + (error.message || '未知错误'));
}
};
//
const updateQuestionOptions = async (questionId: string, questionType: number) => {
try {
console.log('🚀 开始更新题目选项题目ID:', questionId, '题目类型:', questionType);
let optionsToUpdate: Array<{
id: string;
content: string;
izCorrent: number;
orderNo: number;
}> = [];
if (questionType === 0) { //
const options = questionForm.options || [];
const correctAnswer = questionForm.correctAnswer;
optionsToUpdate = options.map((option: any, index: number) => ({
id: existingOptionsIds.value[index] || '',
content: option.content || '',
izCorrent: index === correctAnswer ? 1 : 0,
orderNo: index
})).filter(option => option.id); // ID
} else if (questionType === 1) { //
const options = questionForm.options || [];
const correctAnswers = questionForm.correctAnswers || [];
optionsToUpdate = options.map((option: any, index: number) => ({
id: existingOptionsIds.value[index] || '',
content: option.content || '',
izCorrent: correctAnswers.includes(index) ? 1 : 0,
orderNo: index
})).filter(option => option.id); // ID
} else if (questionType === 2) { //
const correctAnswer = questionForm.trueFalseAnswer;
const isCorrectTrue = correctAnswer === true;
if (existingOptionsIds.value.length >= 2) {
optionsToUpdate = [
{
id: existingOptionsIds.value[0],
content: '正确',
izCorrent: isCorrectTrue ? 1 : 0,
orderNo: 0
},
{
id: existingOptionsIds.value[1],
content: '错误',
izCorrent: isCorrectTrue ? 0 : 1,
orderNo: 1
}
];
}
}
console.log('📊 准备更新的选项:', optionsToUpdate);
//
for (const option of optionsToUpdate) {
const optionData = {
id: option.id,
questionId: questionId,
content: option.content,
izCorrent: option.izCorrent,
orderNo: option.orderNo
};
console.log('🚀 更新选项:', optionData);
const optionResponse = await ExamApi.updateQuestionOption(optionData);
console.log('✅ 选项更新成功:', optionResponse);
}
console.log('✅ 所有选项更新完成');
} catch (error: any) {
console.error('❌ 更新题目选项失败:', error);
throw new Error('更新题目选项失败:' + (error.message || '未知错误'));
}
};
//
@ -911,69 +1162,81 @@ const renderQuestionData = (questionData: any) => {
};
//
const renderSingleChoiceData = (answers: any[]) => {
console.log('🔘 渲染单选题数据:', answers);
const renderSingleChoiceData = (options: any[]) => {
console.log('🔘 渲染单选题数据:', options);
if (answers && answers.length > 0) {
if (options && options.length > 0) {
// orderNo
const sortedAnswers = answers.sort((a, b) => a.orderNo - b.orderNo);
const sortedOptions = options.sort((a, b) => a.orderNo - b.orderNo);
// ID
existingOptionsIds.value = sortedOptions.map(option => option.id);
//
questionForm.options = sortedAnswers.map(answer => ({
content: answer.content || ''
questionForm.options = sortedOptions.map(option => ({
content: option.content || ''
}));
// orderNo10
const correctAnswer = sortedAnswers.find(answer => answer.izCorrent === 1);
if (correctAnswer) {
questionForm.correctAnswer = correctAnswer.orderNo - 1;
//
const correctOption = sortedOptions.find(option => option.izCorrent === 1);
if (correctOption) {
questionForm.correctAnswer = correctOption.orderNo;
}
console.log('✅ 单选题渲染完成:', {
options: questionForm.options,
correctAnswer: questionForm.correctAnswer
correctAnswer: questionForm.correctAnswer,
existingOptionsIds: existingOptionsIds.value
});
}
};
//
const renderMultipleChoiceData = (answers: any[]) => {
console.log('☑️ 渲染多选题数据:', answers);
const renderMultipleChoiceData = (options: any[]) => {
console.log('☑️ 渲染多选题数据:', options);
if (answers && answers.length > 0) {
if (options && options.length > 0) {
// orderNo
const sortedAnswers = answers.sort((a, b) => a.orderNo - b.orderNo);
const sortedOptions = options.sort((a, b) => a.orderNo - b.orderNo);
// ID
existingOptionsIds.value = sortedOptions.map(option => option.id);
//
questionForm.options = sortedAnswers.map(answer => ({
content: answer.content || ''
questionForm.options = sortedOptions.map(option => ({
content: option.content || ''
}));
//
questionForm.correctAnswers = sortedAnswers
.filter(answer => answer.izCorrent === 1)
.map(answer => answer.orderNo - 1);
questionForm.correctAnswers = sortedOptions
.filter(option => option.izCorrent === 1)
.map(option => option.orderNo);
console.log('✅ 多选题渲染完成:', {
options: questionForm.options,
correctAnswers: questionForm.correctAnswers
correctAnswers: questionForm.correctAnswers,
existingOptionsIds: existingOptionsIds.value
});
}
};
//
const renderTrueFalseData = (answers: any[]) => {
console.log('✔️ 渲染判断题数据:', answers);
const renderTrueFalseData = (options: any[]) => {
console.log('✔️ 渲染判断题数据:', options);
if (answers && answers.length > 0) {
const correctAnswer = answers.find(answer => answer.izCorrent === 1);
if (correctAnswer) {
if (options && options.length > 0) {
// ID
existingOptionsIds.value = options.map(option => option.id);
const correctOption = options.find(option => option.izCorrent === 1);
if (correctOption) {
// content""""
questionForm.trueFalseAnswer = correctAnswer.content === '正确' || correctAnswer.content === 'true';
questionForm.trueFalseAnswer = correctOption.content === '正确' || correctOption.content === 'true';
}
console.log('✅ 判断题渲染完成:', {
trueFalseAnswer: questionForm.trueFalseAnswer
trueFalseAnswer: questionForm.trueFalseAnswer,
existingOptionsIds: existingOptionsIds.value
});
}
};
@ -983,14 +1246,18 @@ const renderFillBlankData = (answers: any[]) => {
console.log('📝 渲染填空题数据:', answers);
if (answers && answers.length > 0) {
// ID
existingAnswersIds.value = answers.map(answer => answer.id);
questionForm.fillBlankAnswers = answers.map(answer => ({
content: answer.content || '',
content: answer.answerText || answer.content || '',
score: 1,
caseSensitive: false
}));
console.log('✅ 填空题渲染完成:', {
fillBlankAnswers: questionForm.fillBlankAnswers
fillBlankAnswers: questionForm.fillBlankAnswers,
existingAnswersIds: existingAnswersIds.value
});
}
};
@ -1000,10 +1267,14 @@ const renderShortAnswerData = (answers: any[]) => {
console.log('📄 渲染简答题数据:', answers);
if (answers && answers.length > 0) {
questionForm.shortAnswer = answers[0]?.content || '';
// ID
existingAnswersIds.value = answers.map(answer => answer.id);
questionForm.shortAnswer = answers[0]?.answerText || answers[0]?.content || '';
console.log('✅ 简答题渲染完成:', {
shortAnswer: questionForm.shortAnswer
shortAnswer: questionForm.shortAnswer,
existingAnswersIds: existingAnswersIds.value
});
}
};

View File

@ -46,18 +46,74 @@
</n-modal>
<!-- 导入弹窗 -->
<ImportModal v-model:show="showImportModal" template-name="question_bank_template.xlsx"
import-type="questionBank" @success="handleImportSuccess" @template-download="handleTemplateDownload" />
<n-modal v-model:show="showImportModal" preset="dialog" title="导入题库" style="width: 600px;">
<div class="import-content">
<div class="import-info">
<p>将题目导入到题库<strong>{{ getSelectedBankName() }}</strong></p>
<p class="import-tip">请选择要导入的Excel文件支持.xlsx, .xls格式</p>
</div>
<n-upload
ref="uploadRef"
v-model:file-list="importFileList"
:max="1"
accept=".xlsx,.xls"
:custom-request="handleImportUpload"
@change="handleImportFileChange"
show-file-list
list-type="text"
:default-upload="false"
>
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3">
<CloudUploadOutline />
</n-icon>
</div>
<n-text style="font-size: 16px">
点击或者拖动文件到该区域来上传
</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">
请上传Excel格式的题库文件
</n-p>
</n-upload-dragger>
</n-upload>
<div v-if="importResult" class="import-result">
<n-alert :type="importResult.success ? 'success' : 'error'" :title="importResult.message">
<div v-if="importResult.details">
<p>成功导入{{ importResult.details.success }} </p>
<p v-if="importResult.details.failed > 0">失败{{ importResult.details.failed }} </p>
</div>
</n-alert>
</div>
</div>
<template #action>
<n-space>
<n-button @click="closeImportModal">取消</n-button>
<n-button
type="primary"
@click="startImport"
:loading="importing"
:disabled="!selectedImportFile"
>
{{ importing ? '导入中...' : '开始导入' }}
</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, h, VNode } from 'vue';
import { NButton, NSpace, NSelect, useMessage, useDialog } from 'naive-ui';
import { NButton, NSpace, NSelect, NIcon, NText, NP, NAlert, NUpload, NUploadDragger, useMessage, useDialog } from 'naive-ui';
import { CloudUploadOutline } from '@vicons/ionicons5';
import { useRouter, useRoute } from 'vue-router';
import ImportModal from '@/components/common/ImportModal.vue';
import { ExamApi } from '@/api';
import type { Repo } from '@/api/types';
import type { UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui';
//
const message = useMessage();
@ -112,6 +168,17 @@ const createForm = reactive({
//
const showImportModal = ref(false);
const importFileList = ref<UploadFileInfo[]>([]);
const selectedImportFile = ref<File | null>(null);
const importing = ref(false);
const importResult = ref<{
success: boolean;
message: string;
details?: {
success: number;
failed: number;
};
} | null>(null);
//
const pagination = reactive({
@ -375,12 +442,63 @@ const addQuestionBank = () => {
};
const importQuestionBank = () => {
console.log('导入题库,选中的题库:', selectedRowKeys.value);
if (selectedRowKeys.value.length === 0) {
message.warning('请先选择要导入到的题库');
return;
}
if (selectedRowKeys.value.length > 1) {
message.warning('一次只能向一个题库导入,请选择单个题库');
return;
}
showImportModal.value = true;
};
const exportQuestionBank = () => {
console.log('导出题库');
message.info('导出功能开发中...');
const exportQuestionBank = async () => {
console.log('导出题库,选中的题库:', selectedRowKeys.value);
if (selectedRowKeys.value.length === 0) {
message.warning('请先选择要导出的题库');
return;
}
if (selectedRowKeys.value.length > 1) {
message.warning('一次只能导出一个题库,请选择单个题库');
return;
}
const repoId = selectedRowKeys.value[0];
const selectedBank = questionBankList.value.find(bank => bank.id === repoId);
const bankName = selectedBank?.name || '题库';
try {
message.loading('正在导出题库,请稍候...', { duration: 0 });
console.log('🚀 开始导出题库:', { repoId, bankName });
const blob = await ExamApi.exportRepo(repoId);
//
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${bankName}_题库导出_${new Date().toISOString().slice(0, 10)}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.destroyAll();
message.success('题库导出成功!');
console.log('✅ 题库导出完成');
} catch (error: any) {
message.destroyAll();
console.error('❌ 导出题库失败:', error);
message.error(error.message || '导出题库失败,请重试');
}
};
const deleteSelected = () => {
@ -550,7 +668,125 @@ const submitQuestionBank = async () => {
}
};
//
//
const getSelectedBankName = () => {
if (selectedRowKeys.value.length === 0) return '';
const selectedBank = questionBankList.value.find(bank => bank.id === selectedRowKeys.value[0]);
return selectedBank?.name || '';
};
//
const handleImportFileChange = (options: { fileList: UploadFileInfo[] }) => {
console.log('文件列表变化:', options.fileList);
if (options.fileList.length > 0) {
selectedImportFile.value = options.fileList[0].file || null;
} else {
selectedImportFile.value = null;
}
importResult.value = null;
};
//
const handleImportUpload = (options: UploadCustomRequestOptions) => {
const { file, onProgress, onFinish, onError } = options;
// (10MB)
if (file.file && file.file.size > 10 * 1024 * 1024) {
message.error('文件大小不能超过 10MB');
onError();
return;
}
//
if (file.file && !file.file.name.match(/\.(xlsx|xls)$/i)) {
message.error('只支持 Excel 文件格式');
onError();
return;
}
//
let progress = 0;
const progressInterval = setInterval(() => {
if (progress < 90) {
progress += Math.random() * 20;
onProgress({ percent: Math.min(progress, 90) });
}
}, 200);
//
setTimeout(() => {
clearInterval(progressInterval);
onProgress({ percent: 100 });
onFinish();
console.log('文件准备完成:', file.name);
}, 1000);
};
//
const startImport = async () => {
if (!selectedImportFile.value) {
message.warning('请先选择要导入的文件');
return;
}
if (selectedRowKeys.value.length === 0) {
message.warning('请先选择目标题库');
return;
}
const repoId = selectedRowKeys.value[0];
importing.value = true;
importResult.value = null;
try {
console.log('🚀 开始导入题库:', { repoId, fileName: selectedImportFile.value.name });
const result = await ExamApi.importRepo(repoId, selectedImportFile.value);
console.log('✅ 导入结果:', result);
if (result.success || result.code === 200) {
importResult.value = {
success: true,
message: '题库导入成功!',
details: {
success: result.data?.success || 0,
failed: result.data?.failed || 0
}
};
message.success('题库导入成功!');
//
setTimeout(() => {
loadQuestionBanks();
closeImportModal();
}, 2000);
} else {
throw new Error(result.message || '导入失败');
}
} catch (error: any) {
console.error('❌ 导入题库失败:', error);
importResult.value = {
success: false,
message: error.message || '导入失败,请重试'
};
message.error(error.message || '导入失败,请重试');
} finally {
importing.value = false;
}
};
//
const closeImportModal = () => {
showImportModal.value = false;
importFileList.value = [];
selectedImportFile.value = null;
importing.value = false;
importResult.value = null;
};
//
const handleImportSuccess = (result: any) => {
console.log('导入成功:', result);
message.success('题库导入成功');
@ -670,4 +906,34 @@ onMounted(() => {
width: 80px !important;
}
}
/* 导入弹窗样式 */
.import-content {
padding: 16px 0;
}
.import-info {
margin-bottom: 20px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 6px;
}
.import-info p {
margin: 0 0 8px 0;
font-size: 14px;
}
.import-info p:last-child {
margin-bottom: 0;
}
.import-tip {
color: #666;
font-size: 13px !important;
}
.import-result {
margin-top: 16px;
}
</style>