feat(vocab): add hard status management for SRS items and update related logic
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user