feat: split admin dashboard, improve match ops, and player closed-match UX
Admin: add match/player overview sub-nav; refine settlement flow and league match management UI; improve action button enabled/disabled styles; enhance logo upload and outright odds sync. API: expose matchPhase/bettingOpen for closed matches; league publish guards; settlement preview with auto score save; outright team auto-sync. Player: watermark for closed/settled states; keep match and bet details visible; remove default login credentials. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -69,6 +69,7 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const previewing = ref(false);
|
||||
const match = ref<AdminMatchDetail | null>(null);
|
||||
const score = ref({ htHome: 0, htAway: 0, ftHome: 0, ftAway: 0 });
|
||||
const preview = ref<Record<string, unknown> | null>(null);
|
||||
@@ -340,20 +341,6 @@ function settlementApiError(e: unknown, fallback: string) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
async function saveScore() {
|
||||
await api.post(`/admin/matches/${matchId.value}/settlement/score`, score.value);
|
||||
}
|
||||
|
||||
async function recordScore() {
|
||||
try {
|
||||
await saveScore();
|
||||
ElMessage.success(t('msg.score_recorded'));
|
||||
await loadMatch();
|
||||
} catch (e: unknown) {
|
||||
ElMessage.error(settlementApiError(e, t('msg.save_failed')));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreviewItems(page = previewPage.value, pageSize = previewPageSize.value) {
|
||||
const batch = preview.value?.batch as { id: string } | undefined;
|
||||
if (!batch) return;
|
||||
@@ -374,9 +361,10 @@ async function loadPreviewItems(page = previewPage.value, pageSize = previewPage
|
||||
async function previewSettlement() {
|
||||
preview.value = null;
|
||||
previewPage.value = 1;
|
||||
previewing.value = true;
|
||||
try {
|
||||
await saveScore();
|
||||
const { data } = await api.post(`/admin/matches/${matchId.value}/settlement/preview`, {
|
||||
...score.value,
|
||||
page: 1,
|
||||
pageSize: previewPageSize.value,
|
||||
});
|
||||
@@ -386,6 +374,8 @@ async function previewSettlement() {
|
||||
previewPageSize.value = itemsPage.pageSize;
|
||||
} catch (e: unknown) {
|
||||
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
|
||||
} finally {
|
||||
previewing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,77 +416,76 @@ onMounted(() => {
|
||||
|
||||
<el-card v-if="match" class="settle-top-card" shadow="never">
|
||||
<p v-if="leagueLabel" class="match-league">{{ leagueLabel }}</p>
|
||||
<div class="settle-top-row">
|
||||
<div class="match-inline">
|
||||
<div class="team-chip">
|
||||
<img
|
||||
v-if="match.homeTeamLogoUrl"
|
||||
:src="match.homeTeamLogoUrl"
|
||||
alt=""
|
||||
class="team-logo-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span class="team-name-inline">{{ homeLabel }}</span>
|
||||
<div class="match-inline">
|
||||
<div class="team-chip">
|
||||
<img
|
||||
v-if="match.homeTeamLogoUrl"
|
||||
:src="match.homeTeamLogoUrl"
|
||||
alt=""
|
||||
class="team-logo-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span class="team-name-inline">{{ homeLabel }}</span>
|
||||
</div>
|
||||
<span class="vs">VS</span>
|
||||
<div class="team-chip">
|
||||
<img
|
||||
v-if="match.awayTeamLogoUrl"
|
||||
:src="match.awayTeamLogoUrl"
|
||||
alt=""
|
||||
class="team-logo-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span class="team-name-inline">{{ awayLabel }}</span>
|
||||
</div>
|
||||
<span class="kickoff-inline">
|
||||
<span class="meta-k">{{ t('settlement.kickoff') }}</span>
|
||||
{{ kickoffLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="settle-score-row">
|
||||
<div class="score-inline-group">
|
||||
<div class="score-block compact">
|
||||
<span class="score-title">{{ t('settlement.ht_score') }}</span>
|
||||
<div class="score-inputs">
|
||||
<el-input-number v-model="score.htHome" :min="0" controls-position="right" style="width: 88px" />
|
||||
<span class="score-sep">—</span>
|
||||
<el-input-number v-model="score.htAway" :min="0" controls-position="right" style="width: 88px" />
|
||||
</div>
|
||||
</div>
|
||||
<span class="vs">VS</span>
|
||||
<div class="team-chip">
|
||||
<img
|
||||
v-if="match.awayTeamLogoUrl"
|
||||
:src="match.awayTeamLogoUrl"
|
||||
alt=""
|
||||
class="team-logo-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span class="team-name-inline">{{ awayLabel }}</span>
|
||||
<div class="score-block compact">
|
||||
<span class="score-title">{{ t('settlement.ft_score') }}</span>
|
||||
<div class="score-inputs">
|
||||
<el-input-number v-model="score.ftHome" :min="0" controls-position="right" style="width: 88px" />
|
||||
<span class="score-sep">—</span>
|
||||
<el-input-number v-model="score.ftAway" :min="0" controls-position="right" style="width: 88px" />
|
||||
</div>
|
||||
</div>
|
||||
<span class="kickoff-inline">
|
||||
<span class="meta-k">{{ t('settlement.kickoff') }}</span>
|
||||
{{ kickoffLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="score-panel">
|
||||
<div class="score-inline-group">
|
||||
<div class="score-block compact">
|
||||
<span class="score-title">{{ t('settlement.ht_score') }}</span>
|
||||
<div class="score-inputs">
|
||||
<el-input-number v-model="score.htHome" :min="0" controls-position="right" style="width: 88px" />
|
||||
<span class="score-sep">—</span>
|
||||
<el-input-number v-model="score.htAway" :min="0" controls-position="right" style="width: 88px" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-block compact">
|
||||
<span class="score-title">{{ t('settlement.ft_score') }}</span>
|
||||
<div class="score-inputs">
|
||||
<el-input-number v-model="score.ftHome" :min="0" controls-position="right" style="width: 88px" />
|
||||
<span class="score-sep">—</span>
|
||||
<el-input-number v-model="score.ftAway" :min="0" controls-position="right" style="width: 88px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<!-- 智能比分入口已关闭
|
||||
<el-button size="small" type="warning" plain @click="openSmartDialog">
|
||||
{{ t('settlement.smart.btn') }}
|
||||
</el-button>
|
||||
-->
|
||||
<el-button size="small" @click="recordScore">{{ t('settlement.record_score') }}</el-button>
|
||||
<el-button type="primary" size="small" @click="previewSettlement">
|
||||
<div class="settle-actions">
|
||||
<template v-if="!isSettled">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="previewing"
|
||||
@click="previewSettlement"
|
||||
>
|
||||
{{ t('settlement.preview_btn') }}
|
||||
</el-button>
|
||||
<span class="preview-hint">{{ t('settlement.preview_hint') }}</span>
|
||||
<template v-if="isSettled">
|
||||
<el-input
|
||||
v-model="resettleReason"
|
||||
size="small"
|
||||
:placeholder="t('settlement.resettle_reason')"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<el-button type="warning" size="small" plain @click="previewResettlement">
|
||||
{{ t('settlement.resettle_preview') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-input
|
||||
v-model="resettleReason"
|
||||
size="small"
|
||||
:placeholder="t('settlement.resettle_reason')"
|
||||
class="settle-resettle-reason"
|
||||
/>
|
||||
<el-button type="warning" plain @click="previewResettlement">
|
||||
{{ t('settlement.resettle_preview') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -741,12 +730,27 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settle-top-row {
|
||||
.settle-score-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px 24px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.settle-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settle-resettle-reason {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.match-inline {
|
||||
@@ -789,14 +793,6 @@ onMounted(() => {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.score-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.score-inline-group {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -949,13 +945,6 @@ onMounted(() => {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-hint {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
Reference in New Issue
Block a user