feat: 类型帖互动、积分经济与发帖体验
补齐问答采纳/重开、投票匿名与有票禁编、悬赏过期退回、抽奖回帖开奖,并接入积分账本与发帖附件可见性。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
130
frontend/app/points/PointsClient.tsx
Normal file
130
frontend/app/points/PointsClient.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user