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

This commit is contained in:
2026-06-18 09:38:33 +08:00
parent ec51b675e3
commit e56029225e
86 changed files with 6828 additions and 928 deletions

View File

@@ -13,17 +13,19 @@ import BackToTopButton from '../components/BackToTopButton.vue';
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
const BetSlipDrawer = defineAsyncComponent(() => import('../components/BetSlipDrawer.vue'));
const CustomerServiceModal = defineAsyncComponent(
() => import('../components/CustomerServiceModal.vue'),
);
import { usePlayerHome } from '../composables/usePlayerHome';
import { useInboxFeature } from '../composables/useInboxFeature';
import { usePlayerProfile } from '../composables/usePlayerProfile';
import { useDepositNotifications } from '../composables/useDepositNotifications';
import { usePlayerMessages } from '../composables/usePlayerMessages';
import { startPresencePing, stopPresencePing } from '../composables/usePresencePing';
const { t, locale } = useI18n();
const auth = useAuthStore();
const { initFromUser } = useAppLocale();
const route = useRoute();
const slip = useBetSlipStore();
const { inboxEnabled, hubRoute, hubOpenLabelKey } = useInboxFeature();
const isDetailPage = computed(() => {
const p = route.path;
@@ -32,31 +34,40 @@ const isDetailPage = computed(() => {
p.startsWith('/bet/') ||
p.startsWith('/bets/') ||
p.startsWith('/wallet/') ||
p.startsWith('/messages') ||
p === '/profile/edit' ||
p === '/profile/cashbacks'
);
});
const showHeader = computed(() => !isDetailPage.value);
const showAnnouncement = computed(() => !isDetailPage.value && !route.path.startsWith('/profile'));
const showAnnouncement = computed(
() => !isDetailPage.value && !route.path.startsWith('/profile') && !route.path.startsWith('/announcements'),
);
const showBottomNav = computed(() => {
const p = route.path;
if (
p === '/' ||
p === '/bet' ||
p === '/announcements' ||
p.startsWith('/announcements/') ||
p.startsWith('/match/') ||
p === '/bets' ||
p === '/wallet' ||
p === '/profile'
) return true;
// 邮箱关闭时客服页保留底部导航,避免用户无法离开
if (!inboxEnabled.value && p === '/messages') return true;
return false;
});
const { announcements, load: loadPlayerHome } = usePlayerHome();
const { announcements, announcementItems, load: loadPlayerHome } = usePlayerHome();
const { loadProfile, refreshProfile, bindProfileVisibilityRefresh } = usePlayerProfile();
const { startPolling, stopPolling } = useDepositNotifications();
const { unreadCount, refreshUnreadCount, resetMessagesState } = usePlayerMessages();
const mainRef = ref<HTMLElement | null>(null);
const tabScrollTops = new Map<string, number>();
const customerServiceOpen = ref(false);
const primaryAnnouncementId = computed(() => announcementItems.value[0]?.id ?? '');
watch(locale, (next, prev) => {
if (prev && next !== prev) void loadPlayerHome(true);
@@ -87,6 +98,13 @@ watch(
// 个人资料仅登录用户需要
if (token) {
void loadProfile(true);
startPolling();
startPresencePing();
if (inboxEnabled.value) void refreshUnreadCount();
} else {
stopPolling();
stopPresencePing();
resetMessagesState();
}
},
{ immediate: true },
@@ -96,6 +114,9 @@ watch(
() => route.path,
(path) => {
if (!auth.token) return;
if (inboxEnabled.value && path.startsWith('/messages')) {
void refreshUnreadCount();
}
if (balanceRefreshPaths.some((p) => path === p || path.startsWith(`${p}/`))) {
void refreshProfile();
}
@@ -112,11 +133,10 @@ watch(
<text x="74" y="18" font-family="'Inter','SF Pro Display',system-ui,sans-serif" font-size="18" font-weight="900" fill="#F8971F" letter-spacing="0.01em">365</text>
</svg>
<div class="header-actions">
<button
type="button"
<RouterLink
:to="hubRoute"
class="support-btn"
:aria-label="t('support.open')"
@click="customerServiceOpen = true"
:aria-label="inboxEnabled && unreadCount ? t('messages.unread_badge', { count: unreadCount }) : t(hubOpenLabelKey)"
>
<svg class="support-icon" viewBox="0 0 24 24" aria-hidden="true">
<path
@@ -130,7 +150,8 @@ watch(
<circle cx="12" cy="11" r="1" fill="currentColor" />
<circle cx="15" cy="11" r="1" fill="currentColor" />
</svg>
</button>
<span v-if="auth.user && inboxEnabled && unreadCount > 0" class="hub-badge">{{ unreadCount > 99 ? '99+' : unreadCount }}</span>
</RouterLink>
<div class="header-divider" />
<LocaleSwitcher />
<template v-if="auth.user">
@@ -144,7 +165,11 @@ watch(
</header>
<div v-if="showAnnouncement" class="announce-strip">
<AnnouncementMarquee :items="announcements" embedded />
<AnnouncementMarquee
:items="announcements"
:target-id="primaryAnnouncementId"
embedded
/>
</div>
<main ref="mainRef" :class="['main', { 'has-nav': showBottomNav }]">
@@ -186,7 +211,6 @@ watch(
<BackToTopButton :scroll-el="mainRef" :above-nav="showBottomNav" />
<BetSlipDrawer v-model="slip.drawerOpen" />
<CustomerServiceModal v-model="customerServiceOpen" />
</div>
</template>
@@ -258,6 +282,8 @@ watch(
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
flex-shrink: 0;
position: relative;
text-decoration: none;
}
.support-btn:active {
background: var(--bg-hover);
@@ -270,6 +296,22 @@ watch(
flex-shrink: 0;
}
.hub-badge {
position: absolute;
top: -4px;
right: -4px;
min-width: 16px;
padding: 0 4px;
border-radius: 8px;
background: var(--primary);
color: #fff;
font-size: 10px;
font-weight: 700;
line-height: 16px;
text-align: center;
pointer-events: none;
}
.support-label {
display: none;
}