fix(player+admin): 移动端 PWA 安全区、防缩放与局域网调试

玩家端适配主屏幕 safe-area、底部导航滚动隐藏与全屏布局;双端禁止页面缩放并开放 Vite host 供手机联调。
This commit is contained in:
2026-06-22 17:58:51 +08:00
parent b9257cc50a
commit fa06fee64c
9 changed files with 123 additions and 15 deletions

View File

@@ -10,7 +10,7 @@ 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';
const BetSlipDrawer = defineAsyncComponent(() => import('../components/BetSlipDrawer.vue'));
import { usePlayerHome } from '../composables/usePlayerHome';
@@ -68,14 +68,36 @@ const { startPolling, stopPolling } = useDepositNotifications();
const { unreadCount, refreshUnreadCount, resetMessagesState } = usePlayerMessages();
const mainRef = ref<HTMLElement | null>(null);
const tabScrollTops = new Map<string, number>();
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 +106,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(
@@ -176,7 +210,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>
@@ -203,7 +242,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>
@@ -211,18 +253,23 @@ watch(
<style scoped>
.layout {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
height: 100%;
min-height: 100dvh;
overflow: hidden;
background: transparent;
box-sizing: border-box;
padding-top: var(--safe-top);
}
.header {
flex-shrink: 0;
position: relative;
display: flex; justify-content: space-between; align-items: center;
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
min-height: var(--player-header-h);
background: rgba(17, 17, 17, 0.94);
border-bottom: 1px solid var(--border);
z-index: 120;
@@ -321,15 +368,30 @@ watch(
}
.main.has-nav {
padding-bottom: 16px;
padding-bottom: calc(var(--player-bottom-nav-h) + var(--safe-bottom));
}
.bottom-nav {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
box-sizing: border-box;
min-height: calc(var(--player-bottom-nav-h) + var(--safe-bottom));
padding-bottom: var(--safe-bottom);
background: rgba(17, 17, 17, 0.96);
border-top: 1px solid var(--border);
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.35);
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;
@@ -338,7 +400,7 @@ watch(
align-items: center;
justify-content: center;
gap: 3px;
padding: 6px 2px 8px;
padding: 6px 2px 4px;
font-size: 9px;
color: var(--text-muted);
font-weight: 600;