用顶栏/主栏高度令牌消除 sticky 首滚空走,面包屑叠到中栏上方,公开页增加回顶浮钮。 Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
4.3 KiB
TypeScript
131 lines
4.3 KiB
TypeScript
"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 -mt-4 sm:-mt-6 mb-1.5 flex items-center gap-1 text-[12.5px] leading-none">
|
||
<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="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>
|
||
);
|
||
}
|