Files
thebet365/apps/admin/src/views/matches/LeagueOutrightPanel.vue
Mars 24fa1b275c feat(admin,api,player): 优胜赛配置、赛事管理重构与玩家端投注体验优化
管理端拆分赛事/优胜赛 Tab,新增联赛优胜赔率面板(批量、排序、外侧删除);统一 list-chrome 工具栏对齐与列表页布局;Dashboard 失败重试、Users 操作下拉、小屏侧栏等体验修复。

API 扩展优胜赛与赛事目录接口,完善投注与钱包查询;玩家端重构赛事卡片、串关面板、注单/钱包页,新增注单详情、下注成功动画与下拉刷新。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 09:55:56 +08:00

227 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>