feat(admin,api,player): settlement stats, team crests, MS fields and list bet summary

This commit is contained in:
2026-06-04 17:30:48 +08:00
parent cc737e2924
commit 9fcee31a9a
27 changed files with 2296 additions and 427 deletions

View File

@@ -15,6 +15,10 @@ import CorrectScoreConfirmModal, {
} from '../components/match-detail/CorrectScoreConfirmModal.vue';
import { isCorrectScoreMarket, parseScoreCode } from '../utils/correctScoreLayout';
import { useOnLocaleChange } from '../composables/useOnLocaleChange';
import vsImg from '../assets/images/vs.png';
import cardBg from '../assets/images/卡片.png';
const heroCardBg = `url(${cardBg})`;
const route = useRoute();
const router = useRouter();
@@ -257,6 +261,7 @@ function hasSlipPickForMarket(marketType: string) {
<div class="detail-page">
<header class="toolbar">
<button type="button" class="icon-btn" :aria-label="t('bet.back')" @click="router.back()"></button>
<span class="toolbar-title">{{ match?.leagueName ?? '' }}</span>
<div class="toolbar-actions">
<MatchBetGuide />
<button
@@ -274,26 +279,49 @@ function hasSlipPickForMarket(marketType: string) {
<div v-if="loading" class="state">{{ t('bet.loading') }}</div>
<template v-else-if="match">
<section v-if="match.leagueName" class="league-banner">
<span class="league-title">*{{ match.leagueName }}</span>
<img
v-if="match.leagueLogoUrl"
:src="match.leagueLogoUrl"
alt=""
class="league-logo"
/>
</section>
<section class="match-hero">
<p class="kickoff">{{ kickoff }}</p>
<div class="match-line">
<img v-if="homeFlag" :src="homeFlag" alt="" class="flag" />
<div class="names">
<span class="team">{{ match.homeTeamName }}</span>
<span class="vs">vs</span>
<span class="team">{{ match.awayTeamName }}</span>
<div class="hero-teams">
<!-- home -->
<div class="hero-team">
<img v-if="homeFlag" :src="homeFlag" alt="" class="hero-flag" />
<span v-else class="hero-flag-ph"></span>
<span class="hero-name">{{ match.homeTeamName }}</span>
</div>
<!-- VS arena with lightning -->
<div class="vs-arena">
<svg class="hz-lightning" viewBox="0 0 72 28" aria-hidden="true">
<defs>
<linearGradient id="hzDetailGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#5eb8ff" stop-opacity="0.2" />
<stop offset="35%" stop-color="#b8ecff" stop-opacity="1" />
<stop offset="50%" stop-color="#ffffff" stop-opacity="1" />
<stop offset="65%" stop-color="#ffd080" stop-opacity="1" />
<stop offset="100%" stop-color="#ff9040" stop-opacity="0.2" />
</linearGradient>
</defs>
<path
class="hz-path hz-path-main"
stroke="url(#hzDetailGrad)"
d="M1 14 H16 L20 5 L24 23 L28 9 L32 14 H40 L44 6 L48 22 L52 12 L56 14 H71"
/>
<path
class="hz-path hz-path-sub"
stroke="url(#hzDetailGrad)"
d="M3 19 H14 L18 15 L22 19 H50 L54 16 L58 19 H69"
/>
</svg>
<span class="hz-beam" aria-hidden="true" />
<img :src="vsImg" alt="" class="vs-img" />
</div>
<!-- away -->
<div class="hero-team">
<img v-if="awayFlag" :src="awayFlag" alt="" class="hero-flag" />
<span v-else class="hero-flag-ph"></span>
<span class="hero-name">{{ match.awayTeamName }}</span>
</div>
<img v-if="awayFlag" :src="awayFlag" alt="" class="flag" />
</div>
</section>
@@ -377,6 +405,18 @@ function hasSlipPickForMarket(marketType: string) {
gap: 8px;
}
.toolbar-title {
flex: 1;
text-align: center;
font-size: 13px;
font-weight: 800;
color: var(--primary-light);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 4px;
}
.toolbar-actions {
display: flex;
align-items: center;
@@ -401,72 +441,190 @@ function hasSlipPickForMarket(marketType: string) {
opacity: 0.45;
}
/* ── match hero ── */
.match-hero {
padding: 2px 12px 10px;
position: relative;
isolation: isolate;
margin: 0 0 10px;
padding: 14px 12px 16px;
overflow: hidden;
}
.league-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 6px 12px 2px;
}
.league-banner .league-title {
flex: 1;
font-size: 12px;
font-weight: 800;
color: var(--primary-light);
line-height: 1.35;
}
.league-logo {
flex-shrink: 0;
height: 40px;
width: auto;
max-width: 44px;
object-fit: contain;
.match-hero::before {
content: '';
position: absolute;
inset: 0;
background: v-bind(heroCardBg) center / 100% 100% no-repeat;
opacity: 0.22;
z-index: 0;
pointer-events: none;
}
.kickoff {
position: relative;
z-index: 1;
font-size: 11px;
color: var(--text-muted);
text-align: center;
margin-bottom: 6px;
margin-bottom: 14px;
}
.match-line {
.hero-teams {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.flag {
width: 36px;
height: 24px;
object-fit: cover;
border-radius: 2px;
flex-shrink: 0;
}
.names {
.hero-team {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
text-align: center;
line-height: 1.35;
}
.team {
display: block;
font-size: 14px;
.hero-flag {
width: 54px;
height: 36px;
object-fit: cover;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.hero-flag-ph {
width: 54px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
opacity: 0.45;
}
.hero-name {
font-size: 13px;
font-weight: 800;
color: var(--primary-light);
text-align: center;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.vs {
font-size: 10px;
color: var(--text-muted);
/* VS arena — same as HomeView */
.vs-arena {
position: relative;
flex-shrink: 0;
width: 140px;
height: 126px;
display: flex;
align-items: center;
justify-content: center;
}
.hz-lightning {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
overflow: visible;
}
.hz-path {
fill: none;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 0 4px rgba(120, 210, 255, 0.95)) drop-shadow(0 0 8px rgba(255, 180, 80, 0.55));
opacity: 0;
}
.hz-path-main {
animation: hz-strike-main 2.6s ease-in-out infinite;
}
.hz-path-sub {
stroke-width: 1.6;
animation: hz-strike-sub 2.6s ease-in-out infinite;
animation-delay: 0.12s;
}
.hz-beam {
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 2px;
transform: translateY(-50%);
pointer-events: none;
z-index: 1;
background: linear-gradient(
90deg,
rgba(94, 184, 255, 0) 0%,
rgba(184, 236, 255, 0.95) 28%,
#fff 50%,
rgba(255, 208, 128, 0.95) 72%,
rgba(255, 144, 64, 0) 100%
);
opacity: 0;
filter: blur(0.4px);
animation: hz-beam-flash 2.6s ease-in-out infinite;
}
.vs-img {
position: relative;
z-index: 0;
width: 126px;
height: auto;
object-fit: contain;
}
@keyframes hz-strike-main {
0%, 72%, 100% { opacity: 0; }
74% { opacity: 1; }
75% { opacity: 0.25; }
76% { opacity: 0.95; }
78% { opacity: 0; }
}
@keyframes hz-strike-sub {
0%, 74%, 100% { opacity: 0; }
76% { opacity: 0.85; }
77% { opacity: 0.2; }
78% { opacity: 0.7; }
80% { opacity: 0; }
}
@keyframes hz-beam-flash {
0%, 71%, 100% { opacity: 0; transform: translateY(-50%) scaleX(0.6); }
73% { opacity: 0.85; transform: translateY(-50%) scaleX(1); }
75% { opacity: 0.15; transform: translateY(-50%) scaleX(0.95); }
76% { opacity: 0.75; transform: translateY(-50%) scaleX(1); }
78% { opacity: 0; transform: translateY(-50%) scaleX(1.05); }
}
@keyframes vs-glow {
0%, 100% {
opacity: 0.82;
filter: drop-shadow(0 0 2px rgba(212, 175, 55, 0.3));
}
50% {
opacity: 1;
filter: drop-shadow(0 0 3px rgba(255, 230, 140, 0.7)) drop-shadow(0 0 6px rgba(212, 175, 55, 0.35));
}
}
@media (prefers-reduced-motion: reduce) {
.vs-img { animation: none; filter: drop-shadow(0 0 3px rgba(212, 175, 55, 0.35)); }
.hz-path, .hz-beam { animation: none; opacity: 0; }
}
.markets-section {