移除旧版 HTML 模板与兼容层,并完善私信、举报、媒体存储与 SEO。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 16:37:11 +08:00
parent 060b7707cb
commit 48db333272
121 changed files with 11147 additions and 3225 deletions

View File

@@ -0,0 +1,176 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Eye, FileText, Heart, Mail, MessageCircle, UserRound } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { api } from '../api/client';
import type { User, UserActivityStats, UserPublic } from '../api/types';
import { useAuth } from '../hooks/useAuth';
import { loginPath } from '../utils/authRedirect';
import { formatTime } from '../utils/content';
import { userPath } from '../utils/userPath';
import ComposeMessageDialog from './ComposeMessageDialog';
import UserLink from './UserLink';
interface Props {
author?: User | null;
publishedAt?: string;
viewCount?: number;
}
/** 帖子详情右栏:作者信息卡(私信 / 主页 / 统计) */
export default function PostAuthorCard({
author,
publishedAt,
viewCount,
}: Props) {
const nav = useNavigate();
const { user: me } = useAuth();
const [profile, setProfile] = useState<UserPublic | null>(null);
const [stats, setStats] = useState<UserActivityStats | null>(null);
const [msgOpen, setMsgOpen] = useState(false);
useEffect(() => {
if (!author?.id) {
setProfile(null);
setStats(null);
return;
}
let cancelled = false;
api.userProfile(author.id)
.then((r) => {
if (cancelled) return;
setProfile(r.user);
setStats(r.stats);
})
.catch(() => {
if (cancelled) return;
// 详情里已有轻量 user接口失败时仍可展示基本信息
setProfile(null);
setStats(null);
});
return () => { cancelled = true; };
}, [author?.id]);
if (!author?.id) {
return (
<div className="widget-card widget-card--author">
<div className="widget-card-head">
<UserRound className="widget-card-icon widget-card-icon--author" aria-hidden />
</div>
<div className="widget-card-body">
<div className="widget-empty"></div>
</div>
</div>
);
}
const display = profile ?? author;
const nick = display.nickname || display.username || `用户 #${author.id}`;
const initial = nick.charAt(0) || '?';
const signature = (profile?.signature ?? author.signature ?? '').trim();
const isAdmin = display.role === 'admin';
const isSelf = !!me && me.id === author.id;
const profileHref = userPath(author.id);
const openMessage = () => {
if (!me) {
nav(loginPath(profileHref));
return;
}
setMsgOpen(true);
};
return (
<div className="widget-card widget-card--author">
<div className="widget-card-head">
<UserRound className="widget-card-icon widget-card-icon--author" aria-hidden />
</div>
<div className="widget-author-panel">
<div className="widget-author-body">
<UserLink
user={display}
showAvatar={false}
showName={false}
className="widget-author-avatar user-link--avatar-only"
>
{display.avatar
? <img src={display.avatar} alt="" loading="lazy" decoding="async" />
: initial}
</UserLink>
<div className="widget-author-meta">
<div className="widget-author-name-row">
<UserLink user={display} className="widget-author-name" />
{isAdmin && <Badge variant="green" className="widget-author-badge"></Badge>}
{display.banned && <Badge variant="destructive" className="widget-author-badge"></Badge>}
</div>
{signature ? (
<p className="widget-author-signature" title={signature}>{signature}</p>
) : null}
{(publishedAt || typeof viewCount === 'number') && (
<p className="widget-author-stats">
{publishedAt ? <span>{formatTime(publishedAt)} </span> : null}
{publishedAt && typeof viewCount === 'number' ? (
<span className="widget-author-stats-dot" aria-hidden>·</span>
) : null}
{typeof viewCount === 'number' ? (
<span className="widget-author-views">
<Eye size={12} aria-hidden />
{viewCount}
</span>
) : null}
</p>
)}
</div>
</div>
<div className="widget-author-metrics" aria-label="作者统计">
<div className="widget-author-metric">
<FileText size={13} aria-hidden />
<strong>{stats?.post_count ?? '—'}</strong>
<span></span>
</div>
<div className="widget-author-metric">
<MessageCircle size={13} aria-hidden />
<strong>{stats?.comment_count ?? '—'}</strong>
<span></span>
</div>
<div className="widget-author-metric">
<Heart size={13} aria-hidden />
<strong>{stats?.like_received ?? '—'}</strong>
<span></span>
</div>
</div>
<div className="widget-author-actions">
{!isSelf && (
<Button size="sm" className="widget-author-action" onClick={openMessage}>
<Mail size={14} />
</Button>
)}
<Button
size="sm"
variant="outline"
className="widget-author-action"
onClick={() => nav(isSelf ? '/profile' : profileHref)}
>
{isSelf ? '我的主页' : '查看主页'}
</Button>
</div>
</div>
{!isSelf && (
<ComposeMessageDialog
open={msgOpen}
onOpenChange={setMsgOpen}
toUserId={author.id}
toNickname={nick}
onSent={() => nav(`/messages?peer=${author.id}`)}
/>
)}
</div>
);
}