chore: 去掉未上线前的旧兼容垫片,板块流收口到首页
鉴权只认 cookie、SEO 只走 Next、sort 与推荐对齐;删除 Bearer、Go sitemap、post_type 回填等冗余路径。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -270,7 +270,7 @@ export default function ModerationAdmin({
|
||||
<div key={p.id} className="panel p-4 sm:p-5">
|
||||
<div className="flex items-center gap-2 flex-wrap mb-2.5">
|
||||
<Link
|
||||
href={`/board/${p.board.id}`}
|
||||
href={`/?board=${p.board.id}`}
|
||||
className="chip"
|
||||
style={{ fontSize: 11.5 }}
|
||||
>
|
||||
@@ -354,7 +354,7 @@ export default function ModerationAdmin({
|
||||
{c.user.nickname}
|
||||
</span>
|
||||
<Link
|
||||
href={`/board/${c.board.id}`}
|
||||
href={`/?board=${c.board.id}`}
|
||||
className="chip"
|
||||
style={{ fontSize: 11 }}
|
||||
>
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { PenSquare, MessagesSquare, ChevronRight } from "lucide-react";
|
||||
import { fetchPosts, fetchBoards } from "@/lib/api";
|
||||
import { authCookieHeader } from "@/lib/cookies";
|
||||
import { getMeCached } from "@/lib/serverData";
|
||||
import Pagination from "@/components/Pagination";
|
||||
import PostRow from "@/components/PostRow";
|
||||
import PinnedPosts from "@/components/PinnedPosts";
|
||||
import FeedNewTip from "@/components/home/FeedNewTip";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
const { boards } = await fetchBoards();
|
||||
const board = boards.find((b) => b.id === Number(id));
|
||||
return {
|
||||
title: board ? `${board.name} · 姜十三论坛` : "板块 · 姜十三论坛",
|
||||
description: board?.description || "板块帖子列表",
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BoardPage({ params, searchParams }: PageProps) {
|
||||
const { id } = await params;
|
||||
const { page: pageStr } = await searchParams;
|
||||
const page = Math.max(1, parseInt(pageStr || "1", 10) || 1);
|
||||
const cookie = authCookieHeader(await cookies());
|
||||
const [postsData, boardsData, me] = await Promise.all([
|
||||
fetchPosts(page, 20, Number(id), "latest", "", false, cookie),
|
||||
fetchBoards(),
|
||||
getMeCached(cookie || undefined),
|
||||
]);
|
||||
const boardIndex = boardsData.boards.findIndex((b) => b.id === Number(id));
|
||||
const board = boardIndex >= 0 ? boardsData.boards[boardIndex] : null;
|
||||
const user = me.user;
|
||||
|
||||
// 第一页:置顶帖独立成区,列表只保留非置顶
|
||||
const pinned = page === 1 ? postsData.posts.filter((p) => p.pinned > 0) : [];
|
||||
const rows = page === 1 ? postsData.posts.filter((p) => p.pinned <= 0) : postsData.posts;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 面包屑 */}
|
||||
<nav className="meta mb-5 flex items-center gap-1.5 text-[13px]">
|
||||
<Link href="/" className="hover:text-[var(--accent)] transition-colors">首页</Link>
|
||||
<ChevronRight size={13} />
|
||||
<span style={{ color: "var(--accent)" }}>{board?.name || "未知板块"}</span>
|
||||
</nav>
|
||||
|
||||
<div className="panel p-6 sm:p-7 mb-6 relative overflow-hidden">
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute -top-16 -right-12 w-48 h-48 rounded-full"
|
||||
style={{ background: "var(--accent-soft)" }}
|
||||
/>
|
||||
<div className="relative flex items-end justify-between gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-4">
|
||||
<span
|
||||
className={`av-${Math.max(0, boardIndex) % 8} w-14 h-14 sm:w-16 sm:h-16 rounded-2xl flex items-center justify-center text-2xl font-extrabold shrink-0`}
|
||||
>
|
||||
{board?.name.slice(0, 1) || "?"}
|
||||
</span>
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-[28px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
|
||||
{board?.name || "未知板块"}
|
||||
</h1>
|
||||
{board?.description && (
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--ink-2)" }}>
|
||||
{board.description}
|
||||
</p>
|
||||
)}
|
||||
<p className="meta mt-1.5">{postsData.total} 篇讨论</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link href={`/compose?board=${id}`} className="btn btn-primary">
|
||||
<PenSquare size={15} /> 在此板块发帖
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr_290px] gap-6">
|
||||
<div>
|
||||
<PinnedPosts posts={pinned} showBoard={false} />
|
||||
{rows.length > 0 ? (
|
||||
<div className="panel overflow-hidden">
|
||||
<div className="px-4 sm:px-5 py-3.5 flex items-center justify-between" style={{ borderBottom: "1px solid var(--line)" }}>
|
||||
<span className="font-bold text-[15px]" style={{ color: "var(--ink)" }}>全部讨论</span>
|
||||
<span className="meta text-xs">第 {page} 页</span>
|
||||
</div>
|
||||
{user && (
|
||||
<div className="pt-2.5">
|
||||
<FeedNewTip
|
||||
sort="latest"
|
||||
boardId={Number(id)}
|
||||
page={page}
|
||||
userId={user.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{rows.map((post, i) => (
|
||||
<PostRow key={post.id} post={post} index={i} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
pinned.length === 0 && (
|
||||
<div className="panel text-center py-20 px-6">
|
||||
<span
|
||||
className="inline-flex w-14 h-14 rounded-full items-center justify-center mb-4"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
>
|
||||
<MessagesSquare size={26} />
|
||||
</span>
|
||||
<p className="font-bold text-lg" style={{ color: "var(--ink)" }}>该板块还没有帖子</p>
|
||||
<p className="meta mt-1.5">来发第一篇,抢占沙发</p>
|
||||
<Link href={`/compose?board=${id}`} className="btn btn-primary mt-6">
|
||||
<PenSquare size={15} /> 写新帖
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<Pagination total={postsData.total} page={page} size={20} basePath={`/board/${id}`} />
|
||||
</div>
|
||||
|
||||
<aside>
|
||||
<div className="panel p-5">
|
||||
<p className="font-bold text-[15px] mb-3.5" style={{ color: "var(--ink)" }}>其他板块</p>
|
||||
<div className="space-y-1">
|
||||
{boardsData.boards
|
||||
.map((b, i) => ({ b, i }))
|
||||
.filter(({ b }) => b.id !== Number(id))
|
||||
.map(({ b, i }) => (
|
||||
<Link
|
||||
key={b.id}
|
||||
href={`/board/${b.id}`}
|
||||
className="group flex items-center gap-3 py-1.5 px-2 -mx-2 rounded-xl transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]"
|
||||
>
|
||||
<span className={`av-${i % 8} w-8 h-8 rounded-xl flex items-center justify-center text-[13px] font-bold shrink-0`}>
|
||||
{b.name.slice(0, 1)}
|
||||
</span>
|
||||
<span className="text-sm font-medium flex-1 truncate group-hover:text-[var(--accent)] transition-colors" style={{ color: "var(--ink)" }}>
|
||||
{b.name}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import { ChevronRight, MessagesSquare, LayoutGrid } from "lucide-react";
|
||||
import { fetchBoards } from "@/lib/api";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "板块目录 · 姜十三论坛",
|
||||
description: "浏览论坛全部板块,找到感兴趣的话题社区",
|
||||
};
|
||||
|
||||
// 板块汇总列表页:首页左栏「板块目录」标题的落地页,
|
||||
// 展示全部板块(介绍 + 讨论量),点击进入板块独立页
|
||||
export default async function BoardsPage() {
|
||||
const { boards } = await fetchBoards();
|
||||
const totalPosts = boards.reduce((sum, b) => sum + (b.post_count || 0), 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 面包屑 */}
|
||||
<nav className="meta mb-5 flex items-center gap-1.5 text-[13px]">
|
||||
<Link href="/" className="hover:text-[var(--accent)] transition-colors">首页</Link>
|
||||
<ChevronRight size={13} />
|
||||
<span style={{ color: "var(--accent)" }}>板块目录</span>
|
||||
</nav>
|
||||
|
||||
{/* 页头 */}
|
||||
<div className="panel p-6 sm:p-7 mb-6 relative overflow-hidden">
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute -top-16 -right-12 w-48 h-48 rounded-full"
|
||||
style={{ background: "var(--accent-soft)" }}
|
||||
/>
|
||||
<div className="relative flex items-center gap-4">
|
||||
<span
|
||||
className="w-14 h-14 sm:w-16 sm:h-16 rounded-2xl flex items-center justify-center shrink-0"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
>
|
||||
<LayoutGrid size={28} />
|
||||
</span>
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-[28px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
|
||||
板块目录
|
||||
</h1>
|
||||
<p className="meta mt-1.5">
|
||||
共 {boards.length} 个板块 · {totalPosts} 篇讨论
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 板块卡片网格 */}
|
||||
{boards.length === 0 ? (
|
||||
<div className="panel text-center py-20 px-6">
|
||||
<span
|
||||
className="inline-flex w-14 h-14 rounded-full items-center justify-center mb-4"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
>
|
||||
<MessagesSquare size={26} />
|
||||
</span>
|
||||
<p className="font-bold text-lg" style={{ color: "var(--ink)" }}>还没有任何板块</p>
|
||||
<p className="meta mt-1.5">板块由管理员创建,敬请期待</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{boards.map((b, i) => (
|
||||
<Link
|
||||
key={b.id}
|
||||
href={`/board/${b.id}`}
|
||||
className="j13-module group flex items-start gap-3.5 transition-[border-color,background] duration-150 hover:border-[var(--nav-active-line)] hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]"
|
||||
aria-label={`${b.name}板块,${b.post_count} 篇讨论`}
|
||||
>
|
||||
<span className={`av-${i % 8} j13-bcard-ico !w-11 !h-11 !text-lg`} aria-hidden="true">
|
||||
{b.name.slice(0, 1)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center justify-between gap-2">
|
||||
<span className="truncate text-[15px] font-extrabold" style={{ color: "var(--ink)" }}>
|
||||
{b.name}
|
||||
</span>
|
||||
<span className="meta shrink-0 text-[11.5px] tabular-nums">{b.post_count} 篇</span>
|
||||
</span>
|
||||
{b.description?.trim() && (
|
||||
<span
|
||||
className="block mt-1 text-[12.5px] leading-relaxed line-clamp-2"
|
||||
style={{ color: "var(--ink-3)" }}
|
||||
>
|
||||
{b.description.trim()}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className="mt-2.5 inline-flex items-center gap-0.5 text-[12px] font-bold transition-colors group-hover:text-[var(--accent)]"
|
||||
style={{ color: "var(--ink-2)" }}
|
||||
>
|
||||
进入板块
|
||||
<ChevronRight
|
||||
size={12}
|
||||
className="transition-transform group-hover:translate-x-0.5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import ThemeSync from "@/components/ThemeSync";
|
||||
import RealtimeProvider from "@/components/RealtimeProvider";
|
||||
import type { User } from "@/lib/api";
|
||||
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
|
||||
import { TOKEN_COOKIE, REFRESH_COOKIE, authCookieHeader } from "@/lib/cookies";
|
||||
import { TOKEN_COOKIE, authCookieHeader } from "@/lib/cookies";
|
||||
import { accentStyleCSS } from "@/lib/theme";
|
||||
|
||||
// 构建期下载并自托管字体:同源加载 + 自动 preload + fallback 度量校正,
|
||||
@@ -59,11 +59,9 @@ export default async function RootLayout({
|
||||
// 浅色优先的设计系统,仅在显式选择暗色时挂 .dark
|
||||
const theme = cookieStore.get("j13-theme")?.value === "dark" ? "dark" : "light";
|
||||
|
||||
// SSR 直出登录态(access 过期时 middleware 已先行轮转,见 middleware.ts),
|
||||
// SSR 直出登录态(access 过期时 middleware 已先行轮转并注入请求头,见 middleware.ts),
|
||||
// 保证 F5 初始 HTML 中的右上角就是正确的用户区,而非先闪"登录/注册"。
|
||||
// 旧版 refresh cookie 可能是 Path=/api/auth(页面请求不带),因此只要
|
||||
// token 或 refresh 任一存在就向后端确认一次,避免半登录态(列表点赞态与
|
||||
// Header 游客态不一致),并兼容 access 15 分钟内的自然窗口。
|
||||
// 无 access cookie 即按游客渲染;客户端 Header 在 CSRF 可读时再静默校正一次。
|
||||
// 站点主题色(公开设置):派生浅/暗两套 accent CSS 变量后随首屏 HTML 内联,
|
||||
// 覆盖 globals.css 默认令牌——无主题闪动(FOUC),后端不可用时降级为默认靛蓝
|
||||
const { accent } = await getPublicSettingsCached();
|
||||
@@ -72,8 +70,7 @@ export default async function RootLayout({
|
||||
let user: User | null = null;
|
||||
let unread = 0;
|
||||
let chatUnread = 0;
|
||||
const hasAuthCookie =
|
||||
!!cookieStore.get(TOKEN_COOKIE)?.value || !!cookieStore.get(REFRESH_COOKIE)?.value;
|
||||
const hasAuthCookie = !!cookieStore.get(TOKEN_COOKIE)?.value;
|
||||
if (hasAuthCookie) {
|
||||
// 用户信息与未读数合并为一次请求;只转发最小化的 access cookie
|
||||
const me = await getMeCached(authCookieHeader(cookieStore) || undefined);
|
||||
|
||||
@@ -23,8 +23,8 @@ export default function NotFound() {
|
||||
<Link href="/" className="btn btn-primary">
|
||||
<Home size={15} /> 返回首页
|
||||
</Link>
|
||||
<Link href="/board/1" className="btn btn-line">
|
||||
逛逛板块
|
||||
<Link href="/" className="btn btn-line">
|
||||
逛逛讨论
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,9 @@ import { PenSquare, SearchX, MessagesSquare } from "lucide-react";
|
||||
import {
|
||||
fetchPosts,
|
||||
fetchOverview,
|
||||
fetchBoardSidebar,
|
||||
type OverviewResponse,
|
||||
type BoardSidebarResponse,
|
||||
} from "@/lib/api";
|
||||
import { authCookieHeader } from "@/lib/cookies";
|
||||
import { getMeCached } from "@/lib/serverData";
|
||||
@@ -15,6 +17,11 @@ import FeedTabs, { type FeedSort } from "@/components/home/FeedTabs";
|
||||
import FeedNewTip from "@/components/home/FeedNewTip";
|
||||
import { WorkspaceNav, WorkspaceNavMobile } from "@/components/home/WorkspaceNav";
|
||||
import { CommunityPanels, CommunityPanelsMobile } from "@/components/home/CommunityPanels";
|
||||
import {
|
||||
BoardPanels,
|
||||
BoardPanelsMobile,
|
||||
BoardMissingPanel,
|
||||
} from "@/components/home/BoardPanels";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "姜十三论坛 · 技术讨论社区",
|
||||
@@ -27,8 +34,6 @@ interface HomePageProps {
|
||||
|
||||
function normalizeSort(v?: string): FeedSort {
|
||||
if (v === "new" || v === "recommended") return v;
|
||||
// 兼容旧的 ?sort=hot 链接:落到最接近的推荐帖
|
||||
if (v === "hot") return "recommended";
|
||||
return "latest";
|
||||
}
|
||||
|
||||
@@ -81,22 +86,30 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
||||
page,
|
||||
20,
|
||||
isSearch || boardId <= 0 ? undefined : boardId,
|
||||
sort === "recommended" ? "hot" : sort,
|
||||
sort,
|
||||
keyword,
|
||||
sort === "recommended",
|
||||
cookie
|
||||
);
|
||||
// 聚合面板失败不阻塞帖子流
|
||||
const overviewPromise: Promise<OverviewResponse | null> = fetchOverview(cookie).catch(() => null);
|
||||
const boardSidebarPromise: Promise<BoardSidebarResponse | null> =
|
||||
boardId > 0 && !isSearch
|
||||
? fetchBoardSidebar(boardId, cookie).catch(() => null)
|
||||
: Promise.resolve(null);
|
||||
|
||||
const [me, postsData, overview] = await Promise.all([
|
||||
const [me, postsData, overview, boardSidebar] = await Promise.all([
|
||||
mePromise,
|
||||
postsPromise,
|
||||
overviewPromise,
|
||||
boardSidebarPromise,
|
||||
]);
|
||||
const user = me.user;
|
||||
const boards = overview?.boards ?? [];
|
||||
const activeBoard = boards.find((b) => b.id === boardId) || null;
|
||||
const activeBoard = boards.find((b) => b.id === boardId) || boardSidebar?.board || null;
|
||||
const showBoardPanels = boardId > 0 && !isSearch;
|
||||
// boardId 有效但接口失败 → 显示简卡;成功则板级右栏;否则全站右栏
|
||||
const boardPanelsMissing = showBoardPanels && !boardSidebar;
|
||||
|
||||
// 搜索态:单栏占满,无 Tabs/侧栏
|
||||
if (isSearch) {
|
||||
@@ -193,31 +206,61 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 右栏:社区面板 */}
|
||||
{overview && (
|
||||
<CommunityPanels
|
||||
stats={overview.stats}
|
||||
hot={overview.hot}
|
||||
activeUsers={overview.active_users}
|
||||
announcements={overview.announcements}
|
||||
newUsers={overview.new_users}
|
||||
checkin={overview.checkin ?? undefined}
|
||||
/>
|
||||
{/* 右栏:选中板块时切板级面板,否则全站社区面板 */}
|
||||
{showBoardPanels ? (
|
||||
boardSidebar ? (
|
||||
<BoardPanels
|
||||
board={boardSidebar.board}
|
||||
moderators={boardSidebar.moderators}
|
||||
stats={boardSidebar.stats}
|
||||
hot={boardSidebar.hot}
|
||||
activeUsers={boardSidebar.active_users}
|
||||
checkin={boardSidebar.checkin ?? undefined}
|
||||
/>
|
||||
) : boardPanelsMissing ? (
|
||||
<BoardMissingPanel />
|
||||
) : null
|
||||
) : (
|
||||
overview && (
|
||||
<CommunityPanels
|
||||
stats={overview.stats}
|
||||
hot={overview.hot}
|
||||
activeUsers={overview.active_users}
|
||||
announcements={overview.announcements}
|
||||
newUsers={overview.new_users}
|
||||
checkin={overview.checkin ?? undefined}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 窄屏:面板下沉到帖子流之后 */}
|
||||
{overview && (
|
||||
<div className="mt-6">
|
||||
<CommunityPanelsMobile
|
||||
stats={overview.stats}
|
||||
hot={overview.hot}
|
||||
activeUsers={overview.active_users}
|
||||
announcements={overview.announcements}
|
||||
newUsers={overview.new_users}
|
||||
checkin={overview.checkin ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
{showBoardPanels ? (
|
||||
boardSidebar ? (
|
||||
<div className="mt-6">
|
||||
<BoardPanelsMobile
|
||||
board={boardSidebar.board}
|
||||
moderators={boardSidebar.moderators}
|
||||
stats={boardSidebar.stats}
|
||||
hot={boardSidebar.hot}
|
||||
activeUsers={boardSidebar.active_users}
|
||||
checkin={boardSidebar.checkin ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
) : (
|
||||
overview && (
|
||||
<div className="mt-6">
|
||||
<CommunityPanelsMobile
|
||||
stats={overview.stats}
|
||||
hot={overview.hot}
|
||||
activeUsers={overview.active_users}
|
||||
announcements={overview.announcements}
|
||||
newUsers={overview.new_users}
|
||||
checkin={overview.checkin ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -121,7 +121,7 @@ function BoardCard({ board }: { board: Board }) {
|
||||
<p className="j13-module-title mb-3" id="board-card-title">
|
||||
<Layers size={15} style={{ color: "var(--accent)" }} /> 所属板块
|
||||
</p>
|
||||
<Link href={`/board/${board.id}`} className="flex items-center gap-3 group">
|
||||
<Link href={`/?board=${board.id}`} className="flex items-center gap-3 group">
|
||||
<span
|
||||
className={`av-${colorIdx} w-11 h-11 rounded-xl flex items-center justify-center text-lg font-extrabold shrink-0`}
|
||||
>
|
||||
@@ -140,10 +140,10 @@ function BoardCard({ board }: { board: Board }) {
|
||||
</p>
|
||||
)}
|
||||
<Link
|
||||
href={`/board/${board.id}`}
|
||||
href={`/?board=${board.id}`}
|
||||
className="btn btn-line btn-sm w-full justify-center mt-3.5"
|
||||
>
|
||||
进入板块 <ArrowRight size={13} />
|
||||
查看该板讨论 <ArrowRight size={13} />
|
||||
</Link>
|
||||
</section>
|
||||
);
|
||||
@@ -266,7 +266,7 @@ function RelatedPosts({
|
||||
))}
|
||||
</ol>
|
||||
<Link
|
||||
href={`/board/${boardId}`}
|
||||
href={`/?board=${boardId}`}
|
||||
className="j13-raillink justify-center mt-1.5 text-[12.5px]"
|
||||
style={{ color: "var(--accent)" }}
|
||||
>
|
||||
@@ -334,7 +334,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
fetchComments(id, commentPage, cookie),
|
||||
getMeCached(cookie || undefined),
|
||||
fetchUserProfile(String(post.user.id), 1, cookie).catch(() => null),
|
||||
fetchPosts(1, 8, post.board_id, "hot", "", false, cookie).catch(() => null),
|
||||
fetchPosts(1, 8, post.board_id, "recommended", "", false, cookie).catch(() => null),
|
||||
]);
|
||||
const comments = commentsData.comments;
|
||||
const related = (relatedData?.posts ?? []).filter((p) => p.id !== post.id).slice(0, 5);
|
||||
@@ -424,7 +424,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
<nav className="meta mb-5 flex items-center gap-1.5 flex-wrap text-[13px]">
|
||||
<Link href="/" className="hover:text-[var(--accent)] transition-colors">首页</Link>
|
||||
<ChevronRight size={13} />
|
||||
<Link href={`/board/${post.board.id}`} className="hover:text-[var(--accent)] transition-colors">
|
||||
<Link href={`/?board=${post.board.id}`} className="hover:text-[var(--accent)] transition-colors">
|
||||
{post.board.name}
|
||||
</Link>
|
||||
</nav>
|
||||
@@ -432,7 +432,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
<div className="panel p-6 sm:p-8">
|
||||
<header className="mb-7">
|
||||
<div className="flex items-center gap-2 mb-4 flex-wrap">
|
||||
<Link href={`/board/${post.board.id}`} className="chip">
|
||||
<Link href={`/?board=${post.board.id}`} className="chip">
|
||||
{post.board.name}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { fetchPosts, fetchBoards } from "@/lib/api";
|
||||
import { fetchPosts } from "@/lib/api";
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const baseUrl = "http://localhost:3000";
|
||||
@@ -8,16 +8,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
];
|
||||
|
||||
try {
|
||||
const { boards } = await fetchBoards();
|
||||
for (const board of boards) {
|
||||
entries.push({
|
||||
url: `${baseUrl}/board/${board.id}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
});
|
||||
}
|
||||
|
||||
const { posts } = await fetchPosts(1, 100);
|
||||
for (const post of posts) {
|
||||
entries.push({
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
ChevronDown,
|
||||
ShieldCheck,
|
||||
MessageSquare,
|
||||
LayoutGrid,
|
||||
} from "lucide-react";
|
||||
import { apiLogout, apiMe, apiChatUnreadSummary, type User } from "@/lib/api";
|
||||
import { hasAuthCookieHint } from "@/lib/cookies";
|
||||
@@ -87,8 +86,8 @@ export default function Header({
|
||||
});
|
||||
}), [router]);
|
||||
|
||||
// 挂载后静默校正一次(仅一次):覆盖 SSR 期间后端不可用、旧 Path refresh
|
||||
// 半登录等边缘情况。正常 F5 下结果与 SSR 一致,不产生可见闪动;
|
||||
// 挂载后静默校正一次(仅一次):覆盖 SSR 期间后端不可用等边缘情况。
|
||||
// 正常 F5 下结果与 SSR 一致,不产生可见闪动;
|
||||
// 无可读 CSRF cookie 说明必然是游客,跳过请求
|
||||
useEffect(() => {
|
||||
if (!hasAuthCookieHint()) return;
|
||||
@@ -238,13 +237,6 @@ export default function Header({
|
||||
<NavPill href="/" active={pathname === "/"} icon={<Home size={15} />}>
|
||||
首页
|
||||
</NavPill>
|
||||
<NavPill
|
||||
href="/boards"
|
||||
active={pathname === "/boards" || pathname?.startsWith("/board/") === true}
|
||||
icon={<LayoutGrid size={15} />}
|
||||
>
|
||||
板块
|
||||
</NavPill>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -378,12 +370,6 @@ export default function Header({
|
||||
</form>
|
||||
<div className="flex flex-col">
|
||||
<MobileLink href="/" active={pathname === "/"}>首页</MobileLink>
|
||||
<MobileLink
|
||||
href="/boards"
|
||||
active={pathname === "/boards" || pathname?.startsWith("/board/") === true}
|
||||
>
|
||||
板块
|
||||
</MobileLink>
|
||||
{user && <MobileLink href="/compose">发帖</MobileLink>}
|
||||
{user && (
|
||||
<MobileLink href="/chat" active={pathname?.startsWith("/chat") ?? false}>
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function PostRow({
|
||||
}`}
|
||||
>
|
||||
{boardHref && (
|
||||
<Link href={`/board/${post.board.id}`} className="j13-board-tag">
|
||||
<Link href={`/?board=${post.board.id}`} className="j13-board-tag">
|
||||
{post.board.name}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function ThemeSync({ initialAccent }: { initialAccent: string })
|
||||
|
||||
useEffect(() => {
|
||||
// 外观设置页存在"未保存的本地实时预览",该页面内暂停同步,
|
||||
// 避免轮询结果覆盖管理员正在预览的颜色;离开页面后 effect 重跑立即对齐
|
||||
// 避免 HTTP 校准覆盖管理员正在预览的颜色;离开页面后 effect 重跑立即对齐
|
||||
if (pathname?.startsWith("/admin/appearance")) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
307
frontend/components/home/BoardPanels.tsx
Normal file
307
frontend/components/home/BoardPanels.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Flame,
|
||||
Users,
|
||||
MessageCircle,
|
||||
Heart,
|
||||
Shield,
|
||||
LayoutGrid,
|
||||
PenLine,
|
||||
PenSquare,
|
||||
BarChart3,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import type {
|
||||
PostListItem,
|
||||
ActiveUser,
|
||||
BoardWithCount,
|
||||
BoardModerator,
|
||||
BoardSidebarStats,
|
||||
CheckinStatus,
|
||||
} from "@/lib/api";
|
||||
import Avatar from "@/components/Avatar";
|
||||
import { PulseCheckin } from "./CheckinCard";
|
||||
import { formatCompact } from "@/lib/format";
|
||||
|
||||
/* ---------- 1. 板块管理员 ---------- */
|
||||
|
||||
function Moderators({ items }: { items: BoardModerator[] }) {
|
||||
return (
|
||||
<section className="j13-module" aria-label="板块管理员">
|
||||
<p className="j13-module-title mb-2">
|
||||
<Shield size={15} style={{ color: "var(--accent)" }} /> 板块管理员
|
||||
</p>
|
||||
{items.length === 0 ? (
|
||||
<p className="meta px-0.5 py-1.5">暂无板块管理员</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{items.map((u) => {
|
||||
const name = u.nickname || u.username;
|
||||
return (
|
||||
<li key={u.id}>
|
||||
<Link href={`/u/${u.id}`} className="j13-raillink" data-tip={name}>
|
||||
<Avatar name={name} src={u.avatar || undefined} size={30} />
|
||||
<span className="min-w-0 flex-1 truncate text-[13px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
{name}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 2. 板块概况 ---------- */
|
||||
|
||||
function BoardIntro({ board }: { board: BoardWithCount }) {
|
||||
return (
|
||||
<section className="j13-module" aria-label="板块概况">
|
||||
<p className="j13-module-title mb-2">
|
||||
<LayoutGrid size={15} style={{ color: "var(--accent)" }} /> 板块概况
|
||||
</p>
|
||||
<p className="text-[15px] font-medium leading-snug" style={{ color: "var(--ink)" }}>
|
||||
{board.name}
|
||||
</p>
|
||||
{board.description ? (
|
||||
<p className="meta mt-1.5 text-[12.5px] leading-relaxed line-clamp-3">{board.description}</p>
|
||||
) : (
|
||||
<p className="meta mt-1.5">暂无板块介绍</p>
|
||||
)}
|
||||
<p className="meta mt-2 tabular-nums">讨论 {formatCompact(board.post_count)} 篇</p>
|
||||
<div className="mt-3">
|
||||
<Link href={`/compose?board=${board.id}`} className="btn btn-primary btn-sm w-full justify-center">
|
||||
<PenSquare size={14} /> 在此发帖
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 3. 板块数据 + 签到 ---------- */
|
||||
|
||||
function BoardPulse({
|
||||
stats,
|
||||
checkin,
|
||||
}: {
|
||||
stats: BoardSidebarStats;
|
||||
checkin?: CheckinStatus | null;
|
||||
}) {
|
||||
const cells = [
|
||||
{
|
||||
icon: <PenLine size={12.5} />,
|
||||
label: "话题总数",
|
||||
value: formatCompact(stats.post_count),
|
||||
foot: `+${formatCompact(stats.today_posts)} 今日`,
|
||||
up: true,
|
||||
},
|
||||
{
|
||||
icon: <TrendingUp size={12.5} />,
|
||||
label: "今日新帖",
|
||||
value: formatCompact(stats.today_posts),
|
||||
foot: "本日新增",
|
||||
up: stats.today_posts > 0,
|
||||
},
|
||||
{
|
||||
icon: <MessageCircle size={12.5} />,
|
||||
label: "回复总数",
|
||||
value: formatCompact(stats.comment_count),
|
||||
foot: `+${formatCompact(stats.today_comments)} 今日`,
|
||||
up: true,
|
||||
},
|
||||
{
|
||||
icon: <Heart size={12.5} />,
|
||||
label: "今日回复",
|
||||
value: formatCompact(stats.today_comments),
|
||||
foot: "本日新增",
|
||||
up: stats.today_comments > 0,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<section className="j13-module" aria-label="板块数据">
|
||||
<p className="j13-module-title">
|
||||
<BarChart3 size={15} style={{ color: "var(--accent)" }} /> 板块数据
|
||||
</p>
|
||||
<div className="j13-pulse">
|
||||
{cells.map((c) => (
|
||||
<div key={c.label} className="j13-pulse-cell">
|
||||
<p className="j13-pulse-lab">
|
||||
<span style={{ color: "var(--accent)" }} aria-hidden="true">
|
||||
{c.icon}
|
||||
</span>
|
||||
{c.label}
|
||||
</p>
|
||||
<p className="j13-pulse-num">{c.value}</p>
|
||||
<p className={`j13-pulse-foot${c.up ? " is-up" : ""}`}>
|
||||
{c.up && <TrendingUp size={10.5} aria-hidden="true" />}
|
||||
{c.foot}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="j13-pulse-checkin">
|
||||
<PulseCheckin initial={checkin} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 4. 本板热门 ---------- */
|
||||
|
||||
function BoardHot({ posts }: { posts: PostListItem[] }) {
|
||||
return (
|
||||
<section className="j13-module" aria-label="本板热门">
|
||||
<p className="j13-module-title mb-1.5">
|
||||
<Flame size={15} style={{ color: "var(--clay)" }} /> 本板热门
|
||||
</p>
|
||||
{posts.length === 0 ? (
|
||||
<p className="meta px-1.5 py-3">本板还没有热门讨论</p>
|
||||
) : (
|
||||
<ol className="j13-ranklist">
|
||||
{posts.map((p, i) => (
|
||||
<li key={p.id}>
|
||||
<Link href={`/post/${p.id}`} className="j13-rank group" data-tip={p.title}>
|
||||
<span className="j13-rank-no">{i + 1}</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span
|
||||
className="block truncate text-[13px] font-medium leading-snug transition-colors group-hover:text-[var(--accent)]"
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
{p.title}
|
||||
</span>
|
||||
<span className="mt-1 flex items-center gap-2.5 meta text-[11.5px]">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<MessageCircle size={11.5} /> {p.comment_count}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Heart size={11.5} /> {p.like_count}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 5. 本板活跃 ---------- */
|
||||
|
||||
function BoardActive({ users }: { users: ActiveUser[] }) {
|
||||
return (
|
||||
<section className="j13-module" aria-label="本板活跃成员">
|
||||
<p className="j13-module-title mb-2">
|
||||
<Users size={15} style={{ color: "var(--accent)" }} /> 本板活跃
|
||||
<span
|
||||
className="ml-auto font-normal normal-case tracking-normal text-[11px]"
|
||||
style={{ color: "var(--ink-3)" }}
|
||||
>
|
||||
本周
|
||||
</span>
|
||||
</p>
|
||||
{users.length === 0 ? (
|
||||
<p className="meta px-1.5 py-2">本周本板还没有人互动</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{users.map((u) => {
|
||||
const name = u.nickname || u.username;
|
||||
return (
|
||||
<li key={u.id}>
|
||||
<Link href={`/u/${u.id}`} className="j13-raillink" data-tip={name}>
|
||||
<Avatar name={name} src={u.avatar || undefined} size={30} />
|
||||
<span className="flex-1 min-w-0">
|
||||
<span
|
||||
className="block truncate text-[13px] font-medium leading-tight"
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
<span className="mt-px flex items-center gap-2.5 text-[11px] meta">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<PenLine size={10.5} /> 发帖 {u.post_count}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<MessageCircle size={10.5} /> 回复 {u.comment_count}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="shrink-0 text-[13px] tabular-nums"
|
||||
style={{ color: "var(--accent)" }}
|
||||
aria-label={`本周共 ${u.post_count + u.comment_count} 次互动`}
|
||||
>
|
||||
{u.post_count + u.comment_count}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 装配 ---------- */
|
||||
|
||||
type BoardPanelsProps = {
|
||||
board: BoardWithCount;
|
||||
moderators: BoardModerator[];
|
||||
stats: BoardSidebarStats;
|
||||
hot: PostListItem[];
|
||||
activeUsers: ActiveUser[];
|
||||
checkin?: CheckinStatus | null;
|
||||
};
|
||||
|
||||
function BoardPanelsBody(props: BoardPanelsProps) {
|
||||
return (
|
||||
<>
|
||||
<Moderators items={props.moderators} />
|
||||
<BoardIntro board={props.board} />
|
||||
<BoardPulse stats={props.stats} checkin={props.checkin} />
|
||||
<BoardHot posts={props.hot} />
|
||||
<BoardActive users={props.activeUsers} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function BoardPanels(props: BoardPanelsProps) {
|
||||
return (
|
||||
<aside className="hidden w-[280px] shrink-0 lg:block xl:w-[300px]" aria-label="板块面板">
|
||||
<div className="sticky top-24 space-y-4">
|
||||
<BoardPanelsBody {...props} />
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
export function BoardPanelsMobile(props: BoardPanelsProps) {
|
||||
return (
|
||||
<div className="space-y-4 lg:hidden" aria-label="板块面板">
|
||||
<BoardPanelsBody {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 板块不存在时的简卡 */
|
||||
export function BoardMissingPanel() {
|
||||
return (
|
||||
<aside className="hidden w-[280px] shrink-0 lg:block xl:w-[300px]" aria-label="板块面板">
|
||||
<div className="sticky top-24">
|
||||
<section className="j13-module">
|
||||
<p className="j13-module-title mb-2">
|
||||
<LayoutGrid size={15} style={{ color: "var(--ink-3)" }} /> 板块
|
||||
</p>
|
||||
<p className="meta">板块不存在或已删除</p>
|
||||
<Link href="/" className="btn btn-line btn-sm mt-3 w-full justify-center">
|
||||
返回全部讨论
|
||||
</Link>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { PenSquare, Home, ChevronRight } from "lucide-react";
|
||||
import { PenSquare, Home } from "lucide-react";
|
||||
import type { BoardWithCount, User } from "@/lib/api";
|
||||
|
||||
// 首页壳内板块切换:仅替换中栏帖子流,不离开工作台
|
||||
@@ -34,26 +34,14 @@ export function WorkspaceNav({ boards }: { boards: BoardWithCount[]; user?: User
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 板块目录:标题可点击进入板块汇总列表页(/boards);
|
||||
下方板块卡片点击不离开工作台,仅在中栏筛选该板块帖子流 */}
|
||||
{/* 板块目录:纯分区标题;下方卡片仅在中栏筛选该板帖子流 */}
|
||||
<div>
|
||||
<Link
|
||||
href="/boards"
|
||||
className="group px-1 mb-2 flex items-center justify-between text-[11.5px] font-medium tracking-[0.08em] uppercase text-[var(--ink-3)] hover:text-[var(--accent)] transition-colors"
|
||||
aria-label="进入板块目录列表页"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span>板块目录</span>
|
||||
<ChevronRight
|
||||
size={12}
|
||||
className="ml-0.5 opacity-0 -translate-x-0.5 group-hover:opacity-100 group-hover:translate-x-0 transition-all"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<div className="px-1 mb-2 flex items-center justify-between text-[11.5px] font-medium tracking-[0.08em] uppercase text-[var(--ink-3)]">
|
||||
<span>板块目录</span>
|
||||
{boards.length > 0 && (
|
||||
<span className="tracking-normal tabular-nums">{boards.length} 个</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{boards.map((b, i) => (
|
||||
<Link
|
||||
|
||||
@@ -134,6 +134,32 @@ export interface OverviewResponse {
|
||||
checkin?: CheckinStatus | null;
|
||||
}
|
||||
|
||||
/** 板块右栏:管理员公开资料 */
|
||||
export interface BoardModerator {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
/** 板块右栏:板级统计 */
|
||||
export interface BoardSidebarStats {
|
||||
post_count: number;
|
||||
today_posts: number;
|
||||
comment_count: number;
|
||||
today_comments: number;
|
||||
}
|
||||
|
||||
/** 首页选中板块后的右栏聚合 */
|
||||
export interface BoardSidebarResponse {
|
||||
board: BoardWithCount;
|
||||
moderators: BoardModerator[];
|
||||
stats: BoardSidebarStats;
|
||||
hot: PostListItem[];
|
||||
active_users: ActiveUser[];
|
||||
checkin?: CheckinStatus | null;
|
||||
}
|
||||
|
||||
export interface PostListItem {
|
||||
id: number;
|
||||
board_id: number;
|
||||
@@ -462,7 +488,7 @@ export async function fetchPosts(
|
||||
page = 1,
|
||||
size = 20,
|
||||
boardId?: number,
|
||||
sort = "latest",
|
||||
sort = "latest", // latest | recommended | new
|
||||
keyword = "",
|
||||
recommended = false,
|
||||
cookieHeader?: string
|
||||
@@ -483,6 +509,19 @@ export async function fetchOverview(cookieHeader?: string): Promise<OverviewResp
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** SSR 板块右栏;失败时抛错,由首页降级 */
|
||||
export async function fetchBoardSidebar(
|
||||
boardId: number,
|
||||
cookieHeader?: string
|
||||
): Promise<BoardSidebarResponse> {
|
||||
const res = await fetch(
|
||||
`${API_BASE}/api/boards/${boardId}/sidebar`,
|
||||
ssrInit(cookieHeaders(cookieHeader))
|
||||
);
|
||||
if (!res.ok) throw new Error("获取板块侧栏失败");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchBoards(): Promise<{ boards: BoardWithCount[] }> {
|
||||
const res = await fetch(`${API_BASE}/api/boards`, ssrInit());
|
||||
if (!res.ok) throw new Error("获取板块失败");
|
||||
@@ -1375,7 +1414,7 @@ export async function apiAdminUserAuditMessages(
|
||||
// ===== 站点外观设置 =====
|
||||
|
||||
export async function apiGetSettings(): Promise<PublicSettings> {
|
||||
// no-store:主题同步轮询必须读到管理员刚保存的最新配色,不走浏览器缓存
|
||||
// no-store:主题同步必须读到管理员刚保存的最新配色,不走浏览器缓存
|
||||
const res = await fetchWithRefresh("/api/settings", { method: "GET", cache: "no-store" });
|
||||
if (!res.ok) throw new Error("获取站点设置失败");
|
||||
return res.json();
|
||||
|
||||
@@ -23,7 +23,7 @@ export const BOUNTY_EXPIRE_DAYS = [3, 7, 14, 30] as const;
|
||||
export const LOTTERY_EXPIRE_DAYS = [1, 3, 7, 14] as const;
|
||||
|
||||
export function normalizePostType(t?: string): PostTypeValue {
|
||||
if (t === "normal" || !t) return "discussion";
|
||||
if (!t) return "discussion";
|
||||
if (POST_TYPES.some((x) => x.value === t)) return t as PostTypeValue;
|
||||
return "discussion";
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// - 在线集合:hello 帧给 staff 全量快照,之后 presence:update 增量维护。
|
||||
// - 群聊:进入群详情页调用 joinRoom(roomID) 订阅 chat:{id} 房间,
|
||||
// 重连后会自动重订当前已订阅集合;离开页面/退出群调用 leaveRoom。
|
||||
// - 轮询仅作兜底:回前台 / 进页时 HTTP 校准;实时靠 WS(通知、待审、主题、群聊未读)。
|
||||
// - HTTP 仅作回前台 / 进页校准;实时靠 WS(通知、待审、主题、群聊未读)。
|
||||
|
||||
export const RT_HELLO = "hello";
|
||||
export const RT_PONG = "pong";
|
||||
|
||||
Reference in New Issue
Block a user