Add search functionality for vocabulary in VocabController and VocabService

- Implemented a new searchVocabs method in VocabService to allow users to search for vocabulary based on learning and mother tongue terms.
- Updated VocabController to include the searchVocabs method wrapped with user authentication.
- Added a new route in vocabRouter for searching vocabulary by language ID.
- Enhanced VocabChapterView and VocabLanguageView components to include a button for opening the search dialog.
- Added translations for search-related terms in both German and English locales, improving user accessibility.
This commit is contained in:
Torsten Schulz (local)
2026-01-05 16:53:38 +01:00
parent dab3391aa2
commit f5e3a9a4a2
8 changed files with 266 additions and 1 deletions

View File

@@ -0,0 +1,170 @@
<template>
<DialogWidget
ref="dialog"
:title="$t('socialnetwork.vocab.search.title')"
:show-close="true"
:buttons="buttons"
:modal="true"
:isTitleTranslated="false"
width="60em"
height="34em"
name="VocabSearchDialog"
display="flex"
@close="close"
>
<div class="layout">
<div class="top">
<div class="row">
<label class="field">
{{ $t('socialnetwork.vocab.search.motherTongue') }}
<input v-model="motherTongue" type="text" @keydown.enter.prevent="runSearch" />
</label>
<label class="field">
{{ learningLabel }}
<input v-model="learning" type="text" @keydown.enter.prevent="runSearch" />
</label>
<button class="btn" :disabled="loading || (!motherTongue.trim() && !learning.trim())" @click="runSearch">
{{ loading ? $t('general.loading') : $t('socialnetwork.vocab.search.search') }}
</button>
</div>
</div>
<div class="body">
<div v-if="error" class="error">{{ error }}</div>
<div v-else-if="results.length === 0">
{{ $t('socialnetwork.vocab.search.noResults') }}
</div>
<table v-else class="tbl">
<thead>
<tr>
<th>{{ $t('socialnetwork.vocab.search.motherTongue') }}</th>
<th>{{ learningLabel }}</th>
<th>{{ $t('socialnetwork.vocab.search.lesson') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="r in results" :key="r.id">
<td>{{ r.motherTongue }}</td>
<td>{{ r.learning }}</td>
<td>{{ r.chapterTitle }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
import apiClient from '@/utils/axios.js';
export default {
name: 'VocabSearchDialog',
components: { DialogWidget },
data() {
return {
languageId: null,
languageName: '',
motherTongue: '',
learning: '',
loading: false,
results: [],
error: '',
};
},
computed: {
buttons() {
return [{ text: this.$t('message.close'), action: this.close }];
},
learningLabel() {
return this.languageName || this.$t('socialnetwork.vocab.search.learningLanguage');
},
},
methods: {
open({ languageId, languageName = '' } = {}) {
this.languageId = languageId;
this.languageName = languageName || '';
this.motherTongue = '';
this.learning = '';
this.results = [];
this.error = '';
this.loading = false;
this.$refs.dialog.open();
this.$nextTick(() => {});
},
close() {
this.$refs.dialog.close();
},
async runSearch() {
if (!this.languageId) return;
const mt = this.motherTongue.trim();
const l = this.learning.trim();
if (!mt && !l) return;
this.loading = true;
this.error = '';
try {
const res = await apiClient.get(`/api/vocab/languages/${this.languageId}/search`, {
params: {
motherTongue: mt || undefined,
learning: l || undefined,
},
});
this.results = res.data?.results || [];
} catch (e) {
console.error('Search failed:', e);
this.results = [];
this.error = this.$t('socialnetwork.vocab.search.error');
} finally {
this.loading = false;
}
},
},
};
</script>
<style scoped>
.layout {
display: flex;
flex-direction: column;
gap: 10px;
height: 100%;
}
.top .row {
display: flex;
gap: 10px;
align-items: flex-end;
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.field input {
padding: 6px;
}
.btn {
padding: 8px 12px;
}
.body {
flex: 1;
overflow: auto;
}
.tbl {
width: 100%;
border-collapse: collapse;
}
.tbl th,
.tbl td {
border: 1px solid #ccc;
padding: 6px;
}
.error {
color: #b00020;
margin-bottom: 8px;
}
</style>

View File

@@ -308,6 +308,16 @@
"stats": "Statistik",
"success": "Erfolg",
"fail": "Misserfolg"
},
"search": {
"open": "Suche",
"title": "Vokabeln suchen",
"motherTongue": "Muttersprache",
"learningLanguage": "Lernsprache",
"lesson": "Lektion",
"search": "Suchen",
"noResults": "Keine Treffer.",
"error": "Suche fehlgeschlagen."
}
}
}

View File

@@ -308,6 +308,16 @@
"stats": "Stats",
"success": "Success",
"fail": "Fail"
},
"search": {
"open": "Search",
"title": "Search vocabulary",
"motherTongue": "Mother tongue",
"learningLanguage": "Learning language",
"lesson": "Lesson",
"search": "Search",
"noResults": "No results.",
"error": "Search failed."
}
}
}

