feat: первое подключение фронтенда
🚀 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
🚀 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
This commit is contained in:
+137
-68
@@ -1,90 +1,159 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { User, UserRole } from '@/types'
|
||||
import { computed, ref } from 'vue'
|
||||
import { authApi } from '@/api'
|
||||
import { mapApiUser } from '@/api/mappers'
|
||||
import { buildApiUrl, setApiAccessToken } from '@/api/client'
|
||||
import type { AuthResponse } from '@/api/types'
|
||||
import type { User } from '@/types'
|
||||
|
||||
const defaultUsers: Record<UserRole, User> = {
|
||||
student: {
|
||||
id: 'stu-1',
|
||||
name: 'Алексей Морозов',
|
||||
email: 'a.morozov@sfedu.ru',
|
||||
role: 'student',
|
||||
institute: 'ИКТИБ',
|
||||
department: 'Программная инженерия',
|
||||
year: 3,
|
||||
direction: 'Программная инженерия',
|
||||
coins: 340,
|
||||
level: 3,
|
||||
xp: 120,
|
||||
lecturesAttended: 12,
|
||||
hoursLearned: 18.5,
|
||||
achievements: ['1', '2', '3'],
|
||||
},
|
||||
teacher: {
|
||||
id: 't-1',
|
||||
name: 'Михаил Сергеевич Волков',
|
||||
email: 'm.volkov@sfedu.ru',
|
||||
role: 'teacher',
|
||||
institute: 'ИКТИБ',
|
||||
department: 'каф. Информатики',
|
||||
year: 0,
|
||||
direction: 'Информатика и вычислительная техника',
|
||||
coins: 90,
|
||||
level: 4,
|
||||
xp: 240,
|
||||
lecturesAttended: 24,
|
||||
hoursLearned: 56,
|
||||
achievements: ['1', '2', '3', '4'],
|
||||
},
|
||||
admin: {
|
||||
id: 'adm-1',
|
||||
name: 'Виктор Алексеев',
|
||||
email: 'admin@sfedu.ru',
|
||||
role: 'admin',
|
||||
institute: 'ЮФУ',
|
||||
department: 'Администрация',
|
||||
year: 0,
|
||||
direction: 'Цифровое развитие',
|
||||
coins: 0,
|
||||
level: 5,
|
||||
xp: 500,
|
||||
lecturesAttended: 0,
|
||||
hoursLearned: 0,
|
||||
achievements: [],
|
||||
},
|
||||
const TOKEN_STORAGE_KEY = 'universe.accessToken'
|
||||
|
||||
function applyAuthResponse(response: AuthResponse) {
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, response.accessToken)
|
||||
setApiAccessToken(response.accessToken)
|
||||
return mapApiUser(response.user)
|
||||
}
|
||||
|
||||
function getAuthReturnUrl() {
|
||||
return import.meta.env.VITE_AUTH_RETURN_URL || '/auth/callback'
|
||||
}
|
||||
|
||||
function getAbsoluteAuthReturnUrl() {
|
||||
return new URL(getAuthReturnUrl(), window.location.origin).toString()
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User | null>(null)
|
||||
const isAuthenticated = ref(false)
|
||||
const loading = ref(false)
|
||||
const initialized = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const accessToken = ref<string | null>(localStorage.getItem(TOKEN_STORAGE_KEY))
|
||||
|
||||
async function login(role: UserRole = 'student', shouldFail = false) {
|
||||
if (accessToken.value) setApiAccessToken(accessToken.value)
|
||||
|
||||
const isAuthenticated = computed(() => Boolean(user.value && accessToken.value))
|
||||
|
||||
async function hydrateFromResponse(response: AuthResponse) {
|
||||
accessToken.value = response.accessToken
|
||||
user.value = applyAuthResponse(response)
|
||||
error.value = null
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
if (initialized.value) return isAuthenticated.value
|
||||
loading.value = true
|
||||
error.value = null
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
if (shouldFail) {
|
||||
loading.value = false
|
||||
error.value = 'Не удалось подтвердить доступ через ЮФУ. Попробуйте еще раз.'
|
||||
|
||||
try {
|
||||
const refreshed = await authApi.refresh()
|
||||
await hydrateFromResponse(refreshed)
|
||||
const me = await authApi.me()
|
||||
user.value = mapApiUser(me)
|
||||
return true
|
||||
} catch (refreshError) {
|
||||
if (accessToken.value) {
|
||||
try {
|
||||
const me = await authApi.me()
|
||||
user.value = mapApiUser(me)
|
||||
return true
|
||||
} catch {
|
||||
// Fall through to local cleanup below.
|
||||
}
|
||||
}
|
||||
clearSession()
|
||||
error.value = refreshError instanceof Error ? refreshError.message : null
|
||||
return false
|
||||
} finally {
|
||||
initialized.value = true
|
||||
loading.value = false
|
||||
}
|
||||
user.value = { ...defaultUsers[role] }
|
||||
isAuthenticated.value = true
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function startMicrosoftLogin() {
|
||||
window.location.assign(buildApiUrl('/auth/login/microsoft', { returnUrl: getAuthReturnUrl() }))
|
||||
return true
|
||||
}
|
||||
|
||||
function logout() {
|
||||
async function completeMicrosoftLogin(code: string, state: string | null) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const redirectUri = getAbsoluteAuthReturnUrl()
|
||||
const response = await authApi.loginMicrosoft(code, redirectUri)
|
||||
await hydrateFromResponse(response)
|
||||
initialized.value = true
|
||||
return true
|
||||
} catch (err) {
|
||||
clearSession()
|
||||
error.value = err instanceof Error ? err.message : 'Ошибка авторизации через Microsoft.'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function completeTokenLogin(token: string) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
accessToken.value = token
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, token)
|
||||
setApiAccessToken(token)
|
||||
const me = await authApi.me()
|
||||
user.value = mapApiUser(me)
|
||||
initialized.value = true
|
||||
return true
|
||||
} catch (err) {
|
||||
clearSession()
|
||||
error.value = err instanceof Error ? err.message : 'Не удалось получить пользователя после входа.'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
loading.value = true
|
||||
try {
|
||||
await authApi.logout()
|
||||
} catch {
|
||||
// Local cleanup is still correct if the server session is already gone.
|
||||
} finally {
|
||||
clearSession()
|
||||
initialized.value = true
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
user.value = null
|
||||
isAuthenticated.value = false
|
||||
accessToken.value = null
|
||||
localStorage.removeItem(TOKEN_STORAGE_KEY)
|
||||
setApiAccessToken(null)
|
||||
}
|
||||
|
||||
function switchRole(role?: UserRole) {
|
||||
if (!user.value) return
|
||||
const roles: UserRole[] = ['student', 'teacher', 'admin']
|
||||
const nextRole = (role ?? roles[(roles.indexOf(user.value.role) + 1) % roles.length]) as UserRole
|
||||
user.value = { ...defaultUsers[nextRole] }
|
||||
function setUser(nextUser: User) {
|
||||
user.value = nextUser
|
||||
}
|
||||
|
||||
return { user, isAuthenticated, loading, error, login, logout, switchRole }
|
||||
function switchRole() {
|
||||
error.value = 'Смена роли доступна только через backend.'
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
accessToken,
|
||||
isAuthenticated,
|
||||
loading,
|
||||
initialized,
|
||||
error,
|
||||
initialize,
|
||||
startMicrosoftLogin,
|
||||
completeMicrosoftLogin,
|
||||
completeTokenLogin,
|
||||
logout,
|
||||
clearSession,
|
||||
setUser,
|
||||
switchRole,
|
||||
}
|
||||
})
|
||||
|
||||
+94
-142
@@ -1,163 +1,115 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Lecture } from '@/types'
|
||||
|
||||
export const LECTURES: Lecture[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Введение в нейронные сети и глубокое обучение',
|
||||
description: 'Лекция охватывает базовые концепции нейронных сетей: перцептрон, многослойные сети, метод обратного распространения ошибки, а также современные архитектуры — CNN, RNN, Transformer. Рассматриваются практические примеры на Python с использованием PyTorch.',
|
||||
teacher: 'Волков М.С.',
|
||||
teacherTitle: 'Профессор',
|
||||
department: 'каф. Информатики',
|
||||
institute: 'ИКТИБ',
|
||||
date: '2025-05-07',
|
||||
time: '14:00',
|
||||
duration: 90,
|
||||
building: 'ИКТИБ',
|
||||
room: '305',
|
||||
format: 'offline',
|
||||
totalSeats: 30,
|
||||
freeSeats: 12,
|
||||
tags: ['#ML', '#ИИ', '#Python', '#нейросети'],
|
||||
rating: 4.8,
|
||||
reviewCount: 24,
|
||||
status: 'upcoming',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Квантовые вычисления: от теории к практике',
|
||||
description: 'Обзор квантовых алгоритмов и их применения. Рассматриваются кубиты, суперпозиция, запутанность, алгоритмы Шора и Гровера, а также введение в программирование на Qiskit.',
|
||||
teacher: 'Петров А.И.',
|
||||
teacherTitle: 'Доцент',
|
||||
department: 'каф. Теоретической физики',
|
||||
institute: 'ИФиМКН',
|
||||
date: '2025-05-08',
|
||||
time: '16:00',
|
||||
duration: 120,
|
||||
building: 'ИФиМКН',
|
||||
room: '201',
|
||||
format: 'offline',
|
||||
totalSeats: 25,
|
||||
freeSeats: 5,
|
||||
tags: ['#квантовые-вычисления', '#физика', '#алгоритмы'],
|
||||
rating: 4.6,
|
||||
reviewCount: 18,
|
||||
status: 'upcoming',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Современные методы биоинформатики',
|
||||
description: 'Введение в биоинформатику: анализ последовательностей ДНК/РНК, геномная сборка, аннотация генов, инструменты BLAST, Biopython. Актуальные задачи вычислительной биологии.',
|
||||
teacher: 'Смирнова Е.В.',
|
||||
teacherTitle: 'Доктор биологических наук',
|
||||
department: 'каф. Биологии',
|
||||
institute: 'АГиС',
|
||||
date: '2025-05-09',
|
||||
time: '10:00',
|
||||
duration: 90,
|
||||
building: 'АГиС',
|
||||
room: '118',
|
||||
format: 'offline',
|
||||
totalSeats: 20,
|
||||
freeSeats: 2,
|
||||
tags: ['#биоинформатика', '#генетика', '#Python'],
|
||||
rating: 4.7,
|
||||
reviewCount: 31,
|
||||
status: 'upcoming',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Философия цифровой эпохи',
|
||||
description: 'Как цифровые технологии меняют мышление, идентичность и общество. Тема охватывает этику ИИ, постгуманизм, цифровой дуализм и проблему сознания в эпоху автоматизации.',
|
||||
teacher: 'Дмитриев К.О.',
|
||||
teacherTitle: 'Кандидат философских наук',
|
||||
department: 'каф. Философии',
|
||||
institute: 'ИФиСН',
|
||||
date: '2025-05-10',
|
||||
time: '18:00',
|
||||
duration: 90,
|
||||
building: 'Онлайн',
|
||||
format: 'online',
|
||||
totalSeats: 40,
|
||||
freeSeats: 16,
|
||||
tags: ['#философия', '#этика', '#ИИ'],
|
||||
rating: 4.5,
|
||||
reviewCount: 42,
|
||||
status: 'upcoming',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Право в информационном обществе',
|
||||
description: 'Правовые аспекты работы с данными: GDPR, ФЗ-152, авторское право в сети, кибербезопасность с точки зрения права, ответственность разработчиков и операторов персональных данных.',
|
||||
teacher: 'Захарова Н.А.',
|
||||
teacherTitle: 'Доцент',
|
||||
department: 'каф. Гражданского права',
|
||||
institute: 'ЮФ',
|
||||
date: '2025-05-12',
|
||||
time: '15:30',
|
||||
duration: 90,
|
||||
building: 'ЮФ',
|
||||
room: '412',
|
||||
format: 'offline',
|
||||
totalSeats: 30,
|
||||
freeSeats: 0,
|
||||
registrationClosed: true,
|
||||
tags: ['#право', '#данные', '#GDPR'],
|
||||
rating: 4.4,
|
||||
reviewCount: 15,
|
||||
status: 'upcoming',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Нейромаркетинг и поведение потребителей',
|
||||
description: 'Как нейронауки применяются в маркетинге: eye-tracking, EEG-анализ реакций, влияние UX на покупки, нейропсихология принятия решений и кейсы ведущих брендов.',
|
||||
teacher: 'Орлов П.Р.',
|
||||
teacherTitle: 'Кандидат экономических наук',
|
||||
department: 'каф. Маркетинга',
|
||||
institute: 'ИУЭиП',
|
||||
date: '2025-05-14',
|
||||
time: '11:00',
|
||||
duration: 120,
|
||||
building: 'Онлайн',
|
||||
format: 'online',
|
||||
totalSeats: 35,
|
||||
freeSeats: 27,
|
||||
tags: ['#маркетинг', '#нейронауки', '#поведение'],
|
||||
rating: 4.3,
|
||||
reviewCount: 9,
|
||||
status: 'upcoming',
|
||||
},
|
||||
]
|
||||
import { lecturesApi, usersApi } from '@/api'
|
||||
import { mapApiLecture, mapApiReview } from '@/api/mappers'
|
||||
import type { Lecture, Review } from '@/types'
|
||||
|
||||
export const useLecturesStore = defineStore('lectures', () => {
|
||||
const lectures = ref<Lecture[]>(LECTURES)
|
||||
const registered = ref<string[]>(['1', '3'])
|
||||
const lectures = ref<Lecture[]>([])
|
||||
const registered = ref<string[]>([])
|
||||
const reviewsByLecture = ref<Record<string, Review[]>>({})
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const all = computed(() => lectures.value)
|
||||
const registeredIds = computed(() => registered.value)
|
||||
const registeredLectures = computed(() =>
|
||||
lectures.value.filter(l => registered.value.includes(l.id))
|
||||
lectures.value.filter(l => registered.value.includes(l.id) || l.registered),
|
||||
)
|
||||
|
||||
function register(lectureId: string) {
|
||||
if (!registered.value.includes(lectureId)) {
|
||||
const l = lectures.value.find(x => x.id === lectureId)
|
||||
if (!l || l.freeSeats === 0 || l.registrationClosed) return
|
||||
registered.value.push(lectureId)
|
||||
l.freeSeats--
|
||||
async function fetchLectures() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const payload = await lecturesApi.list({ PageSize: 100 })
|
||||
lectures.value = payload.map(mapApiLecture)
|
||||
registered.value = lectures.value.filter(l => l.registered).map(l => l.id)
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Не удалось загрузить лекции.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function unregister(lectureId: string) {
|
||||
async function fetchLecture(id: string) {
|
||||
error.value = null
|
||||
try {
|
||||
const lecture = mapApiLecture(await lecturesApi.get(id))
|
||||
const index = lectures.value.findIndex(item => item.id === lecture.id)
|
||||
if (index >= 0) lectures.value[index] = lecture
|
||||
else lectures.value.push(lecture)
|
||||
if (lecture.registered && !registered.value.includes(lecture.id)) registered.value.push(lecture.id)
|
||||
return lecture
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Не удалось загрузить лекцию.'
|
||||
return lectures.value.find(item => item.id === id)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRegisteredForUser(userId: string) {
|
||||
try {
|
||||
const enrollments = await usersApi.enrollments(userId)
|
||||
const mapped = enrollments.map(mapApiLecture)
|
||||
if (mapped.length) {
|
||||
mapped.forEach(lecture => {
|
||||
const index = lectures.value.findIndex(item => item.id === lecture.id)
|
||||
if (index >= 0) lectures.value[index] = { ...lectures.value[index], ...lecture, registered: true }
|
||||
else lectures.value.push({ ...lecture, registered: true })
|
||||
})
|
||||
registered.value = mapped.map(lecture => lecture.id)
|
||||
}
|
||||
} catch {
|
||||
// Some backend builds return an empty 200 for this endpoint; catalog detail still carries isEnrolled.
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchReviews(lectureId: string) {
|
||||
try {
|
||||
reviewsByLecture.value[lectureId] = (await lecturesApi.reviews(lectureId)).map(mapApiReview)
|
||||
} catch {
|
||||
reviewsByLecture.value[lectureId] = []
|
||||
}
|
||||
}
|
||||
|
||||
async function register(lectureId: string) {
|
||||
const lecture = lectures.value.find(item => item.id === lectureId)
|
||||
if (!lecture || lecture.freeSeats === 0 || lecture.registrationClosed || registered.value.includes(lectureId)) return
|
||||
|
||||
await lecturesApi.enroll(lectureId)
|
||||
registered.value.push(lectureId)
|
||||
lecture.freeSeats = Math.max(lecture.freeSeats - 1, 0)
|
||||
lecture.registered = true
|
||||
}
|
||||
|
||||
async function unregister(lectureId: string) {
|
||||
await lecturesApi.unenroll(lectureId)
|
||||
registered.value = registered.value.filter(id => id !== lectureId)
|
||||
const l = lectures.value.find(x => x.id === lectureId)
|
||||
if (l) l.freeSeats++
|
||||
const lecture = lectures.value.find(item => item.id === lectureId)
|
||||
if (lecture) {
|
||||
lecture.freeSeats = Math.min(lecture.freeSeats + 1, lecture.totalSeats)
|
||||
lecture.registered = false
|
||||
}
|
||||
}
|
||||
|
||||
function isRegistered(lectureId: string) {
|
||||
return registered.value.includes(lectureId)
|
||||
}
|
||||
|
||||
return { lectures, registered, all, registeredIds, registeredLectures, register, unregister, isRegistered }
|
||||
return {
|
||||
lectures,
|
||||
registered,
|
||||
reviewsByLecture,
|
||||
loading,
|
||||
error,
|
||||
all,
|
||||
registeredIds,
|
||||
registeredLectures,
|
||||
fetchLectures,
|
||||
fetchLecture,
|
||||
fetchRegisteredForUser,
|
||||
fetchReviews,
|
||||
register,
|
||||
unregister,
|
||||
isRegistered,
|
||||
}
|
||||
})
|
||||
|
||||
+51
-29
@@ -1,37 +1,50 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { Achievement, Notification, CoinTransaction } from '@/types'
|
||||
import { usersApi } from '@/api'
|
||||
import { mapApiAchievement, mapApiCoinTransaction } from '@/api/mappers'
|
||||
import type { Achievement, CoinTransaction, Notification } from '@/types'
|
||||
import { useAuthStore } from './auth'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const achievements = ref<Achievement[]>([
|
||||
{ id: '1', title: 'Первый отзыв', description: 'Оставьте первый отзыв о лекции', icon: '⭐', unlocked: true, unlockedAt: '2025-04-10', coins: 20 },
|
||||
{ id: '2', title: 'Межфакультетский исследователь', description: 'Посетите лекции 3 разных институтов', icon: '🔭', unlocked: true, unlockedAt: '2025-04-18', coins: 50 },
|
||||
{ id: '3', title: '10 часов лекций', description: 'Наберите 10 часов посещённых лекций', icon: '⏱', unlocked: true, unlockedAt: '2025-04-25', coins: 30 },
|
||||
{ id: '4', title: 'Полезный критик', description: 'Получите 5 монет за качественный отзыв', icon: '💡', unlocked: false },
|
||||
{ id: '5', title: 'Знаток науки', description: 'Посетите лекции по 5 разным тематикам', icon: '🎓', unlocked: false },
|
||||
{ id: '6', title: 'Ранний пташка', description: 'Запишитесь на лекцию за 7 дней до начала', icon: '🌅', unlocked: false },
|
||||
{ id: '7', title: 'Социальная бабочка', description: 'Приведите друга на межфакультетскую лекцию', icon: '🦋', unlocked: false },
|
||||
])
|
||||
const achievements = ref<Achievement[]>([])
|
||||
const notifications = ref<Notification[]>([])
|
||||
const coinHistory = ref<CoinTransaction[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const notifications = ref<Notification[]>([
|
||||
{ id: '1', type: 'reminder', title: 'Напоминание о лекции', body: 'Завтра в 14:00 — «Введение в нейронные сети». Ауд. 305, ИКТИБ', read: false, createdAt: '2025-05-06T09:00:00' },
|
||||
{ id: '2', type: 'coins', title: 'Начислено 20 монет', body: 'Ваш отзыв о лекции «Квантовые вычисления» признан полезным', read: false, createdAt: '2025-05-05T18:30:00' },
|
||||
{ id: '3', type: 'achievement', title: 'Новое достижение!', body: 'Вы получили значок «Межфакультетский исследователь» 🔭', read: false, createdAt: '2025-05-04T12:00:00' },
|
||||
{ id: '4', type: 'recommendation', title: 'Рекомендация для вас', body: 'Новая лекция «Нейромаркетинг» — может быть интересна вам', read: true, createdAt: '2025-05-03T10:00:00' },
|
||||
{ id: '5', type: 'schedule-change', title: 'Изменение расписания', body: 'Лекция «Философия цифровой эпохи» перенесена с 18:00 на 19:00', read: true, createdAt: '2025-05-02T16:00:00' },
|
||||
{ id: '6', type: 'coins', title: 'Начислено 30 монет', body: 'Поздравляем с достижением «10 часов лекций»!', read: true, createdAt: '2025-04-25T11:00:00' },
|
||||
])
|
||||
async function fetchStudentData(userId?: string) {
|
||||
const auth = useAuthStore()
|
||||
const id = userId ?? auth.user?.id
|
||||
if (!id) return
|
||||
|
||||
const coinHistory = ref<CoinTransaction[]>([
|
||||
{ id: '1', date: '2025-05-05', description: 'Полезный отзыв о лекции', amount: 20, type: 'earned' },
|
||||
{ id: '2', date: '2025-04-25', description: 'Достижение «10 часов лекций»', amount: 30, type: 'earned' },
|
||||
{ id: '3', date: '2025-04-18', description: 'Достижение «Исследователь»', amount: 50, type: 'earned' },
|
||||
{ id: '4', date: '2025-04-10', description: 'Первый отзыв', amount: 20, type: 'earned' },
|
||||
{ id: '5', date: '2025-04-05', description: 'Покупка стикерпака ЮФУ', amount: -80, type: 'spent' },
|
||||
{ id: '6', date: '2025-03-20', description: 'Посещение серии лекций', amount: 60, type: 'earned' },
|
||||
{ id: '7', date: '2025-03-10', description: 'Покупка термокружки ЮФУ', amount: -120, type: 'spent' },
|
||||
{ id: '8', date: '2025-02-28', description: 'Первое посещение лекции вне факультета', amount: 40, type: 'earned' },
|
||||
])
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const [stats, achievementPayload, transactions] = await Promise.all([
|
||||
usersApi.stats(id),
|
||||
usersApi.achievements(id),
|
||||
usersApi.transactions(id),
|
||||
])
|
||||
|
||||
if (auth.user) {
|
||||
auth.setUser({
|
||||
...auth.user,
|
||||
coins: stats.coins,
|
||||
level: stats.level,
|
||||
xp: stats.xp,
|
||||
lecturesAttended: stats.attendedLectures,
|
||||
hoursLearned: Math.round(stats.attendedLectures * 1.5 * 10) / 10,
|
||||
achievements: Array.from({ length: stats.achievementsCount }, (_, index) => String(index + 1)),
|
||||
})
|
||||
}
|
||||
achievements.value = achievementPayload.map(mapApiAchievement)
|
||||
coinHistory.value = transactions.map(mapApiCoinTransaction)
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Не удалось загрузить данные профиля.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function markAllRead() {
|
||||
notifications.value.forEach(n => (n.read = true))
|
||||
@@ -39,5 +52,14 @@ export const useUserStore = defineStore('user', () => {
|
||||
|
||||
const unreadCount = () => notifications.value.filter(n => !n.read).length
|
||||
|
||||
return { achievements, notifications, coinHistory, markAllRead, unreadCount }
|
||||
return {
|
||||
achievements,
|
||||
notifications,
|
||||
coinHistory,
|
||||
loading,
|
||||
error,
|
||||
fetchStudentData,
|
||||
markAllRead,
|
||||
unreadCount,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user