Files
taiji-pda-v0/app/billing/page.tsx
T

523 lines
22 KiB
TypeScript

"use client"
import { DashboardLayout } from "@/components/dashboard-layout"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Zap, Download, Filter, Calendar, Plus, Wallet } from "lucide-react"
import { Progress } from "@/components/ui/progress"
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, LineChart, Line } from "recharts"
import { useLanguage } from "@/hooks/useLanguage"
import { useState, useEffect } from "react"
import { TaijiAPIClient } from "@/lib/api-client"
import { useToast } from "@/hooks/use-toast"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export default function BillingPage() {
const { t } = useLanguage()
const { toast } = useToast()
const [loading, setLoading] = useState(true)
const [showFilterDialog, setShowFilterDialog] = useState(false)
const [showDateDialog, setShowDateDialog] = useState(false)
const [showExportDialog, setShowExportDialog] = useState(false)
const [showRechargeDialog, setShowRechargeDialog] = useState(false)
const [rechargeAmount, setRechargeAmount] = useState("")
const [balance, setBalance] = useState({ balance: 0, monthlySpent: 0, euBalance: 0 })
const [euUsageData, setEuUsageData] = useState<Array<{ date: string; eu: number }>>([])
const [costBreakdown, setCostBreakdown] = useState<Array<{ category: string; cost: number; eu: number }>>([])
const [resourceUsage, setResourceUsage] = useState({
cpu: { used: 0, total: 100 },
memory: { used: 0, total: 200 },
storage: { used: 0, total: 1000 },
apiCalls: { used: 0, total: 100000 },
})
useEffect(() => {
loadBillingData()
}, [])
const loadBillingData = async () => {
try {
setLoading(true)
// 并行加载数据
const [balanceResult, historyResult, costResult] = await Promise.allSettled([
TaijiAPIClient.getBillingBalance(),
TaijiAPIClient.getBillingHistory({
startTime: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
endTime: new Date().toISOString(),
}),
TaijiAPIClient.getBillingHistory({
startTime: new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString(),
endTime: new Date().toISOString(),
}),
])
// 处理余额
if (balanceResult.status === "fulfilled" && balanceResult.value?.success) {
const data = balanceResult.value.data
setBalance({
balance: data.balance || 0,
monthlySpent: data.monthlySpent || 0,
euBalance: data.euBalance || 0,
})
}
// 处理历史数据(用于图表)
if (historyResult.status === "fulfilled" && historyResult.value?.success) {
const records = historyResult.value.data?.records || []
// 按日期聚合EU消耗
const dailyEu: Record<string, number> = {}
records.forEach((record: any) => {
const date = new Date(record.timestamp).toLocaleDateString("en-US", { month: "short", day: "numeric" })
dailyEu[date] = (dailyEu[date] || 0) + (record.eu || 0)
})
setEuUsageData(
Object.entries(dailyEu).map(([date, eu]) => ({ date, eu })).slice(-8),
)
}
// 处理费用明细
if (costResult.status === "fulfilled" && costResult.value?.success) {
const records = costResult.value.data?.records || []
// 按类别聚合费用
const categoryCosts: Record<string, { cost: number; eu: number }> = {}
records.forEach((record: any) => {
const category = record.agentType || "Other"
if (!categoryCosts[category]) {
categoryCosts[category] = { cost: 0, eu: 0 }
}
categoryCosts[category].cost += record.cost || 0
categoryCosts[category].eu += record.eu || 0
})
setCostBreakdown(
Object.entries(categoryCosts).map(([category, data]) => ({
category,
...data,
})),
)
}
} catch (error) {
console.error("Failed to load billing data:", error)
} finally {
setLoading(false)
}
}
const handleRecharge = async () => {
if (!rechargeAmount || Number.parseFloat(rechargeAmount) <= 0) {
toast({
title: t("错误", "Error"),
description: t("请输入有效的充值金额", "Please enter a valid amount"),
variant: "destructive",
})
return
}
try {
const result = await TaijiAPIClient.rechargeBalance(Number.parseFloat(rechargeAmount))
if (result?.success) {
toast({
title: t("充值成功", "Recharge Successful"),
description: t("余额已更新", "Balance updated"),
})
setShowRechargeDialog(false)
setRechargeAmount("")
loadBillingData()
} else {
throw new Error(result?.message || "Recharge failed")
}
} catch (error: any) {
toast({
title: t("充值失败", "Recharge Failed"),
description: error.message || t("无法完成充值", "Failed to recharge"),
variant: "destructive",
})
}
}
return (
<DashboardLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">{t("计费与资源平面", "Billing & Resources Plane")}</h1>
<p className="text-muted-foreground mt-1">
{t("即用即付,按实际消费计费", "Pay-as-you-go, billing based on actual consumption")}
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" className="gap-2 bg-transparent" onClick={() => setShowDateDialog(true)}>
<Calendar className="h-4 w-4" />
{t("时间查询", "Date Query")}
</Button>
<Button variant="outline" className="gap-2 bg-transparent" onClick={() => setShowFilterDialog(true)}>
<Filter className="h-4 w-4" />
{t("筛选", "Filter")}
</Button>
<Button variant="outline" className="gap-2 bg-transparent" onClick={() => setShowExportDialog(true)}>
<Download className="h-4 w-4" />
{t("导出", "Export")}
</Button>
</div>
</div>
<Card className="bg-gradient-to-r from-primary/10 to-primary/5 border-primary/20">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Wallet className="h-5 w-5" />
{t("账户余额", "Account Balance")}
</CardTitle>
<CardDescription className="mt-2">
{t("即用即付模式,按实际消费扣费", "Pay-as-you-go mode, deducted based on actual consumption")}
</CardDescription>
</div>
<Button className="gap-2" onClick={() => setShowRechargeDialog(true)}>
<Plus className="h-4 w-4" />
{t("充值", "Recharge")}
</Button>
</div>
</CardHeader>
<CardContent>
<div className="text-4xl font-bold">
{loading ? (
<span className="inline-block h-10 w-32 animate-pulse bg-muted rounded" />
) : (
`¥${balance.balance.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
)}
</div>
<p className="text-sm text-muted-foreground mt-2">
{loading ? (
<span className="inline-block h-4 w-40 animate-pulse bg-muted rounded" />
) : (
t("本月已消费", "This month consumed") + `: ¥${balance.monthlySpent.toFixed(2)}`
)}
</p>
</CardContent>
</Card>
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">{t("EU余额", "EU Balance")}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{loading ? (
<span className="inline-block h-7 w-16 animate-pulse bg-muted rounded" />
) : (
balance.euBalance.toLocaleString()
)}
</div>
<p className="text-xs text-muted-foreground mt-1">{t("可用单位", "Available units")}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">{t("本月", "This Month")}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{loading ? (
<span className="inline-block h-7 w-16 animate-pulse bg-muted rounded" />
) : (
`¥${balance.monthlySpent.toFixed(2)}`
)}
</div>
<p className="text-xs text-muted-foreground mt-1">
{loading ? "" : `${Math.round(balance.monthlySpent * 100)} EU ${t("已消费", "consumed")}`}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">
{t("平均每日费用", "Avg Daily Cost")}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$34</div>
<p className="text-xs text-green-500 mt-1">-5% {t("vs上月", "vs last month")}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">{t("预计", "Projected")}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$1,050</div>
<p className="text-xs text-muted-foreground mt-1">{t("月底", "End of month")}</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>{t("EU消费历史", "EU Consumption History")}</CardTitle>
<CardDescription>{t("执行单位使用情况随时间变化", "Execution Units usage over time")}</CardDescription>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={euUsageData.length > 0 ? euUsageData : [{ date: "", eu: 0 }]}>
<XAxis dataKey="date" stroke="oklch(0.45 0 0)" fontSize={12} tickLine={false} />
<YAxis stroke="oklch(0.45 0 0)" fontSize={12} tickLine={false} />
<Tooltip
contentStyle={{
backgroundColor: "oklch(0.15 0 0)",
border: "1px solid oklch(0.22 0 0)",
borderRadius: "8px",
}}
/>
<Line
type="monotone"
dataKey="eu"
stroke="oklch(0.60 0.20 264)"
strokeWidth={2}
dot={{ fill: "oklch(0.60 0.20 264)", r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
<div className="grid gap-4 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>{t("费用明细", "Cost Breakdown")}</CardTitle>
<CardDescription>{t("当月支出(按类别)", "Current month spending by category")}</CardDescription>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={costBreakdown.length > 0 ? costBreakdown : [{ category: "", cost: 0, eu: 0 }]}>
<XAxis dataKey="category" stroke="oklch(0.45 0 0)" fontSize={11} tickLine={false} />
<YAxis stroke="oklch(0.45 0 0)" fontSize={12} tickLine={false} />
<Tooltip
contentStyle={{
backgroundColor: "oklch(0.15 0 0)",
border: "1px solid oklch(0.22 0 0)",
borderRadius: "8px",
}}
/>
<Bar dataKey="cost" fill="oklch(0.65 0.16 220)" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t("资源使用", "Resource Usage")}</CardTitle>
<CardDescription>{t("当前分配和限制", "Current allocation and limits")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{[
{
name: "CPU",
used: resourceUsage.cpu.used,
total: resourceUsage.cpu.total,
unit: t("核", "cores"),
},
{
name: t("内存", "Memory"),
used: resourceUsage.memory.used,
total: resourceUsage.memory.total,
unit: "GB",
},
{
name: t("存储", "Storage"),
used: resourceUsage.storage.used,
total: resourceUsage.storage.total,
unit: "GB",
},
{
name: t("API调用", "API Calls"),
used: resourceUsage.apiCalls.used,
total: resourceUsage.apiCalls.total,
unit: t("次", "calls"),
},
].map((resource) => (
<div key={resource.name}>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">{resource.name}</span>
<span className="text-sm text-muted-foreground">
{resource.used} / {resource.total} {resource.unit}
</span>
</div>
<Progress value={(resource.used / resource.total) * 100} className="h-2" />
</div>
))}
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>{t("EU定价详情", "EU Pricing Details")}</CardTitle>
<CardDescription>{t("了解执行单位", "Understanding Execution Units")}</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{[
{
service: t("模型推理", "Model Inference"),
rate: "0.025 EU/call",
desc: t("LLM API调用", "LLM API calls"),
},
{
service: t("API工具", "API Tools"),
rate: "0.001 EU/call",
desc: t("外部API使用", "External API usage"),
},
{
service: t("VM计算", "VM Compute"),
rate: "0.5 EU/hour",
desc: t("Firecracker VM运行时", "Firecracker VM runtime"),
},
{
service: t("数据存储", "Data Storage"),
rate: "0.01 EU/GB/day",
desc: t("持久存储", "Persistent storage"),
},
].map((pricing) => (
<div key={pricing.service} className="rounded-lg border border-border bg-muted/30 p-4">
<div className="flex items-center gap-2 mb-2">
<Zap className="h-4 w-4 text-primary" />
<h3 className="font-semibold text-sm">{pricing.service}</h3>
</div>
<p className="text-lg font-mono font-bold mb-1">{pricing.rate}</p>
<p className="text-xs text-muted-foreground">{pricing.desc}</p>
</div>
))}
</div>
</CardContent>
</Card>
<Dialog open={showFilterDialog} onOpenChange={setShowFilterDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("筛选选项", "Filter Options")}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<Label>{t("客户名称", "Customer Name")}</Label>
<Input placeholder={t("输入客户名称", "Enter customer name")} />
</div>
<div>
<Label>{t("最小调用次数", "Min Calls")}</Label>
<Input type="number" placeholder="0" />
</div>
<div>
<Label>{t("最大调用次数", "Max Calls")}</Label>
<Input type="number" placeholder="10000" />
</div>
<Button className="w-full">{t("应用筛选", "Apply Filter")}</Button>
</div>
</DialogContent>
</Dialog>
<Dialog open={showDateDialog} onOpenChange={setShowDateDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("时间范围", "Date & Time Range")}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<Label>{t("开始时间 (年-月-日 时:分)", "Start Date & Time (YYYY-MM-DD HH:MM)")}</Label>
<Input type="datetime-local" />
</div>
<div>
<Label>{t("结束时间 (年-月-日 时:分)", "End Date & Time (YYYY-MM-DD HH:MM)")}</Label>
<Input type="datetime-local" />
</div>
<Button className="w-full">{t("查询", "Query")}</Button>
</div>
</DialogContent>
</Dialog>
<Dialog open={showExportDialog} onOpenChange={setShowExportDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("导出格式", "Export Format")}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<p className="text-sm text-muted-foreground">{t("选择导出文件格式", "Select export file format")}</p>
<div className="grid grid-cols-2 gap-3">
<Button variant="outline" className="h-20 flex-col gap-2 bg-transparent">
<Download className="h-5 w-5" />
<span>Excel (.xlsx)</span>
</Button>
<Button variant="outline" className="h-20 flex-col gap-2 bg-transparent">
<Download className="h-5 w-5" />
<span>CSV (.csv)</span>
</Button>
<Button variant="outline" className="h-20 flex-col gap-2 col-span-2 bg-transparent">
<Download className="h-5 w-5" />
<span>PDF (.pdf)</span>
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<Dialog open={showRechargeDialog} onOpenChange={setShowRechargeDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("账户充值", "Account Recharge")}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<Label>{t("充值金额 (USD)", "Recharge Amount (USD)")}</Label>
<Input
type="number"
placeholder="100"
value={rechargeAmount}
onChange={(e) => setRechargeAmount(e.target.value)}
/>
<p className="text-xs text-muted-foreground mt-1">{t("最低充值金额: $10", "Minimum recharge: $10")}</p>
</div>
<div className="grid grid-cols-4 gap-2">
{[50, 100, 500, 1000].map((amount) => (
<Button
key={amount}
variant="outline"
onClick={() => setRechargeAmount(amount.toString())}
className="bg-transparent"
>
${amount}
</Button>
))}
</div>
<div className="rounded-lg bg-muted p-3 space-y-1">
<div className="flex justify-between text-sm">
<span>{t("充值金额", "Recharge Amount")}</span>
<span className="font-medium">${rechargeAmount || "0.00"}</span>
</div>
<div className="flex justify-between text-sm">
<span>{t("当前余额", "Current Balance")}</span>
<span className="font-medium">$2,487.50</span>
</div>
<div className="flex justify-between text-sm font-bold pt-2 border-t">
<span>{t("充值后余额", "Balance After Recharge")}</span>
<span>${(2487.5 + Number.parseFloat(rechargeAmount || "0")).toFixed(2)}</span>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowRechargeDialog(false)}>
{t("取消", "Cancel")}
</Button>
<Button onClick={handleRecharge}>{t("确认充值", "Confirm Recharge")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</DashboardLayout>
)
}