feat(admin,player,api): 优胜冠军通用管理与界面精简

管理端新增冠军盘列表/编辑、展开懒加载与 ECharts 修复;各列表页去掉重复标题。玩家端支持多赛事冠军盘、分批加载与语言切换刷新。API 扩展 outright CRUD 与列表性能优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-04 09:17:01 +08:00
parent 9b63d67e7c
commit 27580b2479
39 changed files with 2250 additions and 578 deletions

View File

@@ -0,0 +1,232 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import OutrightOptionCard from './OutrightOptionCard.vue';
import saishiImg from '../../assets/images/saishi.png';
export interface OutrightSelection {
id: string;
teamCode: string;
teamName: string;
odds: string;
oddsVersion: string;
}
export interface OutrightEvent {
id: string;
leagueId: string;
leagueCode?: string;
leagueName: string;
title: string;
selectionCount?: number;
selections: OutrightSelection[];
}
const props = defineProps<{
event: OutrightEvent;
expanded: boolean;
visibleLimit: number;
loadingMore: boolean;
}>();
const emit = defineEmits<{
toggle: [];
loadMore: [];
pick: [selection: OutrightSelection];
}>();
const { t } = useI18n();
const INITIAL_BATCH = 20;
const headTitle = computed(() => {
const raw = props.event.title.replace(/^\*+/, '').trim();
return raw || props.event.leagueName || t('bet.tab_outright');
});
const headMeta = computed(() => {
const total = props.event.selectionCount ?? props.event.selections.length;
return t('bet.outright_teams_count', { n: total });
});
const visibleSelections = computed(() =>
props.event.selections.slice(0, props.visibleLimit),
);
const hasMore = computed(
() => props.event.selections.length > props.visibleLimit,
);
const showLoadMore = computed(
() => props.event.selections.length > INITIAL_BATCH && hasMore.value,
);
</script>
<template>
<section class="event-block">
<button type="button" class="event-head" :aria-expanded="expanded" @click="emit('toggle')">
<span class="toggle-icon" :class="{ open: expanded }">
<span class="toggle-mark">{{ expanded ? '' : '+' }}</span>
</span>
<span class="event-head-text">
<span class="event-title">*{{ headTitle }}</span>
<span v-if="event.leagueName && event.leagueName !== headTitle" class="event-league">
{{ event.leagueName }}
</span>
<span class="event-meta">{{ headMeta }}</span>
</span>
<img :src="saishiImg" alt="" class="event-saishi" />
</button>
<div v-show="expanded" class="options-wrap">
<div class="options-grid">
<OutrightOptionCard
v-for="sel in visibleSelections"
:key="sel.id"
:team-code="sel.teamCode"
:team-name="sel.teamName"
:odds="sel.odds"
@pick="emit('pick', sel)"
/>
</div>
<div v-if="showLoadMore" class="load-more-zone">
<p class="load-more-hint">
{{
t('bet.outright_shown_count', {
shown: visibleSelections.length,
total: event.selections.length,
})
}}
</p>
<button
type="button"
class="load-more-btn"
:disabled="loadingMore"
@click="emit('loadMore')"
>
{{ loadingMore ? t('bet.loading') : t('bet.outright_load_more') }}
</button>
</div>
</div>
</section>
</template>
<style scoped>
.event-block {
margin-bottom: 10px;
}
.event-head {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 10px;
background: #141414;
border: 1px solid #2e2e2e;
border-radius: 6px;
text-align: left;
}
.toggle-icon {
flex-shrink: 0;
width: 26px;
height: 26px;
border-radius: 50%;
background: #141414;
border: 1px solid var(--border-gold-soft);
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.15s;
}
.toggle-icon.open {
border-color: var(--primary-light);
}
.toggle-mark {
color: var(--primary-light);
font-size: 17px;
font-weight: 900;
line-height: 1;
}
.event-head-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.event-title {
font-size: 13px;
font-weight: 800;
color: var(--primary-light);
line-height: 1.35;
}
.event-league {
font-size: 11px;
font-weight: 600;
color: #888;
line-height: 1.3;
}
.event-meta {
font-size: 11px;
font-weight: 600;
color: #666;
}
.event-saishi {
flex-shrink: 0;
height: 44px;
width: auto;
max-width: 40px;
object-fit: contain;
padding-left: 8px;
border-left: 1px solid #2a2a2a;
}
.options-wrap {
padding: 10px 0 4px;
}
.options-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.load-more-zone {
padding: 14px 8px 6px;
text-align: center;
}
.load-more-hint {
margin: 0 0 10px;
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
}
.load-more-btn {
width: 100%;
max-width: 280px;
padding: 11px 16px;
border-radius: 8px;
border: 1px solid var(--border-gold-soft);
background: linear-gradient(180deg, #1f1f1f, #141414);
color: var(--primary-light);
font-size: 13px;
font-weight: 800;
font-family: inherit;
letter-spacing: 0.04em;
}
.load-more-btn:disabled {
opacity: 0.65;
}
</style>