107 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|