Files
talorr cda36918e8 init
2026-03-27 03:36:08 +03:00

272 lines
9.1 KiB
Vue
Raw Permalink 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 Message from "primevue/message";
import Skeleton from "primevue/skeleton";
import Tag from "primevue/tag";
import type { Signal } from "~/types";
definePageMeta({
middleware: "auth"
});
type SignalDetails = Signal & {
settlement?: {
explanation: string;
} | null;
};
const route = useRoute();
const { formatDateTime, browserTimeZone } = useBrowserDateTime();
const { copyText } = useClipboard();
const signal = ref<SignalDetails | null>(null);
const loadError = ref("");
const botName = computed(() => signal.value?.rawPayload?.botName || null);
const botKey = computed(() => signal.value?.rawPayload?.botKey || null);
const forecastImageUrl = computed(() => signal.value?.rawPayload?.forecastImageUrl || null);
const displayForecast = computed(() => signal.value?.forecast || signal.value?.rawPayload?.forecast || "");
const forecastImageFailed = ref(false);
const isInactiveForecast = computed(() => signal.value?.rawPayload?.forecastInactive === true);
const copiedMatch = ref(false);
let copiedMatchResetTimeout: ReturnType<typeof setTimeout> | null = null;
const statusLabels: Record<Signal["status"], string> = {
pending: "LIVE",
win: "WIN",
lose: "LOSE",
void: "VOID",
manual_review: "CHECK",
unpublished: "OFF"
};
const displayStatusLabel = computed(() => {
if (!signal.value) {
return "";
}
if (signal.value.status === "pending" && isInactiveForecast.value) {
return "OFF";
}
return statusLabels[signal.value.status];
});
const statusSeverity = computed(() => {
if (!signal.value) {
return "secondary";
}
if (signal.value.status === "pending" && isInactiveForecast.value) {
return "secondary";
}
switch (signal.value.status) {
case "win":
return "success";
case "lose":
return "danger";
case "manual_review":
return "warn";
case "pending":
return "info";
default:
return "secondary";
}
});
const formattedSignalTime = computed(() => {
return signal.value ? formatDateTime(signal.value.signalTime) : ""
});
const formattedOdds = computed(() => (signal.value ? signal.value.odds.toFixed(2) : ""));
const formattedLineValue = computed(() => {
if (!signal.value || signal.value.lineValue === null) {
return null;
}
return String(signal.value.lineValue);
});
const shouldShowForecastImage = computed(() => Boolean(forecastImageUrl.value) && !forecastImageFailed.value);
const detailItems = computed(() => {
if (!signal.value) {
return [];
}
return [
{ label: "Статус", value: displayStatusLabel.value },
// { label: "Источник", value: signal.value.sourceType },
{ label: "Время сигнала", value: formattedSignalTime.value },
{ label: "Часовой пояс", value: browserTimeZone.value ?? "UTC" },
{ label: "Коэффициент", value: formattedOdds.value },
{ label: "Линия", value: formattedLineValue.value ?? "Не указана" }
];
});
onMounted(async () => {
try {
signal.value = await useApi<SignalDetails>(`/signals/${route.params.id}`);
loadError.value = "";
} catch (error) {
loadError.value = error instanceof Error ? error.message : "Не удалось загрузить сигнал";
}
});
watch(
() => signal.value?.rawPayload?.forecastImageUrl,
() => {
forecastImageFailed.value = false;
}
);
const copyMatchName = async () => {
const copied = await copyText(`${signal.value?.homeTeam ?? ""} - ${signal.value?.awayTeam ?? ""}`);
if (!copied) {
return;
}
copiedMatch.value = true;
if (copiedMatchResetTimeout) {
clearTimeout(copiedMatchResetTimeout);
}
copiedMatchResetTimeout = setTimeout(() => {
copiedMatch.value = false;
}, 1600);
};
</script>
<template>
<section class="sakai-page">
<div class="sakai-hero-card">
<div>
<div class="sakai-page-back">
<NuxtLink :to="botKey ? `/bots/${botKey}` : '/'">
<Button
text
severity="secondary"
icon="pi pi-arrow-left"
:label="botKey ? 'Назад к сигналам' : 'На главную'"
/>
</NuxtLink>
</div>
<span class="sakai-section-label">Детали сигнала</span>
<h2>{{ signal ? `${signal.homeTeam} - ${signal.awayTeam}` : "Загрузка сигнала" }}</h2>
</div>
<div v-if="signal" class="sakai-hero-card__tags">
<Tag class="rounded" :value="signal.sportType" severity="contrast" />
<Tag class="rounded" :value="signal.leagueName" severity="info" />
</div>
</div>
<Message v-if="loadError" severity="error" :closable="false">{{ loadError }}</Message>
<div v-else-if="!signal" class="sakai-signal-detail-loading">
<Card class="sakai-signal-panel">
<template #content>
<div class="sakai-signal-detail-skeleton">
<Skeleton width="30%" height="1rem" />
<Skeleton width="65%" height="2rem" />
<Skeleton width="45%" height="1.2rem" />
<Skeleton width="100%" height="16rem" />
</div>
</template>
</Card>
</div>
<div v-else class="sakai-signal-layout sakai-signal-layout--detail">
<div class="sakai-signal-detail-main">
<Card class="sakai-signal-panel">
<template #content>
<article class="sakai-signal-card sakai-signal-card--detail">
<div class="sakai-signal-card__main">
<div class="sakai-signal-card__meta">
<span>
<i class="pi pi-calendar" />
Сигнал: {{ formattedSignalTime }}
</span>
<span>
<i class="pi pi-flag" />
{{ signal.leagueName }}
</span>
<span v-if="botName">
<AppLogo size="14px" />
{{ botName }}
</span>
</div>
<div class="sakai-signal-card__teams">
<strong>{{ signal.homeTeam }}</strong>
<strong>{{ signal.awayTeam }}</strong>
</div>
<div class="sakai-signal-card__actions">
<button
type="button"
class="sakai-copy-button sakai-copy-button--wide"
:aria-label="`Скопировать матч ${signal.homeTeam} - ${signal.awayTeam}`"
@click="copyMatchName"
>
<i class="pi" :class="copiedMatch ? 'pi-check' : 'pi-copy'" aria-hidden="true" />
<span>{{ copiedMatch ? "Скопировано" : "Скопировать матч" }}</span>
</button>
</div>
<div class="sakai-signal-card__market">
<span>{{ signal.marketType }}</span>
<span>{{ signal.selection }}</span>
<span v-if="formattedLineValue">{{ formattedLineValue }}</span>
<span>{{ signal.sourceType }}</span>
</div>
<p class="sakai-signal-detail__timezone">
Время показано в часовом поясе: {{ browserTimeZone ?? "UTC" }}
</p>
</div>
<div class="sakai-signal-card__side sakai-signal-card__side--detail">
<div v-if="forecastImageUrl || displayForecast" class="sakai-signal-card__forecast">
<img
v-if="shouldShowForecastImage"
:src="forecastImageUrl"
:alt="displayForecast || `${signal.homeTeam} - ${signal.awayTeam}`"
class="sakai-signal-card__forecast-image sakai-signal-card__forecast-image--detail"
loading="lazy"
@error="forecastImageFailed = true"
>
<p v-else-if="displayForecast" class="sakai-signal-card__forecast-text">
{{ displayForecast }}
</p>
<p v-else class="sakai-signal-card__forecast-text sakai-signal-card__forecast-text--empty">
Прогноз недоступен
</p>
</div>
<div class="sakai-signal-detail__status-row">
<Tag class="rounded" :value="displayStatusLabel" :severity="statusSeverity" />
<div class="sakai-signal-card__odds">{{ formattedOdds }}</div>
</div>
</div>
</article>
</template>
</Card>
</div>
<div class="sakai-signal-detail-aside">
<Card class="sakai-summary-panel sakai-summary-panel--sticky">
<template #title>Сводка</template>
<template #content>
<div class="sakai-signal-detail-grid">
<div v-for="item in detailItems" :key="item.label" class="sakai-signal-detail-item">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
</template>
</Card>
</div>
</div>
</section>
</template>