Files
jiang13-bbs/frontend/app/points/PointsClient.tsx
freefire b9ba8cb8c9 feat: 类型帖互动、积分经济与发帖体验
补齐问答采纳/重开、投票匿名与有票禁编、悬赏过期退回、抽奖回帖开奖,并接入积分账本与发帖附件可见性。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 03:17:07 +08:00

131 lines
4.3 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 Link from "next/link";
import { ChevronRight } from "lucide-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">
<nav className="meta mb-5 flex items-center gap-1.5 text-[13px]">
<Link href="/" className="hover:text-[var(--accent)] transition-colors">
首页
</Link>
<ChevronRight size={13} />
<span style={{ color: "var(--ink-2)" }}>我的积分</span>
</nav>
<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="font-bold 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>
);
}