Files
CC-Switch/src/components/usage/PricingConfigPanel.tsx
T
YoVinchen 434ffd559d feat(ui): add usage dashboard components
Add UsageDashboard with summary cards, trend chart, and data tables.
Implement model pricing configuration panel.
Add request log viewer with filtering and detail panel.
2025-12-03 00:25:41 +08:00

290 lines
9.7 KiB
TypeScript

import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useModelPricing, useDeleteModelPricing } from "@/lib/query/usage";
import { PricingEditModal } from "./PricingEditModal";
import type { ModelPricing } from "@/types/usage";
import { Plus, Pencil, Trash2, ChevronDown, ChevronRight } from "lucide-react";
export function PricingConfigPanel() {
const { t } = useTranslation();
const { data: pricing, isLoading, error } = useModelPricing();
const deleteMutation = useDeleteModelPricing();
const [editingModel, setEditingModel] = useState<ModelPricing | null>(null);
const [isAddingNew, setIsAddingNew] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
const [isExpanded, setIsExpanded] = useState(false);
const handleDelete = (modelId: string) => {
deleteMutation.mutate(modelId, {
onSuccess: () => {
setDeleteConfirm(null);
},
});
};
const handleAddNew = () => {
setIsAddingNew(true);
setEditingModel({
modelId: "",
displayName: "",
inputCostPerMillion: "0",
outputCostPerMillion: "0",
cacheReadCostPerMillion: "0",
cacheCreationCostPerMillion: "0",
});
};
if (isLoading) {
return (
<Card className="border-none bg-transparent p-0 shadow-none">
<CardHeader
className="cursor-pointer"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-2">
<ChevronRight className="h-4 w-4" />
<CardTitle className="text-base">
{t("usage.modelPricing", "模型定价")}
</CardTitle>
</div>
</CardHeader>
</Card>
);
}
if (error) {
return (
<Card className="border-none bg-transparent p-0 shadow-none">
<CardHeader
className="cursor-pointer"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
<CardTitle className="text-base">
{t("usage.modelPricing", "模型定价")}
</CardTitle>
</div>
</CardHeader>
{isExpanded && (
<CardContent>
<Alert variant="destructive">
<AlertDescription>
{t("usage.loadPricingError", "加载定价数据失败")}:{" "}
{String(error)}
</AlertDescription>
</Alert>
</CardContent>
)}
</Card>
);
}
return (
<Card className="border-none bg-transparent shadow-none">
<CardHeader
className="cursor-pointer select-none"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
)}
<div>
<CardTitle className="text-base">
{t("usage.modelPricing", "模型定价")}
{pricing && pricing.length > 0 && (
<span className="ml-2 text-sm font-normal text-muted-foreground">
({pricing.length})
</span>
)}
</CardTitle>
{!isExpanded && (
<CardDescription className="mt-1">
{t(
"usage.modelPricingDesc",
"配置各模型的 Token 成本(每百万 tokens 的 USD 价格,支持 * 与 ? 通配)",
)}
</CardDescription>
)}
</div>
</div>
<Button
onClick={(e) => {
e.stopPropagation();
handleAddNew();
}}
size="sm"
>
<Plus className="mr-1 h-4 w-4" />
{t("common.add", "新增")}
</Button>
</div>
</CardHeader>
{isExpanded && (
<CardContent>
{!pricing || pricing.length === 0 ? (
<Alert>
<AlertDescription>
{t(
"usage.noPricingData",
'暂无定价数据。点击"新增"添加模型定价配置。',
)}
</AlertDescription>
</Alert>
) : (
<div className="rounded-md bg-card/60 shadow-sm">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("usage.model", "模型")}</TableHead>
<TableHead>{t("usage.displayName", "显示名称")}</TableHead>
<TableHead className="text-right">
{t("usage.inputCost", "输入成本")}
</TableHead>
<TableHead className="text-right">
{t("usage.outputCost", "输出成本")}
</TableHead>
<TableHead className="text-right">
{t("usage.cacheReadCost", "缓存读取")}
</TableHead>
<TableHead className="text-right">
{t("usage.cacheWriteCost", "缓存写入")}
</TableHead>
<TableHead className="text-right">
{t("common.actions", "操作")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pricing.map((model) => (
<TableRow key={model.modelId}>
<TableCell className="font-mono text-sm">
{model.modelId}
</TableCell>
<TableCell>{model.displayName}</TableCell>
<TableCell className="text-right font-mono text-sm">
${model.inputCostPerMillion}
</TableCell>
<TableCell className="text-right font-mono text-sm">
${model.outputCostPerMillion}
</TableCell>
<TableCell className="text-right font-mono text-sm">
${model.cacheReadCostPerMillion}
</TableCell>
<TableCell className="text-right font-mono text-sm">
${model.cacheCreationCostPerMillion}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => {
setIsAddingNew(false);
setEditingModel(model);
}}
title={t("common.edit", "编辑")}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setDeleteConfirm(model.modelId)}
title={t("common.delete", "删除")}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
)}
{editingModel && (
<PricingEditModal
model={editingModel}
isNew={isAddingNew}
onClose={() => {
setEditingModel(null);
setIsAddingNew(false);
}}
/>
)}
<Dialog
open={!!deleteConfirm}
onOpenChange={() => setDeleteConfirm(null)}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
{t("usage.deleteConfirmTitle", "确认删除")}
</DialogTitle>
<DialogDescription>
{t(
"usage.deleteConfirmDesc",
"确定要删除此模型定价配置吗?此操作无法撤销。",
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteConfirm(null)}>
{t("common.cancel", "取消")}
</Button>
<Button
variant="destructive"
onClick={() => deleteConfirm && handleDelete(deleteConfirm)}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending
? t("common.deleting", "删除中...")
: t("common.delete", "删除")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
);
}