TEST UPD
- test leader board
This commit is contained in:
264
frontend/src/pages/LeaderBoard.vue
Normal file
264
frontend/src/pages/LeaderBoard.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<div v-if="loading" class="text-center p-10 text-gray-600">
|
||||
Загружаем таблицу лидеров...
|
||||
</div>
|
||||
|
||||
<div v-else class="page-container">
|
||||
|
||||
<h1 class="page-title">Таблица лидеров по очкам</h1>
|
||||
|
||||
<!-- Блок с текущим пользователем и лучшим -->
|
||||
<section class="user-results">
|
||||
|
||||
<!-- Ваш результат -->
|
||||
<div class="box results-box">
|
||||
<h2 class="box-heading">Ваш результат</h2>
|
||||
|
||||
<!-- Статусы -->
|
||||
<template v-if="currentUserPosition">
|
||||
<img
|
||||
class="cup-img"
|
||||
:src="statusIcon"
|
||||
/>
|
||||
<p class="status" :class="statusClass">{{ statusLabel }}</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<img class="cup-img" src="/files/dart-board.svg">
|
||||
<p class="status">Вы пока не в рейтинге</p>
|
||||
</template>
|
||||
|
||||
<p class="points">{{ currentUserPoints }} баллов</p>
|
||||
|
||||
<template v-if="currentUserPosition > 1">
|
||||
<p class="motivation-text">
|
||||
До {{ currentUserPosition - 1 }}-го места нужно:
|
||||
<strong>{{ pointsToNext }} баллов</strong>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Лучший участник -->
|
||||
<div class="box results-box">
|
||||
<h2 class="box-heading">Лучший участник</h2>
|
||||
|
||||
<template v-if="topUser">
|
||||
<div class="initial-circle">
|
||||
{{ topUser.user[0].toUpperCase() }}
|
||||
</div>
|
||||
|
||||
<p class="username">
|
||||
<a
|
||||
class="user-link"
|
||||
:href="`/lms/user/${topUser.username}`"
|
||||
>
|
||||
{{ topUser.full_name }}
|
||||
</a>
|
||||
<img class="cup-badge" src="/files/gold-cup.png">
|
||||
</p>
|
||||
|
||||
<p class="points">{{ topUser.points }} баллов</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<p class="no-leader">Нет лучшего участника</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Таблица лидеров -->
|
||||
<section class="box">
|
||||
<h2 class="box-heading">Топ лидеров</h2>
|
||||
|
||||
<div class="leaders-table-wrapper">
|
||||
<table class="leaders-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Пользователь</th>
|
||||
<th>Баллы активности</th>
|
||||
<th>Общая сумма</th>
|
||||
<th>Бонус (МПГУ)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(item, index) in sorted"
|
||||
:key="item.user"
|
||||
:class="{'current-user': item.user === currentUserEmail}"
|
||||
>
|
||||
<td>{{ index + 1 }}</td>
|
||||
|
||||
<td>
|
||||
<div class="flex items-center">
|
||||
<div class="initial-circle-small">
|
||||
{{ item.user[0].toUpperCase() }}
|
||||
</div>
|
||||
<div>
|
||||
<a :href="`/lms/user/${item.username}`"
|
||||
class="user-link">{{ item.full_name }}</a>
|
||||
|
||||
<!-- значки -->
|
||||
<img v-if="index === 0" class="cup-badge" src="/files/gold-cup.png">
|
||||
<img v-else-if="index === 1" class="cup-badge" src="/files/silver-cup.png">
|
||||
<img v-else-if="index === 2" class="cup-badge" src="/files/bronze-cup.png">
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>{{ item.points }}</td>
|
||||
<td>{{ item.points }}</td>
|
||||
<td>{{ item.bonus }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="stats-note">
|
||||
* Бонус — 1 балл за каждые 100 очков (максимум 10)
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, ref, computed, onMounted } from "vue";
|
||||
import { createResource } from "frappe-ui";
|
||||
|
||||
const $user = inject("$user");
|
||||
const currentUserEmail = $user.data.email;
|
||||
|
||||
const loading = ref(true);
|
||||
|
||||
// Загружаем все Energy Point Log
|
||||
const logs = createResource({
|
||||
url: "frappe.client.get_list",
|
||||
params: {
|
||||
doctype: "Energy Point Log",
|
||||
fields: ["name", "user", "points"],
|
||||
limit_page_length: 2000
|
||||
},
|
||||
auto: false,
|
||||
onSuccess() {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Собираем баллы по пользователям
|
||||
const userPoints = computed(() => {
|
||||
const map = {};
|
||||
|
||||
if (!logs.data) return map;
|
||||
|
||||
logs.data.forEach(entry => {
|
||||
if (!map[entry.user]) map[entry.user] = 0;
|
||||
map[entry.user] += entry.points;
|
||||
});
|
||||
|
||||
return map;
|
||||
});
|
||||
|
||||
// Загружаем данные пользователей (имя + username)
|
||||
const userCache = ref({});
|
||||
|
||||
async function loadUserProfile(email) {
|
||||
if (userCache.value[email]) return userCache.value[email];
|
||||
|
||||
const profile = await frappe.call({
|
||||
method: "frappe.client.get",
|
||||
args: {
|
||||
doctype: "User",
|
||||
name: email
|
||||
}
|
||||
});
|
||||
|
||||
userCache.value[email] = {
|
||||
full_name: profile.message.full_name,
|
||||
username: profile.message.username
|
||||
};
|
||||
|
||||
return userCache.value[email];
|
||||
}
|
||||
|
||||
// Готовим сортированный массив
|
||||
const sorted = ref([]);
|
||||
|
||||
async function buildSorted() {
|
||||
const arr = [];
|
||||
|
||||
for (const [user, points] of Object.entries(userPoints.value)) {
|
||||
const data = await loadUserProfile(user);
|
||||
|
||||
arr.push({
|
||||
user,
|
||||
points,
|
||||
full_name: data.full_name,
|
||||
username: data.username,
|
||||
bonus: Math.min(Math.floor(points / 100), 10)
|
||||
});
|
||||
}
|
||||
|
||||
arr.sort((a, b) => b.points - a.points);
|
||||
|
||||
sorted.value = arr;
|
||||
}
|
||||
|
||||
// Текущий пользователь
|
||||
const currentUserPoints = computed(
|
||||
() => userPoints.value[currentUserEmail] || 0
|
||||
);
|
||||
|
||||
const currentUserPosition = computed(() => {
|
||||
const index = sorted.value.findIndex(u => u.user === currentUserEmail);
|
||||
return index === -1 ? null : index + 1;
|
||||
});
|
||||
|
||||
const pointsToNext = computed(() => {
|
||||
if (!currentUserPosition.value || currentUserPosition.value <= 1) return 0;
|
||||
const next = sorted.value[currentUserPosition.value - 2];
|
||||
return next.points - currentUserPoints.value;
|
||||
});
|
||||
|
||||
// Топ-1 участник
|
||||
const topUser = computed(() => sorted.value[0]);
|
||||
|
||||
// UI статусы
|
||||
const statusLabel = computed(() => {
|
||||
if (!currentUserPosition.value) return "Не в рейтинге";
|
||||
|
||||
if (currentUserPosition.value === 1) return "Чемпион";
|
||||
if (currentUserPosition.value === 2) return "Второе место";
|
||||
if (currentUserPosition.value === 3) return "Третье место";
|
||||
if (currentUserPosition.value <= 5) return "В топ-5";
|
||||
if (currentUserPosition.value <= 10) return "В топ-10";
|
||||
|
||||
return "Стремитесь выше";
|
||||
});
|
||||
|
||||
const statusIcon = computed(() => {
|
||||
if (currentUserPosition.value === 1) return "/files/gold-cup.png";
|
||||
if (currentUserPosition.value === 2) return "/files/silver-cup.png";
|
||||
if (currentUserPosition.value === 3) return "/files/bronze-cup.png";
|
||||
return "/files/dart-board.svg";
|
||||
});
|
||||
|
||||
const statusClass = computed(() => {
|
||||
if (currentUserPosition.value === 1) return "gold";
|
||||
if (currentUserPosition.value === 2) return "silver";
|
||||
if (currentUserPosition.value === 3) return "bronze";
|
||||
return "";
|
||||
});
|
||||
|
||||
// Загружаем
|
||||
onMounted(async () => {
|
||||
await logs.fetch();
|
||||
await buildSorted();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* сюда полностью переносите ваш CSS — он совместим */
|
||||
</style>
|
||||
@@ -35,6 +35,11 @@ const routes = [
|
||||
name: 'MyPoints',
|
||||
component: () => import('@/pages/MyPoints.vue'),
|
||||
},
|
||||
{
|
||||
path: '/leaderboard',
|
||||
name: 'LeaderBoard',
|
||||
component: () => import('@/pages/LeaderBoard.vue'),
|
||||
},
|
||||
// End of test of page
|
||||
{
|
||||
path: '/courses',
|
||||
|
||||
Reference in New Issue
Block a user