Enhance ledger operations and referral lifecycle
CI / verify (push) Has been cancelled
CI / publish (push) Has been cancelled

Add traceable ledger filtering and permanent referral codes so operators can investigate credit activity without weakening immutable accounting guarantees.
This commit is contained in:
Rocky
2026-08-20 22:14:22 +08:00
parent 74c3fcd45f
commit b5212dcdc2
38 changed files with 3080 additions and 620 deletions
+77 -17
View File
@@ -5,8 +5,14 @@ import {
type LegacyColumnDef,
useLegacyTable,
} from "@tanstack/react-table/legacy";
import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react";
import { type ReactNode } from "react";
import {
ArrowDown,
ArrowUp,
ArrowUpDown,
ChevronDown,
ChevronRight,
} from "lucide-react";
import { Fragment, type ReactNode, useState } from "react";
import type { SortOrder } from "../api/types";
import { cn } from "../lib/utils";
import { EmptyState } from "./primitives";
@@ -23,6 +29,9 @@ export function DataTable<T extends RowData>({
sort,
sortableColumns,
onSortChange,
renderExpandedRow,
getRowId,
expandLabel = "详情",
}: {
data: T[];
columns: DataColumn<T>[];
@@ -33,7 +42,11 @@ export function DataTable<T extends RowData>({
sort?: { key: string; order: SortOrder };
sortableColumns?: Record<string, string>;
onSortChange?: (key: string, order: SortOrder) => void;
renderExpandedRow?: (row: T) => ReactNode;
getRowId?: (row: T) => string;
expandLabel?: string;
}) {
const [expandedRows, setExpandedRows] = useState<Set<string>>(() => new Set());
const table = useLegacyTable({
data,
columns,
@@ -94,25 +107,72 @@ export function DataTable<T extends RowData>({
</th>
);
})}
{renderExpandedRow ? (
<th
className="sticky top-0 z-[1] border-b border-border bg-surface/95 px-5 py-3.5 text-left text-[11px] font-bold uppercase tracking-[0.08em] text-muted backdrop-blur"
scope="col"
>
{expandLabel}
</th>
) : null}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr
key={row.id}
className="group transition-colors hover:bg-surface-muted/70"
>
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
className="border-b border-border/70 px-5 py-4 align-middle text-foreground last:text-right group-last:border-b-0"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
{table.getRowModel().rows.map((row) => {
const expansionKey = getRowId?.(row.original) ?? row.id;
const expanded = expandedRows.has(expansionKey);
const panelId = `expanded-row-${expansionKey.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
return (
<Fragment key={row.id}>
<tr className="group transition-colors hover:bg-surface-muted/70">
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
className="border-b border-border/70 px-5 py-4 align-middle text-foreground last:text-right group-last:border-b-0"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
{renderExpandedRow ? (
<td className="border-b border-border/70 px-5 py-4 text-right align-middle">
<button
className="inline-flex min-h-8 items-center gap-1.5 rounded-lg px-2 text-xs font-semibold text-primary transition hover:bg-primary-soft focus-visible:ring-4 focus-visible:ring-primary/15"
type="button"
aria-expanded={expanded}
aria-controls={panelId}
onClick={() =>
setExpandedRows((current) => {
const next = new Set(current);
if (next.has(expansionKey)) next.delete(expansionKey);
else next.add(expansionKey);
return next;
})
}
>
{expanded ? (
<ChevronDown className="size-3.5" aria-hidden />
) : (
<ChevronRight className="size-3.5" aria-hidden />
)}
{expanded ? "收起" : "展开"}
</button>
</td>
) : null}
</tr>
{renderExpandedRow && expanded ? (
<tr id={panelId}>
<td
className="border-b border-border bg-surface-muted/45 px-5 py-4"
colSpan={row.getVisibleCells().length + 1}
>
{renderExpandedRow(row.original)}
</td>
</tr>
) : null}
</Fragment>
);
})}
</tbody>
</table>
</div>
+3 -1
View File
@@ -5,10 +5,12 @@ export function TableToolbar({
children,
active,
onClear,
clearLabel = "清除筛选",
}: {
children: ReactNode;
active: boolean;
onClear: () => void;
clearLabel?: string;
}) {
return (
<div
@@ -24,7 +26,7 @@ export function TableToolbar({
disabled={!active}
onClick={onClear}
>
{clearLabel}
</Button>
</div>
);