feat: 站点媒体库与移动端底部导航,服务测试补强
- 新增媒体库:media_library/media_thumbs 服务(WebP 缩略图)、管理端 media 页面 - 移动端底部导航 MobileTabBar 替换 MobilePostBar - 旧数据导入增强与测试、路由与登录会话/板块侧边栏测试补强 - site-doc 组件精简(移除 Breadcrumb),文档新增迁移公告说明
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { PenSquare, Home, LayoutGrid, ShieldCheck } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Bookmark, Home, ShieldCheck } from "lucide-react";
|
||||
import type { BoardWithCount, User } from "@/lib/api";
|
||||
import { isStaff } from "@/lib/roles";
|
||||
import BoardIcon, { boardColor } from "@/components/BoardIcon";
|
||||
@@ -27,17 +28,8 @@ export function WorkspaceNavPanel({
|
||||
|
||||
return (
|
||||
<section className="j13-module flex max-h-[var(--rail-max-h)] flex-col overflow-hidden" aria-label="板块目录">
|
||||
<p className="j13-module-title shrink-0">
|
||||
<LayoutGrid size={15} style={{ color: "var(--accent)" }} aria-hidden />
|
||||
板块目录
|
||||
{boards.length > 0 ? (
|
||||
<span className="ml-auto meta text-[11.5px] font-medium tracking-normal tabular-nums">
|
||||
{boards.length} 个
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
|
||||
<div className="j13-rail-scroll j13-board-list mt-2.5 min-h-0 flex-1">
|
||||
<div className="j13-rail-scroll j13-board-list min-h-0 flex-1">
|
||||
<p className="j13-rail-group">浏览</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="j13-bcard"
|
||||
@@ -53,25 +45,41 @@ export function WorkspaceNavPanel({
|
||||
<span className="j13-bcard-name flex-1 truncate">全部讨论</span>
|
||||
</Link>
|
||||
|
||||
{boards.length > 0 ? <div className="j13-board-split" role="separator" /> : null}
|
||||
|
||||
{boards.map((b) => (
|
||||
<Link
|
||||
key={b.id}
|
||||
href={boardHref(b.id)}
|
||||
className="j13-bcard"
|
||||
aria-current={activeBoard === b.id ? "page" : undefined}
|
||||
aria-label={`${b.name}板块,${b.post_count} 篇讨论`}
|
||||
>
|
||||
<span className={`av-${boardColor(b)} j13-bcard-ico`} aria-hidden="true">
|
||||
<BoardIcon icon={b.icon} name={b.name} size={15} />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 flex items-center justify-between gap-2">
|
||||
<span className="j13-bcard-name truncate">{b.name}</span>
|
||||
<span className="meta shrink-0 text-[11px] tabular-nums">{b.post_count} 篇</span>
|
||||
{user ? (
|
||||
<Link href={`/u/${user.id}?tab=favorites`} className="j13-bcard">
|
||||
<span
|
||||
className="j13-bcard-ico"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Bookmark size={15} />
|
||||
</span>
|
||||
<span className="j13-bcard-name flex-1 truncate">我的收藏</span>
|
||||
</Link>
|
||||
))}
|
||||
) : null}
|
||||
|
||||
{boards.length > 0 ? (
|
||||
<>
|
||||
<p className="j13-rail-group">板块</p>
|
||||
{boards.map((b) => (
|
||||
<Link
|
||||
key={b.id}
|
||||
href={boardHref(b.id)}
|
||||
className="j13-bcard"
|
||||
aria-current={activeBoard === b.id ? "page" : undefined}
|
||||
aria-label={`${b.name}板块,${b.post_count} 篇讨论`}
|
||||
>
|
||||
<span className={`av-${boardColor(b)} j13-bcard-ico`} aria-hidden="true">
|
||||
<BoardIcon icon={b.icon} name={b.name} size={15} />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 flex items-center justify-between gap-2">
|
||||
<span className="j13-bcard-name truncate">{b.name}</span>
|
||||
<span className="meta shrink-0 text-[11px] tabular-nums">{b.post_count} 篇</span>
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{showAdmin ? (
|
||||
@@ -105,46 +113,77 @@ export function WorkspaceNav({ boards, user }: { boards: BoardWithCount[]; user?
|
||||
);
|
||||
}
|
||||
|
||||
// 移动端:发帖入口 + 横向滑动板块条
|
||||
export function WorkspaceNavMobile({
|
||||
boards,
|
||||
user,
|
||||
}: {
|
||||
boards: BoardWithCount[];
|
||||
user: User | null;
|
||||
}) {
|
||||
// 移动端:横向滑动板块条(两端渐隐指示「可左右滑动」);发帖入口由底部公共菜单承载
|
||||
export function WorkspaceNavMobile({ boards }: { boards: BoardWithCount[] }) {
|
||||
const params = useSearchParams();
|
||||
const activeBoard = Number(params.get("board")) || 0;
|
||||
const composePath =
|
||||
activeBoard > 0 ? `/compose?board=${activeBoard}` : "/compose";
|
||||
const composeHref = user
|
||||
? composePath
|
||||
: `/login?redirect=${encodeURIComponent(composePath)}`;
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||
const [canScrollRight, setCanScrollRight] = useState(false);
|
||||
|
||||
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<HTMLElement>('[aria-current="page"]')?.scrollIntoView({
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
});
|
||||
}, [activeBoard, boards]);
|
||||
|
||||
return (
|
||||
<div className="lg:hidden -mx-4 sm:-mx-6 px-4 sm:px-6">
|
||||
<div className="j13-chips">
|
||||
<Link href={composeHref} className="btn btn-primary btn-sm shrink-0">
|
||||
<PenSquare size={14} /> 发帖
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="j13-chip"
|
||||
aria-current={activeBoard === 0 ? "page" : undefined}
|
||||
<div className="lg:hidden -mx-4 sm:-mx-6 px-4 sm:px-6 bg-[var(--main-bg)] border-b border-[var(--line)] py-2.5">
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="j13-chips"
|
||||
onScroll={updateScrollState}
|
||||
>
|
||||
<Home size={13} /> 全部
|
||||
</Link>
|
||||
{boards.map((b) => (
|
||||
<Link
|
||||
key={b.id}
|
||||
href={boardHref(b.id)}
|
||||
href="/"
|
||||
className="j13-chip"
|
||||
aria-current={activeBoard === b.id ? "page" : undefined}
|
||||
aria-current={activeBoard === 0 ? "page" : undefined}
|
||||
>
|
||||
<BoardIcon icon={b.icon} name={b.name} size={13} />
|
||||
{b.name}
|
||||
<Home size={13} /> 全部
|
||||
</Link>
|
||||
))}
|
||||
{boards.map((b) => (
|
||||
<Link
|
||||
key={b.id}
|
||||
href={boardHref(b.id)}
|
||||
className="j13-chip"
|
||||
aria-current={activeBoard === b.id ? "page" : undefined}
|
||||
>
|
||||
<BoardIcon icon={b.icon} name={b.name} size={13} />
|
||||
{b.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{/* 渐隐滑动指示:遮罩与背景同色,pointer-events-none 不挡触控 */}
|
||||
<div
|
||||
className={`j13-chip-fade j13-chip-fade-left ${canScrollLeft ? "is-on" : ""}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
className={`j13-chip-fade j13-chip-fade-right ${canScrollRight ? "is-on" : ""}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user