refactor: почистил фронтенд
This commit is contained in:
@@ -5,7 +5,6 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useLecturesStore } from '@/stores/lectures'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import GlassCard from '@/components/ui/GlassCard.vue'
|
||||
import StatsWidget from '@/components/ui/StatsWidget.vue'
|
||||
import LectureCard from '@/components/ui/LectureCard.vue'
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||
import AchievementBadge from '@/components/ui/AchievementBadge.vue'
|
||||
@@ -90,7 +89,6 @@ async function registerLecture(id: string) {
|
||||
<div class="quick-actions">
|
||||
<button class="btn-primary" @click="router.push('/catalog')">Найти лекцию</button>
|
||||
<button class="btn-secondary" @click="router.push('/my-lectures')">Мои записи</button>
|
||||
<button class="btn-secondary" :disabled="!nextLecture" @click="nextLecture && router.push(`/review/${nextLecture.id}`)">Оставить отзыв</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -122,13 +120,6 @@ async function registerLecture(id: string) {
|
||||
</GlassCard>
|
||||
<EmptyState v-else-if="!lectures.loading" title="Пока нет лекций" subtitle="Каталог пуст или данные ещё не синхронизированы." />
|
||||
|
||||
<div class="stats-row">
|
||||
<StatsWidget label="Посещено лекций" :value="user.lecturesAttended ?? 12" icon="books" color="green" />
|
||||
<StatsWidget label="Часов обучения" :value="user.hoursLearned ?? 18.5" icon="stopwatch" color="aqua" />
|
||||
<StatsWidget label="Монет" :value="user.coins" icon="coin" color="orange" />
|
||||
<StatsWidget label="Уровень" :value="user.level" icon="star" color="purple" sub="текущий уровень" />
|
||||
</div>
|
||||
|
||||
<GlassCard>
|
||||
<div class="xp-section">
|
||||
<div class="xp-header">
|
||||
@@ -186,7 +177,7 @@ async function registerLecture(id: string) {
|
||||
<div class="section-title">
|
||||
<span class="title-with-icon">
|
||||
<AppIcon class="title-icon" icon="bell" :size="18" />
|
||||
Напоминания
|
||||
Увидомления
|
||||
</span>
|
||||
</div>
|
||||
<div class="reminders">
|
||||
|
||||
@@ -21,14 +21,12 @@ const lecture = computed(() => lecturesStore.all.find(l => l.id === lectureId.va
|
||||
const isRegistered = computed(() => (lecture.value ? lecturesStore.isRegistered(lecture.value.id) : false))
|
||||
const slotRegistrationDisabled = computed(() => !userStore.hasEnrollmentSlotAvailable && !isRegistered.value)
|
||||
const isAttended = computed(() => lecture.value?.status === 'completed')
|
||||
const reviews = computed(() => lecturesStore.reviewsByLecture[lectureId.value] ?? [])
|
||||
|
||||
const similarLectures = computed(() => lecturesStore.all.filter(l => l.id !== lectureId.value).slice(0, 3))
|
||||
|
||||
onMounted(async () => {
|
||||
if (!lecturesStore.all.length) await lecturesStore.fetchLectures()
|
||||
await lecturesStore.fetchLecture(lectureId.value)
|
||||
await lecturesStore.fetchReviews(lectureId.value)
|
||||
})
|
||||
|
||||
async function registerLecture() {
|
||||
@@ -109,21 +107,6 @@ async function registerLecture() {
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard>
|
||||
<div class="section-title">LLM-сводка отзывов</div>
|
||||
<p class="summary">
|
||||
Студенты отмечают «понятные примеры» и «много практики». Предлагается добавить больше времени на вопросы и
|
||||
прикладные кейсы. Средняя оценка — 4.8/5.
|
||||
</p>
|
||||
<div class="reviews" v-if="reviews.length">
|
||||
<div v-for="review in reviews" :key="review.id" class="review">
|
||||
<div class="review-head">{{ review.userName }} {{ review.sentiment }}</div>
|
||||
<div class="review-body">{{ review.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-secondary text-sm">Отзывов пока нет.</p>
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
@@ -155,11 +138,6 @@ async function registerLecture() {
|
||||
.info-value { font-weight: 700; }
|
||||
.info-sub { font-size: 13px; color: var(--color-text-secondary); margin-top: 4px; }
|
||||
.tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.summary { font-size: 14px; color: var(--color-text-secondary); line-height: 1.5; }
|
||||
.reviews { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.review { padding: 10px; border-radius: var(--radius-sm); background: rgba(255,255,255,0.6); border: 1px solid var(--color-border-glass); }
|
||||
.review-head { font-weight: 600; margin-bottom: 4px; }
|
||||
.review-body { font-size: 13px; color: var(--color-text-secondary); }
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import GlassCard from '@/components/ui/GlassCard.vue'
|
||||
import AppIcon from '@/components/ui/AppIcon.vue'
|
||||
import { reviewsApi } from '@/api'
|
||||
import { lecturesApi, reviewsApi } from '@/api'
|
||||
import type { LectureDto } from '@/api/types'
|
||||
|
||||
const route = useRoute()
|
||||
const rating = ref<'positive' | 'neutral' | 'negative'>('positive')
|
||||
const text = ref('Лекция была хорошо структурирована, особенно понравились практические примеры и разбор кейсов.')
|
||||
const text = ref('')
|
||||
const submitted = ref(false)
|
||||
const editing = ref(false)
|
||||
const loading = ref(false)
|
||||
const lectureLoading = ref(false)
|
||||
const error = ref('')
|
||||
const lecture = ref<LectureDto | null>(null)
|
||||
|
||||
const ratingMap = {
|
||||
positive: 'Like',
|
||||
@@ -19,43 +21,77 @@ const ratingMap = {
|
||||
negative: 'Dislike',
|
||||
} as const
|
||||
|
||||
const lectureTitle = computed(() => lecture.value?.title || lecture.value?.courseName || 'Отзыв о лекции')
|
||||
const lectureMeta = computed(() => {
|
||||
if (!lecture.value) return ''
|
||||
|
||||
const startsAt = new Date(lecture.value.startsAt)
|
||||
const date = Number.isNaN(startsAt.getTime())
|
||||
? ''
|
||||
: startsAt.toLocaleString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
const teacher = lecture.value.teacherName ? `Преподаватель: ${lecture.value.teacherName}` : ''
|
||||
const location = lecture.value.format === 'Online' ? 'Онлайн' : lecture.value.locationName
|
||||
|
||||
return [date, teacher, location].filter(Boolean).join(' ')
|
||||
})
|
||||
|
||||
async function fetchLecture() {
|
||||
lectureLoading.value = true
|
||||
try {
|
||||
lecture.value = await lecturesApi.get(String(route.params.id))
|
||||
} catch {
|
||||
lecture.value = null
|
||||
} finally {
|
||||
lectureLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
await reviewsApi.create(String(route.params.id), ratingMap[rating.value], text.value)
|
||||
submitted.value = true
|
||||
editing.value = false
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Не удалось отправить отзыв.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void fetchLecture()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="review page-content">
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1 class="page-title">Отзыв о лекции #{{ route.params.id }}</h1>
|
||||
<p class="text-secondary">Ваш отзыв помогает улучшать качество лекций и приносит бонусные монеты.</p>
|
||||
<h1 class="page-title">{{ lectureLoading ? 'Загрузка лекции...' : lectureTitle }}</h1>
|
||||
<p class="text-secondary">
|
||||
{{ lectureMeta || 'Ваш отзыв помогает улучшать качество лекций и приносит бонусные монеты.' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GlassCard>
|
||||
<div v-if="submitted && !editing" class="success-state">
|
||||
<div v-if="submitted" class="success-state">
|
||||
<AppIcon class="success-icon" icon="circle-check" :size="32" />
|
||||
<div class="success-title">Отзыв отправлен и будет обработан</div>
|
||||
<div class="success-sub">
|
||||
Спасибо! Оценка полезности отзыва рассчитывается автоматически. Студентам не показывается техническая оценка LLM.
|
||||
</div>
|
||||
<button class="btn-secondary" @click="editing = true">Редактировать отзыв</button>
|
||||
</div>
|
||||
|
||||
<form v-else @submit.prevent="submit" class="form">
|
||||
<label class="field-label">Ваш отзыв о лекции</label>
|
||||
<textarea v-model="text" rows="6" placeholder="Опишите, что было полезно, а что можно улучшить"></textarea>
|
||||
<textarea v-model="text" rows="6"></textarea>
|
||||
|
||||
<label class="field-label">Оценка впечатлений</label>
|
||||
<div class="rating-options">
|
||||
@@ -71,7 +107,6 @@ async function submit() {
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn-primary" type="submit" :disabled="loading">{{ loading ? 'Отправляем...' : 'Отправить отзыв' }}</button>
|
||||
<button class="btn-secondary" type="button" :disabled="submitted">Сохранить черновик</button>
|
||||
</div>
|
||||
</form>
|
||||
</GlassCard>
|
||||
|
||||
Reference in New Issue
Block a user