"use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { Fragment, useEffect, useState } from "react"; import { Home, BookOpen, PenSquare, MessageSquare, User as UserIcon, } from "lucide-react"; import { apiChatUnreadSummary, type User } from "@/lib/api"; import { useAllowMessages } from "@/components/MessagesPolicyProvider"; import { useSiteBrand } from "@/components/SiteBrandProvider"; import { onDocumentVisible } from "@/lib/visibilityReconcile"; import { userPath, libraryPath } from "@/lib/urlStyle"; import { realtime, RT_CHAT_UNREAD } from "@/lib/realtime"; import type { RtChatUnreadData } from "@/lib/realtime"; import type { MobileTabKey } from "@/lib/mobileTabs"; /** * 移动端全局底部公共菜单(lg 以下显示): * 按 SiteBrand.mobile_tabs 配置顺序渲染(消息在未登录/未开放时不显示)。 * SiteChrome 只在启用 tab 的根路径渲染本组件, * 避免与聊天 / 发帖工作台的锁视口布局冲突。 */ export default function MobileTabBar({ user, initialChatUnread = 0, }: { user: User | null; initialChatUnread?: number; }) { const pathname = usePathname(); const allowMessages = useAllowMessages(); const { url_style: urlStyle, mobile_tabs: mobileTabs } = useSiteBrand(); const libHref = libraryPath(urlStyle); const [chatUnread, setChatUnread] = useState(initialChatUnread); // 登录态经 router.refresh() 下发新 props 时校正徽标 useEffect(() => { setChatUnread(initialChatUnread); }, [initialChatUnread]); // 实时未读:与 Header 同源(chat:unread 对全体成员推送;在当前会话页则忽略) useEffect(() => { if (!user || !allowMessages) return; const off = realtime.on(RT_CHAT_UNREAD, (data) => { if (!data || typeof data.room_id !== "number") return; if (pathname === `/chat/${data.room_id}`) return; const delta = typeof data.delta === "number" ? data.delta : 1; setChatUnread((c) => Math.max(0, c + delta)); }); return off; }, [user, pathname, allowMessages]); // 会话内已读 / 回前台:合并对账(短时间多次同步只打一次 summary) useEffect(() => { if (!user || !allowMessages) return; let debounceTimer: ReturnType | null = null; const pullSummary = () => { apiChatUnreadSummary() .then((res) => setChatUnread(res.total || 0)) .catch(() => {}); }; const onSync = () => { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(pullSummary, 250); }; window.addEventListener("j13-chat-unread-sync", onSync); const offVisible = onDocumentVisible(pullSummary); return () => { if (debounceTimer) clearTimeout(debounceTimer); window.removeEventListener("j13-chat-unread-sync", onSync); offVisible(); }; }, [user, allowMessages]); const showMessages = !!user && allowMessages; const isActive = (match: (p: string) => boolean) => !!pathname && match(pathname); // 配置已由 SiteBrandProvider normalize(白名单内子集,空回落全量);数组顺序即展示顺序 const tabNodes: Record = { home: ( } label="首页" active={isActive((p) => p === "/")} /> ), library: ( } label="书库" active={isActive((p) => p === libHref)} /> ), compose: ( } label="发帖" prominent active={false} /> ), messages: showMessages ? ( } label="消息" active={isActive((p) => p === "/chat")} badge={chatUnread > 0 ? (chatUnread > 99 ? "99+" : chatUnread) : 0} /> ) : null, me: ( } label="我的" active={!!user && isActive((p) => p === userPath(urlStyle, user.id))} /> ), }; return ( ); } function TabLink({ href, icon, label, active, prominent = false, badge = 0, }: { href: string; icon: React.ReactNode; label: string; active: boolean; prominent?: boolean; badge?: number | string; }) { const ink = active ? "var(--accent)" : "var(--ink-2)"; return ( {prominent ? ( ) : ( )} {label} ); }