Files
Genarrative/packages/shared/src/components/PlatformProfileWalletLedgerModal/index.tsx
T
k88936 125927e0e8 Ai game creator app home sidebar (#86)
![shotmd-1784186268.jpg](/attachments/c828773c-4f1a-4654-b76d-c944b82ec023)
![shotmd-1784186877.jpg](/attachments/f1782ba3-245a-40c1-8caa-cbd7e4cce74b)
![shotmd-1784186948-compressed.webp](/attachments/f3d02ac3-db7a-439b-9a84-7aceefad9048)
TODO: 补充说明书链接

Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/86
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-07-20 15:03:23 +08:00

131 lines
4.7 KiB
TypeScript

import './index.css';
import { Coins, X } from 'lucide-react';
import { useId } from 'react';
import type { ProfileWalletLedgerResponse } from '../../contracts/runtime';
import { buildWalletLedgerPresentation } from './model';
export type PlatformProfileWalletLedgerModalProps = {
ledger: ProfileWalletLedgerResponse | null;
fallbackBalance: number;
isLoading: boolean;
error: string | null;
onClose: () => void;
onRetry: () => void;
};
export function PlatformProfileWalletLedgerModal({
ledger,
fallbackBalance,
isLoading,
error,
onClose,
onRetry,
}: PlatformProfileWalletLedgerModalProps) {
const titleId = useId();
const presentation = buildWalletLedgerPresentation(ledger, fallbackBalance);
return (
<div className="platform-profile-wallet-ledger-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-3 py-5">
<section
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className="platform-profile-wallet-ledger-modal relative max-h-[min(92vh,42rem)] w-full max-w-[30rem] overflow-y-auto rounded-[1.35rem] px-4 pb-5 pt-4 sm:px-5"
>
<button
type="button"
aria-label="关闭泥点账单"
title="关闭泥点账单"
onClick={onClose}
className="platform-profile-wallet-ledger-modal__close absolute right-3 top-3 z-10 flex h-8 w-8 items-center justify-center rounded-full"
>
<X className="h-4 w-4" aria-hidden="true" />
</button>
<header className="pr-10">
<div className="text-[10px] font-black uppercase text-rose-500">
LEDGER
</div>
<div className="mt-1 flex flex-wrap items-center gap-2.5">
<h2 id={titleId} className="text-xl font-black text-zinc-950">
泥点账单
</h2>
<span className="platform-profile-wallet-ledger-modal__badge inline-flex items-center gap-1.5 rounded-full border border-rose-100 bg-white/70 px-2.5 py-1 text-[11px] font-black">
<Coins
className="h-3.5 w-3.5 text-[#ff4056]"
aria-hidden="true"
/>
{presentation.balanceLabel}
</span>
</div>
</header>
{error ? (
<div
role="alert"
className="platform-profile-wallet-ledger-modal__status-error mt-4 rounded-xl px-3 py-3 text-sm font-semibold"
>
<div>{error}</div>
<button
type="button"
className="platform-profile-wallet-ledger-modal__retry mt-3 rounded-full px-3 py-1.5 text-xs font-black"
onClick={onRetry}
>
重新加载
</button>
</div>
) : isLoading ? (
<div
role="status"
aria-label="泥点账单加载中"
className="mt-5 space-y-3"
>
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className="h-16 animate-pulse rounded-xl bg-zinc-100"
/>
))}
</div>
) : presentation.entries.length === 0 ? (
<div className="platform-empty-state mt-5 rounded-xl border border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)] px-4 py-8 text-center text-sm font-semibold text-[var(--platform-text-soft)]">
暂无账单记录
</div>
) : (
<div className="mt-5 space-y-2.5">
{presentation.entries.map((entry) => (
<div
key={entry.id}
className="platform-profile-wallet-ledger-modal__row flex items-center justify-between gap-3 rounded-lg border border-[var(--platform-subpanel-border)] px-3 py-3 shadow-sm"
>
<div className="min-w-0">
<div className="truncate text-sm font-black text-zinc-900">
{entry.sourceLabel}
</div>
<div className="mt-1 text-xs font-semibold text-zinc-500">
{entry.createdAtLabel}
</div>
</div>
<div className="shrink-0 text-right">
<div
className={`text-base font-black ${
entry.isIncome ? 'text-emerald-600' : 'text-rose-500'
}`}
>
{entry.amountLabel}
</div>
<div className="mt-1 text-[11px] font-semibold text-zinc-400">
{entry.balanceLabel}
</div>
</div>
</div>
))}
</div>
)}
</section>
</div>
);
}