"use client"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { Bookmark, Home, LayoutGrid, Menu, ShieldCheck } from "lucide-react"; import type { BoardWithCount, User } from "@/lib/api"; import { isStaff } from "@/lib/roles"; import BoardIcon, { boardColor } from "@/components/BoardIcon"; import { useUrlStyle } from "@/components/SiteBrandProvider"; import { userPath } from "@/lib/urlStyle"; import { RailDrawerPanel, useDrawerChrome } from "@/components/MobileRailDrawers"; // 首页壳内板块切换:仅替换中栏帖子流,不离开工作台 function boardHref(id: number) { return `/?board=${id}`; } // 板块目录裸面板:只有 j13-module 本体,不含外层 nav / 宽度 / sticky 类, // 供侧栏注册表按管理端配置在任意侧栏渲染。 export function WorkspaceNavPanel({ boards, user, }: { boards: BoardWithCount[]; user?: User | null; }) { const params = useSearchParams(); const urlStyle = useUrlStyle(); const activeBoard = Number(params.get("board")) || 0; const showAdmin = !!user && isStaff(user.role); return ( 浏览 全部讨论 {user ? ( 我的收藏 ) : null} {boards.length > 0 ? ( <> 板块 {boards.map((b) => ( {b.name} {b.post_count} 篇 ))} > ) : null} {showAdmin ? ( 管理面板 ) : null} ); } // 桌面左栏:整块模块,与右栏 j13-module 对齐 export function WorkspaceNav({ boards, user }: { boards: BoardWithCount[]; user?: User | null }) { return ( // sticky 必须挂在 flex 子项上:父级 items-start 会把内层缩成内容高,失去粘滞行程 ); } // 移动端:横向滑动板块条(两端渐隐指示「可左右滑动」);发帖入口由底部公共菜单承载。 // 板块条两端嵌左右抽屉入口(☰ 导航 / ▦ 面板),与书籍阅读页的左右工具栏同构: // 左抽屉 = 桌面左栏(板块目录 + 工作台),右抽屉 = 桌面右栏面板;内容为空则对应入口不渲染 export function WorkspaceNavMobile({ boards, left, right, }: { boards: BoardWithCount[]; /** 左抽屉内容(桌面左栏,服务端渲染节点) */ left?: ReactNode; /** 右抽屉内容(桌面右栏,服务端渲染节点) */ right?: ReactNode; }) { const params = useSearchParams(); const activeBoard = Number(params.get("board")) || 0; const scrollRef = useRef(null); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); const [drawer, setDrawer] = useState<"left" | "right" | null>(null); useDrawerChrome(drawer !== null, () => setDrawer(null)); const updateScrollState = useCallback(() => { const el = scrollRef.current; if (!el) return; const max = el.scrollWidth - el.clientWidth; setCanScrollLeft(el.scrollLeft > 4); setCanScrollRight(max > 4 && el.scrollLeft < max - 4); }, []); useEffect(() => { updateScrollState(); const el = scrollRef.current; if (!el || typeof ResizeObserver === "undefined") return; const ro = new ResizeObserver(updateScrollState); ro.observe(el); return () => ro.disconnect(); }, [boards, updateScrollState]); // 进页 / 切板块时把激活 chip 滚到可视区中央 useEffect(() => { const el = scrollRef.current; if (!el) return; el.querySelector('[aria-current="page"]')?.scrollIntoView({ block: "nearest", inline: "center", }); }, [activeBoard, boards]); const triggerBtn = "relative inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full border transition-colors hover:bg-[var(--panel-2)] active:scale-95 " + // 视觉 28px、触控热区扩到 44px(HIG 最小触控目标),仅扩大命中范围不改变外观 "before:absolute before:-inset-2 before:rounded-full before:content-['']"; return ( {left ? ( setDrawer("left")} aria-haspopup="dialog" aria-label="打开导航抽屉" > ) : null} 全部 {boards.map((b) => ( {b.name} ))} {/* 渐隐滑动指示:遮罩与背景同色,pointer-events-none 不挡触控 */} {right ? ( setDrawer("right")} aria-haspopup="dialog" aria-label="打开面板抽屉" > ) : null} {/* 左右抽屉:内容由服务端渲染后传入;lg 起隐藏(桌面用常驻侧栏) */} {left ? ( setDrawer(null)}> {left} ) : null} {right ? ( setDrawer(null)}> {right} ) : null} ); }
浏览
板块