TEST UPD
- add front
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user