feat(vocab): implement pagination and localization for vocabulary dictionaries
All checks were successful
Deploy to production / deploy (push) Successful in 3m2s
All checks were successful
Deploy to production / deploy (push) Successful in 3m2s
- Added pagination functionality to the vocabulary dictionary views, allowing users to navigate through results efficiently. - Introduced a new method `_parseDictionaryPaging` in `VocabService` to handle pagination parameters. - Updated `getLanguageDictionary` and `getCourseDictionary` methods to return pagination details alongside results. - Enhanced the `VocabDictionaryView` component with pagination controls and updated UI for better user experience. - Added localization entries for pagination in Cebuano, German, English, Spanish, and French, ensuring a consistent user experience across languages.
This commit is contained in:
@@ -802,7 +802,10 @@
|
||||
"notFound": "Walay access o wala makita.",
|
||||
"languageTitle": "Dictionary: {name}",
|
||||
"courseTitle": "Dictionary sa kurso: {name}",
|
||||
"courseLearningColumn": "Pagkat-on nga pinulongan (kurso)"
|
||||
"courseLearningColumn": "Pagkat-on nga pinulongan (kurso)",
|
||||
"dictionaryPagerPrev": "Balik",
|
||||
"dictionaryPagerNext": "Sunod",
|
||||
"dictionaryPager": "{from}–{to} sa {total} · Page {page} sa {pages}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,7 +479,10 @@
|
||||
"notFound": "Kein Zugriff oder nicht gefunden.",
|
||||
"languageTitle": "Wörterbuch: {name}",
|
||||
"courseTitle": "Kurs-Wörterbuch: {name}",
|
||||
"courseLearningColumn": "Lernsprache (Kurs)"
|
||||
"courseLearningColumn": "Lernsprache (Kurs)",
|
||||
"dictionaryPagerPrev": "Zurück",
|
||||
"dictionaryPagerNext": "Weiter",
|
||||
"dictionaryPager": "{from}–{to} von {total} · Seite {page} von {pages}"
|
||||
},
|
||||
"courses": {
|
||||
"title": "Sprachlernkurse",
|
||||
|
||||
@@ -479,7 +479,10 @@
|
||||
"notFound": "No access or not found.",
|
||||
"languageTitle": "Dictionary: {name}",
|
||||
"courseTitle": "Course dictionary: {name}",
|
||||
"courseLearningColumn": "Learning language (course)"
|
||||
"courseLearningColumn": "Learning language (course)",
|
||||
"dictionaryPagerPrev": "Previous",
|
||||
"dictionaryPagerNext": "Next",
|
||||
"dictionaryPager": "{from}–{to} of {total} · Page {page} of {pages}"
|
||||
},
|
||||
"courses": {
|
||||
"title": "Language Learning Courses",
|
||||
|
||||
@@ -477,7 +477,10 @@
|
||||
"notFound": "Sin acceso o no encontrado.",
|
||||
"languageTitle": "Diccionario: {name}",
|
||||
"courseTitle": "Diccionario del curso: {name}",
|
||||
"courseLearningColumn": "Idioma de aprendizaje (curso)"
|
||||
"courseLearningColumn": "Idioma de aprendizaje (curso)",
|
||||
"dictionaryPagerPrev": "Anterior",
|
||||
"dictionaryPagerNext": "Siguiente",
|
||||
"dictionaryPager": "{from}–{to} de {total} · Página {page} de {pages}"
|
||||
},
|
||||
"courses": {
|
||||
"title": "Cursos de idiomas",
|
||||
|
||||
@@ -477,7 +477,10 @@
|
||||
"notFound": "Pas d’accès ou introuvable.",
|
||||
"languageTitle": "Dictionnaire : {name}",
|
||||
"courseTitle": "Dictionnaire du cours : {name}",
|
||||
"courseLearningColumn": "Langue apprise (cours)"
|
||||
"courseLearningColumn": "Langue apprise (cours)",
|
||||
"dictionaryPagerPrev": "Précédent",
|
||||
"dictionaryPagerNext": "Suivant",
|
||||
"dictionaryPager": "{from}–{to} sur {total} · Page {page} sur {pages}"
|
||||
},
|
||||
"courses": {
|
||||
"title": "Cours d'apprentissage des langues",
|
||||
|
||||
@@ -26,23 +26,44 @@
|
||||
<div class="dictionary-body surface-card">
|
||||
<div v-if="listLoading" class="dictionary-state">{{ $t('general.loading') }}</div>
|
||||
<div v-else-if="error" class="dictionary-state dictionary-state--error">{{ error }}</div>
|
||||
<div v-else-if="results.length === 0" class="dictionary-state">{{ $t('socialnetwork.vocab.dictionary.empty') }}</div>
|
||||
<table v-else class="dictionary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('socialnetwork.vocab.search.motherTongue') }}</th>
|
||||
<th>{{ learningColumnLabel }}</th>
|
||||
<th v-if="!isCourseMode">{{ $t('socialnetwork.vocab.chapters') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, idx) in results" :key="row.id != null ? `id-${row.id}` : `r-${idx}-${row.learning}-${row.reference}`">
|
||||
<td>{{ row.reference }}</td>
|
||||
<td>{{ row.learning }}</td>
|
||||
<td v-if="!isCourseMode">{{ row.chapterTitle }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else-if="dictionaryTotal === 0" class="dictionary-state">{{ $t('socialnetwork.vocab.dictionary.empty') }}</div>
|
||||
<template v-else>
|
||||
<table class="dictionary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('socialnetwork.vocab.search.motherTongue') }}</th>
|
||||
<th>{{ learningColumnLabel }}</th>
|
||||
<th v-if="!isCourseMode">{{ $t('socialnetwork.vocab.chapters') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, idx) in results" :key="row.id != null ? `id-${row.id}` : `r-${idx}-${row.learning}-${row.reference}`">
|
||||
<td>{{ row.reference }}</td>
|
||||
<td>{{ row.learning }}</td>
|
||||
<td v-if="!isCourseMode">{{ row.chapterTitle }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="dictionaryTotalPages > 1" class="dictionary-pager">
|
||||
<button
|
||||
type="button"
|
||||
class="dictionary-pager__btn"
|
||||
:disabled="dictionaryPage <= 1"
|
||||
@click="dictionaryGoPrev"
|
||||
>
|
||||
{{ $t('socialnetwork.vocab.dictionary.dictionaryPagerPrev') }}
|
||||
</button>
|
||||
<span class="dictionary-pager__meta">{{ $t('socialnetwork.vocab.dictionary.dictionaryPager', dictionaryPagerMeta) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="dictionary-pager__btn"
|
||||
:disabled="dictionaryPage >= dictionaryTotalPages"
|
||||
@click="dictionaryGoNext"
|
||||
>
|
||||
{{ $t('socialnetwork.vocab.dictionary.dictionaryPagerNext') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -50,6 +71,8 @@
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
|
||||
const DICTIONARY_PAGE_SIZE = 25;
|
||||
|
||||
export default {
|
||||
name: 'VocabDictionaryView',
|
||||
data() {
|
||||
@@ -60,6 +83,10 @@ export default {
|
||||
languageName: '',
|
||||
q: '',
|
||||
results: [],
|
||||
dictionaryPage: 1,
|
||||
dictionaryTotal: 0,
|
||||
dictionaryTotalPages: 1,
|
||||
dictionaryPageSize: DICTIONARY_PAGE_SIZE,
|
||||
error: '',
|
||||
debounceId: null,
|
||||
};
|
||||
@@ -81,6 +108,18 @@ export default {
|
||||
}
|
||||
return this.languageName || this.$t('socialnetwork.vocab.search.learningLanguage');
|
||||
},
|
||||
dictionaryPagerMeta() {
|
||||
const n = this.dictionaryTotal;
|
||||
const totalPages = this.dictionaryTotalPages;
|
||||
const page = this.dictionaryPage;
|
||||
const pageSize = this.dictionaryPageSize || DICTIONARY_PAGE_SIZE;
|
||||
if (n === 0) {
|
||||
return { from: 0, to: 0, page: 1, pages: 1, total: 0 };
|
||||
}
|
||||
const from = (page - 1) * pageSize + 1;
|
||||
const to = Math.min(page * pageSize, n);
|
||||
return { from, to, page, pages: totalPages, total: n };
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.fullPath'() {
|
||||
@@ -99,6 +138,9 @@ export default {
|
||||
async bootstrap() {
|
||||
this.q = '';
|
||||
this.results = [];
|
||||
this.dictionaryPage = 1;
|
||||
this.dictionaryTotal = 0;
|
||||
this.dictionaryTotalPages = 1;
|
||||
this.error = '';
|
||||
await this.loadContext();
|
||||
if (!this.contextTitle) {
|
||||
@@ -112,9 +154,22 @@ export default {
|
||||
}
|
||||
this.debounceId = setTimeout(() => {
|
||||
this.debounceId = null;
|
||||
this.dictionaryPage = 1;
|
||||
this.loadList();
|
||||
}, 320);
|
||||
},
|
||||
dictionaryGoPrev() {
|
||||
if (this.dictionaryPage > 1) {
|
||||
this.dictionaryPage -= 1;
|
||||
this.loadList();
|
||||
}
|
||||
},
|
||||
dictionaryGoNext() {
|
||||
if (this.dictionaryPage < this.dictionaryTotalPages) {
|
||||
this.dictionaryPage += 1;
|
||||
this.loadList();
|
||||
}
|
||||
},
|
||||
async loadContext() {
|
||||
this.metaLoading = true;
|
||||
this.contextTitle = '';
|
||||
@@ -148,17 +203,27 @@ export default {
|
||||
this.listLoading = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const params = { q: this.q.trim() };
|
||||
if (this.isCourseMode) {
|
||||
const { data } = await apiClient.get(`/api/vocab/courses/${this.$route.params.courseId}/dictionary`, { params });
|
||||
this.results = data?.results || [];
|
||||
} else {
|
||||
const { data } = await apiClient.get(`/api/vocab/languages/${this.$route.params.languageId}/dictionary`, { params });
|
||||
this.results = data?.results || [];
|
||||
const params = {
|
||||
q: this.q.trim(),
|
||||
page: this.dictionaryPage,
|
||||
pageSize: DICTIONARY_PAGE_SIZE,
|
||||
};
|
||||
const url = this.isCourseMode
|
||||
? `/api/vocab/courses/${this.$route.params.courseId}/dictionary`
|
||||
: `/api/vocab/languages/${this.$route.params.languageId}/dictionary`;
|
||||
const { data } = await apiClient.get(url, { params });
|
||||
this.results = data?.results || [];
|
||||
this.dictionaryTotal = typeof data?.total === 'number' ? data.total : this.results.length;
|
||||
this.dictionaryTotalPages = typeof data?.totalPages === 'number' ? data.totalPages : 1;
|
||||
this.dictionaryPageSize = typeof data?.pageSize === 'number' ? data.pageSize : DICTIONARY_PAGE_SIZE;
|
||||
if (typeof data?.page === 'number') {
|
||||
this.dictionaryPage = data.page;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Vocab dictionary list failed:', e);
|
||||
this.results = [];
|
||||
this.dictionaryTotal = 0;
|
||||
this.dictionaryTotalPages = 1;
|
||||
this.error = this.$t('socialnetwork.vocab.dictionary.loadError');
|
||||
} finally {
|
||||
this.listLoading = false;
|
||||
@@ -250,4 +315,41 @@ export default {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.dictionary-pager {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px 16px;
|
||||
margin-top: 16px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.dictionary-pager__btn {
|
||||
padding: 6px 14px;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface-elevated, #faf8f5);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dictionary-pager__btn:hover:not(:disabled) {
|
||||
filter: brightness(0.97);
|
||||
}
|
||||
|
||||
.dictionary-pager__btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dictionary-pager__meta {
|
||||
font-size: 0.88rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
min-width: min(100%, 14rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user