feat: split admin dashboard, improve match ops, and player closed-match UX

Admin: add match/player overview sub-nav; refine settlement flow and league
match management UI; improve action button enabled/disabled styles; enhance
logo upload and outright odds sync.

API: expose matchPhase/bettingOpen for closed matches; league publish guards;
settlement preview with auto score save; outright team auto-sync.

Player: watermark for closed/settled states; keep match and bet details visible;
remove default login credentials.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-10 13:00:14 +08:00
parent 6124313369
commit 03f54ca689
43 changed files with 2787 additions and 519 deletions

View File

@@ -8,6 +8,8 @@ import { PARLAY_MARKET_TYPES, PARLAY_SELECTION_KEYS, PARLAY_MARKET_GROUPS } from
import BetGuideHelp from '../BetGuideHelp.vue';
import GoldSpinner from '../GoldSpinner.vue';
import TeamEmblem from '../TeamEmblem.vue';
import StatusWatermark from '../StatusWatermark.vue';
import { matchPhaseLabel, matchPhaseVariant, type MatchPhase } from '../../utils/matchPhase';
import { useOnLocaleChange } from '../../composables/useOnLocaleChange';
type TimeFilter = 'all' | 'today';
@@ -39,6 +41,14 @@ interface ParlayMatch {
homeTeamLogoUrl?: string | null;
awayTeamLogoUrl?: string | null;
startTime: string;
bettingOpen?: boolean;
matchPhase?: MatchPhase;
score?: {
htHome: number;
htAway: number;
ftHome: number;
ftAway: number;
} | null;
markets: Market[];
}
@@ -213,6 +223,7 @@ function isPicked(selectionId: string) {
const parlayHint = ref('');
function pickSelection(match: ParlayMatch, market: Market, sel: Selection) {
if (match.bettingOpen === false) return;
const err = slip.addParlayLeg({
selectionId: sel.id,
oddsVersion: String(sel.oddsVersion),
@@ -280,8 +291,22 @@ function toggleCollapse(id: string) {
</div>
<div v-else-if="filteredMatches.length" class="match-list">
<div v-for="match in filteredMatches" :key="match.id" class="match-card" :class="{ collapsed: collapsed.has(match.id) }">
<div
v-for="match in filteredMatches"
:key="match.id"
class="match-card"
:class="{
collapsed: collapsed.has(match.id),
'match-card--phase': (match.matchPhase ?? (match.bettingOpen === false ? 'closed_pending' : 'open')) !== 'open',
}"
>
<button type="button" class="match-head" @click="toggleCollapse(match.id)">
<StatusWatermark
v-if="(match.matchPhase ?? (match.bettingOpen === false ? 'closed_pending' : 'open')) !== 'open'"
:label="matchPhaseLabel(t, match.matchPhase ?? 'closed_pending')"
:variant="matchPhaseVariant(match.matchPhase ?? 'closed_pending')"
size="sm"
/>
<div class="match-head-top">
<span class="m-league">{{ match.leagueName }}</span>
<span class="toggle-dot" :class="{ open: !collapsed.has(match.id) }">
@@ -299,7 +324,13 @@ function toggleCollapse(id: string) {
<span class="m-name">{{ match.homeTeamName }}</span>
</div>
<div class="m-center">
<span class="m-time">{{ formatKickoff(match.startTime) }}</span>
<span
v-if="(match.matchPhase ?? (match.bettingOpen === false ? 'closed_pending' : 'open')) !== 'open' && match.score"
class="m-score"
>
{{ match.score.ftHome }} - {{ match.score.ftAway }}
</span>
<span v-else class="m-time">{{ formatKickoff(match.startTime) }}</span>
<span class="m-vs">VS</span>
</div>
<div class="m-team away">
@@ -335,7 +366,11 @@ function toggleCollapse(id: string) {
:key="sel.id"
type="button"
class="odd-btn"
:class="{ picked: isPicked(sel.id) }"
:class="{
picked: isPicked(sel.id),
'odd-btn--locked': match.bettingOpen === false,
}"
:disabled="match.bettingOpen === false"
@click="pickSelection(match, getMarket(match, col.key)!, sel)"
>
<span class="odd-label">{{ selLabel(sel) }}</span>
@@ -467,6 +502,8 @@ function toggleCollapse(id: string) {
}
.match-head {
position: relative;
overflow: hidden;
width: 100%;
display: flex;
flex-direction: column;
@@ -647,6 +684,27 @@ function toggleCollapse(id: string) {
background: rgba(212, 175, 55, 0.15);
}
.odd-btn--locked {
opacity: 0.65;
cursor: not-allowed;
border-color: #333;
}
.odd-btn--locked .odd-label,
.odd-btn--locked .odd-val {
color: #666;
}
.match-card--phase {
opacity: 0.94;
}
.m-score {
font-size: 12px;
font-weight: 800;
color: #fff;
}
.odd-label {
font-size: 8.5px;
font-weight: 700;