feat(i18n, vocab): enhance localization and vocabulary preparation in VocabLessonView
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:
Torsten Schulz (local)
2026-04-01 10:21:13 +02:00
parent 8bbfd46ada
commit b8e3732ef8
4 changed files with 614 additions and 179 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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>