Files
UniVerse/frontend/src/views/student/DashboardView.vue
T
serega404 779b6aba77
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 8s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 54s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 27s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 6s
feat: первое подключение фронтенда
2026-05-11 01:33:38 +03:00

184 lines
7.5 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 } from 'vue'
import { useRouter } from 'vue-router'
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'
import EmptyState from '@/components/ui/EmptyState.vue'
const auth = useAuthStore()
const lectures = useLecturesStore()
const userStore = useUserStore()
const router = useRouter()
const user = computed(() => auth.user!)
const nextLecture = computed(() => lectures.registeredLectures[0] ?? lectures.all[0])
const recommended = computed(() =>
lectures.all.filter(l => !lectures.registeredIds.includes(l.id)).slice(0, 3)
)
const achievements = computed(() => userStore.achievements.filter(a => a.unlocked).slice(0, 3))
const reminders = computed(() => userStore.notifications.slice(0, 3))
const xpToNext = 200
const xpProgress = computed(() => user.value.xp ?? 120)
onMounted(async () => {
await Promise.all([
lectures.all.length ? Promise.resolve() : lectures.fetchLectures(),
userStore.fetchStudentData(user.value.id),
])
await lectures.fetchRegisteredForUser(user.value.id)
})
</script>
<template>
<div class="dashboard page-content">
<div class="dashboard-welcome">
<div>
<h1 class="page-title">Добрый день, {{ user.name.split(' ')[0] }}! 👋</h1>
<p class="text-secondary">{{ user.institute }} · {{ user.direction }} · {{ user.year }} курс</p>
</div>
<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>
<GlassCard v-if="nextLecture">
<div class="next-lecture">
<div>
<div class="section-title">Ближайшая лекция</div>
<div class="next-title">{{ nextLecture.title }}</div>
<div class="next-meta">
<span>📅 Завтра, {{ nextLecture.time }}</span>
<span>🏛 {{ nextLecture.building }}, ауд. {{ nextLecture.room ?? 'онлайн' }}</span>
<span>👤 {{ nextLecture.teacher }}</span>
</div>
</div>
<div class="next-actions">
<button class="btn-primary" @click="router.push(`/lecture/${nextLecture.id}`)">Открыть</button>
<button class="btn-secondary">Добавить в календарь</button>
</div>
</div>
</GlassCard>
<EmptyState v-else-if="!lectures.loading" title="Пока нет лекций" subtitle="Каталог пуст или данные ещё не синхронизированы." />
<div class="stats-row">
<StatsWidget label="Посещено лекций" :value="user.lecturesAttended ?? 12" icon="📚" color="green" />
<StatsWidget label="Часов обучения" :value="user.hoursLearned ?? 18.5" icon="⏱" color="aqua" />
<StatsWidget label="Монет" :value="user.coins" icon="💰" color="orange" />
<StatsWidget label="Уровень" :value="user.level" icon="⭐" color="purple" sub="текущий уровень" />
</div>
<GlassCard>
<div class="xp-section">
<div class="xp-header">
<span class="xp-label">Прогресс до уровня {{ user.level + 1 }}</span>
<span class="xp-val">{{ xpProgress }} / {{ xpToNext }} XP</span>
</div>
<ProgressBar :value="xpProgress" :max="xpToNext" />
</div>
</GlassCard>
<section>
<div class="section-header">
<h2 class="section-title"> Рекомендуемые лекции</h2>
<button class="link-btn" @click="router.push('/catalog')">Все лекции </button>
</div>
<EmptyState v-if="lectures.loading" title="Загружаем рекомендации" subtitle="Получаем данные с backend." />
<div v-else class="cards-grid">
<LectureCard
v-for="l in recommended"
:key="l.id"
:lecture="l"
:registered="lectures.registeredIds.includes(l.id)"
@register="lectures.register"
/>
</div>
</section>
<section class="two-column">
<GlassCard>
<div class="section-title">🏆 Достижения</div>
<div class="achievements">
<AchievementBadge
v-for="a in achievements"
:key="a.id"
:icon="a.icon"
:title="a.title"
:description="a.description"
:unlocked="a.unlocked"
:unlockedAt="a.unlockedAt"
:coins="a.coins"
/>
</div>
</GlassCard>
<GlassCard>
<div class="section-title">🔔 Напоминания</div>
<div class="reminders">
<div class="reminder-item" v-for="n in reminders" :key="n.id">
<div class="reminder-title">{{ n.title }}</div>
<div class="reminder-body">{{ n.body }}</div>
<div class="reminder-date">{{ new Date(n.createdAt).toLocaleDateString('ru-RU') }}</div>
</div>
</div>
</GlassCard>
</section>
</div>
</template>
<style scoped>
.dashboard { display: flex; flex-direction: column; gap: 24px; }
.dashboard-welcome {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.quick-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.next-lecture { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.next-title { font-size: 18px; font-weight: 700; margin: 6px 0; }
.next-meta { display: flex; flex-direction: column; gap: 4px; color: var(--color-text-secondary); font-size: 13px; }
.next-actions { display: flex; gap: 10px; align-items: flex-start; }
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
.xp-section { display: flex; flex-direction: column; gap: 10px; }
.xp-header { display: flex; justify-content: space-between; font-size: 13px; font-weight: 600; }
.xp-val { color: var(--color-text-secondary); }
.section-header { display: flex; align-items: center; justify-content: space-between; }
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
margin-top: 12px;
}
.link-btn {
background: none;
border: none;
color: var(--color-primary-dark);
font-weight: 600;
font-size: 14px;
cursor: pointer;
}
.two-column { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
.achievements { display: flex; flex-direction: column; gap: 12px; margin-top: 10px; }
.reminders { display: flex; flex-direction: column; gap: 12px; margin-top: 10px; }
.reminder-item {
border-bottom: 1px solid var(--color-border-glass);
padding-bottom: 10px;
}
.reminder-item:last-child { border-bottom: none; padding-bottom: 0; }
.reminder-title { font-weight: 700; margin-bottom: 4px; }
.reminder-body { font-size: 13px; color: var(--color-text-secondary); }
.reminder-date { font-size: 11px; color: var(--color-text-secondary); margin-top: 4px; }
</style>