feat(admin,api,player): 优胜赛配置、赛事管理重构与玩家端投注体验优化
管理端拆分赛事/优胜赛 Tab,新增联赛优胜赔率面板(批量、排序、外侧删除);统一 list-chrome 工具栏对齐与列表页布局;Dashboard 失败重试、Users 操作下拉、小屏侧栏等体验修复。 API 扩展优胜赛与赛事目录接口,完善投注与钱包查询;玩家端重构赛事卡片、串关面板、注单/钱包页,新增注单详情、下注成功动画与下拉刷新。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
273
apps/admin/src/views/MatchesOutrights.vue
Normal file
273
apps/admin/src/views/MatchesOutrights.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import api from '../api';
|
||||
import MatchesSubNav from '../components/MatchesSubNav.vue';
|
||||
import LeagueOutrightOddsPanel from './matches/LeagueOutrightOddsPanel.vue';
|
||||
import {
|
||||
readMatchesListUiState,
|
||||
writeMatchesListUiState,
|
||||
} from '../utils/matchesListState.ts';
|
||||
|
||||
const { t } = useAdminLocale();
|
||||
const route = useRoute();
|
||||
|
||||
const leagues = ref<unknown[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const keyword = ref('');
|
||||
const expandedRowKeys = ref<string[]>([]);
|
||||
|
||||
function persistListUiState() {
|
||||
writeMatchesListUiState({
|
||||
expandedLeagueIds: [...expandedRowKeys.value],
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
filterStatus: '',
|
||||
keyword: keyword.value,
|
||||
});
|
||||
}
|
||||
|
||||
function applyExpandedFromSaved(savedIds: string[]) {
|
||||
const allowed = new Set(leagues.value.map((row) => leagueId(row)));
|
||||
expandedRowKeys.value = savedIds.filter((id) => allowed.has(id));
|
||||
}
|
||||
|
||||
type LoadOptions = { restoreExpand?: boolean; keepExpand?: boolean };
|
||||
|
||||
async function load(options: LoadOptions = {}) {
|
||||
const saved = options.restoreExpand ? readMatchesListUiState() : null;
|
||||
if (saved) {
|
||||
page.value = saved.page;
|
||||
pageSize.value = saved.pageSize;
|
||||
keyword.value = saved.keyword;
|
||||
}
|
||||
|
||||
const { data } = await api.get('/admin/leagues', {
|
||||
params: {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: keyword.value.trim() || undefined,
|
||||
},
|
||||
});
|
||||
leagues.value = data.data.items;
|
||||
total.value = data.data.total;
|
||||
|
||||
if (options.restoreExpand && saved) {
|
||||
applyExpandedFromSaved(saved.expandedLeagueIds);
|
||||
} else if (!options.keepExpand) {
|
||||
expandedRowKeys.value = [];
|
||||
} else {
|
||||
applyExpandedFromSaved(expandedRowKeys.value);
|
||||
}
|
||||
persistListUiState();
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
page.value = 1;
|
||||
expandedRowKeys.value = [];
|
||||
load();
|
||||
}
|
||||
|
||||
async function resolveExpandFromQuery() {
|
||||
const qLeague = route.query.leagueId;
|
||||
if (typeof qLeague === 'string' && qLeague.trim()) {
|
||||
expandedRowKeys.value = [qLeague.trim()];
|
||||
persistListUiState();
|
||||
return;
|
||||
}
|
||||
const qMatch = route.query.matchId;
|
||||
if (typeof qMatch === 'string' && qMatch.trim()) {
|
||||
try {
|
||||
const { data } = await api.get(`/admin/outrights/${qMatch.trim()}`);
|
||||
const lid = data.data?.leagueId as string | undefined;
|
||||
if (lid) {
|
||||
expandedRowKeys.value = [lid];
|
||||
persistListUiState();
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await load({ restoreExpand: true });
|
||||
await resolveExpandFromQuery();
|
||||
});
|
||||
onBeforeUnmount(persistListUiState);
|
||||
|
||||
function onPageChange(p: number) {
|
||||
page.value = p;
|
||||
load({ keepExpand: true });
|
||||
}
|
||||
|
||||
function onSizeChange(size: number) {
|
||||
pageSize.value = size;
|
||||
page.value = 1;
|
||||
load({ keepExpand: true });
|
||||
}
|
||||
|
||||
function onExpandChange(_row: unknown, expanded: unknown[]) {
|
||||
expandedRowKeys.value = expanded.map((r) => leagueId(r));
|
||||
persistListUiState();
|
||||
}
|
||||
|
||||
function onRowClick(row: unknown, _column: unknown, event: MouseEvent) {
|
||||
if ((event.target as HTMLElement).closest('.el-table__expand-icon')) return;
|
||||
const id = leagueId(row);
|
||||
expandedRowKeys.value = expandedRowKeys.value.includes(id) ? [] : [id];
|
||||
persistListUiState();
|
||||
}
|
||||
|
||||
function rowClassName() {
|
||||
return 'row-expandable';
|
||||
}
|
||||
|
||||
function rowOf(row: unknown) {
|
||||
return row as Record<string, unknown>;
|
||||
}
|
||||
function leagueId(row: unknown) {
|
||||
return String(rowOf(row).id ?? '');
|
||||
}
|
||||
function leagueNameZh(row: unknown) {
|
||||
const zh = String(rowOf(row).leagueZh ?? '').trim();
|
||||
return zh || '—';
|
||||
}
|
||||
function leagueNameEn(row: unknown) {
|
||||
const en = String(rowOf(row).leagueEn ?? '').trim();
|
||||
return en || '—';
|
||||
}
|
||||
function outrightTeamCount(row: unknown) {
|
||||
return Number(rowOf(row).outrightTeamCount ?? 0);
|
||||
}
|
||||
function isLeagueExpanded(id: string) {
|
||||
return expandedRowKeys.value.includes(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-list-page matches-page">
|
||||
<div class="list-chrome">
|
||||
<div class="list-chrome__row">
|
||||
<div class="list-chrome__left">
|
||||
<MatchesSubNav embedded />
|
||||
<div class="list-chrome__filters">
|
||||
<div class="list-chrome__field">
|
||||
<span class="list-chrome__label">{{ t('common.keyword') }}</span>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
:placeholder="t('match.filter.keyword_ph')"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="onSearch"
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" class="list-chrome__submit" @click="onSearch">
|
||||
{{ t('common.search') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="list-panel">
|
||||
<p class="list-hint">{{ t('match.expand_outright_hint') }}</p>
|
||||
<div class="table-wrap">
|
||||
<el-table
|
||||
:data="leagues"
|
||||
stripe
|
||||
row-key="id"
|
||||
:expand-row-keys="expandedRowKeys"
|
||||
:row-class-name="rowClassName"
|
||||
@expand-change="onExpandChange"
|
||||
@row-click="onRowClick"
|
||||
>
|
||||
<el-table-column type="expand" width="40">
|
||||
<template #default="{ row }">
|
||||
<LeagueOutrightOddsPanel
|
||||
v-if="isLeagueExpanded(leagueId(row))"
|
||||
:league-id="leagueId(row)"
|
||||
@updated="load({ keepExpand: true })"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="id" label="ID" width="72" />
|
||||
<el-table-column :label="t('match.col.league')" width="148" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div class="league-cell">
|
||||
<img
|
||||
v-if="rowOf(row).logoUrl"
|
||||
:src="String(rowOf(row).logoUrl)"
|
||||
alt=""
|
||||
class="league-logo"
|
||||
/>
|
||||
<span class="matchup-link">{{ leagueNameZh(row) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('match.col.league_en')" width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="league-en">{{ leagueNameEn(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('outright.col.teams_total')" width="120" align="center">
|
||||
<template #default="{ row }">{{ outrightTeamCount(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('match.col.league_code')" width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ rowOf(row).code }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
@current-change="onPageChange"
|
||||
@size-change="onSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.matches-page .table-wrap .el-table {
|
||||
height: auto !important;
|
||||
}
|
||||
.matches-page .table-wrap :deep(.el-table__header),
|
||||
.matches-page .table-wrap :deep(.el-table__body) {
|
||||
width: 100% !important;
|
||||
}
|
||||
.matches-page :deep(.el-table__expanded-cell) {
|
||||
padding: 0 !important;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
.list-panel :deep(.row-expandable) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.matchup-link {
|
||||
color: var(--green-text);
|
||||
}
|
||||
.league-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.league-logo {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.league-en {
|
||||
color: #aaa;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user