init
This commit is contained in:
@@ -0,0 +1,969 @@
|
||||
import '../models/test_models.dart';
|
||||
|
||||
/// 综合测试静态数据类
|
||||
class TestStaticData {
|
||||
// 私有构造函数
|
||||
TestStaticData._();
|
||||
|
||||
/// 测试题目静态数据
|
||||
static final List<TestQuestion> _questions = [
|
||||
// 词汇题目
|
||||
TestQuestion(
|
||||
id: 'vocab_001',
|
||||
type: QuestionType.multipleChoice,
|
||||
skillType: SkillType.vocabulary,
|
||||
difficulty: DifficultyLevel.beginner,
|
||||
content: 'What does "apple" mean?',
|
||||
options: ['苹果', '香蕉', '橙子', '葡萄'],
|
||||
correctAnswers: ['苹果'],
|
||||
explanation: 'Apple means 苹果 in Chinese.',
|
||||
points: 1,
|
||||
timeLimit: 30,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'vocab_002',
|
||||
type: QuestionType.multipleChoice,
|
||||
skillType: SkillType.vocabulary,
|
||||
difficulty: DifficultyLevel.elementary,
|
||||
content: 'Choose the correct synonym for "happy":',
|
||||
options: ['sad', 'joyful', 'angry', 'tired'],
|
||||
correctAnswers: ['joyful'],
|
||||
explanation: 'Joyful is a synonym for happy.',
|
||||
points: 1,
|
||||
timeLimit: 30,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'vocab_003',
|
||||
type: QuestionType.multipleSelect,
|
||||
skillType: SkillType.vocabulary,
|
||||
difficulty: DifficultyLevel.intermediate,
|
||||
content: 'Which of the following are adjectives?',
|
||||
options: ['beautiful', 'quickly', 'run', 'intelligent', 'book'],
|
||||
correctAnswers: ['beautiful', 'intelligent'],
|
||||
explanation: 'Beautiful and intelligent are adjectives that describe nouns.',
|
||||
points: 2,
|
||||
timeLimit: 45,
|
||||
),
|
||||
|
||||
// 语法题目
|
||||
TestQuestion(
|
||||
id: 'grammar_001',
|
||||
type: QuestionType.multipleChoice,
|
||||
skillType: SkillType.grammar,
|
||||
difficulty: DifficultyLevel.beginner,
|
||||
content: 'Choose the correct form: "She ___ to school every day."',
|
||||
options: ['go', 'goes', 'going', 'gone'],
|
||||
correctAnswers: ['goes'],
|
||||
explanation: 'Use "goes" for third person singular in present tense.',
|
||||
points: 1,
|
||||
timeLimit: 30,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'grammar_002',
|
||||
type: QuestionType.fillInBlank,
|
||||
skillType: SkillType.grammar,
|
||||
difficulty: DifficultyLevel.elementary,
|
||||
content: 'Fill in the blank: "I have ___ this book before."',
|
||||
options: ['read', 'reading', 'reads', 'to read'],
|
||||
correctAnswers: ['read'],
|
||||
explanation: 'Use past participle "read" with present perfect tense.',
|
||||
points: 1,
|
||||
timeLimit: 45,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'grammar_003',
|
||||
type: QuestionType.multipleChoice,
|
||||
skillType: SkillType.grammar,
|
||||
difficulty: DifficultyLevel.intermediate,
|
||||
content: 'Which sentence is grammatically correct?',
|
||||
options: [
|
||||
'If I was you, I would study harder.',
|
||||
'If I were you, I would study harder.',
|
||||
'If I am you, I would study harder.',
|
||||
'If I will be you, I would study harder.'
|
||||
],
|
||||
correctAnswers: ['If I were you, I would study harder.'],
|
||||
explanation: 'Use subjunctive mood "were" in hypothetical conditions.',
|
||||
points: 2,
|
||||
timeLimit: 60,
|
||||
),
|
||||
|
||||
// 阅读理解题目
|
||||
TestQuestion(
|
||||
id: 'reading_001',
|
||||
type: QuestionType.reading,
|
||||
skillType: SkillType.reading,
|
||||
difficulty: DifficultyLevel.elementary,
|
||||
content: '''
|
||||
Read the passage and answer the question:
|
||||
|
||||
"Tom is a student. He goes to school by bus every morning. His favorite subject is English. After school, he likes to play football with his friends."
|
||||
|
||||
What is Tom's favorite subject?
|
||||
''',
|
||||
options: ['Math', 'English', 'Science', 'History'],
|
||||
correctAnswers: ['English'],
|
||||
explanation: 'The passage clearly states that his favorite subject is English.',
|
||||
points: 1,
|
||||
timeLimit: 90,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'reading_002',
|
||||
type: QuestionType.reading,
|
||||
skillType: SkillType.reading,
|
||||
difficulty: DifficultyLevel.intermediate,
|
||||
content: '''
|
||||
Read the passage and answer the question:
|
||||
|
||||
"Climate change is one of the most pressing issues of our time. Rising global temperatures are causing ice caps to melt, sea levels to rise, and weather patterns to become increasingly unpredictable. Scientists agree that immediate action is necessary to mitigate these effects."
|
||||
|
||||
What is the main concern discussed in the passage?
|
||||
''',
|
||||
options: [
|
||||
'Economic development',
|
||||
'Climate change and its effects',
|
||||
'Scientific research methods',
|
||||
'Weather forecasting'
|
||||
],
|
||||
correctAnswers: ['Climate change and its effects'],
|
||||
explanation: 'The passage focuses on climate change as a pressing issue and its various effects.',
|
||||
points: 2,
|
||||
timeLimit: 120,
|
||||
),
|
||||
|
||||
// 听力题目
|
||||
TestQuestion(
|
||||
id: 'listening_001',
|
||||
type: QuestionType.listening,
|
||||
skillType: SkillType.listening,
|
||||
difficulty: DifficultyLevel.beginner,
|
||||
content: 'Listen to the audio and choose what you hear:',
|
||||
options: ['Hello', 'Help', 'Hill', 'Hall'],
|
||||
correctAnswers: ['Hello'],
|
||||
audioUrl: 'assets/audio/hello.mp3',
|
||||
explanation: 'The audio says "Hello".',
|
||||
points: 1,
|
||||
timeLimit: 30,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'listening_002',
|
||||
type: QuestionType.listening,
|
||||
skillType: SkillType.listening,
|
||||
difficulty: DifficultyLevel.intermediate,
|
||||
content: 'Listen to the conversation and answer: What time does the meeting start?',
|
||||
options: ['9:00 AM', '10:00 AM', '11:00 AM', '2:00 PM'],
|
||||
correctAnswers: ['10:00 AM'],
|
||||
audioUrl: 'assets/audio/meeting_time.mp3',
|
||||
explanation: 'The speaker mentions the meeting starts at 10:00 AM.',
|
||||
points: 2,
|
||||
timeLimit: 60,
|
||||
),
|
||||
|
||||
// 口语题目
|
||||
TestQuestion(
|
||||
id: 'speaking_001',
|
||||
type: QuestionType.speaking,
|
||||
skillType: SkillType.speaking,
|
||||
difficulty: DifficultyLevel.beginner,
|
||||
content: 'Introduce yourself in English. Include your name, age, and hobby.',
|
||||
options: [],
|
||||
correctAnswers: [],
|
||||
explanation: 'A good introduction should include personal information clearly.',
|
||||
points: 3,
|
||||
timeLimit: 120,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'speaking_002',
|
||||
type: QuestionType.speaking,
|
||||
skillType: SkillType.speaking,
|
||||
difficulty: DifficultyLevel.intermediate,
|
||||
content: 'Describe your favorite place and explain why you like it.',
|
||||
options: [],
|
||||
correctAnswers: [],
|
||||
explanation: 'Focus on descriptive language and clear reasoning.',
|
||||
points: 4,
|
||||
timeLimit: 180,
|
||||
),
|
||||
|
||||
// 写作题目
|
||||
TestQuestion(
|
||||
id: 'writing_001',
|
||||
type: QuestionType.writing,
|
||||
skillType: SkillType.writing,
|
||||
difficulty: DifficultyLevel.elementary,
|
||||
content: 'Write a short paragraph (50-80 words) about your daily routine.',
|
||||
options: [],
|
||||
correctAnswers: [],
|
||||
explanation: 'Include time expressions and daily activities.',
|
||||
points: 5,
|
||||
timeLimit: 300,
|
||||
),
|
||||
|
||||
// 更多词汇题目
|
||||
TestQuestion(
|
||||
id: 'vocab_004',
|
||||
type: QuestionType.multipleChoice,
|
||||
skillType: SkillType.vocabulary,
|
||||
difficulty: DifficultyLevel.intermediate,
|
||||
content: 'What is the meaning of "procrastinate"?',
|
||||
options: ['to delay or postpone', 'to work quickly', 'to organize', 'to celebrate'],
|
||||
correctAnswers: ['to delay or postpone'],
|
||||
explanation: 'Procrastinate means to delay or postpone action.',
|
||||
points: 2,
|
||||
timeLimit: 45,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'vocab_005',
|
||||
type: QuestionType.fillInBlank,
|
||||
skillType: SkillType.vocabulary,
|
||||
difficulty: DifficultyLevel.upperIntermediate,
|
||||
content: 'The company\'s success was _____ to their innovative approach.',
|
||||
options: ['attributed', 'contributed', 'distributed', 'substituted'],
|
||||
correctAnswers: ['attributed'],
|
||||
explanation: 'Attributed means credited or ascribed to a particular cause.',
|
||||
points: 3,
|
||||
timeLimit: 60,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'vocab_006',
|
||||
type: QuestionType.multipleSelect,
|
||||
skillType: SkillType.vocabulary,
|
||||
difficulty: DifficultyLevel.advanced,
|
||||
content: 'Which words are synonyms for "meticulous"?',
|
||||
options: ['careful', 'careless', 'thorough', 'precise', 'sloppy'],
|
||||
correctAnswers: ['careful', 'thorough', 'precise'],
|
||||
explanation: 'Meticulous means showing great attention to detail; very careful and precise.',
|
||||
points: 4,
|
||||
timeLimit: 90,
|
||||
),
|
||||
|
||||
// 更多语法题目
|
||||
TestQuestion(
|
||||
id: 'grammar_004',
|
||||
type: QuestionType.multipleChoice,
|
||||
skillType: SkillType.grammar,
|
||||
difficulty: DifficultyLevel.intermediate,
|
||||
content: 'Choose the correct passive voice: "The teacher explains the lesson."',
|
||||
options: [
|
||||
'The lesson is explained by the teacher.',
|
||||
'The lesson was explained by the teacher.',
|
||||
'The lesson will be explained by the teacher.',
|
||||
'The lesson has been explained by the teacher.'
|
||||
],
|
||||
correctAnswers: ['The lesson is explained by the teacher.'],
|
||||
explanation: 'Present simple active becomes present simple passive.',
|
||||
points: 2,
|
||||
timeLimit: 60,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'grammar_005',
|
||||
type: QuestionType.fillInBlank,
|
||||
skillType: SkillType.grammar,
|
||||
difficulty: DifficultyLevel.upperIntermediate,
|
||||
content: 'By the time you arrive, I _____ the report.',
|
||||
options: ['will finish', 'will have finished', 'finish', 'finished'],
|
||||
correctAnswers: ['will have finished'],
|
||||
explanation: 'Use future perfect for actions completed before a future time.',
|
||||
points: 3,
|
||||
timeLimit: 75,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'grammar_006',
|
||||
type: QuestionType.multipleChoice,
|
||||
skillType: SkillType.grammar,
|
||||
difficulty: DifficultyLevel.advanced,
|
||||
content: 'Which sentence uses the subjunctive mood correctly?',
|
||||
options: [
|
||||
'I wish I was taller.',
|
||||
'I wish I were taller.',
|
||||
'I wish I am taller.',
|
||||
'I wish I will be taller.'
|
||||
],
|
||||
correctAnswers: ['I wish I were taller.'],
|
||||
explanation: 'Use "were" in subjunctive mood for hypothetical situations.',
|
||||
points: 4,
|
||||
timeLimit: 90,
|
||||
),
|
||||
|
||||
// 更多阅读理解题目
|
||||
TestQuestion(
|
||||
id: 'reading_003',
|
||||
type: QuestionType.reading,
|
||||
skillType: SkillType.reading,
|
||||
difficulty: DifficultyLevel.upperIntermediate,
|
||||
content: '''
|
||||
Read the passage and answer the question:
|
||||
|
||||
"Artificial Intelligence has revolutionized numerous industries, from healthcare to finance. Machine learning algorithms can now diagnose diseases with remarkable accuracy, while automated trading systems execute millions of transactions per second. However, this technological advancement raises important ethical questions about job displacement and privacy concerns."
|
||||
|
||||
What is the author's main point about AI?
|
||||
''',
|
||||
options: [
|
||||
'AI is only useful in healthcare',
|
||||
'AI has transformed industries but raises ethical concerns',
|
||||
'AI should be banned from trading',
|
||||
'AI is not accurate enough for medical use'
|
||||
],
|
||||
correctAnswers: ['AI has transformed industries but raises ethical concerns'],
|
||||
explanation: 'The passage discusses both AI\'s benefits and the ethical concerns it raises.',
|
||||
points: 3,
|
||||
timeLimit: 150,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'reading_004',
|
||||
type: QuestionType.reading,
|
||||
skillType: SkillType.reading,
|
||||
difficulty: DifficultyLevel.advanced,
|
||||
content: '''
|
||||
Read the passage and answer the question:
|
||||
|
||||
"The concept of sustainable development emerged in the 1980s as a response to growing environmental concerns. It encompasses three pillars: economic growth, social equity, and environmental protection. Critics argue that these goals are inherently contradictory, as unlimited economic growth is incompatible with finite planetary resources."
|
||||
|
||||
What criticism is mentioned regarding sustainable development?
|
||||
''',
|
||||
options: [
|
||||
'It focuses too much on the environment',
|
||||
'It ignores social equity',
|
||||
'Its three pillars are contradictory',
|
||||
'It was developed too recently'
|
||||
],
|
||||
correctAnswers: ['Its three pillars are contradictory'],
|
||||
explanation: 'Critics argue that unlimited economic growth contradicts environmental protection.',
|
||||
points: 4,
|
||||
timeLimit: 180,
|
||||
),
|
||||
|
||||
// 更多听力题目
|
||||
TestQuestion(
|
||||
id: 'listening_003',
|
||||
type: QuestionType.listening,
|
||||
skillType: SkillType.listening,
|
||||
difficulty: DifficultyLevel.elementary,
|
||||
content: 'Listen to the weather forecast and choose the correct information:',
|
||||
options: ['Sunny, 25°C', 'Rainy, 18°C', 'Cloudy, 22°C', 'Snowy, 5°C'],
|
||||
correctAnswers: ['Cloudy, 22°C'],
|
||||
audioUrl: 'assets/audio/weather_forecast.mp3',
|
||||
explanation: 'The forecast mentions cloudy weather with 22 degrees.',
|
||||
points: 1,
|
||||
timeLimit: 45,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'listening_004',
|
||||
type: QuestionType.listening,
|
||||
skillType: SkillType.listening,
|
||||
difficulty: DifficultyLevel.upperIntermediate,
|
||||
content: 'Listen to the academic lecture and identify the main topic:',
|
||||
options: [
|
||||
'Renewable energy sources',
|
||||
'Climate change effects',
|
||||
'Economic policy',
|
||||
'Educational reform'
|
||||
],
|
||||
correctAnswers: ['Renewable energy sources'],
|
||||
audioUrl: 'assets/audio/academic_lecture.mp3',
|
||||
explanation: 'The lecture focuses on various renewable energy technologies.',
|
||||
points: 3,
|
||||
timeLimit: 120,
|
||||
),
|
||||
|
||||
// 更多口语题目
|
||||
TestQuestion(
|
||||
id: 'speaking_003',
|
||||
type: QuestionType.speaking,
|
||||
skillType: SkillType.speaking,
|
||||
difficulty: DifficultyLevel.elementary,
|
||||
content: 'Describe your hometown. Include information about its location, population, and main attractions.',
|
||||
options: [],
|
||||
correctAnswers: [],
|
||||
explanation: 'Use descriptive adjectives and present tense. Organize your response logically.',
|
||||
points: 3,
|
||||
timeLimit: 150,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'speaking_004',
|
||||
type: QuestionType.speaking,
|
||||
skillType: SkillType.speaking,
|
||||
difficulty: DifficultyLevel.upperIntermediate,
|
||||
content: 'Express your opinion on remote work. Discuss both advantages and disadvantages.',
|
||||
options: [],
|
||||
correctAnswers: [],
|
||||
explanation: 'Present balanced arguments and use appropriate linking words.',
|
||||
points: 4,
|
||||
timeLimit: 240,
|
||||
),
|
||||
|
||||
// 更多写作题目
|
||||
TestQuestion(
|
||||
id: 'writing_002',
|
||||
type: QuestionType.writing,
|
||||
skillType: SkillType.writing,
|
||||
difficulty: DifficultyLevel.intermediate,
|
||||
content: 'Write an email (100-150 words) to your friend describing a recent trip you took.',
|
||||
options: [],
|
||||
correctAnswers: [],
|
||||
explanation: 'Use informal tone, past tense, and descriptive language.',
|
||||
points: 6,
|
||||
timeLimit: 450,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'writing_003',
|
||||
type: QuestionType.writing,
|
||||
skillType: SkillType.writing,
|
||||
difficulty: DifficultyLevel.upperIntermediate,
|
||||
content: 'Write an argumentative essay (200-250 words) about the benefits and drawbacks of social media.',
|
||||
options: [],
|
||||
correctAnswers: [],
|
||||
explanation: 'Include introduction, body paragraphs with examples, and conclusion.',
|
||||
points: 8,
|
||||
timeLimit: 600,
|
||||
),
|
||||
TestQuestion(
|
||||
id: 'writing_002',
|
||||
type: QuestionType.writing,
|
||||
skillType: SkillType.writing,
|
||||
difficulty: DifficultyLevel.intermediate,
|
||||
content: 'Write an essay (150-200 words) about the advantages and disadvantages of social media.',
|
||||
options: [],
|
||||
correctAnswers: [],
|
||||
explanation: 'Present balanced arguments with clear structure.',
|
||||
points: 8,
|
||||
timeLimit: 600,
|
||||
),
|
||||
];
|
||||
|
||||
/// 测试模板静态数据
|
||||
static final List<TestTemplate> _templates = [
|
||||
TestTemplate(
|
||||
id: 'template_quick',
|
||||
name: '快速测试',
|
||||
description: '15分钟快速评估,包含基础词汇和语法题目',
|
||||
type: TestType.quick,
|
||||
duration: 15,
|
||||
totalQuestions: 10,
|
||||
skillDistribution: {
|
||||
SkillType.vocabulary: 5,
|
||||
SkillType.grammar: 3,
|
||||
SkillType.reading: 2,
|
||||
},
|
||||
difficultyDistribution: {
|
||||
DifficultyLevel.beginner: 4,
|
||||
DifficultyLevel.elementary: 4,
|
||||
DifficultyLevel.intermediate: 2,
|
||||
},
|
||||
questionIds: [
|
||||
'vocab_001', 'vocab_002', 'grammar_001', 'grammar_002',
|
||||
'reading_001', 'vocab_003', 'grammar_003', 'reading_002',
|
||||
'listening_001', 'speaking_001'
|
||||
],
|
||||
createdAt: DateTime.now().subtract(const Duration(days: 30)),
|
||||
updatedAt: DateTime.now().subtract(const Duration(days: 1)),
|
||||
),
|
||||
TestTemplate(
|
||||
id: 'template_standard',
|
||||
name: '标准测试',
|
||||
description: '45分钟标准测试,全面评估英语水平',
|
||||
type: TestType.standard,
|
||||
duration: 45,
|
||||
totalQuestions: 25,
|
||||
skillDistribution: {
|
||||
SkillType.vocabulary: 6,
|
||||
SkillType.grammar: 6,
|
||||
SkillType.reading: 5,
|
||||
SkillType.listening: 4,
|
||||
SkillType.speaking: 2,
|
||||
SkillType.writing: 2,
|
||||
},
|
||||
difficultyDistribution: {
|
||||
DifficultyLevel.beginner: 5,
|
||||
DifficultyLevel.elementary: 8,
|
||||
DifficultyLevel.intermediate: 8,
|
||||
DifficultyLevel.upperIntermediate: 3,
|
||||
DifficultyLevel.advanced: 1,
|
||||
},
|
||||
questionIds: [
|
||||
'vocab_001', 'vocab_002', 'vocab_004', 'grammar_001', 'grammar_002', 'grammar_004',
|
||||
'reading_001', 'reading_003', 'listening_001', 'listening_003',
|
||||
'speaking_001', 'speaking_003', 'writing_001', 'writing_002',
|
||||
'vocab_003', 'vocab_005', 'grammar_003', 'grammar_005',
|
||||
'reading_002', 'reading_004', 'listening_002', 'listening_004',
|
||||
'speaking_002', 'speaking_004', 'writing_003'
|
||||
],
|
||||
createdAt: DateTime.now().subtract(const Duration(days: 25)),
|
||||
updatedAt: DateTime.now().subtract(const Duration(days: 2)),
|
||||
),
|
||||
TestTemplate(
|
||||
id: 'template_full',
|
||||
name: '完整测试',
|
||||
description: '90分钟完整测试,深度评估所有技能',
|
||||
type: TestType.full,
|
||||
duration: 90,
|
||||
totalQuestions: 50,
|
||||
skillDistribution: {
|
||||
SkillType.vocabulary: 10,
|
||||
SkillType.grammar: 10,
|
||||
SkillType.reading: 10,
|
||||
SkillType.listening: 8,
|
||||
SkillType.speaking: 6,
|
||||
SkillType.writing: 6,
|
||||
},
|
||||
difficultyDistribution: {
|
||||
DifficultyLevel.beginner: 8,
|
||||
DifficultyLevel.elementary: 12,
|
||||
DifficultyLevel.intermediate: 15,
|
||||
DifficultyLevel.upperIntermediate: 10,
|
||||
DifficultyLevel.advanced: 4,
|
||||
DifficultyLevel.expert: 1,
|
||||
},
|
||||
questionIds: List.generate(50, (index) => 'question_${index + 1}'),
|
||||
createdAt: DateTime.now().subtract(const Duration(days: 20)),
|
||||
updatedAt: DateTime.now().subtract(const Duration(days: 3)),
|
||||
),
|
||||
TestTemplate(
|
||||
id: 'template_mock',
|
||||
name: '模拟考试',
|
||||
description: '120分钟模拟真实考试环境',
|
||||
type: TestType.mock,
|
||||
duration: 120,
|
||||
totalQuestions: 60,
|
||||
skillDistribution: {
|
||||
SkillType.vocabulary: 12,
|
||||
SkillType.grammar: 12,
|
||||
SkillType.reading: 12,
|
||||
SkillType.listening: 10,
|
||||
SkillType.speaking: 7,
|
||||
SkillType.writing: 7,
|
||||
},
|
||||
difficultyDistribution: {
|
||||
DifficultyLevel.beginner: 5,
|
||||
DifficultyLevel.elementary: 10,
|
||||
DifficultyLevel.intermediate: 20,
|
||||
DifficultyLevel.upperIntermediate: 15,
|
||||
DifficultyLevel.advanced: 8,
|
||||
DifficultyLevel.expert: 2,
|
||||
},
|
||||
questionIds: [
|
||||
// 重复使用现有题目来达到60题的要求
|
||||
'vocab_001', 'vocab_002', 'vocab_003', 'vocab_004', 'vocab_005', 'vocab_006',
|
||||
'grammar_001', 'grammar_002', 'grammar_003', 'grammar_004', 'grammar_005', 'grammar_006',
|
||||
'reading_001', 'reading_002', 'reading_003', 'reading_004',
|
||||
'listening_001', 'listening_002', 'listening_003', 'listening_004',
|
||||
'speaking_001', 'speaking_002', 'speaking_003', 'speaking_004',
|
||||
'writing_001', 'writing_002', 'writing_003',
|
||||
// 重复一些题目
|
||||
'vocab_001', 'vocab_002', 'vocab_003', 'vocab_004', 'vocab_005', 'vocab_006',
|
||||
'grammar_001', 'grammar_002', 'grammar_003', 'grammar_004', 'grammar_005', 'grammar_006',
|
||||
'reading_001', 'reading_002', 'reading_003', 'reading_004',
|
||||
'listening_001', 'listening_002', 'listening_003', 'listening_004',
|
||||
'speaking_001', 'speaking_002', 'speaking_003', 'speaking_004',
|
||||
'writing_001', 'writing_002', 'writing_003',
|
||||
// 再次重复以达到60题
|
||||
'vocab_001', 'vocab_002', 'vocab_003', 'vocab_004', 'vocab_005', 'vocab_006'
|
||||
],
|
||||
createdAt: DateTime.now().subtract(const Duration(days: 15)),
|
||||
updatedAt: DateTime.now().subtract(const Duration(days: 1)),
|
||||
),
|
||||
// 添加专项测试模板
|
||||
TestTemplate(
|
||||
id: 'template_vocabulary',
|
||||
name: '词汇专项测试',
|
||||
description: '30分钟专注词汇能力评估',
|
||||
type: TestType.vocabulary,
|
||||
duration: 30,
|
||||
totalQuestions: 20,
|
||||
skillDistribution: {
|
||||
SkillType.vocabulary: 20,
|
||||
},
|
||||
difficultyDistribution: {
|
||||
DifficultyLevel.beginner: 5,
|
||||
DifficultyLevel.elementary: 6,
|
||||
DifficultyLevel.intermediate: 5,
|
||||
DifficultyLevel.upperIntermediate: 3,
|
||||
DifficultyLevel.advanced: 1,
|
||||
},
|
||||
questionIds: [
|
||||
'vocab_001', 'vocab_002', 'vocab_003', 'vocab_004', 'vocab_005', 'vocab_006',
|
||||
'vocab_001', 'vocab_002', 'vocab_003', 'vocab_004', 'vocab_005', 'vocab_006',
|
||||
'vocab_001', 'vocab_002', 'vocab_003', 'vocab_004', 'vocab_005', 'vocab_006',
|
||||
'vocab_001', 'vocab_002'
|
||||
],
|
||||
createdAt: DateTime.now().subtract(const Duration(days: 10)),
|
||||
updatedAt: DateTime.now().subtract(const Duration(hours: 12)),
|
||||
),
|
||||
TestTemplate(
|
||||
id: 'template_grammar',
|
||||
name: '语法专项测试',
|
||||
description: '30分钟专注语法能力评估',
|
||||
type: TestType.grammar,
|
||||
duration: 30,
|
||||
totalQuestions: 20,
|
||||
skillDistribution: {
|
||||
SkillType.grammar: 20,
|
||||
},
|
||||
difficultyDistribution: {
|
||||
DifficultyLevel.beginner: 5,
|
||||
DifficultyLevel.elementary: 6,
|
||||
DifficultyLevel.intermediate: 5,
|
||||
DifficultyLevel.upperIntermediate: 3,
|
||||
DifficultyLevel.advanced: 1,
|
||||
},
|
||||
questionIds: [
|
||||
'grammar_001', 'grammar_002', 'grammar_003', 'grammar_004', 'grammar_005', 'grammar_006',
|
||||
'grammar_001', 'grammar_002', 'grammar_003', 'grammar_004', 'grammar_005', 'grammar_006',
|
||||
'grammar_001', 'grammar_002', 'grammar_003', 'grammar_004', 'grammar_005', 'grammar_006',
|
||||
'grammar_001', 'grammar_002'
|
||||
],
|
||||
createdAt: DateTime.now().subtract(const Duration(days: 8)),
|
||||
updatedAt: DateTime.now().subtract(const Duration(hours: 6)),
|
||||
),
|
||||
TestTemplate(
|
||||
id: 'template_reading',
|
||||
name: '阅读理解专项测试',
|
||||
description: '45分钟专注阅读理解能力评估',
|
||||
type: TestType.reading,
|
||||
duration: 45,
|
||||
totalQuestions: 15,
|
||||
skillDistribution: {
|
||||
SkillType.reading: 15,
|
||||
},
|
||||
difficultyDistribution: {
|
||||
DifficultyLevel.elementary: 4,
|
||||
DifficultyLevel.intermediate: 5,
|
||||
DifficultyLevel.upperIntermediate: 4,
|
||||
DifficultyLevel.advanced: 2,
|
||||
},
|
||||
questionIds: [
|
||||
'reading_001', 'reading_002', 'reading_003', 'reading_004',
|
||||
'reading_001', 'reading_002', 'reading_003', 'reading_004',
|
||||
'reading_001', 'reading_002', 'reading_003', 'reading_004',
|
||||
'reading_001', 'reading_002', 'reading_003'
|
||||
],
|
||||
createdAt: DateTime.now().subtract(const Duration(days: 12)),
|
||||
updatedAt: DateTime.now().subtract(const Duration(hours: 18)),
|
||||
),
|
||||
];
|
||||
|
||||
/// 测试结果静态数据
|
||||
static final List<TestResult> _results = [
|
||||
TestResult(
|
||||
id: 'result_001',
|
||||
testId: 'template_quick',
|
||||
userId: 'user_001',
|
||||
testType: TestType.quick,
|
||||
totalScore: 8,
|
||||
maxScore: 10,
|
||||
percentage: 80.0,
|
||||
overallLevel: DifficultyLevel.elementary,
|
||||
skillScores: [
|
||||
SkillScore(
|
||||
skillType: SkillType.vocabulary,
|
||||
score: 4,
|
||||
maxScore: 5,
|
||||
percentage: 80.0,
|
||||
level: DifficultyLevel.elementary,
|
||||
feedback: '词汇掌握良好,建议继续扩充词汇量',
|
||||
),
|
||||
SkillScore(
|
||||
skillType: SkillType.grammar,
|
||||
score: 2,
|
||||
maxScore: 3,
|
||||
percentage: 66.7,
|
||||
level: DifficultyLevel.beginner,
|
||||
feedback: '语法基础需要加强,多练习时态和句型',
|
||||
),
|
||||
SkillScore(
|
||||
skillType: SkillType.reading,
|
||||
score: 2,
|
||||
maxScore: 2,
|
||||
percentage: 100.0,
|
||||
level: DifficultyLevel.intermediate,
|
||||
feedback: '阅读理解能力优秀',
|
||||
),
|
||||
],
|
||||
answers: [],
|
||||
startTime: DateTime.now().subtract(const Duration(days: 7, hours: 2)),
|
||||
endTime: DateTime.now().subtract(const Duration(days: 7, hours: 1, minutes: 45)),
|
||||
duration: 900, // 15分钟
|
||||
feedback: '总体表现良好,建议重点提升语法技能',
|
||||
recommendations: {
|
||||
'nextLevel': 'elementary',
|
||||
'focusAreas': ['grammar', 'vocabulary'],
|
||||
'suggestedStudyTime': 30,
|
||||
},
|
||||
),
|
||||
TestResult(
|
||||
id: 'result_002',
|
||||
testId: 'template_standard',
|
||||
userId: 'user_001',
|
||||
testType: TestType.standard,
|
||||
totalScore: 18,
|
||||
maxScore: 25,
|
||||
percentage: 72.0,
|
||||
overallLevel: DifficultyLevel.elementary,
|
||||
skillScores: [
|
||||
SkillScore(
|
||||
skillType: SkillType.vocabulary,
|
||||
score: 5,
|
||||
maxScore: 6,
|
||||
percentage: 83.3,
|
||||
level: DifficultyLevel.intermediate,
|
||||
feedback: '词汇水平较好',
|
||||
),
|
||||
SkillScore(
|
||||
skillType: SkillType.grammar,
|
||||
score: 4,
|
||||
maxScore: 6,
|
||||
percentage: 66.7,
|
||||
level: DifficultyLevel.elementary,
|
||||
feedback: '语法需要加强',
|
||||
),
|
||||
SkillScore(
|
||||
skillType: SkillType.reading,
|
||||
score: 4,
|
||||
maxScore: 5,
|
||||
percentage: 80.0,
|
||||
level: DifficultyLevel.elementary,
|
||||
feedback: '阅读理解良好',
|
||||
),
|
||||
SkillScore(
|
||||
skillType: SkillType.listening,
|
||||
score: 3,
|
||||
maxScore: 4,
|
||||
percentage: 75.0,
|
||||
level: DifficultyLevel.elementary,
|
||||
feedback: '听力理解不错',
|
||||
),
|
||||
SkillScore(
|
||||
skillType: SkillType.speaking,
|
||||
score: 1,
|
||||
maxScore: 2,
|
||||
percentage: 50.0,
|
||||
level: DifficultyLevel.beginner,
|
||||
feedback: '口语表达需要大量练习',
|
||||
),
|
||||
SkillScore(
|
||||
skillType: SkillType.writing,
|
||||
score: 1,
|
||||
maxScore: 2,
|
||||
percentage: 50.0,
|
||||
level: DifficultyLevel.beginner,
|
||||
feedback: '写作技能需要提升',
|
||||
),
|
||||
],
|
||||
answers: [],
|
||||
startTime: DateTime.now().subtract(const Duration(days: 3, hours: 1)),
|
||||
endTime: DateTime.now().subtract(const Duration(days: 3, minutes: 15)),
|
||||
duration: 2700, // 45分钟
|
||||
feedback: '整体水平为初级,建议加强语法、口语和写作练习',
|
||||
recommendations: {
|
||||
'nextLevel': 'intermediate',
|
||||
'focusAreas': ['grammar', 'speaking', 'writing'],
|
||||
'suggestedStudyTime': 60,
|
||||
},
|
||||
),
|
||||
TestResult(
|
||||
id: 'result_003',
|
||||
testId: 'template_quick',
|
||||
userId: 'user_001',
|
||||
testType: TestType.quick,
|
||||
totalScore: 9,
|
||||
maxScore: 10,
|
||||
percentage: 90.0,
|
||||
overallLevel: DifficultyLevel.intermediate,
|
||||
skillScores: [
|
||||
SkillScore(
|
||||
skillType: SkillType.vocabulary,
|
||||
score: 5,
|
||||
maxScore: 5,
|
||||
percentage: 100.0,
|
||||
level: DifficultyLevel.intermediate,
|
||||
feedback: '词汇掌握优秀',
|
||||
),
|
||||
SkillScore(
|
||||
skillType: SkillType.grammar,
|
||||
score: 3,
|
||||
maxScore: 3,
|
||||
percentage: 100.0,
|
||||
level: DifficultyLevel.intermediate,
|
||||
feedback: '语法掌握良好',
|
||||
),
|
||||
SkillScore(
|
||||
skillType: SkillType.reading,
|
||||
score: 1,
|
||||
maxScore: 2,
|
||||
percentage: 50.0,
|
||||
level: DifficultyLevel.elementary,
|
||||
feedback: '阅读理解需要提升',
|
||||
),
|
||||
],
|
||||
answers: [],
|
||||
startTime: DateTime.now().subtract(const Duration(days: 1, hours: 2)),
|
||||
endTime: DateTime.now().subtract(const Duration(days: 1, hours: 1, minutes: 45)),
|
||||
duration: 900, // 15分钟
|
||||
feedback: '进步明显,继续保持',
|
||||
recommendations: {
|
||||
'nextLevel': 'intermediate',
|
||||
'focusAreas': ['reading'],
|
||||
'suggestedStudyTime': 45,
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
/// 获取所有测试模板
|
||||
static List<TestTemplate> getAllTemplates() {
|
||||
return List.from(_templates);
|
||||
}
|
||||
|
||||
/// 根据类型获取测试模板
|
||||
static List<TestTemplate> getTemplatesByType(TestType type) {
|
||||
return _templates.where((template) => template.type == type).toList();
|
||||
}
|
||||
|
||||
/// 根据ID获取测试模板
|
||||
static TestTemplate? getTemplateById(String id) {
|
||||
try {
|
||||
return _templates.firstWhere((template) => template.id == id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取所有题目
|
||||
static List<TestQuestion> getAllQuestions() {
|
||||
return List.from(_questions);
|
||||
}
|
||||
|
||||
/// 根据技能类型获取题目
|
||||
static List<TestQuestion> getQuestionsBySkill(SkillType skillType) {
|
||||
return _questions.where((q) => q.skillType == skillType).toList();
|
||||
}
|
||||
|
||||
/// 根据难度获取题目
|
||||
static List<TestQuestion> getQuestionsByDifficulty(DifficultyLevel difficulty) {
|
||||
return _questions.where((q) => q.difficulty == difficulty).toList();
|
||||
}
|
||||
|
||||
/// 根据ID列表获取题目
|
||||
static List<TestQuestion> getQuestionsByIds(List<String> ids) {
|
||||
return _questions.where((q) => ids.contains(q.id)).toList();
|
||||
}
|
||||
|
||||
/// 根据ID获取单个题目
|
||||
static TestQuestion? getQuestionById(String id) {
|
||||
try {
|
||||
return _questions.firstWhere((q) => q.id == id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户测试结果
|
||||
static List<TestResult> getUserResults(String userId) {
|
||||
return _results.where((result) => result.userId == userId).toList();
|
||||
}
|
||||
|
||||
/// 获取最近的测试结果
|
||||
static List<TestResult> getRecentResults(String userId, {int limit = 5}) {
|
||||
final userResults = getUserResults(userId);
|
||||
userResults.sort((a, b) => b.endTime.compareTo(a.endTime));
|
||||
return userResults.take(limit).toList();
|
||||
}
|
||||
|
||||
/// 根据ID获取测试结果
|
||||
static TestResult? getResultById(String id) {
|
||||
try {
|
||||
return _results.firstWhere((result) => result.id == id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取技能统计
|
||||
static Map<SkillType, double> getSkillStatistics(String userId) {
|
||||
final userResults = getUserResults(userId);
|
||||
if (userResults.isEmpty) return {};
|
||||
|
||||
final skillAverages = <SkillType, List<double>>{};
|
||||
|
||||
for (final result in userResults) {
|
||||
for (final skillScore in result.skillScores) {
|
||||
skillAverages.putIfAbsent(skillScore.skillType, () => []);
|
||||
skillAverages[skillScore.skillType]!.add(skillScore.percentage);
|
||||
}
|
||||
}
|
||||
|
||||
return skillAverages.map((skill, scores) {
|
||||
final average = scores.reduce((a, b) => a + b) / scores.length;
|
||||
return MapEntry(skill, average);
|
||||
});
|
||||
}
|
||||
|
||||
/// 获取难度分布统计
|
||||
static Map<DifficultyLevel, int> getDifficultyStatistics() {
|
||||
final distribution = <DifficultyLevel, int>{};
|
||||
for (final question in _questions) {
|
||||
distribution[question.difficulty] =
|
||||
(distribution[question.difficulty] ?? 0) + 1;
|
||||
}
|
||||
return distribution;
|
||||
}
|
||||
|
||||
/// 获取技能分布统计
|
||||
static Map<SkillType, int> getSkillDistributionStatistics() {
|
||||
final distribution = <SkillType, int>{};
|
||||
for (final question in _questions) {
|
||||
distribution[question.skillType] =
|
||||
(distribution[question.skillType] ?? 0) + 1;
|
||||
}
|
||||
return distribution;
|
||||
}
|
||||
|
||||
/// 创建新的测试会话
|
||||
static TestSession createTestSession({
|
||||
required String templateId,
|
||||
required String userId,
|
||||
}) {
|
||||
final template = getTemplateById(templateId);
|
||||
if (template == null) {
|
||||
throw Exception('Template not found: $templateId');
|
||||
}
|
||||
|
||||
final questions = getQuestionsByIds(template.questionIds);
|
||||
|
||||
return TestSession(
|
||||
id: 'session_${DateTime.now().millisecondsSinceEpoch}',
|
||||
templateId: templateId,
|
||||
userId: userId,
|
||||
status: TestStatus.notStarted,
|
||||
questions: questions,
|
||||
answers: [],
|
||||
currentQuestionIndex: 0,
|
||||
startTime: DateTime.now(),
|
||||
timeRemaining: template.duration * 60, // 转换为秒
|
||||
);
|
||||
}
|
||||
|
||||
/// 添加测试结果
|
||||
static void addTestResult(TestResult result) {
|
||||
_results.add(result);
|
||||
}
|
||||
|
||||
/// 更新测试结果
|
||||
static bool updateTestResult(String id, TestResult updatedResult) {
|
||||
final index = _results.indexWhere((result) => result.id == id);
|
||||
if (index != -1) {
|
||||
_results[index] = updatedResult;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 删除测试结果
|
||||
static bool deleteTestResult(String id) {
|
||||
final index = _results.indexWhere((result) => result.id == id);
|
||||
if (index != -1) {
|
||||
_results.removeAt(index);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
559
client/lib/features/comprehensive_test/models/test_models.dart
Normal file
559
client/lib/features/comprehensive_test/models/test_models.dart
Normal file
@@ -0,0 +1,559 @@
|
||||
/// 测试类型枚举
|
||||
enum TestType {
|
||||
quick, // 快速测试
|
||||
standard, // 标准测试
|
||||
full, // 完整测试
|
||||
mock, // 模拟考试
|
||||
vocabulary, // 词汇专项
|
||||
grammar, // 语法专项
|
||||
reading, // 阅读专项
|
||||
listening, // 听力专项
|
||||
speaking, // 口语专项
|
||||
writing, // 写作专项
|
||||
}
|
||||
|
||||
/// 测试状态枚举
|
||||
enum TestStatus {
|
||||
notStarted, // 未开始
|
||||
inProgress, // 进行中
|
||||
paused, // 暂停
|
||||
completed, // 已完成
|
||||
expired, // 已过期
|
||||
}
|
||||
|
||||
/// 题目类型枚举
|
||||
enum QuestionType {
|
||||
multipleChoice, // 单选题
|
||||
multipleSelect, // 多选题
|
||||
fillInBlank, // 填空题
|
||||
reading, // 阅读理解
|
||||
listening, // 听力理解
|
||||
speaking, // 口语题
|
||||
writing, // 写作题
|
||||
}
|
||||
|
||||
/// 难度等级枚举
|
||||
enum DifficultyLevel {
|
||||
beginner, // 初级
|
||||
elementary, // 基础
|
||||
intermediate, // 中级
|
||||
upperIntermediate, // 中高级
|
||||
advanced, // 高级
|
||||
expert, // 专家级
|
||||
}
|
||||
|
||||
/// 技能类型枚举
|
||||
enum SkillType {
|
||||
vocabulary, // 词汇
|
||||
grammar, // 语法
|
||||
reading, // 阅读
|
||||
listening, // 听力
|
||||
speaking, // 口语
|
||||
writing, // 写作
|
||||
}
|
||||
|
||||
/// 测试题目模型
|
||||
class TestQuestion {
|
||||
final String id;
|
||||
final QuestionType type;
|
||||
final SkillType skillType;
|
||||
final DifficultyLevel difficulty;
|
||||
final String content;
|
||||
final List<String> options;
|
||||
final List<String> correctAnswers;
|
||||
final String? audioUrl;
|
||||
final String? imageUrl;
|
||||
final String? explanation;
|
||||
final int points;
|
||||
final int timeLimit; // 秒
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
const TestQuestion({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.skillType,
|
||||
required this.difficulty,
|
||||
required this.content,
|
||||
this.options = const [],
|
||||
this.correctAnswers = const [],
|
||||
this.audioUrl,
|
||||
this.imageUrl,
|
||||
this.explanation,
|
||||
this.points = 1,
|
||||
this.timeLimit = 60,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
factory TestQuestion.fromJson(Map<String, dynamic> json) {
|
||||
return TestQuestion(
|
||||
id: json['id'] as String,
|
||||
type: QuestionType.values.firstWhere(
|
||||
(e) => e.name == json['type'],
|
||||
orElse: () => QuestionType.multipleChoice,
|
||||
),
|
||||
skillType: SkillType.values.firstWhere(
|
||||
(e) => e.name == json['skillType'],
|
||||
orElse: () => SkillType.vocabulary,
|
||||
),
|
||||
difficulty: DifficultyLevel.values.firstWhere(
|
||||
(e) => e.name == json['difficulty'],
|
||||
orElse: () => DifficultyLevel.intermediate,
|
||||
),
|
||||
content: json['content'] as String,
|
||||
options: List<String>.from(json['options'] ?? []),
|
||||
correctAnswers: List<String>.from(json['correctAnswers'] ?? []),
|
||||
audioUrl: json['audioUrl'] as String?,
|
||||
imageUrl: json['imageUrl'] as String?,
|
||||
explanation: json['explanation'] as String?,
|
||||
points: json['points'] as int? ?? 1,
|
||||
timeLimit: json['timeLimit'] as int? ?? 60,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'type': type.name,
|
||||
'skillType': skillType.name,
|
||||
'difficulty': difficulty.name,
|
||||
'content': content,
|
||||
'options': options,
|
||||
'correctAnswers': correctAnswers,
|
||||
'audioUrl': audioUrl,
|
||||
'imageUrl': imageUrl,
|
||||
'explanation': explanation,
|
||||
'points': points,
|
||||
'timeLimit': timeLimit,
|
||||
'metadata': metadata,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 语言技能枚举 (用于UI显示)
|
||||
enum LanguageSkill {
|
||||
listening, // 听力
|
||||
reading, // 阅读
|
||||
speaking, // 口语
|
||||
writing, // 写作
|
||||
vocabulary, // 词汇
|
||||
grammar, // 语法
|
||||
pronunciation, // 发音
|
||||
comprehension, // 理解
|
||||
}
|
||||
|
||||
/// 用户技能统计模型
|
||||
class UserSkillStatistics {
|
||||
final SkillType skillType;
|
||||
final double averageScore;
|
||||
final int totalTests;
|
||||
final int correctAnswers;
|
||||
final int totalQuestions;
|
||||
final DifficultyLevel strongestLevel;
|
||||
final DifficultyLevel weakestLevel;
|
||||
final DateTime lastTestDate;
|
||||
|
||||
const UserSkillStatistics({
|
||||
required this.skillType,
|
||||
required this.averageScore,
|
||||
required this.totalTests,
|
||||
required this.correctAnswers,
|
||||
required this.totalQuestions,
|
||||
required this.strongestLevel,
|
||||
required this.weakestLevel,
|
||||
required this.lastTestDate,
|
||||
});
|
||||
|
||||
factory UserSkillStatistics.fromJson(Map<String, dynamic> json) {
|
||||
return UserSkillStatistics(
|
||||
skillType: SkillType.values.firstWhere(
|
||||
(e) => e.toString().split('.').last == json['skillType'],
|
||||
),
|
||||
averageScore: (json['averageScore'] as num).toDouble(),
|
||||
totalTests: json['totalTests'] as int,
|
||||
correctAnswers: json['correctAnswers'] as int,
|
||||
totalQuestions: json['totalQuestions'] as int,
|
||||
strongestLevel: DifficultyLevel.values.firstWhere(
|
||||
(e) => e.toString().split('.').last == json['strongestLevel'],
|
||||
),
|
||||
weakestLevel: DifficultyLevel.values.firstWhere(
|
||||
(e) => e.toString().split('.').last == json['weakestLevel'],
|
||||
),
|
||||
lastTestDate: DateTime.parse(json['lastTestDate']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'skillType': skillType.toString().split('.').last,
|
||||
'averageScore': averageScore,
|
||||
'totalTests': totalTests,
|
||||
'correctAnswers': correctAnswers,
|
||||
'totalQuestions': totalQuestions,
|
||||
'strongestLevel': strongestLevel.toString().split('.').last,
|
||||
'weakestLevel': weakestLevel.toString().split('.').last,
|
||||
'lastTestDate': lastTestDate.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
double get accuracy => totalQuestions > 0 ? correctAnswers / totalQuestions : 0.0;
|
||||
}
|
||||
|
||||
/// 用户答案模型
|
||||
class UserAnswer {
|
||||
final String questionId;
|
||||
final List<String> selectedAnswers;
|
||||
final String? textAnswer;
|
||||
final String? audioUrl;
|
||||
final DateTime answeredAt;
|
||||
final int timeSpent; // 秒
|
||||
|
||||
const UserAnswer({
|
||||
required this.questionId,
|
||||
this.selectedAnswers = const [],
|
||||
this.textAnswer,
|
||||
this.audioUrl,
|
||||
required this.answeredAt,
|
||||
required this.timeSpent,
|
||||
});
|
||||
|
||||
factory UserAnswer.fromJson(Map<String, dynamic> json) {
|
||||
return UserAnswer(
|
||||
questionId: json['questionId'] as String,
|
||||
selectedAnswers: List<String>.from(json['selectedAnswers'] ?? []),
|
||||
textAnswer: json['textAnswer'] as String?,
|
||||
audioUrl: json['audioUrl'] as String?,
|
||||
answeredAt: DateTime.parse(json['answeredAt'] as String),
|
||||
timeSpent: json['timeSpent'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'questionId': questionId,
|
||||
'selectedAnswers': selectedAnswers,
|
||||
'textAnswer': textAnswer,
|
||||
'audioUrl': audioUrl,
|
||||
'answeredAt': answeredAt.toIso8601String(),
|
||||
'timeSpent': timeSpent,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 技能得分模型
|
||||
class SkillScore {
|
||||
final SkillType skillType;
|
||||
final int score;
|
||||
final int maxScore;
|
||||
final double percentage;
|
||||
final DifficultyLevel level;
|
||||
final String feedback;
|
||||
|
||||
const SkillScore({
|
||||
required this.skillType,
|
||||
required this.score,
|
||||
required this.maxScore,
|
||||
required this.percentage,
|
||||
required this.level,
|
||||
required this.feedback,
|
||||
});
|
||||
|
||||
factory SkillScore.fromJson(Map<String, dynamic> json) {
|
||||
return SkillScore(
|
||||
skillType: SkillType.values.firstWhere(
|
||||
(e) => e.name == json['skillType'],
|
||||
orElse: () => SkillType.vocabulary,
|
||||
),
|
||||
score: json['score'] as int,
|
||||
maxScore: json['maxScore'] as int,
|
||||
percentage: (json['percentage'] as num).toDouble(),
|
||||
level: DifficultyLevel.values.firstWhere(
|
||||
(e) => e.name == json['level'],
|
||||
orElse: () => DifficultyLevel.intermediate,
|
||||
),
|
||||
feedback: json['feedback'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'skillType': skillType.name,
|
||||
'score': score,
|
||||
'maxScore': maxScore,
|
||||
'percentage': percentage,
|
||||
'level': level.name,
|
||||
'feedback': feedback,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试结果模型
|
||||
class TestResult {
|
||||
final String id;
|
||||
final String testId;
|
||||
final String userId;
|
||||
final TestType testType;
|
||||
final int totalScore;
|
||||
final int maxScore;
|
||||
final double percentage;
|
||||
final DifficultyLevel overallLevel;
|
||||
final List<SkillScore> skillScores;
|
||||
final List<UserAnswer> answers;
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
final int duration; // 秒
|
||||
final String feedback;
|
||||
final Map<String, dynamic>? recommendations;
|
||||
|
||||
const TestResult({
|
||||
required this.id,
|
||||
required this.testId,
|
||||
required this.userId,
|
||||
required this.testType,
|
||||
required this.totalScore,
|
||||
required this.maxScore,
|
||||
required this.percentage,
|
||||
required this.overallLevel,
|
||||
required this.skillScores,
|
||||
required this.answers,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.duration,
|
||||
required this.feedback,
|
||||
this.recommendations,
|
||||
});
|
||||
|
||||
factory TestResult.fromJson(Map<String, dynamic> json) {
|
||||
return TestResult(
|
||||
id: json['id'] as String,
|
||||
testId: json['testId'] as String,
|
||||
userId: json['userId'] as String,
|
||||
testType: TestType.values.firstWhere(
|
||||
(e) => e.name == json['testType'],
|
||||
orElse: () => TestType.standard,
|
||||
),
|
||||
totalScore: json['totalScore'] as int,
|
||||
maxScore: json['maxScore'] as int,
|
||||
percentage: (json['percentage'] as num).toDouble(),
|
||||
overallLevel: DifficultyLevel.values.firstWhere(
|
||||
(e) => e.name == json['overallLevel'],
|
||||
orElse: () => DifficultyLevel.intermediate,
|
||||
),
|
||||
skillScores: (json['skillScores'] as List<dynamic>)
|
||||
.map((e) => SkillScore.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
answers: (json['answers'] as List<dynamic>)
|
||||
.map((e) => UserAnswer.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
startTime: DateTime.parse(json['startTime'] as String),
|
||||
endTime: DateTime.parse(json['endTime'] as String),
|
||||
duration: json['duration'] as int,
|
||||
feedback: json['feedback'] as String,
|
||||
recommendations: json['recommendations'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'testId': testId,
|
||||
'userId': userId,
|
||||
'testType': testType.name,
|
||||
'totalScore': totalScore,
|
||||
'maxScore': maxScore,
|
||||
'percentage': percentage,
|
||||
'overallLevel': overallLevel.name,
|
||||
'skillScores': skillScores.map((e) => e.toJson()).toList(),
|
||||
'answers': answers.map((e) => e.toJson()).toList(),
|
||||
'startTime': startTime.toIso8601String(),
|
||||
'endTime': endTime.toIso8601String(),
|
||||
'duration': duration,
|
||||
'feedback': feedback,
|
||||
'recommendations': recommendations,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试模板模型
|
||||
class TestTemplate {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final TestType type;
|
||||
final int duration; // 分钟
|
||||
final int totalQuestions;
|
||||
final Map<SkillType, int> skillDistribution;
|
||||
final Map<DifficultyLevel, int> difficultyDistribution;
|
||||
final List<String> questionIds;
|
||||
final bool isActive;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const TestTemplate({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.type,
|
||||
required this.duration,
|
||||
required this.totalQuestions,
|
||||
required this.skillDistribution,
|
||||
required this.difficultyDistribution,
|
||||
required this.questionIds,
|
||||
this.isActive = true,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory TestTemplate.fromJson(Map<String, dynamic> json) {
|
||||
return TestTemplate(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
type: TestType.values.firstWhere(
|
||||
(e) => e.name == json['type'],
|
||||
orElse: () => TestType.standard,
|
||||
),
|
||||
duration: json['duration'] as int,
|
||||
totalQuestions: json['totalQuestions'] as int,
|
||||
skillDistribution: Map<SkillType, int>.fromEntries(
|
||||
(json['skillDistribution'] as Map<String, dynamic>).entries.map(
|
||||
(e) => MapEntry(
|
||||
SkillType.values.firstWhere((skill) => skill.name == e.key),
|
||||
e.value as int,
|
||||
),
|
||||
),
|
||||
),
|
||||
difficultyDistribution: Map<DifficultyLevel, int>.fromEntries(
|
||||
(json['difficultyDistribution'] as Map<String, dynamic>).entries.map(
|
||||
(e) => MapEntry(
|
||||
DifficultyLevel.values.firstWhere((level) => level.name == e.key),
|
||||
e.value as int,
|
||||
),
|
||||
),
|
||||
),
|
||||
questionIds: List<String>.from(json['questionIds']),
|
||||
isActive: json['isActive'] as bool? ?? true,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'type': type.name,
|
||||
'duration': duration,
|
||||
'totalQuestions': totalQuestions,
|
||||
'skillDistribution': skillDistribution.map(
|
||||
(key, value) => MapEntry(key.name, value),
|
||||
),
|
||||
'difficultyDistribution': difficultyDistribution.map(
|
||||
(key, value) => MapEntry(key.name, value),
|
||||
),
|
||||
'questionIds': questionIds,
|
||||
'isActive': isActive,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试会话模型
|
||||
class TestSession {
|
||||
final String id;
|
||||
final String templateId;
|
||||
final String userId;
|
||||
final TestStatus status;
|
||||
final List<TestQuestion> questions;
|
||||
final List<UserAnswer> answers;
|
||||
final int currentQuestionIndex;
|
||||
final DateTime startTime;
|
||||
final DateTime? endTime;
|
||||
final int timeRemaining; // 秒
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
const TestSession({
|
||||
required this.id,
|
||||
required this.templateId,
|
||||
required this.userId,
|
||||
required this.status,
|
||||
required this.questions,
|
||||
this.answers = const [],
|
||||
this.currentQuestionIndex = 0,
|
||||
required this.startTime,
|
||||
this.endTime,
|
||||
required this.timeRemaining,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
factory TestSession.fromJson(Map<String, dynamic> json) {
|
||||
return TestSession(
|
||||
id: json['id'] as String,
|
||||
templateId: json['templateId'] as String,
|
||||
userId: json['userId'] as String,
|
||||
status: TestStatus.values.firstWhere(
|
||||
(e) => e.name == json['status'],
|
||||
orElse: () => TestStatus.notStarted,
|
||||
),
|
||||
questions: (json['questions'] as List<dynamic>)
|
||||
.map((e) => TestQuestion.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
answers: (json['answers'] as List<dynamic>?)
|
||||
?.map((e) => UserAnswer.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ?? [],
|
||||
currentQuestionIndex: json['currentQuestionIndex'] as int? ?? 0,
|
||||
startTime: DateTime.parse(json['startTime'] as String),
|
||||
endTime: json['endTime'] != null
|
||||
? DateTime.parse(json['endTime'] as String)
|
||||
: null,
|
||||
timeRemaining: json['timeRemaining'] as int,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'templateId': templateId,
|
||||
'userId': userId,
|
||||
'status': status.name,
|
||||
'questions': questions.map((e) => e.toJson()).toList(),
|
||||
'answers': answers.map((e) => e.toJson()).toList(),
|
||||
'currentQuestionIndex': currentQuestionIndex,
|
||||
'startTime': startTime.toIso8601String(),
|
||||
'endTime': endTime?.toIso8601String(),
|
||||
'timeRemaining': timeRemaining,
|
||||
'metadata': metadata,
|
||||
};
|
||||
}
|
||||
|
||||
TestSession copyWith({
|
||||
String? id,
|
||||
String? templateId,
|
||||
String? userId,
|
||||
TestStatus? status,
|
||||
List<TestQuestion>? questions,
|
||||
List<UserAnswer>? answers,
|
||||
int? currentQuestionIndex,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
int? timeRemaining,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) {
|
||||
return TestSession(
|
||||
id: id ?? this.id,
|
||||
templateId: templateId ?? this.templateId,
|
||||
userId: userId ?? this.userId,
|
||||
status: status ?? this.status,
|
||||
questions: questions ?? this.questions,
|
||||
answers: answers ?? this.answers,
|
||||
currentQuestionIndex: currentQuestionIndex ?? this.currentQuestionIndex,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
timeRemaining: timeRemaining ?? this.timeRemaining,
|
||||
metadata: metadata ?? this.metadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/test_models.dart';
|
||||
import '../services/test_api_service.dart';
|
||||
import '../../../core/models/api_response.dart';
|
||||
|
||||
/// 综合测试状态管理类
|
||||
class TestProvider with ChangeNotifier {
|
||||
final TestApiService _testService;
|
||||
|
||||
TestProvider({TestApiService? testService})
|
||||
: _testService = testService ?? TestApiService();
|
||||
|
||||
// 加载状<E8BDBD>? bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
// 错误信息
|
||||
String? _errorMessage;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
// 测试模板
|
||||
List<TestTemplate> _templates = [];
|
||||
List<TestTemplate> get templates => _templates;
|
||||
|
||||
// 当前测试会话
|
||||
TestSession? _currentSession;
|
||||
TestSession? get currentSession => _currentSession;
|
||||
|
||||
// 当前题目
|
||||
TestQuestion? get currentQuestion {
|
||||
if (_currentSession == null ||
|
||||
_currentSession!.currentQuestionIndex >= _currentSession!.questions.length) {
|
||||
return null;
|
||||
}
|
||||
return _currentSession!.questions[_currentSession!.currentQuestionIndex];
|
||||
}
|
||||
|
||||
// 测试结果
|
||||
List<TestResult> _testResults = [];
|
||||
List<TestResult> get testResults => _testResults;
|
||||
|
||||
TestResult? _currentResult;
|
||||
TestResult? get currentResult => _currentResult;
|
||||
|
||||
// 用户技能统<E883BD>? Map<SkillType, double> _skillStatistics = {};
|
||||
Map<SkillType, double> get skillStatistics => _skillStatistics;
|
||||
|
||||
// 题目统计
|
||||
Map<String, dynamic> _questionStatistics = {};
|
||||
Map<String, dynamic> get questionStatistics => _questionStatistics;
|
||||
|
||||
// 计时器相<E599A8>? int _timeRemaining = 0;
|
||||
int get timeRemaining => _timeRemaining;
|
||||
|
||||
bool _isTimerRunning = false;
|
||||
bool get isTimerRunning => _isTimerRunning;
|
||||
|
||||
/// 设置加载状<E8BDBD>? void _setLoading(bool loading) {
|
||||
_isLoading = loading;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 设置错误信息
|
||||
void _setError(String? error) {
|
||||
_errorMessage = error;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 清除错误信息
|
||||
void clearError() {
|
||||
_setError(null);
|
||||
}
|
||||
|
||||
/// 加载所有测试模<E8AF95>? Future<void> loadTestTemplates() async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.getTestTemplates();
|
||||
if (response.success && response.data != null) {
|
||||
_templates = response.data!;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('加载测试模板失败: ${e.toString()}');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据类型加载测试模板
|
||||
Future<void> loadTestTemplatesByType(TestType type) async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.getTestTemplatesByType(type);
|
||||
if (response.success && response.data != null) {
|
||||
_templates = response.data!;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('加载测试模板失败: ${e.toString()}');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建测试会话
|
||||
Future<bool> createTestSession({
|
||||
required String templateId,
|
||||
required String userId,
|
||||
}) async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.createTestSession(
|
||||
templateId: templateId,
|
||||
userId: userId,
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_currentSession = response.data!;
|
||||
_timeRemaining = _currentSession!.timeRemaining;
|
||||
return true;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('创建测试会话失败: ${e.toString()}');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始测<E5A78B>? Future<bool> startTest() async {
|
||||
if (_currentSession == null) {
|
||||
_setError('没有活动的测试会<EFBFBD>?);
|
||||
return false;
|
||||
}
|
||||
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.startTest(_currentSession!.id);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_currentSession = response.data!;
|
||||
_timeRemaining = _currentSession!.timeRemaining;
|
||||
_startTimer();
|
||||
return true;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('开始测试失<EFBFBD>? ${e.toString()}');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交答案
|
||||
Future<bool> submitAnswer(UserAnswer answer) async {
|
||||
if (_currentSession == null) {
|
||||
_setError('没有活动的测试会<EFBFBD>?);
|
||||
return false;
|
||||
}
|
||||
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.submitAnswer(
|
||||
sessionId: _currentSession!.id,
|
||||
answer: answer,
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_currentSession = response.data!;
|
||||
_timeRemaining = _currentSession!.timeRemaining;
|
||||
return true;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('提交答案失败: ${e.toString()}');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 下一<E4B88B>? void nextQuestion() {
|
||||
if (_currentSession != null &&
|
||||
_currentSession!.currentQuestionIndex < _currentSession!.questions.length - 1) {
|
||||
_currentSession = _currentSession!.copyWith(
|
||||
currentQuestionIndex: _currentSession!.currentQuestionIndex + 1,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 上一<E4B88A>? void previousQuestion() {
|
||||
if (_currentSession != null && _currentSession!.currentQuestionIndex > 0) {
|
||||
_currentSession = _currentSession!.copyWith(
|
||||
currentQuestionIndex: _currentSession!.currentQuestionIndex - 1,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 跳转到指定题<E5AE9A>? void goToQuestion(int index) {
|
||||
if (_currentSession != null &&
|
||||
index >= 0 &&
|
||||
index < _currentSession!.questions.length) {
|
||||
_currentSession = _currentSession!.copyWith(
|
||||
currentQuestionIndex: index,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停测试
|
||||
Future<bool> pauseTest() async {
|
||||
if (_currentSession == null) {
|
||||
_setError('没有活动的测试会<EFBFBD>?);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _testService.pauseTest(_currentSession!.id);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_currentSession = response.data!;
|
||||
_pauseTimer();
|
||||
return true;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('暂停测试失败: ${e.toString()}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复测试
|
||||
Future<bool> resumeTest() async {
|
||||
if (_currentSession == null) {
|
||||
_setError('没有活动的测试会<EFBFBD>?);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _testService.resumeTest(_currentSession!.id);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_currentSession = response.data!;
|
||||
_startTimer();
|
||||
return true;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('恢复测试失败: ${e.toString()}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 完成测试
|
||||
Future<bool> completeTest() async {
|
||||
if (_currentSession == null) {
|
||||
_setError('没有活动的测试会话');
|
||||
return false;
|
||||
}
|
||||
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.completeTest(_currentSession!.id);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_currentResult = response.data!;
|
||||
_stopTimer();
|
||||
_currentSession = _currentSession!.copyWith(
|
||||
status: TestStatus.completed,
|
||||
endTime: DateTime.now(),
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('完成测试失败: ${e.toString()}');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载用户测试历史
|
||||
Future<void> loadUserTestHistory(String userId, {int page = 1, int limit = 10}) async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.getUserTestHistory(
|
||||
page: page,
|
||||
limit: limit,
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
if (page == 1) {
|
||||
_testResults = response.data!;
|
||||
} else {
|
||||
_testResults.addAll(response.data!);
|
||||
}
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('加载测试历史失败: ${e.toString()}');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载最近的测试结果
|
||||
Future<void> loadRecentTestResults(String userId, {int limit = 5}) async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.getRecentTestResults(
|
||||
userId,
|
||||
limit: limit,
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_testResults = response.data!;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('加载最近测试结果失<EFBFBD>? ${e.toString()}');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载测试结果详情
|
||||
Future<void> loadTestResultById(String resultId) async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.getTestResultById(resultId);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_currentResult = response.data!;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('加载测试结果失败: ${e.toString()}');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载用户技能统<E883BD>? Future<void> loadUserSkillStatistics(String userId) async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.getUserSkillStatistics(userId);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_skillStatistics = response.data!;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('加载技能统计失<EFBFBD>? ${e.toString()}');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载题目统计
|
||||
Future<void> loadQuestionStatistics() async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.getQuestionStatistics();
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
_questionStatistics = response.data!;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('加载题目统计失败: ${e.toString()}');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除测试结果
|
||||
Future<bool> deleteTestResult(String resultId) async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.deleteTestResult(resultId);
|
||||
|
||||
if (response.success) {
|
||||
_testResults.removeWhere((result) => result.id == resultId);
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('删除测试结果失败: ${e.toString()}');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置当前会话
|
||||
void resetCurrentSession() {
|
||||
_currentSession = null;
|
||||
_currentResult = null;
|
||||
_stopTimer();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 开始计时器
|
||||
void _startTimer() {
|
||||
_isTimerRunning = true;
|
||||
_updateTimer();
|
||||
}
|
||||
|
||||
/// 暂停计时<E8AEA1>? void _pauseTimer() {
|
||||
_isTimerRunning = false;
|
||||
}
|
||||
|
||||
/// 停止计时<E8AEA1>? void _stopTimer() {
|
||||
_isTimerRunning = false;
|
||||
_timeRemaining = 0;
|
||||
}
|
||||
|
||||
/// 更新计时<E8AEA1>? void _updateTimer() {
|
||||
if (_isTimerRunning && _timeRemaining > 0) {
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (_isTimerRunning) {
|
||||
_timeRemaining--;
|
||||
notifyListeners();
|
||||
|
||||
if (_timeRemaining <= 0) {
|
||||
// 时间到,自动完成测试
|
||||
completeTest();
|
||||
} else {
|
||||
_updateTimer();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 格式化剩余时<E4BD99>? String getFormattedTimeRemaining() {
|
||||
final hours = _timeRemaining ~/ 3600;
|
||||
final minutes = (_timeRemaining % 3600) ~/ 60;
|
||||
final seconds = _timeRemaining % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取测试进度百分<E799BE>? double getTestProgress() {
|
||||
if (_currentSession == null || _currentSession!.questions.isEmpty) {
|
||||
return 0.0;
|
||||
}
|
||||
return (_currentSession!.currentQuestionIndex + 1) / _currentSession!.questions.length;
|
||||
}
|
||||
|
||||
/// 获取已回答题目数<E79BAE>? int getAnsweredQuestionsCount() {
|
||||
return _currentSession?.answers.length ?? 0;
|
||||
}
|
||||
|
||||
/// 获取未回答题目数<E79BAE>? int getUnansweredQuestionsCount() {
|
||||
if (_currentSession == null) return 0;
|
||||
return _currentSession!.questions.length - _currentSession!.answers.length;
|
||||
}
|
||||
|
||||
/// 检查是否所有题目都已回<E5B7B2>? bool areAllQuestionsAnswered() {
|
||||
if (_currentSession == null) return false;
|
||||
return _currentSession!.answers.length == _currentSession!.questions.length;
|
||||
}
|
||||
|
||||
/// 获取技能类型的中文名称
|
||||
String getSkillTypeName(SkillType skillType) {
|
||||
switch (skillType) {
|
||||
case SkillType.vocabulary:
|
||||
return '词汇';
|
||||
case SkillType.grammar:
|
||||
return '语法';
|
||||
case SkillType.reading:
|
||||
return '阅读';
|
||||
case SkillType.listening:
|
||||
return '听力';
|
||||
case SkillType.speaking:
|
||||
return '口语';
|
||||
case SkillType.writing:
|
||||
return '写作';
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取难度等级的中文名<E69687>? String getDifficultyLevelName(DifficultyLevel level) {
|
||||
switch (level) {
|
||||
case DifficultyLevel.beginner:
|
||||
return '初级';
|
||||
case DifficultyLevel.elementary:
|
||||
return '基础';
|
||||
case DifficultyLevel.intermediate:
|
||||
return '中级';
|
||||
case DifficultyLevel.upperIntermediate:
|
||||
return '中高<EFBFBD>?;
|
||||
case DifficultyLevel.advanced:
|
||||
return '高级';
|
||||
case DifficultyLevel.expert:
|
||||
return '专家<EFBFBD>?;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取测试类型的中文名<E69687>? String getTestTypeName(TestType type) {
|
||||
switch (type) {
|
||||
case TestType.quick:
|
||||
return '快速测<EFBFBD>?;
|
||||
case TestType.standard:
|
||||
return '标准测试';
|
||||
case TestType.full:
|
||||
return '完整测试';
|
||||
case TestType.mock:
|
||||
return '模拟考试';
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载最近的测试结果
|
||||
Future<void> loadRecentResults({String? userId}) async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.getRecentTestResults(userId);
|
||||
if (response.success && response.data != null) {
|
||||
_testResults = response.data!;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('加载测试结果失败: ${e.toString()}');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopTimer();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../models/test_models.dart';
|
||||
import '../services/test_service.dart';
|
||||
import '../services/test_service_impl.dart';
|
||||
import '../../../core/models/api_response.dart';
|
||||
|
||||
/// 测试服务提供者
|
||||
final testServiceProvider = Provider<TestService>((ref) {
|
||||
return TestServiceImpl();
|
||||
});
|
||||
|
||||
/// 测试状态类
|
||||
class TestState {
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
final List<TestTemplate> templates;
|
||||
final TestSession? currentSession;
|
||||
final List<TestResult> testResults;
|
||||
final TestResult? currentResult;
|
||||
final Map<SkillType, double> skillStatistics;
|
||||
final Map<String, dynamic> questionStatistics;
|
||||
final int timeRemaining;
|
||||
final bool isTimerRunning;
|
||||
|
||||
const TestState({
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
this.templates = const [],
|
||||
this.currentSession,
|
||||
this.testResults = const [],
|
||||
this.currentResult,
|
||||
this.skillStatistics = const {},
|
||||
this.questionStatistics = const {},
|
||||
this.timeRemaining = 0,
|
||||
this.isTimerRunning = false,
|
||||
});
|
||||
|
||||
TestState copyWith({
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
List<TestTemplate>? templates,
|
||||
TestSession? currentSession,
|
||||
List<TestResult>? testResults,
|
||||
TestResult? currentResult,
|
||||
Map<SkillType, double>? skillStatistics,
|
||||
Map<String, dynamic>? questionStatistics,
|
||||
int? timeRemaining,
|
||||
bool? isTimerRunning,
|
||||
}) {
|
||||
return TestState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
templates: templates ?? this.templates,
|
||||
currentSession: currentSession ?? this.currentSession,
|
||||
testResults: testResults ?? this.testResults,
|
||||
currentResult: currentResult ?? this.currentResult,
|
||||
skillStatistics: skillStatistics ?? this.skillStatistics,
|
||||
questionStatistics: questionStatistics ?? this.questionStatistics,
|
||||
timeRemaining: timeRemaining ?? this.timeRemaining,
|
||||
isTimerRunning: isTimerRunning ?? this.isTimerRunning,
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取当前题目
|
||||
TestQuestion? get currentQuestion {
|
||||
if (currentSession == null ||
|
||||
currentSession!.currentQuestionIndex >= currentSession!.questions.length) {
|
||||
return null;
|
||||
}
|
||||
return currentSession!.questions[currentSession!.currentQuestionIndex];
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试状态管理器
|
||||
class TestNotifier extends StateNotifier<TestState> {
|
||||
final TestService _testService;
|
||||
|
||||
TestNotifier(this._testService) : super(const TestState());
|
||||
|
||||
/// 设置加载状态
|
||||
void _setLoading(bool loading) {
|
||||
state = state.copyWith(isLoading: loading);
|
||||
}
|
||||
|
||||
/// 设置错误信息
|
||||
void _setError(String? error) {
|
||||
state = state.copyWith(errorMessage: error);
|
||||
}
|
||||
|
||||
/// 清除错误信息
|
||||
void clearError() {
|
||||
_setError(null);
|
||||
}
|
||||
|
||||
/// 加载所有测试模板
|
||||
Future<void> loadTestTemplates() async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.getTestTemplates();
|
||||
if (response.success && response.data != null) {
|
||||
state = state.copyWith(templates: response.data!, isLoading: false);
|
||||
} else {
|
||||
_setError(response.message);
|
||||
_setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('加载测试模板失败: ${e.toString()}');
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载指定类型的测试模板
|
||||
Future<void> loadTestTemplatesByType(TestType type) async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.getTestTemplatesByType(type);
|
||||
if (response.success && response.data != null) {
|
||||
state = state.copyWith(templates: response.data!, isLoading: false);
|
||||
} else {
|
||||
_setError(response.message);
|
||||
_setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('加载测试模板失败: ${e.toString()}');
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载最近的测试结果
|
||||
Future<void> loadRecentResults({String? userId}) async {
|
||||
if (userId == null) {
|
||||
_setError('用户ID不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.getRecentTestResults(userId);
|
||||
if (response.success && response.data != null) {
|
||||
state = state.copyWith(testResults: response.data!, isLoading: false);
|
||||
} else {
|
||||
_setError(response.message);
|
||||
_setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('加载测试结果失败: ${e.toString()}');
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建测试会话
|
||||
Future<bool> createTestSession({
|
||||
required String templateId,
|
||||
required String userId,
|
||||
}) async {
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.createTestSession(
|
||||
templateId: templateId,
|
||||
userId: userId,
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
state = state.copyWith(
|
||||
currentSession: response.data!,
|
||||
timeRemaining: response.data!.timeRemaining,
|
||||
isLoading: false,
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
_setLoading(false);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('创建测试会话失败: ${e.toString()}');
|
||||
_setLoading(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始测试
|
||||
Future<bool> startTest() async {
|
||||
if (state.currentSession == null) {
|
||||
_setError('没有活动的测试会话');
|
||||
return false;
|
||||
}
|
||||
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.startTest(state.currentSession!.id);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
state = state.copyWith(
|
||||
currentSession: response.data!,
|
||||
isTimerRunning: true,
|
||||
isLoading: false,
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
_setLoading(false);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('开始测试失败: ${e.toString()}');
|
||||
_setLoading(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置当前测试会话
|
||||
void setCurrentSession(TestSession session) {
|
||||
state = state.copyWith(
|
||||
currentSession: session,
|
||||
timeRemaining: session.timeRemaining,
|
||||
isTimerRunning: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// 设置当前测试结果
|
||||
void setCurrentResult(TestResult result) {
|
||||
state = state.copyWith(
|
||||
currentResult: result,
|
||||
isLoading: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 记录答案(本地记录,不提交到服务器)
|
||||
void recordAnswer(UserAnswer answer) {
|
||||
if (state.currentSession == null) {
|
||||
_setError('没有活动的测试会话');
|
||||
return;
|
||||
}
|
||||
|
||||
final currentSession = state.currentSession!;
|
||||
final updatedAnswers = List<UserAnswer>.from(currentSession.answers);
|
||||
|
||||
// 移除该题目的旧答案
|
||||
updatedAnswers.removeWhere((a) => a.questionId == answer.questionId);
|
||||
// 添加新答案
|
||||
updatedAnswers.add(answer);
|
||||
|
||||
final updatedSession = currentSession.copyWith(answers: updatedAnswers);
|
||||
state = state.copyWith(currentSession: updatedSession);
|
||||
}
|
||||
|
||||
/// 提交答案
|
||||
Future<bool> submitAnswer(UserAnswer answer) async {
|
||||
if (state.currentSession == null) {
|
||||
_setError('没有活动的测试会话');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _testService.submitAnswer(
|
||||
sessionId: state.currentSession!.id,
|
||||
answer: answer,
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
state = state.copyWith(currentSession: response.data!);
|
||||
return true;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('提交答案失败: ${e.toString()}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 完成测试
|
||||
Future<TestResult?> completeTest() async {
|
||||
if (state.currentSession == null) {
|
||||
_setError('没有活动的测试会话');
|
||||
return null;
|
||||
}
|
||||
|
||||
_setLoading(true);
|
||||
_setError(null);
|
||||
|
||||
try {
|
||||
final response = await _testService.completeTest(
|
||||
sessionId: state.currentSession!.id,
|
||||
answers: state.currentSession!.answers,
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
state = state.copyWith(
|
||||
currentResult: response.data!,
|
||||
currentSession: null,
|
||||
isTimerRunning: false,
|
||||
isLoading: false,
|
||||
);
|
||||
return response.data!;
|
||||
} else {
|
||||
_setError(response.message);
|
||||
_setLoading(false);
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('完成测试失败: ${e.toString()}');
|
||||
_setLoading(false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 下一题
|
||||
void nextQuestion() {
|
||||
if (state.currentSession == null) {
|
||||
_setError('没有活动的测试会话');
|
||||
return;
|
||||
}
|
||||
|
||||
final currentSession = state.currentSession!;
|
||||
if (currentSession.currentQuestionIndex < currentSession.questions.length - 1) {
|
||||
final updatedSession = TestSession(
|
||||
id: currentSession.id,
|
||||
userId: currentSession.userId,
|
||||
templateId: currentSession.templateId,
|
||||
questions: currentSession.questions,
|
||||
answers: currentSession.answers,
|
||||
startTime: currentSession.startTime,
|
||||
endTime: currentSession.endTime,
|
||||
status: currentSession.status,
|
||||
timeRemaining: currentSession.timeRemaining,
|
||||
currentQuestionIndex: currentSession.currentQuestionIndex + 1,
|
||||
);
|
||||
|
||||
state = state.copyWith(currentSession: updatedSession);
|
||||
}
|
||||
}
|
||||
|
||||
/// 上一题
|
||||
void previousQuestion() {
|
||||
if (state.currentSession == null) {
|
||||
_setError('没有活动的测试会话');
|
||||
return;
|
||||
}
|
||||
|
||||
final currentSession = state.currentSession!;
|
||||
if (currentSession.currentQuestionIndex > 0) {
|
||||
final updatedSession = TestSession(
|
||||
id: currentSession.id,
|
||||
userId: currentSession.userId,
|
||||
templateId: currentSession.templateId,
|
||||
questions: currentSession.questions,
|
||||
answers: currentSession.answers,
|
||||
startTime: currentSession.startTime,
|
||||
endTime: currentSession.endTime,
|
||||
status: currentSession.status,
|
||||
timeRemaining: currentSession.timeRemaining,
|
||||
currentQuestionIndex: currentSession.currentQuestionIndex - 1,
|
||||
);
|
||||
|
||||
state = state.copyWith(currentSession: updatedSession);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取测试类型的中文名称
|
||||
String getTestTypeName(TestType type) {
|
||||
switch (type) {
|
||||
case TestType.quick:
|
||||
return '快速测试';
|
||||
case TestType.standard:
|
||||
return '标准测试';
|
||||
case TestType.full:
|
||||
return '完整测试';
|
||||
case TestType.mock:
|
||||
return '模拟考试';
|
||||
case TestType.vocabulary:
|
||||
return '词汇专项';
|
||||
case TestType.grammar:
|
||||
return '语法专项';
|
||||
case TestType.reading:
|
||||
return '阅读专项';
|
||||
case TestType.listening:
|
||||
return '听力专项';
|
||||
case TestType.speaking:
|
||||
return '口语专项';
|
||||
case TestType.writing:
|
||||
return '写作专项';
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取技能类型的中文名称
|
||||
String getSkillTypeName(SkillType skill) {
|
||||
switch (skill) {
|
||||
case SkillType.vocabulary:
|
||||
return '词汇';
|
||||
case SkillType.grammar:
|
||||
return '语法';
|
||||
case SkillType.reading:
|
||||
return '阅读';
|
||||
case SkillType.listening:
|
||||
return '听力';
|
||||
case SkillType.speaking:
|
||||
return '口语';
|
||||
case SkillType.writing:
|
||||
return '写作';
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取难度等级的中文名称
|
||||
String getDifficultyLevelName(DifficultyLevel level) {
|
||||
switch (level) {
|
||||
case DifficultyLevel.beginner:
|
||||
return '初级';
|
||||
case DifficultyLevel.elementary:
|
||||
return '基础';
|
||||
case DifficultyLevel.intermediate:
|
||||
return '中级';
|
||||
case DifficultyLevel.upperIntermediate:
|
||||
return '中高级';
|
||||
case DifficultyLevel.advanced:
|
||||
return '高级';
|
||||
case DifficultyLevel.expert:
|
||||
return '专家级';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试状态提供者
|
||||
final testProvider = StateNotifierProvider<TestNotifier, TestState>((ref) {
|
||||
final testService = ref.watch(testServiceProvider);
|
||||
return TestNotifier(testService);
|
||||
});
|
||||
|
||||
/// 当前测试会话提供者
|
||||
final currentTestSessionProvider = Provider<TestSession?>((ref) {
|
||||
return ref.watch(testProvider).currentSession;
|
||||
});
|
||||
|
||||
/// 当前题目提供者
|
||||
final currentQuestionProvider = Provider<TestQuestion?>((ref) {
|
||||
return ref.watch(testProvider).currentQuestion;
|
||||
});
|
||||
|
||||
/// 测试模板提供者
|
||||
final testTemplatesProvider = Provider<List<TestTemplate>>((ref) {
|
||||
return ref.watch(testProvider).templates;
|
||||
});
|
||||
|
||||
/// 测试结果提供者
|
||||
final testResultsProvider = Provider<List<TestResult>>((ref) {
|
||||
return ref.watch(testProvider).testResults;
|
||||
});
|
||||
@@ -0,0 +1,726 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../models/test_models.dart';
|
||||
import '../providers/test_riverpod_provider.dart';
|
||||
import '../screens/test_execution_screen.dart';
|
||||
import '../../auth/providers/auth_provider.dart' as auth;
|
||||
|
||||
/// 综合测试页面
|
||||
class ComprehensiveTestScreen extends ConsumerStatefulWidget {
|
||||
const ComprehensiveTestScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ComprehensiveTestScreen> createState() => _ComprehensiveTestScreenState();
|
||||
}
|
||||
|
||||
class _ComprehensiveTestScreenState extends ConsumerState<ComprehensiveTestScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 加载测试模板和最近结果
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(testProvider.notifier).loadTestTemplates();
|
||||
_loadRecentResults();
|
||||
});
|
||||
}
|
||||
|
||||
void _loadRecentResults() async {
|
||||
// 尝试获取认证状态,但不强制要求登录
|
||||
try {
|
||||
final authState = ref.read(auth.authProvider);
|
||||
final userId = authState.user?.id;
|
||||
if (userId != null) {
|
||||
ref.read(testProvider.notifier).loadRecentResults(userId: userId);
|
||||
} else {
|
||||
// 用户未登录时,显示模拟数据或空状态
|
||||
print('用户未登录,显示静态内容');
|
||||
}
|
||||
} catch (e) {
|
||||
// 认证服务出错时,继续显示静态内容
|
||||
print('认证服务错误: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final testState = ref.watch(testProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'综合测试',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
backgroundColor: const Color(0xFF2196F3),
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: _buildBody(testState),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(TestState testState) {
|
||||
// 如果正在加载,显示加载指示器
|
||||
if (testState.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// 如果有错误,显示错误信息
|
||||
if (testState.errorMessage != null) {
|
||||
return _buildErrorView(testState.errorMessage!);
|
||||
}
|
||||
|
||||
// 显示正常内容(静态展示,不强制登录)
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWelcomeSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildTestTypes(testState),
|
||||
const SizedBox(height: 32),
|
||||
_buildRecentResults(testState),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorView(String errorMessage) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.red[300],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
errorMessage,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.red[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.read(testProvider.notifier).loadTestTemplates();
|
||||
_loadRecentResults();
|
||||
},
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcomeSection() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.teal.withOpacity(0.1),
|
||||
Colors.teal.withOpacity(0.05),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.teal.withOpacity(0.2)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.teal.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.quiz,
|
||||
color: Colors.teal,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'全面能力评估',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'测试您的英语综合能力水平',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTestTypes(TestState testState) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'测试类型',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (testState.templates.isEmpty)
|
||||
const Center(
|
||||
child: Text(
|
||||
'暂无可用的测试模板',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 1.2,
|
||||
),
|
||||
itemCount: testState.templates.length,
|
||||
itemBuilder: (context, index) {
|
||||
final template = testState.templates[index];
|
||||
return _buildTestCard(
|
||||
template: template,
|
||||
onTap: () => _startTest(template),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTestCard({
|
||||
required TestTemplate template,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final color = _getColorForTestType(template.type);
|
||||
final icon = _getIconForTestType(template.type);
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
color.withOpacity(0.1),
|
||||
color.withOpacity(0.05),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 24,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
template.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${template.duration}分钟',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (template.totalQuestions > 0)
|
||||
Text(
|
||||
'${template.totalQuestions}题',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecentResults(TestState testState) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'最近测试结果',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (testState.testResults.isEmpty)
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.quiz_outlined,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无测试记录',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'完成第一次测试后,结果将显示在这里',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: testState.testResults
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
final result = entry.value;
|
||||
return Column(
|
||||
children: [
|
||||
if (index > 0) const Divider(),
|
||||
_buildResultItem(result: result),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultItem({
|
||||
required TestResult result,
|
||||
}) {
|
||||
final template = ref.read(testProvider)
|
||||
.templates
|
||||
.firstWhere(
|
||||
(t) => t.id == result.testId,
|
||||
orElse: () => TestTemplate(
|
||||
id: result.testId,
|
||||
name: '未知测试',
|
||||
description: '',
|
||||
type: TestType.standard,
|
||||
duration: 0,
|
||||
totalQuestions: 0,
|
||||
skillDistribution: {},
|
||||
difficultyDistribution: {},
|
||||
questionIds: [],
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => _showResultDetails(result),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.teal.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.quiz,
|
||||
color: Colors.teal,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
template.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatDate(result.endTime),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${result.totalScore.toStringAsFixed(0)}分',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getScoreColor(result.totalScore.toDouble()),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_getScoreLevel(result.totalScore.toDouble()),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.grey[400],
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _startTest(TestTemplate template) {
|
||||
// 显示测试开始确认对话框
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('开始测试'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('确定要开始「${template.name}」吗?'),
|
||||
const SizedBox(height: 16),
|
||||
if (template.description.isNotEmpty) ...[
|
||||
Text(
|
||||
template.description,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.timer, size: 16, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${template.duration}分钟',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Icon(Icons.quiz, size: 16, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${template.totalQuestions}题',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_navigateToTest(template);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.teal,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('开始'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToTest(TestTemplate template) {
|
||||
// 导航到测试执行页面
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TestExecutionScreen(template: template),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showResultDetails(TestResult result) {
|
||||
// 显示测试结果详情
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('测试结果详情'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('总分:${result.totalScore.toStringAsFixed(1)}'),
|
||||
const SizedBox(height: 8),
|
||||
Text('完成时间:${_formatDate(result.endTime)}'),
|
||||
const SizedBox(height: 8),
|
||||
Text('用时:${result.duration}秒'),
|
||||
if (result.skillScores.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'各项技能得分:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...result.skillScores.map(
|
||||
(skillScore) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(_getSkillTypeName(skillScore.skillType)),
|
||||
Text('${skillScore.score}/${skillScore.maxScore}分'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Color _getColorForTestType(TestType type) {
|
||||
switch (type) {
|
||||
case TestType.quick:
|
||||
return Colors.blue;
|
||||
case TestType.standard:
|
||||
return Colors.green;
|
||||
case TestType.full:
|
||||
return Colors.orange;
|
||||
case TestType.mock:
|
||||
return Colors.purple;
|
||||
case TestType.vocabulary:
|
||||
return Colors.teal;
|
||||
case TestType.grammar:
|
||||
return Colors.indigo;
|
||||
case TestType.reading:
|
||||
return Colors.brown;
|
||||
case TestType.listening:
|
||||
return Colors.cyan;
|
||||
case TestType.speaking:
|
||||
return Colors.pink;
|
||||
case TestType.writing:
|
||||
return Colors.amber;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getIconForTestType(TestType type) {
|
||||
switch (type) {
|
||||
case TestType.quick:
|
||||
return Icons.flash_on;
|
||||
case TestType.standard:
|
||||
return Icons.school;
|
||||
case TestType.full:
|
||||
return Icons.quiz;
|
||||
case TestType.mock:
|
||||
return Icons.assignment;
|
||||
case TestType.vocabulary:
|
||||
return Icons.book;
|
||||
case TestType.grammar:
|
||||
return Icons.text_fields;
|
||||
case TestType.reading:
|
||||
return Icons.menu_book;
|
||||
case TestType.listening:
|
||||
return Icons.headphones;
|
||||
case TestType.speaking:
|
||||
return Icons.mic;
|
||||
case TestType.writing:
|
||||
return Icons.edit;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
Color _getScoreColor(double score) {
|
||||
if (score >= 90) return Colors.green;
|
||||
if (score >= 80) return Colors.blue;
|
||||
if (score >= 70) return Colors.orange;
|
||||
return Colors.red;
|
||||
}
|
||||
|
||||
String _getScoreLevel(double score) {
|
||||
if (score >= 90) return '优秀';
|
||||
if (score >= 80) return '良好';
|
||||
if (score >= 70) return '中等';
|
||||
if (score >= 60) return '及格';
|
||||
return '不及格';
|
||||
}
|
||||
|
||||
String _getSkillName(LanguageSkill skill) {
|
||||
switch (skill) {
|
||||
case LanguageSkill.listening:
|
||||
return '听力';
|
||||
case LanguageSkill.reading:
|
||||
return '阅读';
|
||||
case LanguageSkill.speaking:
|
||||
return '口语';
|
||||
case LanguageSkill.writing:
|
||||
return '写作';
|
||||
case LanguageSkill.vocabulary:
|
||||
return '词汇';
|
||||
case LanguageSkill.grammar:
|
||||
return '语法';
|
||||
case LanguageSkill.pronunciation:
|
||||
return '发音';
|
||||
case LanguageSkill.comprehension:
|
||||
return '理解';
|
||||
}
|
||||
}
|
||||
|
||||
String _getSkillTypeName(SkillType skillType) {
|
||||
switch (skillType) {
|
||||
case SkillType.vocabulary:
|
||||
return '词汇';
|
||||
case SkillType.grammar:
|
||||
return '语法';
|
||||
case SkillType.listening:
|
||||
return '听力';
|
||||
case SkillType.reading:
|
||||
return '阅读';
|
||||
case SkillType.speaking:
|
||||
return '口语';
|
||||
case SkillType.writing:
|
||||
return '写作';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,698 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'dart:async';
|
||||
import '../models/test_models.dart';
|
||||
import '../providers/test_riverpod_provider.dart';
|
||||
import '../data/test_static_data.dart';
|
||||
import '../widgets/question_widgets.dart';
|
||||
import 'test_result_screen.dart';
|
||||
|
||||
/// 测试执行页面
|
||||
class TestExecutionScreen extends ConsumerStatefulWidget {
|
||||
final TestTemplate template;
|
||||
|
||||
const TestExecutionScreen({
|
||||
super.key,
|
||||
required this.template,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<TestExecutionScreen> createState() => _TestExecutionScreenState();
|
||||
}
|
||||
|
||||
class _TestExecutionScreenState extends ConsumerState<TestExecutionScreen> {
|
||||
Timer? _timer;
|
||||
int _timeRemaining = 0;
|
||||
bool _isTestStarted = false;
|
||||
bool _isTestCompleted = false;
|
||||
Map<String, UserAnswer> _userAnswers = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_timeRemaining = widget.template.duration * 60; // 转换为秒
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_startTest();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startTest() async {
|
||||
setState(() {
|
||||
_isTestStarted = true;
|
||||
});
|
||||
|
||||
// 创建测试会话(使用静态数据)
|
||||
final questions = TestStaticData.getAllQuestions().take(10).toList(); // 取前10题作为测试
|
||||
final session = TestSession(
|
||||
id: 'test_session_${DateTime.now().millisecondsSinceEpoch}',
|
||||
templateId: widget.template.id,
|
||||
userId: 'current_user',
|
||||
status: TestStatus.inProgress,
|
||||
questions: questions,
|
||||
answers: [],
|
||||
currentQuestionIndex: 0,
|
||||
startTime: DateTime.now(),
|
||||
timeRemaining: _timeRemaining,
|
||||
metadata: {},
|
||||
);
|
||||
|
||||
// 更新provider状态
|
||||
ref.read(testProvider.notifier).setCurrentSession(session);
|
||||
|
||||
// 启动计时器
|
||||
_startTimer();
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
setState(() {
|
||||
if (_timeRemaining > 0) {
|
||||
_timeRemaining--;
|
||||
} else {
|
||||
_submitTest();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _submitTest() async {
|
||||
if (_isTestCompleted) return;
|
||||
|
||||
_timer?.cancel();
|
||||
setState(() {
|
||||
_isTestCompleted = true;
|
||||
});
|
||||
|
||||
// 获取当前会话和答案
|
||||
final currentSession = ref.read(testProvider).currentSession;
|
||||
if (currentSession == null) return;
|
||||
|
||||
// 创建测试结果
|
||||
final testResult = _createTestResult(currentSession);
|
||||
|
||||
// 更新provider状态
|
||||
ref.read(testProvider.notifier).setCurrentResult(testResult);
|
||||
|
||||
// 导航到结果页面
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TestResultScreen(
|
||||
testResult: testResult,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TestResult _createTestResult(TestSession session) {
|
||||
final answers = session.answers;
|
||||
int totalScore = 0;
|
||||
int maxScore = 0;
|
||||
Map<SkillType, List<int>> skillScores = {};
|
||||
|
||||
// 计算得分
|
||||
for (final question in session.questions) {
|
||||
maxScore += question.points;
|
||||
final userAnswer = answers.where((a) => a.questionId == question.id).firstOrNull;
|
||||
|
||||
if (userAnswer != null) {
|
||||
// 简单的评分逻辑
|
||||
bool isCorrect = false;
|
||||
if (question.type == QuestionType.multipleChoice) {
|
||||
isCorrect = userAnswer.selectedAnswers.isNotEmpty &&
|
||||
question.correctAnswers.contains(userAnswer.selectedAnswers.first);
|
||||
} else if (question.type == QuestionType.fillInBlank) {
|
||||
isCorrect = userAnswer.textAnswer?.toLowerCase().trim() != null &&
|
||||
question.correctAnswers.any((correct) =>
|
||||
correct.toLowerCase().trim() == userAnswer.textAnswer!.toLowerCase().trim());
|
||||
}
|
||||
|
||||
if (isCorrect) {
|
||||
totalScore += question.points;
|
||||
}
|
||||
|
||||
// 记录技能得分
|
||||
final skill = question.skillType;
|
||||
if (!skillScores.containsKey(skill)) {
|
||||
skillScores[skill] = [];
|
||||
}
|
||||
skillScores[skill]!.add(isCorrect ? question.points : 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算技能得分
|
||||
final skillScoresList = skillScores.entries.map((entry) {
|
||||
final skill = entry.key;
|
||||
final scores = entry.value;
|
||||
final skillTotal = scores.reduce((a, b) => a + b);
|
||||
final skillMax = scores.length * 10; // 假设每题10分
|
||||
final percentage = skillMax > 0 ? (skillTotal / skillMax * 100) : 0.0;
|
||||
|
||||
return SkillScore(
|
||||
skillType: skill,
|
||||
score: skillTotal,
|
||||
maxScore: skillMax,
|
||||
percentage: percentage,
|
||||
level: _getSkillLevel(percentage),
|
||||
feedback: _getSkillFeedback(skill, percentage),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final overallPercentage = maxScore > 0 ? (totalScore / maxScore * 100) : 0.0;
|
||||
|
||||
return TestResult(
|
||||
id: 'result_${DateTime.now().millisecondsSinceEpoch}',
|
||||
testId: session.id,
|
||||
userId: session.userId,
|
||||
testType: TestType.standard,
|
||||
totalScore: totalScore,
|
||||
maxScore: maxScore,
|
||||
percentage: overallPercentage,
|
||||
overallLevel: _getSkillLevel(overallPercentage),
|
||||
skillScores: skillScoresList,
|
||||
answers: answers,
|
||||
startTime: session.startTime!,
|
||||
endTime: DateTime.now(),
|
||||
duration: DateTime.now().difference(session.startTime!).inSeconds,
|
||||
feedback: _getOverallFeedback(totalScore, maxScore),
|
||||
recommendations: _getRecommendations(skillScoresList),
|
||||
);
|
||||
}
|
||||
|
||||
LanguageSkill _convertSkillType(SkillType skillType) {
|
||||
switch (skillType) {
|
||||
case SkillType.vocabulary:
|
||||
return LanguageSkill.vocabulary;
|
||||
case SkillType.grammar:
|
||||
return LanguageSkill.grammar;
|
||||
case SkillType.reading:
|
||||
return LanguageSkill.reading;
|
||||
case SkillType.listening:
|
||||
return LanguageSkill.listening;
|
||||
case SkillType.speaking:
|
||||
return LanguageSkill.speaking;
|
||||
case SkillType.writing:
|
||||
return LanguageSkill.writing;
|
||||
}
|
||||
}
|
||||
|
||||
DifficultyLevel _getSkillLevel(double percentage) {
|
||||
if (percentage >= 90) return DifficultyLevel.expert;
|
||||
if (percentage >= 80) return DifficultyLevel.advanced;
|
||||
if (percentage >= 70) return DifficultyLevel.upperIntermediate;
|
||||
if (percentage >= 60) return DifficultyLevel.intermediate;
|
||||
if (percentage >= 50) return DifficultyLevel.elementary;
|
||||
return DifficultyLevel.beginner;
|
||||
}
|
||||
|
||||
String _getSkillTypeName(SkillType skillType) {
|
||||
switch (skillType) {
|
||||
case SkillType.vocabulary:
|
||||
return '词汇';
|
||||
case SkillType.grammar:
|
||||
return '语法';
|
||||
case SkillType.reading:
|
||||
return '阅读';
|
||||
case SkillType.listening:
|
||||
return '听力';
|
||||
case SkillType.speaking:
|
||||
return '口语';
|
||||
case SkillType.writing:
|
||||
return '写作';
|
||||
}
|
||||
}
|
||||
|
||||
String _getSkillFeedback(SkillType skillType, double percentage) {
|
||||
final skillName = _getSkillTypeName(skillType);
|
||||
if (percentage >= 80) {
|
||||
return '$skillName 掌握得很好,继续保持!';
|
||||
} else if (percentage >= 60) {
|
||||
return '$skillName 基础不错,还有提升空间。';
|
||||
} else {
|
||||
return '$skillName 需要加强练习。';
|
||||
}
|
||||
}
|
||||
|
||||
String _getSkillName(SkillType skillType) {
|
||||
switch (skillType) {
|
||||
case SkillType.vocabulary:
|
||||
return '词汇';
|
||||
case SkillType.grammar:
|
||||
return '语法';
|
||||
case SkillType.reading:
|
||||
return '阅读';
|
||||
case SkillType.listening:
|
||||
return '听力';
|
||||
case SkillType.speaking:
|
||||
return '口语';
|
||||
case SkillType.writing:
|
||||
return '写作';
|
||||
}
|
||||
}
|
||||
|
||||
String _getOverallFeedback(int totalScore, int maxScore) {
|
||||
final percentage = (totalScore / maxScore * 100).round();
|
||||
if (percentage >= 90) {
|
||||
return '恭喜!您的表现非常出色,各项技能都达到了很高的水平。';
|
||||
} else if (percentage >= 80) {
|
||||
return '您的表现很好!大部分技能都掌握得不错,但还有一些提升空间。';
|
||||
} else if (percentage >= 70) {
|
||||
return '您的基础还不错,但需要在某些技能上加强练习。';
|
||||
} else if (percentage >= 60) {
|
||||
return '您已经掌握了一些基础知识,但还需要更多的练习和学习。';
|
||||
} else {
|
||||
return '建议您从基础开始系统性地学习,多做练习。';
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getRecommendations(List<SkillScore> skillScores) {
|
||||
final weakSkills = skillScores
|
||||
.where((skill) => skill.percentage < 70)
|
||||
.map((skill) => _getSkillTypeName(skill.skillType))
|
||||
.toList();
|
||||
|
||||
List<String> recommendations = [];
|
||||
|
||||
if (weakSkills.isNotEmpty) {
|
||||
recommendations.add('重点加强${weakSkills.join('、')}方面的练习');
|
||||
}
|
||||
|
||||
recommendations.addAll([
|
||||
'每天坚持30分钟的英语学习',
|
||||
'多做相关类型的练习题',
|
||||
'建议参加更多的模拟测试',
|
||||
]);
|
||||
|
||||
return {
|
||||
'general': recommendations,
|
||||
'weakSkills': weakSkills,
|
||||
};
|
||||
}
|
||||
|
||||
void _onAnswerChanged(String questionId, UserAnswer answer) {
|
||||
setState(() {
|
||||
_userAnswers[questionId] = answer;
|
||||
});
|
||||
}
|
||||
|
||||
void _nextQuestion() {
|
||||
ref.read(testProvider.notifier).nextQuestion();
|
||||
}
|
||||
|
||||
void _previousQuestion() {
|
||||
ref.read(testProvider.notifier).previousQuestion();
|
||||
}
|
||||
|
||||
String _formatTime(int seconds) {
|
||||
final minutes = seconds ~/ 60;
|
||||
final remainingSeconds = seconds % 60;
|
||||
return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final testState = ref.watch(testProvider);
|
||||
final currentQuestion = testState.currentQuestion;
|
||||
final currentSession = testState.currentSession;
|
||||
|
||||
if (!_isTestStarted || currentSession == null) {
|
||||
return _buildLoadingScreen();
|
||||
}
|
||||
|
||||
if (currentQuestion == null) {
|
||||
return _buildCompletionScreen();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: Text(widget.template.name),
|
||||
backgroundColor: const Color(0xFF2196F3),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: _timeRemaining < 300 ? Colors.red : Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timer,
|
||||
size: 16,
|
||||
color: _timeRemaining < 300 ? Colors.white : Colors.white,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_formatTime(_timeRemaining),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _timeRemaining < 300 ? Colors.white : Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildProgressBar(currentSession),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildQuestionHeader(currentQuestion, currentSession),
|
||||
const SizedBox(height: 24),
|
||||
_buildQuestionContent(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildNavigationBar(currentSession),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingScreen() {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
body: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'正在准备测试...',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompletionScreen() {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
size: 64,
|
||||
color: Colors.green,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'测试已完成!',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'正在计算结果...',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _submitTest,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||
),
|
||||
child: const Text('查看结果'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressBar(TestSession session) {
|
||||
final progress = (session.currentQuestionIndex + 1) / session.questions.length;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'题目 ${session.currentQuestionIndex + 1} / ${session.questions.length}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(progress * 100).toInt()}% 完成',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
value: progress,
|
||||
backgroundColor: Colors.grey[200],
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF2196F3)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuestionHeader(TestQuestion question, TestSession session) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getSkillColor(question.skillType).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_getSkillName(question.skillType),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _getSkillColor(question.skillType),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getDifficultyColor(question.difficulty).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_getDifficultyName(question.difficulty),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _getDifficultyColor(question.difficulty),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.star,
|
||||
size: 16,
|
||||
color: Colors.amber[600],
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${question.points} 分',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuestionContent() {
|
||||
final testState = ref.watch(testProvider);
|
||||
final currentQuestion = testState.currentQuestion;
|
||||
|
||||
if (currentQuestion == null) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'没有找到题目',
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final userAnswer = testState.currentSession?.answers
|
||||
.where((answer) => answer.questionId == currentQuestion.id)
|
||||
.firstOrNull;
|
||||
|
||||
return QuestionWidget(
|
||||
question: currentQuestion,
|
||||
userAnswer: userAnswer,
|
||||
onAnswerChanged: (answer) {
|
||||
ref.read(testProvider.notifier).recordAnswer(answer);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavigationBar(TestSession session) {
|
||||
final canGoPrevious = session.currentQuestionIndex > 0;
|
||||
final canGoNext = session.currentQuestionIndex < session.questions.length - 1;
|
||||
final isLastQuestion = session.currentQuestionIndex == session.questions.length - 1;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: canGoPrevious ? _previousQuestion : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[300],
|
||||
foregroundColor: Colors.grey[700],
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: const Text('上一题'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLastQuestion ? _submitTest : (canGoNext ? _nextQuestion : null),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isLastQuestion ? Colors.green : const Color(0xFF2196F3),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text(isLastQuestion ? '提交测试' : '下一题'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getSkillColor(SkillType skillType) {
|
||||
switch (skillType) {
|
||||
case SkillType.vocabulary:
|
||||
return Colors.blue;
|
||||
case SkillType.grammar:
|
||||
return Colors.green;
|
||||
case SkillType.reading:
|
||||
return Colors.orange;
|
||||
case SkillType.listening:
|
||||
return Colors.purple;
|
||||
case SkillType.speaking:
|
||||
return Colors.red;
|
||||
case SkillType.writing:
|
||||
return Colors.teal;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getDifficultyColor(DifficultyLevel difficulty) {
|
||||
switch (difficulty) {
|
||||
case DifficultyLevel.beginner:
|
||||
return Colors.green;
|
||||
case DifficultyLevel.elementary:
|
||||
return Colors.lightGreen;
|
||||
case DifficultyLevel.intermediate:
|
||||
return Colors.orange;
|
||||
case DifficultyLevel.upperIntermediate:
|
||||
return Colors.deepOrange;
|
||||
case DifficultyLevel.advanced:
|
||||
return Colors.red;
|
||||
case DifficultyLevel.expert:
|
||||
return Colors.purple;
|
||||
}
|
||||
}
|
||||
|
||||
String _getDifficultyName(DifficultyLevel difficulty) {
|
||||
switch (difficulty) {
|
||||
case DifficultyLevel.beginner:
|
||||
return '初级';
|
||||
case DifficultyLevel.elementary:
|
||||
return '基础';
|
||||
case DifficultyLevel.intermediate:
|
||||
return '中级';
|
||||
case DifficultyLevel.upperIntermediate:
|
||||
return '中高级';
|
||||
case DifficultyLevel.advanced:
|
||||
return '高级';
|
||||
case DifficultyLevel.expert:
|
||||
return '专家级';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../models/test_models.dart';
|
||||
import '../providers/test_riverpod_provider.dart';
|
||||
|
||||
/// 测试结果页面
|
||||
class TestResultScreen extends ConsumerWidget {
|
||||
final TestResult testResult;
|
||||
|
||||
const TestResultScreen({
|
||||
super.key,
|
||||
required this.testResult,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: const Text('测试结果'),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black87,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _shareResult(context),
|
||||
icon: const Icon(Icons.share),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildOverallScore(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSkillBreakdown(),
|
||||
const SizedBox(height: 24),
|
||||
_buildPerformanceAnalysis(),
|
||||
const SizedBox(height: 24),
|
||||
_buildRecommendations(),
|
||||
const SizedBox(height: 24),
|
||||
_buildActionButtons(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverallScore() {
|
||||
final percentage = (testResult.totalScore / testResult.maxScore * 100).round();
|
||||
final level = _getScoreLevel(percentage);
|
||||
final color = _getScoreColor(percentage);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.emoji_events,
|
||||
size: 32,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'总体成绩',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
'$percentage',
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'%',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
level,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildScoreDetail('得分', '${testResult.totalScore}'),
|
||||
_buildScoreDetail('满分', '${testResult.maxScore}'),
|
||||
_buildScoreDetail('用时', _formatDuration(testResult.duration)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScoreDetail(String label, String value) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSkillBreakdown() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.analytics,
|
||||
size: 24,
|
||||
color: Colors.blue,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'技能分析',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
...testResult.skillScores.map((skillScore) => _buildSkillItem(skillScore)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSkillItem(SkillScore skillScore) {
|
||||
final color = _getSkillColor(skillScore.skillType);
|
||||
final percentage = skillScore.percentage;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_getSkillName(skillScore.skillType),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${percentage.round()}%',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
value: percentage / 100,
|
||||
backgroundColor: Colors.grey[200],
|
||||
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_getDifficultyName(skillScore.level),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPerformanceAnalysis() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.insights,
|
||||
size: 24,
|
||||
color: Colors.green,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'表现分析',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (testResult.feedback != null) ...[
|
||||
Text(
|
||||
testResult.feedback!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
_buildDefaultAnalysis(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDefaultAnalysis() {
|
||||
final percentage = (testResult.totalScore / testResult.maxScore * 100).round();
|
||||
String analysis;
|
||||
|
||||
if (percentage >= 90) {
|
||||
analysis = '恭喜!您的表现非常出色,各项技能都达到了很高的水平。继续保持这种学习状态,可以尝试更高难度的挑战。';
|
||||
} else if (percentage >= 80) {
|
||||
analysis = '您的表现很好!大部分技能都掌握得不错,但还有一些提升空间。建议重点关注得分较低的技能领域。';
|
||||
} else if (percentage >= 70) {
|
||||
analysis = '您的基础还不错,但需要在某些技能上加强练习。建议制定针对性的学习计划,逐步提升薄弱环节。';
|
||||
} else if (percentage >= 60) {
|
||||
analysis = '您已经掌握了一些基础知识,但还需要更多的练习和学习。建议从基础开始,循序渐进地提升各项技能。';
|
||||
} else {
|
||||
analysis = '建议您从基础开始系统性地学习,多做练习,不要着急,学习是一个循序渐进的过程。';
|
||||
}
|
||||
|
||||
return Text(
|
||||
analysis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecommendations() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lightbulb,
|
||||
size: 24,
|
||||
color: Colors.orange,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'学习建议',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (testResult.recommendations != null && testResult.recommendations!.isNotEmpty) ...[
|
||||
...testResult.recommendations!.values.where((v) => v is String).map((recommendation) => _buildRecommendationItem(recommendation as String)),
|
||||
] else ...[
|
||||
..._getDefaultRecommendations().map((recommendation) => _buildRecommendationItem(recommendation)),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecommendationItem(String recommendation) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
recommendation,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _getDefaultRecommendations() {
|
||||
final weakSkills = testResult.skillScores
|
||||
.where((skill) => skill.percentage < 70)
|
||||
.map((skill) => _getSkillName(skill.skillType))
|
||||
.toList();
|
||||
|
||||
List<String> recommendations = [];
|
||||
|
||||
if (weakSkills.isNotEmpty) {
|
||||
recommendations.add('重点加强${weakSkills.join('、')}方面的练习');
|
||||
}
|
||||
|
||||
recommendations.addAll([
|
||||
'每天坚持30分钟的英语学习',
|
||||
'多做相关类型的练习题',
|
||||
'建议参加更多的模拟测试',
|
||||
'可以寻求老师或同学的帮助',
|
||||
]);
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _retakeTest(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2196F3),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'重新测试',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _backToHome(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF2196F3),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'返回首页',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _shareResult(BuildContext context) {
|
||||
// 这里应该实现分享功能
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('分享功能正在开发中...'),
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _retakeTest(BuildContext context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
|
||||
void _backToHome(BuildContext context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
|
||||
String _formatDuration(int seconds) {
|
||||
final minutes = seconds ~/ 60;
|
||||
final remainingSeconds = seconds % 60;
|
||||
return '${minutes}分${remainingSeconds}秒';
|
||||
}
|
||||
|
||||
String _getScoreLevel(int percentage) {
|
||||
if (percentage >= 90) return '优秀';
|
||||
if (percentage >= 80) return '良好';
|
||||
if (percentage >= 70) return '中等';
|
||||
if (percentage >= 60) return '及格';
|
||||
return '需要努力';
|
||||
}
|
||||
|
||||
Color _getScoreColor(int percentage) {
|
||||
if (percentage >= 90) return Colors.green;
|
||||
if (percentage >= 80) return Colors.blue;
|
||||
if (percentage >= 70) return Colors.orange;
|
||||
if (percentage >= 60) return Colors.amber;
|
||||
return Colors.red;
|
||||
}
|
||||
|
||||
Color _getSkillColor(SkillType skill) {
|
||||
switch (skill) {
|
||||
case SkillType.vocabulary:
|
||||
return Colors.blue;
|
||||
case SkillType.grammar:
|
||||
return Colors.green;
|
||||
case SkillType.reading:
|
||||
return Colors.orange;
|
||||
case SkillType.listening:
|
||||
return Colors.purple;
|
||||
case SkillType.speaking:
|
||||
return Colors.red;
|
||||
case SkillType.writing:
|
||||
return Colors.teal;
|
||||
}
|
||||
}
|
||||
|
||||
String _getSkillName(SkillType skill) {
|
||||
switch (skill) {
|
||||
case SkillType.vocabulary:
|
||||
return '词汇';
|
||||
case SkillType.grammar:
|
||||
return '语法';
|
||||
case SkillType.reading:
|
||||
return '阅读';
|
||||
case SkillType.listening:
|
||||
return '听力';
|
||||
case SkillType.speaking:
|
||||
return '口语';
|
||||
case SkillType.writing:
|
||||
return '写作';
|
||||
}
|
||||
}
|
||||
|
||||
String _getDifficultyName(DifficultyLevel difficulty) {
|
||||
switch (difficulty) {
|
||||
case DifficultyLevel.beginner:
|
||||
return '初级';
|
||||
case DifficultyLevel.elementary:
|
||||
return '基础';
|
||||
case DifficultyLevel.intermediate:
|
||||
return '中级';
|
||||
case DifficultyLevel.upperIntermediate:
|
||||
return '中高级';
|
||||
case DifficultyLevel.advanced:
|
||||
return '高级';
|
||||
case DifficultyLevel.expert:
|
||||
return '专家级';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
import '../../../core/models/api_response.dart';
|
||||
import '../../../core/services/enhanced_api_service.dart';
|
||||
import '../models/test_models.dart';
|
||||
|
||||
/// 综合测试API服务类
|
||||
/// 使用后端API替代静态数据
|
||||
class TestApiService {
|
||||
static final TestApiService _instance = TestApiService._internal();
|
||||
factory TestApiService() => _instance;
|
||||
TestApiService._internal();
|
||||
|
||||
final EnhancedApiService _enhancedApiService = EnhancedApiService();
|
||||
|
||||
// 缓存时长配置
|
||||
static const Duration _shortCacheDuration = Duration(minutes: 5);
|
||||
static const Duration _longCacheDuration = Duration(hours: 1);
|
||||
|
||||
/// 获取所有测试模板
|
||||
Future<ApiResponse<List<TestTemplate>>> getTestTemplates() async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<TestTemplate>>(
|
||||
'/tests/templates',
|
||||
cacheDuration: _longCacheDuration,
|
||||
fromJson: (data) {
|
||||
final templates = data['templates'] as List?;
|
||||
if (templates == null) return [];
|
||||
return templates.map((json) => TestTemplate.fromJson(json)).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '获取测试模板成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试模板失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据类型获取测试模板
|
||||
Future<ApiResponse<List<TestTemplate>>> getTestTemplatesByType(
|
||||
TestType type,
|
||||
) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<TestTemplate>>(
|
||||
'/tests/templates',
|
||||
queryParameters: {
|
||||
'type': type.toString().split('.').last,
|
||||
},
|
||||
cacheDuration: _longCacheDuration,
|
||||
fromJson: (data) {
|
||||
final templates = data['templates'] as List?;
|
||||
if (templates == null) return [];
|
||||
return templates.map((json) => TestTemplate.fromJson(json)).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '获取测试模板成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试模板失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据ID获取测试模板
|
||||
Future<ApiResponse<TestTemplate>> getTestTemplateById(String id) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<TestTemplate>(
|
||||
'/tests/templates/$id',
|
||||
cacheDuration: _longCacheDuration,
|
||||
fromJson: (data) => TestTemplate.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '获取测试模板成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试模板失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建测试会话
|
||||
Future<ApiResponse<TestSession>> createTestSession({
|
||||
required String templateId,
|
||||
required String userId,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.post<TestSession>(
|
||||
'/tests/sessions',
|
||||
data: {
|
||||
'template_id': templateId,
|
||||
'user_id': userId,
|
||||
},
|
||||
fromJson: (data) => TestSession.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '创建测试会话成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '创建测试会话失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始测试
|
||||
Future<ApiResponse<TestSession>> startTest(String sessionId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.put<TestSession>(
|
||||
'/tests/sessions/$sessionId/start',
|
||||
fromJson: (data) => TestSession.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '开始测试成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '开始测试失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交答案
|
||||
Future<ApiResponse<TestSession>> submitAnswer({
|
||||
required String sessionId,
|
||||
required UserAnswer answer,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.post<TestSession>(
|
||||
'/tests/sessions/$sessionId/answers',
|
||||
data: {
|
||||
'question_id': answer.questionId,
|
||||
'answer': answer.answer,
|
||||
},
|
||||
fromJson: (data) => TestSession.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '提交答案成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '提交答案失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停测试
|
||||
Future<ApiResponse<TestSession>> pauseTest(String sessionId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.put<TestSession>(
|
||||
'/tests/sessions/$sessionId/pause',
|
||||
fromJson: (data) => TestSession.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '测试已暂停');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '暂停测试失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复测试
|
||||
Future<ApiResponse<TestSession>> resumeTest(String sessionId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.put<TestSession>(
|
||||
'/tests/sessions/$sessionId/resume',
|
||||
fromJson: (data) => TestSession.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '测试已恢复');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '恢复测试失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 完成测试
|
||||
Future<ApiResponse<TestResult>> completeTest(String sessionId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.put<TestResult>(
|
||||
'/tests/sessions/$sessionId/complete',
|
||||
fromJson: (data) => TestResult.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '完成测试成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '完成测试失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取测试结果
|
||||
Future<ApiResponse<TestResult>> getTestResult(String sessionId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<TestResult>(
|
||||
'/tests/sessions/$sessionId/result',
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) => TestResult.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '获取测试结果成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试结果失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户测试历史
|
||||
Future<ApiResponse<List<TestSession>>> getUserTestHistory({
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<TestSession>>(
|
||||
'/tests/sessions',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
},
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) {
|
||||
final sessions = data['sessions'] as List?;
|
||||
if (sessions == null) return [];
|
||||
return sessions.map((json) => TestSession.fromJson(json)).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '获取测试历史成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试历史失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户测试统计
|
||||
Future<ApiResponse<Map<String, dynamic>>> getUserTestStats() async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<Map<String, dynamic>>(
|
||||
'/tests/stats',
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) => data,
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '获取测试统计成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取最近的测试结果
|
||||
Future<ApiResponse<List<TestResult>>> getRecentTestResults(
|
||||
String? userId, {
|
||||
int limit = 5,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<TestResult>>(
|
||||
'/tests/sessions/recent',
|
||||
queryParameters: {
|
||||
if (userId != null) 'user_id': userId,
|
||||
'limit': limit,
|
||||
},
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) {
|
||||
final sessions = data['sessions'] as List?;
|
||||
if (sessions == null) return [];
|
||||
return sessions.map((json) => TestResult.fromJson(json)).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '获取最近测试结果成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取最近测试结果失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取测试结果详情
|
||||
Future<ApiResponse<TestResult>> getTestResultById(String resultId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<TestResult>(
|
||||
'/tests/results/$resultId',
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) => TestResult.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '获取测试结果成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试结果失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户技能统计
|
||||
Future<ApiResponse<Map<SkillType, double>>> getUserSkillStatistics(
|
||||
String userId,
|
||||
) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<Map<SkillType, double>>(
|
||||
'/tests/users/$userId/skills',
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) {
|
||||
final skillsData = data['skills'] as Map<String, dynamic>?;
|
||||
if (skillsData == null) return {};
|
||||
|
||||
final result = <SkillType, double>{};
|
||||
skillsData.forEach((key, value) {
|
||||
final skillType = _parseSkillType(key);
|
||||
if (skillType != null) {
|
||||
result[skillType] = (value as num).toDouble();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '获取技能统计成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取技能统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取题目统计
|
||||
Future<ApiResponse<Map<String, dynamic>>> getQuestionStatistics() async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<Map<String, dynamic>>(
|
||||
'/tests/questions/statistics',
|
||||
cacheDuration: _longCacheDuration,
|
||||
fromJson: (data) => data,
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(data: response.data!, message: '获取题目统计成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取题目统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除测试结果
|
||||
Future<ApiResponse<void>> deleteTestResult(String resultId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.delete<void>(
|
||||
'/tests/results/$resultId',
|
||||
fromJson: (data) => null,
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
return ApiResponse.success(message: '删除测试结果成功');
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '删除测试结果失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析技能类型
|
||||
SkillType? _parseSkillType(String key) {
|
||||
switch (key.toLowerCase()) {
|
||||
case 'vocabulary':
|
||||
return SkillType.vocabulary;
|
||||
case 'grammar':
|
||||
return SkillType.grammar;
|
||||
case 'reading':
|
||||
return SkillType.reading;
|
||||
case 'listening':
|
||||
return SkillType.listening;
|
||||
case 'speaking':
|
||||
return SkillType.speaking;
|
||||
case 'writing':
|
||||
return SkillType.writing;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
import '../models/test_models.dart';
|
||||
import '../data/test_static_data.dart';
|
||||
import '../../../core/models/api_response.dart';
|
||||
|
||||
/// 综合测试服务类
|
||||
class TestService {
|
||||
// 单例模式
|
||||
static final TestService _instance = TestService._internal();
|
||||
factory TestService() => _instance;
|
||||
TestService._internal();
|
||||
|
||||
/// 获取所有测试模板
|
||||
Future<ApiResponse<List<TestTemplate>>> getTestTemplates() async {
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final templates = TestStaticData.getAllTemplates();
|
||||
return ApiResponse.success(
|
||||
data: templates,
|
||||
message: '获取测试模板成功',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '获取测试模板失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据类型获取测试模板
|
||||
Future<ApiResponse<List<TestTemplate>>> getTestTemplatesByType(
|
||||
TestType type,
|
||||
) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
final templates = TestStaticData.getTemplatesByType(type);
|
||||
return ApiResponse.success(
|
||||
data: templates,
|
||||
message: '获取${_getTestTypeName(type)}模板成功',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '获取测试模板失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据ID获取测试模板
|
||||
Future<ApiResponse<TestTemplate>> getTestTemplateById(String id) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
final template = TestStaticData.getTemplateById(id);
|
||||
if (template == null) {
|
||||
return ApiResponse.error(message: '测试模板不存在');
|
||||
}
|
||||
|
||||
return ApiResponse.success(
|
||||
data: template,
|
||||
message: '获取测试模板成功',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '获取测试模板失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建测试会话
|
||||
Future<ApiResponse<TestSession>> createTestSession({
|
||||
required String templateId,
|
||||
required String userId,
|
||||
}) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
|
||||
final session = TestStaticData.createTestSession(
|
||||
templateId: templateId,
|
||||
userId: userId,
|
||||
);
|
||||
|
||||
return ApiResponse.success(
|
||||
data: session,
|
||||
message: '创建测试会话成功',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '创建测试会话失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始测试
|
||||
Future<ApiResponse<TestSession>> startTest(String sessionId) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
// 模拟开始测试,实际应用中会更新数据库中的会话状态
|
||||
// 这里返回一个模拟的已开始的会话
|
||||
return ApiResponse.success(
|
||||
data: TestSession(
|
||||
id: sessionId,
|
||||
templateId: 'template_quick',
|
||||
userId: 'user_001',
|
||||
status: TestStatus.inProgress,
|
||||
questions: TestStaticData.getQuestionsByIds(['vocab_001', 'grammar_001']),
|
||||
answers: [],
|
||||
currentQuestionIndex: 0,
|
||||
startTime: DateTime.now(),
|
||||
timeRemaining: 900, // 15分钟
|
||||
),
|
||||
message: '测试已开始',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '开始测试失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交答案
|
||||
Future<ApiResponse<TestSession>> submitAnswer({
|
||||
required String sessionId,
|
||||
required UserAnswer answer,
|
||||
}) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
// 模拟提交答案并返回更新后的会话
|
||||
return ApiResponse.success(
|
||||
data: TestSession(
|
||||
id: sessionId,
|
||||
templateId: 'template_quick',
|
||||
userId: 'user_001',
|
||||
status: TestStatus.inProgress,
|
||||
questions: TestStaticData.getQuestionsByIds(['vocab_001', 'grammar_001']),
|
||||
answers: [answer],
|
||||
currentQuestionIndex: 1,
|
||||
startTime: DateTime.now().subtract(const Duration(minutes: 5)),
|
||||
timeRemaining: 600, // 剩余10分钟
|
||||
),
|
||||
message: '答案提交成功',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '提交答案失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停测试
|
||||
Future<ApiResponse<TestSession>> pauseTest(String sessionId) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
return ApiResponse.success(
|
||||
data: TestSession(
|
||||
id: sessionId,
|
||||
templateId: 'template_quick',
|
||||
userId: 'user_001',
|
||||
status: TestStatus.paused,
|
||||
questions: TestStaticData.getQuestionsByIds(['vocab_001', 'grammar_001']),
|
||||
answers: [],
|
||||
currentQuestionIndex: 0,
|
||||
startTime: DateTime.now().subtract(const Duration(minutes: 5)),
|
||||
timeRemaining: 600,
|
||||
),
|
||||
message: '测试已暂停',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '暂停测试失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复测试
|
||||
Future<ApiResponse<TestSession>> resumeTest(String sessionId) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
return ApiResponse.success(
|
||||
data: TestSession(
|
||||
id: sessionId,
|
||||
templateId: 'template_quick',
|
||||
userId: 'user_001',
|
||||
status: TestStatus.inProgress,
|
||||
questions: TestStaticData.getQuestionsByIds(['vocab_001', 'grammar_001']),
|
||||
answers: [],
|
||||
currentQuestionIndex: 0,
|
||||
startTime: DateTime.now().subtract(const Duration(minutes: 5)),
|
||||
timeRemaining: 600,
|
||||
),
|
||||
message: '测试已恢复',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '恢复测试失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 完成测试
|
||||
Future<ApiResponse<TestResult>> completeTest({
|
||||
required String sessionId,
|
||||
required List<UserAnswer> answers,
|
||||
}) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
|
||||
// 模拟计算测试结果
|
||||
final result = _calculateTestResult(sessionId, answers);
|
||||
|
||||
// 保存结果到静态数据
|
||||
TestStaticData.addTestResult(result);
|
||||
|
||||
return ApiResponse.success(
|
||||
data: result,
|
||||
message: '测试完成,结果已保存',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '完成测试失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户测试历史
|
||||
Future<ApiResponse<List<TestResult>>> getUserTestHistory(
|
||||
String userId, {
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
}) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
|
||||
final allResults = TestStaticData.getUserResults(userId);
|
||||
final startIndex = (page - 1) * limit;
|
||||
final endIndex = startIndex + limit;
|
||||
|
||||
final results = allResults.length > startIndex
|
||||
? allResults.sublist(
|
||||
startIndex,
|
||||
endIndex > allResults.length ? allResults.length : endIndex,
|
||||
)
|
||||
: <TestResult>[];
|
||||
|
||||
return ApiResponse.success(
|
||||
data: results,
|
||||
message: '获取测试历史成功',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '获取测试历史失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取最近的测试结果
|
||||
Future<ApiResponse<List<TestResult>>> getRecentTestResults(
|
||||
String userId, {
|
||||
int limit = 5,
|
||||
}) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
final results = TestStaticData.getRecentResults(userId, limit: limit);
|
||||
return ApiResponse.success(
|
||||
data: results,
|
||||
message: '获取最近测试结果成功',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '获取最近测试结果失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取测试结果详情
|
||||
Future<ApiResponse<TestResult>> getTestResultById(String resultId) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
final result = TestStaticData.getResultById(resultId);
|
||||
if (result == null) {
|
||||
return ApiResponse.error(message: '测试结果不存在');
|
||||
}
|
||||
|
||||
return ApiResponse.success(
|
||||
data: result,
|
||||
message: '获取测试结果成功',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '获取测试结果失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户技能统计
|
||||
Future<ApiResponse<Map<SkillType, double>>> getUserSkillStatistics(
|
||||
String userId,
|
||||
) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final statistics = TestStaticData.getSkillStatistics(userId);
|
||||
return ApiResponse.success(
|
||||
data: statistics,
|
||||
message: '获取技能统计成功',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '获取技能统计失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取题目统计
|
||||
Future<ApiResponse<Map<String, dynamic>>> getQuestionStatistics() async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
final difficultyStats = TestStaticData.getDifficultyStatistics();
|
||||
final skillStats = TestStaticData.getSkillDistributionStatistics();
|
||||
|
||||
return ApiResponse.success(
|
||||
data: {
|
||||
'difficultyDistribution': difficultyStats,
|
||||
'skillDistribution': skillStats,
|
||||
'totalQuestions': TestStaticData.getAllQuestions().length,
|
||||
},
|
||||
message: '获取题目统计成功',
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '获取题目统计失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除测试结果
|
||||
Future<ApiResponse<bool>> deleteTestResult(String resultId) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
final success = TestStaticData.deleteTestResult(resultId);
|
||||
if (success) {
|
||||
return ApiResponse.success(
|
||||
data: true,
|
||||
message: '删除测试结果成功',
|
||||
);
|
||||
} else {
|
||||
return ApiResponse.error(message: '测试结果不存在');
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(
|
||||
message: '删除测试结果失败: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取测试类型名称
|
||||
String _getTestTypeName(TestType type) {
|
||||
switch (type) {
|
||||
case TestType.quick:
|
||||
return '快速测试';
|
||||
case TestType.standard:
|
||||
return '标准测试';
|
||||
case TestType.full:
|
||||
return '完整测试';
|
||||
case TestType.mock:
|
||||
return '模拟考试';
|
||||
case TestType.vocabulary:
|
||||
return '词汇专项';
|
||||
case TestType.grammar:
|
||||
return '语法专项';
|
||||
case TestType.reading:
|
||||
return '阅读专项';
|
||||
case TestType.listening:
|
||||
return '听力专项';
|
||||
case TestType.speaking:
|
||||
return '口语专项';
|
||||
case TestType.writing:
|
||||
return '写作专项';
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算测试结果(模拟)
|
||||
TestResult _calculateTestResult(String sessionId, List<UserAnswer> answers) {
|
||||
// 这里是一个简化的计算逻辑,实际应用中会更复杂
|
||||
final totalQuestions = answers.length;
|
||||
final correctAnswers = (totalQuestions * 0.8).round(); // 模拟80%正确率
|
||||
final totalScore = correctAnswers;
|
||||
final maxScore = totalQuestions;
|
||||
final percentage = (totalScore / maxScore) * 100;
|
||||
|
||||
return TestResult(
|
||||
id: 'result_${DateTime.now().millisecondsSinceEpoch}',
|
||||
testId: 'template_quick',
|
||||
userId: 'user_001',
|
||||
testType: TestType.quick,
|
||||
totalScore: totalScore,
|
||||
maxScore: maxScore,
|
||||
percentage: percentage,
|
||||
overallLevel: _calculateOverallLevel(percentage),
|
||||
skillScores: _calculateSkillScores(answers),
|
||||
answers: answers,
|
||||
startTime: DateTime.now().subtract(const Duration(minutes: 15)),
|
||||
endTime: DateTime.now(),
|
||||
duration: 900,
|
||||
feedback: _generateFeedback(percentage),
|
||||
recommendations: {
|
||||
'nextLevel': 'intermediate',
|
||||
'focusAreas': ['grammar', 'vocabulary'],
|
||||
'suggestedStudyTime': 45,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 计算整体水平
|
||||
DifficultyLevel _calculateOverallLevel(double percentage) {
|
||||
if (percentage >= 90) return DifficultyLevel.advanced;
|
||||
if (percentage >= 80) return DifficultyLevel.upperIntermediate;
|
||||
if (percentage >= 70) return DifficultyLevel.intermediate;
|
||||
if (percentage >= 60) return DifficultyLevel.elementary;
|
||||
return DifficultyLevel.beginner;
|
||||
}
|
||||
|
||||
/// 计算技能得分
|
||||
List<SkillScore> _calculateSkillScores(List<UserAnswer> answers) {
|
||||
// 简化的技能得分计算
|
||||
return [
|
||||
SkillScore(
|
||||
skillType: SkillType.vocabulary,
|
||||
score: 4,
|
||||
maxScore: 5,
|
||||
percentage: 80.0,
|
||||
level: DifficultyLevel.elementary,
|
||||
feedback: '词汇掌握良好',
|
||||
),
|
||||
SkillScore(
|
||||
skillType: SkillType.grammar,
|
||||
score: 3,
|
||||
maxScore: 4,
|
||||
percentage: 75.0,
|
||||
level: DifficultyLevel.elementary,
|
||||
feedback: '语法基础扎实',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// 生成反馈
|
||||
String _generateFeedback(double percentage) {
|
||||
if (percentage >= 90) {
|
||||
return '优秀!您的英语水平很高,继续保持。';
|
||||
} else if (percentage >= 80) {
|
||||
return '良好!您的英语基础扎实,可以尝试更高难度的内容。';
|
||||
} else if (percentage >= 70) {
|
||||
return '不错!您的英语水平中等,建议加强薄弱环节的练习。';
|
||||
} else if (percentage >= 60) {
|
||||
return '及格!您的英语基础还需要加强,建议多练习基础知识。';
|
||||
} else {
|
||||
return '需要努力!建议从基础开始,系统性地学习英语。';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import '../models/test_models.dart';
|
||||
import 'test_service.dart';
|
||||
import '../../../core/models/api_response.dart';
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/network/api_endpoints.dart';
|
||||
|
||||
/// 测试服务实现类(HTTP,使用后端真实API)
|
||||
class TestServiceImpl implements TestService {
|
||||
final ApiClient _apiClient = ApiClient.instance;
|
||||
|
||||
@override
|
||||
Future<ApiResponse<List<TestTemplate>>> getTestTemplates() async {
|
||||
try {
|
||||
final response = await _apiClient.get('${ApiEndpoints.baseUrl}/tests/templates');
|
||||
if (response.statusCode == 200) {
|
||||
final root = response.data['data'];
|
||||
List<dynamic> list = const [];
|
||||
if (root is List) {
|
||||
list = root;
|
||||
} else if (root is Map<String, dynamic>) {
|
||||
final v = root['templates'];
|
||||
if (v is List) list = v;
|
||||
}
|
||||
final templates = list.map((e) => TestTemplate.fromJson(e)).toList();
|
||||
return ApiResponse.success(message: '获取测试模板成功', data: templates);
|
||||
}
|
||||
return ApiResponse.error(message: '加载测试模板失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试模板失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<List<TestTemplate>>> getTestTemplatesByType(
|
||||
TestType type, {
|
||||
bool? isActive,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'${ApiEndpoints.baseUrl}/tests/templates',
|
||||
queryParameters: {
|
||||
'type': type.name,
|
||||
if (isActive != null) 'active': isActive,
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final root = response.data['data'];
|
||||
List<dynamic> list = const [];
|
||||
if (root is List) {
|
||||
list = root;
|
||||
} else if (root is Map<String, dynamic>) {
|
||||
final v = root['templates'];
|
||||
if (v is List) list = v;
|
||||
}
|
||||
final templates = list.map((e) => TestTemplate.fromJson(e)).toList();
|
||||
return ApiResponse.success(message: '获取测试模板成功', data: templates);
|
||||
}
|
||||
return ApiResponse.error(message: '加载测试模板失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试模板失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<TestTemplate>> getTestTemplateById(String id) async {
|
||||
try {
|
||||
final response = await _apiClient.get('${ApiEndpoints.baseUrl}/tests/templates/$id');
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: '获取测试模板成功',
|
||||
data: TestTemplate.fromJson(response.data['data']),
|
||||
);
|
||||
}
|
||||
return ApiResponse.error(message: '模板不存在', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试模板失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<TestSession>> createTestSession({
|
||||
required String templateId,
|
||||
required String userId,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'${ApiEndpoints.baseUrl}/tests/sessions',
|
||||
data: {
|
||||
'template_id': templateId,
|
||||
'user_id': userId,
|
||||
'metadata': metadata ?? {},
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: '创建测试会话成功',
|
||||
data: TestSession.fromJson(response.data['data']),
|
||||
);
|
||||
}
|
||||
return ApiResponse.error(message: '创建测试会话失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '创建测试会话失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<TestSession>> startTest(String sessionId) async {
|
||||
try {
|
||||
final response = await _apiClient.put('${ApiEndpoints.baseUrl}/tests/sessions/$sessionId/start');
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: '测试已开始',
|
||||
data: TestSession.fromJson(response.data['data']),
|
||||
);
|
||||
}
|
||||
return ApiResponse.error(message: '开始测试失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '开始测试失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<TestSession>> submitAnswer({
|
||||
required String sessionId,
|
||||
required UserAnswer answer,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'${ApiEndpoints.baseUrl}/tests/sessions/$sessionId/answers',
|
||||
data: answer.toJson(),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: '答案提交成功',
|
||||
data: TestSession.fromJson(response.data['data']),
|
||||
);
|
||||
}
|
||||
return ApiResponse.error(message: '提交答案失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '提交答案失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<TestSession>> pauseTest(String sessionId) async {
|
||||
try {
|
||||
final response = await _apiClient.put('${ApiEndpoints.baseUrl}/tests/sessions/$sessionId/pause');
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: '测试已暂停',
|
||||
data: TestSession.fromJson(response.data['data']),
|
||||
);
|
||||
}
|
||||
return ApiResponse.error(message: '暂停测试失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '暂停测试失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<TestSession>> resumeTest(String sessionId) async {
|
||||
try {
|
||||
final response = await _apiClient.put('${ApiEndpoints.baseUrl}/tests/sessions/$sessionId/resume');
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: '测试已恢复',
|
||||
data: TestSession.fromJson(response.data['data']),
|
||||
);
|
||||
}
|
||||
return ApiResponse.error(message: '恢复测试失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '恢复测试失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<TestResult>> completeTest({
|
||||
required String sessionId,
|
||||
required List<UserAnswer> answers,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.put(
|
||||
'${ApiEndpoints.baseUrl}/tests/sessions/$sessionId/complete',
|
||||
data: {
|
||||
'answers': answers.map((a) => a.toJson()).toList(),
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: '提交测试成功',
|
||||
data: TestResult.fromJson(response.data['data']),
|
||||
);
|
||||
}
|
||||
return ApiResponse.error(message: '完成测试失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '完成测试失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<List<TestResult>>> getUserTestHistory(
|
||||
String userId, {
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'${ApiEndpoints.baseUrl}/tests/sessions',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final root = response.data['data'];
|
||||
List<dynamic> list = const [];
|
||||
if (root is List) {
|
||||
list = root;
|
||||
} else if (root is Map<String, dynamic>) {
|
||||
final v = root['sessions'];
|
||||
if (v is List) list = v;
|
||||
}
|
||||
final results = list.map((e) => TestResult.fromJson(e)).toList();
|
||||
return ApiResponse.success(message: '获取测试历史成功', data: results);
|
||||
}
|
||||
return ApiResponse.error(message: '获取测试历史失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试历史失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<List<TestResult>>> getRecentTestResults(
|
||||
String userId, {
|
||||
int limit = 10,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'${ApiEndpoints.baseUrl}/tests/sessions',
|
||||
queryParameters: {
|
||||
'limit': limit,
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final root = response.data['data'];
|
||||
List<dynamic> list = const [];
|
||||
if (root is List) {
|
||||
list = root;
|
||||
} else if (root is Map<String, dynamic>) {
|
||||
final v = root['sessions'];
|
||||
if (v is List) list = v;
|
||||
}
|
||||
final results = list.map((e) => TestResult.fromJson(e)).toList();
|
||||
return ApiResponse.success(message: '获取最近测试结果成功', data: results);
|
||||
}
|
||||
return ApiResponse.error(message: '获取最近测试结果失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取最近测试结果失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<TestResult>> getTestResultById(String resultId) async {
|
||||
try {
|
||||
final response = await _apiClient.get('${ApiEndpoints.baseUrl}/tests/results/$resultId');
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: '获取测试结果成功',
|
||||
data: TestResult.fromJson(response.data['data']),
|
||||
);
|
||||
}
|
||||
return ApiResponse.error(message: '测试结果不存在', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取测试结果失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<Map<SkillType, double>>> getUserSkillStatistics(
|
||||
String userId, {
|
||||
int? daysPeriod,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get('${ApiEndpoints.baseUrl}/tests/stats');
|
||||
if (response.statusCode == 200) {
|
||||
final Map<String, dynamic> data = response.data['data'] ?? {};
|
||||
final Map<SkillType, double> stats = {};
|
||||
data.forEach((k, v) {
|
||||
final skill = SkillType.values.firstWhere((s) => s.name == k, orElse: () => SkillType.reading);
|
||||
stats[skill] = (v as num).toDouble();
|
||||
});
|
||||
return ApiResponse.success(message: '获取技能统计成功', data: stats);
|
||||
}
|
||||
return ApiResponse.error(message: '获取技能统计失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取技能统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<Map<String, dynamic>>> getQuestionStatistics() async {
|
||||
try {
|
||||
final response = await _apiClient.get('${ApiEndpoints.baseUrl}/tests/stats');
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(
|
||||
message: '获取题目统计成功',
|
||||
data: response.data['data'] ?? {},
|
||||
);
|
||||
}
|
||||
return ApiResponse.error(message: '获取题目统计失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取题目统计失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResponse<bool>> deleteTestResult(String resultId) async {
|
||||
try {
|
||||
final response = await _apiClient.delete('${ApiEndpoints.baseUrl}/tests/results/$resultId');
|
||||
if (response.statusCode == 200) {
|
||||
return ApiResponse.success(message: '删除测试结果成功', data: true);
|
||||
}
|
||||
return ApiResponse.error(message: '删除测试结果失败', code: response.statusCode);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '删除测试结果失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:async';
|
||||
import '../models/test_models.dart';
|
||||
|
||||
/// 题目展示组件
|
||||
class QuestionWidget extends StatefulWidget {
|
||||
final TestQuestion question;
|
||||
final UserAnswer? userAnswer;
|
||||
final Function(UserAnswer) onAnswerChanged;
|
||||
|
||||
const QuestionWidget({
|
||||
super.key,
|
||||
required this.question,
|
||||
this.userAnswer,
|
||||
required this.onAnswerChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<QuestionWidget> createState() => _QuestionWidgetState();
|
||||
}
|
||||
|
||||
class _QuestionWidgetState extends State<QuestionWidget> {
|
||||
late List<String> _selectedAnswers;
|
||||
late TextEditingController _textController;
|
||||
Timer? _debounceTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedAnswers = widget.userAnswer?.selectedAnswers ?? [];
|
||||
_textController = TextEditingController(text: widget.userAnswer?.textAnswer ?? '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
_debounceTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateAnswer() {
|
||||
final answer = UserAnswer(
|
||||
questionId: widget.question.id,
|
||||
selectedAnswers: _selectedAnswers,
|
||||
textAnswer: _textController.text.isNotEmpty ? _textController.text : null,
|
||||
answeredAt: DateTime.now(),
|
||||
timeSpent: 0, // 这里应该计算实际用时
|
||||
);
|
||||
widget.onAnswerChanged(answer);
|
||||
}
|
||||
|
||||
void _onTextChanged(String text) {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
|
||||
_updateAnswer();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildQuestionContent(),
|
||||
const SizedBox(height: 24),
|
||||
_buildAnswerSection(),
|
||||
if (widget.question.explanation != null && widget.userAnswer != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildExplanation(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuestionContent() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.question.content,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
if (widget.question.imageUrl != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
widget.question.imageUrl!,
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
color: Colors.grey[200],
|
||||
child: const Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.question.audioUrl != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildAudioPlayer(),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAudioPlayer() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.headphones,
|
||||
color: Colors.blue[700],
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'音频材料',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.blue[700],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'点击播放按钮收听音频',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// 这里应该实现音频播放功能
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('音频播放功能正在开发中...'),
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.play_circle_filled,
|
||||
color: Colors.blue[700],
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnswerSection() {
|
||||
switch (widget.question.type) {
|
||||
case QuestionType.multipleChoice:
|
||||
return _buildMultipleChoice();
|
||||
case QuestionType.multipleSelect:
|
||||
return _buildMultipleSelect();
|
||||
case QuestionType.fillInBlank:
|
||||
return _buildFillInBlank();
|
||||
case QuestionType.reading:
|
||||
return _buildMultipleChoice(); // 阅读理解通常是选择题
|
||||
case QuestionType.listening:
|
||||
return _buildMultipleChoice(); // 听力理解通常是选择题
|
||||
case QuestionType.speaking:
|
||||
return _buildSpeaking();
|
||||
case QuestionType.writing:
|
||||
return _buildWriting();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMultipleChoice() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'请选择正确答案:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...widget.question.options.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final option = entry.value;
|
||||
final optionLabel = String.fromCharCode(65 + index); // A, B, C, D
|
||||
final isSelected = _selectedAnswers.contains(option);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedAnswers = [option];
|
||||
});
|
||||
_updateAnswer();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.blue.withOpacity(0.1) : Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected ? Colors.blue : Colors.grey[300]!,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isSelected ? Colors.blue : Colors.transparent,
|
||||
border: Border.all(
|
||||
color: isSelected ? Colors.blue : Colors.grey[400]!,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'$optionLabel.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected ? Colors.blue : Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
option,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: isSelected ? Colors.blue[700] : Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMultipleSelect() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'请选择所有正确答案:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...widget.question.options.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final option = entry.value;
|
||||
final optionLabel = String.fromCharCode(65 + index); // A, B, C, D
|
||||
final isSelected = _selectedAnswers.contains(option);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (isSelected) {
|
||||
_selectedAnswers.remove(option);
|
||||
} else {
|
||||
_selectedAnswers.add(option);
|
||||
}
|
||||
});
|
||||
_updateAnswer();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.green.withOpacity(0.1) : Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected ? Colors.green : Colors.grey[300]!,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: isSelected ? Colors.green : Colors.transparent,
|
||||
border: Border.all(
|
||||
color: isSelected ? Colors.green : Colors.grey[400]!,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'$optionLabel.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected ? Colors.green : Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
option,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: isSelected ? Colors.green[700] : Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFillInBlank() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'请填入正确答案:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _textController,
|
||||
onChanged: _onTextChanged,
|
||||
decoration: InputDecoration(
|
||||
hintText: '请输入答案...',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Colors.blue, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
if (widget.question.options.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'提示选项:',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: widget.question.options.map((option) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
_textController.text = option;
|
||||
_updateAnswer();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Text(
|
||||
option,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpeaking() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'口语回答:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.red.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.mic,
|
||||
size: 48,
|
||||
color: Colors.red[600],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'点击开始录音',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.red[700],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'建议录音时长:${widget.question.timeLimit}秒',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.red[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// 这里应该实现录音功能
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('录音功能正在开发中...'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.fiber_manual_record),
|
||||
label: const Text('开始录音'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWriting() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'请写出您的答案:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _textController,
|
||||
onChanged: _onTextChanged,
|
||||
maxLines: 8,
|
||||
decoration: InputDecoration(
|
||||
hintText: '请在此输入您的答案...',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Colors.teal, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
style: const TextStyle(fontSize: 16, height: 1.5),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'字数:${_textController.text.length}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'建议时长:${widget.question.timeLimit ~/ 60}分钟',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExplanation() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.blue.withOpacity(0.2)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lightbulb_outline,
|
||||
color: Colors.blue[600],
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'解析',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.blue[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.question.explanation!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blue[700],
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user