135 lines
4.6 KiB
Vue
135 lines
4.6 KiB
Vue
<script setup lang="ts">
|
||
import Card from "primevue/card";
|
||
import Message from "primevue/message";
|
||
import Skeleton from "primevue/skeleton";
|
||
import Tag from "primevue/tag";
|
||
import type { ActiveSignalCountByBot, Bot } from "~/types";
|
||
|
||
definePageMeta({
|
||
middleware: "auth"
|
||
});
|
||
|
||
type BotCard = {
|
||
bot: Bot;
|
||
totalSignals: number;
|
||
};
|
||
|
||
const { user } = useAuth();
|
||
|
||
const loading = ref(true);
|
||
const loadError = ref("");
|
||
const botCards = ref<BotCard[]>([]);
|
||
|
||
const availableBots = computed(() => user.value?.botAccesses?.map((access) => access.bot) ?? []);
|
||
|
||
const formatSignalCount = (count: number) => {
|
||
if (count === 0) return "Нет активных сигналов";
|
||
if (count === 1) return "1 активный сигнал";
|
||
if (count < 5) return `${count} активных сигнала`;
|
||
return `${count} активных сигналов`;
|
||
};
|
||
|
||
const loadBotCards = async () => {
|
||
loading.value = true;
|
||
|
||
try {
|
||
const response = await useApi<{ items: ActiveSignalCountByBot[] }>("/signals/active-counts");
|
||
const countsByBotKey = new Map(response.items.map((item) => [item.botKey, item.activeSignals]));
|
||
const cards = availableBots.value.map((bot) => ({
|
||
bot,
|
||
totalSignals: countsByBotKey.get(bot.key) ?? 0
|
||
}));
|
||
|
||
botCards.value = cards;
|
||
loadError.value = "";
|
||
} catch (error) {
|
||
loadError.value = error instanceof Error ? error.message : "Не удалось загрузить список ботов";
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
onMounted(async () => {
|
||
await loadBotCards();
|
||
});
|
||
|
||
watch(
|
||
() => availableBots.value.map((bot) => bot.key).join("|"),
|
||
(nextKeys, previousKeys) => {
|
||
if (nextKeys === previousKeys) return;
|
||
void loadBotCards();
|
||
}
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<section class="grid gap-4">
|
||
<div
|
||
class="flex flex-col gap-4 rounded-[24px] border p-5 md:flex-row md:items-start md:justify-between"
|
||
:style="{
|
||
borderColor: 'var(--border)',
|
||
background: 'radial-gradient(circle at top right, color-mix(in srgb, var(--accent) 10%, transparent), transparent 28%), var(--surface)',
|
||
boxShadow: '0 10px 30px color-mix(in srgb, var(--text) 4%, transparent)'
|
||
}"
|
||
>
|
||
<div>
|
||
<span class="mb-2 inline-block text-xs font-semibold uppercase tracking-[0.18em] text-(--muted)">Доступные боты</span>
|
||
<h2 class="m-0 text-3xl font-semibold">Выберите бота</h2>
|
||
<p class="m-0 text-sm leading-6 text-(--muted)">
|
||
Каждая карточка открывает отдельную ленту сигналов без общего шума и лишних фильтров на старте.
|
||
</p>
|
||
</div>
|
||
<Tag class="rounded" :value="`${availableBots.length} в доступе`" severity="contrast" />
|
||
</div>
|
||
|
||
<Message v-if="loadError" severity="error" :closable="false">{{ loadError }}</Message>
|
||
<Message v-else-if="!loading && botCards.length === 0" severity="info" :closable="false">
|
||
У пользователя пока нет доступа ни к одному боту.
|
||
</Message>
|
||
|
||
<div v-if="loading" class="bot-grid">
|
||
<Card v-for="index in 6" :key="index" class="overflow-hidden rounded-[24px] border shadow-sm">
|
||
<template #content>
|
||
<div class="grid gap-3 p-4">
|
||
<div class="flex items-center justify-between gap-3">
|
||
<Skeleton width="2.75rem" height="2.75rem" borderRadius="16px" />
|
||
<Skeleton width="2.25rem" height="1.75rem" borderRadius="999px" />
|
||
</div>
|
||
<Skeleton width="3rem" height="0.7rem" />
|
||
<Skeleton width="72%" height="1.45rem" />
|
||
<Skeleton width="58%" height="0.95rem" />
|
||
<Skeleton width="100%" height="2.5rem" borderRadius="16px" />
|
||
</div>
|
||
</template>
|
||
</Card>
|
||
</div>
|
||
|
||
<div v-else class="bot-grid">
|
||
<NuxtLink
|
||
v-for="card in botCards"
|
||
:key="card.bot.id"
|
||
:to="`/bots/${card.bot.key}`"
|
||
class="bot-tile"
|
||
>
|
||
<div class="bot-tile__top">
|
||
<div class="bot-tile__icon">
|
||
<AppLogo size="26px" />
|
||
</div>
|
||
<Tag class="bot-tile__count" :value="String(card.totalSignals)" severity="contrast" />
|
||
</div>
|
||
|
||
<div class="bot-tile__body">
|
||
<p class="bot-tile__eyebrow">Бот</p>
|
||
<h2>{{ card.bot.name }}</h2>
|
||
<p class="bot-tile__meta">{{ formatSignalCount(card.totalSignals) }}</p>
|
||
</div>
|
||
|
||
<div class="bot-tile__footer">
|
||
<span class="bot-tile__action">Открыть ленту</span>
|
||
<i class="pi pi-arrow-right bot-tile__arrow" />
|
||
</div>
|
||
</NuxtLink>
|
||
</div>
|
||
</section>
|
||
</template>
|