Files
UniVerse/frontend/src/views/teacher/TeacherAnalyticsView.vue
T

146 lines
4.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import GlassCard from '@/components/ui/GlassCard.vue'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import EmptyState from '@/components/ui/EmptyState.vue'
import { lecturesApi } from '@/api'
import type { Review } from '@/types'
import { mapApiReview } from '@/api/mappers'
import { useLecturesStore } from '@/stores/lectures'
import { useAuthStore } from '@/stores/auth'
const lecturesStore = useLecturesStore()
const auth = useAuthStore()
const reviews = ref<Review[]>([])
const positive = computed(() => reviews.value.filter((r) => r.sentiment === 'positive').length)
const neutral = computed(() => reviews.value.filter((r) => r.sentiment === 'neutral').length)
const negative = computed(() => reviews.value.filter((r) => r.sentiment === 'negative').length)
const total = computed(() => reviews.value.length)
const pct = (value: number) => (total.value ? Math.round((value / total.value) * 100) : 0)
const ratio = (value: number) => `${value}/${total.value}`
async function fetchTeacherAnalytics() {
if (!auth.user?.id) return
await lecturesStore.fetchLectures({ TeacherId: auth.user.id })
const targetLectures = lecturesStore.all.slice(0, 5)
const payload = await Promise.allSettled(targetLectures.map((l) => lecturesApi.reviews(l.id)))
reviews.value = payload.flatMap((result) =>
result.status === 'fulfilled' ? result.value.map(mapApiReview) : [],
)
}
onMounted(fetchTeacherAnalytics)
watch(() => auth.user?.id, fetchTeacherAnalytics)
</script>
<template>
<div class="teacher-analytics page-content">
<h1 class="page-title">Аналитика преподавателя</h1>
<div class="grid">
<GlassCard>
<div class="section-title">Sentiment-анализ отзывов</div>
<div class="sentiment">
<div>
<div class="sentiment-label">Позитивные {{ ratio(positive) }}</div>
<ProgressBar :value="pct(positive)" :max="100" :text="ratio(positive)" />
</div>
<div>
<div class="sentiment-label">Нейтральные {{ ratio(neutral) }}</div>
<ProgressBar
:value="pct(neutral)"
:max="100"
:text="ratio(neutral)"
color="linear-gradient(90deg, #7DD3FC, #BAE6FD)"
/>
</div>
<div>
<div class="sentiment-label">Негативные {{ ratio(negative) }}</div>
<ProgressBar
:value="pct(negative)"
:max="100"
:text="ratio(negative)"
color="linear-gradient(90deg, #FCA5A5, #FECACA)"
/>
</div>
</div>
</GlassCard>
</div>
<GlassCard>
<div class="section-title">LLM-сводка проблем и рекомендаций</div>
<p class="summary">
Студенты отмечают сильную практическую часть, но хотят больше разборов вопросов из
аудитории. Рекомендуется добавить блок с кейсами из реальных проектов и увеличить время на
интерактив.
</p>
<div class="tags">
<span class="tag-chip">много практики</span>
<span class="tag-chip">понятные примеры</span>
<span class="tag-chip">сложный материал</span>
<span class="tag-chip">нужны задания</span>
</div>
</GlassCard>
<GlassCard>
<div class="section-title">Отзывы</div>
<EmptyState
v-if="!reviews.length"
title="Отзывов пока нет"
subtitle="Когда студенты оставят отзывы, они появятся здесь."
/>
<div v-else class="reviews">
<div v-for="review in reviews" :key="review.id" class="review">«{{ review.text }}»</div>
</div>
</GlassCard>
</div>
</template>
<style scoped>
.teacher-analytics {
display: flex;
flex-direction: column;
gap: 18px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.sentiment {
display: flex;
flex-direction: column;
gap: 12px;
}
.sentiment-label {
font-size: 12px;
color: var(--color-text-secondary);
margin-bottom: 4px;
}
.summary {
font-size: 14px;
color: var(--color-text-secondary);
line-height: 1.5;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.reviews {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
.review {
background: rgba(255, 255, 255, 0.6);
border: 1px solid var(--color-border-glass);
padding: 10px;
border-radius: var(--radius-sm);
font-size: 13px;
}
</style>