update
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user