feat(admin,api,player): settlement stats, team crests, MS fields and list bet summary

This commit is contained in:
2026-06-04 17:30:48 +08:00
parent cc737e2924
commit 9fcee31a9a
27 changed files with 2296 additions and 427 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ref, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import api from '../api';
import BetHistoryCard, { type BetHistoryItem } from '../components/BetHistoryCard.vue';
@@ -7,30 +7,82 @@ import { useOnLocaleChange } from '../composables/useOnLocaleChange';
const { t } = useI18n();
const bets = ref<{ items: BetHistoryItem[]; total: number }>({ items: [], total: 0 });
const loading = ref(true);
const items = ref<BetHistoryItem[]>([]);
const total = ref(0);
const page = ref(1);
const loading = ref(false);
const initialLoading = ref(true);
const hasMore = ref(true);
async function load() {
const sentinel = ref<HTMLElement | null>(null);
let observer: IntersectionObserver | null = null;
async function loadPage(p: number) {
if (loading.value) return;
loading.value = true;
try {
const { data } = await api.get('/player/bets');
bets.value = data.data ?? { items: [], total: 0 };
const { data } = await api.get('/player/bets', { params: { page: p } });
const result = data.data ?? { items: [], total: 0, pageSize: 20 };
total.value = result.total ?? 0;
const pageSize = result.pageSize ?? 20;
if (p === 1) {
items.value = result.items ?? [];
} else {
items.value = [...items.value, ...(result.items ?? [])];
}
hasMore.value = items.value.length < total.value && (result.items?.length ?? 0) >= pageSize;
page.value = p;
} finally {
loading.value = false;
initialLoading.value = false;
}
}
useOnLocaleChange(load);
function reset() {
items.value = [];
total.value = 0;
page.value = 1;
hasMore.value = true;
initialLoading.value = true;
loadPage(1);
}
useOnLocaleChange(reset);
onMounted(() => {
observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore.value && !loading.value) {
loadPage(page.value + 1);
}
},
{ rootMargin: '120px' },
);
if (sentinel.value) observer.observe(sentinel.value);
});
onUnmounted(() => {
observer?.disconnect();
});
</script>
<template>
<div class="history-page">
<h2 class="page-title">{{ t('nav.bet_history') }}</h2>
<div v-if="loading" class="state">{{ t('bet.loading') }}</div>
<div v-if="initialLoading" class="state">{{ t('bet.loading') }}</div>
<template v-else-if="bets.items.length">
<BetHistoryCard v-for="bet in bets.items" :key="bet.betNo" :bet="bet" />
<template v-else-if="items.length">
<BetHistoryCard v-for="bet in items" :key="bet.betNo" :bet="bet" />
<div ref="sentinel" class="sentinel" />
<div v-if="loading" class="load-more-spinner">
<span class="spinner" />
</div>
<div v-else-if="!hasMore && items.length > 0" class="end-hint">
{{ t('history.no_more') }}
</div>
</template>
<div v-else class="state">{{ t('history.empty') }}</div>
@@ -39,15 +91,7 @@ useOnLocaleChange(load);
<style scoped>
.history-page {
padding-bottom: 8px;
}
.page-title {
font-size: 20px;
font-weight: 800;
color: var(--text);
margin-bottom: 16px;
letter-spacing: 0.02em;
padding-bottom: 24px;
}
.state {
@@ -57,4 +101,37 @@ useOnLocaleChange(load);
font-weight: 600;
font-size: 14px;
}
.sentinel {
height: 1px;
}
.load-more-spinner {
display: flex;
justify-content: center;
padding: 20px 0 8px;
}
.spinner {
display: inline-block;
width: 22px;
height: 22px;
border: 3px solid #2a2a2a;
border-top-color: var(--primary-light);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.end-hint {
text-align: center;
font-size: 12px;
color: #555;
font-weight: 600;
padding: 16px 0 4px;
letter-spacing: 0.03em;
}
</style>