This commit is contained in:
team3
2026-06-24 07:19:01 +02:00
parent 9e888c216a
commit 12125d0f2d
2 changed files with 60 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed, ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { computed, reactive, ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { fetchGuideContent, chatGuide, fetchBausteinLernstand } from '../api.js'
import { renderMarkdown } from '../markdown.js'
import { useChat } from '../composables/useChat.js'
@@ -27,12 +27,47 @@ const loadError = ref(null)
const scrollEl = ref(null)
const lernstand = ref({}) // Prüfungs-Stand pro Baustein-Titel — VOR dem immediate-Watch (loadContent nutzt es)
// --- Lazy-Render + Markdown-Cache: nur sichtbare Sections parsen, jede nur einmal.
// Behebt das Blockieren beim Öffnen (160× marked/highlight.js) und Re-Parse bei jedem Update. ---
const mdCache = new Map() // `${modus}:${num}` → html
const sichtbar = reactive({}) // num → true (bleibt true, sobald je sichtbar)
let mdObserver = null
function htmlFor(s) {
const key = `${props.ansichtModus}:${s.num}`
let h = mdCache.get(key)
if (h === undefined) {
h = renderMarkdown(props.ansichtModus === 'kompakt' ? (s.kompakt || s.md) : s.md)
mdCache.set(key, h)
}
return h
}
// Sections rendern erst, wenn sie (fast) im Viewport sind — Observer auf den Scroll-Container.
function setupLazy() {
mdObserver?.disconnect()
if (!scrollEl.value) return
mdObserver = new IntersectionObserver((entries) => {
for (const e of entries) {
if (!e.isIntersecting) continue
sichtbar[Number(e.target.dataset.num)] = true
mdObserver.unobserve(e.target)
}
}, { root: scrollEl.value, rootMargin: '800px 0px' })
for (const el of scrollEl.value.querySelectorAll('.section-card')) mdObserver.observe(el)
}
watch(content, () => nextTick(setupLazy))
onUnmounted(() => mdObserver?.disconnect())
watch(() => props.previewGuide?.id, loadContent, { immediate: true })
async function loadContent() {
content.value = null
loadError.value = null
lernstand.value = {}
mdCache.clear()
for (const k in sichtbar) delete sichtbar[k]
const g = props.previewGuide
if (!g || g.status !== 'done') return
try {
@@ -198,6 +233,7 @@ function extractContext() {
<article
v-for="s in ch.sections"
:key="s.num"
:data-num="s.num"
:class="['section-card', lernstand[s.title]?.gemeistert ? 'gemeistert' : (lernstand[s.title]?.verstanden ? 'verstanden' : (lernstand[s.title]?.absolviert ? 'absolviert' : ''))]"
>
<h3 :class="{ 'baustein-klick': istPruefbar(s) }" @click="istPruefbar(s) && openFokus(s, 'pruefung')">
@@ -208,7 +244,8 @@ function extractContext() {
<span v-else-if="lernstand[s.title]?.absolviert" class="baustein-done" title="Prüfung bestanden"> Absolviert</span>
</template>
</h3>
<div class="section-body markdown" v-html="renderMarkdown(ansichtModus === 'kompakt' ? (s.kompakt || s.md) : s.md)"></div>
<div v-if="sichtbar[s.num]" class="section-body markdown" v-html="htmlFor(s)"></div>
<div v-else class="section-body skeleton"></div>
<BausteinPanel
v-if="istPruefbar(s)"
mode="trigger"
@@ -444,6 +481,10 @@ function extractContext() {
.section-card h3.baustein-klick { cursor: pointer; width: fit-content; }
.section-card h3.baustein-klick:hover { color: var(--accent); }
/* Platzhalter für noch nicht gerenderte Sections (Lazy-Render). Stabile Höhe,
damit der IntersectionObserver die folgenden Karten gestaffelt erkennt. */
.section-body.skeleton { min-height: 160px; }
.empty-preview {
display: flex;
align-items: center;