import { useMemo, useRef } from 'react'; import { boardPath, type PermalinkOpts } from '../utils/permalink'; import { getCachedForumLimits, useForumLimits } from '../hooks/useForumLimits'; import { Clock, MessageCircle, BadgeCheck, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { moveTabIndex } from '../hooks/useOverlayA11y'; import { enabledFeedSortTabs, getDefaultFeedSort, normalizeFeedSortTabs, } from '../utils/feedSortTabs'; import type { FeedSortTab } from '../api/types'; export type FeedSort = 'latest' | 'reply' | 'hot'; const SORT_META: Record = { reply: { hint: '最近有人评论', icon: MessageCircle }, latest: { hint: '按发帖时间', icon: Clock }, hot: { hint: '仅展示推荐帖', icon: BadgeCheck }, }; interface Props { value: FeedSort; onChange: (sort: FeedSort) => void; postTotal?: number; /** 正在加载、尚未提交到画面的目标排序 */ pendingValue?: FeedSort | null; } /** 当前配置下的默认排序(读缓存,供非 hook 路径) */ export function getDefaultFeedSortFromCache(): FeedSort { return getDefaultFeedSort(getCachedForumLimits().feed_sort_tabs); } /** 解析 URL sort;未启用或不合法时回落默认 */ export function parseFeedSort(raw: string | null, tabs?: FeedSortTab[] | null): FeedSort { const list = tabs ?? getCachedForumLimits().feed_sort_tabs; const def = getDefaultFeedSort(list); if (raw === 'latest' || raw === 'hot' || raw === 'reply') { const enabled = enabledFeedSortTabs(list).some(t => t.id === raw); if (enabled) return raw; } return def; } export function buildHomeUrl( boardId: number, sort?: FeedSort, opts?: { keyword?: string; tag?: string; author?: string; titleOnly?: boolean; permalink?: PermalinkOpts }, ) { const def = getDefaultFeedSortFromCache(); const effective = sort ?? def; const p = new URLSearchParams(); const tag = opts?.tag?.trim(); const keyword = opts?.keyword?.trim(); const author = opts?.author?.trim(); // 标签筛选与关键词搜索互斥:有 tag 时不带 keyword if (tag) p.set('tag', tag); else if (keyword) { p.set('keyword', keyword); if (opts?.titleOnly) p.set('title_only', '1'); if (author) p.set('author', author); } else if (author) { p.set('author', author); } // 默认排序不写进 URL if (effective !== def) p.set('sort', effective); const qs = p.toString(); if (boardId) { const base = boardPath(boardId, opts?.permalink ?? getCachedForumLimits()); return qs ? `${base}?${qs}` : base; } return qs ? `/?${qs}` : '/'; } export function feedSortLabel(sort: FeedSort, tabs?: FeedSortTab[] | null): string { const list = normalizeFeedSortTabs(tabs ?? getCachedForumLimits().feed_sort_tabs); return list.find(t => t.id === sort)?.label ?? '帖子列表'; } export default function FeedSortBar({ value, onChange, postTotal, pendingValue, }: Props) { const { limits } = useForumLimits(); const options = useMemo( () => enabledFeedSortTabs(limits.feed_sort_tabs).map(t => ({ key: t.id as FeedSort, label: t.label, hint: SORT_META[t.id]?.hint ?? '', icon: SORT_META[t.id]?.icon ?? Clock, })), [limits.feed_sort_tabs], ); const listRef = useRef(null); const activeIndex = Math.max(0, options.findIndex(o => o.key === value)); const onKeyDown = (e: React.KeyboardEvent) => { const next = moveTabIndex(e.key, activeIndex, options.length); if (next == null) return; e.preventDefault(); onChange(options[next].key); requestAnimationFrame(() => { const tabs = listRef.current?.querySelectorAll('[role="tab"]'); tabs?.[next]?.focus(); }); }; if (options.length === 0) return null; return (
{options.map(({ key, label, hint, icon: Icon }, i) => { const pending = pendingValue === key; return ( ); })}
{postTotal != null && ( 共 {postTotal} 条 )}
); }