|
|
@@ -72,7 +72,11 @@
|
|
|
<!-- 中间主操作区 -->
|
|
|
<main class="main-content" style="padding-top: 36px; position: relative;">
|
|
|
<!-- 返回AI问答按钮 -->
|
|
|
- <button v-if="!showExamDetail" class="return-ai-btn has-before" @click="handleReturnToAI">
|
|
|
+ <button v-if="!showExamDetail" class="return-ai-btn" @click="handleReturnToAI">
|
|
|
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <circle cx="12" cy="12" r="12" fill="white" stroke="#E5E7EB" stroke-width="1"/>
|
|
|
+ <path d="M14 7L9 12L14 17" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>
|
|
|
返回AI问答
|
|
|
</button>
|
|
|
|
|
|
@@ -86,7 +90,7 @@
|
|
|
<label class="form-label">出题依据内容</label>
|
|
|
<textarea class="form-control" v-model="questionBasis" placeholder="在此输入知识点、章节或培训内容..." :disabled="isGenerating || uploadedFiles.length > 0"></textarea>
|
|
|
|
|
|
- <div class="ppt-upload-section" style="flex-direction: column; align-items: flex-start;" @click="!isGenerating ? triggerFileUpload() : null">
|
|
|
+ <div class="ppt-upload-section" style="flex-direction: column; align-items: flex-start; justify-content: flex-start;" @click="!isGenerating ? triggerFileUpload() : null">
|
|
|
<div style="display: flex; width: 100%; justify-content: space-between; align-items: center;">
|
|
|
<div class="ppt-upload-content">
|
|
|
<div class="ppt-upload-icon-wrapper">
|
|
|
@@ -127,7 +131,7 @@
|
|
|
</div>
|
|
|
<div class="slider-container">
|
|
|
<span class="slider-label">数量</span>
|
|
|
- <input type="range" class="question-slider" v-model.number="type.questionCount" min="0" :max="type.max || 50" :disabled="isGenerating">
|
|
|
+ <input type="range" class="question-slider" v-model.number="type.questionCount" :min="type.name === '单选题' ? 1 : 0" :max="type.max || 50" :disabled="isGenerating" @input="validateQuestionCount(type)">
|
|
|
<div class="question-count-stepper">
|
|
|
<span class="question-count">{{ type.questionCount }} 题</span>
|
|
|
<div class="stepper-buttons">
|
|
|
@@ -142,7 +146,7 @@
|
|
|
class="stepper-btn stepper-btn-down"
|
|
|
type="button"
|
|
|
@click="adjustQuestionCount(type, -1)"
|
|
|
- :disabled="isGenerating || type.questionCount <= 0"
|
|
|
+ :disabled="isGenerating || (type.name === '单选题' ? type.questionCount <= 1 : type.questionCount <= 0)"
|
|
|
aria-label="减少题目数量"
|
|
|
></button>
|
|
|
</div>
|
|
|
@@ -195,7 +199,7 @@
|
|
|
</div>
|
|
|
|
|
|
<div class="preview-footer">
|
|
|
- <div class="preview-total">
|
|
|
+ <div class="preview-total" style="font-size: 20px; color: #000000;">
|
|
|
<span>配置总分</span>
|
|
|
<span class="preview-total-score" style="color: #000000; font-size: 24px;">{{ totalScore }}</span>
|
|
|
</div>
|
|
|
@@ -216,15 +220,24 @@
|
|
|
<div class="header-left">
|
|
|
</div>
|
|
|
<div class="header-right" style="display: flex; align-items: center; gap: 12px;">
|
|
|
- <button class="return-ai-btn has-before" style="position: static;" @click="backToConfig" :disabled="isGenerating">
|
|
|
+ <button class="return-ai-btn" style="position: static;" @click="backToConfig" :disabled="isGenerating">
|
|
|
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <circle cx="12" cy="12" r="12" fill="white" stroke="#E5E7EB" stroke-width="1"/>
|
|
|
+ <path d="M14 7L9 12L14 17" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>
|
|
|
返回修改
|
|
|
</button>
|
|
|
<!-- <button class="save-btn" @click="saveExam" :disabled="isGenerating">
|
|
|
<img :src="saveIcon" alt="保存试卷" class="save-icon" />
|
|
|
</button> -->
|
|
|
<div class="download-dropdown" :class="{ 'disabled': isGenerating, 'show': showDownloadMenu }" @click.stop>
|
|
|
- <button class="download-btn" :disabled="isGenerating" @click="toggleDownloadMenu">
|
|
|
- <img :src="downloadIcon" alt="下载Word" class="download-icon" />
|
|
|
+ <button class="download-btn-new" :disabled="isGenerating" @click="toggleDownloadMenu">
|
|
|
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <rect x="1.5" y="1.5" width="21" height="21" rx="5" fill="white" stroke="#E5E7EB" stroke-width="1.5"/>
|
|
|
+ <path d="M12 15L7.5 10H10.5V5H13.5V10H16.5L12 15Z" fill="currentColor"/>
|
|
|
+ <path d="M6 17.5H18V19.5H6V17.5Z" fill="currentColor"/>
|
|
|
+ </svg>
|
|
|
+ <span class="download-icon-text">下载Word</span>
|
|
|
</button>
|
|
|
<div class="dropdown-menu">
|
|
|
<div class="dropdown-item" @click="exportToWordWithAnswers" :disabled="isGenerating">
|
|
|
@@ -317,6 +330,10 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div class="answer-section">
|
|
|
+ <span class="answer-label">正确答案:</span>
|
|
|
+ <span class="answer-value">{{ question.selectedAnswer || '未设置' }}</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -384,6 +401,10 @@
|
|
|
<span class="option-text">错误</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div class="answer-section">
|
|
|
+ <span class="answer-label">正确答案:</span>
|
|
|
+ <span class="answer-value">{{ question.selectedAnswer || '未设置' }}</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -452,6 +473,10 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div class="answer-section">
|
|
|
+ <span class="answer-label">正确答案:</span>
|
|
|
+ <span class="answer-value">{{ (question.selectedAnswers || []).join(', ') || '未设置' }}</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -820,7 +845,7 @@ const projectTypes = {
|
|
|
|
|
|
// 题型配置
|
|
|
const questionTypes = ref([
|
|
|
- { name: "单选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "一" },
|
|
|
+ { name: "单选题", scorePerQuestion: 0, questionCount: 1, romanNumeral: "一" },
|
|
|
{ name: "判断题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "二" },
|
|
|
{ name: "多选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "三" },
|
|
|
{ name: "简答题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "四" },
|
|
|
@@ -922,7 +947,7 @@ const createNewChat = async () => {
|
|
|
} else {
|
|
|
// 如果没有初始配置,使用默认配置
|
|
|
questionTypes.value = [
|
|
|
- { name: "单选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "一" },
|
|
|
+ { name: "单选题", scorePerQuestion: 0, questionCount: 1, romanNumeral: "一" },
|
|
|
{ name: "判断题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "二" },
|
|
|
{ name: "多选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "三" },
|
|
|
{ name: "简答题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "四" },
|
|
|
@@ -1034,7 +1059,7 @@ const clearSettings = () => {
|
|
|
// 保留原数组引用,更新每个对象的属性,避免破坏 Vue 3 响应式绑定
|
|
|
questionTypes.value.forEach(type => {
|
|
|
type.scorePerQuestion = 0;
|
|
|
- type.questionCount = 0;
|
|
|
+ type.questionCount = type.name === '单选题' ? 1 : 0;
|
|
|
});
|
|
|
console.log("清除设置");
|
|
|
};
|
|
|
@@ -1073,7 +1098,9 @@ const validateQuestionCount = (type) => {
|
|
|
type.questionCount = 99;
|
|
|
ElMessage.warning(`${type.name}题目数量不能超过99题`);
|
|
|
}
|
|
|
- if (type.questionCount < 0) {
|
|
|
+ if (type.name === '单选题' && type.questionCount < 1) {
|
|
|
+ type.questionCount = 1;
|
|
|
+ } else if (type.questionCount < 0) {
|
|
|
type.questionCount = 0;
|
|
|
}
|
|
|
};
|
|
|
@@ -1122,6 +1149,14 @@ const generateExam = async () => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // 检查出题依据内容是否为空
|
|
|
+ const pptContents = uploadedFiles.value.map(file => file.content).join('\n\n');
|
|
|
+ const finalContentBasis = pptContents || questionBasis.value || '';
|
|
|
+ if (!finalContentBasis.trim()) {
|
|
|
+ ElMessage.warning("请输入出题依据内容或上传PPT文件");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
console.log("生成试卷:", {
|
|
|
function: selectedFunction.value,
|
|
|
projectType: projectTypes[selectedProjectType.value].name,
|
|
|
@@ -1265,10 +1300,8 @@ const generateAIExam = async () => {
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('AI生成试卷失败:', error);
|
|
|
- ElMessage.error('AI生成试卷失败,请稍后重试或检查网络连接');
|
|
|
-
|
|
|
- // 失败时显示默认试卷
|
|
|
- showExamDetail.value = true;
|
|
|
+ ElMessage.error(error?.message || 'AI生成试卷失败,请稍后重试或检查网络连接');
|
|
|
+ showExamDetail.value = false;
|
|
|
} finally {
|
|
|
// 重置发送状态
|
|
|
isGenerating.value = false;
|
|
|
@@ -1315,27 +1348,166 @@ const extractExamDataFromContent = (content) => {
|
|
|
throw new Error('试卷内容为空');
|
|
|
}
|
|
|
|
|
|
- const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
|
- if (!jsonMatch) {
|
|
|
- throw new Error('未找到有效的JSON数据');
|
|
|
+ const cleaned = String(content)
|
|
|
+ .replace(/\uFEFF/g, '')
|
|
|
+ .replace(/```(?:json)?/gi, '')
|
|
|
+ .replace(/```/g, '')
|
|
|
+ .trim();
|
|
|
+
|
|
|
+ const extractJsonObjects = (text) => {
|
|
|
+ const objects = [];
|
|
|
+ let start = -1;
|
|
|
+ let depth = 0;
|
|
|
+ let inString = false;
|
|
|
+ let stringQuote = '"';
|
|
|
+ let escaped = false;
|
|
|
+
|
|
|
+ for (let i = 0; i < text.length; i++) {
|
|
|
+ const ch = text[i];
|
|
|
+ if (escaped) {
|
|
|
+ escaped = false;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (inString) {
|
|
|
+ if (ch === '\\') {
|
|
|
+ escaped = true;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (ch === stringQuote) {
|
|
|
+ inString = false;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ch === '"' || ch === "'") {
|
|
|
+ inString = true;
|
|
|
+ stringQuote = ch;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ch === '{') {
|
|
|
+ if (depth === 0) {
|
|
|
+ start = i;
|
|
|
+ }
|
|
|
+ depth += 1;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ch === '}' && depth > 0) {
|
|
|
+ depth -= 1;
|
|
|
+ if (depth === 0 && start >= 0) {
|
|
|
+ objects.push(text.slice(start, i + 1));
|
|
|
+ start = -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return objects;
|
|
|
+ };
|
|
|
+
|
|
|
+ const looksLikeExamPayload = (parsed) => {
|
|
|
+ if (!parsed || typeof parsed !== 'object') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return Boolean(
|
|
|
+ parsed.singleChoice ||
|
|
|
+ parsed.single_choice ||
|
|
|
+ parsed['单选题'] ||
|
|
|
+ parsed.judge ||
|
|
|
+ parsed['判断题'] ||
|
|
|
+ parsed.multiple ||
|
|
|
+ parsed.multiple_choice ||
|
|
|
+ parsed.multipleChoice ||
|
|
|
+ parsed['多选题'] ||
|
|
|
+ parsed.short ||
|
|
|
+ parsed.short_answer ||
|
|
|
+ parsed.shortAnswer ||
|
|
|
+ parsed['简答题'] ||
|
|
|
+ parsed.questions?.single_choice ||
|
|
|
+ parsed.questions?.singleChoice ||
|
|
|
+ parsed.questions?.['单选题'] ||
|
|
|
+ parsed.questions?.judge ||
|
|
|
+ parsed.questions?.['判断题'] ||
|
|
|
+ parsed.questions?.multiple ||
|
|
|
+ parsed.questions?.multiple_choice ||
|
|
|
+ parsed.questions?.multipleChoice ||
|
|
|
+ parsed.questions?.['多选题'] ||
|
|
|
+ parsed.questions?.short ||
|
|
|
+ parsed.questions?.short_answer ||
|
|
|
+ parsed.questions?.shortAnswer ||
|
|
|
+ parsed.questions?.['简答题']
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const candidates = extractJsonObjects(cleaned);
|
|
|
+ if (!candidates.length) {
|
|
|
+ throw new Error('未找到完整的JSON对象');
|
|
|
+ }
|
|
|
+
|
|
|
+ const parsedCandidates = [];
|
|
|
+ for (const candidate of candidates) {
|
|
|
+ try {
|
|
|
+ const normalizedJson = candidate
|
|
|
+ .replace(/[“”]/g, '"')
|
|
|
+ .replace(/,\s*([}\]])/g, '$1');
|
|
|
+ parsedCandidates.push(JSON.parse(normalizedJson));
|
|
|
+ } catch (_) {
|
|
|
+ // 跳过无效片段,继续寻找真正的试卷 JSON
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const examCandidate = parsedCandidates.find(looksLikeExamPayload);
|
|
|
+ if (examCandidate) {
|
|
|
+ return examCandidate;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (parsedCandidates.length > 0) {
|
|
|
+ return parsedCandidates.sort((a, b) => JSON.stringify(b).length - JSON.stringify(a).length)[0];
|
|
|
}
|
|
|
|
|
|
- return JSON.parse(jsonMatch[0]);
|
|
|
+ throw new Error('未找到可用的试卷JSON数据');
|
|
|
};
|
|
|
|
|
|
const parseAIExamResponse = (aiReply) => {
|
|
|
+ const hasPlaceholderContent = (value) => {
|
|
|
+ if (Array.isArray(value)) {
|
|
|
+ return value.some(item => hasPlaceholderContent(item));
|
|
|
+ }
|
|
|
+ if (value && typeof value === 'object') {
|
|
|
+ return Object.values(value).some(item => hasPlaceholderContent(item));
|
|
|
+ }
|
|
|
+ if (typeof value === 'string') {
|
|
|
+ const text = value.trim();
|
|
|
+ return [
|
|
|
+ /^题目内容$/,
|
|
|
+ /^解析内容$/,
|
|
|
+ /^参考措施$/,
|
|
|
+ /^答题要点$/,
|
|
|
+ /^具体选项内容$/,
|
|
|
+ /^选项[ABCD]$/,
|
|
|
+ /^.+工程相关(?:单选题|多选题|判断题|简答题)\d+$/,
|
|
|
+ /^.+相关(?:单选题|多选题|判断题|简答题)\d+$/
|
|
|
+ ].some(pattern => pattern.test(text));
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ };
|
|
|
+
|
|
|
try {
|
|
|
const examData = extractExamDataFromContent(aiReply);
|
|
|
const normalizedExam = normalizeGeneratedExam(examData);
|
|
|
|
|
|
+ if (hasPlaceholderContent(normalizedExam)) {
|
|
|
+ throw new Error('AI返回的是占位题目,不是可用的具体试题');
|
|
|
+ }
|
|
|
+
|
|
|
// 确保所有题目都有正确的初始值
|
|
|
ensureQuestionInitialValues(normalizedExam);
|
|
|
|
|
|
return normalizedExam;
|
|
|
} catch (error) {
|
|
|
console.error('解析AI回复失败:', error);
|
|
|
- // 返回默认试卷结构
|
|
|
- return generateDefaultExam();
|
|
|
+ throw error;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -1375,35 +1547,35 @@ const normalizeQuestions = (questions = [], sectionKey) => {
|
|
|
return questions.map((question = {}) => {
|
|
|
if (sectionKey === 'singleChoice') {
|
|
|
return {
|
|
|
- text: question.text || question.question_text || "",
|
|
|
+ text: question.text || question.question_text || question.question || question.title || question.content || question['题干'] || question['题目'] || "",
|
|
|
options: normalizeOptions(question.options),
|
|
|
- selectedAnswer: question.selectedAnswer || question.correct_answer || question.answer || "",
|
|
|
- analysis: question.analysis || question.explanation || "",
|
|
|
+ selectedAnswer: question.selectedAnswer || question.correct_answer || question.answer || question['正确答案'] || question['答案'] || "",
|
|
|
+ analysis: question.analysis || question.explanation || question['解析'] || "",
|
|
|
};
|
|
|
}
|
|
|
|
|
|
if (sectionKey === 'judge') {
|
|
|
return {
|
|
|
- text: question.text || question.question_text || "",
|
|
|
- selectedAnswer: question.selectedAnswer || question.correct_answer || question.answer || "",
|
|
|
- analysis: question.analysis || question.explanation || "",
|
|
|
+ text: question.text || question.question_text || question.question || question.title || question.content || question['题干'] || question['题目'] || "",
|
|
|
+ selectedAnswer: question.selectedAnswer || question.correct_answer || question.answer || question['正确答案'] || question['答案'] || "",
|
|
|
+ analysis: question.analysis || question.explanation || question['解析'] || "",
|
|
|
};
|
|
|
}
|
|
|
|
|
|
if (sectionKey === 'multiple') {
|
|
|
- const selectedAnswers = question.selectedAnswers || question.correct_answers || question.answers || [];
|
|
|
+ const selectedAnswers = question.selectedAnswers || question.correct_answers || question.answers || question.answer || question['正确答案'] || question['答案'] || [];
|
|
|
return {
|
|
|
- text: question.text || question.question_text || "",
|
|
|
+ text: question.text || question.question_text || question.question || question.title || question.content || question['题干'] || question['题目'] || "",
|
|
|
options: normalizeOptions(question.options),
|
|
|
selectedAnswers: Array.isArray(selectedAnswers) ? selectedAnswers : [selectedAnswers].filter(Boolean),
|
|
|
- analysis: question.analysis || question.explanation || "",
|
|
|
+ analysis: question.analysis || question.explanation || question['解析'] || "",
|
|
|
};
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
- text: question.text || question.question_text || "",
|
|
|
- outline: question.outline || question.answer_outline || { keyFactors: question.answer || "答题要点、关键因素、示例答案" },
|
|
|
- analysis: question.analysis || question.explanation || "",
|
|
|
+ text: question.text || question.question_text || question.question || question.title || question.content || question['题干'] || question['题目'] || "",
|
|
|
+ outline: question.outline || question.answer_outline || question['答题要点'] || { keyFactors: question.answer || question['答案'] || "答题要点、关键因素、示例答案" },
|
|
|
+ analysis: question.analysis || question.explanation || question['解析'] || "",
|
|
|
};
|
|
|
});
|
|
|
};
|
|
|
@@ -1413,11 +1585,11 @@ const normalizeSection = (rawSection, sectionKey, fallbackName, fallbackScore =
|
|
|
const config = getQuestionTypeConfig(fallbackName, fallbackScore);
|
|
|
const sourceQuestions = Array.isArray(section)
|
|
|
? section
|
|
|
- : (section.questions || section.items || section.question_list || []);
|
|
|
+ : (section.questions || section.items || section.question_list || section.questionList || section['题目'] || []);
|
|
|
const normalizedQuestions = normalizeQuestions(sourceQuestions, sectionKey);
|
|
|
- const count = Number(section.count ?? section.question_count ?? normalizedQuestions.length ?? config.questionCount) || 0;
|
|
|
- const scorePerQuestion = Number(section.scorePerQuestion ?? section.score_per_question ?? config.scorePerQuestion) || 0;
|
|
|
- const totalScore = Number(section.totalScore ?? section.total_score ?? (scorePerQuestion * count)) || 0;
|
|
|
+ const count = Number(section.count ?? section.question_count ?? section['数量'] ?? normalizedQuestions.length ?? config.questionCount) || 0;
|
|
|
+ const scorePerQuestion = Number(section.scorePerQuestion ?? section.score_per_question ?? section['每题分值'] ?? config.scorePerQuestion) || 0;
|
|
|
+ const totalScore = Number(section.totalScore ?? section.total_score ?? section['总分'] ?? (scorePerQuestion * count)) || 0;
|
|
|
|
|
|
return {
|
|
|
scorePerQuestion,
|
|
|
@@ -1428,13 +1600,13 @@ const normalizeSection = (rawSection, sectionKey, fallbackName, fallbackScore =
|
|
|
};
|
|
|
|
|
|
const normalizeGeneratedExam = (examData = {}) => {
|
|
|
- const singleSource = examData.singleChoice || examData.questions?.single_choice || examData.single_choice;
|
|
|
- const judgeSource = examData.judge || examData.questions?.judge;
|
|
|
- const multipleSource = examData.multiple || examData.questions?.multiple;
|
|
|
- const shortSource = examData.short || examData.questions?.short;
|
|
|
+ const singleSource = examData.singleChoice || examData.questions?.single_choice || examData.questions?.singleChoice || examData.questions?.['单选题'] || examData.single_choice || examData['单选题'];
|
|
|
+ const judgeSource = examData.judge || examData.questions?.judge || examData.questions?.['判断题'] || examData['判断题'];
|
|
|
+ const multipleSource = examData.multiple || examData.multiple_choice || examData.multipleChoice || examData.questions?.multiple || examData.questions?.multiple_choice || examData.questions?.multipleChoice || examData.questions?.['多选题'] || examData['多选题'];
|
|
|
+ const shortSource = examData.short || examData.short_answer || examData.shortAnswer || examData.questions?.short || examData.questions?.short_answer || examData.questions?.shortAnswer || examData.questions?.['简答题'] || examData['简答题'];
|
|
|
|
|
|
const normalizedExam = {
|
|
|
- title: examData.title || examData.exam_name || examName.value,
|
|
|
+ title: examData.title || examData.exam_name || examData.examTitle || examData['试卷标题'] || examData['标题'] || examName.value,
|
|
|
totalScore: Number(examData.totalScore ?? examData.total_score ?? totalScore.value) || 0,
|
|
|
totalQuestions: Number(examData.totalQuestions ?? examData.total_questions) || 0,
|
|
|
singleChoice: normalizeSection(singleSource, 'singleChoice', '单选题', 2),
|
|
|
@@ -3020,62 +3192,8 @@ const restoreExamFromHistory = (examData) => {
|
|
|
];
|
|
|
}
|
|
|
|
|
|
- // 恢复题目内容
|
|
|
- if (exam.singleChoice || exam.questions?.single_choice) {
|
|
|
- const singleChoice = exam.singleChoice || exam.questions.single_choice;
|
|
|
- const judge = exam.judge || exam.questions.judge;
|
|
|
- const multiple = exam.multiple || exam.questions.multiple;
|
|
|
- const short = exam.short || exam.questions.short;
|
|
|
-
|
|
|
- console.log('单选题数据:', singleChoice);
|
|
|
- console.log('判断题数据:', judge);
|
|
|
- console.log('多选题数据:', multiple);
|
|
|
- console.log('简答题数据:', short);
|
|
|
-
|
|
|
- currentExam.value = {
|
|
|
- title: examName.value,
|
|
|
- totalScore: totalScore.value,
|
|
|
- totalQuestions: exam.totalQuestions || exam.total_questions,
|
|
|
- singleChoice: {
|
|
|
- scorePerQuestion: singleChoice.scorePerQuestion || singleChoice.score_per_question,
|
|
|
- totalScore: singleChoice.totalScore || singleChoice.total_score,
|
|
|
- count: singleChoice.count,
|
|
|
- questions: singleChoice.questions.map(q => ({
|
|
|
- text: q.text || q.question_text,
|
|
|
- options: q.options || [],
|
|
|
- selectedAnswer: q.selectedAnswer || ""
|
|
|
- }))
|
|
|
- },
|
|
|
- judge: {
|
|
|
- scorePerQuestion: judge.scorePerQuestion || judge.score_per_question,
|
|
|
- totalScore: judge.totalScore || judge.total_score,
|
|
|
- count: judge.count,
|
|
|
- questions: judge.questions.map(q => ({
|
|
|
- text: q.text || q.question_text,
|
|
|
- selectedAnswer: q.selectedAnswer || q.correct_answer || q.answer || ""
|
|
|
- }))
|
|
|
- },
|
|
|
- multiple: {
|
|
|
- scorePerQuestion: multiple.scorePerQuestion || multiple.score_per_question,
|
|
|
- totalScore: multiple.totalScore || multiple.total_score,
|
|
|
- count: multiple.count,
|
|
|
- questions: multiple.questions.map(q => ({
|
|
|
- text: q.text || q.question_text,
|
|
|
- options: q.options || [],
|
|
|
- selectedAnswers: q.selectedAnswers || q.correct_answers || q.answers || []
|
|
|
- }))
|
|
|
- },
|
|
|
- short: {
|
|
|
- scorePerQuestion: short.scorePerQuestion || short.score_per_question,
|
|
|
- totalScore: short.totalScore || short.total_score,
|
|
|
- count: short.count,
|
|
|
- questions: short.questions.map(q => ({
|
|
|
- text: q.text || q.question_text,
|
|
|
- outline: q.outline || q.answer_outline || { keyFactors: "答题要点、关键因素、示例答案" }
|
|
|
- }))
|
|
|
- }
|
|
|
- };
|
|
|
- }
|
|
|
+ currentExam.value = normalizeGeneratedExam(exam);
|
|
|
+ ensureQuestionInitialValues(currentExam.value);
|
|
|
|
|
|
// 恢复用户答案(如果有)
|
|
|
if (exam.user_answers) {
|
|
|
@@ -3790,7 +3908,7 @@ onUnmounted(() => {
|
|
|
|
|
|
textarea.form-control {
|
|
|
resize: none;
|
|
|
- height: 250px;
|
|
|
+ height: 180px;
|
|
|
}
|
|
|
|
|
|
.char-count {
|
|
|
@@ -3814,6 +3932,9 @@ onUnmounted(() => {
|
|
|
justify-content: space-between;
|
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 统一阴影 */
|
|
|
position: relative;
|
|
|
+ height: 140px; /* 固定高度,防止上传文件后撑开高度导致下方模块下移 */
|
|
|
+ box-sizing: border-box;
|
|
|
+ overflow-y: auto; /* 文件过多时内部滚动 */
|
|
|
}
|
|
|
|
|
|
.ppt-upload-section:hover {
|
|
|
@@ -4956,20 +5077,32 @@ onUnmounted(() => {
|
|
|
pointer-events: none;
|
|
|
}
|
|
|
|
|
|
- .download-btn {
|
|
|
- // padding: 8px;
|
|
|
- border: none;
|
|
|
- background: transparent;
|
|
|
+ .download-btn-new {
|
|
|
+ border: 1px solid rgba(0, 0, 0, 0.06);
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
|
+ padding: 6px 16px;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #28a745;
|
|
|
cursor: pointer;
|
|
|
- transition: opacity 0.3s ease;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ height: 36px;
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
|
- &:hover {
|
|
|
- opacity: 0.8;
|
|
|
+ &:hover:not(:disabled) {
|
|
|
+ box-shadow: 0 8px 24px rgba(40, 167, 69, 0.12);
|
|
|
+ color: #218838;
|
|
|
+ border-color: rgba(40, 167, 69, 0.2);
|
|
|
}
|
|
|
-
|
|
|
- .download-icon {
|
|
|
- width: 107px;
|
|
|
- height: 34px;
|
|
|
+
|
|
|
+ &:disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ cursor: not-allowed;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -5363,6 +5496,28 @@ onUnmounted(() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ .answer-section {
|
|
|
+ margin-top: 12px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-radius: 6px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ .answer-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #6b7280;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .answer-value {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #1f2937;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.answer-box {
|
|
|
.answer-outline {
|
|
|
display: flex;
|
|
|
@@ -5793,10 +5948,4 @@ onUnmounted(() => {
|
|
|
color: #0d6efd;
|
|
|
border-color: rgba(13, 110, 253, 0.2);
|
|
|
}
|
|
|
-
|
|
|
-.return-ai-btn.has-before::before {
|
|
|
- content: '←';
|
|
|
- font-size: 16px;
|
|
|
- font-weight: bold;
|
|
|
-}
|
|
|
</style>
|