- Updated VocabService to support multiple correct answers in multiple choice exercises, allowing for better answer validation and user feedback. - Enhanced the extraction of correct answers and alternatives to accommodate both single and multiple correct indices. - Improved CSS styles in VocabCourseView for better table layout, including adjustments for overflow handling and vertical alignment, enhancing overall user experience.
492 lines
12 KiB
Vue
492 lines
12 KiB
Vue
<template>
|
|
<div class="vocab-course-view">
|
|
<div v-if="loading">{{ $t('general.loading') }}</div>
|
|
<div v-else-if="course">
|
|
<h2>{{ course.title }}</h2>
|
|
<p v-if="course.description">{{ course.description }}</p>
|
|
|
|
<div class="course-info">
|
|
<span>{{ $t('socialnetwork.vocab.courses.difficulty') }}: {{ course.difficultyLevel }}</span>
|
|
<span v-if="course.isPublic">{{ $t('socialnetwork.vocab.courses.public') }}</span>
|
|
<span v-if="course.shareCode && isOwner" class="share-code">
|
|
{{ $t('socialnetwork.vocab.courses.shareCode') }}: <code>{{ course.shareCode }}</code>
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="isOwner" class="owner-actions">
|
|
<button @click="showAddLessonDialog = true">{{ $t('socialnetwork.vocab.courses.addLesson') }}</button>
|
|
<button @click="editCourse">{{ $t('socialnetwork.vocab.courses.edit') }}</button>
|
|
</div>
|
|
|
|
<div v-if="course.lessons && course.lessons.length > 0" class="lessons-list">
|
|
<h3>{{ $t('socialnetwork.vocab.courses.lessons') }}</h3>
|
|
<table class="lessons-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="col-number">{{ $t('socialnetwork.vocab.courses.lessonNumber') }}</th>
|
|
<th class="col-title">{{ $t('socialnetwork.vocab.courses.title') }}</th>
|
|
<th class="col-status">Status</th>
|
|
<th class="col-actions">Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="lesson in course.lessons" :key="lesson.id" class="lesson-row">
|
|
<td class="lesson-number">{{ lesson.lessonNumber }}</td>
|
|
<td class="lesson-title">
|
|
<span class="title-label">{{ lesson.title }}</span>
|
|
<span v-if="lesson.description" class="lesson-description">{{ lesson.description }}</span>
|
|
</td>
|
|
<td class="lesson-status">
|
|
<span v-if="getLessonProgress(lesson.id)?.completed" class="badge completed">
|
|
{{ $t('socialnetwork.vocab.courses.completed') }}
|
|
</span>
|
|
<span v-else-if="getLessonProgress(lesson.id)?.score" class="score">
|
|
{{ $t('socialnetwork.vocab.courses.score') }}: {{ getLessonProgress(lesson.id).score }}%
|
|
</span>
|
|
<span v-else class="status-new">
|
|
{{ $t('socialnetwork.vocab.courses.notStarted') }}
|
|
</span>
|
|
</td>
|
|
<td class="lesson-actions">
|
|
<button @click="openLesson(lesson.id)" class="btn-start">
|
|
{{ getLessonProgress(lesson.id)?.completed ? $t('socialnetwork.vocab.courses.review') : $t('socialnetwork.vocab.courses.start') }}
|
|
</button>
|
|
<button v-if="isOwner" @click="editLesson(lesson.id)" class="btn-edit">{{ $t('socialnetwork.vocab.courses.edit') }}</button>
|
|
<button v-if="isOwner" @click="deleteLesson(lesson.id)" class="btn-delete">{{ $t('general.delete') }}</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-else>
|
|
<p>{{ $t('socialnetwork.vocab.courses.noLessons') }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Lesson Dialog -->
|
|
<div v-if="showAddLessonDialog" class="dialog-overlay" @click="showAddLessonDialog = false">
|
|
<div class="dialog" @click.stop>
|
|
<h3>{{ $t('socialnetwork.vocab.courses.addLesson') }}</h3>
|
|
<form @submit.prevent="addLesson">
|
|
<div class="form-group">
|
|
<label>{{ $t('socialnetwork.vocab.courses.lessonNumber') }}</label>
|
|
<input type="number" v-model.number="newLesson.lessonNumber" min="1" required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ $t('socialnetwork.vocab.courses.title') }}</label>
|
|
<input v-model="newLesson.title" required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ $t('socialnetwork.vocab.courses.description') }}</label>
|
|
<textarea v-model="newLesson.description"></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ $t('socialnetwork.vocab.courses.chapter') }}</label>
|
|
<select v-model="newLesson.chapterId" required>
|
|
<option value="">{{ $t('socialnetwork.vocab.courses.selectChapter') }}</option>
|
|
<option v-for="chapter in chapters" :key="chapter.id" :value="chapter.id">{{ chapter.title }}</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit">{{ $t('general.create') }}</button>
|
|
<button type="button" @click="showAddLessonDialog = false">{{ $t('general.cancel') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import apiClient from '@/utils/axios.js';
|
|
|
|
export default {
|
|
name: 'VocabCourseView',
|
|
props: {
|
|
courseId: {
|
|
type: String,
|
|
required: true
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
loading: false,
|
|
course: null,
|
|
progress: [],
|
|
chapters: [],
|
|
showAddLessonDialog: false,
|
|
newLesson: {
|
|
lessonNumber: 1,
|
|
title: '',
|
|
description: '',
|
|
chapterId: null
|
|
}
|
|
};
|
|
},
|
|
computed: {
|
|
...mapGetters(['user']),
|
|
isOwner() {
|
|
return this.course && this.course.ownerUserId === this.user?.id;
|
|
}
|
|
},
|
|
watch: {
|
|
courseId() {
|
|
this.loadCourse();
|
|
}
|
|
},
|
|
methods: {
|
|
async loadCourse() {
|
|
this.loading = true;
|
|
try {
|
|
const res = await apiClient.get(`/api/vocab/courses/${this.courseId}`);
|
|
this.course = res.data;
|
|
await this.loadProgress();
|
|
if (this.course.languageId) {
|
|
await this.loadChapters();
|
|
}
|
|
} catch (e) {
|
|
console.error('Konnte Kurs nicht laden:', e);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
async loadProgress() {
|
|
try {
|
|
const res = await apiClient.get(`/api/vocab/courses/${this.courseId}/progress`);
|
|
this.progress = res.data || [];
|
|
} catch (e) {
|
|
// Nicht eingeschrieben? Progress ist leer
|
|
this.progress = [];
|
|
}
|
|
},
|
|
async loadChapters() {
|
|
try {
|
|
const res = await apiClient.get(`/api/vocab/languages/${this.course.languageId}/chapters`);
|
|
this.chapters = res.data?.chapters || [];
|
|
} catch (e) {
|
|
console.error('Konnte Kapitel nicht laden:', e);
|
|
}
|
|
},
|
|
getLessonProgress(lessonId) {
|
|
return this.progress.find(p => p.lessonId === lessonId);
|
|
},
|
|
async addLesson() {
|
|
try {
|
|
await apiClient.post(`/api/vocab/courses/${this.courseId}/lessons`, this.newLesson);
|
|
this.showAddLessonDialog = false;
|
|
this.newLesson = {
|
|
lessonNumber: 1,
|
|
title: '',
|
|
description: '',
|
|
chapterId: null
|
|
};
|
|
await this.loadCourse();
|
|
} catch (e) {
|
|
console.error('Fehler beim Hinzufügen der Lektion:', e);
|
|
alert(e.response?.data?.error || 'Fehler beim Hinzufügen der Lektion');
|
|
}
|
|
},
|
|
async deleteLesson(lessonId) {
|
|
if (!confirm(this.$t('socialnetwork.vocab.courses.confirmDelete'))) {
|
|
return;
|
|
}
|
|
try {
|
|
await apiClient.delete(`/api/vocab/lessons/${lessonId}`);
|
|
await this.loadCourse();
|
|
} catch (e) {
|
|
console.error('Fehler beim Löschen der Lektion:', e);
|
|
alert(e.response?.data?.error || 'Fehler beim Löschen der Lektion');
|
|
}
|
|
},
|
|
openLesson(lessonId) {
|
|
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/lessons/${lessonId}`);
|
|
},
|
|
editCourse() {
|
|
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/edit`);
|
|
},
|
|
editLesson(lessonId) {
|
|
// TODO: Implement edit lesson
|
|
console.log('Edit lesson', lessonId);
|
|
}
|
|
},
|
|
async mounted() {
|
|
await this.loadCourse();
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.vocab-course-view {
|
|
padding: 20px;
|
|
}
|
|
|
|
.course-info {
|
|
display: flex;
|
|
gap: 15px;
|
|
margin: 15px 0;
|
|
color: #666;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.share-code {
|
|
font-family: monospace;
|
|
}
|
|
|
|
.share-code code {
|
|
background: #f0f0f0;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.owner-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin: 15px 0;
|
|
}
|
|
|
|
.lessons-list {
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.lessons-table {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
.lessons-table th,
|
|
.lessons-table td {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.lessons-table thead {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.lessons-table th {
|
|
padding: 12px 15px;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
color: #333;
|
|
font-size: 0.9em;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.lessons-table th.col-number,
|
|
.lessons-table td.lesson-number {
|
|
width: 80px;
|
|
min-width: 80px;
|
|
max-width: 80px;
|
|
overflow: visible;
|
|
}
|
|
|
|
.lessons-table th.col-title,
|
|
.lessons-table td.lesson-title {
|
|
width: auto;
|
|
min-width: 200px;
|
|
overflow: visible;
|
|
}
|
|
|
|
.lessons-table th.col-status,
|
|
.lessons-table td.lesson-status {
|
|
width: 200px;
|
|
min-width: 200px;
|
|
max-width: 200px;
|
|
overflow: visible;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.lessons-table th.col-actions,
|
|
.lessons-table td.lesson-actions {
|
|
width: 250px;
|
|
min-width: 250px;
|
|
max-width: 250px;
|
|
overflow: visible;
|
|
}
|
|
|
|
.lessons-table tbody tr {
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.lessons-table tbody tr:hover {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.lessons-table td {
|
|
padding: 15px;
|
|
vertical-align: top;
|
|
}
|
|
|
|
.lesson-number {
|
|
font-weight: 600;
|
|
color: #666;
|
|
font-size: 0.95em;
|
|
vertical-align: top;
|
|
padding-top: 15px;
|
|
}
|
|
|
|
.lesson-title {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.title-label {
|
|
font-weight: 500;
|
|
color: #333;
|
|
font-size: 1em;
|
|
}
|
|
|
|
.lesson-description {
|
|
color: #666;
|
|
font-size: 0.85em;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.lesson-status {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
align-items: flex-start;
|
|
justify-content: flex-start;
|
|
white-space: nowrap;
|
|
overflow: visible;
|
|
}
|
|
|
|
.badge.completed {
|
|
background: #4CAF50;
|
|
color: white;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
font-size: 0.8em;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.score {
|
|
color: #666;
|
|
font-size: 0.85em;
|
|
}
|
|
|
|
.status-new {
|
|
color: #999;
|
|
font-size: 0.85em;
|
|
font-style: italic;
|
|
}
|
|
|
|
.lesson-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
align-items: flex-start;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.btn-start {
|
|
padding: 8px 16px;
|
|
background: #F9A22C;
|
|
color: #000000;
|
|
border: 1px solid #F9A22C;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.9em;
|
|
font-weight: 500;
|
|
transition: background 0.05s;
|
|
}
|
|
|
|
.btn-start:hover {
|
|
background: #fdf1db;
|
|
color: #7E471B;
|
|
border: 1px solid #7E471B;
|
|
}
|
|
|
|
.btn-edit {
|
|
padding: 6px 12px;
|
|
background: #F9A22C;
|
|
color: #000000;
|
|
border: 1px solid #F9A22C;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.85em;
|
|
transition: background 0.05s;
|
|
}
|
|
|
|
.btn-edit:hover {
|
|
background: #fdf1db;
|
|
color: #7E471B;
|
|
border: 1px solid #7E471B;
|
|
}
|
|
|
|
.btn-delete {
|
|
padding: 6px 12px;
|
|
background: #dc3545;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.85em;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.btn-delete:hover {
|
|
background: #c82333;
|
|
}
|
|
|
|
.dialog-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.dialog {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.form-group {
|
|
margin: 15px 0;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group textarea,
|
|
.form-group select {
|
|
width: 100%;
|
|
padding: 8px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.form-group textarea {
|
|
min-height: 80px;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: flex-end;
|
|
margin-top: 20px;
|
|
}
|
|
</style>
|