Merge main into theme-2 and resolve conflicts for desktop architecture
This commit is contained in:
@@ -7,10 +7,11 @@ import CashBalanceChip from '../components/CashBalanceChip.vue';
|
||||
import UserAvatarMenu from '../components/UserAvatarMenu.vue';
|
||||
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
||||
import { useAppLocale } from '../composables/useAppLocale';
|
||||
import AnnouncementMarquee from '../components/AnnouncementMarquee.vue';
|
||||
import BottomNavIcon from '../components/BottomNavIcon.vue';
|
||||
import BackToTopButton from '../components/BackToTopButton.vue';
|
||||
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useViewport } from '../composables/useViewport';
|
||||
import DesktopShell from '../components/desktop/DesktopShell.vue';
|
||||
|
||||
const BetSlipDrawer = defineAsyncComponent(() => import('../components/BetSlipDrawer.vue'));
|
||||
import { usePlayerHome } from '../composables/usePlayerHome';
|
||||
@@ -20,6 +21,7 @@ import { useDepositNotifications } from '../composables/useDepositNotifications'
|
||||
import { usePlayerMessages } from '../composables/usePlayerMessages';
|
||||
import { startPresencePing, stopPresencePing } from '../composables/usePresencePing';
|
||||
|
||||
const { isDesktop } = useViewport();
|
||||
const { t, locale } = useI18n();
|
||||
const auth = useAuthStore();
|
||||
const { initFromUser } = useAppLocale();
|
||||
@@ -40,6 +42,7 @@ const isDetailPage = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const showHeader = computed(() => !isDetailPage.value);
|
||||
const showHeader = computed(() => !isDetailPage.value);
|
||||
const showAnnouncement = computed(
|
||||
() => !isDetailPage.value && !route.path.startsWith('/profile') && !route.path.startsWith('/announcements'),
|
||||
@@ -68,14 +71,36 @@ const { unreadCount, refreshUnreadCount, resetMessagesState } = usePlayerMessage
|
||||
const mainRef = ref<HTMLElement | null>(null);
|
||||
const tabScrollTops = new Map<string, number>();
|
||||
const primaryAnnouncementId = computed(() => announcementItems.value[0]?.id ?? '');
|
||||
const bottomNavHidden = ref(false);
|
||||
let lastMainScrollTop = 0;
|
||||
|
||||
watch(locale, (next, prev) => {
|
||||
if (prev && next !== prev) void loadPlayerHome(true);
|
||||
function onMainScroll() {
|
||||
const el = mainRef.value;
|
||||
if (!el || !showBottomNav.value) return;
|
||||
|
||||
const scrollTop = el.scrollTop;
|
||||
const delta = scrollTop - lastMainScrollTop;
|
||||
|
||||
if (scrollTop <= 8) {
|
||||
bottomNavHidden.value = false;
|
||||
} else if (delta > 10) {
|
||||
bottomNavHidden.value = true;
|
||||
} else if (delta < -10) {
|
||||
bottomNavHidden.value = false;
|
||||
}
|
||||
|
||||
lastMainScrollTop = scrollTop;
|
||||
}
|
||||
|
||||
watch(showBottomNav, (visible) => {
|
||||
if (!visible) bottomNavHidden.value = false;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(_path, oldPath) => {
|
||||
lastMainScrollTop = 0;
|
||||
bottomNavHidden.value = false;
|
||||
const el = mainRef.value;
|
||||
if (!el) return;
|
||||
if (oldPath) tabScrollTops.set(oldPath, el.scrollTop);
|
||||
@@ -84,10 +109,22 @@ watch(
|
||||
);
|
||||
|
||||
const balanceRefreshPaths = ['/profile', '/wallet', '/bets'];
|
||||
let mainScrollEl: HTMLElement | null = null;
|
||||
|
||||
watch(locale, (next, prev) => {
|
||||
if (prev && next !== prev) void loadPlayerHome(true);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (auth.user?.locale) void initFromUser(auth.user.locale);
|
||||
bindProfileVisibilityRefresh();
|
||||
mainScrollEl = mainRef.value;
|
||||
mainScrollEl?.addEventListener('scroll', onMainScroll, { passive: true });
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
mainScrollEl?.removeEventListener('scroll', onMainScroll);
|
||||
mainScrollEl = null;
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -125,7 +162,8 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<DesktopShell v-if="isDesktop" />
|
||||
<div v-else class="layout">
|
||||
<header v-if="showHeader" class="header">
|
||||
<svg class="logo" viewBox="0 0 130 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="TheBet365">
|
||||
<text x="0" y="18" font-family="'Inter','SF Pro Display',system-ui,sans-serif" font-size="11" font-weight="800" fill="#6B7280" letter-spacing="0.14em">THE</text>
|
||||
@@ -138,7 +176,7 @@ watch(
|
||||
class="support-btn"
|
||||
:aria-label="inboxEnabled && unreadCount ? t('messages.unread_badge', { count: unreadCount }) : t(hubOpenLabelKey)"
|
||||
>
|
||||
<svg class="support-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<svg class="hub-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M12 3C7.03 3 3 6.58 3 11c0 2.02.9 3.86 2.38 5.24L4 21l4.2-1.02A10.8 10.8 0 0 0 12 19c4.97 0 9-3.58 9-8s-4.03-8-9-8Z"
|
||||
fill="none"
|
||||
@@ -181,7 +219,12 @@ watch(
|
||||
</RouterView>
|
||||
</main>
|
||||
|
||||
<nav v-if="showBottomNav" class="bottom-nav" aria-label="Main">
|
||||
<nav
|
||||
v-if="showBottomNav"
|
||||
class="bottom-nav"
|
||||
:class="{ 'bottom-nav--hidden': bottomNavHidden }"
|
||||
aria-label="Main"
|
||||
>
|
||||
<RouterLink to="/" class="nav-item" :class="{ active: route.path === '/' }">
|
||||
<BottomNavIcon name="home" />
|
||||
<span class="nav-label">{{ t('nav.home') }}</span>
|
||||
@@ -208,7 +251,10 @@ watch(
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
<BackToTopButton :scroll-el="mainRef" :above-nav="showBottomNav" />
|
||||
<BackToTopButton
|
||||
:scroll-el="mainRef"
|
||||
:above-nav="showBottomNav && !bottomNavHidden"
|
||||
/>
|
||||
|
||||
<BetSlipDrawer v-model="slip.drawerOpen" />
|
||||
</div>
|
||||
@@ -216,23 +262,26 @@ watch(
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 100dvh;
|
||||
overflow: hidden;
|
||||
background: var(--bg-body);
|
||||
box-sizing: border-box;
|
||||
padding-top: var(--safe-top);
|
||||
}
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
height: 48px;
|
||||
padding: 8px 12px;
|
||||
min-height: var(--player-header-h, 48px);
|
||||
background: #FFFFFF;
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 110;
|
||||
z-index: 120;
|
||||
}
|
||||
.logo {
|
||||
height: 22px;
|
||||
@@ -290,7 +339,7 @@ watch(
|
||||
border-color: var(--border-active);
|
||||
}
|
||||
|
||||
.support-icon {
|
||||
.support-icon, .hub-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
@@ -335,6 +384,7 @@ watch(
|
||||
flex-shrink: 0;
|
||||
z-index: 105;
|
||||
background: #FFFFFF;
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -347,18 +397,32 @@ watch(
|
||||
padding: 10px 14px 0;
|
||||
}
|
||||
.main.has-nav {
|
||||
padding-bottom: 16px;
|
||||
padding-bottom: calc(var(--player-bottom-nav-h) + var(--safe-bottom));
|
||||
}
|
||||
.main.main--detail {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
background: #FFFFFF;
|
||||
box-sizing: border-box;
|
||||
min-height: calc(var(--player-bottom-nav-h) + var(--safe-bottom));
|
||||
padding-bottom: var(--safe-bottom);
|
||||
border-top: 1px solid var(--border);
|
||||
z-index: 100;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
transition: transform 0.28s ease, opacity 0.28s ease;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.bottom-nav--hidden {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
|
||||
Reference in New Issue
Block a user