fix(player+admin): 移动端 PWA 安全区、防缩放与局域网调试
玩家端适配主屏幕 safe-area、底部导航滚动隐藏与全屏布局;双端禁止页面缩放并开放 Vite host 供手机联调。
This commit is contained in:
@@ -2,8 +2,12 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="TheBet365" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="apple-touch-icon" href="/logo.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
@@ -53,7 +53,7 @@ const { visible, scrollToTop } = useBackToTop(toRef(props, 'scrollEl'));
|
||||
}
|
||||
|
||||
.back-top-btn.above-nav {
|
||||
bottom: calc(68px + env(safe-area-inset-bottom, 0px));
|
||||
bottom: calc(var(--player-bottom-nav-h, 54px) + var(--safe-bottom, env(safe-area-inset-bottom, 0px)) + 12px);
|
||||
}
|
||||
|
||||
.back-top-btn:active {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,6 +11,11 @@ import {
|
||||
type PlayerLocale,
|
||||
} from './i18n/index.ts';
|
||||
|
||||
/** iOS Safari:阻止双指捏合缩放,配合 viewport 保持页面比例稳定 */
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('gesturestart', (event) => event.preventDefault());
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const initialLocale = readStoredLocale();
|
||||
const initialMessages = await loadLocaleMessages(initialLocale);
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
#5C4A12 100%
|
||||
);
|
||||
--gradient-card: linear-gradient(160deg, #1E1A12 0%, #141414 40%, #0A0A0A 100%);
|
||||
--player-header-h: 52px;
|
||||
--player-bottom-nav-h: 54px;
|
||||
--safe-top: env(safe-area-inset-top, 0px);
|
||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
||||
/** 金色描边按钮(避免大面积实心填充) */
|
||||
@@ -188,9 +192,19 @@ html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
min-height: -webkit-fill-available;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
/* 保留 bg 位图;用 scroll 替代 fixed,减轻移动端滚动重绘(后续可换 WebP/AVIF) */
|
||||
background-color: var(--tertiary);
|
||||
@@ -207,6 +221,11 @@ body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100%;
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
|
||||
button {
|
||||
@@ -223,7 +242,7 @@ input {
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export default defineConfig({
|
||||
},
|
||||
publicDir: resolve(__dirname, '../../packages/shared/public'),
|
||||
server: {
|
||||
host: true,
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': { target: 'http://localhost:3000', changeOrigin: true },
|
||||
|
||||
Reference in New Issue
Block a user