feat: Web端401队列拦截器 + 消息/任务/个人中心重写 + TabBar红点 + Admin页面完善

This commit is contained in:
2026-08-07 11:44:12 +08:00
parent b71c5a2d07
commit 721cfe1504
25 changed files with 1730 additions and 435 deletions

View File

@ -19,7 +19,7 @@ const MENU = [
title: "任务全景",
path: "/admin/tasks",
icon: GitBranch,
description: "序列号查任务树 · 裂变/返工可视化",
description: "身份证查任务树 · 裂变/返工可视化",
},
];

View File

@ -1,7 +1,10 @@
import { NavLink, Outlet } from "react-router-dom";
import { ScanLine, ClipboardList, Bell, User } from "lucide-react";
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 配置 */
/** 底部导航 Tab 配置 — 与 uni-app pages.json tabBar.list 完全一致 */
const TABS = [
{ path: "/scan", label: "扫码干活", icon: ScanLine },
{ path: "/tasks", label: "我的任务", icon: ClipboardList },
@ -10,38 +13,92 @@ const TABS = [
] as const;
/** 导航栏高度(供页面计算偏移量) */
export const TAB_BAR_HEIGHT = 64; // px(h-16)
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">
<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 z-50 w-full border-t border-gray-200 bg-white pb-safe"
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 }) => (
<NavLink
key={path}
to={path}
className={({ isActive }) =>
`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"
}`
}
>
<Icon size={22} strokeWidth={2} />
<span className="text-[10px] font-medium leading-none">{label}</span>
</NavLink>
))}
{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>