diff --git a/backend/service/points.go b/backend/service/points.go
index 8ffc07a..c2bed31 100644
--- a/backend/service/points.go
+++ b/backend/service/points.go
@@ -125,6 +125,170 @@ type LedgerItem struct {
CreatedAt time.Time `json:"created_at"`
}
+// LedgerParty 管理端流水相关用户摘要
+type LedgerParty struct {
+ ID uint `json:"id"`
+ Username string `json:"username"`
+ Nickname string `json:"nickname"`
+ Avatar string `json:"avatar"`
+}
+
+// AdminLedgerItem 管理端最近流水:单边入账带 user;用户互转合并为 from → to
+type AdminLedgerItem struct {
+ ID uint `json:"id"`
+ Delta int `json:"delta"`
+ Balance int `json:"balance"`
+ Reason string `json:"reason"`
+ RefType string `json:"ref_type"`
+ RefID uint `json:"ref_id"`
+ Note string `json:"note"`
+ CreatedAt time.Time `json:"created_at"`
+ User *LedgerParty `json:"user,omitempty"`
+ From *LedgerParty `json:"from,omitempty"`
+ To *LedgerParty `json:"to,omitempty"`
+}
+
+const (
+ adminRecentLedgerLimit = 15
+ adminRecentLedgerFetch = 32
+ ledgerPairMaxDist = 8
+)
+
+func absInt(n int) int {
+ if n < 0 {
+ return -n
+ }
+ return n
+}
+
+func ledgerPartyOf(u model.User) LedgerParty {
+ return LedgerParty{ID: u.ID, Username: u.Username, Nickname: u.Nickname, Avatar: u.Avatar}
+}
+
+func copyLedgerParty(p LedgerParty) *LedgerParty {
+ if p.ID == 0 {
+ return nil
+ }
+ cp := p
+ return &cp
+}
+
+// complementaryLedgerRef 用户互转对向 ref_type;非互转返回空
+func complementaryLedgerRef(reason, refType string) string {
+ switch reason {
+ case model.PointReasonUnlockPost:
+ if refType == "post" {
+ return "post_earn"
+ }
+ if refType == "post_earn" {
+ return "post"
+ }
+ case model.PointReasonDownloadFile:
+ if refType == "attachment" {
+ return "attachment_earn"
+ }
+ if refType == "attachment_earn" {
+ return "attachment"
+ }
+ }
+ return ""
+}
+
+func ledgersComplement(a, b model.PointLedger) bool {
+ comp := complementaryLedgerRef(a.Reason, a.RefType)
+ if comp == "" || b.Reason != a.Reason || b.RefID != a.RefID || b.RefType != comp {
+ return false
+ }
+ if absInt(a.Delta) != absInt(b.Delta) || absInt(a.Delta) == 0 {
+ return false
+ }
+ return (a.Delta > 0) != (b.Delta > 0)
+}
+
+func idDist(a, b uint) int {
+ if a > b {
+ return int(a - b)
+ }
+ return int(b - a)
+}
+
+func findComplementIndex(rows []model.PointLedger, i int, used []bool) int {
+ r := rows[i]
+ if complementaryLedgerRef(r.Reason, r.RefType) == "" {
+ return -1
+ }
+ best := -1
+ bestDist := ledgerPairMaxDist + 1
+ for j := range rows {
+ if j == i || used[j] || !ledgersComplement(r, rows[j]) {
+ continue
+ }
+ d := idDist(r.ID, rows[j].ID)
+ if d == 0 || d > ledgerPairMaxDist || d >= bestDist {
+ continue
+ }
+ bestDist = d
+ best = j
+ }
+ return best
+}
+
+// pairLedgerIndices 将用户互转的支出/收益配成一对;返回 [本条, 对向](无对向则为 -1)
+func pairLedgerIndices(rows []model.PointLedger) [][2]int {
+ used := make([]bool, len(rows))
+ out := make([][2]int, 0, len(rows))
+ for i := range rows {
+ if used[i] {
+ continue
+ }
+ j := findComplementIndex(rows, i, used)
+ if j >= 0 {
+ used[i] = true
+ used[j] = true
+ out = append(out, [2]int{i, j})
+ continue
+ }
+ used[i] = true
+ out = append(out, [2]int{i, -1})
+ }
+ return out
+}
+
+func mergePairedLedger(a, b model.PointLedger) model.PointLedger {
+ newer, older := a, b
+ if b.ID > a.ID {
+ newer, older = b, a
+ }
+ out := newer
+ out.Delta = absInt(newer.Delta)
+ if newer.Delta < 0 {
+ out.Delta = absInt(older.Delta)
+ }
+ debit, credit := a, b
+ if a.Delta > 0 {
+ debit, credit = b, a
+ }
+ if debit.Note != "" {
+ out.Note = debit.Note
+ } else {
+ out.Note = credit.Note
+ }
+ return out
+}
+
+func transferEnds(a, b model.PointLedger) (fromID, toID uint) {
+ if a.Delta < 0 {
+ return a.UserID, b.UserID
+ }
+ if b.Delta < 0 {
+ return b.UserID, a.UserID
+ }
+ if a.Delta > 0 {
+ return b.UserID, a.UserID
+ }
+ return a.UserID, b.UserID
+}
+
// Ledger 分页查询本人积分流水
func (s *PointsService) Ledger(userID uint, page, size int) ([]LedgerItem, int64, error) {
if page < 1 {
@@ -161,17 +325,17 @@ type DeltaDay struct {
// PointsEconomyStats 全站积分与类型帖概览(管理端)
type PointsEconomyStats struct {
- TotalBalance int64 `json:"total_balance"`
- LedgerCount int64 `json:"ledger_count"`
- CheckinToday int64 `json:"checkin_today"`
- OpenBounties int64 `json:"open_bounties"`
- EscrowedPoints int64 `json:"escrowed_points"`
- PostsByType map[string]int64 `json:"posts_by_type"`
- RecentLedger []LedgerItem `json:"recent_ledger"`
- CheckinSeries []DayCount `json:"checkin_series"`
- LedgerByReason []NamedCount `json:"ledger_by_reason"`
- DeltaSeries []DeltaDay `json:"delta_series"`
- NetDelta7d int64 `json:"net_delta_7d"`
+ TotalBalance int64 `json:"total_balance"`
+ LedgerCount int64 `json:"ledger_count"`
+ CheckinToday int64 `json:"checkin_today"`
+ OpenBounties int64 `json:"open_bounties"`
+ EscrowedPoints int64 `json:"escrowed_points"`
+ PostsByType map[string]int64 `json:"posts_by_type"`
+ RecentLedger []AdminLedgerItem `json:"recent_ledger"`
+ CheckinSeries []DayCount `json:"checkin_series"`
+ LedgerByReason []NamedCount `json:"ledger_by_reason"`
+ DeltaSeries []DeltaDay `json:"delta_series"`
+ NetDelta7d int64 `json:"net_delta_7d"`
}
// AdminEconomyStats 管理端经济看板
@@ -181,6 +345,7 @@ func (s *PointsService) AdminEconomyStats() (*PointsEconomyStats, error) {
CheckinSeries: make([]DayCount, 0),
LedgerByReason: make([]NamedCount, 0),
DeltaSeries: make([]DeltaDay, 0),
+ RecentLedger: make([]AdminLedgerItem, 0),
}
if err := s.db.Model(&model.User{}).Where("deleted_at IS NULL").
Select("COALESCE(SUM(points),0)").Scan(&st.TotalBalance).Error; err != nil {
@@ -228,17 +393,11 @@ func (s *PointsService) AdminEconomyStats() (*PointsEconomyStats, error) {
}
}
- var recent []model.PointLedger
- if err := s.db.Order("id DESC").Limit(15).Find(&recent).Error; err != nil {
+ recent, err := s.buildAdminRecentLedger()
+ if err != nil {
return nil, err
}
- st.RecentLedger = make([]LedgerItem, 0, len(recent))
- for _, r := range recent {
- st.RecentLedger = append(st.RecentLedger, LedgerItem{
- ID: r.ID, Delta: r.Delta, Balance: r.Balance, Reason: r.Reason,
- RefType: r.RefType, RefID: r.RefID, Note: r.Note, CreatedAt: r.CreatedAt,
- })
- }
+ st.RecentLedger = recent
now := time.Now()
day0 := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
@@ -313,3 +472,206 @@ func (s *PointsService) AdminEconomyStats() (*PointsEconomyStats, error) {
return st, nil
}
+
+func (s *PointsService) lookupLedgerMate(r model.PointLedger) (*model.PointLedger, error) {
+ comp := complementaryLedgerRef(r.Reason, r.RefType)
+ if comp == "" || r.RefID == 0 {
+ return nil, nil
+ }
+ amt := absInt(r.Delta)
+ if amt == 0 {
+ return nil, nil
+ }
+ lo, hi := uint(1), r.ID+uint(ledgerPairMaxDist)
+ if r.ID > uint(ledgerPairMaxDist) {
+ lo = r.ID - uint(ledgerPairMaxDist)
+ }
+ var mates []model.PointLedger
+ q := s.db.Where("reason = ? AND ref_id = ? AND ref_type = ? AND id BETWEEN ? AND ? AND id <> ?",
+ r.Reason, r.RefID, comp, lo, hi, r.ID)
+ if r.Delta > 0 {
+ q = q.Where("delta = ?", -amt)
+ } else {
+ q = q.Where("delta = ?", amt)
+ }
+ if err := q.Find(&mates).Error; err != nil {
+ return nil, err
+ }
+ best := -1
+ bestDist := ledgerPairMaxDist + 1
+ for i := range mates {
+ d := idDist(r.ID, mates[i].ID)
+ if d == 0 || d > ledgerPairMaxDist || d >= bestDist {
+ continue
+ }
+ bestDist = d
+ best = i
+ }
+ if best < 0 {
+ return nil, nil
+ }
+ return &mates[best], nil
+}
+
+func (s *PointsService) loadLedgerParties(ids []uint) (map[uint]LedgerParty, error) {
+ out := map[uint]LedgerParty{}
+ if len(ids) == 0 {
+ return out, nil
+ }
+ seen := map[uint]struct{}{}
+ uniq := make([]uint, 0, len(ids))
+ for _, id := range ids {
+ if id == 0 {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ uniq = append(uniq, id)
+ }
+ if len(uniq) == 0 {
+ return out, nil
+ }
+ var users []model.User
+ if err := s.db.Unscoped().Select("id", "username", "nickname", "avatar").
+ Where("id IN ?", uniq).Find(&users).Error; err != nil {
+ return nil, err
+ }
+ for _, u := range users {
+ out[u.ID] = ledgerPartyOf(u)
+ }
+ return out, nil
+}
+
+func (s *PointsService) bountyPayers(rows []model.PointLedger) (map[uint]uint, error) {
+ ids := make([]uint, 0)
+ seen := map[uint]struct{}{}
+ for _, r := range rows {
+ if r.Reason != model.PointReasonBountyAward || r.RefType != "post" || r.RefID == 0 {
+ continue
+ }
+ if _, ok := seen[r.RefID]; ok {
+ continue
+ }
+ seen[r.RefID] = struct{}{}
+ ids = append(ids, r.RefID)
+ }
+ out := map[uint]uint{}
+ if len(ids) == 0 {
+ return out, nil
+ }
+ var posts []model.Post
+ if err := s.db.Unscoped().Select("id", "user_id").Where("id IN ?", ids).Find(&posts).Error; err != nil {
+ return nil, err
+ }
+ for _, p := range posts {
+ out[p.ID] = p.UserID
+ }
+ return out, nil
+}
+
+func (s *PointsService) buildAdminRecentLedger() ([]AdminLedgerItem, error) {
+ out := make([]AdminLedgerItem, 0, adminRecentLedgerLimit)
+ var recent []model.PointLedger
+ if err := s.db.Order("id DESC").Limit(adminRecentLedgerFetch).Find(&recent).Error; err != nil {
+ return nil, err
+ }
+ if len(recent) == 0 {
+ return out, nil
+ }
+
+ pairs := pairLedgerIndices(recent)
+ outside := map[int]model.PointLedger{}
+ for _, p := range pairs {
+ if p[1] >= 0 {
+ continue
+ }
+ r := recent[p[0]]
+ if complementaryLedgerRef(r.Reason, r.RefType) == "" {
+ continue
+ }
+ mate, err := s.lookupLedgerMate(r)
+ if err != nil {
+ return nil, err
+ }
+ if mate != nil {
+ outside[p[0]] = *mate
+ }
+ }
+
+ payers, err := s.bountyPayers(recent)
+ if err != nil {
+ return nil, err
+ }
+
+ userIDs := make([]uint, 0, len(recent)+len(outside)+len(payers))
+ for _, r := range recent {
+ userIDs = append(userIDs, r.UserID)
+ }
+ for _, m := range outside {
+ userIDs = append(userIDs, m.UserID)
+ }
+ for _, uid := range payers {
+ userIDs = append(userIDs, uid)
+ }
+ parties, err := s.loadLedgerParties(userIDs)
+ if err != nil {
+ return nil, err
+ }
+ party := func(id uint) *LedgerParty {
+ if id == 0 {
+ return nil
+ }
+ if p, ok := parties[id]; ok {
+ return copyLedgerParty(p)
+ }
+ return &LedgerParty{ID: id, Nickname: "已注销"}
+ }
+
+ itemFrom := func(r model.PointLedger) AdminLedgerItem {
+ return AdminLedgerItem{
+ ID: r.ID, Delta: r.Delta, Balance: r.Balance, Reason: r.Reason,
+ RefType: r.RefType, RefID: r.RefID, Note: r.Note, CreatedAt: r.CreatedAt,
+ User: party(r.UserID),
+ }
+ }
+
+ for _, p := range pairs {
+ if len(out) >= adminRecentLedgerLimit {
+ break
+ }
+ a := recent[p[0]]
+ var mate *model.PointLedger
+ if p[1] >= 0 {
+ m := recent[p[1]]
+ mate = &m
+ } else if m, ok := outside[p[0]]; ok {
+ mate = &m
+ }
+
+ if mate != nil {
+ merged := mergePairedLedger(a, *mate)
+ fromID, toID := transferEnds(a, *mate)
+ item := itemFrom(merged)
+ item.Delta = absInt(merged.Delta)
+ item.User = nil
+ item.From = party(fromID)
+ item.To = party(toID)
+ out = append(out, item)
+ continue
+ }
+
+ item := itemFrom(a)
+ if a.Reason == model.PointReasonBountyAward && a.Delta > 0 {
+ if payerID := payers[a.RefID]; payerID > 0 && payerID != a.UserID {
+ item.From = party(payerID)
+ item.To = party(a.UserID)
+ item.User = nil
+ item.Delta = a.Delta
+ }
+ }
+ out = append(out, item)
+ }
+ return out, nil
+}
diff --git a/backend/service/points_ledger_test.go b/backend/service/points_ledger_test.go
new file mode 100644
index 0000000..5373320
--- /dev/null
+++ b/backend/service/points_ledger_test.go
@@ -0,0 +1,75 @@
+package service
+
+import (
+ "testing"
+
+ "github.com/freefire/jiang13-bbs/model"
+)
+
+func TestComplementaryLedgerRef(t *testing.T) {
+ if got := complementaryLedgerRef(model.PointReasonUnlockPost, "post"); got != "post_earn" {
+ t.Fatalf("unlock debit: got %q", got)
+ }
+ if got := complementaryLedgerRef(model.PointReasonUnlockPost, "post_earn"); got != "post" {
+ t.Fatalf("unlock credit: got %q", got)
+ }
+ if got := complementaryLedgerRef(model.PointReasonDownloadFile, "attachment"); got != "attachment_earn" {
+ t.Fatalf("download debit: got %q", got)
+ }
+ if got := complementaryLedgerRef(model.PointReasonCheckin, "checkin"); got != "" {
+ t.Fatalf("checkin should not pair, got %q", got)
+ }
+}
+
+func TestPairLedgerIndicesMergesUnlock(t *testing.T) {
+ rows := []model.PointLedger{
+ {ID: 4, UserID: 2, Delta: 10, Reason: model.PointReasonUnlockPost, RefType: "post_earn", RefID: 9},
+ {ID: 3, UserID: 1, Delta: -10, Reason: model.PointReasonUnlockPost, RefType: "post", RefID: 9},
+ {ID: 2, UserID: 3, Delta: 5, Reason: model.PointReasonCheckin, RefType: "checkin", RefID: 1},
+ }
+ pairs := pairLedgerIndices(rows)
+ if len(pairs) != 2 {
+ t.Fatalf("pairs=%d want 2: %#v", len(pairs), pairs)
+ }
+ if pairs[0] != [2]int{0, 1} {
+ t.Fatalf("first pair=%v want [0 1]", pairs[0])
+ }
+ if pairs[1] != [2]int{2, -1} {
+ t.Fatalf("checkin pair=%v want [2 -1]", pairs[1])
+ }
+}
+
+func TestPairLedgerIndicesNearestSamePost(t *testing.T) {
+ rows := []model.PointLedger{
+ {ID: 10, UserID: 2, Delta: 8, Reason: model.PointReasonUnlockPost, RefType: "post_earn", RefID: 1},
+ {ID: 9, UserID: 1, Delta: -8, Reason: model.PointReasonUnlockPost, RefType: "post", RefID: 1},
+ {ID: 8, UserID: 4, Delta: 8, Reason: model.PointReasonUnlockPost, RefType: "post_earn", RefID: 1},
+ {ID: 7, UserID: 3, Delta: -8, Reason: model.PointReasonUnlockPost, RefType: "post", RefID: 1},
+ }
+ pairs := pairLedgerIndices(rows)
+ if len(pairs) != 2 {
+ t.Fatalf("pairs=%d want 2: %#v", len(pairs), pairs)
+ }
+ if pairs[0] != [2]int{0, 1} || pairs[1] != [2]int{2, 3} {
+ t.Fatalf("pairs=%v", pairs)
+ }
+ from, to := transferEnds(rows[0], rows[1])
+ if from != 1 || to != 2 {
+ t.Fatalf("flow=%d→%d want 1→2", from, to)
+ }
+}
+
+func TestMergePairedLedgerKeepsNewer(t *testing.T) {
+ a := model.PointLedger{ID: 4, UserID: 2, Delta: 10, Note: "正文解锁收益", Reason: model.PointReasonUnlockPost}
+ b := model.PointLedger{ID: 3, UserID: 1, Delta: -10, Note: "解锁帖子:长标题", Reason: model.PointReasonUnlockPost}
+ got := mergePairedLedger(a, b)
+ if got.ID != 4 {
+ t.Fatalf("id=%d want 4", got.ID)
+ }
+ if got.Note != "解锁帖子:长标题" {
+ t.Fatalf("note=%q", got.Note)
+ }
+ if got.Delta != 10 {
+ t.Fatalf("delta=%d want 10", got.Delta)
+ }
+}
diff --git a/frontend/app/admin/economy/EconomyClient.tsx b/frontend/app/admin/economy/EconomyClient.tsx
index 44d24e3..ceec513 100644
--- a/frontend/app/admin/economy/EconomyClient.tsx
+++ b/frontend/app/admin/economy/EconomyClient.tsx
@@ -1,12 +1,14 @@
"use client";
+import { useMemo, useState } from "react";
import Link from "next/link";
-import { Coins, Flame, Gift, TrendingUp } from "lucide-react";
-import type { EconomyStats } from "@/lib/api";
+import { ArrowRight, Coins, Flame, Gift, TrendingUp } from "lucide-react";
+import type { EconomyStats, LedgerParty, PointLedgerItem } from "@/lib/api";
import { postTypeLabel } from "@/lib/postMeta";
import { pointReasonLabel } from "@/lib/points";
import { formatRelative } from "@/lib/format";
-import { AdminPageHeader, AdminStatCard } from "@/components/admin";
+import { AdminPageHeader, AdminSegmented, AdminStatCard } from "@/components/admin";
+import Avatar from "@/components/Avatar";
import {
AdminChartCard,
DeltaBarChart,
@@ -22,7 +24,223 @@ function netDeltaLabel(n: number) {
return fmt(n);
}
+type LedgerKind = "checkin" | "bounty" | "file" | "typed" | "system";
+type LedgerFilter =
+ | "all"
+ | "granted"
+ | "spent"
+ | "poll"
+ | "lottery"
+ | "discussion"
+ | "question";
+
+const LEDGER_FILTERS: { key: LedgerFilter; label: string }[] = [
+ { key: "all", label: "全部" },
+ { key: "granted", label: "发放" },
+ { key: "spent", label: "消耗" },
+ { key: "poll", label: "投票" },
+ { key: "lottery", label: "抽奖" },
+ { key: "discussion", label: "讨论" },
+ { key: "question", label: "问答" },
+];
+
+const EMPTY_LEDGER: PointLedgerItem[] = [];
+
+const NOTE_STRIP_EXTRAS = [
+ "解锁帖子",
+ "下载附件",
+ "附件收益",
+ "正文解锁收益",
+ "悬赏采纳",
+ "悬赏截止后采纳扣款",
+ "悬赏过期退回",
+ "历史签到积分回填",
+ "每日签到",
+];
+
+function partyName(u: LedgerParty) {
+ return u.nickname || u.username || `#${u.id}`;
+}
+
+function isToday(iso: string) {
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return false;
+ const n = new Date();
+ return (
+ d.getFullYear() === n.getFullYear() &&
+ d.getMonth() === n.getMonth() &&
+ d.getDate() === n.getDate()
+ );
+}
+
+function ledgerKind(reason: string): LedgerKind {
+ switch (reason) {
+ case "checkin":
+ case "migrate_checkin":
+ return "checkin";
+ case "bounty_escrow":
+ case "bounty_refund":
+ case "bounty_award":
+ return "bounty";
+ case "download_file":
+ return "file";
+ default: {
+ const blob = pointReasonLabel(reason);
+ if (/投票|抽奖|问答/.test(blob)) return "typed";
+ return "system";
+ }
+ }
+}
+
+function ledgerKindEmoji(kind: LedgerKind) {
+ if (kind === "checkin") return "📅";
+ if (kind === "bounty") return "🎯";
+ if (kind === "file") return "📎";
+ if (kind === "typed") return "🗳";
+ return "⚙";
+}
+
+function blobOf(it: PointLedgerItem) {
+ return `${pointReasonLabel(it.reason)} ${it.note || ""} ${it.reason}`;
+}
+
+function matchLedgerFilter(it: PointLedgerItem, filter: LedgerFilter) {
+ if (filter === "all") return true;
+ const delta = Number(it.delta) || 0;
+ const transfer = Boolean(it.from && it.to);
+ if (filter === "granted") return transfer || delta > 0;
+ if (filter === "spent") return transfer || delta < 0;
+ const blob = blobOf(it);
+ if (filter === "poll") return /投票/.test(blob);
+ if (filter === "lottery") return /抽奖/.test(blob);
+ if (filter === "question") return /问答/.test(blob);
+ if (filter === "discussion") return it.reason === "unlock_post" || /讨论/.test(blob);
+ return true;
+}
+
+/** 去掉与类型标签重复的前缀,避免「悬赏托管:悬赏托管:悬赏」 */
+function ledgerDesc(reason: string, note: string) {
+ const label = pointReasonLabel(reason);
+ let s = (note || "").trim();
+ const prefixes = [label, ...NOTE_STRIP_EXTRAS].filter(Boolean);
+ let guard = 0;
+ let changed = true;
+ while (changed && s && guard < 8) {
+ changed = false;
+ guard += 1;
+ for (const p of prefixes) {
+ if (s.startsWith(p)) {
+ s = s.slice(p.length).replace(/^[\s::·\-—]+/, "");
+ changed = true;
+ }
+ }
+ }
+ if (s === label) return "";
+ return s;
+}
+
+function todayDelta(items: PointLedgerItem[]) {
+ let granted = 0;
+ let spent = 0;
+ for (const it of items) {
+ if (!isToday(it.created_at)) continue;
+ const n = Math.abs(Number(it.delta) || 0);
+ if (n === 0) continue;
+ if (it.from && it.to) {
+ granted += n;
+ spent += n;
+ continue;
+ }
+ if ((it.delta || 0) > 0) granted += n;
+ else spent += n;
+ }
+ return { granted, spent };
+}
+
+function LedgerUserLink({ user, compact }: { user: LedgerParty; compact?: boolean }) {
+ const name = partyName(user);
+ return (
+
+
+ 最近流水 +
++ 今日 发放{" "} + + +{fmt(today.granted)} + + {" · "} + 消耗{" "} + + -{fmt(today.spent)} + +
+ {ledger.length === 0 ? (暂无流水
+ ) : filtered.length === 0 ? ( +该分类下暂无流水
) : (- {pointReasonLabel(it.reason)} - {it.note ? ` · ${it.note}` : ""} -
-{formatRelative(it.created_at)}
-暂无发帖
评论加载失败
网络异常,请稍后重试
@@ -89,13 +148,13 @@ export default function UserProfileContent({暂无评论
- @{u.username}
- ·
-
- {u.signature} -
- )} -
- {s.label}
- {s.foot && (
-
-
+
+ {data.stats.post_count}
+ {" "}帖子
+
+ {allowComments && (
+ <>
+ ·
+
+ {data.stats.comment_count}
+ {" "}评论
+
+ >
+ )}
+ ·
+
+ {data.stats.points}
+ {" "}积分
+
+ {data.stats.streak > 0 && (
+ <>
+ ·
+
+
+ {u.signature} +
+ )} +