- add front
This commit is contained in:
Alexandrina-Kuzeleva
2025-11-25 10:51:18 +03:00
parent ebde8a0171
commit a3b9e4f7b2

View File

@@ -1,205 +1,218 @@
<template>
<div style="border: 2px solid red; padding: 10px">
DEBUG: Template rendered
</div>
<div v-if="loading" class="text-center p-10 text-gray-600">
Загружаем таблицу лидеров...
</div>
<div class="border-2 border-red-600 p-4 rounded-lg mb-8">
DEBUG: Template rendered
</div>
<div v-else class="page-container">
<div v-if="loading" class="flex justify-center items-center py-20">
<p class="text-lg text-gray-600">Загружаем таблицу лидеров...</p>
</div>
<h1 class="page-title">Таблица лидеров по очкам</h1>
<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-10 text-gray-900">
Таблица лидеров по очкам
</h1>
<!-- Блок с текущим пользователем и лучшим -->
<section class="user-results">
<section class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
<!-- Ваш результат -->
<div class="box results-box">
<h2 class="box-heading">Ваш результат</h2>
<div class="bg-white rounded-xl shadow-xl p-8 border border-gray-200 relative overflow-hidden">
<!-- Фоновая иконка дартс для тех, кто не в топе -->
<img v-if="!currentUserPosition"
src="/files/dart-board.svg"
class="absolute -top-8 -right-8 w-48 h-48 opacity-10 pointer-events-none">
<!-- Статусы -->
<template v-if="currentUserPosition">
<!--<img
class="cup-img"
:src="statusIcon"
/>-->
<p class="status" :class="statusClass">{{ statusLabel }}</p>
<p class="place" v-if="currentUserPosition <= 3">{{ currentUserPosition }}-е место</p>
</template>
<h2 class="text-2xl font-semibold text-gray-800 mb-6">Ваш результат</h2>
<template v-else>
<!--<img class="cup-img" src="/files/dart-board.svg">-->
<p class="status">Вы пока не в рейтинге</p>
</template>
<div v-if="currentUserPosition" class="text-center">
<!-- Кубок/иконка статуса -->
<img :src="statusIcon" class="w-24 h-24 mx-auto mb-4">
<p class="points">{{ currentUserPoints }} баллов</p>
<p :class="[
'text-3xl font-bold mb-3',
statusClass === 'gold' ? 'text-yellow-500' :
statusClass === 'silver' ? 'text-gray-500' :
statusClass === 'bronze' ? 'text-orange-600' : 'text-indigo-600'
]">
{{ statusLabel }}
</p>
<!-- Мотивационный текст -->
<template v-if="currentUserPosition && currentUserPosition > 1">
<p class="motivation-text">
<p v-if="currentUserPosition <= 3" class="text-2xl text-gray-700 font-medium">
{{ currentUserPosition }}-е место
</p>
</div>
<div v-else class="text-center">
<p class="text-xl text-gray-500 font-medium">Вы пока не в рейтинге</p>
</div>
<p class="text-4xl font-bold text-center mt-8 text-indigo-600">
{{ currentUserPoints }} баллов
</p>
<!-- Мотивация -->
<div class="mt-6 text-center">
<p v-if="currentUserPosition && currentUserPosition > 1"
class="text-gray-700 text-lg">
До {{ currentUserPosition - 1 }}-го места нужно:
<strong>{{ pointsToNext }} баллов</strong>
<strong class="text-green-600 text-2xl font-bold"> {{ pointsToNext }} баллов</strong>
</p>
</template>
<template v-else-if="!currentUserPosition && sorted.length">
<p class="motivation-text">
<p v-else-if="!currentUserPosition && sorted.length"
class="text-gray-700 text-lg">
До лидера нужно:
<strong>{{ sorted[0].points - currentUserPoints }} баллов</strong>
<strong class="text-green-600 text-2xl font-bold">
{{ sorted[0].points - currentUserPoints }} баллов
</strong>
</p>
</template>
</div>
</div>
<!-- Лучший участник -->
<div class="box results-box">
<h2 class="box-heading">Лучший участник</h2>
<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>
<template v-if="topUser">
<div class="initial-circle">
{{ topUserInitial }}
<div v-if="topUser" class="space-y-4">
<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">
{{ topUserInitial }}
</div>
<!-- Золотой кубок рядом с аватаром -->
<img src="/files/gold-cup.png" class="absolute -top-2 -right-4 w-16 h-16">
</div>
<p class="username">
<a
class="user-link"
:href="userProfileLink(topUser.user)"
>
<div>
<a :href="userProfileLink(topUser.user)"
class="text-2xl font-bold text-indigo-600 hover:text-indigo-800 hover:underline block">
{{ topUser.full_name }}
</a>
<!--<img class="cup-badge" src="/files/gold-cup.png">-->
</p>
<p class="text-4xl font-bold text-yellow-500 mt-3">
{{ topUser.points }} баллов
</p>
</div>
</div>
<p class="points">{{ topUser.points }} баллов</p>
</template>
<template v-else>
<p class="no-leader">Нет лучшего участника</p>
</template>
<p v-else class="text-center text-gray-500">Нет лучшего участника</p>
</div>
</section>
<!-- Таблица лидеров -->
<section class="box">
<h2 class="box-heading">Топ лидеров</h2>
<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">
<h2 class="text-2xl font-semibold text-gray-800">Топ лидеров</h2>
</div>
<div class="leaders-table-wrapper">
<table class="leaders-table">
<thead>
<tr>
<th>#</th>
<th style="min-width: 150px;">Пользователь</th>
<th>Баллы активности</th>
<th>Общая сумма баллов</th>
<th>Бонусные баллы (МПГУ)</th>
</tr>
<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" style="min-width: 180px;">Пользователь</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 sorted"
:key="item.user"
:class="[
'hover:bg-gray-50 transition',
item.user === currentUserEmail ? 'bg-indigo-50 font-semibold' : 'bg-white'
]">
<td class="px-6 py-5 text-lg font-bold text-gray-800">
{{ index + 1 }}
</td>
<tbody>
<tr
v-for="(item, index) in sorted"
:key="item.user"
:class="{'current-user': item.user === currentUserEmail}"
>
<td>{{ 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>
<!-- Кубки в таблице для топ-3 -->
<img v-if="index === 0" src="/files/gold-cup.png" class="absolute -top-3 -right-3 w-10 h-10">
<img v-else-if="index === 1" src="/files/silver-cup.png" class="absolute -top-3 -right-3 w-10 h-10">
<img v-else-if="index === 2" src="/files/bronze-cup.png" class="absolute -top-3 -right-3 w-10 h-10">
</div>
<td>
<div class="flex items-center">
<div class="initial-circle-small">
{{ getUserInitial(item.user) }}
</div>
<div>
<a
:href="userProfileLink(item.user)"
class="user-link"
>
<a :href="userProfileLink(item.user)"
class="text-indigo-600 hover:text-indigo-800 hover:underline font-medium text-base">
{{ 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>
<td>{{ item.points }}</td>
<td>{{ item.points }}</td>
<td>{{ item.bonus }}</td>
</tr>
<td class="px-6 py-5 text-gray-900 font-medium">{{ item.points }}</td>
<td class="px-6 py-5 text-gray-900 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>
<p class="stats-note">
* Бонусные баллы рассчитываются как 1 балл за каждые 100 очков активности (максимум 10)
</p>
<div class="px-8 py-4 bg-gray-50 text-sm text-gray-600 italic">
* Бонусные баллы рассчитываются как 1 балл за каждые 100 очков активности (максимум 10)
</div>
</section>
</div>
</template>
<script setup>
import { inject, ref, computed, onMounted } from "vue";
import { createResource } from "frappe-ui";
import { inject, ref, computed, onMounted } from "vue"
import { createResource } from "frappe-ui"
const $user = inject("$user");
const currentUserEmail = $user.data.email;
const $user = inject("$user")
const currentUserEmail = $user.data.email
const loading = ref(true)
const sorted = ref([])
const loading = ref(true);
const sorted = ref([]);
// Загружаем все Energy Point Log
// Ресурсы (без изменений)
const logsResource = createResource({
url: "frappe.client.get_list",
params: {
doctype: "Energy Point Log",
fields: ["user", "points"],
limit_page_length: 10000 // увеличим лимит для больших данных
limit_page_length: 10000
},
auto: false,
});
})
// Загружаем данные пользователей
const usersResource = createResource({
url: "frappe.client.get_list",
params: {
doctype: "User",
fields: ["name", "full_name", "username"],
filters: [["enabled", "=", 1]], // только активные пользователи
filters: [["enabled", "=", 1]],
limit_page_length: 1000
},
auto: false,
});
})
// Основная функция загрузки данных
async function loadData() {
try {
loading.value = true;
// Параллельно загружаем логи и пользователей
loading.value = true
const [logsData, usersData] = await Promise.all([
logsResource.fetch(),
usersResource.fetch()
]);
])
// Создаем карту пользователей для быстрого доступа
const usersMap = {};
const usersMap = {}
usersData.forEach(user => {
usersMap[user.name] = {
full_name: user.full_name || user.name,
username: user.username
};
});
}
})
// Суммируем баллы по пользователям
const userPoints = {};
const userPoints = {}
logsData.forEach(log => {
if (!userPoints[log.user]) userPoints[log.user] = 0;
userPoints[log.user] += log.points;
});
userPoints[log.user] = (userPoints[log.user] || 0) + log.points
})
// Создаем сортированный массив
const leaderboard = Object.entries(userPoints)
.map(([user, points]) => ({
user,
@@ -208,87 +221,71 @@ async function loadData() {
username: usersMap[user]?.username || user,
bonus: Math.min(Math.floor(points / 100), 10)
}))
.sort((a, b) => b.points - a.points);
sorted.value = leaderboard;
.sort((a, b) => b.points - a.points)
sorted.value = leaderboard
} catch (error) {
console.error("Ошибка загрузки данных:", error);
console.error("Ошибка загрузки данных:", error)
} finally {
loading.value = false;
loading.value = false
}
}
// Computed свойства
const currentUserPoints = computed(() => {
const user = sorted.value.find(u => u.user === currentUserEmail);
return user ? user.points : 0;
});
// Computed (без изменений)
const currentUserPoints = computed(() => sorted.value.find(u => u.user === currentUserEmail)?.points || 0)
const currentUserPosition = computed(() => {
const index = sorted.value.findIndex(u => u.user === currentUserEmail);
return index >= 0 ? index + 1 : null;
});
const topUser = computed(() => sorted.value[0]);
const index = sorted.value.findIndex(u => u.user === currentUserEmail)
return index >= 0 ? index + 1 : null
})
const topUser = computed(() => sorted.value[0])
const pointsToNext = computed(() => {
if (!currentUserPosition.value || currentUserPosition.value <= 1) return 0;
const nextUser = sorted.value[currentUserPosition.value - 2];
return nextUser.points - currentUserPoints.value;
});
if (!currentUserPosition.value || currentUserPosition.value <= 1) return 0
const nextUser = sorted.value[currentUserPosition.value - 2]
return nextUser.points - currentUserPoints.value
})
// UI статусы
const statusLabel = computed(() => {
if (!currentUserPosition.value) return "Стремитесь выше";
if (!currentUserPosition.value) return "Стремитесь выше"
switch (currentUserPosition.value) {
case 1: return "Чемпион";
case 2: return "Второе место";
case 3: return "Третье место";
case 1: return "Чемпион"
case 2: return "Второе место"
case 3: return "Третье место"
default:
if (currentUserPosition.value <= 5) return "В топ-5";
if (currentUserPosition.value <= 10) return "В топ-10";
return "Стремитесь выше";
return currentUserPosition.value <= 5 ? "В топ-5" :
currentUserPosition.value <= 10 ? "В топ-10" : "Стремитесь выше"
}
});
})
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";
if (currentUserPosition.value && currentUserPosition.value <= 10) return "/files/star.svg";
return "/files/dart-board.svg";
});
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"
if (currentUserPosition.value && currentUserPosition.value <= 10) return "/files/star.svg"
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 "";
});
if (currentUserPosition.value === 1) return "gold"
if (currentUserPosition.value === 2) return "silver"
if (currentUserPosition.value === 3) return "bronze"
return ""
})
const topUserInitial = computed(() => {
return topUser.value ? topUser.value.user[0].toUpperCase() : 'N/A';
});
const topUserInitial = computed(() => topUser.value ? topUser.value.user[0].toUpperCase() : "N/A")
// Вспомогательные функции
function getUserInitial(email) {
return email ? email[0].toUpperCase() : '?';
return email ? email[0].toUpperCase() : "?"
}
function userProfileLink(userEmail) {
const user = sorted.value.find(u => u.user === userEmail);
return user ? `/lms/user/${user.username}` : '#';
const user = sorted.value.find(u => u.user === userEmail)
return user ? `/lms/user/${user.username}` : "#"
}
// Загружаем данные при монтировании
onMounted(() => {
loadData();
});
onMounted(() => loadData())
</script>
<style>
/* Ваш полный CSS стиль здесь */
/* Общие стили */
<style scoped>
/* При необходимости можно добавить кастомные стили, но Tailwind всё покрывает */
</style>