Files
jiang13-bbs/frontend/app/points/PointsClient.tsx
freefire d7c6b3a961 feat: 帖子收藏与用户关注
- 新增收藏功能:favorite 服务与 handler、FavoriteButton 组件
- 新增关注功能:follow 服务与 handler、FollowButton 组件
- 用户主页适配关注/粉丝与收藏展示
- flash 消息工具、广告与侧边栏组件微调
2026-09-26 03:02:40 +08:00

121 lines
4.0 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.
"use client";
import { useCallback, useEffect, useState } from "react";
import {
apiGetPoints,
apiGetPointsLedger,
type PointLedgerItem,
} from "@/lib/api";
import { pointReasonLabel } from "@/lib/points";
import { formatRelative } from "@/lib/format";
import Pagination from "@/components/Pagination";
export default function PointsClient() {
const [balance, setBalance] = useState<number | null>(null);
const [items, setItems] = useState<PointLedgerItem[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const size = 20;
const load = useCallback(async (p: number) => {
setLoading(true);
try {
const [bal, led] = await Promise.all([
apiGetPoints(),
apiGetPointsLedger(p, size),
]);
setBalance(typeof bal.points === "number" ? bal.points : 0);
setItems(led.items || []);
setTotal(led.total || 0);
setPage(led.page || p);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load(1);
}, [load]);
return (
<div className="max-w-3xl mx-auto">
<section className="panel p-6 sm:p-8 mb-5">
<p className="meta text-[13px]">可用余额</p>
<p
className="mt-2 text-[36px] font-extrabold tabular-nums leading-none"
style={{ color: "var(--accent)" }}
>
{balance === null ? "—" : balance.toLocaleString("zh-CN")}
</p>
<p className="meta text-[12.5px] mt-3">
签到、悬赏采纳与附件收益会增加余额;解锁正文、悬赏托管与积分附件会扣减。
</p>
</section>
<section className="panel overflow-hidden">
<div
className="px-5 py-3.5 flex items-center justify-between"
style={{ borderBottom: "1px solid var(--line)" }}
>
<h1 className="font-extrabold text-[16px]" style={{ color: "var(--ink)" }}>
积分流水
</h1>
<span className="meta text-[12px] tabular-nums">{total} 条</span>
</div>
{loading && items.length === 0 ? (
<p className="meta text-center py-12">加载中…</p>
) : items.length === 0 ? (
<p className="meta text-center py-12">暂无流水,去首页签到赚第一笔积分吧</p>
) : (
<ul>
{items.map((it) => (
<li
key={it.id}
className="px-5 py-3.5 flex items-start justify-between gap-4"
style={{ borderTop: "1px solid var(--line)" }}
>
<div className="min-w-0">
<p className="text-[14px]" style={{ color: "var(--ink)" }}>
{pointReasonLabel(it.reason)}
</p>
{it.note && (
<p className="meta text-[12.5px] mt-0.5 truncate">{it.note}</p>
)}
<p className="meta text-[12px] mt-1 tabular-nums">
{formatRelative(it.created_at)}
</p>
</div>
<div className="text-right shrink-0">
<p
className="font-extrabold tabular-nums text-[15px]"
style={{ color: it.delta >= 0 ? "var(--accent)" : "var(--danger)" }}
>
{it.delta >= 0 ? `+${it.delta}` : it.delta}
</p>
<p className="meta text-[12px] tabular-nums mt-0.5">
余 {it.balance}
</p>
</div>
</li>
))}
</ul>
)}
{total > size && (
<div className="px-5 py-4" style={{ borderTop: "1px solid var(--line)" }}>
<Pagination
total={total}
page={page}
size={size}
basePath="/points"
onChange={(p) => void load(p)}
/>
</div>
)}
</section>
</div>
);
}