@@ -6,12 +6,14 @@ import { render, lesestat } from '../markdown.js'
const props = defineProps ( { daten : Object , topic : String } )
const emit = defineEmits ( [ 'schliessen' ] )
const SCHWELLE = 10 // Anzeige; die Wahrheit entscheidet der Server (PRUEFUNG_SCHWELLE)
const wurzel = ref ( null )
const pos = ref ( 0 )
const fertig = ref ( new Set ( ) ) // baustein-ids mit abgeschlossenem Abruf
const fertig = ref ( new Set ( ) ) // baustein-ids mit bestandener Prüfung
const xpMap = ref ( { } ) // baustein-id → Saldo
// Flache Screen-Liste aus den Guide-Da ten (alle Kapitel/Level in ord):
// je Kapitel ein Intro, je Baustein Fundament + Abruf, am Ende ein Abschluss.
// Screen-Liste: je Kapitel ein Intro, je Baus tei n Fundament → Abruf → Prüfung.
const screens = computed ( ( ) => {
const out = [ ]
const kaps = props . daten ? . kapitel || [ ]
@@ -20,6 +22,7 @@ const screens = computed(() => {
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 : 'pruefung' , baustein : s . baustein , titel : s . titel } )
}
} )
out . push ( { typ : 'ende' } )
@@ -45,26 +48,20 @@ function fundamentHtml(s) {
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 >= screen s. value . length - 1 ) return
if ( screen . value . typ === 'abruf' ) markFertig ( screen . value . baustein )
pos . value ++
}
// ── Navigation + Prüfungs-Gate ─────────────────────────────────────────────────
// Lesen + Abruf sind frei. Nur die Prüfung sperrt: weiter erst ab Saldo ≥ Schwelle.
// 0 Panels sperren NICHT (leerer Baustein darf den Guide nie blockieren ).
const gesperrt = computed ( ( ) =>
screen . value . typ === 'pruefung' && panel s. value . length > 0
&& ! fertig . value . has ( screen . value . baustein ) )
function vor ( ) { if ( ! gesperrt . value && pos . value < screens . value . length - 1 ) pos . value ++ }
function zurueck ( ) { if ( pos . value > 0 ) pos . value -- }
function taste ( e ) {
if ( e . key === 'ArrowRight' ) vor ( )
else if ( e . key === 'ArrowLeft' ) zurueck ( )
}
async function markFertig ( bausteinId ) {
if ( fertig . value . has ( bausteinId ) ) return
fertig . value = new Set ( [ ... fertig . value , bausteinId ] )
await api . bausteinFertig ( props . topic , bausteinId ) . catch ( ( ) => { } )
}
// ── Abrufprüfung (Karteikarten, Loop wie Ueben.vue) ─────────────────────────────
// ── Abrufprüfung (Karteikarten) ─────────────────── ─────────────────────────────
const stapel = ref ( [ ] )
const zeigeAntwort = ref ( false )
const sende = ref ( false )
@@ -81,26 +78,73 @@ async function antworten(richtig) {
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 }
}
// Beim Betreten eines Abruf-Screens die Karten des Bausteins laden.
// ── Prüfung (4er-Panels, exakte Teilmenge ankreuzen) ───────────────────────────
const panels = ref ( [ ] )
const panelIdx = ref ( 0 )
const auswahl = ref ( new Set ( ) )
const aufloesung = ref ( null ) // nach dem Prüfen: [{id, wahr, erklaerung}]
const warRichtig = ref ( false )
const saldo = ref ( 0 )
const panel = computed ( ( ) => panels . value [ panelIdx . value ] ? . panel || [ ] )
async function pruefungLaden ( bausteinId ) {
panels . value = await api . pruefung ( props . topic , bausteinId )
panelIdx . value = 0 ; auswahl . value = new Set ( ) ; aufloesung . value = null
saldo . value = xpMap . value [ bausteinId ] || 0
if ( ! panels . value . length && ! fertig . value . has ( bausteinId ) ) {
// keine Aussagen generiert → Baustein nicht blockieren, als erledigt werten
fertig . value = new Set ( [ ... fertig . value , bausteinId ] )
await api . bausteinFertig ( props . topic , bausteinId ) . catch ( ( ) => { } )
}
}
function toggle ( id ) {
if ( aufloesung . value ) return
const s = new Set ( auswahl . value )
s . has ( id ) ? s . delete ( id ) : s . add ( id )
auswahl . value = s
}
async function pruefen ( ) {
if ( aufloesung . value || sende . value ) return
sende . value = true
try {
const ids = panel . value . map ( x => x . id )
const r = await api . pruefungAntwort ( props . topic , ids , [ ... auswahl . value ] )
aufloesung . value = r . aufloesung
warRichtig . value = r . richtig
saldo . value = r . xp
xpMap . value = { ... xpMap . value , [ screen . value . baustein ] : r . xp }
if ( r . fertig ) fertig . value = new Set ( [ ... fertig . value , screen . value . baustein ] )
} finally { sende . value = false }
}
async function naechstesPanel ( ) {
aufloesung . value = null ; auswahl . value = new Set ( )
if ( panelIdx . value < panels . value . length - 1 ) panelIdx . value ++
else await pruefungLaden ( screen . value . baustein ) // neu mischen, Nachschub
}
function urteil ( id ) {
const a = ( aufloesung . value || [ ] ) . find ( x => x . id === id )
return a ? a . wahr : null
}
watch ( screen , ( s ) => {
if ( s . typ === 'abruf' ) abrufLaden ( s . baustein )
else if ( s . typ === 'pruefung' ) pruefungLaden ( s . baustein )
} )
// ── Fullscreen + Resume ─────────────────────────────────────────────────────────
// ── 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 */ }
try { await wurzel . value ? . requestFullscreen ( ) } catch { /* CSS-Overlay reicht */ }
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 erledigten Bausteins springen.
xpMap . value = Object . fromEntries ( stand . map ( s => [ s . baustein _id , s . xp ] ) )
const idx = screens . value . findIndex ( s => s . typ === 'fundament' && ! fertig . value . has ( s . baustein ) )
pos . value = idx >= 0 ? idx : screens . value . length - 1
} )
@@ -151,6 +195,32 @@ onUnmounted(() => {
< div v-else class = "lm-bestanden" > ✓ Abruf durch — weiter mit → < / div >
< / section >
< section v-else-if = "screen.typ === 'pruefung'" class="lm-screen lm-pruefung" >
< div class = "lm-label" > Prüfung · { { screen . titel } } · Saldo { { saldo } } / { { SCHWELLE } } < / div >
< template v-if = "panel.length" >
< h3 class = "pf-frage" > Was gilt ? < span class = "pf-hinweis" > ( 0 – { { panel . length } } Aussagen können wahr sein ) < / span > < / h3 >
< div class = "pf-panel" >
< label v-for = "x in panel" :key="x.id" class="pf-aussage"
: class = "{ gewaehlt: auswahl.has(x.id),
'pf-wahr': urteil(x.id) === true,
'pf-falsch2': urteil(x.id) === false }"
@click.prevent ="toggle(x.id)" >
< input type = "checkbox" :checked = "auswahl.has(x.id)" @click.stop ="toggle(x.id)" / >
< span class = "markdown" v-html = "render(x.text)" > < / span >
< / label >
< / div >
< div v-if = "aufloesung" class="pf-feedback" :class="warRichtig ? 'pf-fb-ok' : 'pf-fb-bad'" >
{{ warRichtig ? ' ✓ Panel korrekt ( + 1 ) ' : ' ✗ Nicht exakt ( − 1 ) ' }}
< / div >
< div class = "pf-akt" >
< button v-if = "!aufloesung" @click="pruefen" > Prüfen < / button >
< button v-else @click ="naechstesPanel" > Nächstes Panel → < / button >
< span v-if = "fertig.has(screen.baustein)" class="lm-bestanden" style="margin: 0" > ✓ bestanden < / span >
< / div >
< / template >
< div v-else class = "lm-bestanden" > Keine Prüfungs - Aussagen für diesen Baustein — weiter mit → < / div >
< / section >
< section v-else class = "lm-screen lm-ende" >
< h1 > Fundament abgeschlossen 🎉 < / h1 >
< p class = "lm-introtext" > Schritt 2 ( Anwendung ) und Schritt 3 ( eigene Aufgaben ) folgen . < / p >
@@ -159,7 +229,9 @@ onUnmounted(() => {
< footer class = "lm-fuss" >
< button : disabled = "pos === 0" @click ="zurueck" > ← Zurück < / button >
< button : disabled = "pos >= screens.length - 1" @click ="vor" > Weiter → < / button >
< button : disabled = "gesperrt || pos >= screens.length - 1" @click ="vor" >
{{ gesperrt ? ` Saldo $ { saldo } / $ { SCHWELLE } — weiter prüfen ` : ' Weiter → ' }}
< / button >
< / footer >
< / div >
< / template >