Enhance grammar exercise functionality in VocabLessonView
- Added support for multiple exercise types including multiple choice, gap fill, and transformation. - Updated UI to display exercise instructions and results with improved styling. - Implemented logic to handle answer checking based on exercise type, enhancing user interaction. - Added new translations for exercise-related terms in both English and German.
This commit is contained in:
@@ -13,15 +13,73 @@
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.grammarExercises') }}</h3>
|
||||
<div v-for="exercise in lesson.grammarExercises" :key="exercise.id" class="exercise-item">
|
||||
<h4>{{ exercise.title }}</h4>
|
||||
<p v-if="exercise.description">{{ exercise.description }}</p>
|
||||
<div v-if="exercise.type === 'gap_fill'" class="gap-fill-exercise">
|
||||
<p v-html="formatGapFill(exercise.content)"></p>
|
||||
<input v-model="exerciseAnswers[exercise.id]" :placeholder="$t('socialnetwork.vocab.courses.enterAnswer')" />
|
||||
<button @click="checkAnswer(exercise.id)">{{ $t('socialnetwork.vocab.courses.checkAnswer') }}</button>
|
||||
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
|
||||
{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}
|
||||
<p v-if="exercise.explanation">{{ exercise.explanation }}</p>
|
||||
<p v-if="exercise.instruction" class="exercise-instruction">{{ exercise.instruction }}</p>
|
||||
|
||||
<!-- Multiple Choice Übung -->
|
||||
<div v-if="getExerciseType(exercise) === 'multiple_choice'" class="multiple-choice-exercise">
|
||||
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
|
||||
<div class="options">
|
||||
<label v-for="(option, index) in getOptions(exercise)" :key="index" class="option-label">
|
||||
<input
|
||||
type="radio"
|
||||
:name="'exercise-' + exercise.id"
|
||||
:value="index"
|
||||
v-model="exerciseAnswers[exercise.id]"
|
||||
/>
|
||||
<span>{{ option }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button @click="checkAnswer(exercise.id)" :disabled="!exerciseAnswers[exercise.id] && exerciseAnswers[exercise.id] !== 0">
|
||||
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
|
||||
</button>
|
||||
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
|
||||
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
|
||||
<p v-if="exercise.explanation" class="exercise-explanation">{{ exercise.explanation }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gap Fill Übung -->
|
||||
<div v-else-if="getExerciseType(exercise) === 'gap_fill'" class="gap-fill-exercise">
|
||||
<p class="exercise-text" v-html="formatGapFill(exercise)"></p>
|
||||
<div class="gap-inputs">
|
||||
<input
|
||||
v-for="(gap, index) in getGapCount(exercise)"
|
||||
:key="index"
|
||||
v-model="exerciseAnswers[exercise.id][index]"
|
||||
:placeholder="$t('socialnetwork.vocab.courses.enterAnswer')"
|
||||
class="gap-input"
|
||||
/>
|
||||
</div>
|
||||
<button @click="checkAnswer(exercise.id)" :disabled="!hasAllGapsFilled(exercise)">
|
||||
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
|
||||
</button>
|
||||
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
|
||||
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
|
||||
<p v-if="exercise.explanation" class="exercise-explanation">{{ exercise.explanation }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transformation Übung -->
|
||||
<div v-else-if="getExerciseType(exercise) === 'transformation'" class="transformation-exercise">
|
||||
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
|
||||
<input
|
||||
v-model="exerciseAnswers[exercise.id]"
|
||||
:placeholder="$t('socialnetwork.vocab.courses.enterAnswer')"
|
||||
class="transformation-input"
|
||||
/>
|
||||
<button @click="checkAnswer(exercise.id)" :disabled="!exerciseAnswers[exercise.id]">
|
||||
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
|
||||
</button>
|
||||
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
|
||||
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
|
||||
<p v-if="exercise.explanation" class="exercise-explanation">{{ exercise.explanation }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fallback für unbekannte Typen -->
|
||||
<div v-else class="unknown-exercise">
|
||||
<p>Übungstyp: {{ getExerciseType(exercise) }}</p>
|
||||
<pre>{{ JSON.stringify(exercise, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,23 +145,115 @@ export default {
|
||||
async loadGrammarExercises() {
|
||||
try {
|
||||
const res = await apiClient.get(`/api/vocab/lessons/${this.lessonId}/grammar-exercises`);
|
||||
this.lesson.grammarExercises = res.data || [];
|
||||
const exercises = res.data || [];
|
||||
this.lesson.grammarExercises = exercises;
|
||||
|
||||
// Initialisiere Antwort-Arrays für Gap Fill Übungen
|
||||
exercises.forEach(exercise => {
|
||||
const exerciseType = this.getExerciseType(exercise);
|
||||
if (exerciseType === 'gap_fill') {
|
||||
const gapCount = this.getGapCount(exercise);
|
||||
this.$set(this.exerciseAnswers, exercise.id, new Array(gapCount).fill(''));
|
||||
} else {
|
||||
this.$set(this.exerciseAnswers, exercise.id, '');
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Konnte Grammatik-Übungen nicht laden:', e);
|
||||
this.lesson.grammarExercises = [];
|
||||
}
|
||||
},
|
||||
formatGapFill(content) {
|
||||
// Ersetze Platzhalter mit Input-Feldern
|
||||
return content.replace(/\{gap\}/g, '<span class="gap">_____</span>');
|
||||
getExerciseType(exercise) {
|
||||
// Hole den Typ aus questionData oder exerciseType
|
||||
if (exercise.questionData) {
|
||||
const qData = typeof exercise.questionData === 'string'
|
||||
? JSON.parse(exercise.questionData)
|
||||
: exercise.questionData;
|
||||
return qData.type;
|
||||
}
|
||||
// Fallback: Prüfe exerciseTypeId
|
||||
const typeMap = {
|
||||
1: 'gap_fill',
|
||||
2: 'multiple_choice',
|
||||
3: 'sentence_building',
|
||||
4: 'transformation',
|
||||
5: 'conjugation',
|
||||
6: 'declension'
|
||||
};
|
||||
return typeMap[exercise.exerciseTypeId] || 'unknown';
|
||||
},
|
||||
getQuestionData(exercise) {
|
||||
if (!exercise.questionData) return null;
|
||||
return typeof exercise.questionData === 'string'
|
||||
? JSON.parse(exercise.questionData)
|
||||
: exercise.questionData;
|
||||
},
|
||||
getAnswerData(exercise) {
|
||||
if (!exercise.answerData) return null;
|
||||
return typeof exercise.answerData === 'string'
|
||||
? JSON.parse(exercise.answerData)
|
||||
: exercise.answerData;
|
||||
},
|
||||
getQuestionText(exercise) {
|
||||
const qData = this.getQuestionData(exercise);
|
||||
if (!qData) return exercise.title;
|
||||
|
||||
if (qData.question) return qData.question;
|
||||
if (qData.text) return qData.text;
|
||||
return exercise.title;
|
||||
},
|
||||
getOptions(exercise) {
|
||||
const qData = this.getQuestionData(exercise);
|
||||
return qData?.options || [];
|
||||
},
|
||||
formatGapFill(exercise) {
|
||||
const qData = this.getQuestionData(exercise);
|
||||
if (!qData || !qData.text) return '';
|
||||
|
||||
// Ersetze {gap} mit Platzhaltern
|
||||
return qData.text.replace(/\{gap\}/g, '<span class="gap">_____</span>');
|
||||
},
|
||||
getGapCount(exercise) {
|
||||
const qData = this.getQuestionData(exercise);
|
||||
if (!qData) return 0;
|
||||
|
||||
// Zähle {gap} im Text
|
||||
const matches = qData.text?.match(/\{gap\}/g);
|
||||
return matches ? matches.length : (qData.gaps || 1);
|
||||
},
|
||||
hasAllGapsFilled(exercise) {
|
||||
const answers = this.exerciseAnswers[exercise.id];
|
||||
if (!answers || !Array.isArray(answers)) return false;
|
||||
const gapCount = this.getGapCount(exercise);
|
||||
return answers.length === gapCount && answers.every(a => a && a.trim());
|
||||
},
|
||||
async checkAnswer(exerciseId) {
|
||||
try {
|
||||
const answer = this.exerciseAnswers[exerciseId];
|
||||
const res = await apiClient.post(`/api/vocab/exercises/${exerciseId}/check`, { answer });
|
||||
this.exerciseResults[exerciseId] = res.data;
|
||||
const exercise = this.lesson.grammarExercises.find(e => e.id === exerciseId);
|
||||
if (!exercise) return;
|
||||
|
||||
const exerciseType = this.getExerciseType(exercise);
|
||||
let answer = this.exerciseAnswers[exerciseId];
|
||||
|
||||
// Formatiere Antwort je nach Typ
|
||||
if (exerciseType === 'gap_fill') {
|
||||
// Gap Fill: Array von Antworten
|
||||
if (!Array.isArray(answer)) {
|
||||
answer = [answer];
|
||||
}
|
||||
} else if (exerciseType === 'multiple_choice') {
|
||||
// Multiple Choice: Index als Zahl
|
||||
answer = Number(answer);
|
||||
} else if (exerciseType === 'transformation') {
|
||||
// Transformation: String
|
||||
answer = String(answer || '').trim();
|
||||
}
|
||||
|
||||
const res = await apiClient.post(`/api/vocab/grammar-exercises/${exerciseId}/check`, { answer });
|
||||
this.$set(this.exerciseResults, exerciseId, res.data);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Prüfen der Antwort:', e);
|
||||
alert(e.response?.data?.error || 'Fehler beim Prüfen der Antwort');
|
||||
}
|
||||
},
|
||||
back() {
|
||||
@@ -153,27 +303,130 @@ export default {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.gap-fill-exercise input {
|
||||
.exercise-instruction {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.exercise-question {
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.exercise-text {
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.gap {
|
||||
display: inline-block;
|
||||
min-width: 100px;
|
||||
border-bottom: 2px solid #333;
|
||||
margin: 0 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.multiple-choice-exercise .options {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
margin: 8px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.option-label:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.option-label input[type="radio"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.gap-fill-exercise .gap-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.gap-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.transformation-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.exercise-item button {
|
||||
padding: 10px 20px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
margin-top: 10px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.exercise-item button:hover:not(:disabled) {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.exercise-item button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.exercise-result {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.exercise-result.correct {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.exercise-result.wrong {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.exercise-explanation {
|
||||
margin-top: 10px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.unknown-exercise {
|
||||
padding: 15px;
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.unknown-exercise pre {
|
||||
margin-top: 10px;
|
||||
font-size: 0.85em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user