This commit is contained in:
talorr
2026-03-27 03:36:08 +03:00
parent 8a97ce6d54
commit cda36918e8
225 changed files with 35641 additions and 0 deletions

View File

@@ -0,0 +1,321 @@
<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>