init
This commit is contained in:
321
frontend/pages/bots/[key].vue
Normal file
321
frontend/pages/bots/[key].vue
Normal 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>
|
||||
Reference in New Issue
Block a user