"use client"; import { useState } from "react"; import { Table, TablePaginationConfig, Select } from "antd"; import type { FilterValue } from "antd/es/table/interface"; import type { SorterResult } from "antd/es/table/interface"; import dayjs from "@/lib/dayjs"; import { useTranslation } from "react-i18next"; interface UsageRecord { id: number; nickname: string; use_time: string; model_name: string; input_tokens: number; output_tokens: number; cost: number; balance_after: number; } interface TableParams { pagination: TablePaginationConfig; sortField?: string; sortOrder?: string; filters?: Record; } interface Props { loading: boolean; records: UsageRecord[]; tableParams: TableParams; models: { model_name: string }[]; users: { nickname: string }[]; onTableChange: ( pagination: TablePaginationConfig, filters: Record, sorter: SorterResult | SorterResult[] ) => void; } const MobileCard = ({ record, t, }: { record: UsageRecord; t: (key: string) => string; }) => { return (
{record.nickname}
{dayjs(record.use_time).format("YYYY-MM-DD HH:mm:ss")}
¥{Number(record.cost).toFixed(4)}
{t("panel.usageDetails.table.balance")}: ¥ {Number(record.balance_after).toFixed(4)}
{t("panel.usageDetails.table.model")}
{record.model_name}
Tokens
{(record.input_tokens + record.output_tokens).toLocaleString()}
); }; export default function UsageRecordsTable({ loading, records, tableParams, models, users, onTableChange, }: Props) { const { t } = useTranslation("common"); const [filters, setFilters] = useState>( tableParams.filters || {} ); const handleFilterChange = (field: string, value: string[] | null) => { const newFilters = { ...filters, [field]: value, }; setFilters(newFilters); onTableChange(tableParams.pagination, newFilters, {}); }; const columns = [ { title: t("panel.usageDetails.table.user"), dataIndex: "nickname", key: "nickname", width: 120, filters: users.map((user) => ({ text: user.nickname, value: user.nickname, })), filterMode: "menu" as const, filtered: filters.nickname ? filters.nickname.length > 0 : false, filteredValue: filters.nickname || null, }, { title: t("panel.usageDetails.table.time"), dataIndex: "use_time", key: "use_time", width: 180, sorter: true, render: (time: string) => dayjs(time).format("YYYY-MM-DD HH:mm:ss"), }, { title: t("panel.usageDetails.table.model"), dataIndex: "model_name", key: "model_name", width: 150, filters: models.map((model) => ({ text: model.model_name, value: model.model_name, })), filterMode: "menu" as const, filtered: filters.model_name ? filters.model_name.length > 0 : false, filteredValue: filters.model_name || null, }, { title: t("panel.usageDetails.table.tokens"), key: "tokens", width: 120, sorter: true, render: (_: unknown, record: UsageRecord) => (record.input_tokens + record.output_tokens).toLocaleString(), }, { title: t("panel.usageDetails.table.cost"), dataIndex: "cost", key: "cost", width: 100, sorter: true, render: (_: unknown, record: UsageRecord) => `${t("common.currency")}${Number(record.cost).toFixed(4)}`, }, { title: t("panel.usageDetails.table.balance"), dataIndex: "balance_after", key: "balance_after", width: 100, sorter: true, render: (_: unknown, record: UsageRecord) => `${t("common.currency")}${Number(record.balance_after).toFixed(2)}`, }, ]; return (
handleFilterChange("model_name", value)} options={models.map((model) => ({ label: model.model_name, value: model.model_name, }))} maxTagCount="responsive" />
`${t("common.total")} ${total}`, itemRender: (page, type, originalElement) => { if (type === "prev") { return ( ); } if (type === "next") { return ( ); } return originalElement; }, }} rowKey="id" scroll={{ x: 800 }} className="bg-background rounded-md border [&_.ant-table-thead]:bg-muted [&_.ant-table-thead>tr>th]:bg-transparent [&_.ant-table-thead>tr>th]:text-muted-foreground [&_.ant-table-tbody>tr>td]:border-muted [&_.ant-table-tbody>tr:last-child>td]:border-b-0 [&_.ant-table-tbody>tr:hover>td]:bg-muted/50 [&_.ant-pagination]:flex [&_.ant-pagination]:items-center [&_.ant-pagination]:px-2 [&_.ant-pagination]:py-4 [&_.ant-pagination]:border-t [&_.ant-pagination]:border-muted [&_.ant-pagination-item]:border-muted [&_.ant-pagination-item]:bg-transparent [&_.ant-pagination-item]:hover:border-primary [&_.ant-pagination-item]:hover:text-primary [&_.ant-pagination-item-active]:border-primary [&_.ant-pagination-item-active]:text-primary [&_.ant-pagination-item-active]:bg-transparent [&_.ant-pagination-prev]:hover:text-primary [&_.ant-pagination-next]:hover:text-primary [&_.ant-pagination-prev>button]:hover:border-primary [&_.ant-pagination-next>button]:hover:border-primary [&_.ant-pagination-options]:ml-auto [&_.ant-select]:border-muted [&_.ant-select]:hover:border-primary [&_.ant-select-focused]:border-primary" />
{loading ? (
) : ( <>
{records.map((record) => ( ))}
)} ); }