Files
UniVerse/frontend/src/views/student/NotificationsView.vue
T
serega404 b0a4a6d259
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 9s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 26s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 19s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 8s
feat: добавил личные уведомления для пользователей
Реализовал хранение, получение и отметку прочитанными пользовательских уведомлений. Обновил фронтенд для отображения и управления уведомлениями в профиле студента.
2026-05-12 23:54:55 +03:00

93 lines
3.0 KiB
Vue

<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import GlassCard from '@/components/ui/GlassCard.vue'
import AppIcon from '@/components/ui/AppIcon.vue'
import EmptyState from '@/components/ui/EmptyState.vue'
const userStore = useUserStore()
onMounted(() => {
void userStore.fetchNotifications()
})
const grouped = computed(() => {
const map: Record<string, typeof userStore.notifications> = {}
userStore.notifications.forEach(n => {
const day = new Date(n.createdAt).toLocaleDateString('ru-RU')
map[day] = map[day] || []
map[day].push(n)
})
return Object.entries(map)
})
const typeIcon: Record<string, string> = {
reminder: 'alarm',
'schedule-change': 'calendar-event',
achievement: 'trophy',
coins: 'coin',
recommendation: 'sparkles',
}
</script>
<template>
<div class="notifications page-content">
<div class="header">
<h1 class="page-title">Уведомления</h1>
<button class="btn-secondary" @click="userStore.markAllRead">Отметить все как прочитанные</button>
</div>
<div v-if="userStore.notifications.length === 0" class="empty-wrap">
<EmptyState
icon="bell"
title="Нет уведомлений"
subtitle="Здесь будут появляться важные события и обновления"
/>
</div>
<div v-else class="notification-groups">
<GlassCard v-for="([day, items]) in grouped" :key="day" class="group">
<div class="group-title">{{ day }}</div>
<div class="items">
<div v-for="n in items" :key="n.id" class="item" :class="{ unread: !n.read }">
<AppIcon class="icon" :icon="typeIcon[n.type]" :size="20" />
<div>
<div class="item-title">{{ n.title }}</div>
<div class="item-body">{{ n.body }}</div>
</div>
</div>
</div>
</GlassCard>
</div>
</div>
</template>
<style scoped>
.notifications {
display: flex;
flex-direction: column;
gap: 18px;
min-height: calc(100vh - var(--topbar-height) - 28px - 80px);
}
.header { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.notification-groups { display: flex; flex-direction: column; gap: 14px; }
.group-title { font-weight: 700; margin-bottom: 10px; }
.items { display: flex; flex-direction: column; gap: 10px; }
.item { display: flex; gap: 12px; padding: 10px; border-radius: var(--radius-sm); background: rgba(255,255,255,0.6); border: 1px solid var(--color-border-glass); }
.item.unread { border-color: rgba(34,197,94,0.4); background: rgba(34,197,94,0.08); }
.icon { color: var(--color-text); flex-shrink: 0; }
.item-title { font-weight: 600; }
.item-body { font-size: 13px; color: var(--color-text-secondary); }
.empty-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.notifications { min-height: calc(100vh - var(--topbar-height) - 16px - 80px); }
}
</style>