Files
thebet365/apps/admin/src/composables/useAdminDashboard.ts
Mars 03f54ca689 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>
2026-06-10 13:00:14 +08:00

127 lines
3.4 KiB
TypeScript

import { ref, computed } from 'vue';
import api from '../api';
import type { AdminDashboard } from '../views/dashboard-types';
import { formatAmount, formatAmountFull } from '../utils/format-amount';
import { useAdminLocale } from './useAdminLocale';
const stats = ref<AdminDashboard | null>(null);
const loading = ref(false);
const loadError = ref(false);
let inflight: Promise<void> | null = null;
export function useAdminDashboard() {
const { t, locale, localeTag } = useAdminLocale();
const s = computed(() => stats.value);
async function load(force = false) {
if (inflight && !force) return inflight;
if (stats.value && !force) return;
loading.value = true;
loadError.value = false;
inflight = (async () => {
try {
const { data } = await api.get('/admin/dashboard');
stats.value = data.data as AdminDashboard;
} catch {
stats.value = null;
loadError.value = true;
} finally {
loading.value = false;
inflight = null;
}
})();
return inflight;
}
function fmtCount(val: number | undefined) {
return (val ?? 0).toLocaleString(localeTag.value, { maximumFractionDigits: 0 });
}
function formatTime(v: string) {
return new Date(v).toLocaleString(localeTag.value, {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
function toNum(v: string | number | undefined) {
const n = typeof v === 'number' ? v : parseFloat(v ?? '0');
return Number.isFinite(n) ? n : 0;
}
function pctChange(today: string | number, yesterday: string | number) {
const t0 = toNum(today);
const y = toNum(yesterday);
if (y === 0) return t0 > 0 ? '+100%' : '—';
const p = ((t0 - y) / y) * 100;
const sign = p > 0 ? '+' : '';
return `${sign}${p.toFixed(1)}%`;
}
const chartI18n = computed(() => ({
locale: locale.value,
betCountSeries: t('dash.chart_bet_count'),
axisAmount: t('dash.axis_amount'),
axisCount: t('dash.axis_count'),
countSuffix: t('dash.count_suffix'),
pieTooltip: t('dash.chart_tooltip'),
noData: t('common.no_data'),
pieEmpty: t('dash.pie_empty'),
}));
const trendLabels = computed(() => s.value?.trend7d?.map((d) => d.label) ?? []);
const kpiPrimary = computed(() => {
if (!s.value) return [];
const td = s.value.today;
const y = s.value.yesterday;
const yLabel = t('common.yesterday');
return [
{
label: t('dash.kpi_bet_count'),
value: fmtCount(td.betCount),
sub: `${yLabel} ${fmtCount(y.betCount)}`,
delta: pctChange(td.betCount, y.betCount),
},
{
label: t('dash.kpi_stake'),
value: formatAmount(td.stake, 2, locale.value),
sub: formatAmountFull(td.stake, locale.value),
delta: pctChange(td.stake, y.stake),
},
{
label: t('dash.kpi_payout'),
value: formatAmount(td.payout, 2, locale.value),
sub: `${yLabel} ${formatAmount(y.payout, 2, locale.value)}`,
delta: pctChange(td.payout, y.payout),
},
{
label: t('dash.kpi_ggr'),
value: formatAmount(td.ggr, 2, locale.value),
sub: `${yLabel} ${formatAmount(y.ggr, 2, locale.value)}`,
delta: pctChange(td.ggr, y.ggr),
},
];
});
return {
stats,
s,
loading,
loadError,
load,
fmtCount,
formatTime,
toNum,
chartI18n,
trendLabels,
kpiPrimary,
};
}