文件
hyb-oa/client/src/components/mobile/MobileBottomNav.tsx
T

114 行
4.4 KiB
TypeScript

import { NavLink } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { fetchWorkflowTasks } from "../../api/workflow";
import { useAuth } from "../../auth/AuthContext";
import { hasAnyPermission, hasBusinessApproverRole } from "../../auth/permissions";
import { MOBILE_NAV_ITEMS } from "../../features/mobile/navigation";
import { cn } from "../../lib/utils";
import { MobileIcon } from "./MobileIcon";
function resolveBadgeLabel(count: number) {
if (count > 99) {
return "99+";
}
return String(count);
}
export function MobileBottomNav() {
const { me, permissions } = useAuth();
const canAccessApprovals = hasBusinessApproverRole(me?.roleCodes, permissions);
const pendingCountQuery = useQuery({
enabled: canAccessApprovals,
queryKey: ["mobile", "workflow", "pending-count"],
queryFn: async () => {
const response = await fetchWorkflowTasks({ status: "pending", page: 1, pageSize: 1 });
return response.total;
},
refetchInterval: 30_000,
});
const pendingCount = pendingCountQuery.data ?? 0;
return (
<nav
className="mobile-bottom-nav-surface pointer-events-auto fixed inset-x-0 bottom-0 z-[70] transform-gpu border-t border-zinc-200 bg-white/95 shadow-[0_-6px_18px_rgba(15,23,42,0.08)] backdrop-blur md:hidden"
data-testid="mobile-bottom-nav"
>
<div className="relative z-[71] grid grid-cols-4 gap-1">
{MOBILE_NAV_ITEMS.map((item) => {
const disabled = item.anyOf
? !hasAnyPermission(permissions, [...item.anyOf])
: false;
const badge = item.key === "approvals" && pendingCount > 0 ? resolveBadgeLabel(pendingCount) : null;
const label = item.key === "approvals" && !canAccessApprovals ? "进度" : item.label;
if (disabled) {
return (
<button
key={item.key}
type="button"
data-testid={`mobile-nav-${item.key}`}
className="touch-target flex h-12 min-h-0 min-w-0 select-none flex-col items-center justify-center gap-0.5 rounded-xl px-0.5 py-1 text-[11px] font-medium leading-none text-zinc-400"
disabled
aria-disabled="true"
title={`${label}不可用`}
>
<MobileIcon name={item.icon} className="size-5" />
<span
className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
data-testid={`mobile-nav-${item.key}-label`}
>
{label}
</span>
</button>
);
}
return (
<NavLink
key={item.key}
end={item.to === "/mobile"}
to={item.to}
data-testid={`mobile-nav-${item.key}`}
className={({ isActive }) =>
cn(
"touch-target pointer-events-auto relative flex h-12 min-h-0 min-w-0 select-none flex-col items-center justify-center gap-0.5 rounded-xl px-0.5 py-1 text-[11px] font-medium leading-none transition-colors",
isActive
? "bg-primary/10 text-primary ring-1 ring-primary/15"
: "text-zinc-500 hover:bg-zinc-50 hover:text-zinc-900",
)
}
>
{({ isActive }) => (
<>
<span className="relative">
<MobileIcon name={item.icon} className="size-5" />
{badge ? (
<span
data-testid={`mobile-nav-${item.key}-badge`}
className={cn(
"pointer-events-none absolute left-[calc(100%+1px)] top-0 min-w-4 -translate-y-1/2 rounded-full px-1 text-center text-[10px] font-semibold leading-4 shadow-sm",
isActive ? "bg-red-500 text-white" : "bg-red-500 text-white",
)}
>
{badge}
</span>
) : null}
</span>
<span
className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
data-testid={`mobile-nav-${item.key}-label`}
>
{label}
</span>
</>
)}
</NavLink>
);
})}
</div>
</nav>
);
}