new version
This commit is contained in:
116
analyze_question_tips.py
Normal file
116
analyze_question_tips.py
Normal 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
80
analyze_question_types.py
Normal 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]}...")
|
||||||
2642
exam-viewer/public/question_tips.json
Normal file
2642
exam-viewer/public/question_tips.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
|||||||
@@ -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
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
110
retry_failed_tips.py
Normal 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()
|
||||||
Reference in New Issue
Block a user