Files
enlight-lms/frontend/src/pages/LeaderBoard.vue
Alexandrina-Kuzeleva c6d05111cc Update LeaderBoard.vue
2025-11-25 15:34:35 +03:00

289 lines
12 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.
<template>
<!-- ДЕБАГ ПАНЕЛЬ ВСЕГДА ВИДНА -->
<div class="fixed top-4 right-4 z-50 space-y-3">
<div class="bg-red-600 text-white p-4 rounded-lg shadow-2xl font-mono text-xs max-w-sm">
<p class="font-bold">ДЕБАГ ИНФО</p>
<p>Пользователь: {{ currentUserEmail }}</p>
<p>Роли: {{ currentUserRoles.join(', ') || 'нет' }}</p>
<p>Текущая роль: {{ currentUserRole || 'не определена' }}</p>
<p>Выбрана вкладка: {{ selectedRole }}</p>
<p>Всего пользователей в ЛБ: {{ leaderboard.length }}</p>
<p>В текущей группе: {{ filteredLeaderboard.length }}</p>
<p>Загрузка: {{ loading ? 'идёт...' : 'завершена' }}</p>
</div>
<button @click="loadData()" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm font-medium">
Перезагрузить данные
</button>
</div>
<div v-if="loading" class="flex justify-center items-center py-20">
<div class="text-center">
<div class="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-600 border-t-transparent mb-4"></div>
<p class="text-xl text-gray-600">Загружаем таблицы лидеров...</p>
</div>
</div>
<div v-else class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-3xl font-bold text-center mb-8 text-gray-900">
Таблица лидеров по очкам
</h1>
<!-- Переключатель ролей -->
<div class="flex justify-center mb-10">
<div class="inline-flex rounded-lg shadow-lg bg-white p-1" role="group">
<button
v-for="tab in tabs"
:key="tab.role"
@click="selectedRole = tab.role"
:class="[ 'px-6 py-3 text-sm font-medium rounded-md transition-all duration-200',
selectedRole === tab.role ? 'bg-indigo-600 text-white shadow-md' : 'text-gray-700 hover:bg-gray-100' ]"
>
{{ tab.label }}
<span class="ml-2 font-bold">({{ countByRole(tab.role) }})</span>
</button>
</div>
</div>
<!-- Нет данных -->
<div v-if="leaderboard.length === 0" class="text-center py-20">
<div class="bg-yellow-100 border border-yellow-400 text-yellow-800 px-8 py-6 rounded-xl max-w-2xl mx-auto">
<p class="text-2xl font-bold mb-2">Нет данных по баллам</p>
<p>Проверьте, есть ли записи в <code>Energy Point Log</code> и правильно ли загружаются роли.</p>
<button @click="loadData()" class="mt-4 bg-yellow-600 text-white px-6 py-3 rounded hover:bg-yellow-700">
Попробовать снова
</button>
</div>
</div>
<div v-else>
<!-- Карточки -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
<!-- Ваш результат -->
<div class="bg-white rounded-xl shadow-xl p-8 border border-gray-200">
<h2 class="text-2xl font-semibold text-gray-800 mb-6">Ваш результат</h2>
<div v-if="currentUserInGroup" class="text-center">
<img :src="currentUserStatusIcon" class="w-24 h-24 mx-auto mb-4">
<p :class="['text-3xl font-bold mb-3', currentUserStatusClass]">
{{ currentUserStatusLabel }}
</p>
<p class="text-4xl font-bold text-indigo-600 mt-6">
{{ currentUserGroupPoints }} баллов
</p>
</div>
<div v-else class="text-center text-gray-500">
<p class="text-xl">Вы не входите в группу «{{ selectedRole }}»</p>
<p class="text-4xl font-bold text-gray-400 mt-4">{{ currentUserGroupPoints }} баллов</p>
</div>
</div>
<!-- Лучший в группе -->
<div class="bg-white rounded-xl shadow-xl p-8 border border-gray-200 text-center">
<h2 class="text-2xl font-semibold text-gray-800 mb-6">Лучший участник</h2>
<div v-if="groupTopUser">
<div class="relative inline-block">
<div class="w-28 h-28 rounded-full bg-gradient-to-br from-yellow-400 to-yellow-600 flex items-center justify-center text-5xl font-bold text-white shadow-2xl">
{{ groupTopInitial }}
</div>
<img src="/leaderboard/gold-cup.png" class="absolute -top-2 -right-4 w-16 h-16">
</div>
<a :href="userProfileLink(groupTopUser.user)" class="block mt-4 text-2xl font-bold text-indigo-600 hover:underline">
{{ groupTopUser.full_name }}
</a>
<p class="text-4xl font-bold text-yellow-500 mt-3">{{ groupTopUser.points }} баллов</p>
</div>
<p v-else class="text-gray-500">Нет участников</p>
</div>
</section>
<!-- Таблица -->
<section class="bg-white rounded-xl shadow-xl border border-gray-200 overflow-hidden">
<div class="px-8 py-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-purple-50">
<h2 class="text-2xl font-semibold text-gray-800">
Топ {{ tabs.find(t => t.role === selectedRole)?.title }}
</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-4 text-sm font-medium text-gray-700">#</th>
<th class="px-6 py-4 text-sm font-medium text-gray-700">Пользователь</th>
<th class="px-6 py-4 text-sm font-medium text-gray-700">Баллы</th>
<th class="px-6 py-4 text-sm font-medium text-gray-700">Бонус</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="(item, index) in filteredLeaderboard" :key="item.user"
:class="item.user === currentUserEmail ? 'bg-indigo-50 font-semibold' : 'hover:bg-gray-50'">
<td class="px-6 py-5 text-lg font-bold">{{ index + 1 }}</td>
<td class="px-6 py-5">
<div class="flex items-center space-x-4">
<div class="relative">
<div class="w-12 h-12 rounded-full bg-indigo-600 flex items-center justify-center text-white font-bold text-xl">
{{ getUserInitial(item.user) }}
</div>
<img v-if="index === 0" src="/leaderboard/gold-cup.png" class="absolute -top-3 -right-3 w-10 h-10">
<img v-else-if="index === 1" src="/leaderboard/silver-cup.png" class="absolute -top-3 -right-3 w-10 h-10">
<img v-else-if="index === 2" src="/leaderboard/bronze-cup.png" class="absolute -top-3 -right-3 w-10 h-10">
</div>
<a :href="userProfileLink(item.user)" class="text-indigo-600 hover:underline">
{{ item.full_name }}
</a>
</div>
</td>
<td class="px-6 py-5 font-medium">{{ item.points }}</td>
<td class="px-6 py-5">
<span class="inline-flex items-center px-4 py-2 rounded-full text-sm font-bold bg-green-100 text-green-800">
+{{ item.bonus }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</div>
</template>
<script setup>
import { inject, ref, computed, onMounted, watchEffect } from "vue"
import { createResource } from "frappe-ui"
import { usersStore } from '@/stores/user'
import { sessionStore } from '@/stores/session'
const $user = inject("$user")
const currentUserEmail = $user.data.email
const currentUserRoles = computed(() => $user.data.roles || [])
const { userResource } = usersStore()
const { user } = sessionStore()
const loading = ref(true)
const leaderboard = ref([])
const tabs = [
{ role: "Course Creator", label: "Преподаватели", title: "Преподаватели" },
{ role: "LMS Student", label: "Студенты", title: "Студенты ВУЗа" },
{ role: "LMS Schoolchild",label: "Школьники", title: "Школьники" }
]
const selectedRole = ref("LMS Student")
const currentUserRole = computed(() => {
if (currentUserRoles.value.includes("Course Creator")) return "Course Creator"
if (currentUserRoles.value.includes("LMS Student")) return "LMS Student"
if (currentUserRoles.value.includes("LMS Schoolchild")) return "LMS Schoolchild"
return null
})
// Автовыбор роли
watchEffect(() => {
if (currentUserRole.value && !loading.value) selectedRole.value = currentUserRole.value
})
// Получаем роли пользователя безопасно
function getRolesByEmail(email) {
if (!userResource.data || !Array.isArray(userResource.data)) return []
const u = userResource.data.find(u => u.name === email)
return u?.roles || []
}
// Подсчёт по ролям для табов
const countByRole = (role) => {
if (!userResource.data || !Array.isArray(userResource.data)) return 0
return userResource.data.filter(u => u.roles.includes(role)).length
}
// Фильтруем leaderboard по выбранной роли
const filteredLeaderboard = computed(() => {
if (!userResource.data || !Array.isArray(userResource.data)) return []
return leaderboard.value
.map(u => ({ ...u, roles: getRolesByEmail(u.user) }))
.filter(u => u.roles.includes(selectedRole.value))
})
const currentUserInGroup = computed(() => currentUserRole.value === selectedRole.value)
const currentUserGroupPoints = computed(() => {
const u = leaderboard.value.find(x => x.user === currentUserEmail)
return u?.points || 0
})
const currentUserGroupPosition = computed(() => {
if (!currentUserInGroup.value) return null
const idx = filteredLeaderboard.value.findIndex(x => x.user === currentUserEmail)
return idx >= 0 ? idx + 1 : null
})
const currentUserStatusLabel = computed(() => "Чемпион")
const currentUserStatusIcon = computed(() => {
const pos = currentUserGroupPosition.value
if (pos === 1) return "/leaderboard/gold-cup.png"
if (pos === 2) return "/leaderboard/silver-cup.png"
if (pos === 3) return "/leaderboard/bronze-cup.png"
return "/leaderboard/dart-board.svg"
})
const currentUserStatusClass = computed(() => {
const pos = currentUserGroupPosition.value
if (pos === 1) return "text-yellow-500"
if (pos === 2) return "text-gray-500"
if (pos === 3) return "text-orange-600"
return "text-indigo-600"
})
const groupTopUser = computed(() => filteredLeaderboard.value[0] || null)
const groupTopInitial = computed(() => groupTopUser.value?.user[0].toUpperCase() || "N/A")
function getUserInitial(email) { return email?.[0].toUpperCase() || "?" }
function userProfileLink(email) {
const u = leaderboard.value.find(x => x.user === email)
return u ? `/lms/user/${u.username}` : "#"
}
// Ресурс для логов очков
const logsResource = createResource({
url: "frappe.client.get_list",
params: { doctype: "Energy Point Log", fields: ["user", "points"], limit_page_length: 10000 },
auto: false,
onError: (err) => console.error("Ошибка загрузки Energy Point Log:", err)
})
async function loadData() {
console.clear()
console.log("Загрузка данных...")
loading.value = true
try {
await userResource.fetch()
const logsResponse = await logsResource.fetch()
const pointsMap = {}
logsResponse.forEach(log => {
if (!pointsMap[log.user]) pointsMap[log.user] = 0
pointsMap[log.user] += log.points
})
leaderboard.value = Object.keys(pointsMap).map(user => {
const u = userResource.data?.find(x => x.name === user) || { full_name: user, username: user.split('@')[0], roles: [] }
return {
user,
points: pointsMap[user],
full_name: u.full_name,
username: u.username,
roles: u.roles,
bonus: Math.min(Math.floor(pointsMap[user]/100), 10)
}
}).sort((a,b) => b.points - a.points)
console.log("Leaderboard:", leaderboard.value)
} catch(err) {
console.error("Ошибка загрузки:", err)
} finally {
loading.value = false
}
}
onMounted(() => loadData())
</script>