feat(theme-3): sync inbox/announcements/presence/deposit/search from main

This commit is contained in:
2026-06-18 10:02:32 +08:00
parent 0d430857c7
commit 6881d325d5
87 changed files with 6944 additions and 964 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onActivated } from 'vue';
import { onActivated, computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import emptyMatchesImg from '../assets/images/empty-matches.svg';
@@ -11,11 +11,28 @@ import TeamEmblem from '../components/TeamEmblem.vue';
import GoldSpinner from '../components/GoldSpinner.vue';
import { usePullToRefresh } from '../composables/usePullToRefresh';
import { formatLocalMatchDateTime } from '@thebet365/shared';
import type { PlayerHomeMatch } from '../composables/usePlayerHome';
type HotTab = 'hot' | 'upcoming';
const matchCardBg = `url(${cardBg})`;
const { t, locale } = useI18n();
const router = useRouter();
const { banners, hotMatches, loading, load } = usePlayerHome();
const { banners, hotMatches, upcomingMatches, loading, load, announcementItems } = usePlayerHome();
const activeTab = ref<HotTab>('hot');
const bannerFallbackTo = computed(() => {
const id = announcementItems.value[0]?.id;
return id ? `/announcements/${id}` : '/announcements';
});
const displayedMatches = computed<PlayerHomeMatch[]>(() =>
activeTab.value === 'hot' ? hotMatches.value : upcomingMatches.value,
);
const emptyMessage = computed(() =>
activeTab.value === 'hot' ? t('home.no_matches') : t('home.upcoming_empty'),
);
const { pullDistance, refreshing, spinning, progress } = usePullToRefresh({
onRefresh: async () => { await load(true); },
@@ -47,16 +64,36 @@ function formatKickoff(startTime: string) {
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
</div>
<BannerCarousel :banners="banners" />
<BannerCarousel :banners="banners" :fallback-to="bannerFallbackTo" />
<div class="hot-tabs" role="tablist" :aria-label="t('home.hot_matches')">
<button
type="button"
role="tab"
class="hot-tab"
:class="{ active: activeTab === 'hot' }"
:aria-selected="activeTab === 'hot'"
@click="activeTab = 'hot'"
>
{{ t('home.hot_tab') }}
</button>
<button
type="button"
role="tab"
class="hot-tab"
:class="{ active: activeTab === 'upcoming' }"
:aria-selected="activeTab === 'upcoming'"
@click="activeTab = 'upcoming'"
>
{{ t('home.upcoming_tab') }}
</button>
</div>
<h2 class="section-title">{{ t('home.hot_matches') }}</h2>
<div
v-for="(match, index) in hotMatches"
v-for="(match, index) in displayedMatches"
:key="match.id"
class="match-card"
:class="{ 'match-card--live-anim': index < 3 }"
:class="{ 'match-card--live-anim': activeTab === 'hot' && index < 3 }"
@click="goMatch(match.id)"
>
<div class="match-info">
@@ -104,9 +141,9 @@ function formatKickoff(startTime: string) {
</div>
</div>
<div v-if="!loading && !hotMatches.length" class="empty">
<div v-if="!loading && !displayedMatches.length" class="empty">
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
<p>{{ t('home.no_matches') }}</p>
<p>{{ emptyMessage }}</p>
</div>
</div>
</template>
@@ -120,6 +157,31 @@ function formatKickoff(startTime: string) {
transition: height 0.15s ease;
}
.hot-tabs {
display: flex;
gap: 0;
margin-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.hot-tab {
flex: 1;
padding: 12px 6px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--text-muted);
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.hot-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.quick-entries {
display: grid;
grid-template-columns: repeat(4, 1fr);