feat(i18n, vocab): enhance localization and vocabulary preparation in VocabLessonView
Some checks failed
Deploy to production / deploy (push) Has been cancelled
Some checks failed
Deploy to production / deploy (push) Has been cancelled
- Added new localization keys in German, English, and Spanish for exercise flow, progress, and learning path instructions, improving user guidance across languages. - Updated VocabLessonView to incorporate these new keys, enhancing the clarity of vocabulary preparation steps and overall user experience during lessons. - Refactored the layout to better present the learning path and vocabulary preparation stages, ensuring a more cohesive and informative interface.
This commit is contained in:
@@ -431,6 +431,19 @@
|
||||
"invalidCode": "Ungültiger Code",
|
||||
"courseNotFound": "Kurs nicht gefunden",
|
||||
"grammarExercises": "Grammatik-Prüfung",
|
||||
"exerciseFlowIntro": "Arbeite die Aufgaben der Reihe nach durch. Jede korrekt gelöste Aufgabe bringt dich direkt näher zum Abschluss der Lektion.",
|
||||
"exerciseProgressLabel": "Fortschritt",
|
||||
"exerciseTargetLabel": "Benötigt",
|
||||
"exerciseCardLabel": "Aufgabe {number}",
|
||||
"exerciseStatusOpen": "Offen",
|
||||
"exerciseStatusCorrect": "Erledigt",
|
||||
"exerciseStatusRetry": "Nochmal prüfen",
|
||||
"exerciseAnswerAllHint": "Beantworte zuerst alle {total} Aufgaben. Aktuell bearbeitet: {answered}. Zum Bestehen brauchst du mindestens {target}%.",
|
||||
"exerciseNeedMoreCorrectHint": "Du hast aktuell {score}%. Für den Abschluss dieser Lektion brauchst du mindestens {target}%.",
|
||||
"exercisePassedHint": "Ziel erreicht: {score}% von benötigten {target}%. Sobald alle Aufgaben bearbeitet sind, gilt die Prüfung als bestanden.",
|
||||
"exerciseReinforcementHint": "Nach einem Fehler geht es kurz zurück in den Lernmodus. Übe noch {count} Trainerfragen, dann wird die Kapitel-Prüfung wieder freigeschaltet.",
|
||||
"exercisePrepReinforcementHint": "Nach einem Fehler gehst du noch einmal durch die vorbereiteten Begriffe. Danach wird die Kapitel-Prüfung wieder freigeschaltet.",
|
||||
"exerciseGrammarLead": "Wichtige Grammatik für diese Prüfung",
|
||||
"noExercises": "Keine Prüfung verfügbar",
|
||||
"enterAnswer": "Antwort eingeben",
|
||||
"checkAnswer": "Antwort prüfen",
|
||||
@@ -456,6 +469,9 @@
|
||||
"vocabPrepStep2": "Gehe die gleichen Begriffe noch einmal durch (aktive Wiederholung, ohne zu üben).",
|
||||
"vocabPrepConfirm2": "Zweite Durchsicht erledigt",
|
||||
"vocabPrepReady": "Du kannst jetzt mit dem Vokabeltrainer starten.",
|
||||
"learningPathLabel": "Hauptpfad",
|
||||
"learningPathTitle": "Dein Lernweg für diese Lektion",
|
||||
"learningPathIntro": "Arbeite diese Schritte nacheinander durch: vorbereiten, kurz überblicken, trainieren und dann zur Kapitel-Prüfung wechseln.",
|
||||
"lessonDetailsToggle": "Mehr Lektionsdetails anzeigen",
|
||||
"deepenSectionTitle": "Vertiefen und nachlesen",
|
||||
"assistantSectionTitle": "Mit Sprachassistent vertiefen",
|
||||
|
||||
@@ -431,6 +431,19 @@
|
||||
"invalidCode": "Invalid code",
|
||||
"courseNotFound": "Course not found",
|
||||
"grammarExercises": "Chapter Test",
|
||||
"exerciseFlowIntro": "Work through the tasks in order. Every correct answer moves you closer to completing the lesson.",
|
||||
"exerciseProgressLabel": "Progress",
|
||||
"exerciseTargetLabel": "Required",
|
||||
"exerciseCardLabel": "Task {number}",
|
||||
"exerciseStatusOpen": "Open",
|
||||
"exerciseStatusCorrect": "Done",
|
||||
"exerciseStatusRetry": "Try again",
|
||||
"exerciseAnswerAllHint": "Answer all {total} tasks first. Completed so far: {answered}. You need at least {target}% to pass.",
|
||||
"exerciseNeedMoreCorrectHint": "You currently have {score}%. You need at least {target}% to complete this lesson.",
|
||||
"exercisePassedHint": "Target reached: {score}% out of the required {target}%. Once all tasks have been answered, the chapter test is passed.",
|
||||
"exerciseReinforcementHint": "After a mistake, the flow returns briefly to learning mode. Practice {count} more trainer questions and the chapter test will unlock again.",
|
||||
"exercisePrepReinforcementHint": "After a mistake, go through the prepared terms once more. Then the chapter test will unlock again.",
|
||||
"exerciseGrammarLead": "Key grammar for this test",
|
||||
"noExercises": "No test available",
|
||||
"enterAnswer": "Enter answer",
|
||||
"checkAnswer": "Check Answer",
|
||||
@@ -456,6 +469,9 @@
|
||||
"vocabPrepStep2": "Go through the same items again (active review, not testing yet).",
|
||||
"vocabPrepConfirm2": "Second pass done",
|
||||
"vocabPrepReady": "You can start the vocabulary trainer now.",
|
||||
"learningPathLabel": "Main path",
|
||||
"learningPathTitle": "Your learning flow for this lesson",
|
||||
"learningPathIntro": "Work through these steps in order: prepare, review briefly, train, then move to the chapter test.",
|
||||
"lessonDetailsToggle": "Show more lesson details",
|
||||
"deepenSectionTitle": "Deepen and review",
|
||||
"assistantSectionTitle": "Deepen with language assistant",
|
||||
|
||||
@@ -429,6 +429,19 @@
|
||||
"invalidCode": "Código inválido",
|
||||
"courseNotFound": "Curso no encontrado",
|
||||
"grammarExercises": "Prueba de gramática",
|
||||
"exerciseFlowIntro": "Resuelve las tareas en orden. Cada respuesta correcta te acerca al cierre de la lección.",
|
||||
"exerciseProgressLabel": "Progreso",
|
||||
"exerciseTargetLabel": "Necesario",
|
||||
"exerciseCardLabel": "Tarea {number}",
|
||||
"exerciseStatusOpen": "Pendiente",
|
||||
"exerciseStatusCorrect": "Hecha",
|
||||
"exerciseStatusRetry": "Revisar otra vez",
|
||||
"exerciseAnswerAllHint": "Responde primero las {total} tareas. Completadas hasta ahora: {answered}. Necesitas al menos {target}% para aprobar.",
|
||||
"exerciseNeedMoreCorrectHint": "Ahora mismo tienes {score}%. Necesitas al menos {target}% para completar esta lección.",
|
||||
"exercisePassedHint": "Objetivo alcanzado: {score}% de los {target}% necesarios. En cuanto todas las tareas estén respondidas, la prueba queda aprobada.",
|
||||
"exerciseReinforcementHint": "Después de un error, el flujo vuelve brevemente al modo de aprendizaje. Practica {count} preguntas más en el entrenador y la prueba del capítulo se desbloqueará otra vez.",
|
||||
"exercisePrepReinforcementHint": "Después de un error, vuelve a repasar los términos preparados una vez más. Luego la prueba del capítulo se desbloqueará otra vez.",
|
||||
"exerciseGrammarLead": "Gramática clave para esta prueba",
|
||||
"noExercises": "No hay prueba disponible",
|
||||
"enterAnswer": "Introduce la respuesta",
|
||||
"checkAnswer": "Comprobar respuesta",
|
||||
@@ -454,6 +467,9 @@
|
||||
"vocabPrepStep2": "Repasa los mismos elementos otra vez (repaso activo, aún sin practicar).",
|
||||
"vocabPrepConfirm2": "Segunda lectura hecha",
|
||||
"vocabPrepReady": "Ya puedes iniciar el entrenador de vocabulario.",
|
||||
"learningPathLabel": "Ruta principal",
|
||||
"learningPathTitle": "Tu recorrido de aprendizaje para esta lección",
|
||||
"learningPathIntro": "Sigue estos pasos en orden: preparar, repasar brevemente, entrenar y luego pasar a la prueba del capítulo.",
|
||||
"lessonDetailsToggle": "Mostrar más detalles de la lección",
|
||||
"deepenSectionTitle": "Profundizar y repasar",
|
||||
"assistantSectionTitle": "Profundizar con el asistente de idiomas",
|
||||
|
||||
@@ -83,6 +83,13 @@
|
||||
<p>Diese Lektion priorisiert Wiederholung und Vertiefung. Neuer Stoff wird bewusst reduziert, damit vorhandene Muster stabil werden.</p>
|
||||
</div>
|
||||
|
||||
<section class="lesson-primary-flow surface-card">
|
||||
<div class="lesson-primary-flow__header">
|
||||
<span class="lesson-primary-flow__eyebrow">{{ $t('socialnetwork.vocab.courses.learningPathLabel') }}</span>
|
||||
<h4 class="lesson-primary-flow__title">{{ $t('socialnetwork.vocab.courses.learningPathTitle') }}</h4>
|
||||
<p class="lesson-primary-flow__intro">{{ $t('socialnetwork.vocab.courses.learningPathIntro') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Zwei Durchgänge: dieselben Kernmuster schrittweise vor dem Trainer -->
|
||||
<div
|
||||
v-if="prepItems.length > 0 && !vocabTrainerActive"
|
||||
@@ -207,7 +214,6 @@
|
||||
{{ option }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Button entfernt: Prüfung erfolgt automatisch beim Klick auf Option -->
|
||||
</div>
|
||||
<!-- Texteingabe Modus -->
|
||||
<div v-else class="vocab-answer-area typing">
|
||||
@@ -246,6 +252,7 @@
|
||||
{{ $t('socialnetwork.vocab.courses.startExercises') }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details class="lesson-deepen-section surface-card">
|
||||
<summary class="lesson-deepen-section__summary">
|
||||
@@ -403,9 +410,76 @@
|
||||
<!-- Übungen-Tab (Kapitel-Prüfung) -->
|
||||
<div v-if="activeTab === 'exercises'" class="grammar-exercises">
|
||||
<div v-if="lesson && effectiveExercises && effectiveExercises.length > 0">
|
||||
<div class="exercise-flow-header surface-card">
|
||||
<div>
|
||||
<span class="exercise-flow-header__eyebrow">{{ $t('socialnetwork.vocab.courses.exercises') }}</span>
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.grammarExercises') }}</h3>
|
||||
<div v-for="exercise in effectiveExercises" :key="exercise.id" class="exercise-item">
|
||||
<p class="exercise-flow-header__intro">{{ $t('socialnetwork.vocab.courses.exerciseFlowIntro') }}</p>
|
||||
<p class="exercise-flow-header__hint">{{ exerciseStatusHint }}</p>
|
||||
</div>
|
||||
<div class="exercise-flow-header__stats">
|
||||
<div class="exercise-flow-stat">
|
||||
<span>{{ $t('socialnetwork.vocab.courses.exerciseProgressLabel') }}</span>
|
||||
<strong>{{ exerciseCorrectCount }}/{{ effectiveExercises.length }}</strong>
|
||||
</div>
|
||||
<div class="exercise-flow-stat">
|
||||
<span>{{ $t('socialnetwork.vocab.courses.successRate') }}</span>
|
||||
<strong>{{ exerciseProgressPercent }}%</strong>
|
||||
</div>
|
||||
<div class="exercise-flow-stat">
|
||||
<span>{{ $t('socialnetwork.vocab.courses.exerciseTargetLabel') }}</span>
|
||||
<strong>{{ exerciseTargetScore }}%</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="visibleGrammarExplanations.length > 0" class="exercise-grammar-card surface-card">
|
||||
<div class="exercise-grammar-card__header">
|
||||
<span class="exercise-flow-header__eyebrow">{{ $t('socialnetwork.vocab.courses.grammarExplanations') }}</span>
|
||||
<strong>{{ $t('socialnetwork.vocab.courses.exerciseGrammarLead') }}</strong>
|
||||
</div>
|
||||
<div class="exercise-grammar-card__list">
|
||||
<article
|
||||
v-for="(explanation, index) in visibleGrammarExplanations.slice(0, 2)"
|
||||
:key="'exercise-grammar-' + index"
|
||||
class="exercise-grammar-card__item"
|
||||
>
|
||||
<strong>{{ explanation.title || $t('socialnetwork.vocab.courses.grammarImpulse') }}</strong>
|
||||
<p>{{ explanation.text }}</p>
|
||||
<p v-if="explanation.example" class="grammar-example">{{ explanation.example }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="exercise-flow-list">
|
||||
<div
|
||||
v-for="(exercise, index) in effectiveExercises"
|
||||
:key="exercise.id"
|
||||
class="exercise-item surface-card"
|
||||
:class="{
|
||||
'exercise-item--answered': exerciseResults[exercise.id],
|
||||
'exercise-item--correct': exerciseResults[exercise.id]?.correct,
|
||||
'exercise-item--wrong': exerciseResults[exercise.id] && !exerciseResults[exercise.id]?.correct
|
||||
}"
|
||||
>
|
||||
<div class="exercise-item__header">
|
||||
<div>
|
||||
<span class="exercise-item__index">{{ $t('socialnetwork.vocab.courses.exerciseCardLabel', { number: index + 1 }) }}</span>
|
||||
<h4>{{ exercise.title }}</h4>
|
||||
</div>
|
||||
<span
|
||||
class="exercise-item__status"
|
||||
:class="{
|
||||
'exercise-item__status--open': !exerciseResults[exercise.id],
|
||||
'exercise-item__status--correct': exerciseResults[exercise.id]?.correct,
|
||||
'exercise-item__status--wrong': exerciseResults[exercise.id] && !exerciseResults[exercise.id]?.correct
|
||||
}"
|
||||
>
|
||||
{{ !exerciseResults[exercise.id]
|
||||
? $t('socialnetwork.vocab.courses.exerciseStatusOpen')
|
||||
: (exerciseResults[exercise.id]?.correct
|
||||
? $t('socialnetwork.vocab.courses.exerciseStatusCorrect')
|
||||
: $t('socialnetwork.vocab.courses.exerciseStatusRetry')) }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="exercise.instruction" class="exercise-instruction">{{ exercise.instruction }}</p>
|
||||
|
||||
<!-- Multiple Choice Übung -->
|
||||
@@ -697,6 +771,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="lesson && (!effectiveExercises || effectiveExercises.length === 0)">
|
||||
<p>{{ $t('socialnetwork.vocab.courses.noExercises') }}</p>
|
||||
</div>
|
||||
@@ -813,6 +888,8 @@ export default {
|
||||
recognizedText: {}, // { [exerciseId]: string }
|
||||
recordingStatus: {}, // { [exerciseId]: string }
|
||||
isSpeechRecognitionSupported: false,
|
||||
exerciseRetryPending: false,
|
||||
exerciseRetryPendingSinceAttempts: 0,
|
||||
assistantLoading: false,
|
||||
assistantSubmitting: false,
|
||||
assistantSettings: null,
|
||||
@@ -878,6 +955,7 @@ export default {
|
||||
},
|
||||
canAccessExercises() {
|
||||
if (!this.hasExercises) return false;
|
||||
if (this.exerciseNeedsReinforcement) return false;
|
||||
const isReview = this.lesson?.lessonType === 'review' || this.lesson?.lessonType === 'vocab_review';
|
||||
if (isReview) return true;
|
||||
if (this.trainableLessonVocab.length === 0 && this.prepItems.length > 0) {
|
||||
@@ -886,6 +964,11 @@ export default {
|
||||
return this.exercisePreparationCompleted;
|
||||
},
|
||||
exerciseUnlockHint() {
|
||||
if (this.exerciseNeedsReinforcement) {
|
||||
return this.$t('socialnetwork.vocab.courses.exerciseReinforcementHint', {
|
||||
count: this.exerciseRetryRemainingAttempts
|
||||
});
|
||||
}
|
||||
if (this.trainableLessonVocab.length === 0 && this.prepItems.length > 0) {
|
||||
return this.$t('socialnetwork.vocab.courses.exerciseUnlockHintAfterPrep');
|
||||
}
|
||||
@@ -906,6 +989,62 @@ export default {
|
||||
}
|
||||
return [];
|
||||
},
|
||||
exerciseCorrectCount() {
|
||||
return this.effectiveExercises.filter((exercise) => Boolean(this.exerciseResults[exercise.id]?.correct)).length;
|
||||
},
|
||||
exerciseAnsweredCount() {
|
||||
return this.effectiveExercises.filter((exercise) => Boolean(this.exerciseResults[exercise.id])).length;
|
||||
},
|
||||
exerciseProgressPercent() {
|
||||
if (!this.effectiveExercises.length) return 0;
|
||||
return Math.round((this.exerciseCorrectCount / this.effectiveExercises.length) * 100);
|
||||
},
|
||||
exerciseTargetScore() {
|
||||
return Number(this.lesson?.targetScorePercent) || 80;
|
||||
},
|
||||
exerciseRetryUnlockAttempts() {
|
||||
return Math.min(8, Math.max(2, Math.ceil(this.trainerNewFocusTarget * 0.25)));
|
||||
},
|
||||
exerciseRetryRemainingAttempts() {
|
||||
if (!this.exerciseRetryPending) return 0;
|
||||
const sinceWrong = this.vocabTrainerTotalAttempts - this.exerciseRetryPendingSinceAttempts;
|
||||
return Math.max(0, this.exerciseRetryUnlockAttempts - sinceWrong);
|
||||
},
|
||||
exerciseNeedsReinforcement() {
|
||||
return this.exerciseRetryPending && this.exerciseRetryRemainingAttempts > 0;
|
||||
},
|
||||
visibleGrammarExplanations() {
|
||||
const didacticFocus = Array.isArray(this.lessonDidactics?.grammarFocus) ? this.lessonDidactics.grammarFocus : [];
|
||||
if (didacticFocus.length > 0) {
|
||||
return didacticFocus;
|
||||
}
|
||||
return this.grammarExplanations;
|
||||
},
|
||||
exerciseStatusHint() {
|
||||
if (!this.effectiveExercises.length) return '';
|
||||
if (this.exerciseNeedsReinforcement) {
|
||||
return this.$t('socialnetwork.vocab.courses.exerciseReinforcementHint', {
|
||||
count: this.exerciseRetryRemainingAttempts
|
||||
});
|
||||
}
|
||||
if (this.exerciseAnsweredCount < this.effectiveExercises.length) {
|
||||
return this.$t('socialnetwork.vocab.courses.exerciseAnswerAllHint', {
|
||||
answered: this.exerciseAnsweredCount,
|
||||
total: this.effectiveExercises.length,
|
||||
target: this.exerciseTargetScore
|
||||
});
|
||||
}
|
||||
if (this.exerciseProgressPercent >= this.exerciseTargetScore) {
|
||||
return this.$t('socialnetwork.vocab.courses.exercisePassedHint', {
|
||||
score: this.exerciseProgressPercent,
|
||||
target: this.exerciseTargetScore
|
||||
});
|
||||
}
|
||||
return this.$t('socialnetwork.vocab.courses.exerciseNeedMoreCorrectHint', {
|
||||
score: this.exerciseProgressPercent,
|
||||
target: this.exerciseTargetScore
|
||||
});
|
||||
},
|
||||
grammarExplanations() {
|
||||
// Extrahiere Grammatik-Erklärungen aus den Übungen
|
||||
try {
|
||||
@@ -965,7 +1104,7 @@ export default {
|
||||
const reference = String(entry?.reference || '').trim();
|
||||
const learning = String(entry?.learning || '').trim();
|
||||
if (!reference) return;
|
||||
const key = reference.toLowerCase();
|
||||
const key = this.normalizeLessonVocabTerm(reference);
|
||||
if (!vocabByReference.has(key)) {
|
||||
vocabByReference.set(key, { learning, reference });
|
||||
return;
|
||||
@@ -1082,6 +1221,14 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
normalizeLessonVocabTerm(value) {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/^[.,!?;:]+|[.,!?;:]+$/g, '')
|
||||
.trim();
|
||||
},
|
||||
normalizeCorePatternEntry(p) {
|
||||
if (p && typeof p === 'object' && p.target) {
|
||||
return {
|
||||
@@ -1134,6 +1281,9 @@ export default {
|
||||
});
|
||||
},
|
||||
updateExerciseUnlockState() {
|
||||
if (this.exerciseRetryPending && this.exerciseRetryRemainingAttempts <= 0) {
|
||||
this.exerciseRetryPending = false;
|
||||
}
|
||||
if (this.exercisePreparationCompleted) {
|
||||
return;
|
||||
}
|
||||
@@ -1305,6 +1455,8 @@ export default {
|
||||
this.assistantMessages = [];
|
||||
this.assistantInput = '';
|
||||
this.assistantError = '';
|
||||
this.exerciseRetryPending = false;
|
||||
this.exerciseRetryPendingSinceAttempts = 0;
|
||||
this.exercisePreparationCompleted = false;
|
||||
this.lessonPrepStage = 0;
|
||||
this.lessonPrepIndex = 0;
|
||||
@@ -1699,6 +1851,31 @@ export default {
|
||||
const res = await apiClient.post(`/api/vocab/grammar-exercises/${exerciseId}/check`, { answer });
|
||||
this.exerciseResults[exerciseId] = res.data;
|
||||
|
||||
if (!res.data?.correct) {
|
||||
if (this.trainableLessonVocab.length === 0 && this.prepItems.length > 0) {
|
||||
this.lessonPrepStage = 0;
|
||||
this.lessonPrepIndex = 0;
|
||||
this.errorMessage = this.$t('socialnetwork.vocab.courses.exercisePrepReinforcementHint');
|
||||
} else {
|
||||
this.exerciseRetryPending = true;
|
||||
this.exerciseRetryPendingSinceAttempts = this.vocabTrainerTotalAttempts;
|
||||
this.errorMessage = this.$t('socialnetwork.vocab.courses.exerciseReinforcementHint', {
|
||||
count: this.exerciseRetryUnlockAttempts
|
||||
});
|
||||
}
|
||||
this.activeTab = 'learn';
|
||||
this.showErrorDialog = true;
|
||||
this.$nextTick(() => {
|
||||
const scrollEl = document.querySelector('.app-content__scroll.contentscroll');
|
||||
if (scrollEl) {
|
||||
scrollEl.scrollTop = 0;
|
||||
} else {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob alle Übungen bestanden sind (mit Verzögerung, um mehrfache Aufrufe zu vermeiden)
|
||||
this.$nextTick(() => {
|
||||
this.checkLessonCompletion();
|
||||
@@ -1727,29 +1904,25 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob alle Übungen korrekt beantwortet wurden
|
||||
const allCompleted = allExercises.every(exercise => {
|
||||
const result = this.exerciseResults[exercise.id];
|
||||
return result && result.correct;
|
||||
});
|
||||
const answeredExercises = allExercises.filter((exercise) => Boolean(this.exerciseResults[exercise.id])).length;
|
||||
const correctExercises = allExercises.filter((exercise) => this.exerciseResults[exercise.id]?.correct).length;
|
||||
const score = Math.round((correctExercises / allExercises.length) * 100);
|
||||
const passed = answeredExercises === allExercises.length
|
||||
&& score >= this.exerciseTargetScore
|
||||
&& !this.exerciseNeedsReinforcement;
|
||||
|
||||
debugLog('[VocabLessonView] checkLessonCompletion - allCompleted:', allCompleted, 'Übungen:', allExercises.length, 'Korrekt:', allExercises.filter(ex => this.exerciseResults[ex.id]?.correct).length);
|
||||
debugLog('[VocabLessonView] checkLessonCompletion - passed:', passed, 'Beantwortet:', answeredExercises, 'Übungen:', allExercises.length, 'Korrekt:', correctExercises, 'Score:', score);
|
||||
|
||||
if (allCompleted && !this.isCheckingLessonCompletion) {
|
||||
if (passed && !this.isCheckingLessonCompletion) {
|
||||
this.isCheckingLessonCompletion = true;
|
||||
debugLog('[VocabLessonView] Alle Übungen abgeschlossen - starte Fortschritts-Update');
|
||||
|
||||
try {
|
||||
// Berechne Gesamt-Score
|
||||
const totalExercises = allExercises.length;
|
||||
const correctExercises = allExercises.filter(ex => this.exerciseResults[ex.id]?.correct).length;
|
||||
const score = Math.round((correctExercises / totalExercises) * 100);
|
||||
|
||||
debugLog('[VocabLessonView] Score berechnet:', score, '%');
|
||||
|
||||
// Aktualisiere Fortschritt
|
||||
await apiClient.put(`/api/vocab/lessons/${this.lessonId}/progress`, {
|
||||
completed: true,
|
||||
completed: score >= this.exerciseTargetScore,
|
||||
score: score,
|
||||
timeSpentMinutes: 0 // TODO: Zeit tracken
|
||||
});
|
||||
@@ -2407,9 +2580,9 @@ export default {
|
||||
.lesson-overview-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
gap: 16px;
|
||||
padding: 18px 18px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: linear-gradient(135deg, #fff8eb 0%, #f7efe2 100%);
|
||||
border: 1px solid rgba(160, 120, 40, 0.18);
|
||||
border-radius: 12px;
|
||||
@@ -2544,7 +2717,9 @@ export default {
|
||||
}
|
||||
|
||||
.vocab-prep-pass {
|
||||
margin-bottom: 18px;
|
||||
margin-bottom: 14px;
|
||||
background: linear-gradient(180deg, #fffefd 0%, #fff7ec 100%);
|
||||
border: 1px solid rgba(210, 131, 31, 0.18);
|
||||
}
|
||||
|
||||
.vocab-prep-pass__step {
|
||||
@@ -2584,6 +2759,40 @@ export default {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lesson-primary-flow {
|
||||
margin-top: 18px;
|
||||
padding: 18px;
|
||||
border: 1px solid rgba(210, 131, 31, 0.2);
|
||||
background: linear-gradient(180deg, rgba(255, 251, 245, 0.98), rgba(255, 246, 233, 0.94));
|
||||
box-shadow: 0 14px 30px rgba(194, 141, 61, 0.08);
|
||||
}
|
||||
|
||||
.lesson-primary-flow__header {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.lesson-primary-flow__eyebrow {
|
||||
display: inline-flex;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 162, 43, 0.16);
|
||||
color: #7a4b00;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.lesson-primary-flow__title {
|
||||
margin: 10px 0 6px;
|
||||
color: #2d2114;
|
||||
}
|
||||
|
||||
.lesson-primary-flow__intro {
|
||||
margin: 0;
|
||||
color: #6b5535;
|
||||
}
|
||||
|
||||
.vocab-trainer-locked-hint {
|
||||
margin: 0;
|
||||
color: #8a5a00;
|
||||
@@ -2593,6 +2802,8 @@ export default {
|
||||
.lesson-assistant-section {
|
||||
margin-top: 20px;
|
||||
padding: 16px 18px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid rgba(176, 176, 176, 0.2);
|
||||
}
|
||||
|
||||
.lesson-deepen-section .learn-grid,
|
||||
@@ -2661,12 +2872,158 @@ export default {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.exercise-flow-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
padding: 18px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid rgba(80, 118, 178, 0.16);
|
||||
background: linear-gradient(180deg, rgba(246, 250, 255, 0.98), rgba(236, 244, 255, 0.94));
|
||||
}
|
||||
|
||||
.exercise-flow-header h3 {
|
||||
margin: 8px 0 6px;
|
||||
}
|
||||
|
||||
.exercise-flow-header__eyebrow {
|
||||
display: inline-flex;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(58, 117, 196, 0.12);
|
||||
color: #27528f;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.exercise-flow-header__intro {
|
||||
margin: 0;
|
||||
color: #4e6280;
|
||||
}
|
||||
|
||||
.exercise-flow-header__hint {
|
||||
margin: 8px 0 0;
|
||||
color: #39506f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.exercise-flow-header__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(110px, 1fr));
|
||||
gap: 10px;
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.exercise-flow-stat {
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid rgba(80, 118, 178, 0.12);
|
||||
}
|
||||
|
||||
.exercise-flow-stat span {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: #60708b;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.exercise-flow-list {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.exercise-grammar-card {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid rgba(113, 94, 54, 0.18);
|
||||
background: linear-gradient(180deg, rgba(255, 250, 241, 0.98), rgba(255, 246, 229, 0.94));
|
||||
}
|
||||
|
||||
.exercise-grammar-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.exercise-grammar-card__list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.exercise-grammar-card__item {
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(113, 94, 54, 0.12);
|
||||
}
|
||||
|
||||
.exercise-grammar-card__item p {
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
.exercise-item {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
padding: 18px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.exercise-item__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.exercise-item__header h4 {
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.exercise-item__index {
|
||||
display: inline-flex;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(58, 117, 196, 0.08);
|
||||
color: #27528f;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.exercise-item__status {
|
||||
flex: 0 0 auto;
|
||||
padding: 5px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.exercise-item__status--open {
|
||||
background: rgba(96, 112, 139, 0.12);
|
||||
color: #55647d;
|
||||
}
|
||||
|
||||
.exercise-item__status--correct {
|
||||
background: rgba(40, 167, 69, 0.14);
|
||||
color: #1f7a35;
|
||||
}
|
||||
|
||||
.exercise-item__status--wrong {
|
||||
background: rgba(220, 53, 69, 0.12);
|
||||
color: #a12634;
|
||||
}
|
||||
|
||||
.exercise-item--correct {
|
||||
border-color: rgba(40, 167, 69, 0.28);
|
||||
}
|
||||
|
||||
.exercise-item--wrong {
|
||||
border-color: rgba(220, 53, 69, 0.18);
|
||||
}
|
||||
|
||||
.exercise-instruction {
|
||||
@@ -2942,10 +3299,11 @@ export default {
|
||||
|
||||
.vocab-trainer-section {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
padding: 18px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(210, 131, 31, 0.2);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 20px rgba(210, 131, 31, 0.08);
|
||||
}
|
||||
|
||||
.vocab-trainer-section h4 {
|
||||
@@ -3472,7 +3830,36 @@ export default {
|
||||
|
||||
.lesson-meta-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.learn-section {
|
||||
margin-top: 14px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.lesson-primary-flow,
|
||||
.lesson-deepen-section,
|
||||
.lesson-assistant-section {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.lesson-header {
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.exercise-flow-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.exercise-flow-header__stats {
|
||||
grid-template-columns: 1fr;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.exercise-item__header {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user