Files
antigol-service/frontend/pages/bots/[key].vue
talorr cda36918e8 init
2026-03-27 03:36:08 +03:00

322 lines
10 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.
<script setup lang="ts">
import Button from "primevue/button";
import Card from "primevue/card";
import InputText from "primevue/inputtext";
import Message from "primevue/message";
import Paginator from "primevue/paginator";
import Select from "primevue/select";
import SelectButton from "primevue/selectbutton";
import Skeleton from "primevue/skeleton";
import Tag from "primevue/tag";
import type { Bot, PaginatedResponse, Signal } from "~/types";
definePageMeta({
middleware: "auth"
});
const route = useRoute();
const { user } = useAuth();
const botKey = computed(() => String(route.params.key || ""));
const availableBots = computed(() => user.value?.botAccesses?.map((access) => access.bot) ?? []);
const bot = computed<Bot | null>(() => availableBots.value.find((entry) => entry.key === botKey.value) ?? null);
const signals = ref<Signal[]>([]);
const status = ref<string | null>(null);
const query = ref("");
const activeTab = ref<"all" | "1" | "2">("1");
const perPage = ref(20);
const pagination = ref({
page: 1,
perPage: 20,
total: 0,
totalPages: 1
});
const sortBy = ref<"eventStartTime" | "signalTime" | "odds">("eventStartTime");
const sortDirection = ref<"desc" | "asc">("desc");
const loading = ref(true);
const loadError = ref("");
const tabCounts = ref<{ all: number; "1": number; "2": number }>({ all: 0, "1": 0, "2": 0 });
const tabOptions = [
{ label: "Все", value: "all" },
{ label: "Активные", value: "1" },
{ label: "Неактивные", value: "2" }
] as const;
const statusOptions = [
{ label: "Все статусы", value: null },
{ label: "В ожидании", value: "pending" },
{ label: "Выигрыш", value: "win" },
{ label: "Проигрыш", value: "lose" },
{ label: "Возврат", value: "void" },
{ label: "Ручная проверка", value: "manual_review" }
];
const sortOptions = [
{ label: "По дате матча", value: "eventStartTime" },
{ label: "По времени сигнала", value: "signalTime" },
{ label: "По коэффициенту", value: "odds" }
] as const;
const perPageOptions = [
{ label: "20 на странице", value: 20 },
{ label: "40 на странице", value: 40 },
{ label: "100 на странице", value: 100 }
];
const getSortValue = (signal: Signal) => {
if (sortBy.value === "odds") {
return signal.odds;
}
const rawValue = sortBy.value === "eventStartTime" ? signal.eventStartTime || signal.signalTime : signal.signalTime;
const parsedValue = Date.parse(rawValue);
return Number.isNaN(parsedValue) ? 0 : parsedValue;
};
const sortedSignals = computed(() =>
[...signals.value].sort((left, right) => {
const leftValue = getSortValue(left);
const rightValue = getSortValue(right);
if (leftValue !== rightValue) {
return sortDirection.value === "desc" ? rightValue - leftValue : leftValue - rightValue;
}
const leftSignalTime = Date.parse(left.signalTime);
const rightSignalTime = Date.parse(right.signalTime);
return sortDirection.value === "desc" ? rightSignalTime - leftSignalTime : leftSignalTime - rightSignalTime;
})
);
const currentTabLabel = computed(() => tabOptions.find((option) => option.value === activeTab.value)?.label ?? "Все");
const buildParams = (tab: "all" | "1" | "2", targetPage: number, targetPerPage: number) => {
const params = new URLSearchParams();
params.set("published", "true");
params.set("botKey", botKey.value);
if (tab !== "all") params.set("activeTab", tab);
params.set("page", String(targetPage));
params.set("perPage", String(targetPerPage));
if (status.value) params.set("status", status.value);
if (query.value) params.set("q", query.value);
return params;
};
const loadSignals = async () => {
if (!bot.value) {
loadError.value = "Нет доступа к этому боту";
loading.value = false;
return;
}
loading.value = true;
try {
const response = await useApi<PaginatedResponse<Signal>>(
`/signals?${buildParams(activeTab.value, pagination.value.page, perPage.value).toString()}`
);
signals.value = response.items;
pagination.value = response.pagination;
perPage.value = response.pagination.perPage;
if (response.tabCounts) {
tabCounts.value = response.tabCounts;
}
loadError.value = "";
} catch (error) {
loadError.value = error instanceof Error ? error.message : "Не удалось загрузить сигналы";
} finally {
loading.value = false;
}
};
const reloadFromFirstPage = async () => {
pagination.value.page = 1;
await loadSignals();
};
const handlePageChange = async (event: { page: number; rows: number }) => {
pagination.value.page = event.page + 1;
perPage.value = event.rows;
await loadSignals();
};
onMounted(async () => {
await loadSignals();
});
watch([status, query, sortBy], () => {
void reloadFromFirstPage();
});
watch(activeTab, () => {
void reloadFromFirstPage();
});
watch(perPage, () => {
void reloadFromFirstPage();
});
watch(botKey, () => {
pagination.value.page = 1;
void loadSignals();
});
</script>
<template>
<section class="sakai-page">
<div class="sakai-hero-card">
<div>
<div class="sakai-page-back">
<NuxtLink to="/">
<Button text severity="secondary" icon="pi pi-arrow-left" label="Все боты" />
</NuxtLink>
</div>
<span class="sakai-section-label">Лента бота</span>
<h2>{{ bot?.name ?? "Бот" }}</h2>
<p>Сигналы по выбранному боту с отдельными фильтрами, статусами и навигацией по страницам.</p>
</div>
<div class="sakai-hero-card__tags">
<Tag class="rounded" :value="currentTabLabel" severity="contrast" />
<Tag class="rounded" :value="`${pagination.total} сигналов`" severity="info" />
</div>
</div>
<Card class="sakai-filter-card">
<template #content>
<div class="sakai-filter-grid">
<div class="sakai-field">
<label for="signal-search">Поиск</label>
<InputText id="signal-search" v-model="query" placeholder="Лига, команда, рынок" />
</div>
<div class="sakai-field">
<label>Вкладка</label>
<div class="sakai-tab-scroll">
<SelectButton v-model="activeTab" :options="tabOptions" option-label="label" option-value="value" allow-empty="false" />
</div>
</div>
<div class="sakai-field">
<label for="signal-status">Статус</label>
<Select
id="signal-status"
v-model="status"
:options="statusOptions"
option-label="label"
option-value="value"
placeholder="Все статусы"
/>
</div>
<div class="sakai-field">
<label for="signal-sort">Сортировка</label>
<Select
id="signal-sort"
v-model="sortBy"
:options="sortOptions"
option-label="label"
option-value="value"
/>
</div>
<div class="sakai-field">
<label for="signal-limit">Лимит</label>
<Select
id="signal-limit"
v-model="perPage"
:options="perPageOptions"
option-label="label"
option-value="value"
/>
</div>
<div class="sakai-field">
<label>Порядок</label>
<Button
fluid
severity="secondary"
outlined
:label="sortDirection === 'desc' ? 'Сначала новые' : 'Сначала старые'"
:icon="sortDirection === 'desc' ? 'pi pi-sort-amount-down' : 'pi pi-sort-amount-up'"
@click="sortDirection = sortDirection === 'desc' ? 'asc' : 'desc'"
/>
</div>
</div>
</template>
</Card>
<Message v-if="loadError" severity="error" :closable="false">{{ loadError }}</Message>
<div class="sakai-signal-layout">
<Card class="sakai-signal-panel">
<template #title>Сигналы</template>
<template #subtitle>
Всего: {{ tabCounts.all }} · Активные: {{ tabCounts["1"] }} · Неактивные: {{ tabCounts["2"] }}
</template>
<template #content>
<div v-if="loading" class="sakai-signal-list">
<div v-for="index in 5" :key="index" class="sakai-signal-skeleton">
<Skeleton width="35%" height="1rem" />
<Skeleton width="70%" height="1.25rem" />
<Skeleton width="55%" height="1rem" />
</div>
</div>
<Message v-else-if="sortedSignals.length === 0" severity="secondary" :closable="false">
По текущему фильтру сигналов нет.
</Message>
<div v-else class="sakai-signal-list">
<NuxtLink
v-for="signal in sortedSignals"
:key="signal.id"
:to="`/signals/${signal.id}`"
class="sakai-signal-link"
>
<SignalCard :signal="signal" />
</NuxtLink>
</div>
</template>
</Card>
<Card class="sakai-summary-panel">
<template #title>Сводка</template>
<template #content>
<div class="sakai-summary-list">
<div class="sakai-summary-item">
<span>Все сигналы</span>
<strong>{{ tabCounts.all }}</strong>
</div>
<div class="sakai-summary-item">
<span>Активные</span>
<strong>{{ tabCounts["1"] }}</strong>
</div>
<div class="sakai-summary-item">
<span>Неактивные</span>
<strong>{{ tabCounts["2"] }}</strong>
</div>
</div>
</template>
</Card>
</div>
<Card>
<template #content>
<Paginator
:first="(pagination.page - 1) * perPage"
:rows="perPage"
:total-records="pagination.total"
:rows-per-page-options="[20, 40, 100]"
template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
@page="handlePageChange"
/>
</template>
</Card>
</section>
</template>