2026-06-02 12:38:01 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ArrowLeftOutlined,
|
|
|
|
|
|
BarChartOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
LeftOutlined,
|
|
|
|
|
|
LineChartOutlined,
|
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
RightOutlined,
|
|
|
|
|
|
TeamOutlined,
|
|
|
|
|
|
UserOutlined,
|
|
|
|
|
|
WarningOutlined,
|
|
|
|
|
|
} from "@ant-design/icons";
|
|
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
|
|
|
import type {
|
|
|
|
|
|
WebEnterpriseUsageMember,
|
|
|
|
|
|
WebEnterpriseUsageRecord,
|
|
|
|
|
|
WebEnterpriseUsageSummary,
|
|
|
|
|
|
WebImageWorkbenchTool,
|
|
|
|
|
|
WebUsageSummary,
|
|
|
|
|
|
WebUserSession,
|
|
|
|
|
|
WebViewKey,
|
|
|
|
|
|
} from "../../types";
|
|
|
|
|
|
|
|
|
|
|
|
interface TokenUsagePageProps {
|
|
|
|
|
|
session: WebUserSession | null;
|
|
|
|
|
|
usage: WebUsageSummary;
|
|
|
|
|
|
loadEnterpriseUsage?: () => Promise<WebEnterpriseUsageSummary>;
|
|
|
|
|
|
loadPersonalUsage?: () => Promise<WebEnterpriseUsageSummary>;
|
|
|
|
|
|
onOpenMore?: () => void;
|
|
|
|
|
|
onOpenImageTool?: (tool: WebImageWorkbenchTool) => void;
|
|
|
|
|
|
onSelectView?: (view: WebViewKey) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatCredits(cents: number): string {
|
|
|
|
|
|
const value = Math.max(0, cents) / 100;
|
|
|
|
|
|
return value >= 1000 ? `${Math.round(value).toLocaleString()} 积分` : `${value.toFixed(2).replace(/\.00$/, "")} 积分`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDateTime(value?: string | null): string {
|
|
|
|
|
|
if (!value) return "-";
|
|
|
|
|
|
const date = new Date(value);
|
|
|
|
|
|
if (Number.isNaN(date.getTime())) return value;
|
|
|
|
|
|
return date.toLocaleString("zh-CN", {
|
|
|
|
|
|
month: "2-digit",
|
|
|
|
|
|
day: "2-digit",
|
|
|
|
|
|
hour: "2-digit",
|
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getInitials(value: string): string {
|
|
|
|
|
|
const normalized = value.trim();
|
|
|
|
|
|
if (!normalized) return "U";
|
|
|
|
|
|
return normalized.slice(0, 2).toUpperCase();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getEnterpriseBalanceCents(
|
|
|
|
|
|
session: WebUserSession | null,
|
|
|
|
|
|
usage: WebUsageSummary,
|
|
|
|
|
|
enterpriseUsage: WebEnterpriseUsageSummary | null,
|
|
|
|
|
|
) {
|
|
|
|
|
|
if (enterpriseUsage) return enterpriseUsage.balanceCents;
|
|
|
|
|
|
if (typeof usage.enterpriseBalanceCents === "number") return usage.enterpriseBalanceCents;
|
|
|
|
|
|
return session?.user.enterpriseBalanceCents ?? usage.balanceCents;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function memberDisplayName(member: WebEnterpriseUsageMember): string {
|
|
|
|
|
|
return member.displayName || member.username || String(member.userId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function recordDuration(record: WebEnterpriseUsageRecord): string {
|
|
|
|
|
|
return record.durationSeconds ? `${record.durationSeconds}s` : record.resolution || "-";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const LOW_BALANCE_THRESHOLD_CENTS = 2000;
|
|
|
|
|
|
|
|
|
|
|
|
function formatDayLabel(value: string): string {
|
|
|
|
|
|
const date = new Date(value);
|
|
|
|
|
|
if (Number.isNaN(date.getTime())) return value;
|
|
|
|
|
|
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type TrendPoint = { date: string; usedCents: number; taskCount: number };
|
|
|
|
|
|
|
|
|
|
|
|
function UsageTrendChart({ data }: { data: TrendPoint[] }) {
|
2026-06-03 01:39:06 +08:00
|
|
|
|
const W = 680;
|
|
|
|
|
|
const H = 200;
|
|
|
|
|
|
const padX = 32;
|
|
|
|
|
|
const padY = 24;
|
2026-06-02 12:38:01 +08:00
|
|
|
|
const maxCents = Math.max(1, ...data.map((d) => d.usedCents));
|
|
|
|
|
|
const stepX = data.length > 1 ? (W - padX * 2) / (data.length - 1) : 0;
|
|
|
|
|
|
const yOf = (cents: number) => H - padY - (cents / maxCents) * (H - padY * 2);
|
|
|
|
|
|
const xOf = (i: number) => padX + i * stepX;
|
|
|
|
|
|
const points = data.map((d, i) => ({ x: xOf(i), y: yOf(d.usedCents), ...d }));
|
|
|
|
|
|
const linePath = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
|
|
|
|
|
|
const areaPath = points.length
|
|
|
|
|
|
? `${linePath} L${points[points.length - 1].x.toFixed(1)},${H - padY} L${points[0].x.toFixed(1)},${H - padY} Z`
|
|
|
|
|
|
: "";
|
|
|
|
|
|
const totalCents = data.reduce((sum, d) => sum + d.usedCents, 0);
|
|
|
|
|
|
const peak = points.reduce((best, p) => (p.usedCents > best.usedCents ? p : best), points[0] || { usedCents: 0, x: 0, y: 0 });
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="usage-trend">
|
|
|
|
|
|
<svg viewBox={`0 0 ${W} ${H}`} className="usage-trend__svg" role="img" aria-label="近 7 天积分消耗趋势">
|
|
|
|
|
|
<defs>
|
|
|
|
|
|
<linearGradient id="usageTrendFill" x1="0" y1="0" x2="0" y2="1">
|
|
|
|
|
|
<stop offset="0%" stopColor="var(--accent)" stopOpacity="0.28" />
|
|
|
|
|
|
<stop offset="100%" stopColor="var(--accent)" stopOpacity="0" />
|
|
|
|
|
|
</linearGradient>
|
|
|
|
|
|
</defs>
|
|
|
|
|
|
{[0.25, 0.5, 0.75, 1].map((f) => (
|
|
|
|
|
|
<line key={f} x1={padX} y1={yOf(maxCents * f)} x2={W - padX} y2={yOf(maxCents * f)} className="usage-trend__grid" />
|
|
|
|
|
|
))}
|
|
|
|
|
|
{areaPath ? <path d={areaPath} fill="url(#usageTrendFill)" /> : null}
|
|
|
|
|
|
{linePath ? <path d={linePath} className="usage-trend__line" /> : null}
|
|
|
|
|
|
{points.map((p) => (
|
|
|
|
|
|
<circle key={p.date} cx={p.x} cy={p.y} r={3.5} className="usage-trend__dot">
|
|
|
|
|
|
<title>{`${formatDayLabel(p.date)} · ${formatCredits(p.usedCents)} · ${p.taskCount} 次`}</title>
|
|
|
|
|
|
</circle>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{points.map((p, i) => (
|
|
|
|
|
|
<text key={`lbl-${p.date}`} x={p.x} y={H - 6} className="usage-trend__xlabel" textAnchor={i === 0 ? "start" : i === points.length - 1 ? "end" : "middle"}>
|
|
|
|
|
|
{formatDayLabel(p.date)}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<div className="usage-trend__meta">
|
|
|
|
|
|
<span>近 7 天共 <b>{formatCredits(totalCents)}</b></span>
|
|
|
|
|
|
{peak.usedCents > 0 ? <span>峰值 {formatDayLabel(peak.date)} · {formatCredits(peak.usedCents)}</span> : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function TokenUsagePage({
|
|
|
|
|
|
session,
|
|
|
|
|
|
usage,
|
|
|
|
|
|
loadEnterpriseUsage,
|
|
|
|
|
|
loadPersonalUsage,
|
|
|
|
|
|
onOpenMore,
|
|
|
|
|
|
onOpenImageTool,
|
|
|
|
|
|
onSelectView,
|
|
|
|
|
|
}: TokenUsagePageProps) {
|
|
|
|
|
|
const [enterpriseUsage, setEnterpriseUsage] = useState<WebEnterpriseUsageSummary | null>(null);
|
|
|
|
|
|
const [enterpriseUsageLoading, setEnterpriseUsageLoading] = useState(false);
|
|
|
|
|
|
const [enterpriseUsageError, setEnterpriseUsageError] = useState<string | null>(null);
|
|
|
|
|
|
const isEnterpriseAdmin = session?.user.enterpriseRole === "admin";
|
|
|
|
|
|
const isEnterpriseAccount = Boolean(session?.user.enterpriseId || session?.user.accountType === "enterprise");
|
|
|
|
|
|
|
|
|
|
|
|
const refreshEnterpriseUsage = useCallback(async () => {
|
2026-06-03 20:19:07 +08:00
|
|
|
|
if (!session) return;
|
2026-06-02 12:38:01 +08:00
|
|
|
|
const loader = isEnterpriseAdmin ? loadEnterpriseUsage : loadPersonalUsage;
|
|
|
|
|
|
if (!loader) {
|
|
|
|
|
|
setEnterpriseUsage(null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setEnterpriseUsageLoading(true);
|
|
|
|
|
|
setEnterpriseUsageError(null);
|
|
|
|
|
|
try {
|
|
|
|
|
|
setEnterpriseUsage(await loader());
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setEnterpriseUsage(null);
|
2026-06-03 20:33:28 +08:00
|
|
|
|
setEnterpriseUsageError(error instanceof Error ? error.message : "加载失败");
|
2026-06-02 12:38:01 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setEnterpriseUsageLoading(false);
|
|
|
|
|
|
}
|
2026-06-03 20:19:07 +08:00
|
|
|
|
}, [session, isEnterpriseAdmin, loadEnterpriseUsage, loadPersonalUsage]);
|
2026-06-02 12:38:01 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
void refreshEnterpriseUsage();
|
|
|
|
|
|
}, [refreshEnterpriseUsage]);
|
|
|
|
|
|
|
|
|
|
|
|
const enterpriseBalanceCents = getEnterpriseBalanceCents(session, usage, enterpriseUsage);
|
|
|
|
|
|
const totalCalls = enterpriseUsage
|
|
|
|
|
|
? enterpriseUsage.members.reduce((sum, member) => sum + member.taskCount, 0)
|
|
|
|
|
|
: usage.imageUsed + usage.videoUsed + usage.textUsed;
|
|
|
|
|
|
const totalUsedCents = enterpriseUsage?.totalUsedCents ?? 0;
|
|
|
|
|
|
const modelBreakdown = enterpriseUsage?.modelBreakdown ?? [];
|
|
|
|
|
|
const dailyTrend = enterpriseUsage?.dailyTrend ?? [];
|
|
|
|
|
|
const availableBalanceCents = isEnterpriseAccount ? enterpriseBalanceCents : usage.balanceCents;
|
|
|
|
|
|
const isLowBalance = !usage.betaUnlimited && availableBalanceCents < LOW_BALANCE_THRESHOLD_CENTS;
|
|
|
|
|
|
const maxModelCents = Math.max(1, ...modelBreakdown.map((m) => m.usedCents));
|
|
|
|
|
|
const records = enterpriseUsage?.records ?? [];
|
|
|
|
|
|
const [recordPage, setRecordPage] = useState(0);
|
|
|
|
|
|
const pageSize = 10;
|
|
|
|
|
|
const totalPages = Math.max(1, Math.ceil(records.length / pageSize));
|
|
|
|
|
|
const pagedRecords = records.slice(recordPage * pageSize, (recordPage + 1) * pageSize);
|
|
|
|
|
|
const members = useMemo<WebEnterpriseUsageMember[]>(() => {
|
|
|
|
|
|
if (enterpriseUsage && enterpriseUsage.members.length) return enterpriseUsage.members;
|
|
|
|
|
|
if (!session) return [];
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
userId: session.user.id,
|
|
|
|
|
|
username: session.user.username,
|
|
|
|
|
|
displayName: session.user.displayName,
|
|
|
|
|
|
role: session.user.enterpriseRole || session.user.role || "personal",
|
|
|
|
|
|
usedCents: 0,
|
|
|
|
|
|
taskCount: totalCalls,
|
|
|
|
|
|
lastUsedAt: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
}, [enterpriseUsage, session, totalCalls]);
|
|
|
|
|
|
|
|
|
|
|
|
const metricCards = [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "balance",
|
|
|
|
|
|
label: "可用余额",
|
|
|
|
|
|
value: usage.betaUnlimited ? "不限量" : formatCredits(availableBalanceCents),
|
|
|
|
|
|
hint: isLowBalance ? "余额偏低,建议充值" : isEnterpriseAccount ? "企业共享额度" : "个人账户额度",
|
|
|
|
|
|
tone: isLowBalance ? "warn" : "accent",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "used",
|
|
|
|
|
|
label: "已用积分",
|
|
|
|
|
|
value: formatCredits(totalUsedCents),
|
|
|
|
|
|
hint: enterpriseUsage ? `${enterpriseUsage.enterpriseName || "企业"}累计` : "累计消耗",
|
|
|
|
|
|
tone: "neutral",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: "calls",
|
|
|
|
|
|
label: "调用次数",
|
|
|
|
|
|
value: `${totalCalls}`,
|
|
|
|
|
|
hint: `${modelBreakdown.length || 0} 个模型`,
|
|
|
|
|
|
tone: "neutral",
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const systemStatus = [
|
|
|
|
|
|
{ label: "账户类型", value: isEnterpriseAccount ? "企业账户" : "个人账户", tone: "good" },
|
|
|
|
|
|
{ label: "企业空间", value: enterpriseUsage?.enterpriseName || session?.user.enterpriseName || "-" },
|
|
|
|
|
|
];
|
2026-06-03 18:52:14 +08:00
|
|
|
|
const pageStatusClass = enterpriseUsageLoading
|
|
|
|
|
|
? "is-syncing"
|
|
|
|
|
|
: enterpriseUsageError
|
|
|
|
|
|
? "has-sync-error"
|
|
|
|
|
|
: isLowBalance
|
|
|
|
|
|
? "has-low-balance"
|
|
|
|
|
|
: "is-healthy";
|
2026-06-02 12:38:01 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-06-03 18:52:14 +08:00
|
|
|
|
<section className={`script-token-page token-usage-page management-center-page ${pageStatusClass}`} aria-label="管理中心">
|
2026-06-02 12:38:01 +08:00
|
|
|
|
<main className="management-center-shell">
|
|
|
|
|
|
<header className="management-center-toolbar" aria-label="管理中心操作">
|
|
|
|
|
|
<div className="management-center-toolbar__title">
|
|
|
|
|
|
<button type="button" className="management-center-toolbar__back" aria-label="返回工具盒" onClick={onOpenMore}>
|
|
|
|
|
|
<ArrowLeftOutlined />
|
|
|
|
|
|
</button>
|
2026-06-03 18:52:14 +08:00
|
|
|
|
<span>
|
|
|
|
|
|
<strong>管理中心</strong>
|
|
|
|
|
|
<small>用量、成员与模型调用监控</small>
|
|
|
|
|
|
</span>
|
2026-06-02 12:38:01 +08:00
|
|
|
|
</div>
|
2026-06-03 18:52:14 +08:00
|
|
|
|
<span className={`management-center-status-pill ${enterpriseUsageError ? "is-error" : enterpriseUsageLoading ? "is-loading" : "is-online"}`}>
|
2026-06-02 12:38:01 +08:00
|
|
|
|
{enterpriseUsageLoading ? "正在同步企业用量" : enterpriseUsageError || "服务器已连接"}
|
|
|
|
|
|
</span>
|
2026-06-03 18:52:14 +08:00
|
|
|
|
<button type="button" onClick={refreshEnterpriseUsage} disabled={enterpriseUsageLoading}>
|
2026-06-02 12:38:01 +08:00
|
|
|
|
<ReloadOutlined />
|
|
|
|
|
|
刷新数据
|
|
|
|
|
|
</button>
|
2026-06-03 18:52:14 +08:00
|
|
|
|
<button type="button" className="is-muted-action">
|
2026-06-02 12:38:01 +08:00
|
|
|
|
<UserOutlined />
|
|
|
|
|
|
成员管理
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
{isLowBalance ? (
|
|
|
|
|
|
<div className="management-balance-alert" role="alert">
|
|
|
|
|
|
<WarningOutlined />
|
|
|
|
|
|
<span>当前余额 {formatCredits(availableBalanceCents)},可能不足以完成下一次生成,请及时充值。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
<section className="management-metric-cards" aria-label="关键指标">
|
2026-06-03 18:52:14 +08:00
|
|
|
|
{metricCards.map((card, index) => (
|
2026-06-02 12:38:01 +08:00
|
|
|
|
<article key={card.key} className={`management-metric-card is-${card.tone}`}>
|
2026-06-03 18:52:14 +08:00
|
|
|
|
<span className="management-metric-card__index">{String(index + 1).padStart(2, "0")}</span>
|
2026-06-02 12:38:01 +08:00
|
|
|
|
<span className="management-metric-card__label">{card.label}</span>
|
|
|
|
|
|
<strong className="management-metric-card__value">{card.value}</strong>
|
|
|
|
|
|
<span className="management-metric-card__hint">{card.hint}</span>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="management-center-overview" aria-label="系统概览">
|
|
|
|
|
|
<article className="management-card management-card--chart">
|
|
|
|
|
|
<div className="management-card__head">
|
|
|
|
|
|
<h2>
|
|
|
|
|
|
<BarChartOutlined />
|
|
|
|
|
|
模型消耗分布
|
|
|
|
|
|
</h2>
|
2026-06-03 18:52:14 +08:00
|
|
|
|
<span>{enterpriseUsageLoading ? "SYNC" : modelBreakdown.length ? `${modelBreakdown.length} 个模型` : "LIVE"}</span>
|
2026-06-02 12:38:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
{modelBreakdown.length ? (
|
|
|
|
|
|
<div className="management-model-list">
|
|
|
|
|
|
{modelBreakdown.map((item) => (
|
|
|
|
|
|
<div className="management-model-bar" key={item.model}>
|
|
|
|
|
|
<div className="management-model-bar__top">
|
|
|
|
|
|
<strong title={item.model}>{item.model}</strong>
|
|
|
|
|
|
<em>{formatCredits(item.usedCents)}</em>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="management-model-bar__track">
|
|
|
|
|
|
<span style={{ width: `${Math.max(4, Math.round((item.usedCents / maxModelCents) * 100))}%` }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="management-model-bar__sub">{item.taskCount} 次调用 · {Math.round((item.usedCents / Math.max(1, totalUsedCents)) * 100)}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="management-empty-chart">
|
|
|
|
|
|
<BarChartOutlined />
|
|
|
|
|
|
<span>暂无模型用量数据</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
|
|
<article className="management-card management-status-card">
|
|
|
|
|
|
<div className="management-card__head">
|
2026-06-03 18:52:14 +08:00
|
|
|
|
<h2>
|
|
|
|
|
|
<LineChartOutlined />
|
|
|
|
|
|
系统状态
|
|
|
|
|
|
</h2>
|
2026-06-02 12:38:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<dl>
|
|
|
|
|
|
{systemStatus.map((item) => (
|
|
|
|
|
|
<div key={item.label}>
|
|
|
|
|
|
<dt>{item.label}</dt>
|
|
|
|
|
|
<dd className={item.tone === "good" ? "is-good" : undefined}>
|
|
|
|
|
|
{item.tone === "good" ? <i /> : null}
|
|
|
|
|
|
{item.value}
|
|
|
|
|
|
</dd>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
<div className="management-status-trend">
|
|
|
|
|
|
<span className="management-status-trend__title">近 7 天趋势</span>
|
|
|
|
|
|
{dailyTrend.some((d) => d.usedCents > 0) ? (
|
|
|
|
|
|
<UsageTrendChart data={dailyTrend} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="management-status-trend__empty">暂无数据</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="management-card management-members">
|
|
|
|
|
|
<div className="management-card__head">
|
|
|
|
|
|
<h2>
|
|
|
|
|
|
<TeamOutlined />
|
|
|
|
|
|
团队成员 ({members.length})
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<button type="button">{isEnterpriseAdmin ? "企业管理员" : "当前账号"}</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="management-member-list">
|
|
|
|
|
|
{members.map((member) => (
|
|
|
|
|
|
<article key={String(member.userId)} className="management-member-row">
|
|
|
|
|
|
<span className="management-member-avatar">{getInitials(memberDisplayName(member))}</span>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>{memberDisplayName(member)}</strong>
|
|
|
|
|
|
<em>uid: {member.userId}</em>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="management-member-role">{member.role === "admin" ? "管理员" : member.role === "employee" ? "员工" : member.role}</span>
|
|
|
|
|
|
<span>{formatCredits(member.usedCents)}</span>
|
|
|
|
|
|
<span className="management-member-meter">
|
|
|
|
|
|
<b>{member.taskCount} 调用</b>
|
|
|
|
|
|
<b>{formatDateTime(member.lastUsedAt)}</b>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<CheckCircleOutlined />
|
|
|
|
|
|
</article>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="management-card management-records">
|
|
|
|
|
|
<div className="management-card__head">
|
2026-06-03 18:52:14 +08:00
|
|
|
|
<h2>
|
|
|
|
|
|
<BarChartOutlined />
|
|
|
|
|
|
调用记录
|
|
|
|
|
|
</h2>
|
2026-06-02 12:38:01 +08:00
|
|
|
|
<span>{records.length} 条记录</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="management-record-table" role="table" aria-label="调用记录">
|
|
|
|
|
|
<div role="row" className="management-record-table__head">
|
|
|
|
|
|
<span role="columnheader">时间</span>
|
|
|
|
|
|
<span role="columnheader">用户</span>
|
|
|
|
|
|
<span role="columnheader">模型</span>
|
|
|
|
|
|
<span role="columnheader">提示词</span>
|
|
|
|
|
|
<span role="columnheader">状态</span>
|
|
|
|
|
|
<span role="columnheader">耗时</span>
|
|
|
|
|
|
<span role="columnheader">积分</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{pagedRecords.length ? (
|
|
|
|
|
|
pagedRecords.map((record) => (
|
|
|
|
|
|
<div role="row" className="management-record-table__row" key={record.id}>
|
|
|
|
|
|
<span>{formatDateTime(record.createdAt)}</span>
|
|
|
|
|
|
<span>{record.username}</span>
|
|
|
|
|
|
<span>{record.model}</span>
|
|
|
|
|
|
<span title={record.prompt || ""}>{record.prompt || "-"}</span>
|
|
|
|
|
|
<span className={record.status === "completed" ? "is-good" : record.status === "failed" ? "is-error" : undefined}>{record.status === "completed" ? "成功" : record.status === "failed" ? "失败" : record.status}</span>
|
|
|
|
|
|
<span>{recordDuration(record)}</span>
|
|
|
|
|
|
<span>{record.amountCents > 0 ? formatCredits(record.amountCents) : "-"}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="management-record-empty" role="row">
|
|
|
|
|
|
暂无调用记录
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{records.length > pageSize && (
|
|
|
|
|
|
<div className="management-record-pagination">
|
|
|
|
|
|
<button type="button" disabled={recordPage === 0} onClick={() => setRecordPage((p) => p - 1)}>
|
|
|
|
|
|
<LeftOutlined />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span>{recordPage + 1} / {totalPages}</span>
|
|
|
|
|
|
<button type="button" disabled={recordPage >= totalPages - 1} onClick={() => setRecordPage((p) => p + 1)}>
|
|
|
|
|
|
<RightOutlined />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default TokenUsagePage;
|