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:
191
apps/admin/src/views/dashboard/DashboardPlayers.vue
Normal file
191
apps/admin/src/views/dashboard/DashboardPlayers.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useAdminDashboard } from '../../composables/useAdminDashboard';
|
||||
import EChartPanel from '../../components/dashboard/EChartPanel.vue';
|
||||
import { buildTriplePieOption } from '../../utils/dashboard-charts';
|
||||
import { betStatusLabel } from '../../utils/bet-labels';
|
||||
import { formatAmount } from '../../utils/format-amount';
|
||||
import { useAdminLocale } from '../../composables/useAdminLocale';
|
||||
|
||||
const { t, locale, localeTag } = useAdminLocale();
|
||||
const {
|
||||
s,
|
||||
loading,
|
||||
loadError,
|
||||
load,
|
||||
formatTime,
|
||||
chartI18n,
|
||||
fmtCount,
|
||||
} = useAdminDashboard();
|
||||
|
||||
onMounted(() => {
|
||||
void load();
|
||||
});
|
||||
|
||||
const kpiPlayer = computed(() => {
|
||||
if (!s.value) return [];
|
||||
return [
|
||||
{
|
||||
label: t('dash.kpi_users'),
|
||||
value: `${fmtCount(s.value.users.playersTotal)} / ${fmtCount(s.value.users.agentsTotal)}`,
|
||||
sub: t('dash.kpi_new_players', { n: fmtCount(s.value.today.newPlayers) }),
|
||||
},
|
||||
{
|
||||
label: t('dash.kpi_agents_active'),
|
||||
value: fmtCount(s.value.users.agentsActive),
|
||||
sub: t('dash.kpi_agents_active_sub', {
|
||||
suspended: fmtCount(s.value.users.playersSuspended),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: t('dash.kpi_wallet'),
|
||||
value: formatAmount(s.value.wallets.totalAvailable, 2, locale.value),
|
||||
sub: `${t('common.frozen')} ${formatAmount(s.value.wallets.totalFrozen, 2, locale.value)}`,
|
||||
},
|
||||
{
|
||||
label: t('dash.kpi_credit'),
|
||||
value: formatAmount(s.value.agents.totalAvailableCredit, 2, locale.value),
|
||||
sub: `${t('common.used')} ${formatAmount(s.value.agents.totalUsedCredit, 2, locale.value)}`,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const userDistributionOption = computed(() => {
|
||||
const u = s.value?.users;
|
||||
const userSegs = u
|
||||
? [
|
||||
{ label: t('dash.user_active'), value: u.playersActive, color: '#248f54' },
|
||||
{ label: t('dash.user_suspended'), value: u.playersSuspended, color: '#f87171' },
|
||||
{ label: t('dash.user_direct'), value: u.playersDirect, color: '#60a5fa' },
|
||||
{ label: t('dash.user_agents'), value: u.agentsTotal, color: '#a78bfa' },
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
|
||||
return buildTriplePieOption(
|
||||
[
|
||||
{ title: t('dash.pie_users'), segments: userSegs },
|
||||
{ title: '', segments: [] },
|
||||
{ title: '', segments: [] },
|
||||
],
|
||||
chartI18n.value,
|
||||
);
|
||||
});
|
||||
|
||||
function formatBetTime(v: string) {
|
||||
return new Date(v).toLocaleString(localeTag.value, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard-page" v-loading="loading">
|
||||
<el-card v-if="!loading && loadError" class="state-card" shadow="never">
|
||||
<p class="state-title">{{ t('msg.load_failed') }}</p>
|
||||
<p class="state-hint">{{ t('dash.load_error_hint') }}</p>
|
||||
<el-button type="primary" size="small" @click="load(true)">{{ t('common.retry') }}</el-button>
|
||||
</el-card>
|
||||
|
||||
<template v-else-if="s">
|
||||
<el-card class="overview-board" shadow="never">
|
||||
<div v-if="s.generatedAt" class="board-head">
|
||||
<span class="board-hint">{{ t('dash.section_players_hint') }}</span>
|
||||
<span class="dash-updated">
|
||||
{{ t('common.updated_at') }} {{ formatTime(s.generatedAt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="kpi-grid kpi-secondary">
|
||||
<div v-for="item in kpiPlayer" :key="item.label" class="kpi-cell compact">
|
||||
<span class="kpi-label">{{ item.label }}</span>
|
||||
<span class="kpi-value sm">{{ item.value }}</span>
|
||||
<span class="kpi-sub">{{ item.sub }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-stack">
|
||||
<EChartPanel title="" :option="userDistributionOption" height="200px" class="chart-dist" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div class="recent-grid">
|
||||
<el-card class="recent-card" shadow="never">
|
||||
<h3 class="recent-title">{{ t('dash.recent_players') }}</h3>
|
||||
<el-table :data="s.recentPlayers" stripe empty-text="—" size="small">
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="120" />
|
||||
<el-table-column prop="parentUsername" :label="t('dash.col_parent')" min-width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.parentUsername || t('common.platform_direct') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" :label="t('common.status')" width="88" />
|
||||
<el-table-column prop="createdAt" :label="t('user.col.created')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatBetTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card class="recent-card" shadow="never">
|
||||
<h3 class="recent-title">{{ t('dash.recent_bets') }}</h3>
|
||||
<el-table :data="s.recentBets" stripe empty-text="—" size="small">
|
||||
<el-table-column prop="betNo" :label="t('dash.col_bet_no')" min-width="120" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" />
|
||||
<el-table-column prop="stake" :label="t('dash.col_stake')" min-width="88">
|
||||
<template #default="{ row }">
|
||||
{{ formatAmount(row.stake, 2, locale) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" :label="t('common.status')" width="88">
|
||||
<template #default="{ row }">
|
||||
{{ betStatusLabel(row.status) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="placedAt" :label="t('dash.col_placed_at')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatBetTime(row.placedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import './dashboard-board.css';
|
||||
|
||||
.recent-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.recent-card {
|
||||
border-radius: 14px;
|
||||
border: 1px solid #1e1e1e;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.recent-card :deep(.el-card__body) {
|
||||
padding: 16px 18px 12px;
|
||||
}
|
||||
|
||||
.recent-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.recent-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user