feat(vocab): add hard status management for SRS items and update related logic

This commit is contained in:
Torsten Schulz (local)
2026-08-20 15:17:11 +02:00
parent 2c0944ae2c
commit 3a0945734f
10 changed files with 181 additions and 263 deletions

View File

@@ -46,13 +46,6 @@
<strong>{{ entry.learning }}</strong>
<span>{{ entry.reference }}</span>
</div>
<button
type="button"
class="btn-delete"
@click="removeHardVocabEntry(entry.key)"
>
{{ $t('socialnetwork.vocab.courses.unmarkVocabHard') }}
</button>
</div>
</div>
</section>
@@ -85,7 +78,7 @@
<p>{{ $t('socialnetwork.vocab.courses.courseFlowIntro') }}</p>
</div>
<div class="course-flow__stats">
<span class="course-flow__stat">{{ $t('socialnetwork.vocab.courses.srsDueStat', { count: srsDueCount }) }}</span>
<span class="course-flow__stat">{{ $t('socialnetwork.vocab.courses.srsDueStat', { scheduled: srsDailyCount, total: srsDueCount }) }}</span>
<span class="course-flow__stat">{{ $t('socialnetwork.vocab.courses.courseFlowReviewStat', { count: dueReviewLessons.length }) }}</span>
<span class="course-flow__stat">{{ $t('socialnetwork.vocab.courses.courseFlowBlockStat', { block: currentBlockNumber || '—' }) }}</span>
</div>
@@ -94,7 +87,7 @@
<div v-if="srsDueCount > 0" class="course-srs-plan">
<div>
<span class="course-srs-plan__eyebrow">{{ $t('socialnetwork.vocab.courses.srsEyebrow') }}</span>
<h4>{{ $t('socialnetwork.vocab.courses.srsTitle', { count: srsDueCount }) }}</h4>
<h4>{{ $t('socialnetwork.vocab.courses.srsTitle', { scheduled: srsDailyCount, total: srsDueCount }) }}</h4>
<p>{{ $t('socialnetwork.vocab.courses.srsIntro') }}</p>
</div>
<button type="button" class="course-today-plan__action" :disabled="srsLoading" @click="openSrsPractice">
@@ -378,6 +371,7 @@ export default {
chapters: [],
srsDueItems: [],
srsDueTotal: 0,
srsDailyLimit: 50,
srsLoading: false,
showAddLessonDialog: false,
assistantSettings: null,
@@ -427,6 +421,9 @@ export default {
}
return Array.isArray(this.srsDueItems) ? this.srsDueItems.length : 0;
},
srsDailyCount() {
return Math.min(this.srsDueCount, this.srsDailyLimit);
},
hardVocabCount() {
return Array.isArray(this.hardVocabList) ? this.hardVocabList.length : 0;
},
@@ -566,58 +563,27 @@ export default {
displayCourseTitle(course) {
return localizeVocabCourseTitle(course?.title, this.$i18n?.locale) || '';
},
hardStorageKey() {
return this.courseId ? `yourpart:vocab:hardList:${this.courseId}` : null;
},
refreshHardVocabList() {
const key = this.hardStorageKey();
if (!key) {
async refreshHardVocabList() {
if (!this.courseId) {
this.hardVocabList = [];
return;
}
try {
const raw = localStorage.getItem(key);
const parsed = raw ? JSON.parse(raw) : {};
const values = parsed && typeof parsed === 'object' ? Object.entries(parsed) : [];
this.hardVocabList = values
.map(([entryKey, entry], idx) => {
const learning = String(entry?.learning || '').trim();
const reference = String(entry?.reference || '').trim();
if (!learning || !reference) return null;
return {
id: `hard-${idx}-${learning}-${reference}`,
key: String(entryKey || ''),
learning,
reference
};
})
.filter(Boolean);
const { data } = await apiClient.get(`/api/vocab/courses/${this.courseId}/srs/hard`);
this.hardVocabList = (Array.isArray(data?.items) ? data.items : [])
.map((entry) => ({
id: String(entry.itemKey || ''),
key: String(entry.itemKey || ''),
itemKey: String(entry.itemKey || ''),
lessonId: entry.lessonId || null,
learning: String(entry.learning || '').trim(),
reference: String(entry.reference || '').trim()
}))
.filter((entry) => entry.id && entry.learning && entry.reference)
} catch (_) {
this.hardVocabList = [];
}
},
removeHardVocabEntry(entryKey) {
const key = this.hardStorageKey();
if (!key || !entryKey) return;
try {
const raw = localStorage.getItem(key);
const parsed = raw ? JSON.parse(raw) : {};
if (!parsed || typeof parsed !== 'object' || !parsed[entryKey]) return;
const next = { ...parsed };
delete next[entryKey];
localStorage.setItem(key, JSON.stringify(next));
this.refreshHardVocabList();
try {
window.dispatchEvent(new CustomEvent('yourpart:hardvocab:changed', {
detail: { courseId: this.courseId, storageKey: key }
}));
} catch (_) {
// ignore environments without CustomEvent
}
} catch (_) {
// ignore storage parse/write errors
}
},
handleWindowFocus() {
this.refreshHardVocabList();
},
@@ -657,14 +623,16 @@ export default {
this.srsLoading = true;
try {
const { data } = await apiClient.get(`/api/vocab/courses/${this.courseId}/srs/due`, {
params: { limit: 40 }
params: { limit: 50 }
});
this.srsDueItems = Array.isArray(data?.items) ? data.items : [];
this.srsDueTotal = Number.isFinite(Number(data?.totalDueCount)) ? Number(data.totalDueCount) : this.srsDueItems.length;
this.srsDailyLimit = Number.isFinite(Number(data?.dailyLimit)) ? Number(data.dailyLimit) : 50;
} catch (e) {
console.warn('Konnte SRS-Fälligkeiten nicht laden:', e);
this.srsDueItems = [];
this.srsDueTotal = 0;
this.srsDailyLimit = 50;
} finally {
this.srsLoading = false;
}
@@ -966,6 +934,7 @@ export default {
this.$refs.practiceDialog?.open?.({
courseId: this.courseId,
initialPool: this.hardVocabList,
initialHardPool: this.hardVocabList,
closeOnHardCompletion: true,
onClose: () => this.refreshHardVocabList()
});
@@ -978,10 +947,6 @@ export default {
courseId: this.courseId,
initialPool: this.srsDueItems,
srsMode: true,
onDailyHardLimitReached: () => {
this.refreshHardVocabList();
this.$nextTick(() => this.openHardPractice());
},
onClose: () => this.loadSrsDueItems()
});
},
@@ -1040,11 +1005,9 @@ export default {
]);
this.refreshHardVocabList();
window.addEventListener('focus', this.handleWindowFocus);
window.addEventListener('yourpart:hardvocab:changed', this.handleHardVocabChanged);
},
beforeUnmount() {
window.removeEventListener('focus', this.handleWindowFocus);
window.removeEventListener('yourpart:hardvocab:changed', this.handleHardVocabChanged);
},
};
</script>

View File

@@ -315,19 +315,6 @@
{{ $t('socialnetwork.vocab.courses.wrong') }}. {{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ currentVocabQuestion.answer }}
</div>
</div>
<div class="vocab-hard-actions">
<button type="button" class="btn-switch-mode" @click="markCurrentVocabAsHard">
{{ $t('socialnetwork.vocab.courses.markVocabHard') }}
</button>
<button
v-if="isCurrentVocabMarkedHard"
type="button"
class="btn-switch-mode"
@click="unmarkCurrentVocabAsHard"
>
{{ $t('socialnetwork.vocab.courses.unmarkVocabHard') }}
</button>
</div>
<!-- Multiple Choice Modus -->
<div v-if="vocabTrainerMode === 'multiple_choice' && !vocabTrainerAnswered" class="vocab-answer-area multiple-choice">
<div class="choice-buttons">