新增 OIDC/SSO、邮件验证码与 Gitea 项目同步,并强化 Feed 与管理后台。

作为 OIDC Provider 对接 Gitea;注册支持邮件验证码/验证码;侧栏同步公开仓库;Feed 分页、文章大纲、标签云与站点品牌设置。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-31 16:58:22 +08:00
parent 9487c8ab02
commit 822eef96be
83 changed files with 8578 additions and 1222 deletions

View File

@@ -1,14 +1,17 @@
import {
Home, Star, LayoutDashboard,
Home, Star, LayoutDashboard, FolderGit2, ArrowLeft,
} from 'lucide-react';
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import type { Board } from '../api/types';
import type { PostHeading } from '../utils/postHeadings';
import { useAuth } from '../hooks/useAuth';
import { cn } from '@/lib/utils';
import { Skeleton } from '@/components/ui/skeleton';
import { buildHomeUrl, parseFeedSort } from './FeedSortBar';
import { navigateFeed } from '../utils/feedCache';
import BoardIconDisplay from './BoardIconDisplay';
import { getBoardThemeIndex } from '../utils/boardTheme';
import ArticleOutline from './ArticleOutline';
// 内容页不参与左侧栏高亮(非 feed 浏览上下文)
const NEUTRAL_SIDEBAR_PREFIXES = ['/post/', '/profile'];
@@ -20,6 +23,7 @@ export function isNeutralSidebarRoute(pathname: string): boolean {
function resolveMenuKey(pathname: string, activeBoard: number): string | null {
if (isNeutralSidebarRoute(pathname)) return null;
if (pathname.startsWith('/favorites')) return 'favorites';
if (pathname.startsWith('/projects')) return 'projects';
if (pathname.startsWith('/admin')) return 'admin';
return activeBoard === 0 ? 'all' : String(activeBoard);
}
@@ -28,9 +32,25 @@ interface Props {
boards: Board[];
activeBoard: number;
onSelectBoard: (id: number) => void;
/** 板块列表首次拉取中 */
boardsLoading?: boolean;
/** 帖子详情:左侧切换为文章目录 */
outlineMode?: boolean;
outlineHeadings?: PostHeading[];
outlineScrollRoot?: HTMLElement | null;
outlineTitle?: string;
}
export default function Sidebar({ boards, activeBoard, onSelectBoard }: Props) {
export default function Sidebar({
boards,
activeBoard,
onSelectBoard,
boardsLoading = false,
outlineMode = false,
outlineHeadings = [],
outlineScrollRoot = null,
outlineTitle,
}: Props) {
const nav = useNavigate();
const loc = useLocation();
const [params] = useSearchParams();
@@ -52,15 +72,48 @@ export default function Sidebar({ boards, activeBoard, onSelectBoard }: Props) {
</button>
);
if (outlineMode) {
return (
<aside className="sidebar sidebar--outline">
<button
type="button"
className="sidebar-nav-item sidebar-outline-back"
onClick={() => navigateFeed(nav, '/')}
>
<ArrowLeft aria-hidden />
<span className="flex-1 truncate"></span>
</button>
<ArticleOutline
headings={outlineHeadings}
scrollRoot={outlineScrollRoot}
title={outlineTitle || '文章目录'}
/>
</aside>
);
}
return (
<aside className="sidebar">
<div className="sidebar-section"></div>
<nav className="sidebar-nav">
{navItem('all', '全部帖子', <Home aria-hidden />, () => { onSelectBoard(0); navigateFeed(nav, buildHomeUrl(0, sort)); })}
{user && navItem('favorites', '我的收藏', <Star aria-hidden />, () => nav('/favorites'))}
{navItem('projects', '开源码桶', <FolderGit2 aria-hidden />, () => nav('/projects'))}
</nav>
{boards.length > 0 && (
{(boardsLoading && boards.length === 0) ? (
<>
<div className="sidebar-section sidebar-section--boards"></div>
<nav className="sidebar-nav sidebar-nav--skeleton" aria-busy="true" aria-label="板块加载中">
{Array.from({ length: 4 }, (_, i) => (
<div key={i} className="sidebar-nav-item sidebar-nav-item--skeleton">
<Skeleton className="skeleton--sidebar-icon" />
<Skeleton className="skeleton--sidebar-label" style={{ width: `${58 + (i % 3) * 12}%` }} />
</div>
))}
</nav>
</>
) : boards.length > 0 ? (
<>
<div className="sidebar-section sidebar-section--boards"></div>
<nav className="sidebar-nav">
@@ -92,7 +145,7 @@ export default function Sidebar({ boards, activeBoard, onSelectBoard }: Props) {
})}
</nav>
</>
)}
) : null}
{isAdmin && (
<>