update
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import { api } from '../api.js'
|
||||
import { render, gefiltert, lesestat, renderGestuft } from '../markdown.js'
|
||||
import { render, lesestat } from '../markdown.js'
|
||||
|
||||
const props = defineProps({ topic: String, state: Object })
|
||||
const daten = ref(null)
|
||||
const level = ref('E') // Default: Einstieg — Umfang wächst per Toggle (E→M→S)
|
||||
const level = ref('E') // Durchgang-Wahl: jede Stufe ist ein eigener Lese-Durchgang
|
||||
const ansicht = ref('erklaerend') // erklaerend = Fließtext | kompakt = Stichpunkte
|
||||
const LEVEL_ICON = { E: '●○○', M: '●●○', S: '●●●' }
|
||||
const LEVEL_NAME = { E: 'Umfang: Grundgerüst', M: 'Umfang: + Standardstoff', S: 'Umfang: alles' }
|
||||
const LEVEL_NAME = { E: 'Durchgang 1: Grundgerüst', M: 'Durchgang 2: Standardstoff',
|
||||
S: 'Durchgang 3: Feinheiten' }
|
||||
const fokus = ref(false) // Fokus: Vollbild — App-Chrome + Navigation weg, Karten aufgelöst
|
||||
watch(fokus, (f) => document.body.classList.toggle('vollbild', f))
|
||||
onUnmounted(() => document.body.classList.remove('vollbild'))
|
||||
@@ -28,10 +29,9 @@ const htmlCache = new Map()
|
||||
watch(daten, () => htmlCache.clear())
|
||||
|
||||
function html(s) {
|
||||
const key = `${ansicht.value}|${level.value}|${s.baustein}`
|
||||
const key = `${ansicht.value}|${s.baustein}`
|
||||
if (!htmlCache.has(key)) {
|
||||
htmlCache.set(key, ansicht.value === 'kompakt'
|
||||
? render(s.kompakt || s.lang) : renderGestuft(s.lang, level.value))
|
||||
htmlCache.set(key, render(ansicht.value === 'kompakt' ? s.kompakt : s.lang))
|
||||
}
|
||||
return htmlCache.get(key)
|
||||
}
|
||||
@@ -47,23 +47,29 @@ function hoehe(i) {
|
||||
|
||||
function setAnsicht(a) {
|
||||
ansicht.value = a
|
||||
laden()
|
||||
}
|
||||
|
||||
// Nur Durchgänge anbieten, die es in diesem Thema gibt (ohne E- oder
|
||||
// S-Bausteine überspringt der Toggle die Stufe)
|
||||
const levels = computed(() => {
|
||||
const da = new Set((daten.value?.kapitel || []).map((k) => k.level))
|
||||
const folge = ['E', 'M', 'S'].filter((l) => da.has(l))
|
||||
return folge.length ? folge : ['E']
|
||||
})
|
||||
watch(levels, (l) => { if (!l.includes(level.value)) level.value = l[0] })
|
||||
|
||||
function levelToggle() {
|
||||
const folge = ['E', 'M', 'S']
|
||||
level.value = folge[(folge.indexOf(level.value) + 1) % 3]
|
||||
laden()
|
||||
const folge = levels.value
|
||||
level.value = folge[(folge.indexOf(level.value) + 1) % folge.length]
|
||||
}
|
||||
|
||||
// Nur Kapitel/Bausteine mit Inhalt im gewählten Umfang anzeigen — der
|
||||
// Level-Filter ließe sonst leere Titel-Hüllen stehen (36/66 Bausteine ohne E)
|
||||
// Der gewählte Durchgang: nur seine Kapitel, nur Sections mit Inhalt
|
||||
const anzeige = computed(() => {
|
||||
const out = []
|
||||
for (const kap of daten.value?.kapitel || []) {
|
||||
const sections = kap.sections.filter((s) => ansicht.value === 'kompakt'
|
||||
? (s.kompakt || s.lang || '').trim()
|
||||
: lesestat(gefiltert(s.lang, level.value)).woerter >= 10)
|
||||
if (kap.level !== level.value) continue
|
||||
const sections = kap.sections.filter((s) =>
|
||||
((ansicht.value === 'kompakt' ? s.kompakt : s.lang) || '').trim())
|
||||
if (sections.length) out.push({ ...kap, sections })
|
||||
}
|
||||
return out
|
||||
@@ -77,8 +83,7 @@ const stats = computed(() => {
|
||||
let woerter = ansicht.value === 'kompakt' ? 0 : lesestat(kap.intro).woerter
|
||||
let formeln = 0
|
||||
for (const s of kap.sections) {
|
||||
const st = lesestat(ansicht.value === 'kompakt' ? (s.kompakt || s.lang)
|
||||
: gefiltert(s.lang, level.value))
|
||||
const st = lesestat(ansicht.value === 'kompakt' ? s.kompakt : s.lang)
|
||||
woerter += st.woerter
|
||||
formeln += st.displayFormeln
|
||||
}
|
||||
@@ -156,12 +161,23 @@ function onScroll(e) {
|
||||
<div v-for="(kap, i) in anzeige" :key="kap.kapitel_id" class="kapitel"
|
||||
:ref="(el) => setKapEl(i, el)">
|
||||
<h2 class="kapitel-titel">{{ i + 1 }}. {{ kap.titel }}</h2>
|
||||
<div class="kapitel-meta">
|
||||
{{ (stats.je[i]?.woerter || 0).toLocaleString('de-DE') }} Wörter ·
|
||||
{{ zeit(stats.je[i]?.minuten || 0) }}
|
||||
</div>
|
||||
<template v-if="sichtbar(i)">
|
||||
<p v-if="kap.intro && ansicht === 'erklaerend'"
|
||||
style="color: var(--dim); margin-top: 0">{{ kap.intro }}</p>
|
||||
<div v-for="s in kap.sections" :key="s.baustein" class="guide-section">
|
||||
<h3 style="margin-top: 0">{{ s.titel }}</h3>
|
||||
<div :class="ansicht === 'kompakt' ? 'kompakt' : 'markdown'" v-html="html(s)"></div>
|
||||
<details v-if="ansicht === 'erklaerend' && s.fragen?.length" class="antworten">
|
||||
<summary>Antworten zu „Prüfe dich"</summary>
|
||||
<div v-for="(f, fi) in s.fragen" :key="fi" class="antwort">
|
||||
<div class="frage">{{ f.frage }}</div>
|
||||
<div v-html="render(f.antwort)"></div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="kapitel-platzhalter" :style="{ height: hoehe(i) + 'px' }"></div>
|
||||
|
||||
@@ -48,8 +48,10 @@ export function render(text) {
|
||||
return DOMPurify.sanitize(marked.parse(mathe(text || '')))
|
||||
}
|
||||
|
||||
const RANG = { E: 0, M: 1, S: 2 }
|
||||
const MARKER = /<!--\s*atom:\s*(\d+)\s*\|\s*([^|]*)\|\s*([EMS])\s*-->/g
|
||||
// Atom-Marker sind eine unsichtbare Backend-Schnittstelle — vor der Anzeige
|
||||
// ausblenden. Die Stufen-Trennung passiert seit dem Umbau über getrennte
|
||||
// Bausteine je Level, nicht mehr über Marker-Filterung im Text.
|
||||
const MARKER = /<!--\s*atom:[^>]*-->/g
|
||||
|
||||
// Wortzahl + Display-Formeln des sichtbaren Texts (für die Lesezeit-Schätzung)
|
||||
export function lesestat(text) {
|
||||
@@ -61,47 +63,3 @@ export function lesestat(text) {
|
||||
const woerter = t.split(/\s+/).filter(Boolean).length
|
||||
return { woerter, displayFormeln }
|
||||
}
|
||||
|
||||
// Text an Atom-Markern in Segmente schneiden; Filter zeigt Atome bis zum gewählten Level.
|
||||
export function segmente(lang) {
|
||||
const out = []
|
||||
let letzt = 0
|
||||
let aktuell = { level: null, titel: '', text: '' }
|
||||
for (const m of lang.matchAll(MARKER)) {
|
||||
aktuell.text = lang.slice(letzt, m.index)
|
||||
out.push(aktuell)
|
||||
aktuell = { level: m[3], titel: m[2].trim(), text: '' }
|
||||
letzt = m.index + m[0].length
|
||||
}
|
||||
aktuell.text = lang.slice(letzt)
|
||||
out.push(aktuell)
|
||||
return out
|
||||
}
|
||||
|
||||
export function gefiltert(lang, maxLevel) {
|
||||
if (!maxLevel || maxLevel === 'S') return lang
|
||||
return segmente(lang)
|
||||
.filter((s) => s.level === null || RANG[s.level] <= RANG[maxLevel])
|
||||
.map((s) => s.text)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
// Gestuftes Rendern: Segmente über dem Level fallen weg, Segmente DARUNTER
|
||||
// werden gedimmt (schon gelernt) — der Blick geht auf die neuen Inhalte.
|
||||
// Kernpunkte/Prüfe-dich (nach dem letzten Atom) bleiben immer ungedimmt.
|
||||
export function renderGestuft(lang, maxLevel) {
|
||||
if (!maxLevel || maxLevel === 'E') return render(gefiltert(lang, maxLevel))
|
||||
const seg = segmente(lang)
|
||||
const letzt = seg[seg.length - 1]
|
||||
const i = letzt.text.indexOf('**Kernpunkte:**')
|
||||
if (i >= 0) {
|
||||
seg.push({ level: null, titel: '', text: letzt.text.slice(i) })
|
||||
letzt.text = letzt.text.slice(0, i)
|
||||
}
|
||||
return seg.map((s) => {
|
||||
if (s.level !== null && RANG[s.level] > RANG[maxLevel]) return ''
|
||||
const html = render(s.text)
|
||||
const gelernt = s.level !== null && RANG[s.level] < RANG[maxLevel]
|
||||
return gelernt ? `<div class="gelernt">${html}</div>` : html
|
||||
}).join('')
|
||||
}
|
||||
|
||||
@@ -203,9 +203,13 @@ body.nav-zu .hauptbereich { padding-top: 38px; }
|
||||
}
|
||||
.fokus-aus:hover { opacity: 1; }
|
||||
.seite.guide-text::-webkit-scrollbar { display: none; }
|
||||
.markdown .gelernt { opacity: 0.45; } /* niedrigere Level = schon gelernt */
|
||||
.guide-text .kapitel { scroll-margin-top: 8px; }
|
||||
.guide-text .kapitel-titel { margin: 30px 0 6px; }
|
||||
.guide-text .kapitel-titel { margin: 30px 0 2px; }
|
||||
.guide-text .kapitel-meta { color: var(--dim); font-size: 13px; margin: 0 0 10px; }
|
||||
.guide-section .antworten { margin-top: 10px; border-top: 1px solid var(--rand); padding-top: 8px; }
|
||||
.guide-section .antworten summary { cursor: pointer; color: var(--dim); font-size: 14px; }
|
||||
.guide-section .antworten .antwort { margin: 10px 0; }
|
||||
.guide-section .antworten .frage { font-weight: 600; font-size: 14px; margin-bottom: 2px; }
|
||||
.kapitel-platzhalter { border-left: 2px dashed var(--rand); margin: 8px 0 8px 4px; }
|
||||
/* Kompakt-Ansicht: gleiche Lesetypografie wie der Fließtext, kein Grau-Kasten */
|
||||
.guide-text .kompakt {
|
||||
|
||||
Reference in New Issue
Block a user