forked from xiaohei/taiji-pda-v0
523 lines
22 KiB
TypeScript
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>
|
|
)
|
|
}
|