View File

@@ -9,6 +9,7 @@
<div class="row">
<button @click="back">{{ $t('general.back') }}</button>
<button v-if="vocabs.length" @click="openPractice">{{ $t('socialnetwork.vocab.practice.open') }}</button>
<button @click="openSearch">{{ $t('socialnetwork.vocab.search.open') }}</button>
</div>
<div class="row" v-if="chapter.isOwner">
@@ -50,21 +51,24 @@
</div>
<VocabPracticeDialog ref="practiceDialog" />
<VocabSearchDialog ref="searchDialog" />
</template>
<script>
import apiClient from '@/utils/axios.js';
import VocabPracticeDialog from '@/dialogues/socialnetwork/VocabPracticeDialog.vue';
import VocabSearchDialog from '@/dialogues/socialnetwork/VocabSearchDialog.vue';
export default {
name: 'VocabChapterView',
components: { VocabPracticeDialog },
components: { VocabPracticeDialog, VocabSearchDialog },
data() {
return {
loading: false,
saving: false,
practiceOpen: false,
chapter: null,
languageName: '',
vocabs: [],
learning: '',
reference: '',
@@ -89,12 +93,24 @@ export default {
},
});
},
openSearch() {
this.$refs.searchDialog?.open?.({
languageId: this.$route.params.languageId,
languageName: this.languageName || '',
});
},
async load() {
this.loading = true;
try {
const res = await apiClient.get(`/api/vocab/chapters/${this.$route.params.chapterId}/vocabs`);
this.chapter = res.data?.chapter || null;
this.vocabs = res.data?.vocabs || [];
try {
const langRes = await apiClient.get(`/api/vocab/languages/${this.$route.params.languageId}`);
this.languageName = langRes.data?.name || '';
} catch (_) {
this.languageName = '';
}
} catch (e) {
console.error('Load chapter vocabs failed:', e);
this.chapter = null;

View File

@@ -17,6 +17,7 @@
<div class="row">
<button @click="goSubscribe">{{ $t('socialnetwork.vocab.subscribeByCode') }}</button>
<button @click="openSearch">{{ $t('socialnetwork.vocab.search.open') }}</button>
</div>
<hr />
@@ -48,13 +49,17 @@
</div>
</div>
</div>
<VocabSearchDialog ref="searchDialog" />
</template>
<script>
import apiClient from '@/utils/axios.js';
import VocabSearchDialog from '@/dialogues/socialnetwork/VocabSearchDialog.vue';
export default {
name: 'VocabLanguageView',
components: { VocabSearchDialog },
data() {
return {
loading: false,
@@ -69,6 +74,12 @@ export default {
goSubscribe() {
this.$router.push('/socialnetwork/vocab/subscribe');
},
openSearch() {
this.$refs.searchDialog?.open?.({
languageId: this.$route.params.languageId,
languageName: this.language?.name || '',
});
},
openChapter(chapterId) {
this.$router.push(`/socialnetwork/vocab/${this.$route.params.languageId}/chapters/${chapterId}`);
},