feat: 去掉 Feed 顶栏统计并强化开源码桶
按帖子列表风格展示码桶,补齐语言/Star/Fork 与论坛作者关联回填,避免未绑定仓库与空列表。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,80 +1,41 @@
|
||||
import { Users, FileText, LayoutGrid } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { Board, ForumStats } from '../api/types';
|
||||
import { navigateFeed } from '../utils/feedCache';
|
||||
|
||||
interface Props {
|
||||
boardId: number;
|
||||
keyword: string;
|
||||
tag?: string;
|
||||
author?: string;
|
||||
titleOnly?: boolean;
|
||||
boards: Board[];
|
||||
stats: ForumStats | null;
|
||||
postTotal: number;
|
||||
/** 搜索页用 h1;首页/板块页中间栏不再展示标题 */
|
||||
titleAs?: 'h1' | 'h2';
|
||||
}
|
||||
|
||||
export default function FeedHeader({
|
||||
boardId,
|
||||
keyword,
|
||||
tag = '',
|
||||
author = '',
|
||||
boards,
|
||||
stats,
|
||||
postTotal,
|
||||
titleAs = 'h1',
|
||||
}: Props) {
|
||||
const nav = useNavigate();
|
||||
const board = boards.find(b => b.id === boardId);
|
||||
|
||||
const isSearch = !!(keyword || author);
|
||||
const isTag = !!tag;
|
||||
const filtered = isSearch || isTag;
|
||||
const inBoard = !filtered && boardId > 0 && !!board;
|
||||
const TitleTag = titleAs;
|
||||
|
||||
let title = '';
|
||||
if (isTag) title = `标签:${tag}`;
|
||||
else if (isSearch) title = '搜索结果';
|
||||
|
||||
// 无标题且无操作时不占位
|
||||
if (!filtered) return null;
|
||||
|
||||
return (
|
||||
<div className={`feed-head${filtered ? ' feed-head--solo' : ' feed-head--stats-only'}`}>
|
||||
<div className="feed-head feed-head--solo">
|
||||
<div className="feed-head__title">
|
||||
{title ? <TitleTag>{title}</TitleTag> : null}
|
||||
{!filtered && inBoard && (
|
||||
<div className="feed-head__stats">
|
||||
<span className="feed-stat-chip">
|
||||
<FileText aria-hidden />
|
||||
本板块 <strong>{postTotal}</strong> 帖
|
||||
</span>
|
||||
{stats && (
|
||||
<span className="feed-stat-chip feed-stat-chip--muted" title="全站统计">
|
||||
全站 {stats.posts} 帖 · {stats.users} 会员
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!filtered && !inBoard && stats && (
|
||||
<div className="feed-head__stats">
|
||||
<span className="feed-stat-chip">
|
||||
<Users aria-hidden />
|
||||
会员 <strong>{stats.users}</strong>
|
||||
</span>
|
||||
<span className="feed-stat-chip">
|
||||
<FileText aria-hidden />
|
||||
帖子 <strong>{stats.posts}</strong>
|
||||
</span>
|
||||
<span className="feed-stat-chip">
|
||||
<LayoutGrid aria-hidden />
|
||||
板块 <strong>{stats.boards}</strong>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{filtered && (
|
||||
<span className="feed-head__meta feed-head__meta--count">共 {postTotal} 条</span>
|
||||
)}
|
||||
<span className="feed-head__meta feed-head__meta--count">共 {postTotal} 条</span>
|
||||
</div>
|
||||
{isTag && (
|
||||
<button
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/ui/skeleton';
|
||||
import PostListSkeleton from './PostListSkeleton';
|
||||
import { useForumLimits } from '../hooks/useForumLimits';
|
||||
|
||||
/** 首页 Feed 初始骨架(标题区 + 排序栏 + 列表) */
|
||||
/** 首页 Feed 初始骨架(排序栏 + 列表) */
|
||||
export default function FeedPageSkeleton() {
|
||||
const { limits } = useForumLimits();
|
||||
|
||||
@@ -11,15 +11,6 @@ export default function FeedPageSkeleton() {
|
||||
<div className="feed-panel">
|
||||
<div className="feed-top">
|
||||
<div className="feed-top__bar">
|
||||
<div className="feed-head feed-head--stats-only">
|
||||
<div className="feed-head__title">
|
||||
<div className="feed-head__stats">
|
||||
<Skeleton className="skeleton--stat-chip" />
|
||||
<Skeleton className="skeleton--stat-chip" />
|
||||
<Skeleton className="skeleton--stat-chip" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="feed-toolbar feed-toolbar--skeleton" aria-hidden>
|
||||
<Skeleton className="skeleton--sort-tab" />
|
||||
<Skeleton className="skeleton--sort-tab" />
|
||||
|
||||
121
frontend/src/components/ProjectListItem.tsx
Normal file
121
frontend/src/components/ProjectListItem.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { memo } from 'react';
|
||||
import { GitFork, Star } from 'lucide-react';
|
||||
import UserBadges from './UserBadges';
|
||||
import UserLink from './UserLink';
|
||||
import type { GiteaProject } from '../api/types';
|
||||
import { formatTime } from '../utils/content';
|
||||
|
||||
interface Props {
|
||||
project: GiteaProject;
|
||||
}
|
||||
|
||||
/** 开源码桶列表行:结构对齐帖子列表;作者一律用论坛用户,不展示 Gitea login */
|
||||
function ProjectListItem({ project }: Props) {
|
||||
const owner = project.owner;
|
||||
// 未绑定论坛用户的仓库不应出现在列表;兜底不渲染
|
||||
if (!owner?.id) return null;
|
||||
|
||||
const title = project.name || project.full_name;
|
||||
const initial = owner.nickname?.[0] || '?';
|
||||
const remoteIso = project.updated_at_remote ?? undefined;
|
||||
const timeLabel = remoteIso ? formatTime(remoteIso) : '';
|
||||
const stars = project.stars_count ?? 0;
|
||||
const forks = project.forks_count ?? 0;
|
||||
|
||||
const open = () => {
|
||||
if (!project.html_url) return;
|
||||
window.open(project.html_url, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
open();
|
||||
}
|
||||
};
|
||||
|
||||
const onTitleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
open();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="post-row post-row--v2 project-list-item"
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
onClick={open}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<UserLink
|
||||
user={owner}
|
||||
showAvatar={false}
|
||||
showName={false}
|
||||
stopPropagation
|
||||
className="post-avatar user-link--avatar-only"
|
||||
>
|
||||
{owner.avatar
|
||||
? <img src={owner.avatar} alt="" loading="lazy" decoding="async" />
|
||||
: initial}
|
||||
</UserLink>
|
||||
|
||||
<div className="post-main">
|
||||
<div className="post-text">
|
||||
<div className="post-title-row">
|
||||
{project.language ? (
|
||||
<span className="project-lang-badge" title="主要语言">{project.language}</span>
|
||||
) : null}
|
||||
<a
|
||||
href={project.html_url}
|
||||
className="post-title"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={onTitleClick}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
</div>
|
||||
{project.description ? (
|
||||
<p className="post-excerpt">{project.description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="post-meta">
|
||||
<div className="post-meta-left">
|
||||
<UserLink
|
||||
user={owner}
|
||||
stopPropagation
|
||||
className="post-meta-author"
|
||||
showBadges={false}
|
||||
/>
|
||||
<UserBadges user={owner} compact maxAchievement={3} className="project-list-badges" />
|
||||
{timeLabel ? (
|
||||
<>
|
||||
<span className="post-meta-sep post-meta-sep--before-time" aria-hidden>·</span>
|
||||
<span className="post-meta-time post-meta-time--created" title={remoteIso}>
|
||||
{timeLabel}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="post-stats">
|
||||
<span className={`post-stat${stars === 0 ? ' post-stat--zero' : ''}`} title="Stars">
|
||||
<Star aria-hidden />
|
||||
{stars}
|
||||
</span>
|
||||
<span className={`post-stat${forks === 0 ? ' post-stat--zero' : ''}`} title="Forks">
|
||||
<GitFork aria-hidden />
|
||||
{forks}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ProjectListItem);
|
||||
Reference in New Issue
Block a user