feat: 移动端底部导航可配置与工作区导航、置顶后台等前端调整
This commit is contained in:
@@ -6,6 +6,7 @@ import { Fragment, useEffect, useState } from "react";
|
||||
import {
|
||||
Home,
|
||||
BookOpen,
|
||||
CodeXml,
|
||||
PenSquare,
|
||||
MessageSquare,
|
||||
User as UserIcon,
|
||||
@@ -14,7 +15,7 @@ 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 { userPath, libraryPath, snippetsPath } from "@/lib/urlStyle";
|
||||
import { realtime, RT_CHAT_UNREAD } from "@/lib/realtime";
|
||||
import type { RtChatUnreadData } from "@/lib/realtime";
|
||||
import type { MobileTabKey } from "@/lib/mobileTabs";
|
||||
@@ -36,6 +37,7 @@ export default function MobileTabBar({
|
||||
const allowMessages = useAllowMessages();
|
||||
const { url_style: urlStyle, mobile_tabs: mobileTabs } = useSiteBrand();
|
||||
const libHref = libraryPath(urlStyle);
|
||||
const snipHref = snippetsPath(urlStyle);
|
||||
const [chatUnread, setChatUnread] = useState(initialChatUnread);
|
||||
|
||||
// 登录态经 router.refresh() 下发新 props 时校正徽标
|
||||
@@ -100,6 +102,16 @@ export default function MobileTabBar({
|
||||
active={isActive((p) => p === libHref)}
|
||||
/>
|
||||
),
|
||||
snippets: (
|
||||
<TabLink
|
||||
href={snipHref}
|
||||
icon={<CodeXml size={20} />}
|
||||
label="片段"
|
||||
active={isActive(
|
||||
(p) => p === snipHref || p.startsWith("/snippets") || p.startsWith("/snippet-"),
|
||||
)}
|
||||
/>
|
||||
),
|
||||
compose: (
|
||||
<TabLink
|
||||
href={user ? "/compose" : "/login"}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Footer from "@/components/Footer";
|
||||
import BackToTop from "@/components/BackToTop";
|
||||
import MobileTabBar from "@/components/MobileTabBar";
|
||||
import type { User } from "@/lib/api";
|
||||
import { libraryPath, userPath } from "@/lib/urlStyle";
|
||||
import { libraryPath, snippetsPath, userPath } from "@/lib/urlStyle";
|
||||
import { useSiteBrand } from "@/components/SiteBrandProvider";
|
||||
|
||||
function pathIsAdmin(pathname: string | null) {
|
||||
@@ -88,6 +88,7 @@ export default function SiteChrome({
|
||||
const tabBarRoots: string[] = [];
|
||||
if (enabledTabs.has("home")) tabBarRoots.push("/");
|
||||
if (enabledTabs.has("library")) tabBarRoots.push(libraryPath(brand.url_style));
|
||||
if (enabledTabs.has("snippets")) tabBarRoots.push(snippetsPath(brand.url_style));
|
||||
if (enabledTabs.has("messages")) tabBarRoots.push("/chat");
|
||||
if (enabledTabs.has("me") && meHref) tabBarRoots.push(meHref);
|
||||
const showTabBar =
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
GitFork,
|
||||
Plus,
|
||||
Lock,
|
||||
Loader2,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
RotateCcw,
|
||||
@@ -454,24 +453,19 @@ export default function SnippetExplorer({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 结果统计 */}
|
||||
{/* 结果统计:纯文本行,切换范围时数据原地更新,无任何加载指示 */}
|
||||
<div className="mt-3 flex items-center gap-2 text-[12px]" style={{ color: "var(--ink-3)" }}>
|
||||
{loading ? (
|
||||
<Loader2 size={12} className="animate-spin" />
|
||||
) : (
|
||||
<span className="tabular-nums" style={{ fontFamily: MONO }}>
|
||||
<span style={{ color: "var(--accent)" }}>$</span>{" "}
|
||||
{scope === "mine" ? "ls ~/snippets --count" : "ls --count"}
|
||||
</span>
|
||||
)}
|
||||
<span className="tabular-nums" style={{ fontFamily: MONO }}>
|
||||
<span style={{ color: "var(--accent)" }}>$</span>{" "}
|
||||
{scope === "mine" ? "ls ~/snippets --count" : "ls --count"}
|
||||
</span>
|
||||
<span>
|
||||
共 <b className="tabular-nums" style={{ color: "var(--ink-2)" }}>{total}</b> 个片段
|
||||
{loading ? ",加载中…" : ""}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 列表 / 空态 */}
|
||||
{items.length === 0 && loading ? null : items.length === 0 && !loading ? (
|
||||
{/* 列表 / 空态:请求期间保留现有内容,新数据返回后原地替换,不闪空态 */}
|
||||
{items.length === 0 ? (
|
||||
filtering ? (
|
||||
<div
|
||||
className="mt-4 rounded-xl px-6 py-12 text-center"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { Bookmark, Home, LayoutGrid, Menu, ShieldCheck } from "lucide-react";
|
||||
import { Bookmark, CodeXml, Home, LayoutGrid, Menu, ShieldCheck, Trophy } 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 { leaderboardPath, snippetsPath, userPath } from "@/lib/urlStyle";
|
||||
import { RailDrawerPanel, useDrawerChrome } from "@/components/MobileRailDrawers";
|
||||
|
||||
// 首页壳内板块切换:仅替换中栏帖子流,不离开工作台
|
||||
@@ -26,9 +26,16 @@ export function WorkspaceNavPanel({
|
||||
user?: User | null;
|
||||
}) {
|
||||
const params = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const urlStyle = useUrlStyle();
|
||||
const activeBoard = Number(params.get("board")) || 0;
|
||||
const showAdmin = !!user && isStaff(user.role);
|
||||
const snipHref = snippetsPath(urlStyle);
|
||||
const lbHref = leaderboardPath(urlStyle);
|
||||
// 与 Header 高亮规则一致:覆盖标准路径与伪静态(/snippet-*.html、/rank.html 等)
|
||||
const snippetsActive =
|
||||
!!pathname && (pathname.startsWith("/snippets") || pathname.startsWith("/snippet-"));
|
||||
const leaderboardActive = !!pathname && (pathname === lbHref || pathname.startsWith(`${lbHref}/`));
|
||||
|
||||
return (
|
||||
<section className="j13-module flex max-h-[var(--rail-max-h)] flex-col overflow-hidden" aria-label="板块目录">
|
||||
@@ -49,6 +56,37 @@ export function WorkspaceNavPanel({
|
||||
<span className="j13-bcard-name flex-1 truncate">全部讨论</span>
|
||||
</Link>
|
||||
|
||||
{/* 不进底栏的全局入口:仅移动端展示(首页 ☰ 导航抽屉是其在移动端的唯一路径),
|
||||
桌面端由顶部导航承担,左栏不重复 */}
|
||||
<Link
|
||||
href={snipHref}
|
||||
className="j13-bcard lg:hidden"
|
||||
aria-current={snippetsActive ? "page" : undefined}
|
||||
>
|
||||
<span
|
||||
className="j13-bcard-ico"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<CodeXml size={15} />
|
||||
</span>
|
||||
<span className="j13-bcard-name flex-1 truncate">代码片段</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={lbHref}
|
||||
className="j13-bcard lg:hidden"
|
||||
aria-current={leaderboardActive ? "page" : undefined}
|
||||
>
|
||||
<span
|
||||
className="j13-bcard-ico"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Trophy size={15} />
|
||||
</span>
|
||||
<span className="j13-bcard-name flex-1 truncate">排行榜</span>
|
||||
</Link>
|
||||
|
||||
{user ? (
|
||||
<Link href={`${userPath(urlStyle, user.id)}?tab=favorites`} className="j13-bcard">
|
||||
<span
|
||||
|
||||
Reference in New Issue
Block a user