Files
SproutGate/sproutgate-frontend/src/components/PublicUserPage.jsx

215 lines
8.7 KiB
JavaScript
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 React, { useCallback, useEffect, useState } from "react";
import { API_BASE, marked, formatWebsiteLabel, formatUserRegisteredAt } from "../config";
import icons from "../icons";
import { InfoRow, StatItem } from "./common";
import AvatarImg from "./AvatarImg";
export default function PublicUserPage({ account, onReady, onPreviewImage }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [profileLikeCount, setProfileLikeCount] = useState(0);
const [viewerHasLikedToday, setViewerHasLikedToday] = useState(false);
const [viewerLikesRemaining, setViewerLikesRemaining] = useState(5);
const [profileLikeDailyMax, setProfileLikeDailyMax] = useState(5);
const [viewerIsOwner, setViewerIsOwner] = useState(false);
const [likeLoading, setLikeLoading] = useState(false);
const [likeError, setLikeError] = useState("");
const authHeaders = () => {
try {
const token = (localStorage.getItem("sproutgate_token") || "").trim();
return token ? { Authorization: `Bearer ${token}` } : {};
} catch {
return {};
}
};
const loadPublicUser = useCallback(async () => {
if (!account) {
setError("缺少账户名");
setLoading(false);
onReady?.();
return;
}
setLoading(true);
setError("");
setUser(null);
setLikeError("");
try {
const res = await fetch(`${API_BASE}/api/public/users/${encodeURIComponent(account)}`, {
headers: { ...authHeaders() }
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "加载公开主页失败");
setUser(data.user || null);
setProfileLikeCount(Number(data.profileLikeCount) || 0);
setViewerHasLikedToday(Boolean(data.viewerHasLikedToday));
const rem = Number(data.viewerLikesRemainingToday);
if (Number.isFinite(rem) && rem >= 0) setViewerLikesRemaining(rem);
const mx = Number(data.profileLikeDailyMax);
if (Number.isFinite(mx) && mx > 0) setProfileLikeDailyMax(mx);
setViewerIsOwner(Boolean(data.viewerIsOwner));
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
onReady?.();
}
}, [account, onReady]);
useEffect(() => {
loadPublicUser();
}, [loadPublicUser]);
const handleProfileLike = async () => {
if (!account || likeLoading || viewerIsOwner || viewerHasLikedToday || viewerLikesRemaining <= 0) return;
const h = authHeaders();
if (!h.Authorization) {
setLikeError("请先登录用户中心后再点赞");
return;
}
setLikeLoading(true);
setLikeError("");
try {
const res = await fetch(`${API_BASE}/api/public/users/${encodeURIComponent(account)}/like`, {
method: "POST",
headers: { "Content-Type": "application/json", ...h }
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const rem = Number(data.viewerLikesRemainingToday);
if (Number.isFinite(rem) && rem >= 0) setViewerLikesRemaining(rem);
throw new Error(data.error || "点赞失败");
}
setProfileLikeCount(Number(data.profileLikeCount) || 0);
setViewerHasLikedToday(true);
const remOk = Number(data.viewerLikesRemainingToday);
if (Number.isFinite(remOk) && remOk >= 0) setViewerLikesRemaining(remOk);
const mxOk = Number(data.profileLikeDailyMax);
if (Number.isFinite(mxOk) && mxOk > 0) setProfileLikeDailyMax(mxOk);
} catch (err) {
setLikeError(err.message);
} finally {
setLikeLoading(false);
}
};
const displayName = user?.username || user?.account || "未命名用户";
return (
<section className="panel">
{loading && <div className="unified-page-loading">加载公开主页中</div>}
{!loading && error && (
<div className="unified-page-miss">
<h2>未找到用户</h2>
<div className="error">{error}</div>
<div className="actions">
<a className="primary" href="/">返回首页</a>
</div>
</div>
)}
{!loading && user && (
<div className="card profile">
<div className="profile-header">
<AvatarImg
user={user}
placeholderSize={160}
alt={displayName}
className="previewable-image"
previewable
onPreviewImage={onPreviewImage}
previewLabel={displayName}
/>
<div>
<h2>{displayName}</h2>
</div>
</div>
<div className="profile-section-title profile-section-title--lead">基本信息</div>
<div className="profile-info-rows">
<InfoRow icon={icons.account} label="账户" value={user.account} />
<InfoRow icon={icons.username} label="用户名" value={user.username || "未填写"} />
<InfoRow icon={icons.calendar} label="注册时间" value={formatUserRegisteredAt(user.createdAt)} />
{user.websiteUrl ? (
<InfoRow icon={icons.link} label="个人主页">
<a
href={user.websiteUrl}
target="_blank"
rel="noopener noreferrer"
className="profile-external-link"
>
{formatWebsiteLabel(user.websiteUrl)}
</a>
</InfoRow>
) : null}
</div>
<div className="profile-section-title">统计信息</div>
<div className="profile-stats-flow">
<StatItem icon={icons.heart} label="获赞" value={`${profileLikeCount}`} />
<StatItem icon={icons.level} label="等级" value={`${user.level ?? 0}`} />
<StatItem icon={icons.coins} label="萌芽币" value={user.sproutCoins ?? 0} />
<StatItem icon={icons.calendar} label="签到天数" value={`${user.checkInDays ?? 0}`} />
<StatItem icon={icons.statLightning} label="连续签到" value={`${user.checkInStreak ?? 0}`} />
<StatItem icon={icons.statClock} label="访问天数" value={`${user.visitDays ?? 0}`} />
<StatItem icon={icons.statRepeat} label="连续访问" value={`${user.visitStreak ?? 0}`} />
</div>
{!viewerIsOwner && (
<div className="profile-like-row">
{authHeaders().Authorization ? (
<span className="hint profile-like-quota">
今日还可点赞 <strong>{viewerLikesRemaining}</strong> / {profileLikeDailyMax} 每自然日刷新
</span>
) : null}
{viewerHasLikedToday ? (
<span className="profile-like-done">今日已为 Ta 点赞</span>
) : viewerLikesRemaining <= 0 ? (
<span className="profile-like-done profile-like-quota-exhausted">
今日点赞名额已用完明天可再给最多 {profileLikeDailyMax} 人点赞
</span>
) : (
<button
type="button"
className="primary profile-like-btn"
onClick={handleProfileLike}
disabled={likeLoading}
>
{likeLoading ? "提交中…" : "为 Ta 点赞"}
</button>
)}
{likeError ? <span className="error profile-like-error" role="alert">{likeError}</span> : null}
{!authHeaders().Authorization && !viewerHasLikedToday ? (
<span className="hint profile-like-hint">登录同一浏览器的用户中心后即可点赞每人每天最多 {profileLikeDailyMax} </span>
) : null}
</div>
)}
<div className="profile-section-title">活动记录</div>
<div className="profile-activity-row">
<span>最后签到 <strong>{user.lastCheckInAt || user.lastCheckInDate || "未签到"}</strong></span>
<span>最后访问 <strong>{user.lastVisitAt || user.lastVisitDate || "未访问"}</strong></span>
</div>
<div className="profile-activity-row profile-visit-meta">
<span>
最后访问 IP{" "}
<strong className="mono">{user.lastVisitIp || "暂无"}</strong>
</span>
<span>
最后位置 <strong>{user.lastVisitDisplayLocation || "暂无"}</strong>
</span>
</div>
<div className="profile-section-title">个人简介</div>
<div className="markdown profile-markdown">
<div
className="markdown-body"
dangerouslySetInnerHTML={{ __html: marked.parse(user.bio || "暂无简介") }}
/>
</div>
</div>
)}
</section>
);
}