This commit is contained in:
team3
2026-07-13 05:18:55 +02:00
parent 7ee425f7fa
commit 8926f87185
19 changed files with 703 additions and 41 deletions

View File

@@ -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-Daten (alle Kapitel/Level in ord):
// je Kapitel ein Intro, je Baustein Fundament + Abruf, am Ende ein Abschluss.
// Screen-Liste: je Kapitel ein Intro, je Baustein 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 >= screens.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' && panels.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>