This commit is contained in:
team3
2026-07-12 22:41:27 +02:00
parent a9b2f5ab9e
commit 8cedcc87ec
7 changed files with 289 additions and 0 deletions

View File

@@ -2,8 +2,10 @@
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { api } from '../api.js'
import { render, lesestat } from '../markdown.js'
import Lesemodus from './Lesemodus.vue'
const props = defineProps({ topic: String, state: Object })
const lesemodus = ref(false) // Vollbild-Lernmodus (Fundament + Abrufprüfung)
const daten = ref(null)
const level = ref('E') // Durchgang-Wahl: jede Stufe ist ein eigener Lese-Durchgang
const ansicht = ref('erklaerend') // erklaerend = Fließtext | kompakt = Stichpunkte
@@ -156,6 +158,8 @@ function onScroll(e) {
</button>
<button class="schalter-solo" title="Fokus-Modus (ablenkungsfrei lesen)"
@click="fokus = true"></button>
<button v-if="daten" class="schalter-solo lm-start" title="Lernmodus (Vollbild)"
@click="lesemodus = true"></button>
</div>
<div class="fortschritt">
<div>Kapitel {{ aktiv + 1 }} / {{ anzeige.length }}</div>
@@ -191,5 +195,6 @@ function onScroll(e) {
<div v-if="!anzeige.length" class="guide-section">Noch kein Guide generiert.</div>
</template>
</div>
<Lesemodus v-if="lesemodus" :daten="daten" :topic="topic" @schliessen="lesemodus = false" />
</div>
</template>

View File

@@ -0,0 +1,166 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { api } from '../api.js'
import { render, lesestat } from '../markdown.js'
const props = defineProps({ daten: Object, topic: String })
const emit = defineEmits(['schliessen'])
const wurzel = ref(null)
const pos = ref(0)
const fertig = ref(new Set()) // baustein-ids mit bestandener Abrufprüfung
// Flache Screen-Liste aus den Guide-Daten (alle Kapitel/Level in ord):
// je Kapitel ein Intro, je Baustein Fundament + Abruf, am Ende ein Abschluss.
const screens = computed(() => {
const out = []
const kaps = props.daten?.kapitel || []
kaps.forEach((kap, ki) => {
out.push({ typ: 'intro', kap, nr: ki + 1, von: kaps.length })
for (const s of kap.sections) {
out.push({ typ: 'fundament', baustein: s.baustein, titel: s.titel, lang: s.lang })
out.push({ typ: 'abruf', baustein: s.baustein, titel: s.titel })
}
})
out.push({ typ: 'ende' })
return out
})
const screen = computed(() => screens.value[pos.value] || { typ: 'ende' })
const bausteinAnzahl = computed(() =>
(props.daten?.kapitel || []).reduce((a, k) => a + k.sections.length, 0))
const fortschrittText = computed(() => `${fertig.value.size} / ${bausteinAnzahl.value} Bausteine`)
// ── Stats (wie Guide.vue: 100 Wörter/min + 5 s je Display-Formel) ──────────────
function zeit(min) { return !min ? '~0 min' : min < 1 ? '<1 min' : `~${Math.round(min)} min` }
function kapStat(kap) {
let w = 0, f = 0
for (const s of kap.sections) { const t = lesestat(s.lang || ''); w += t.woerter; f += t.displayFormeln }
return { woerter: w, minuten: w / 100 + f * 5 / 60 }
}
const htmlCache = new Map()
function fundamentHtml(s) {
if (!htmlCache.has(s.baustein)) htmlCache.set(s.baustein, render(s.lang))
return htmlCache.get(s.baustein)
}
// ── Navigation ───────────────────────────────────────────────────────────────
// Kein Zwang, alle Karten zu sehen: Weiter ist immer frei. Verlässt man einen
// Abruf-Screen vorwärts, gilt der Baustein als erledigt (Fortschritt persistiert).
function vor() {
if (pos.value >= screens.value.length - 1) return
if (screen.value.typ === 'abruf') markFertig(screen.value.baustein)
pos.value++
}
function zurueck() { if (pos.value > 0) pos.value-- }
function taste(e) {
if (e.key === 'ArrowRight') vor()
else if (e.key === 'ArrowLeft') zurueck()
}
// ── Abrufprüfung (Karteikarten, Loop wie Ueben.vue) ─────────────────────────────
const stapel = ref([])
const zeigeAntwort = ref(false)
const sende = ref(false)
async function abrufLaden(bausteinId) {
stapel.value = await api.bausteinKarten(props.topic, bausteinId)
zeigeAntwort.value = false
if (!stapel.value.length) markFertig(bausteinId) // keine Karten → sofort bestanden
}
async function antworten(richtig) {
if (sende.value || !stapel.value.length) return
sende.value = true
try {
const karte = stapel.value[0]
await api.antwort(karte.id, richtig)
stapel.value = richtig ? stapel.value.slice(1) : [...stapel.value.slice(1), karte]
zeigeAntwort.value = false
if (!stapel.value.length) markFertig(screen.value.baustein)
} finally { sende.value = false }
}
async function markFertig(bausteinId) {
if (fertig.value.has(bausteinId)) return
fertig.value = new Set([...fertig.value, bausteinId])
await api.bausteinFertig(props.topic, bausteinId).catch(() => {})
}
// Beim Betreten eines Abruf-Screens die Karten des Bausteins laden.
watch(screen, (s) => {
if (s.typ === 'abruf' && !fertig.value.has(s.baustein)) abrufLaden(s.baustein)
})
// ── Fullscreen + Resume ─────────────────────────────────────────────────────────
function schliessen() {
if (document.fullscreenElement) document.exitFullscreen().catch(() => {})
emit('schliessen')
}
onMounted(async () => {
window.addEventListener('keydown', taste)
try { await wurzel.value?.requestFullscreen() } catch { /* CSS-Overlay reicht als Fallback */ }
const stand = await api.lernstand(props.topic).catch(() => [])
fertig.value = new Set(stand.filter(s => s.status === 'fertig').map(s => s.baustein_id))
// Resume: zum Fundament des ersten noch nicht bestandenen Bausteins springen.
const idx = screens.value.findIndex(s => s.typ === 'fundament' && !fertig.value.has(s.baustein))
pos.value = idx >= 0 ? idx : screens.value.length - 1
})
onUnmounted(() => {
window.removeEventListener('keydown', taste)
if (document.fullscreenElement) document.exitFullscreen().catch(() => {})
})
</script>
<template>
<div ref="wurzel" class="lesemodus">
<header class="lm-kopf">
<span class="lm-fort">{{ fortschrittText }}</span>
<button class="lm-x" title="Schließen" @click="schliessen"></button>
</header>
<main class="lm-buehne">
<section v-if="screen.typ === 'intro'" class="lm-screen lm-intro">
<div class="lm-kapnr">Kapitel {{ screen.nr }} / {{ screen.von }}</div>
<h1>{{ screen.kap.titel }}</h1>
<p v-if="screen.kap.intro" class="lm-introtext">{{ screen.kap.intro }}</p>
<div class="lm-stats">
{{ kapStat(screen.kap).woerter.toLocaleString('de-DE') }} Wörter ·
{{ zeit(kapStat(screen.kap).minuten) }} · {{ screen.kap.sections.length }} Bausteine
</div>
</section>
<section v-else-if="screen.typ === 'fundament'" class="lm-screen">
<div class="lm-label">Fundament</div>
<h2 class="lm-titel">{{ screen.titel }}</h2>
<div class="markdown" v-html="fundamentHtml(screen)"></div>
</section>
<section v-else-if="screen.typ === 'abruf'" class="lm-screen lm-abruf">
<div class="lm-label">Abrufprüfung · {{ screen.titel }}</div>
<div v-if="stapel.length" class="lm-karte">
<div class="lm-frage markdown" v-html="render(stapel[0].inhalt.frage)"></div>
<div v-if="zeigeAntwort" class="lm-antwort markdown" v-html="render(stapel[0].inhalt.antwort)"></div>
<div class="lm-karte-akt">
<button v-if="!zeigeAntwort" @click="zeigeAntwort = true">Antwort zeigen</button>
<template v-else>
<button class="lm-falsch" @click="antworten(false)">Falsch</button>
<button class="lm-richtig" @click="antworten(true)">Richtig</button>
</template>
</div>
<div class="lm-rest">noch {{ stapel.length }} · Box {{ stapel[0].box }}</div>
</div>
<div v-else class="lm-bestanden"> Abruf bestanden weiter mit </div>
</section>
<section v-else class="lm-screen lm-ende">
<h1>Fundament abgeschlossen 🎉</h1>
<p class="lm-introtext">Schritt 2 (Fehlersuche) und Schritt 3 (Aufgaben) folgen.</p>
</section>
</main>
<footer class="lm-fuss">
<button :disabled="pos === 0" @click="zurueck"> Zurück</button>
<button :disabled="pos >= screens.length - 1" @click="vor">Weiter </button>
</footer>
</div>
</template>