feat: 增加友链申请、独立页面、投票/悬赏/抽奖帖与侧栏签到,并统一开发数据目录

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-27 06:23:54 +08:00
parent df19752a1e
commit 2208af7070
80 changed files with 9620 additions and 641 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate, useOutletContext, useSearchParams, useLocation } from 'react-router-dom';
import { useNavigate, useOutletContext, useSearchParams, useLocation, useParams } from 'react-router-dom';
import { notify } from '@/lib/notify';
import { api } from '../api/client';
import type { PostItem } from '../api/types';
@@ -20,17 +20,35 @@ import {
import { openForumPost } from '../utils/openPost';
import { joinSEOKeywords, usePageSEO } from '../hooks/usePageSEO';
import { siteMetaDescription, useSiteBranding } from '../hooks/useSiteBranding';
import { boardPath, canonicalRedirectPath, parsePermalinkID } from '../utils/permalink';
import NotFoundPage from './NotFoundPage';
/** 仅从 URL 解析板块 ID不以 layout 状态回退,避免回首页误用上一板块) */
function boardIdFromLocation(routeId: string | undefined, searchParams: URLSearchParams): number {
if (routeId) {
const id = parsePermalinkID(routeId);
return Number.isFinite(id) && id > 0 ? id : 0;
}
const q = Number(searchParams.get('board')) || 0;
return q > 0 ? q : 0;
}
export default function HomePage() {
const nav = useNavigate();
const location = useLocation();
const { id: boardRouteId } = useParams();
const [params] = useSearchParams();
const ctx = useOutletContext<LayoutCtx>();
const { branding } = useSiteBranding();
const { limits, loading: limitsLoading } = useForumLimits();
const pageSize = Math.max(1, limits.page_size_default);
const boardId = Number(params.get('board')) || ctx?.boardId || 0;
const boardId = boardIdFromLocation(boardRouteId, params);
const queryBoardId = Number(params.get('board')) || 0;
const isBoardRoute = !!boardRouteId;
const isInvalidBoardRoute = isBoardRoute && boardId === 0;
const boardsLoading = ctx?.boardsLoading ?? true;
const isMissingBoard = isBoardRoute && boardId > 0 && !boardsLoading && !(ctx?.boards ?? []).some(b => b.id === boardId);
const keyword = params.get('keyword') || '';
const tag = params.get('tag') || '';
const author = params.get('author') || '';
@@ -51,11 +69,30 @@ export default function HomePage() {
canonicalPath: tag
? `/?tag=${encodeURIComponent(tag)}`
: boardId
? `/?board=${boardId}`
? boardPath(boardId, limits)
: '/',
ogType: 'website',
});
// 旧版 /?board=id 重定向到规范板块路径
useEffect(() => {
if (queryBoardId && !boardRouteId) {
const p = new URLSearchParams(params);
p.delete('board');
const qs = p.toString();
const target = boardPath(queryBoardId, limits) + (qs ? `?${qs}` : '');
nav(target, { replace: true });
return;
}
if (boardRouteId && boardId > 0) {
const redirect = canonicalRedirectPath('board', boardId, location.pathname, limits);
if (redirect) {
const qs = location.search;
nav(redirect + qs, { replace: true });
}
}
}, [queryBoardId, boardId, boardRouteId, params, nav, limits, location.pathname, location.search]);
const [posts, setPosts] = useState<PostItem[]>([]);
const [postTotal, setPostTotal] = useState(0);
const [page, setPage] = useState(1);
@@ -137,7 +174,7 @@ export default function HomePage() {
// 等限制就绪后再拉列表;筛选变化时重载
useEffect(() => {
if (limitsLoading) return;
if (limitsLoading || isInvalidBoardRoute || isMissingBoard) return;
const forceRefresh = (location.state as FeedNavState | null)?.refreshFeed;
if (forceRefresh) {
@@ -174,6 +211,8 @@ export default function HomePage() {
location.state,
loadFirst,
beginFeedRefresh,
isInvalidBoardRoute,
isMissingBoard,
]);
// 筛选未变时同步列表快照;变筛选的那一帧先保留旧快照供 cleanup 写入
@@ -230,13 +269,22 @@ export default function HomePage() {
loadFirst();
return;
}
navigateFeed(nav, buildHomeUrl(boardId, next, { keyword, tag, author, titleOnly }));
navigateFeed(nav, buildHomeUrl(boardId, next, { keyword, tag, author, titleOnly, permalink: limits }));
};
const showSortBar = !keyword && !tag && !author;
if (isInvalidBoardRoute || isMissingBoard) {
return (
<NotFoundPage
title="板块不存在"
description="该板块不存在,或已被删除。"
/>
);
}
// 首屏用同构骨架,避免标题/列表分区先后出现造成闪动
if ((loading || limitsLoading) && posts.length === 0) {
if ((loading || limitsLoading || (isBoardRoute && boardsLoading)) && posts.length === 0) {
return <FeedPageSkeleton />;
}