feat(admin,api,player): 优胜赛配置、赛事管理重构与玩家端投注体验优化
管理端拆分赛事/优胜赛 Tab,新增联赛优胜赔率面板(批量、排序、外侧删除);统一 list-chrome 工具栏对齐与列表页布局;Dashboard 失败重试、Users 操作下拉、小屏侧栏等体验修复。 API 扩展优胜赛与赛事目录接口,完善投注与钱包查询;玩家端重构赛事卡片、串关面板、注单/钱包页,新增注单详情、下注成功动画与下拉刷新。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
226
apps/admin/src/views/matches/LeagueOutrightPanel.vue
Normal file
226
apps/admin/src/views/matches/LeagueOutrightPanel.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useAdminLocale } from '../../composables/useAdminLocale';
|
||||
import api from '../../api';
|
||||
|
||||
export interface LeagueOutrightSummary {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
leagueCode: string;
|
||||
status: string;
|
||||
selectionCount: number;
|
||||
playerVisible: boolean;
|
||||
playerHiddenReason: string | null;
|
||||
canImportCanonical: boolean;
|
||||
matchName: string;
|
||||
}
|
||||
|
||||
interface SelectionPreview {
|
||||
rank: number;
|
||||
teamZh: string;
|
||||
teamCode: string;
|
||||
odds: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
leagueId: string;
|
||||
event: LeagueOutrightSummary | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
updated: [];
|
||||
create: [];
|
||||
}>();
|
||||
|
||||
const { t } = useAdminLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const applying = ref(false);
|
||||
const selections = ref<SelectionPreview[]>([]);
|
||||
const hiddenReason = ref<string | null>(null);
|
||||
|
||||
function hiddenTip(reason: string | null) {
|
||||
if (!reason) return '';
|
||||
return t(`outright.hidden_reason.${reason}`);
|
||||
}
|
||||
|
||||
function goEdit() {
|
||||
if (!props.event) return;
|
||||
router.push({ name: 'admin-outright-edit', params: { matchId: props.event.id } });
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.event) {
|
||||
selections.value = [];
|
||||
hiddenReason.value = null;
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await api.get(`/admin/outrights/${props.event.id}`);
|
||||
const payload = data.data as {
|
||||
playerHiddenReason: string | null;
|
||||
selections: SelectionPreview[];
|
||||
};
|
||||
hiddenReason.value = payload.playerHiddenReason;
|
||||
selections.value = (payload.selections ?? []).slice(0, 8);
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyCanonical() {
|
||||
if (!props.event?.canImportCanonical) return;
|
||||
applying.value = true;
|
||||
try {
|
||||
await api.post('/admin/outrights/import/wc2026');
|
||||
ElMessage.success(t('msg.outright_canonical_applied'));
|
||||
emit('updated');
|
||||
await loadDetail();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
applying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.event?.id,
|
||||
() => loadDetail(),
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="league-outright-panel">
|
||||
<div class="panel-head">
|
||||
<div class="panel-head-text">
|
||||
<span class="panel-title">{{ t('nav.outrights') }}</span>
|
||||
<span class="panel-hint">{{ t('match.outright.section_hint') }}</span>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<template v-if="event">
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="event.status === 'PUBLISHED' ? 'success' : 'info'"
|
||||
effect="dark"
|
||||
>
|
||||
{{
|
||||
event.status === 'PUBLISHED'
|
||||
? t('outright.status.published')
|
||||
: t('outright.status.draft')
|
||||
}}
|
||||
</el-tag>
|
||||
<el-tag size="small" :type="event.playerVisible ? 'success' : 'warning'" effect="plain">
|
||||
{{ event.playerVisible ? t('outright.col.player_visible') : t('outright.not_on_player') }}
|
||||
</el-tag>
|
||||
<el-button type="primary" size="small" @click="goEdit">
|
||||
{{ t('common.edit') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="event.canImportCanonical"
|
||||
size="small"
|
||||
:loading="applying"
|
||||
@click="applyCanonical"
|
||||
>
|
||||
{{ t('outright.btn.apply_canonical') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<el-button v-else type="primary" plain size="small" @click="emit('create')">
|
||||
{{ t('match.outright.setup') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="event" v-loading="loading" class="panel-body">
|
||||
<p v-if="event.matchName" class="meta-line">{{ event.matchName }}</p>
|
||||
<p class="meta-line">
|
||||
{{ t('outright.col.teams') }}:{{ event.selectionCount }}
|
||||
</p>
|
||||
<p v-if="!event.playerVisible && hiddenReason" class="meta-warn">
|
||||
{{ hiddenTip(hiddenReason) }}
|
||||
</p>
|
||||
<el-table
|
||||
v-if="selections.length"
|
||||
:data="selections"
|
||||
size="small"
|
||||
class="preview-table"
|
||||
max-height="200"
|
||||
>
|
||||
<el-table-column prop="rank" :label="t('outright.col.rank')" width="56" />
|
||||
<el-table-column prop="teamZh" :label="t('outright.col.team_zh')" min-width="100" />
|
||||
<el-table-column prop="teamCode" :label="t('outright.col.code')" width="72" />
|
||||
<el-table-column prop="odds" :label="t('outright.col.odds')" width="88" align="right" />
|
||||
</el-table>
|
||||
<p v-else-if="!loading" class="meta-empty">{{ t('outright.expand_no_teams') }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.league-outright-panel {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: rgba(47, 181, 106, 0.04);
|
||||
border: 1px solid rgba(47, 181, 106, 0.14);
|
||||
}
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.panel-head-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
.panel-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--green-text);
|
||||
}
|
||||
.panel-hint {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.panel-body {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.meta-line {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
||||
.meta-warn {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: #e6a23c;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.meta-empty {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.preview-table {
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user