Files
track/frontend/src/components/layout/AppLayout.tsx

107 lines
3.9 KiB
TypeScript

import { useEffect, useState } from "react";
import { NavLink, Outlet, useNavigate } from "react-router-dom";
import { ScanLine, ClipboardList, Bell, User, ArrowLeft } from "lucide-react";
import { useAuth } from "../../contexts/AuthContext";
import { getNotifications } from "../../services/notificationApi";
/** 底部导航 Tab 配置 — 与 uni-app pages.json tabBar.list 完全一致 */
const TABS = [
{ path: "/scan", label: "扫码干活", icon: ScanLine },
{ path: "/tasks", label: "我的任务", icon: ClipboardList },
{ path: "/notifications", label: "消息", icon: Bell },
{ path: "/profile", label: "我的", icon: User },
] as const;
/** 导航栏高度(供页面计算偏移量) */
export const TAB_BAR_HEIGHT = 64;
const TOP_BAR_HEIGHT = 48;
export default function AppLayout() {
const { user } = useAuth();
const navigate = useNavigate();
const [unreadCount, setUnreadCount] = useState(0);
// 🔔 未读消息数轮询
useEffect(() => {
const userId = user?.username || user?.id || "";
if (!userId) return;
function poll() {
getNotifications(userId, 0, 1)
.then((res) => setUnreadCount(res.unread_count || 0))
.catch(() => {}); // 静默
}
poll(); // 立即请求一次
const id = setInterval(poll, 30_000); // 每 30 秒轮询
return () => clearInterval(id);
}, [user]);
return (
<div className="flex h-dvh flex-col bg-gray-50">
{/* ======== 顶部导航栏 — 返回管理端 ======== */}
<header
className="fixed top-0 left-0 right-0 z-50 flex items-center border-b border-gray-200 bg-white px-4 shadow-sm"
style={{ height: TOP_BAR_HEIGHT }}
>
<button
onClick={() => navigate("/admin/dashboard")}
className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-blue-600 transition-colors hover:bg-blue-50 active:bg-blue-100"
>
<ArrowLeft size={16} strokeWidth={2.5} />
</button>
<span className="ml-auto text-xs text-gray-400"> · </span>
</header>
{/* ======== 主内容区 ======== */}
<main
className="flex-1 overflow-y-auto"
style={{
paddingTop: TOP_BAR_HEIGHT,
paddingBottom: TAB_BAR_HEIGHT,
}}
>
<Outlet />
</main>
{/* ======== 底部导航栏 — 与 uni-app tabBar 完全一致 ======== */}
<nav
className="fixed bottom-0 left-0 right-0 z-50 border-t border-gray-200 bg-white pb-safe"
style={{ height: TAB_BAR_HEIGHT }}
>
<div className="mx-auto flex h-full max-w-lg items-center justify-around">
{TABS.map(({ path, label, icon: Icon }) => {
const isNotifyTab = path === "/notifications";
return (
<NavLink
key={path}
to={path}
className={({ isActive }) =>
`relative flex flex-col items-center gap-0.5 px-3 py-1 transition-colors ${
isActive
? "text-blue-600"
: "text-gray-400 hover:text-gray-600"
}`
}
>
<span className="relative">
<Icon size={22} strokeWidth={2} />
{/* 🔴 消息 Tab 未读红点 */}
{isNotifyTab && unreadCount > 0 && (
<span className="absolute -top-1 -right-2 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-red-500 px-1 text-[9px] font-bold text-white shadow-[0_0_0_2px_white]">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
</span>
<span className="text-[10px] font-medium leading-none">{label}</span>
</NavLink>
);
})}
</div>
</nav>
</div>
);
}