Files
jiang13-forum/frontend/src/components/PostAuthorCard.tsx
freefire 500e452c2f feat: 重设计私信页并优化开源展柜体验
私信改为飞书风布局,支持缩略图表情/图片、按会话草稿与用户搜索发起会话;主页发私信直达消息页。开源展柜修复空列表粘滞并改版目录式 UI。
2026-09-02 02:45:29 +08:00

163 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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);
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 isSelf = !!me && me.id === author.id;
const profileHref = userPath(author.id);
const openMessage = () => {
if (!me) {
nav(loginPath(profileHref));
return;
}
nav(`/messages?peer=${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-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" showBadges />
{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>
</div>
);
}