Files
flvx/vite-frontend/src/layouts/h5.tsx
T
sagit 191aface2e fix(frontend): extend h5 tabbar safe-area coverage (#197)
Ensure the PWA bottom tab bar and tab hit areas fully cover the safe-area inset while keeping related frontend formatting updates consistent.
2026-02-23 07:45:14 +00:00

148 lines
5.3 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { Logo } from "@/components/icons";
import { siteConfig } from "@/config/site";
import { getAdminFlag } from "@/utils/session";
import { useScrollTopOnPathChange } from "@/hooks/useScrollTopOnPathChange";
interface TabItem {
path: string;
label: string;
icon: React.ReactNode;
adminOnly?: boolean;
}
export default function H5Layout({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
const location = useLocation();
const [isAdmin, setIsAdmin] = useState(false);
useScrollTopOnPathChange();
// Tabbar配置
const tabItems: TabItem[] = [
{
path: "/dashboard",
label: "首页",
icon: (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
</svg>
),
},
{
path: "/forward",
label: "转发",
icon: (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path
clipRule="evenodd"
d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
fillRule="evenodd"
/>
</svg>
),
},
{
path: "/tunnel",
label: "隧道",
icon: (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path
clipRule="evenodd"
d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z"
fillRule="evenodd"
/>
</svg>
),
adminOnly: true,
},
{
path: "/node",
label: "节点",
icon: (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path
clipRule="evenodd"
d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 0l-2 2a1 1 0 101.414 1.414L8 10.414l1.293 1.293a1 1 0 001.414 0l4-4z"
fillRule="evenodd"
/>
</svg>
),
adminOnly: true,
},
{
path: "/profile",
label: "我的",
icon: (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
</svg>
),
},
];
useEffect(() => {
setIsAdmin(getAdminFlag());
}, []);
// Tab点击处理
const handleTabClick = (path: string) => {
navigate(path);
};
// 过滤tab项(根据权限)
const filteredTabItems = tabItems.filter(
(item) => !item.adminOnly || isAdmin,
);
return (
<div className="flex flex-col min-h-screen bg-gray-100 dark:bg-black">
{/* 顶部导航栏 */}
<header className="bg-white dark:bg-black shadow-sm border-b border-gray-200 dark:border-gray-600 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
<div className="flex items-center gap-2">
<Logo size={20} />
<h1 className="text-sm font-bold text-foreground">
{siteConfig.name}
</h1>
</div>
<div className="flex items-center gap-2" />
</header>
{/* 主内容区域 */}
<main className="flex-1 bg-gray-100 dark:bg-black">{children}</main>
{/* 用于给固定 Tabbar 腾出空间的占位元素 */}
<div aria-hidden className="h-[calc(4rem+var(--safe-area-bottom))]" />
{/* 底部Tabbar */}
<nav className="bg-white dark:bg-black border-t border-gray-200 dark:border-gray-600 h-[calc(4rem+var(--safe-area-bottom))] flex-shrink-0 flex items-center justify-around px-2 fixed bottom-0 left-0 right-0 z-30">
{filteredTabItems.map((item) => {
const isActive = location.pathname === item.path;
return (
<button
key={item.path}
className={`
flex flex-col items-center justify-center flex-1 h-full pb-[var(--safe-area-bottom)]
transition-colors duration-200 min-h-[44px]
${
isActive
? "text-primary-600 dark:text-primary-400"
: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
}
`}
onClick={() => handleTabClick(item.path)}
>
<div className="flex-shrink-0 mb-1">{item.icon}</div>
<span className="text-xs font-medium">{item.label}</span>
</button>
);
})}
</nav>
</div>
);
}