new version

This commit is contained in:
2026-03-22 09:52:58 +08:00
parent a1e76157c9
commit 7a7244b3d0
8 changed files with 6566 additions and 2 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

116
analyze_question_tips.py Normal file
View File

@@ -0,0 +1,116 @@
import json
from openai import OpenAI
import time
# 配置API
client = OpenAI(
api_key="sk-2b675306a92d4fb389766291ab3f1ec1",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
def analyze_question(question):
"""分析题目并生成记忆要点"""
prompt = f"""分析这道D365考试题目生成简洁的记忆要点帮助快速记忆。
题目:
{question['stem']}
选项:
{chr(10).join([f"{opt['label']}. {opt['text']}" for opt in question['options']])}
正确答案:{question['answer']}
请用中文生成以下内容每项不超过100字
1. 关键词提示:题目中的关键英文单词或短语
2. 题目特点:这道题的独特之处
3. 记忆技巧:如何快速记住这道题的答案
4. 答案解析:为什么这个答案是对的
请用JSON格式返回
{{
"keywords": "关键词提示",
"features": "题目特点",
"memory_tips": "记忆技巧",
"explanation": "答案解析"
}}
"""
try:
response = client.chat.completions.create(
model="qwen-plus",
messages=[
{"role": "system", "content": "你是一个D365考试专家擅长总结题目要点和记忆技巧。"},
{"role": "user", "content": prompt}
],
temperature=0.7,
max_tokens=500
)
result = response.choices[0].message.content.strip()
# 提取JSON
if '```json' in result:
result = result.split('```json')[1].split('```')[0].strip()
elif '```' in result:
result = result.split('```')[1].split('```')[0].strip()
return json.loads(result)
except Exception as e:
print(f"Error analyzing question {question['topic']}-{question['question_num']}: {e}")
return {
"keywords": "分析失败",
"features": "分析失败",
"memory_tips": "分析失败",
"explanation": "分析失败"
}
def main():
# 加载题目
with open('exam_data/questions_translated.json', 'r', encoding='utf-8') as f:
questions = json.load(f)
print(f"总共 {len(questions)} 道题目需要分析")
# 加载已分析的题目
try:
with open('exam_data/question_tips.json', 'r', encoding='utf-8') as f:
tips_data = json.load(f)
analyzed_count = len(tips_data)
except:
tips_data = {}
analyzed_count = 0
print(f"已分析 {analyzed_count} 道题目")
# 分析题目
for i, question in enumerate(questions):
key = f"{question['topic']}-{question['question_num']}"
# 跳过已分析的题目
if key in tips_data:
print(f"[{i+1}/{len(questions)}] 跳过已分析: {key}")
continue
print(f"[{i+1}/{len(questions)}] 分析题目: {key}")
tips = analyze_question(question)
tips_data[key] = tips
# 每分析10道题保存一次
if (i + 1) % 10 == 0:
with open('exam_data/question_tips.json', 'w', encoding='utf-8') as f:
json.dump(tips_data, f, ensure_ascii=False, indent=2)
print(f"已保存 {len(tips_data)} 道题目的分析结果")
# 避免API限流
time.sleep(0.5)
# 最终保存
with open('exam_data/question_tips.json', 'w', encoding='utf-8') as f:
json.dump(tips_data, f, ensure_ascii=False, indent=2)
print(f"\n完成!共分析 {len(tips_data)} 道题目")
if __name__ == "__main__":
main()

80
analyze_question_types.py Normal file
View File

@@ -0,0 +1,80 @@
import json
import re
with open('exam_data/questions_translated.json', 'r', encoding='utf-8') as f:
questions = json.load(f)
# 分类统计
yes_no_questions = [] # 判断题
single_choice = [] # 单选题
multi_choice = [] # 多选题
drag_drop_questions = [] # 拖拽题
order_questions = [] # 排序题
case_questions = [] # 特殊case题
for q in questions:
options = q['options']
answer = q['answer']
stem = q['stem'].lower()
stem_cn = q.get('stem_cn', '').lower()
# 判断题只有2个选项且是Yes/No类型
if len(options) == 2:
opt_texts = [opt['text'].lower() for opt in options]
if any('yes' in t for t in opt_texts) or any('no' in t for t in opt_texts):
yes_no_questions.append(q)
continue
# 拖拽题:包含 drag, drop 关键词
if 'drag' in stem or 'drop' in stem or '拖拽' in stem_cn or '拖放' in stem_cn:
drag_drop_questions.append(q)
continue
# 排序题:包含 order, sequence, arrange 关键词
if 'order' in stem or 'sequence' in stem or 'arrange' in stem or '排序' in stem_cn or '顺序' in stem_cn:
order_questions.append(q)
continue
# 特殊case题包含 case, scenario 关键词
if 'case ' in stem or 'scenario' in stem or '案例' in stem_cn or '场景' in stem_cn:
case_questions.append(q)
continue
# 根据答案长度判断单选/多选
if len(answer) == 1:
single_choice.append(q)
else:
multi_choice.append(q)
print("=" * 50)
print("题目类型统计:")
print("=" * 50)
print(f"总题目数: {len(questions)}")
print(f"判断题 (Yes/No): {len(yes_no_questions)}")
print(f"拖拽题 (Drag/Drop): {len(drag_drop_questions)}")
print(f"排序题 (Order/Sequence): {len(order_questions)}")
print(f"特殊Case题: {len(case_questions)}")
print(f"单选题: {len(single_choice)}")
print(f"多选题: {len(multi_choice)}")
print()
# 显示各类型示例
print("=" * 50)
print("拖拽题示例:")
print("=" * 50)
for q in drag_drop_questions[:2]:
print(f"Topic {q['topic']} - Q{q['question_num']}: {q['stem'][:100]}...")
print()
print("=" * 50)
print("排序题示例:")
print("=" * 50)
for q in order_questions[:2]:
print(f"Topic {q['topic']} - Q{q['question_num']}: {q['stem'][:100]}...")
print()
print("=" * 50)
print("特殊Case题示例:")
print("=" * 50)
for q in case_questions[:2]:
print(f"Topic {q['topic']} - Q{q['question_num']}: {q['stem'][:100]}...")

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,21 @@
<div class="logo"> <div class="logo">
<h2>MB-330 考试学习</h2> <h2>MB-330 考试学习</h2>
</div> </div>
<div class="exam-button-container">
<el-button
type="success"
size="large"
@click="handleStartExam"
:disabled="loading"
>
<el-icon><Edit /></el-icon>
开始考试
</el-button>
</div>
<el-menu <el-menu
v-if="!examMode"
:default-active="String(currentTopic)" :default-active="String(currentTopic)"
@select="handleTopicSelect" @select="handleTopicSelect"
class="topic-menu" class="topic-menu"
@@ -19,6 +33,30 @@
<el-badge :value="topicStats[topic] || 0" class="topic-badge" /> <el-badge :value="topicStats[topic] || 0" class="topic-badge" />
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
<div v-else class="exam-sidebar">
<div class="exam-info">
<el-tag type="warning" size="large">考试模式</el-tag>
<div class="exam-progress">
<span>已答: {{ examProgress }} / 50</span>
</div>
</div>
<div class="exam-question-grid">
<div
v-for="index in 50"
:key="index"
class="exam-question-item"
:class="{
'answered': examAnswers.has(index - 1),
'submitted': examSubmittedQuestions.has(index - 1),
'current': examCurrentIndex === index - 1
}"
@click="handleExamJumpToQuestion(index - 1)"
>
{{ index }}
</div>
</div>
</div>
</el-aside> </el-aside>
<el-main class="main-content"> <el-main class="main-content">
@@ -27,10 +65,237 @@
<p>加载中...</p> <p>加载中...</p>
</div> </div>
<!-- 考试模式 -->
<template v-else-if="examMode">
<div v-if="!examFinished" class="exam-container">
<div class="question-header">
<h3>考试题目 {{ examCurrentIndex + 1 }} / 50</h3>
<div class="header-right">
<el-tag :type="getQuestionTypeTag(currentQuestion?.questionType)">
{{ getQuestionTypeLabel(currentQuestion?.questionType) }}
</el-tag>
<span class="question-progress">
已答: {{ examProgress }}
</span>
<el-button
type="danger"
size="small"
@click="handleExitExam"
>
<el-icon><Close /></el-icon>
退出考试
</el-button>
</div>
</div>
<div class="question-content">
<div class="bilingual-container">
<div class="language-panel english-panel">
<div class="panel-header">
<el-tag type="primary">English</el-tag>
</div>
<div class="stem-text">
{{ currentQuestion?.stem }}
</div>
<div class="options-list">
<div
v-for="option in currentQuestion?.options"
:key="option.label"
class="option-item exam-option"
:class="{
'selected': isOptionSelected(option.label) && !showAnswer,
'correct-option': showAnswer && currentQuestion?.answer.includes(option.label),
'wrong-option': showAnswer && isOptionSelected(option.label) && !currentQuestion?.answer.includes(option.label)
}"
@click="!showAnswer && handleSelectAnswer(option.label)"
>
<el-checkbox
:model-value="isOptionSelected(option.label)"
:disabled="showAnswer"
@change="!showAnswer && handleSelectAnswer(option.label)"
/>
<span class="option-label">{{ option.label }}.</span>
<span class="option-text">{{ option.text }}</span>
</div>
</div>
</div>
<div class="language-panel chinese-panel">
<div class="panel-header">
<el-tag type="success">中文</el-tag>
</div>
<div class="stem-text">
{{ currentQuestion?.stem_cn || '待翻译...' }}
</div>
<div class="options-list">
<div
v-for="option in currentQuestion?.options"
:key="option.label"
class="option-item exam-option"
:class="{
'selected': isOptionSelected(option.label) && !showAnswer,
'correct-option': showAnswer && currentQuestion?.answer.includes(option.label),
'wrong-option': showAnswer && isOptionSelected(option.label) && !currentQuestion?.answer.includes(option.label)
}"
@click="!showAnswer && handleSelectAnswer(option.label)"
>
<el-checkbox
:model-value="isOptionSelected(option.label)"
:disabled="showAnswer"
@change="!showAnswer && handleSelectAnswer(option.label)"
/>
<span class="option-label">{{ option.label }}.</span>
<span class="option-text">{{ option.text_cn || '待翻译...' }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 答题反馈 -->
<div v-if="showAnswer && currentQuestion" class="answer-feedback">
<!-- 没有答案的题目 -->
<el-alert
v-if="!currentQuestion.answer || currentQuestion.answer === ''"
title="此题无标准答案"
type="info"
:closable="false"
show-icon
>
<template #default>
<div class="feedback-content">
<p>此题无标准答案自动计为正确</p>
<p>你的答案: <strong>{{ selectedAnswer || '未作答' }}</strong></p>
</div>
</template>
</el-alert>
<!-- 有答案的题目 -->
<el-alert
v-else-if="selectedAnswer === currentQuestion.answer"
title="回答正确!"
type="success"
:closable="false"
show-icon
>
<template #default>
<div class="feedback-content">
<p>你的答案: <strong>{{ selectedAnswer }}</strong></p>
<p>正确答案: <strong>{{ currentQuestion.answer }}</strong></p>
</div>
</template>
</el-alert>
<el-alert
v-else
title="回答错误!"
type="error"
:closable="false"
show-icon
>
<template #default>
<div class="feedback-content">
<p>你的答案: <strong>{{ selectedAnswer || '未作答' }}</strong></p>
<p>正确答案: <strong>{{ currentQuestion.answer }}</strong></p>
</div>
</template>
</el-alert>
</div>
<div class="question-actions">
<el-button @click="handleExamPrevQuestion" :disabled="examCurrentIndex === 0">
<el-icon><ArrowLeft /></el-icon>
上一题
</el-button>
<el-button type="danger" @click="handleFinishExam">
<el-icon><Check /></el-icon>
提交考试
</el-button>
<el-button
type="primary"
@click="handleExamNextQuestion"
:disabled="examCurrentIndex === 49 && showAnswer"
>
<span v-if="!showAnswer && selectedAnswer">确认答案</span>
<span v-else-if="showAnswer && examCurrentIndex < 49">下一题</span>
<span v-else>下一题</span>
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
</div>
<!-- 考试结果 -->
<div v-else class="exam-result-container">
<el-card class="result-card">
<template #header>
<div class="result-header">
<h2>考试结果</h2>
</div>
</template>
<div class="result-content">
<div class="score-display">
<el-progress
type="circle"
:percentage="(examScore / 50) * 100"
:width="200"
:stroke-width="20"
:color="examPassed ? '#67c23a' : '#f56c6c'"
>
<template #default>
<span class="score-number">{{ examScore }}</span>
<span class="score-total">/ 50</span>
</template>
</el-progress>
</div>
<el-alert
:title="examPassed ? '恭喜!考试通过!' : '很遗憾,考试未通过'"
:type="examPassed ? 'success' : 'error'"
:description="examPassed ? '你答对了40道以上的题目符合考试要求。' : '你需要答对40道题目才能通过考试请继续努力'"
:closable="false"
show-icon
class="result-alert"
/>
<div class="result-stats">
<el-statistic title="正确题目" :value="examScore" />
<el-statistic title="错误题目" :value="50 - examScore" />
<el-statistic title="及格线" :value="40" />
</div>
<div class="result-actions">
<el-button type="primary" size="large" @click="handleReviewExam">
<el-icon><View /></el-icon>
查看答题详情
</el-button>
<el-button type="success" size="large" @click="handleRetakeExam">
<el-icon><Refresh /></el-icon>
重新考试
</el-button>
<el-button size="large" @click="handleExitExam">
<el-icon><Close /></el-icon>
退出考试
</el-button>
</div>
</div>
</el-card>
</div>
</template>
<!-- 学习模式 -->
<template v-else-if="currentQuestion"> <template v-else-if="currentQuestion">
<div class="question-header"> <div class="question-header">
<h3>Topic {{ currentTopic }} - Question {{ currentQuestion.question_num }}</h3> <h3>Topic {{ currentTopic }} - Question {{ currentQuestion.question_num }}</h3>
<div class="header-right"> <div class="header-right">
<el-button
type="info"
size="small"
@click="handleShowTips"
>
<el-icon><QuestionFilled /></el-icon>
记忆要点
</el-button>
<el-button <el-button
type="warning" type="warning"
size="small" size="small"
@@ -181,6 +446,56 @@
</div> </div>
</div> </div>
</Teleport> </Teleport>
<!-- 记忆要点弹窗 -->
<el-dialog
v-model="tipsDialogVisible"
title="记忆要点"
width="600px"
:close-on-click-modal="false"
>
<div v-if="currentQuestion" class="tips-content">
<el-alert
v-if="!currentQuestion.tips"
title="记忆要点正在生成中..."
type="info"
:closable="false"
show-icon
>
<template #default>
<p>请稍后再试或联系管理员生成记忆要点数据</p>
</template>
</el-alert>
<template v-else>
<div class="tip-section">
<h4><el-icon><Key /></el-icon> 关键词提示</h4>
<p>{{ currentQuestion.tips.keywords }}</p>
</div>
<div class="tip-section">
<h4><el-icon><Star /></el-icon> 题目特点</h4>
<p>{{ currentQuestion.tips.features }}</p>
</div>
<div class="tip-section">
<h4><el-icon><MagicStick /></el-icon> 记忆技巧</h4>
<p>{{ currentQuestion.tips.memory_tips }}</p>
</div>
<div class="tip-section">
<h4><el-icon><Document /></el-icon> 答案解析</h4>
<p>{{ currentQuestion.tips.explanation }}</p>
</div>
</template>
</div>
<template #footer>
<el-button type="primary" @click="tipsDialogVisible = false">
知道了
</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
@@ -188,6 +503,7 @@
import { onMounted, ref, watch, computed } from 'vue' import { onMounted, ref, watch, computed } from 'vue'
import { useQuestionStore } from './stores/questions' import { useQuestionStore } from './stores/questions'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { ElMessage, ElMessageBox } from 'element-plus'
const store = useQuestionStore() const store = useQuestionStore()
@@ -199,12 +515,22 @@ const {
currentQuestion, currentQuestion,
currentTopicQuestions, currentTopicQuestions,
showAnswer, showAnswer,
topicStats topicStats,
examMode,
examCurrentIndex,
examAnswers,
examSubmittedQuestions,
examProgress,
examScore,
examPassed,
examFinished
} = storeToRefs(store) } = storeToRefs(store)
const jumpQuestionNum = ref(1) const jumpQuestionNum = ref(1)
const pdfDialogVisible = ref(false) const pdfDialogVisible = ref(false)
const isMaximized = ref(false) const isMaximized = ref(false)
const selectedAnswer = ref<string>('')
const tipsDialogVisible = ref(false)
const currentPdfUrl = computed(() => { const currentPdfUrl = computed(() => {
const topicNum = String(currentTopic.value).padStart(2, '0') const topicNum = String(currentTopic.value).padStart(2, '0')
@@ -215,6 +541,10 @@ watch(currentQuestionIndex, (newIndex) => {
jumpQuestionNum.value = newIndex + 1 jumpQuestionNum.value = newIndex + 1
}) })
watch(examCurrentIndex, () => {
selectedAnswer.value = examAnswers.value.get(examCurrentIndex.value) || ''
})
onMounted(() => { onMounted(() => {
store.loadQuestions() store.loadQuestions()
}) })
@@ -255,6 +585,173 @@ function closePdfDialog() {
pdfDialogVisible.value = false pdfDialogVisible.value = false
isMaximized.value = false isMaximized.value = false
} }
// 考试模式相关函数
function handleStartExam() {
ElMessageBox.confirm(
'考试将随机抽取50道题目包含判断题、拖拽题、排序题、单选题和特殊Case题。答对40道及以上为合格。确定开始考试吗',
'开始考试',
{
confirmButtonText: '开始考试',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
store.startExam()
ElMessage.success('考试开始!祝你成功!')
}).catch(() => {
// 用户取消
})
}
function handleShowTips() {
tipsDialogVisible.value = true
}
function handleSelectAnswer(optionLabel: string) {
if (examFinished.value) return
// 多选题逻辑
const currentAns = selectedAnswer.value
const question = currentQuestion.value
if (!question) return
// 判断是否是多选题
const isMultiChoice = question.answer.length > 1
if (isMultiChoice) {
// 多选题:切换选项
if (currentAns.includes(optionLabel)) {
selectedAnswer.value = currentAns.split('').filter(l => l !== optionLabel).sort().join('')
} else {
selectedAnswer.value = (currentAns + optionLabel).split('').sort().join('')
}
} else {
// 单选题:直接选择
selectedAnswer.value = optionLabel
}
store.submitExamAnswer(selectedAnswer.value)
}
function isOptionSelected(optionLabel: string): boolean {
return selectedAnswer.value.includes(optionLabel)
}
function handleExamNextQuestion() {
// 如果当前题目未提交且已作答,先提交显示答案
if (!examSubmittedQuestions.value.has(examCurrentIndex.value) && selectedAnswer.value) {
store.submitCurrentQuestion()
return
}
// 如果已提交,跳转到下一题
store.examNextQuestion()
}
function handleExamPrevQuestion() {
store.examPrevQuestion()
}
function handleExamJumpToQuestion(index: number) {
store.examJumpToQuestion(index)
}
function handleFinishExam() {
// 先提交当前题目(如果已作答)
if (!examSubmittedQuestions.value.has(examCurrentIndex.value) && selectedAnswer.value) {
store.submitCurrentQuestion()
return
}
const unanswered = 50 - examProgress.value
if (unanswered > 0) {
ElMessageBox.confirm(
`你还有 ${unanswered} 道题目未作答,确定要提交考试吗?`,
'确认提交',
{
confirmButtonText: '确定提交',
cancelButtonText: '继续答题',
type: 'warning'
}
).then(() => {
store.finishExam()
}).catch(() => {
// 用户取消
})
} else {
ElMessageBox.confirm(
'确定要提交考试吗?提交后将无法修改答案。',
'确认提交',
{
confirmButtonText: '确定提交',
cancelButtonText: '继续答题',
type: 'warning'
}
).then(() => {
store.finishExam()
}).catch(() => {
// 用户取消
})
}
}
function handleReviewExam() {
store.examCurrentIndex = 0
store.examFinished = false
}
function handleRetakeExam() {
store.exitExam()
handleStartExam()
}
function handleExitExam() {
if (!examFinished.value) {
ElMessageBox.confirm(
'确定要退出考试吗?当前答题进度将不会保存。',
'退出考试',
{
confirmButtonText: '确定退出',
cancelButtonText: '继续考试',
type: 'warning'
}
).then(() => {
store.exitExam()
ElMessage.info('已退出考试')
}).catch(() => {
// 用户取消
})
} else {
store.exitExam()
}
}
function getQuestionTypeLabel(type?: string): string {
const labels: Record<string, string> = {
'yes_no': '判断题',
'drag_drop': '拖拽题',
'order': '排序题',
'case': '特殊Case题',
'single': '单选题',
'multi': '多选题'
}
return labels[type || 'single'] || '选择题'
}
function getQuestionTypeTag(type?: string): '' | 'success' | 'warning' | 'info' | 'danger' {
const tags: Record<string, '' | 'success' | 'warning' | 'info' | 'danger'> = {
'yes_no': 'info',
'drag_drop': 'warning',
'order': 'warning',
'case': 'danger',
'single': 'success',
'multi': ''
}
return tags[type || 'single'] || ''
}
</script> </script>
<style scoped> <style scoped>
@@ -285,6 +782,15 @@ function closePdfDialog() {
font-size: 18px; font-size: 18px;
} }
.exam-button-container {
padding: 20px;
border-bottom: 1px solid #e4e7ed;
}
.exam-button-container .el-button {
width: 100%;
}
.topic-menu { .topic-menu {
border-right: none; border-right: none;
} }
@@ -293,6 +799,65 @@ function closePdfDialog() {
margin-left: auto; margin-left: auto;
} }
.exam-sidebar {
padding: 16px;
}
.exam-info {
margin-bottom: 16px;
text-align: center;
}
.exam-progress {
margin-top: 8px;
font-size: 14px;
color: #606266;
}
.exam-question-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.exam-question-item {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s;
background-color: #fff;
}
.exam-question-item:hover {
border-color: #409eff;
color: #409eff;
}
.exam-question-item.answered {
background-color: #ecf5ff;
border-color: #409eff;
color: #409eff;
}
.exam-question-item.submitted {
background-color: #67c23a;
border-color: #67c23a;
color: #fff;
}
.exam-question-item.current {
border-color: #409eff;
color: #409eff;
font-weight: bold;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.main-content { .main-content {
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
@@ -411,6 +976,25 @@ function closePdfDialog() {
background-color: #f0f9eb; background-color: #f0f9eb;
} }
.option-item.wrong-option {
border-color: #f56c6c;
background-color: #fef0f0;
}
.option-item.exam-option {
cursor: pointer;
}
.option-item.exam-option.selected {
border-color: #409eff;
background-color: #ecf5ff;
}
.option-item.exam-option:has(.el-checkbox.is-disabled) {
cursor: not-allowed;
opacity: 0.8;
}
.option-label { .option-label {
font-weight: bold; font-weight: bold;
color: #409eff; color: #409eff;
@@ -429,6 +1013,19 @@ function closePdfDialog() {
border-top: 1px solid #e4e7ed; border-top: 1px solid #e4e7ed;
} }
.answer-feedback {
margin-top: 20px;
padding: 16px;
}
.feedback-content {
margin-top: 8px;
}
.feedback-content p {
margin: 4px 0;
}
.question-actions { .question-actions {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -436,6 +1033,102 @@ function closePdfDialog() {
margin-top: 24px; margin-top: 24px;
} }
.tips-content {
padding: 10px 0;
}
.tip-section {
margin-bottom: 20px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
border-left: 4px solid #409eff;
}
.tip-section:last-child {
margin-bottom: 0;
}
.tip-section h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.tip-section h4 .el-icon {
color: #409eff;
}
.tip-section p {
margin: 0;
color: #606266;
line-height: 1.8;
}
.exam-container {
min-height: 100%;
}
.exam-result-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
}
.result-card {
width: 100%;
max-width: 800px;
}
.result-header {
text-align: center;
}
.result-header h2 {
margin: 0;
color: #303133;
}
.result-content {
text-align: center;
}
.score-display {
margin: 30px 0;
}
.score-number {
font-size: 48px;
font-weight: bold;
color: #303133;
}
.score-total {
font-size: 24px;
color: #909399;
}
.result-alert {
margin: 20px 0;
}
.result-stats {
display: flex;
justify-content: space-around;
margin: 30px 0;
}
.result-actions {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 30px;
}
.pdf-overlay { .pdf-overlay {
position: fixed; position: fixed;
top: 0; top: 0;

View File

@@ -14,6 +14,21 @@ export interface Question {
stem_cn?: string stem_cn?: string
options: QuestionOption[] options: QuestionOption[]
answer: string answer: string
questionType?: 'yes_no' | 'drag_drop' | 'order' | 'case' | 'single' | 'multi'
tips?: QuestionTips
}
export interface QuestionTips {
keywords: string
features: string
memory_tips: string
explanation: string
}
export interface ExamAnswer {
questionIndex: number
selectedAnswer: string
isCorrect: boolean
} }
export const useQuestionStore = defineStore('questions', () => { export const useQuestionStore = defineStore('questions', () => {
@@ -23,6 +38,16 @@ export const useQuestionStore = defineStore('questions', () => {
const showAnswer = ref<boolean>(false) const showAnswer = ref<boolean>(false)
const loading = ref<boolean>(true) const loading = ref<boolean>(true)
// 考试模式状态
const examMode = ref<boolean>(false)
const examQuestions = ref<Question[]>([])
const examCurrentIndex = ref<number>(0)
const examAnswers = ref<Map<number, string>>(new Map())
const examSubmittedQuestions = ref<Set<number>>(new Set()) // 已提交的题目索引
const examStartTime = ref<number>(0)
const examEndTime = ref<number>(0)
const examFinished = ref<boolean>(false)
const topics = computed(() => { const topics = computed(() => {
const topicSet = new Set(questions.value.map(q => q.topic)) const topicSet = new Set(questions.value.map(q => q.topic))
return Array.from(topicSet).sort((a, b) => a - b) return Array.from(topicSet).sort((a, b) => a - b)
@@ -33,6 +58,9 @@ export const useQuestionStore = defineStore('questions', () => {
}) })
const currentQuestion = computed(() => { const currentQuestion = computed(() => {
if (examMode.value) {
return examQuestions.value[examCurrentIndex.value] || null
}
return currentTopicQuestions.value[currentQuestionIndex.value] || null return currentTopicQuestions.value[currentQuestionIndex.value] || null
}) })
@@ -44,14 +72,246 @@ export const useQuestionStore = defineStore('questions', () => {
return stats return stats
}) })
// 考试相关计算属性
const examProgress = computed(() => {
return examAnswers.value.size
})
const examScore = computed(() => {
if (!examFinished.value) return 0
let correct = 0
examQuestions.value.forEach((question, index) => {
// 如果题目没有答案,直接算对
if (!question.answer || question.answer === '') {
correct++
} else {
const userAnswer = examAnswers.value.get(index)
if (userAnswer === question.answer) {
correct++
}
}
})
return correct
})
const examPassed = computed(() => {
return examScore.value >= 40
})
// 分类题目
function classifyQuestions() {
const classified = {
yes_no: [] as Question[],
drag_drop: [] as Question[],
order: [] as Question[],
case: [] as Question[],
single: [] as Question[],
multi: [] as Question[]
}
questions.value.forEach(q => {
const stem = q.stem.toLowerCase()
const stem_cn = (q.stem_cn || '').toLowerCase()
const options = q.options
const answer = q.answer
// 判断题只有2个选项且是Yes/No类型
if (options.length === 2) {
const opt_texts = options.map(opt => opt.text.toLowerCase())
if (opt_texts.some(t => t.includes('yes')) || opt_texts.some(t => t.includes('no'))) {
q.questionType = 'yes_no'
classified.yes_no.push(q)
return
}
}
// 拖拽题
if (stem.includes('drag') || stem.includes('drop') || stem_cn.includes('拖拽') || stem_cn.includes('拖放')) {
q.questionType = 'drag_drop'
classified.drag_drop.push(q)
return
}
// 排序题
if (stem.includes('order') || stem.includes('sequence') || stem.includes('arrange') || stem_cn.includes('排序') || stem_cn.includes('顺序')) {
q.questionType = 'order'
classified.order.push(q)
return
}
// 特殊case题
if (stem.includes('case ') || stem.includes('scenario') || stem_cn.includes('案例') || stem_cn.includes('场景')) {
q.questionType = 'case'
classified.case.push(q)
return
}
// 单选/多选
if (answer.length === 1) {
q.questionType = 'single'
classified.single.push(q)
} else {
q.questionType = 'multi'
classified.multi.push(q)
}
})
return classified
}
// 随机抽题
function generateExamQuestions() {
const classified = classifyQuestions()
const selected: Question[] = []
// 判断题 6道
const yesNoQuestions = shuffleArray([...classified.yes_no]).slice(0, 6)
selected.push(...yesNoQuestions)
// 拖拽题 5道
const dragDropQuestions = shuffleArray([...classified.drag_drop]).slice(0, 5)
selected.push(...dragDropQuestions)
// 排序题 2道
const orderQuestions = shuffleArray([...classified.order]).slice(0, 2)
selected.push(...orderQuestions)
// 特殊Case题 7道
const caseQuestions = shuffleArray([...classified.case]).slice(0, 7)
selected.push(...caseQuestions)
// 单选题 30道
const singleQuestions = shuffleArray([...classified.single]).slice(0, 30)
selected.push(...singleQuestions)
// 打乱顺序
return shuffleArray(selected)
}
// Fisher-Yates 洗牌算法
function shuffleArray<T>(array: T[]): T[] {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]]
}
return array
}
// 开始考试
function startExam() {
examMode.value = true
examQuestions.value = generateExamQuestions()
examCurrentIndex.value = 0
examAnswers.value = new Map()
examSubmittedQuestions.value = new Set()
examStartTime.value = Date.now()
examEndTime.value = 0
examFinished.value = false
showAnswer.value = false
}
// 提交考试答案
function submitExamAnswer(answer: string) {
if (examMode.value && !examFinished.value) {
examAnswers.value.set(examCurrentIndex.value, answer)
}
}
// 提交当前题目(显示答案并锁定)
function submitCurrentQuestion() {
if (examMode.value && !examFinished.value) {
examSubmittedQuestions.value.add(examCurrentIndex.value)
showAnswer.value = true
}
}
// 检查当前题目是否已提交
function isCurrentQuestionSubmitted() {
return examSubmittedQuestions.value.has(examCurrentIndex.value)
}
// 考试下一题
function examNextQuestion() {
// 如果当前题目未提交,先提交显示答案
if (!examSubmittedQuestions.value.has(examCurrentIndex.value) && examAnswers.value.has(examCurrentIndex.value)) {
submitCurrentQuestion()
return
}
// 如果已提交,跳转到下一题
if (examCurrentIndex.value < examQuestions.value.length - 1) {
examCurrentIndex.value++
showAnswer.value = examSubmittedQuestions.value.has(examCurrentIndex.value)
}
}
// 考试上一题
function examPrevQuestion() {
if (examCurrentIndex.value > 0) {
examCurrentIndex.value--
showAnswer.value = examSubmittedQuestions.value.has(examCurrentIndex.value)
}
}
// 跳转到考试题目
function examJumpToQuestion(index: number) {
if (index >= 0 && index < examQuestions.value.length) {
examCurrentIndex.value = index
showAnswer.value = examSubmittedQuestions.value.has(index)
}
}
// 结束考试
function finishExam() {
examFinished.value = true
examEndTime.value = Date.now()
showAnswer.value = true
}
// 退出考试模式
function exitExam() {
examMode.value = false
examQuestions.value = []
examCurrentIndex.value = 0
examAnswers.value = new Map()
examSubmittedQuestions.value = new Set()
examStartTime.value = 0
examEndTime.value = 0
examFinished.value = false
showAnswer.value = false
}
async function loadQuestions() { async function loadQuestions() {
try { try {
loading.value = true loading.value = true
// 加载题目
let response = await fetch('/questions_translated.json') let response = await fetch('/questions_translated.json')
if (!response.ok) { if (!response.ok) {
response = await fetch('/questions.json') response = await fetch('/questions.json')
} }
const data = await response.json() const data = await response.json()
// 加载记忆要点
try {
const tipsResponse = await fetch('/question_tips.json')
if (tipsResponse.ok) {
const tipsData = await tipsResponse.json()
// 将记忆要点合并到题目中
data.forEach((question: Question) => {
const key = `${question.topic}-${question.question_num}`
if (tipsData[key]) {
question.tips = tipsData[key]
}
})
console.log('记忆要点加载成功')
}
} catch (error) {
console.warn('记忆要点加载失败:', error)
}
questions.value = data questions.value = data
} catch (error) { } catch (error) {
console.error('Failed to load questions:', error) console.error('Failed to load questions:', error)
@@ -106,6 +366,27 @@ export const useQuestionStore = defineStore('questions', () => {
nextQuestion, nextQuestion,
prevQuestion, prevQuestion,
jumpToQuestion, jumpToQuestion,
toggleAnswer toggleAnswer,
// 考试模式
examMode,
examQuestions,
examCurrentIndex,
examAnswers,
examSubmittedQuestions,
examStartTime,
examEndTime,
examFinished,
examProgress,
examScore,
examPassed,
startExam,
submitExamAnswer,
submitCurrentQuestion,
isCurrentQuestionSubmitted,
examNextQuestion,
examPrevQuestion,
examJumpToQuestion,
finishExam,
exitExam
} }
}) })

2642
exam_data/question_tips.json Normal file

File diff suppressed because it is too large Load Diff

110
retry_failed_tips.py Normal file
View File

@@ -0,0 +1,110 @@
import json
from openai import OpenAI
import time
# 配置API
client = OpenAI(
api_key="sk-2b675306a92d4fb389766291ab3f1ec1",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
def analyze_question(question):
"""分析题目并生成记忆要点"""
prompt = f"""分析这道D365考试题目生成简洁的记忆要点帮助快速记忆。
题目:
{question['stem']}
选项:
{chr(10).join([f"{opt['label']}. {opt['text']}" for opt in question['options']])}
正确答案:{question['answer']}
请用中文生成以下内容每项不超过100字
1. 关键词提示:题目中的关键英文单词或短语
2. 题目特点:这道题的独特之处
3. 记忆技巧:如何快速记住这道题的答案
4. 答案解析:为什么这个答案是对的
请用JSON格式返回
{{
"keywords": "关键词提示",
"features": "题目特点",
"memory_tips": "记忆技巧",
"explanation": "答案解析"
}}
"""
try:
response = client.chat.completions.create(
model="qwen-plus",
messages=[
{"role": "system", "content": "你是一个D365考试专家擅长总结题目要点和记忆技巧。"},
{"role": "user", "content": prompt}
],
temperature=0.7,
max_tokens=500
)
result = response.choices[0].message.content.strip()
# 提取JSON
if '```json' in result:
result = result.split('```json')[1].split('```')[0].strip()
elif '```' in result:
result = result.split('```')[1].split('```')[0].strip()
return json.loads(result)
except Exception as e:
print(f"Error analyzing question {question['topic']}-{question['question_num']}: {e}")
return {
"keywords": "分析失败",
"features": "分析失败",
"memory_tips": "分析失败",
"explanation": "分析失败"
}
def main():
# 加载题目
with open('exam_data/questions_translated.json', 'r', encoding='utf-8') as f:
questions = json.load(f)
# 加载已有的记忆要点
with open('exam_data/question_tips.json', 'r', encoding='utf-8') as f:
tips_data = json.load(f)
print(f"总共 {len(questions)} 道题目")
print(f"已有 {len(tips_data)} 道题目的记忆要点")
# 找出需要重新分析的题目前20道
failed_keys = [k for k, v in tips_data.items() if v.get('keywords') == '分析失败']
print(f"需要重新分析 {len(failed_keys)} 道题目")
# 重新分析失败的题目
for i, question in enumerate(questions):
key = f"{question['topic']}-{question['question_num']}"
if key in failed_keys:
print(f"[{i+1}/{len(questions)}] 重新分析题目: {key}")
tips = analyze_question(question)
tips_data[key] = tips
# 每分析5道题保存一次
if (i + 1) % 5 == 0:
with open('exam_data/question_tips.json', 'w', encoding='utf-8') as f:
json.dump(tips_data, f, ensure_ascii=False, indent=2)
print(f"已保存进度")
# 避免API限流
time.sleep(0.5)
# 最终保存
with open('exam_data/question_tips.json', 'w', encoding='utf-8') as f:
json.dump(tips_data, f, ensure_ascii=False, indent=2)
print(f"\n完成!共重新分析 {len(failed_keys)} 道题目")
if __name__ == "__main__":
main()