管理端拆分赛事/优胜赛 Tab,新增联赛优胜赔率面板(批量、排序、外侧删除);统一 list-chrome 工具栏对齐与列表页布局;Dashboard 失败重试、Users 操作下拉、小屏侧栏等体验修复。 API 扩展优胜赛与赛事目录接口,完善投注与钱包查询;玩家端重构赛事卡片、串关面板、注单/钱包页,新增注单详情、下注成功动画与下拉刷新。 Co-authored-by: Cursor <cursoragent@cursor.com>
227 lines
5.8 KiB
Vue
227 lines
5.8 KiB
Vue
<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>
|