feat: 更新SproutGate前后端代码

This commit is contained in:
2026-04-01 22:04:01 +08:00
parent 90590c7cb0
commit 650e1c7707
49 changed files with 3609 additions and 768 deletions

View File

@@ -1,44 +1,100 @@
import React, { useEffect, useState } from "react";
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("");
useEffect(() => {
let cancelled = false;
const authHeaders = () => {
try {
const token = (localStorage.getItem("sproutgate_token") || "").trim();
return token ? { Authorization: `Bearer ${token}` } : {};
} catch {
return {};
}
};
const loadPublicUser = async () => {
if (!account) {
setError("缺少账户名");
setLoading(false);
if (!cancelled && onReady) onReady();
return;
}
setLoading(true);
setError("");
setUser(null);
try {
const res = await fetch(`${API_BASE}/api/public/users/${encodeURIComponent(account)}`);
const data = await res.json();
if (!res.ok) throw new Error(data.error || "加载公开主页失败");
setUser(data.user || null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
if (!cancelled && onReady) onReady();
}
};
loadPublicUser();
return () => { cancelled = true; };
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]);
const avatarUrl = user?.avatarUrl || "https://dummyimage.com/160x160/ddd/fff&text=Avatar";
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 (
@@ -56,13 +112,14 @@ export default function PublicUserPage({ account, onReady, onPreviewImage }) {
{!loading && user && (
<div className="card profile">
<div className="profile-header">
<img
src={avatarUrl}
<AvatarImg
user={user}
placeholderSize={160}
alt={displayName}
className="previewable-image"
onClick={() => onPreviewImage?.(avatarUrl, displayName)}
role="button"
tabIndex={0}
previewable
onPreviewImage={onPreviewImage}
previewLabel={displayName}
/>
<div>
<h2>{displayName}</h2>
@@ -90,6 +147,7 @@ export default function PublicUserPage({ account, onReady, onPreviewImage }) {
<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}`} />
@@ -97,6 +155,35 @@ export default function PublicUserPage({ account, onReady, onPreviewImage }) {
<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">