refactor(frontend): 重构站点数据面板与签到组件
将原有的站点统计卡片重构为bento布局样式,把签到组件内嵌到站点数据面板中,统一UI风格并简化代码结构
This commit is contained in:
@@ -605,6 +605,101 @@ h1, h2, h3, h4 {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 站点数据 bento 统计瓦片 */
|
||||||
|
.j13-pulse {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.j13-pulse-cell {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 11px 12px 10px;
|
||||||
|
background: var(--panel-2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
}
|
||||||
|
.j13-pulse-lab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--ink-3);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.j13-pulse-num {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.05;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
|
color: var(--ink);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.j13-pulse-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 10.5px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.j13-pulse-foot.is-up {
|
||||||
|
color: var(--ok);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.j13-pulse-dot {
|
||||||
|
position: relative;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--ok);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.j13-pulse-dot::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -3px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--ok);
|
||||||
|
opacity: 0.4;
|
||||||
|
animation: j13-ping 1.8s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||||
|
}
|
||||||
|
@keyframes j13-ping {
|
||||||
|
0% { transform: scale(0.6); opacity: 0.5; }
|
||||||
|
70%, 100% { transform: scale(1.9); opacity: 0; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.j13-pulse-dot::after {
|
||||||
|
animation: none;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 卡内签到条:分隔线 + 紧凑按钮 */
|
||||||
|
.j13-pulse-checkin {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 11px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.j13-pulse-checkin-btn {
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 11px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.j13-pulse-checkin-done {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 11px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* 热门榜单 */
|
/* 热门榜单 */
|
||||||
.j13-rank {
|
.j13-rank {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
|||||||
{overview && (
|
{overview && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<CommunityPanelsMobile
|
<CommunityPanelsMobile
|
||||||
|
stats={overview.stats}
|
||||||
hot={overview.hot}
|
hot={overview.hot}
|
||||||
activeUsers={overview.active_users}
|
activeUsers={overview.active_users}
|
||||||
announcements={overview.announcements}
|
announcements={overview.announcements}
|
||||||
|
|||||||
@@ -6,40 +6,16 @@ import { CalendarDays, CalendarCheck, LogIn, Loader2 } from "lucide-react";
|
|||||||
import { apiDoCheckin, type CheckinStatus } from "@/lib/api";
|
import { apiDoCheckin, type CheckinStatus } from "@/lib/api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每日签到卡(右栏)。
|
* 每日签到紧凑条,内嵌在右栏「站点数据」卡底部。
|
||||||
* initial 为 undefined 表示游客;否则为 SSR 注入的登录用户签到状态。
|
* initial 为 undefined 表示游客;否则为 SSR 注入的登录用户签到状态。
|
||||||
*/
|
*/
|
||||||
export default function CheckinCard({ initial }: { initial?: CheckinStatus | null }) {
|
export function PulseCheckin({ initial }: { initial?: CheckinStatus | null }) {
|
||||||
const [status, setStatus] = useState<CheckinStatus | null>(initial ?? null);
|
const [status, setStatus] = useState<CheckinStatus | null>(initial ?? null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
// 游客态
|
|
||||||
if (initial === undefined) {
|
|
||||||
return (
|
|
||||||
<section className="j13-module" aria-label="每日签到">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span
|
|
||||||
className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0"
|
|
||||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
|
||||||
>
|
|
||||||
<CalendarDays size={19} />
|
|
||||||
</span>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="text-[14px] font-bold" style={{ color: "var(--ink)" }}>
|
|
||||||
每日签到
|
|
||||||
</p>
|
|
||||||
<p className="meta text-[12px] mt-0.5">登录后每日签到领积分</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Link href="/login?redirect=/" className="btn btn-line btn-sm w-full justify-center mt-3.5">
|
|
||||||
<LogIn size={14} /> 登录后签到
|
|
||||||
</Link>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const checked = status?.checked_today ?? false;
|
const checked = status?.checked_today ?? false;
|
||||||
|
const guest = initial === undefined;
|
||||||
|
|
||||||
async function doCheckin() {
|
async function doCheckin() {
|
||||||
if (loading || checked) return;
|
if (loading || checked) return;
|
||||||
@@ -55,59 +31,65 @@ export default function CheckinCard({ initial }: { initial?: CheckinStatus | nul
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="j13-module" aria-label="每日签到">
|
<div aria-label="每日签到">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2.5">
|
||||||
<span
|
<span
|
||||||
className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0"
|
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg"
|
||||||
style={{
|
style={{
|
||||||
background: checked ? "var(--accent-soft)" : "var(--panel-2)",
|
background: checked || guest ? "var(--accent-soft)" : "var(--tint)",
|
||||||
color: checked ? "var(--accent)" : "var(--ink-2)",
|
color: checked || guest ? "var(--accent)" : "var(--ink-2)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{checked ? <CalendarCheck size={19} /> : <CalendarDays size={19} />}
|
{checked ? <CalendarCheck size={16} /> : <CalendarDays size={16} />}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-[14px] font-bold" style={{ color: "var(--ink)" }}>
|
<p className="text-[12.5px] font-bold leading-tight" style={{ color: "var(--ink)" }}>
|
||||||
每日签到
|
每日签到
|
||||||
</p>
|
</p>
|
||||||
<p className="meta text-[12px] mt-0.5">
|
<p className="meta mt-0.5 truncate text-[11px]">
|
||||||
{checked
|
{guest
|
||||||
? `已连续签到 ${status?.streak ?? 0} 天 · 累计 ${status?.total_points ?? 0} 积分`
|
? "登录后每日签到领积分"
|
||||||
: `连续 ${status?.streak ?? 0} 天 · 今日可得 ${status?.today_points ?? 5} 积分`}
|
: checked
|
||||||
|
? `连签 ${status?.streak ?? 0} 天 · 累计 ${status?.total_points ?? 0} 积分`
|
||||||
|
: `连签 ${status?.streak ?? 0} 天 · 今日 +${status?.today_points ?? 5}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{!checked && (
|
{guest ? (
|
||||||
<span className="text-[17px] font-extrabold tabular-nums shrink-0" style={{ color: "var(--accent)" }}>
|
<Link
|
||||||
+{status?.today_points ?? 5}
|
href="/login?redirect=/"
|
||||||
|
className="btn btn-line btn-sm j13-pulse-checkin-btn shrink-0"
|
||||||
|
>
|
||||||
|
<LogIn size={13} /> 登录
|
||||||
|
</Link>
|
||||||
|
) : checked ? (
|
||||||
|
<span
|
||||||
|
className="j13-pulse-checkin-done shrink-0"
|
||||||
|
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||||
|
>
|
||||||
|
<CalendarCheck size={12.5} /> 已签
|
||||||
</span>
|
</span>
|
||||||
)}
|
) : (
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={doCheckin}
|
onClick={doCheckin}
|
||||||
disabled={loading || checked}
|
disabled={loading}
|
||||||
aria-disabled={loading || checked}
|
className="btn btn-primary btn-sm j13-pulse-checkin-btn shrink-0"
|
||||||
className={`btn w-full justify-center mt-3.5 ${checked ? "btn-line" : "btn-primary"}`}
|
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 size={15} className="animate-spin" /> 签到中…
|
<Loader2 size={13} className="animate-spin" /> 签到中
|
||||||
</>
|
|
||||||
) : checked ? (
|
|
||||||
<>
|
|
||||||
<CalendarCheck size={15} /> 今日已签到
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"立即签到"
|
`签到 +${status?.today_points ?? 5}`
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-[12px] mt-2" style={{ color: "var(--clay)" }} role="alert">
|
<p className="mt-1.5 text-[11.5px]" style={{ color: "var(--clay)" }} role="alert">
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
Megaphone,
|
Megaphone,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
PenLine,
|
PenLine,
|
||||||
|
Activity,
|
||||||
|
TrendingUp,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
PostListItem,
|
PostListItem,
|
||||||
@@ -19,52 +21,43 @@ import type {
|
|||||||
CheckinStatus,
|
CheckinStatus,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
import Avatar from "@/components/Avatar";
|
import Avatar from "@/components/Avatar";
|
||||||
import CheckinCard from "./CheckinCard";
|
import { PulseCheckin } from "./CheckinCard";
|
||||||
import { FRIEND_LINKS } from "@/lib/friendLinks";
|
import { FRIEND_LINKS } from "@/lib/friendLinks";
|
||||||
import { formatCompact, formatShortDate } from "@/lib/format";
|
import { formatCompact, formatShortDate } from "@/lib/format";
|
||||||
|
|
||||||
/* ---------- 站点数据(2×2 极简统计表) ---------- */
|
/* ---------- 站点数据(2×2 bento 统计瓦片) ---------- */
|
||||||
|
|
||||||
function StatCell({
|
function PulseCard({ stats, checkin }: { stats: OverviewStats; checkin?: CheckinStatus | null }) {
|
||||||
index,
|
const cells = [
|
||||||
label,
|
{
|
||||||
value,
|
icon: <Users size={12.5} />,
|
||||||
foot,
|
label: "注册用户",
|
||||||
}: {
|
value: formatCompact(stats.users),
|
||||||
index: number;
|
foot: `+${formatCompact(stats.today_users)} 今日`,
|
||||||
label: string;
|
up: true,
|
||||||
value: string;
|
},
|
||||||
foot: string;
|
{
|
||||||
}) {
|
icon: <PenLine size={12.5} />,
|
||||||
const rightCol = index % 2 === 1;
|
label: "话题总数",
|
||||||
const bottomRow = index >= 2;
|
value: formatCompact(stats.posts),
|
||||||
return (
|
foot: `+${formatCompact(stats.today_posts)} 今日`,
|
||||||
<div
|
up: true,
|
||||||
className="min-w-0 py-3.5"
|
},
|
||||||
style={{
|
{
|
||||||
borderLeft: rightCol ? "1px solid var(--line)" : undefined,
|
icon: <MessageCircle size={12.5} />,
|
||||||
borderTop: bottomRow ? "1px solid var(--line)" : undefined,
|
label: "回复总数",
|
||||||
paddingLeft: rightCol ? 14 : 0,
|
value: formatCompact(stats.comments),
|
||||||
paddingRight: rightCol ? 0 : 14,
|
foot: `+${formatCompact(stats.today_comments)} 今日`,
|
||||||
}}
|
up: true,
|
||||||
>
|
},
|
||||||
<p className="text-[12px]" style={{ color: "var(--ink-3)" }}>
|
{
|
||||||
{label}
|
icon: <Activity size={12.5} />,
|
||||||
</p>
|
label: "在线用户",
|
||||||
<p
|
value: formatCompact(stats.online),
|
||||||
className="mt-1.5 text-[20px] font-semibold leading-none tracking-tight tabular-nums"
|
foot: `峰值 ${formatCompact(stats.peak_online)}`,
|
||||||
style={{ color: "var(--ink)" }}
|
up: false,
|
||||||
>
|
},
|
||||||
{value}
|
];
|
||||||
</p>
|
|
||||||
<p className="mt-2 text-[11px] tabular-nums" style={{ color: "var(--ink-3)" }}>
|
|
||||||
{foot}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PulseCard({ stats }: { stats: OverviewStats }) {
|
|
||||||
return (
|
return (
|
||||||
<section className="j13-module" aria-label="站点数据">
|
<section className="j13-module" aria-label="站点数据">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -76,15 +69,29 @@ function PulseCard({ stats }: { stats: OverviewStats }) {
|
|||||||
style={{ color: "var(--ink-3)" }}
|
style={{ color: "var(--ink-3)" }}
|
||||||
title="近 5 分钟在线"
|
title="近 5 分钟在线"
|
||||||
>
|
>
|
||||||
<span className="h-1.5 w-1.5 rounded-full" style={{ background: "var(--ok)" }} />
|
<span className="j13-pulse-dot" aria-hidden="true" />
|
||||||
实时
|
实时
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 grid grid-cols-2">
|
<div className="j13-pulse mt-3">
|
||||||
<StatCell index={0} label="注册用户" value={formatCompact(stats.users)} foot={`+${stats.today_users} 今日`} />
|
{cells.map((c) => (
|
||||||
<StatCell index={1} label="话题总数" value={formatCompact(stats.posts)} foot={`+${stats.today_posts} 今日`} />
|
<div key={c.label} className="j13-pulse-cell">
|
||||||
<StatCell index={2} label="回复总数" value={formatCompact(stats.comments)} foot={`+${stats.today_comments} 今日`} />
|
<p className="j13-pulse-lab" style={{ color: "var(--ink-3)" }}>
|
||||||
<StatCell index={3} label="在线用户" value={formatCompact(stats.online)} foot={`峰值 ${formatCompact(stats.peak_online)}`} />
|
<span style={{ color: "var(--accent)" }} aria-hidden="true">
|
||||||
|
{c.icon}
|
||||||
|
</span>
|
||||||
|
{c.label}
|
||||||
|
</p>
|
||||||
|
<p className="j13-pulse-num">{c.value}</p>
|
||||||
|
<p className={`j13-pulse-foot${c.up ? " is-up" : ""}`}>
|
||||||
|
{c.up && <TrendingUp size={10.5} aria-hidden="true" />}
|
||||||
|
{c.foot}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="j13-pulse-checkin">
|
||||||
|
<PulseCheckin initial={checkin} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -319,9 +326,8 @@ export function CommunityPanels({
|
|||||||
return (
|
return (
|
||||||
<aside className="hidden w-[280px] shrink-0 lg:block xl:w-[300px]" aria-label="社区面板">
|
<aside className="hidden w-[280px] shrink-0 lg:block xl:w-[300px]" aria-label="社区面板">
|
||||||
<div className="sticky top-24 space-y-4">
|
<div className="sticky top-24 space-y-4">
|
||||||
<PulseCard stats={stats} />
|
<PulseCard stats={stats} checkin={checkin} />
|
||||||
<Announcements items={announcements} />
|
<Announcements items={announcements} />
|
||||||
<CheckinCard initial={checkin} />
|
|
||||||
<HotRanking posts={hot} />
|
<HotRanking posts={hot} />
|
||||||
<NewUsers users={newUsers} />
|
<NewUsers users={newUsers} />
|
||||||
<ActiveUsers users={activeUsers} />
|
<ActiveUsers users={activeUsers} />
|
||||||
@@ -332,16 +338,17 @@ export function CommunityPanels({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CommunityPanelsMobile({
|
export function CommunityPanelsMobile({
|
||||||
|
stats,
|
||||||
hot,
|
hot,
|
||||||
activeUsers,
|
activeUsers,
|
||||||
announcements,
|
announcements,
|
||||||
newUsers,
|
newUsers,
|
||||||
checkin,
|
checkin,
|
||||||
}: Omit<PanelsProps, "stats">) {
|
}: PanelsProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 lg:hidden" aria-label="社区面板">
|
<div className="space-y-4 lg:hidden" aria-label="社区面板">
|
||||||
|
<PulseCard stats={stats} checkin={checkin} />
|
||||||
<Announcements items={announcements} />
|
<Announcements items={announcements} />
|
||||||
<CheckinCard initial={checkin} />
|
|
||||||
<HotRanking posts={hot} />
|
<HotRanking posts={hot} />
|
||||||
<NewUsers users={newUsers} />
|
<NewUsers users={newUsers} />
|
||||||
<ActiveUsers users={activeUsers} />
|
<ActiveUsers users={activeUsers} />
|
||||||
|
|||||||
Reference in New Issue
Block a user