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:
170
frontend/src/dialogues/socialnetwork/VocabSearchDialog.vue
Normal file
170
frontend/src/dialogues/socialnetwork/VocabSearchDialog.vue
Normal 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>
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user