Files
taiji-pda-v0/app/admin/dashboard/page.tsx
T

5106 lines
251 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Shield,
Users,
DollarSign,
Activity,
Database,
TrendingUp,
AlertTriangle,
Search,
MoreVertical,
Globe,
LogOut,
Settings,
BarChart3,
Clock,
Building2,
Plus,
Eye,
Edit,
Trash2,
Zap,
Cpu,
Server,
Bot,
Check,
Calendar,
Filter,
Download,
} from "lucide-react"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { useLanguage } from "@/hooks/useLanguage" // Declare useLanguage import
import { Badge } from "@/components/ui/badge"
import { TaijiAPIClient, API_BASE_URLS } from "@/lib/api-client"
import { SettingsTab } from "./components/SettingsTab"
export default function AdminDashboard() {
const router = useRouter()
const { language, setLanguage } = useLanguage()
const [activeTab, setActiveTab] = useState("overview")
const [isAddChannelOpen, setIsAddChannelOpen] = useState(false)
// const [isConfigGoodsOpen, setIsConfigGoodsOpen] = useState(false) // REMOVED
// const [isAgentAllocationOpen, setIsAgentAllocationOpen] = useState(false) // REMOVED
const [isResourceManagementOpen, setIsResourceManagementOpen] = useState(false) // ADDED
const [isCommissionDialogOpen, setIsCommissionDialogOpen] = useState(false)
const [selectedChannel, setSelectedChannel] = useState<(typeof channels)[0] | null>(null)
const [selectedModels, setSelectedModels] = useState<string[]>([])
// const [monthlyQuota, setMonthlyQuota] = useState<string>(""); // REMOVED
const [creditLimit, setCreditLimit] = useState<string>("")
const [isSubscriptionDialogOpen, setIsSubscriptionDialogOpen] = useState(false)
const [selectedSubscriptionLevel, setSelectedSubscriptionLevel] = useState<string | null>(null)
const [commissionRate, setCommissionRate] = useState<string>("")
const [selectedTenant, setSelectedTenant] = useState<string | null>(null) // Declare selectedTenant variable
const [computeConfigOpen, setComputeConfigOpen] = useState(false) // State for compute configuration dialog
// 新增:渠道管理对话框状态
const [isChannelDetailsDialogOpen, setIsChannelDetailsDialogOpen] = useState(false)
const [isChannelEditDialogOpen, setIsChannelEditDialogOpen] = useState(false)
const [channelAllocatedResources, setChannelAllocatedResources] = useState<any>(null) // 渠道资源分配详情(新接口)
const [channelTenantsResources, setChannelTenantsResources] = useState<any>(null) // 租户资源分配数据(旧接口)
const [channelTenantsResourcesLoading, setChannelTenantsResourcesLoading] = useState(false)
const [isViewTenantsDialogOpen, setIsViewTenantsDialogOpen] = useState(false)
const [isAddTenantDialogOpen, setIsAddTenantDialogOpen] = useState(false)
const [isTenantPermissionDialogOpen, setIsTenantPermissionDialogOpen] = useState(false)
const [channelTenantsLoading, setChannelTenantsLoading] = useState(false)
const [channelTenants, setChannelTenants] = useState<any[]>([])
const [selectedTenantForEdit, setSelectedTenantForEdit] = useState<any>(null)
const [selectedTenantForPermission, setSelectedTenantForPermission] = useState<any>(null)
// 新增:修改密码对话框状态
const [isChangePasswordDialogOpen, setIsChangePasswordDialogOpen] = useState(false)
const [newPassword, setNewPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
const [changePasswordLoading, setChangePasswordLoading] = useState(false)
const [newTenantForm, setNewTenantForm] = useState({
name: "",
email: "",
password: "",
systemRole: "tenant" as "tenant" | "billing-admin" | "operations-admin",
})
const [tenantPermissions, setTenantPermissions] = useState<string[]>([])
const [channelEditForm, setChannelEditForm] = useState({
name: "",
contactName: "",
email: "",
phone: "",
})
const [channelAdmins, setChannelAdmins] = useState<any[]>([])
const [newChannelForm, setNewChannelForm] = useState({
name: "",
email: "",
password: "",
commissionRate: "",
})
const [billingData, setBillingData] = useState<any>(null)
const [billingLoading, setBillingLoading] = useState(false)
const [isAddProviderOpen, setIsAddProviderOpen] = useState(false)
const [isConfigProviderOpen, setIsConfigProviderOpen] = useState(false)
const [providerType, setProviderType] = useState<"model" | "data">("model")
const [selectedProvider, setSelectedProvider] = useState<any>(null)
const [providerForm, setProviderForm] = useState({
name: "",
url: "",
apiKey: "",
models: [] as string[], // 改为数组支持多选
rpm: "",
tpm: "",
})
const [litellmModels, setLitellmModels] = useState<string[]>([]) // LiteLLM 模型列表
const [litellmModelsLoading, setLitellmModelsLoading] = useState(false) // 加载状态
const [selectedAgent, setSelectedAgent] = useState<any>(null) // State for selected agent in resources tab
// Agent资源配置表单状态
const [agentConfigForm, setAgentConfigForm] = useState({
cpuRequest: "100m",
cpuLimit: "500m",
memoryRequest: "128Mi",
memoryLimit: "512Mi",
maxInstances: 10,
})
const [customAgentCpu, setCustomAgentCpu] = useState("2")
const [customAgentMemory, setCustomAgentMemory] = useState("4")
const [agentResources, setAgentResources] = useState<any[]>([]) // Agent资源列表
const [modelProviders, setModelProviders] = useState<any[]>([]) // 模型供应商列表
const [resourcesLoading, setResourcesLoading] = useState(false)
// const [isAgentAllocationOpen, setIsAgentAllocationOpen] = useState(false) // REMOVED
const [selectedAgents, setSelectedAgents] = useState<string[]>([])
const [agentQuantities, setAgentQuantities] = useState<Record<string, number>>({})
// Billing state
const [billingView, setBillingView] = useState<"channel" | "tenant" | "calls">("channel")
const [showFilterDialog, setShowFilterDialog] = useState(false)
const [showDateDialog, setShowDateDialog] = useState(false)
const [showExportDialog, setShowExportDialog] = useState(false)
const [selectedRole, setSelectedRole] = useState<string | null>(null)
const [rolePermissions, setRolePermissions] = useState<{ [key: string]: string[] }>({
"billing-admin": ["overview", "channels", "billing"],
"operations-admin": ["overview", "channels", "resources", "monitoring"],
"super-admin": [
"overview",
"channels",
"resources",
"monitoring",
"billing",
"provider-backend",
"channel-backend",
"settings",
],
})
const [showAddAdminDialog, setShowAddAdminDialog] = useState(false)
const [admins, setAdmins] = useState<any[]>([])
const [adminsLoading, setAdminsLoading] = useState(false)
const [deleteAdminConfirm, setDeleteAdminConfirm] = useState<{ open: boolean; admin: any | null }>({
open: false,
admin: null,
})
const [newAdminForm, setNewAdminForm] = useState({
name: "",
email: "",
password: "",
role: "billing_admin",
})
const [providerApprovals, setProviderApprovals] = useState<any[]>([])
const [selectedApproval, setSelectedApproval] = useState<any>(null)
const [isApprovalDialogOpen, setIsApprovalDialogOpen] = useState(false)
// 平台Agent申请审批相关状态
const [platformAgentApplications, setPlatformAgentApplications] = useState<any[]>([])
const [selectedPlatformAgentApplication, setSelectedPlatformAgentApplication] = useState<any>(null)
const [isPlatformAgentApprovalDialogOpen, setIsPlatformAgentApprovalDialogOpen] = useState(false)
const [approvedPodQuota, setApprovedPodQuota] = useState<string>("")
const [approvalReason, setApprovalReason] = useState<string>("")
// Simplified t function for demonstration
const t = (zh: string, en: string) => (language === "zh" ? zh : en)
const translations = {
en: {
title: "Super Admin Console",
subtitle: "Platform Control Center",
overview: "Overview",
channels: "Channels",
resources: "Resources",
monitoring: "Monitoring",
billing: "Billing",
settings: "Settings",
goodsProviders: "Goods Providers", // renamed from separate model/data providers
logout: "Logout",
totalTenants: "Total Tenants",
totalChannels: "Total Channels",
activeTenants: "Active Tenants",
totalRevenue: "Total Revenue",
systemHealth: "System Health",
cpuUsage: "CPU Usage",
memoryUsage: "Memory Usage",
storageUsage: "Storage Usage",
activeAgents: "Active Agents",
recentTenants: "Recent Tenants",
tenantName: "Tenant Name",
status: "Status",
plan: "Plan",
usage: "Usage",
actions: "Actions",
search: "Search tenants...",
searchChannels: "Search channels...",
active: "Active",
enterprise: "Enterprise",
professional: "Professional",
starter: "Starter",
viewDetails: "View Details",
suspend: "Suspend",
systemMetrics: "System Metrics",
healthy: "Healthy",
addChannel: "Add Channel",
channelName: "Channel Name",
channelManagement: "Channel Management",
channelDescription: "Manage distribution channels and their tenant portfolios",
tenantCount: "Tenants",
commission: "Commission Rate",
contactPerson: "Contact Person",
edit: "Edit",
delete: "Delete",
createChannel: "Create New Channel",
cancel: "Cancel",
create: "Create",
channelNamePlaceholder: "Enter channel name",
contactEmail: "Contact Email",
contactPhone: "Contact Phone",
commissionRate: "Commission Rate (%)",
assignSubscriptionLevel: "Assign System Role",
selectedTenantPlaceholder: "Select a tenant",
agentHealthMonitoring: "Agent Health Monitoring",
agentHealthDesc: "Monitor performance and health status of all platform agents",
agentStatus: "Status",
cpuUsageLabel: "CPU Usage",
memoryUsageLabel: "Memory Usage",
responseTime: "Response Time",
requestCount: "Request Count",
errorRate: "Error Rate",
uptime: "Uptime",
lastActive: "Last Active",
ms: "ms",
requests: "requests",
warning: "Warning",
critical: "Critical",
// Added translations for role management
userRoleSettings: "User Role Settings",
billingAdministrator: "Billing Administrator",
responsibleForBilling: "Responsible for billing, invoices, and financial management",
operationsAdministrator: "Operations Administrator",
responsibleForChannels: "Responsible for channels, resources, and monitoring",
superAdministrator: "Super Administrator",
fullAccess: "Full access to all platform features",
permissions: "permissions",
tabAccessPermissions: "Tab Access Permissions",
selectTabs: "Select which tabs this role can access",
providerBackend: "Provider Backend",
channelBackend: "Channel Backend",
savePermissions: "Save Permissions",
// Added translations for add admin dialog
addAdministrator: "Add Administrator",
administratorName: "Administrator Name",
enterAdministratorName: "Enter administrator name",
email: "Email",
enterEmailAddress: "Enter email address",
password: "Password",
setLoginPassword: "Set login password",
role: "Role",
billingAdmin: "Billing Administrator",
operationsAdmin: "Operations Administrator",
superAdmin: "Super Administrator",
createAdministrator: "Create Administrator",
// ADDED translations for billing
billingManagement: "Billing Management",
dateQuery: "Date Query",
filter: "Filter",
export: "Export",
channelBillingDetails: "Channel Billing Details",
callCount: "Call Count",
totalEU: "Total EU",
channelTotal: "Channel Total",
billingPeriod: "Billing Period",
tenantBillingDetails: "Tenant Billing Details",
channel: "Channel",
userTotal: "User Total",
averageSpending: "Average Spending",
callRecordsDetail: "Call Records Detail",
callID: "Call ID",
tenant: "Tenant",
callTime: "Call Time",
durationSeconds: "Duration(s)",
singleCallCost: "Single Call Cost",
timestamp: "Timestamp",
EUCalculationRule: "EU Calculation: 1 EU = 10 seconds call time",
// ADDED translations for filter dialog
filterOptions: "Filter Options",
customerName: "Customer Name",
minCalls: "Min Calls",
maxCalls: "Max Calls",
applyFilter: "Apply Filter",
// ADDED translations for date dialog
dateTimeRange: "Date & Time Range",
startDate: "Start Date & Time (YYYY-MM-DD HH:MM)",
endDate: "End Date & Time (YYYY-MM-DD HH:MM)",
query: "Query",
// ADDED translations for export dialog
exportFormat: "Export Format",
selectExportFormat: "Select export file format",
},
zh: {
title: "超级管理员控制台",
subtitle: "平台控制中心",
overview: "概览",
channels: "渠道管理",
resources: "资源管理",
monitoring: "监控",
billing: "计费",
settings: "设置",
goodsProviders: "货源供应商", // renamed from separate model/data providers
logout: "退出",
totalTenants: "总租户数",
totalChannels: "总渠道数",
activeTenants: "活跃租户",
totalRevenue: "总收入",
systemHealth: "系统健康",
cpuUsage: "CPU 使用率",
memoryUsage: "内存使用率",
storageUsage: "存储使用率",
activeAgents: "活跃Agent",
recentTenants: "最近租户",
tenantName: "租户名称",
status: "状态",
plan: "方案",
usage: "使用量",
actions: "操作",
search: "搜索租户...",
searchChannels: "搜索渠道...",
active: "活跃",
enterprise: "企业版",
professional: "专业版",
starter: "入门版",
viewDetails: "查看详情",
suspend: "暂停",
systemMetrics: "系统指标",
healthy: "健康",
addChannel: "添加渠道",
channelName: "渠道名称",
channelManagement: "渠道管理",
channelDescription: "管理分销渠道及其租户组合",
tenantCount: "租户数",
commission: "佣金比例",
contactPerson: "联系人",
edit: "编辑",
delete: "删除",
createChannel: "创建新渠道",
cancel: "取消",
create: "创建",
channelNamePlaceholder: "输入渠道名称",
contactEmail: "联系邮箱",
contactPhone: "联系电话",
commissionRate: "佣金比例 (%)",
assignSubscriptionLevel: "分配系统权限",
selectedTenantPlaceholder: "选择一个租户",
agentHealthMonitoring: "Agent健康监控",
agentHealthDesc: "监控所有平台Agent的性能和健康状态",
agentStatus: "状态",
cpuUsageLabel: "CPU使用率",
memoryUsageLabel: "内存使用率",
responseTime: "响应时间",
requestCount: "请求数量",
errorRate: "错误率",
uptime: "运行时间",
lastActive: "最后活跃",
warning: "警告",
critical: "严重",
// Added translations for role management
userRoleSettings: "用户身份设置",
billingAdministrator: "计费管理员",
responsibleForBilling: "负责平台计费、账单和财务管理",
operationsAdministrator: "运营管理员",
responsibleForChannels: "负责渠道、资源和系统监控管理",
superAdministrator: "超级管理员",
fullAccess: "拥有所有权限,管理整个平台",
permissions: "个权限",
tabAccessPermissions: "标签页访问权限",
selectTabs: "选择该角色可以访问的标签页",
providerBackend: "供应商管理中心后台",
channelBackend: "渠道管理中心后台",
savePermissions: "保存权限配置",
// Added translations for add admin dialog
addAdministrator: "添加管理员",
administratorName: "管理员名称",
enterAdministratorName: "输入管理员名称",
email: "邮箱",
enterEmailAddress: "输入邮箱地址",
password: "密码",
setLoginPassword: "设置登录密码",
role: "角色",
billingAdmin: "计费管理员",
operationsAdmin: "运营管理员",
superAdmin: "超级管理员",
createAdministrator: "创建管理员",
// ADDED translations for billing
billingManagement: "计费管理",
dateQuery: "时间查询",
filter: "筛选",
export: "导出",
channelBillingDetails: "渠道计费详情",
callCount: "调用次数",
totalEU: "总EU",
channelTotal: "渠道总价",
billingPeriod: "计费周期",
tenantBillingDetails: "租户计费详情",
channel: "渠道",
userTotal: "用户总价",
averageSpending: "平均消费",
callRecordsDetail: "调用记录明细",
callID: "调用ID",
tenant: "租户",
callTime: "调用时间",
durationSeconds: "时长(秒)",
singleCallCost: "单次调用总价",
timestamp: "时间戳",
EUCalculationRule: "EU计算:1 EU = 10秒调用时间",
// ADDED translations for filter dialog
filterOptions: "筛选选项",
customerName: "客户名称",
minCalls: "最小调用次数",
maxCalls: "最大调用次数",
applyFilter: "应用筛选",
// ADDED translations for date dialog
dateTimeRange: "时间范围",
startDate: "开始时间 (年-月-日 时:分)",
endDate: "结束时间 (年-月-日 时:分)",
query: "查询",
// ADDED translations for export dialog
exportFormat: "导出格式",
selectExportFormat: "选择导出文件格式",
},
}
const text = translations[language]
const [agents, setAgents] = useState<any[]>([])
const [agentSummary, setAgentSummary] = useState<any>(null) // Agent监控汇总统计
const [stats, setStats] = useState<any[]>([])
const [systemMetrics, setSystemMetrics] = useState<any[]>([])
// 平台资源分配统计(platformAgents + customAgents)
const [resourceAllocationStats, setResourceAllocationStats] = useState({
totalCount: 0,
totalCpu: 0,
totalMemory: 0,
platformCount: 0, // 平台分配的 Agent 数量
customCount: 0, // 自定义启动的 Agent 数量
})
const [channels, setChannels] = useState<any[]>([])
const [tenants, setTenants] = useState<any[]>([])
const [recentLogins, setRecentLogins] = useState<any[]>([]) // 最近登录的租户列表
const [availableModels, setAvailableModels] = useState<any[]>([])
const [availableDataSources, setAvailableDataSources] = useState<any[]>([])
const [availableAgents, setAvailableAgents] = useState<any[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const token = localStorage.getItem("admin_token")
if (!token) {
router.push("/admin/login")
return
}
loadDashboardData()
}, [router])
const loadDashboardData = async () => {
try {
setLoading(true)
// 加载统计数据 - 使用正确的后端字段名
const statsResponse = await TaijiAPIClient.getAdminDashboardStats()
const statsData = statsResponse?.data || statsResponse
console.log("[v0] Stats Data:", statsData)
if (statsData) {
setStats([
{
title: text.totalChannels,
value: statsData.totalChannels?.toString() || "0",
change: "",
icon: Building2,
trend: "up",
},
{
title: text.totalTenants,
value: statsData.totalTenants?.toString() || "0",
change: "",
icon: Users,
trend: "up",
},
{
title: text.activeTenants,
value: statsData.totalAgents?.toString() || "0", // 使用 totalAgents 作为活跃指标
change: "",
icon: Activity,
trend: "up",
},
{
title: text.totalRevenue,
value: statsData.totalRevenue ? `$${statsData.totalRevenue}` : "$0",
change: "",
icon: DollarSign,
trend: "up",
},
])
// 计算平台资源分配统计 (platformAgents + customAgents) - 数据来自 stats 接口
const platformAgents = statsData.platformAgents || { count: 0, cpu: 0, memory: 0 }
const customAgents = statsData.customAgents || { count: 0, cpu: 0, memory: 0 }
setResourceAllocationStats({
totalCount: (platformAgents.count || 0) + (customAgents.count || 0),
totalCpu: (platformAgents.cpu || 0) + (customAgents.cpu || 0),
totalMemory: (platformAgents.memory || 0) + (customAgents.memory || 0),
platformCount: platformAgents.count || 0,
customCount: customAgents.count || 0,
})
}
// 加载系统性能指标 - 使用正确的监控接口 GET /api/v1/monitoring/metrics
try {
const metricsResponse = await TaijiAPIClient.getMonitoringMetrics()
const metricsData = metricsResponse?.data || metricsResponse
if (metricsData) {
// 增加调试日志
console.log("[v0] Metrics Data:", metricsData)
setSystemMetrics([
{ label: text.cpuUsage, value: metricsData.cpu_usage || metricsData.system?.cpu_usage_percent || 0, max: 100, color: "bg-blue-500" },
{ label: text.memoryUsage, value: metricsData.memory_usage || metricsData.system?.memory_usage_percent || 0, max: 100, color: "bg-green-500" },
{ label: text.storageUsage, value: metricsData.disk_usage || metricsData.system?.disk_usage_percent || 0, max: 100, color: "bg-yellow-500" },
{ label: text.activeAgents, value: metricsData.active_agents || metricsData.services?.active_agents || 0, max: 100, color: "bg-purple-500" },
])
} else {
// 如果监控接口没有返回数据,设置默认值
setSystemMetrics([
{ label: text.cpuUsage, value: 0, max: 100, color: "bg-blue-500" },
{ label: text.memoryUsage, value: 0, max: 100, color: "bg-green-500" },
{ label: text.storageUsage, value: 0, max: 100, color: "bg-yellow-500" },
{ label: text.activeAgents, value: 0, max: 100, color: "bg-purple-500" },
])
}
} catch (metricsError) {
console.error("Failed to load monitoring metrics:", metricsError)
// 监控接口失败时设置默认值
setSystemMetrics([
{ label: text.cpuUsage, value: 0, max: 100, color: "bg-blue-500" },
{ label: text.memoryUsage, value: 0, max: 100, color: "bg-green-500" },
{ label: text.storageUsage, value: 0, max: 100, color: "bg-yellow-500" },
{ label: text.activeAgents, value: 0, max: 100, color: "bg-purple-500" },
])
}
// 加载渠道列表
const channelsData = await TaijiAPIClient.getAdminChannels()
if (channelsData?.data?.channels && Array.isArray(channelsData.data.channels)) {
setChannels(channelsData.data.channels)
} else if (Array.isArray(channelsData)) {
setChannels(channelsData)
}
// 加载供应商申请列表(使用专用接口)
try {
const providerAppsData = await TaijiAPIClient.getProviderApplications()
if (providerAppsData?.success && providerAppsData.data?.applications) {
setProviderApprovals(providerAppsData.data.applications)
} else if (providerAppsData?.data?.data && Array.isArray(providerAppsData.data.data)) {
// 兼容旧的响应格式
setProviderApprovals(providerAppsData.data.data)
} else if (Array.isArray(providerAppsData?.data)) {
setProviderApprovals(providerAppsData.data)
} else {
setProviderApprovals([])
}
} catch (error) {
console.log("Failed to load provider applications:", error)
setProviderApprovals([])
}
// 加载平台Agent申请列表(新的专用接口)
try {
const platformAgentAppsData = await TaijiAPIClient.getPlatformAgentApplications()
if (platformAgentAppsData?.success && platformAgentAppsData.data?.applications) {
setPlatformAgentApplications(platformAgentAppsData.data.applications)
} else if (Array.isArray(platformAgentAppsData?.data)) {
setPlatformAgentApplications(platformAgentAppsData.data)
} else {
setPlatformAgentApplications([])
}
} catch (error) {
console.log("Failed to load platform agent applications:", error)
setPlatformAgentApplications([])
}
// 加载Agent监控数据
try {
const agentMonitoringData = await TaijiAPIClient.getPlatformAgentStatus()
if (agentMonitoringData?.data?.agents && Array.isArray(agentMonitoringData.data.agents)) {
setAgents(agentMonitoringData.data.agents)
// 保存汇总统计信息
if (agentMonitoringData.data.summary) {
setAgentSummary(agentMonitoringData.data.summary)
}
} else if (Array.isArray(agentMonitoringData)) {
setAgents(agentMonitoringData)
}
} catch (error) {
console.error("Failed to load agent monitoring data:", error)
setAgents([])
}
// 加载模型提供商(用于渠道资源分配)
const modelProvidersData = await TaijiAPIClient.getAdminModelProviders()
if (modelProvidersData?.data?.providers && Array.isArray(modelProvidersData.data.providers)) {
setAvailableModels(modelProvidersData.data.providers)
setModelProviders(modelProvidersData.data.providers)
} else if (Array.isArray(modelProvidersData)) {
setAvailableModels(modelProvidersData)
setModelProviders(modelProvidersData)
}
// 加载供应商列表(用于供应商管理)
try {
const providersData = await TaijiAPIClient.getModelProviders()
if (providersData?.data?.providers && Array.isArray(providersData.data.providers)) {
// 如果providers不为空,则使用该列表
setModelProviders(providersData.data.providers)
}
} catch (error) {
console.log("Failed to load providers list:", error)
// 如果获取失败,继续使用admin API的数据
}
// 加载Agent资源 - 使用平台Agent模板接口(管理员端)
const agentResourcesData = await TaijiAPIClient.getPlatformAgentTemplates()
if (agentResourcesData?.data?.templates && Array.isArray(agentResourcesData.data.templates)) {
setAvailableAgents(agentResourcesData.data.templates)
setAgentResources(agentResourcesData.data.templates)
} else if (agentResourcesData?.data?.agents && Array.isArray(agentResourcesData.data.agents)) {
// 兼容旧的响应格式
setAvailableAgents(agentResourcesData.data.agents)
setAgentResources(agentResourcesData.data.agents)
} else if (Array.isArray(agentResourcesData)) {
setAvailableAgents(agentResourcesData)
setAgentResources(agentResourcesData)
}
// 加载租户列表
try {
const tenantsData = await TaijiAPIClient.getAdminTenants()
if (tenantsData?.success && tenantsData.data?.tenants) {
setTenants(tenantsData.data.tenants)
} else if (Array.isArray(tenantsData?.data)) {
setTenants(tenantsData.data)
} else {
setTenants([])
}
} catch (error) {
console.log("Failed to load tenants:", error)
setTenants([])
}
// 加载最近登录的租户列表
try {
const recentLoginsData = await TaijiAPIClient.getRecentLogins(10)
if (recentLoginsData?.success && recentLoginsData.data?.recentTenants && Array.isArray(recentLoginsData.data.recentTenants)) {
setRecentLogins(recentLoginsData.data.recentTenants)
} else if (recentLoginsData?.success && recentLoginsData.data?.tenants && Array.isArray(recentLoginsData.data.tenants)) {
// 兼容:后端返回的字段是 tenants 而不是 recentTenants
setRecentLogins(recentLoginsData.data.tenants)
} else if (Array.isArray(recentLoginsData?.data)) {
setRecentLogins(recentLoginsData.data)
} else {
setRecentLogins([])
}
} catch (error) {
console.log("Failed to load recent logins:", error)
setRecentLogins([])
}
// 数据源列表暂时设为空(后端API待实现)
setAvailableDataSources([])
} catch (error) {
console.error("Failed to load dashboard data:", error)
} finally {
setLoading(false)
}
}
const handleLogout = async () => {
try {
// 调用后端logout API
await TaijiAPIClient.logout()
} catch (error) {
console.error("Logout API call failed:", error)
} finally {
// 清除所有本地存储的token和用户信息
localStorage.removeItem("admin_token")
localStorage.removeItem("auth_token")
localStorage.removeItem("channel_token")
localStorage.removeItem("refresh_token")
localStorage.removeItem("user")
localStorage.removeItem("api_key")
router.push("/admin/login")
}
}
// handlers for goods configuration
const handleConfigGoods = (channel: (typeof channels)[0]) => {
setSelectedChannel(channel)
// setIsConfigGoodsOpen(true) // REMOVED
setIsResourceManagementOpen(true) // ADDED
}
// const handleSaveGoodsConfig = () => { // REMOVED
// // Handle save logic
// console.log("Saving config for:", selectedChannel?.name)
// console.log("Selected models:", selectedModels)
// console.log("Selected data sources:", selectedDataSources)
// console.log("Monthly quota:", monthlyQuota)
// console.log("Monthly budget:", monthlyBudget)
// setIsConfigGoodsOpen(false)
// setSelectedModels([])
// setSelectedDataSources([])
// setMonthlyQuota("")
// setMonthlyBudget("")
// }
const handleEditCommission = (channel: (typeof channels)[0]) => {
setSelectedChannel(channel)
setCommissionRate(channel.commission.toString())
setIsCommissionDialogOpen(true)
}
const handleSaveCommission = async () => {
if (!selectedChannel) return
try {
// 使用专门的佣金更新API
const rate = parseFloat(commissionRate) / 100 // 将百分比转换为小数
const result = await TaijiAPIClient.updateChannelCommission(selectedChannel.id, rate)
if (result?.success) {
alert(language === "zh" ? `佣金比例已更新为 ${commissionRate}%` : `Commission rate updated to ${commissionRate}%`)
}
// 重新加载数据
await loadDashboardData()
setIsCommissionDialogOpen(false)
setCommissionRate("")
} catch (error) {
console.error("Failed to save commission:", error)
alert(language === "zh" ? "保存失败" : "Failed to save")
}
}
const handleAddProvider = async (type: "model" | "data") => {
setProviderType(type)
setProviderForm({
name: "",
url: "",
apiKey: "",
models: [],
rpm: "",
tpm: "",
})
// 加载 LiteLLM 模型列表
setLitellmModelsLoading(true)
try {
const models = await TaijiAPIClient.getLiteLLMModelIds()
setLitellmModels(models)
} catch (error) {
console.error("Failed to load LiteLLM models:", error)
setLitellmModels([])
} finally {
setLitellmModelsLoading(false)
}
setIsAddProviderOpen(true)
}
const handleConfigProvider = async (provider: any, type: "model" | "data") => {
setProviderType(type)
setSelectedProvider(provider)
setProviderForm({
name: provider.name,
url: provider.url || "",
apiKey: "••••••••",
models: provider.models || [],
rpm: provider.rpm?.toString() || "",
tpm: provider.tpm?.toString() || "",
})
// 加载 LiteLLM 模型列表
setLitellmModelsLoading(true)
try {
const models = await TaijiAPIClient.getLiteLLMModelIds()
setLitellmModels(models)
} catch (error) {
console.error("Failed to load LiteLLM models:", error)
setLitellmModels([])
} finally {
setLitellmModelsLoading(false)
}
setIsConfigProviderOpen(true)
}
const handleSaveProvider = async () => {
try {
// 验证必填字段
if (!providerForm.name || !providerForm.url || !providerForm.apiKey || providerForm.models.length === 0) {
alert(language === "zh" ? "请填写所有必填字段" : "Please fill in all required fields")
return
}
const supportedModels = providerForm.models.filter((m: string) => m.length > 0)
if (supportedModels.length === 0) {
alert(language === "zh" ? "请至少选择一个模型" : "Please select at least one model")
return
}
// 判断是添加新的还是更新现有的供应商
if (selectedProvider) {
// 更新现有供应商
await TaijiAPIClient.updateProvider(selectedProvider.id, {
name: providerForm.name,
provider: providerType === "model" ? "openai" : "anthropic",
apiUrl: providerForm.url,
apiKey: providerForm.apiKey,
supportedModels,
rpm: parseInt(providerForm.rpm) || 3500,
tpm: parseInt(providerForm.tpm) || 90000,
})
alert(language === "zh" ? "供应商更新成功" : "Provider updated successfully")
} else {
// 创建新供应商
await TaijiAPIClient.createModelProvider({
name: providerForm.name,
provider: "openai", // 默认为 openai,实际应该根据供应商类型选择
apiUrl: providerForm.url,
apiKey: providerForm.apiKey,
supportedModels,
rpm: parseInt(providerForm.rpm) || 3500,
tpm: parseInt(providerForm.tpm) || 90000,
})
alert(language === "zh" ? "供应商创建成功" : "Provider created successfully")
}
// 重新加载数据
await loadDashboardData()
// 清空表单
setIsAddProviderOpen(false)
setIsConfigProviderOpen(false)
setProviderForm({
name: "",
url: "",
apiKey: "",
models: [],
rpm: "",
tpm: "",
})
setSelectedProvider(null)
} catch (error) {
console.error("Failed to save provider:", error)
alert(language === "zh" ? "保存失败,请检查输入" : "Failed to save provider. Please check your input")
}
}
// const handleAgentAllocation = (channel: (typeof channels)[0]) => { // REMOVED
// setSelectedChannel(channel)
// setSelectedAgents([])
// setAgentQuantities({})
// setIsAgentAllocationOpen(true)
// }
// const handleSaveAgentAllocation = () => { // REMOVED
// console.log("Saving agent allocation for:", selectedChannel?.name)
// console.log("Selected agents:", selectedAgents)
// console.log("Agent quantities:", agentQuantities)
// setIsAgentAllocationOpen(false)
// setSelectedAgents([])
// setAgentQuantities({})
// }
// ADDED: Unified resource management handler
const handleResourceManagement = async (channel: (typeof channels)[0]) => {
setSelectedChannel(channel)
// 重置选择状态
setSelectedModels([])
setSelectedAgents([])
setAgentQuantities({})
setCreditLimit("")
setCustomAgentCpu("2")
setCustomAgentMemory("4")
// 从API加载当前渠道的资源配置
try {
const token = localStorage.getItem('admin_token') || localStorage.getItem('auth_token')
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/admin/channels/${channel.id}/resources`, {
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
const data = await response.json()
console.log("[DEBUG] Channel resources API response:", data)
if (data.success && data.data) {
// 加载已选择的模型
if (data.data.models && Array.isArray(data.data.models)) {
console.log("[DEBUG] Loading models:", data.data.models)
setSelectedModels(data.data.models)
}
// 加载已分配的Agent - 只加载有 agentId 的项目
if (data.data.agents && Array.isArray(data.data.agents)) {
console.log("[DEBUG] Loading agents from API:", data.data.agents)
console.log("[DEBUG] Available agents:", availableAgents)
// 过滤掉无效的 agentId
const validAgents = data.data.agents.filter((a: any) => a.agentId && typeof a.agentId === 'string')
// 将后端返回的 agentId 映射到前端 availableAgents 的 agentKey (id || name)
// 后端可能返回 agent.name 或 agent.id,需要兼容两种情况
const agentKeys: string[] = []
const quantities: { [key: string]: number } = {}
validAgents.forEach((a: any) => {
// 尝试在 availableAgents 中找到匹配的 agent
const matchedAgent = availableAgents.find(
(avail: any) => avail.id === a.agentId || avail.name === a.agentId
)
if (matchedAgent) {
// 使用 availableAgents 中的 agentKey (id || name) 作为选中状态的 key
const agentKey = matchedAgent.id || matchedAgent.name
agentKeys.push(agentKey)
quantities[agentKey] = a.quantity || 1
} else {
// 如果找不到匹配的,直接使用后端返回的 agentId
agentKeys.push(a.agentId)
quantities[a.agentId] = a.quantity || 1
}
})
console.log("[DEBUG] Mapped agent keys:", agentKeys)
console.log("[DEBUG] Agent quantities:", quantities)
setSelectedAgents(agentKeys)
setAgentQuantities(quantities)
}
// 加载自定义Agent资源配置
if (data.data.customAgentResources) {
setCustomAgentCpu(String(data.data.customAgentResources.cpu || 2))
setCustomAgentMemory(String(data.data.customAgentResources.memory || 4))
}
// 加载授信额度
if (data.data.channelCredit) {
setCreditLimit(String(data.data.channelCredit))
}
}
} else {
console.log("[DEBUG] Channel resources API returned non-OK status:", response.status)
}
} catch (error) {
console.error("Failed to load channel resources:", error)
// 如果加载失败,保持默认值(空数组)
}
setIsResourceManagementOpen(true)
}
// ADDED: Unified resource management save handler
const handleSaveResourceManagement = async () => {
if (!selectedChannel) return
try {
// 过滤掉无效的 agentId(undefined、null、空字符串)
const validAgents = selectedAgents
.filter((agentId) => agentId && typeof agentId === 'string' && agentId.trim() !== '')
.map((agentId) => ({
agentId,
quantity: agentQuantities[agentId] || 1,
}))
// 调试日志
console.log("[DEBUG] handleSaveResourceManagement:")
console.log(" - selectedAgents:", selectedAgents)
console.log(" - validAgents:", validAgents)
console.log(" - agentQuantities:", agentQuantities)
console.log(" - selectedModels:", selectedModels)
const requestData = {
models: selectedModels,
agents: validAgents,
customAgentResources: {
cpu: parseFloat(customAgentCpu) || 2,
memory: parseFloat(customAgentMemory) || 4,
},
channelCredit: parseFloat(creditLimit) || 0,
}
console.log("[DEBUG] Request data:", JSON.stringify(requestData, null, 2))
await TaijiAPIClient.manageChannelResources(selectedChannel.id, requestData)
// 重新加载数据
await loadDashboardData()
setIsResourceManagementOpen(false)
setSelectedModels([])
setSelectedAgents([])
setAgentQuantities({})
setCreditLimit("")
} catch (error) {
console.error("Failed to save resource management:", error)
alert(language === "zh" ? "保存失败" : "Failed to save")
}
}
// 新增:查看渠道详情
const handleViewChannelDetails = async (channel: (typeof channels)[0]) => {
setSelectedChannel(channel)
setIsChannelDetailsDialogOpen(true)
setChannelAllocatedResources(null)
setChannelTenantsResources(null)
setChannelTenantsResourcesLoading(true)
// 可选:从API获取最新的渠道详情
try {
const response = await fetch(`/api/admin/channels/${channel.id}`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('admin_token') || localStorage.getItem('auth_token')}` }
})
if (response.ok) {
const data = await response.json()
if (data.success) {
setSelectedChannel(data.data)
}
}
} catch (error) {
console.error('Failed to fetch channel details:', error)
}
// 同时加载渠道资源分配详情(新接口)和租户资源分配(旧接口)
try {
const [allocatedResponse, tenantsResponse] = await Promise.all([
TaijiAPIClient.getChannelAllocatedResources(channel.id),
TaijiAPIClient.getChannelTenantsResources(channel.id)
])
if (allocatedResponse.success && allocatedResponse.data) {
setChannelAllocatedResources(allocatedResponse.data)
}
if (tenantsResponse.success && tenantsResponse.data) {
setChannelTenantsResources(tenantsResponse.data)
}
} catch (error) {
console.error('Failed to fetch channel resources:', error)
} finally {
setChannelTenantsResourcesLoading(false)
}
}
// 新增:编辑渠道信息
const handleEditChannel = async (channel: (typeof channels)[0]) => {
setSelectedChannel(channel)
setChannelEditForm({
name: channel.name,
contactName: channel.contact || "",
email: channel.email,
phone: channel.phone || "",
})
// 获取渠道管理员列表
try {
const token = localStorage.getItem('admin_token') || localStorage.getItem('auth_token')
const response = await fetch(`/api/admin/channels/${channel.id}/admins`, {
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
const data = await response.json()
if (data.success && data.data) {
setChannelAdmins(data.data.admins || [])
}
}
} catch (error) {
console.error('Failed to fetch channel admins:', error)
setChannelAdmins([])
}
setIsChannelEditDialogOpen(true)
}
// 新增:保存渠道编辑
const handleSaveChannelEdit = async () => {
if (!selectedChannel) return
try {
const token = localStorage.getItem('admin_token') || localStorage.getItem('auth_token')
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/admin/channels/${selectedChannel.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
name: channelEditForm.name,
email: channelEditForm.email,
contactName: channelEditForm.contactName,
phone: channelEditForm.phone
})
})
if (response.ok) {
const data = await response.json()
if (data.success) {
setSelectedChannel({
...selectedChannel,
name: channelEditForm.name,
email: channelEditForm.email,
contact: channelEditForm.contactName,
phone: channelEditForm.phone
})
setIsChannelEditDialogOpen(false)
console.log('Channel updated successfully')
}
} else {
console.error('Failed to update channel:', response.statusText)
}
} catch (error) {
console.error("Failed to save channel edit:", error)
}
}
// 新增:查看租户列表
const handleViewTenants = async (channel: (typeof channels)[0]) => {
setSelectedChannel(channel)
setIsViewTenantsDialogOpen(true)
setChannelTenantsLoading(true)
try {
const token = localStorage.getItem('admin_token') || localStorage.getItem('auth_token')
// 超级管理员查看渠道租户时,使用 /api/admin/tenants 接口并传递 channel_id 参数
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/admin/tenants?channel_id=${channel.id}`, {
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
const data = await response.json()
if (data.success && data.data?.tenants) {
setChannelTenants(data.data.tenants)
} else if (Array.isArray(data.data)) {
setChannelTenants(data.data)
} else {
setChannelTenants([])
}
} else {
console.error('Failed to fetch tenants:', response.statusText)
setChannelTenants([])
}
} catch (error) {
console.error("Failed to load tenants:", error)
setChannelTenants([])
} finally {
setChannelTenantsLoading(false)
}
}
// 新增:修改租户密码
const handleTenantChangePassword = async (tenantId: string) => {
const tenant = channelTenants.find(t => t.id === tenantId)
if (!tenant) return
setSelectedTenantForEdit(tenant)
setNewPassword("")
setConfirmPassword("")
setIsChangePasswordDialogOpen(true)
}
// 新增:执行密码修改
const handleConfirmChangePassword = async () => {
if (!selectedTenantForEdit) return
// 验证密码
if (!newPassword || newPassword.length < 6) {
alert(language === "zh" ? "密码长度至少6位" : "Password must be at least 6 characters")
return
}
if (newPassword !== confirmPassword) {
alert(language === "zh" ? "两次输入的密码不一致" : "Passwords do not match")
return
}
setChangePasswordLoading(true)
try {
// 超级管理员修改租户密码时,需要提供 channel_id 参数
const channelId = selectedChannel?.id
const result = await TaijiAPIClient.changeTenantPassword(selectedTenantForEdit.id, newPassword, channelId)
if (result?.success) {
alert(language === "zh" ? "密码修改成功" : "Password changed successfully")
setIsChangePasswordDialogOpen(false)
setNewPassword("")
setConfirmPassword("")
setSelectedTenantForEdit(null)
} else {
alert(result?.message || (language === "zh" ? "密码修改失败" : "Failed to change password"))
}
} catch (error: any) {
console.error("Failed to change password:", error)
alert(error?.message || (language === "zh" ? "密码修改出错" : "Error changing password"))
} finally {
setChangePasswordLoading(false)
}
}
// 新增:删除租户
const handleDeleteTenant = async (tenantId: string) => {
const tenant = channelTenants.find(t => t.id === tenantId)
if (!tenant) return
if (!window.confirm(language === "zh" ? `确定删除租户 ${tenant.name}?` : `Delete tenant ${tenant.name}?`)) return
try {
// 超级管理员删除租户时需要提供 channel_id
const result = await TaijiAPIClient.deleteTenant(tenantId, selectedChannel?.id)
if (result.success) {
setChannelTenants(channelTenants.filter(t => t.id !== tenantId))
console.log('Tenant deleted successfully')
} else {
console.error('Failed to delete tenant:', result.message || 'Unknown error')
}
} catch (error) {
console.error("Failed to delete tenant:", error)
}
}
// 新增:禁用租户
const handleDisableTenant = async (tenantId: string) => {
try {
const token = localStorage.getItem('admin_token') || localStorage.getItem('auth_token')
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/channel/tenants/${tenantId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ status: 'suspended' })
})
if (response.ok) {
const data = await response.json()
if (data.success) {
setChannelTenants(channelTenants.map(t =>
t.id === tenantId ? { ...t, status: "disabled" } : t
))
console.log('Tenant disabled successfully')
}
} else {
console.error('Failed to disable tenant:', response.statusText)
}
} catch (error) {
console.error("Failed to disable tenant:", error)
}
}
// 新增:删除管理员
const handleRemoveAdmin = async (adminId: string) => {
if (!selectedChannel) return
try {
const token = localStorage.getItem('admin_token') || localStorage.getItem('auth_token')
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/admin/channels/${selectedChannel.id}/admins/${adminId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
const data = await response.json()
if (data.success) {
// 从列表中移除该管理员
setChannelAdmins(channelAdmins.filter((admin: any) => admin.id !== adminId))
alert(language === "zh" ? "管理员已删除" : "Admin removed successfully")
} else {
alert(data.message || (language === "zh" ? "删除失败" : "Failed to remove"))
}
} else {
console.error('Failed to remove admin:', response.statusText)
alert(language === "zh" ? "删除失败" : "Failed to remove")
}
} catch (error) {
console.error("Failed to remove admin:", error)
alert(language === "zh" ? "删除出错" : "Error removing admin")
}
}
// 新增:创建租户或管理员
// 根据系统权限选择不同的后端接口:
// - tenant: 调用 /api/channel/tenants/create 创建租户
// - billing-admin/operations-admin: 调用 /api/admin/admins/create 创建管理员
const handleCreateTenant = async () => {
if (!selectedChannel || !newTenantForm.name || !newTenantForm.email || !newTenantForm.password) {
alert(language === "zh" ? "请填写所有必需字段" : "Please fill all required fields")
return
}
try {
console.log("Creating user with data:", {
name: newTenantForm.name,
email: newTenantForm.email,
systemRole: newTenantForm.systemRole,
channelId: selectedChannel.id,
})
let result: any
// 根据系统权限选择不同的接口
if (newTenantForm.systemRole === "tenant") {
// 创建普通租户 - 调用 /api/channel/tenants/create
result = await TaijiAPIClient.createChannelTenant({
name: newTenantForm.name,
email: newTenantForm.email,
password: newTenantForm.password,
subscriptionTier: "free", // 租户默认使用 free 订阅等级
channelId: selectedChannel.id, // 超级管理员创建租户时必须提供channelId
})
console.log("Create tenant result:", result)
} else if (newTenantForm.systemRole === "billing-admin" || newTenantForm.systemRole === "operations-admin") {
// 创建计费管理员或运营管理员 - 调用 /api/admin/admins/create
// 角色映射:billing-admin -> billing_admin, operations-admin -> operations_admin
const roleMap: Record<string, "billing_admin" | "operations_admin"> = {
"billing-admin": "billing_admin",
"operations-admin": "operations_admin"
}
// 构建请求数据,包含 channelId
const adminData = {
name: newTenantForm.name,
email: newTenantForm.email,
password: newTenantForm.password,
role: roleMap[newTenantForm.systemRole],
channelId: selectedChannel.id, // 超级管理员创建管理员时必须提供channelId
}
console.log("Creating admin with data:", adminData)
// 直接调用后端接口,因为 TaijiAPIClient.createAdmin 不支持 channelId 参数
const token = localStorage.getItem('admin_token') || localStorage.getItem('auth_token')
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/admin/admins/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify(adminData),
})
result = await response.json()
console.log("Create admin result:", result)
}
if (result?.success) {
const successMsg = newTenantForm.systemRole === "tenant"
? (language === "zh" ? "租户创建成功" : "Tenant created successfully")
: (language === "zh" ? "管理员创建成功" : "Admin created successfully")
alert(successMsg)
setNewTenantForm({ name: "", email: "", password: "", systemRole: "tenant" })
setIsAddTenantDialogOpen(false)
// 重新加载租户列表
if (selectedChannel) {
handleViewTenants(selectedChannel)
}
} else {
// 显示更详细的错误信息
const errorMsg = result?.message || result?.detail || result?.error?.message || (language === "zh" ? "创建失败" : "Failed to create")
alert(errorMsg)
}
} catch (error: any) {
console.error("Failed to create tenant/admin:", error)
// 显示更详细的错误信息
const errorMsg = error?.message || (language === "zh" ? "创建出错" : "Error creating")
alert(language === "zh" ? `创建出错: ${errorMsg}` : `Error creating: ${errorMsg}`)
}
}
// 新增:更新租户权限
const handleUpdateTenantPermissions = async () => {
if (!selectedTenantForPermission) {
alert(language === "zh" ? "请选择租户" : "Please select a tenant")
return
}
try {
const token = localStorage.getItem("admin_token") || localStorage.getItem("auth_token")
const response = await fetch(
`${API_BASE_URLS.mcpServer || "http://localhost:8002"}/api/channel/tenants/${selectedTenantForPermission.id}/permissions`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ permissions: tenantPermissions }),
}
)
if (response.ok) {
const data = await response.json()
if (data.success) {
alert(language === "zh" ? "权限更新成功" : "Permissions updated successfully")
setIsTenantPermissionDialogOpen(false)
setTenantPermissions([])
// 更新租户列表中的权限信息
setChannelTenants(
channelTenants.map((t) =>
t.id === selectedTenantForPermission.id ? { ...t, permissions: tenantPermissions } : t
)
)
}
} else {
alert(language === "zh" ? "权限更新失败" : "Failed to update permissions")
}
} catch (error) {
console.error("Failed to update permissions:", error)
alert(language === "zh" ? "权限更新出错" : "Error updating permissions")
}
}
const systemRoles = [
{
id: "tenant",
name: language === "zh" ? "租户" : "Tenant",
description: language === "zh" ? "普通租户,基础功能访问" : "Regular tenant, basic feature access",
},
{
id: "billing-admin",
name: language === "zh" ? "计费管理员" : "Billing Admin",
description: language === "zh" ? "计费管理权限,处理账单和支付" : "Billing management, handle invoices and payments",
},
{
id: "operations-admin",
name: language === "zh" ? "运营管理员" : "Operations Admin",
description: language === "zh" ? "运营管理权限,监控和资源运维" : "Operations management, monitoring and resource ops",
},
]
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="sticky top-0 z-50 border-b border-border bg-card/95 backdrop-blur-sm">
<div className="flex items-center justify-between px-6 py-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Shield className="h-6 w-6 text-primary" />
<div>
<h1 className="text-lg font-bold text-foreground">{text.title}</h1>
<p className="text-xs text-muted-foreground">{text.subtitle}</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<Select value={language} onValueChange={(value: "en" | "zh") => setLanguage(value)}>
<SelectTrigger className="w-[120px] bg-background border-border">
<Globe className="mr-2 h-4 w-4" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">English</SelectItem>
<SelectItem value="zh">中文</SelectItem>
</SelectContent>
</Select>
<Button
variant="ghost"
size="sm"
onClick={handleLogout}
className="text-muted-foreground hover:text-foreground"
>
<LogOut className="h-4 w-4 mr-2" />
{text.logout}
</Button>
</div>
</div>
{/* Navigation tabs */}
<div className="px-6 flex gap-6 border-t border-border overflow-x-auto">
{[
{ id: "overview", label: text.overview, icon: BarChart3 },
{ id: "channels", label: text.channels, icon: Building2 },
{ id: "resources", label: text.resources, icon: Database },
{ id: "monitoring", label: text.monitoring, icon: Activity },
{ id: "billing", label: text.billing, icon: DollarSign },
{ id: "settings", label: text.settings, icon: Settings },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-3 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
activeTab === tab.id
? "border-primary text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
))}
</div>
</header>
{/* Main content */}
<main className="p-6 space-y-6">
{activeTab === "overview" && (
<>
{/* Stats grid - 删除活跃租户,保留3个统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{stats.filter((stat, index) => index !== 2).map((stat, index) => (
<Card key={index} className="p-6 bg-card border-border">
<div className="flex items-start justify-between">
<div>
<p className="text-sm text-muted-foreground mb-1">{stat.title}</p>
<p className="text-3xl font-bold text-foreground mb-2">{stat.value}</p>
<div className="flex items-center gap-1">
<TrendingUp className="h-3 w-3 text-green-500" />
<span className="text-xs text-green-500">{stat.change}</span>
</div>
</div>
<div className="p-3 rounded-lg bg-primary/10">
<stat.icon className="h-5 w-5 text-primary" />
</div>
</div>
</Card>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* System Metrics - 保留系统指标 */}
<Card className="lg:col-span-1 p-6 bg-card border-border">
<h3 className="text-lg font-semibold text-foreground mb-4">{text.systemMetrics}</h3>
<div className="space-y-4">
{systemMetrics.map((metric, index) => (
<div key={index}>
<div className="flex justify-between text-sm mb-2">
<span className="text-muted-foreground">{metric.label}</span>
<span className="text-foreground font-medium">
{typeof metric.value === "number" && metric.value < 200 ? `${metric.value}%` : metric.value}
</span>
</div>
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className={`h-full ${metric.color} transition-all duration-300`}
style={{ width: `${(metric.value / metric.max) * 100}%` }}
/>
</div>
</div>
))}
</div>
</Card>
{/* Recent Logins - 最近登录的租户列表 */}
<Card className="lg:col-span-2 p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground">
{language === "zh" ? "最近登录的租户" : "Recent Tenant Logins"}
</h3>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input placeholder={text.search} className="pl-9 w-[240px] bg-background border-border" />
</div>
</div>
<div className="space-y-3">
{loading ? (
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "加载中..." : "Loading..."}</span>
</div>
) : recentLogins.length === 0 ? (
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "暂无登录记录" : "No recent logins"}</span>
</div>
) : (
recentLogins.map((tenant) => (
<div
key={tenant.tenantId || tenant.id}
className="flex items-center justify-between p-4 rounded-lg bg-background border border-border hover:border-primary/50 transition-colors"
>
<div className="flex items-center gap-4 flex-1">
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
<Users className="h-5 w-5 text-primary" />
</div>
<div className="flex-1">
<p className="font-medium text-foreground">{tenant.tenantName || tenant.name}</p>
<p className="text-xs text-muted-foreground">
{tenant.email} · {tenant.channelName || (language === "zh" ? "直属" : "Direct")}
</p>
</div>
</div>
<div className="ml-4">
<span className={`px-2 py-1 rounded text-xs font-medium ${
tenant.status === "active"
? "bg-green-500/10 text-green-500"
: "bg-gray-500/10 text-gray-500"
}`}>
{tenant.status === "active"
? (language === "zh" ? "活跃" : "Active")
: (language === "zh" ? "离线" : "Offline")}
</span>
</div>
</div>
))
)}
</div>
</Card>
</div>
{/* Platform Resource Allocation - 替换系统警告,只显示CPU和内存 */}
<Card className="p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground">
{language === "zh" ? "平台资源分配统计" : "Platform Resource Allocation"}
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* CPU Allocation */}
<div className="p-4 rounded-lg bg-blue-500/10 border border-blue-500/20">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-lg bg-blue-500/20">
<Cpu className="h-5 w-5 text-blue-500" />
</div>
<div>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "已分配 CPU" : "Allocated CPU"}
</p>
<p className="text-xl font-bold text-foreground">
{resourceAllocationStats.totalCpu.toFixed(1)} {language === "zh" ? "核" : "Cores"}
</p>
</div>
</div>
<div className="text-xs text-muted-foreground">
{language === "zh"
? `共 ${resourceAllocationStats.totalCount} 个 Agent`
: `${resourceAllocationStats.totalCount} Agents`}
</div>
</div>
{/* Memory Allocation */}
<div className="p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-lg bg-green-500/20">
<Server className="h-5 w-5 text-green-500" />
</div>
<div>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "已分配内存" : "Allocated Memory"}
</p>
<p className="text-xl font-bold text-foreground">
{resourceAllocationStats.totalMemory.toFixed(1)} GB
</p>
</div>
</div>
<div className="text-xs text-muted-foreground">
{language === "zh"
? `平均 ${resourceAllocationStats.totalCount > 0 ? (resourceAllocationStats.totalMemory / resourceAllocationStats.totalCount).toFixed(1) : 0} GB/Agent`
: `Avg ${resourceAllocationStats.totalCount > 0 ? (resourceAllocationStats.totalMemory / resourceAllocationStats.totalCount).toFixed(1) : 0} GB/Agent`}
</div>
</div>
</div>
{/* Agent 数量分类统计 */}
<div className="grid grid-cols-2 gap-4 mt-4 pt-4 border-t border-border">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-purple-500"></div>
<span className="text-sm text-muted-foreground">
{language === "zh" ? "平台 Agent" : "Platform Agents"}:
</span>
<span className="text-sm font-medium text-foreground">
{resourceAllocationStats.platformCount} {language === "zh" ? "个" : ""}
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500"></div>
<span className="text-sm text-muted-foreground">
{language === "zh" ? "自定义 Agent" : "Custom Agents"}:
</span>
<span className="text-sm font-medium text-foreground">
{resourceAllocationStats.customCount} {language === "zh" ? "个" : ""}
</span>
</div>
</div>
</Card>
</>
)}
{activeTab === "channels" && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-foreground">{text.channelManagement}</h2>
<p className="text-sm text-muted-foreground mt-1">{text.channelDescription}</p>
</div>
<Dialog open={isAddChannelOpen} onOpenChange={setIsAddChannelOpen}>
<DialogTrigger asChild>
<Button className="bg-primary text-primary-foreground">
<Plus className="h-4 w-4 mr-2" />
{text.addChannel}
</Button>
</DialogTrigger>
<DialogContent className="bg-card border-border">
<DialogHeader>
<DialogTitle>{text.createChannel}</DialogTitle>
<DialogDescription>
{language === "zh"
? "创建新的分销渠道账户,渠道可以管理自己的租户组合"
: "Create a new distribution channel account that can manage their own tenant portfolio"}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="channelName">{text.channelName}</Label>
<Input
id="channelName"
placeholder={text.channelNamePlaceholder}
className="bg-background"
value={newChannelForm.name}
onChange={(e) => setNewChannelForm({ ...newChannelForm, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="contactEmail">{text.contactEmail}</Label>
<Input
id="contactEmail"
type="email"
placeholder="contact@example.com"
className="bg-background"
value={newChannelForm.email}
onChange={(e) => setNewChannelForm({ ...newChannelForm, email: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">{text.password}</Label>
<Input
id="password"
type="password"
placeholder={language === "zh" ? "输入密码" : "Enter password"}
className="bg-background"
value={newChannelForm.password}
onChange={(e) => setNewChannelForm({ ...newChannelForm, password: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="commission">{text.commissionRate}</Label>
<Input
id="commission"
type="number"
placeholder="15"
className="bg-background"
value={newChannelForm.commissionRate}
onChange={(e) => setNewChannelForm({ ...newChannelForm, commissionRate: e.target.value })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddChannelOpen(false)}>
{text.cancel}
</Button>
<Button
className="bg-primary text-primary-foreground"
onClick={async () => {
try {
await TaijiAPIClient.createAdminChannel({
name: newChannelForm.name,
email: newChannelForm.email,
password: newChannelForm.password,
commissionRate: parseFloat(newChannelForm.commissionRate) || 0,
})
// 重新加载数据
await loadDashboardData()
setIsAddChannelOpen(false)
setNewChannelForm({ name: "", email: "", password: "", commissionRate: "" })
} catch (error) {
console.error("Failed to create channel:", error)
alert(language === "zh" ? "创建失败" : "Failed to create")
}
}}
>
{text.create}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="relative mb-6">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input placeholder={text.searchChannels} className="pl-9 bg-card border-border" />
</div>
<div className="grid grid-cols-1 gap-4">
{loading ? (
<Card className="p-6 bg-card border-border">
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "加载中..." : "Loading..."}</span>
</div>
</Card>
) : channels.length === 0 ? (
<Card className="p-6 bg-card border-border">
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "暂无渠道" : "No channels available"}</span>
</div>
</Card>
) : (
channels.map((channel) => (
<Card key={channel.id} className="p-6 bg-card border-border hover:border-primary/50 transition-colors">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1">
<div className="w-14 h-14 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
<Building2 className="h-7 w-7 text-primary" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold text-foreground">{channel.name}</h3>
<div
className={`px-2 py-1 rounded text-xs font-medium ${
channel.status === "active"
? "bg-green-500/10 text-green-500"
: "bg-gray-500/10 text-gray-500"
}`}
>
{language === "zh" ? (channel.status === "active" ? "活跃" : "停用") : channel.status}
</div>
</div>
<div className="space-y-1 text-sm text-muted-foreground">
<p>
{text.contactPerson}: {channel.contact}
</p>
<p>{channel.email}</p>
</div>
</div>
</div>
<div className="flex items-start gap-8 ml-6">
<div className="text-center">
<p className="text-2xl font-bold text-foreground">{channel.tenantCount}</p>
<p className="text-xs text-muted-foreground mt-1">{text.tenantCount}</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-foreground">{channel.revenue}</p>
<p className="text-xs text-muted-foreground mt-1">{language === "zh" ? "月收入" : "Monthly"}</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-foreground">{channel.commission}%</p>
<p className="text-xs text-muted-foreground mt-1">{text.commission}</p>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="ml-4">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="bg-card border-border">
<DropdownMenuItem onClick={() => handleViewChannelDetails(channel)}>
<Eye className="h-4 w-4 mr-2" />
{text.viewDetails}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEditChannel(channel)}>
<Edit className="h-4 w-4 mr-2" />
{text.edit}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleViewTenants(channel)}>
<Users className="h-4 w-4 mr-2" />
{language === "zh" ? "查看租户" : "View Tenants"}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEditCommission(channel)}>
<DollarSign className="h-4 w-4 mr-2" />
{language === "zh" ? "修改佣金" : "Edit Commission"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={async () => {
if (!window.confirm(
language === "zh"
? `确定要删除渠道 "${channel.name}" 吗?此操作不可撤销。`
: `Are you sure you want to delete channel "${channel.name}"? This action cannot be undone.`
)) {
return
}
try {
const token = localStorage.getItem('admin_token') || localStorage.getItem('auth_token')
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/admin/channels/${channel.id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
const data = await response.json()
if (data.success) {
alert(language === "zh" ? "渠道删除成功" : "Channel deleted successfully")
await loadDashboardData()
} else {
alert(data.message || (language === "zh" ? "删除失败" : "Failed to delete"))
}
} else {
const errorData = await response.json().catch(() => ({}))
alert(errorData.message || (language === "zh" ? "删除失败" : "Failed to delete"))
}
} catch (error) {
console.error("Failed to delete channel:", error)
alert(language === "zh" ? "删除渠道出错" : "Error deleting channel")
}
}}
>
<Trash2 className="h-4 w-4 mr-2" />
{language === "zh" ? "删除渠道" : "Delete Channel"}
</DropdownMenuItem>
{/* <DropdownMenuItem onClick={() => handleConfigGoods(channel)}> // REMOVED */}
{/* <Package className="h-4 w-4 mr-2" /> */}
{/* {language === "zh" ? "货源配置" : "Goods Configuration"} */}
{/* </DropdownMenuItem> */}
{/* <DropdownMenuItem onClick={() => handleAgentAllocation(channel)}> // REMOVED */}
{/* <Bot className="h-4 w-4 mr-2" /> */}
{/* {language === "zh" ? "Agent分配" : "Agent Allocation"} */}
{/* </DropdownMenuItem> */}
{/* CHANGED: Combine goods configuration and agent allocation into resource management */}
<DropdownMenuItem onClick={() => handleResourceManagement(channel)}>
<Settings className="h-4 w-4 mr-2" />
{language === "zh" ? "资源管理" : "Resource Management"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Card>
))
)}
</div>
{/* Commission Edit Dialog */}
<Dialog open={isCommissionDialogOpen} onOpenChange={setIsCommissionDialogOpen}>
<DialogContent className="bg-card border-border">
<DialogHeader>
<DialogTitle>{language === "zh" ? "修改佣金比例" : "Edit Commission Rate"}</DialogTitle>
<DialogDescription>
{language === "zh"
? `为渠道 "${selectedChannel?.name}" 设置新的佣金比例`
: `Set a new commission rate for channel "${selectedChannel?.name}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="commission-rate">
{language === "zh" ? "佣金比例 (%)" : "Commission Rate (%)"}
</Label>
<Input
id="commission-rate"
type="number"
min="0"
max="100"
step="0.1"
placeholder={language === "zh" ? "例: 15" : "e.g., 15"}
value={commissionRate}
onChange={(e) => setCommissionRate(e.target.value)}
className="bg-background border-border"
/>
<p className="text-xs text-muted-foreground">
{language === "zh"
? "当前佣金比例: " + selectedChannel?.commission + "%"
: "Current rate: " + selectedChannel?.commission + "%"}
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCommissionDialogOpen(false)}>
{text.cancel}
</Button>
<Button className="bg-primary text-primary-foreground" onClick={handleSaveCommission}>
{language === "zh" ? "保存" : "Save"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* CHANGED: Combined Goods Configuration and Agent Allocation into a single Resource Management Dialog */}
{/* <Dialog open={isConfigGoodsOpen} onOpenChange={setIsConfigGoodsOpen}> // REMOVED */}
<Dialog open={isResourceManagementOpen} onOpenChange={setIsResourceManagementOpen}>
<DialogContent className="bg-card border-border max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{language === "zh" ? "资源管理" : "Resource Management"}</DialogTitle>
<DialogDescription>
{language === "zh"
? `为渠道 "${selectedChannel?.name}" 配置可用的模型、Agent和配额`
: `Configure available models, agents and quota for channel "${selectedChannel?.name}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Models Selection */}
<div className="space-y-3">
<Label className="text-base font-semibold">
{language === "zh" ? "模型供应商" : "Model Providers"}
</Label>
<div className="grid grid-cols-1 gap-2">
{availableModels.map((model) => (
<div key={model.id} className="flex items-center space-x-2">
<input
type="checkbox"
id={`model-${model.id}`}
checked={selectedModels.includes(model.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedModels([...selectedModels, model.id])
} else {
setSelectedModels(selectedModels.filter((m) => m !== model.id))
}
}}
className="w-4 h-4 rounded border-border"
/>
<label htmlFor={`model-${model.id}`} className="text-sm font-medium cursor-pointer flex-1">
{model.name}
<span className="text-xs text-muted-foreground ml-2">({model.provider})</span>
</label>
</div>
))}
</div>
</div>
{/* Agent Selection with Quantities */}
<div className="space-y-3">
<Label className="text-base font-semibold">
{language === "zh" ? "Agent分配" : "Agent Allocation"}
</Label>
<p className="text-sm text-muted-foreground">
{language === "zh"
? "选择该渠道可以分配给租户的Agent并设置可用数量"
: "Select agents that this channel can assign to tenants and set available quantities"}
</p>
<div className="grid grid-cols-1 gap-3 mt-4">
{availableAgents.map((agent) => {
// 使用 agent.id 或 agent.name 作为唯一标识符
// 后端返回的 Agent 模板数据中 name 是唯一的,id 可能为 undefined
const agentKey = agent.id || agent.name
const isSelected = selectedAgents.includes(agentKey)
return (
<div
key={agentKey}
className={`border rounded-lg p-4 transition-all ${
isSelected ? "border-primary bg-primary/5" : "border-border"
}`}
>
<div className="flex items-center justify-between gap-4">
<div
className="flex items-center gap-3 flex-1 cursor-pointer"
onClick={() => {
if (isSelected) {
setSelectedAgents(selectedAgents.filter((a) => a !== agentKey))
const newQuantities = { ...agentQuantities }
delete newQuantities[agentKey]
setAgentQuantities(newQuantities)
} else {
setSelectedAgents([...selectedAgents, agentKey])
setAgentQuantities({ ...agentQuantities, [agentKey]: 1 })
}
}}
>
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center ${
isSelected ? "bg-primary/20" : "bg-muted"
}`}
>
<Bot
className={`h-5 w-5 ${isSelected ? "text-primary" : "text-muted-foreground"}`}
/>
</div>
<div>
<p className="font-medium text-foreground">{agent.name}</p>
<p className="text-xs text-muted-foreground">{agent.type}</p>
</div>
</div>
{isSelected && (
<div className="flex items-center gap-2">
<Label className="text-sm text-muted-foreground whitespace-nowrap">
{language === "zh" ? "数量:" : "Quantity:"}
</Label>
<Input
type="number"
min="1"
max="100"
value={agentQuantities[agentKey] || 1}
onChange={(e) => {
const value = Math.max(1, Math.min(100, Number.parseInt(e.target.value) || 1))
setAgentQuantities({ ...agentQuantities, [agentKey]: value })
}}
className="w-20 h-9"
onClick={(e) => e.stopPropagation()}
/>
</div>
)}
<input
type="checkbox"
checked={isSelected}
onChange={(e) => {
e.stopPropagation()
if (isSelected) {
setSelectedAgents(selectedAgents.filter((a) => a !== agentKey))
const newQuantities = { ...agentQuantities }
delete newQuantities[agentKey]
setAgentQuantities(newQuantities)
} else {
setSelectedAgents([...selectedAgents, agentKey])
setAgentQuantities({ ...agentQuantities, [agentKey]: 1 })
}
}}
className="w-5 h-5 rounded border-border cursor-pointer"
/>
</div>
</div>
)
})}
</div>
</div>
<div className="space-y-3 border-t border-border pt-4">
<Label className="text-base font-semibold">
{language === "zh" ? "自定义Agent资源配置" : "Custom Agent Resource Configuration"}
</Label>
<p className="text-sm text-muted-foreground">
{language === "zh"
? "为该渠道的租户创建的自定义Agent配置默认CPU和内存资源"
: "Configure default CPU and memory resources for custom agents created by this channel's tenants"}
</p>
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="space-y-2">
<Label htmlFor="custom-cpu">{language === "zh" ? "CPU核数" : "CPU Cores"}</Label>
<Input
id="custom-cpu"
type="number"
min="0.5"
max="16"
step="0.5"
value={customAgentCpu}
onChange={(e) => setCustomAgentCpu(e.target.value)}
className="bg-background border-border"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "推荐范围: 0.5-16核" : "Recommended: 0.5-16 cores"}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="custom-memory">
{language === "zh" ? "内存大小(GB)" : "Memory Size (GB)"}
</Label>
<Input
id="custom-memory"
type="number"
min="0.5"
max="64"
step="0.5"
value={customAgentMemory}
onChange={(e) => setCustomAgentMemory(e.target.value)}
className="bg-background border-border"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "推荐范围: 0.5-64GB" : "Recommended: 0.5-64GB"}
</p>
</div>
</div>
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 mt-2">
<p className="text-xs text-muted-foreground">
{language === "zh"
? "此配置将应用于该渠道的租户创建的所有自定义Agent,不影响平台原生Agent。"
: "This configuration will apply to all custom agents created by this channel's tenants, not affecting platform native agents."}
</p>
</div>
</div>
{/* REMOVED monthly quota field */}
<div className="space-y-2">
<Label htmlFor="credit-limit">{language === "zh" ? "渠道授信额度" : "Channel Credit Limit"}</Label>
<Input
id="credit-limit"
type="number"
placeholder={language === "zh" ? "例: 10000" : "e.g., 10000"}
value={creditLimit}
onChange={(e) => setCreditLimit(e.target.value)}
className="bg-background border-border"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "渠道可用授信上限 (USD)" : "Channel credit limit (USD)"}
</p>
</div>
<div className="bg-primary/5 border border-primary/20 rounded-lg p-4">
<p className="text-sm font-semibold text-foreground mb-2">
{language === "zh" ? "资源配置摘要" : "Resource Configuration Summary"}
</p>
<div className="space-y-1 text-xs text-muted-foreground">
<p>
{language === "zh" ? "已选择模型" : "Models selected"}: {selectedModels.length}
</p>
<p>
{language === "zh" ? "已选择Agent" : "Agents selected"}: {selectedAgents.length}
</p>
{selectedAgents.length > 0 && (
<p>
{language === "zh" ? "总Agent数量: " : "Total agents: "}
{Object.values(agentQuantities).reduce((sum, qty) => sum + qty, 0)}
</p>
)}
<p className="pt-2 border-t border-border/50">
{language === "zh" ? "自定义Agent CPU: " : "Custom Agent CPU: "}
{customAgentCpu} {language === "zh" ? "核" : "cores"}
</p>
<p>
{language === "zh" ? "自定义Agent 内存: " : "Custom Agent Memory: "}
{customAgentMemory} GB
</p>
{/* REMOVED monthly quota from summary */}
<p>
{language === "zh" ? "渠道授信额度" : "Channel Credit Limit"}: ${creditLimit || "0"}
</p>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsResourceManagementOpen(false)}>
{text.cancel}
</Button>
<Button className="bg-primary text-primary-foreground" onClick={handleSaveResourceManagement}>
{language === "zh" ? "保存配置" : "Save Configuration"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Channel Details Dialog */}
<Dialog open={isChannelDetailsDialogOpen} onOpenChange={setIsChannelDetailsDialogOpen}>
<DialogContent className="bg-card border-border max-w-3xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{language === "zh" ? "渠道详情" : "Channel Details"}</DialogTitle>
<DialogDescription>
{language === "zh" ? `查看渠道 "${selectedChannel?.name}" 的详细信息` : `View details for channel "${selectedChannel?.name}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Basic Information */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-muted-foreground">
{language === "zh" ? "渠道名称" : "Channel Name"}
</label>
<p className="text-base font-medium text-foreground">{selectedChannel?.name}</p>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-muted-foreground">
{language === "zh" ? "状态" : "Status"}
</label>
<Badge variant="secondary" className="bg-green-500/10 text-green-600 border-green-500/20">
{language === "zh" ? "活跃" : "Active"}
</Badge>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-muted-foreground">
{language === "zh" ? "联系人邮箱" : "Contact Email"}
</label>
<p className="text-base font-medium text-foreground">{selectedChannel?.email}</p>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-muted-foreground">
{language === "zh" ? "创建日期" : "Created Date"}
</label>
<p className="text-base font-medium text-foreground">{selectedChannel?.createdAt || "-"}</p>
</div>
</div>
{/* Quota Information */}
<div className="border-t border-border pt-6">
<h4 className="text-base font-semibold text-foreground mb-4">
{language === "zh" ? "配额信息" : "Quota Information"}
</h4>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">
{language === "zh" ? "租户总数" : "Total Tenants"}
</span>
<span className="text-lg font-semibold text-foreground">{selectedChannel?.tenantCount ?? 0}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">
{language === "zh" ? "授信额度" : "Credit Limit"}
</span>
<span className="text-lg font-semibold text-foreground">
${selectedChannel?.creditLimit ?? 0}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">
{language === "zh" ? "已用授信" : "Used Credit"}
</span>
<span className="text-lg font-semibold text-foreground">
${selectedChannel?.usedCredit ?? 0}
</span>
</div>
<div className="border-t border-border pt-3 mt-3">
<div className="flex justify-between items-center">
<span className="text-sm font-semibold text-foreground">
{language === "zh" ? "剩余授信" : "Remaining Credit"}
</span>
<span className="text-lg font-bold text-green-600">
${selectedChannel?.remainingCredit ?? 0}
</span>
</div>
</div>
</div>
</div>
{/* Channel Resource Allocation Details */}
<div className="border-t border-border pt-6">
<h4 className="text-base font-semibold text-foreground mb-4">
{language === "zh" ? "渠道资源分配详情" : "Channel Resource Allocation Details"}
</h4>
{channelTenantsResourcesLoading ? (
<div className="text-center py-4 text-muted-foreground">
{language === "zh" ? "加载中..." : "Loading..."}
</div>
) : channelAllocatedResources ? (
<div className="space-y-6">
{/* Summary Statistics */}
{channelAllocatedResources.summary && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 p-4 bg-muted/30 rounded-lg">
<div className="text-center">
<p className="text-lg font-bold text-foreground">
{channelAllocatedResources.summary.totalCustomAgentCpuQuota || 0}
</p>
<p className="text-xs text-muted-foreground">{language === "zh" ? "自定义Agent CPU配额" : "Custom Agent CPU Quota"}</p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-foreground">
{channelAllocatedResources.summary.totalCustomAgentMemoryQuota || 0}GB
</p>
<p className="text-xs text-muted-foreground">{language === "zh" ? "自定义Agent内存配额" : "Custom Agent Memory Quota"}</p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-foreground">
{channelAllocatedResources.summary.totalPlatformAgentPodUsed || 0}/{channelAllocatedResources.summary.totalPlatformAgentPodQuota || 0}
</p>
<p className="text-xs text-muted-foreground">{language === "zh" ? "平台Agent Pod使用" : "Platform Agent Pods Used"}</p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-foreground">
{channelAllocatedResources.summary.totalModels || 0}
</p>
<p className="text-xs text-muted-foreground">{language === "zh" ? "模型总数" : "Total Models"}</p>
</div>
</div>
)}
{/* Custom Agent Quota */}
{channelAllocatedResources.customAgentQuota && (
<div className="p-4 border border-border rounded-lg">
<h5 className="font-medium text-foreground mb-3 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-purple-500"></span>
{language === "zh" ? "自定义 Agent 配额" : "Custom Agent Quota"}
</h5>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<p className="text-muted-foreground mb-1">CPU {language === "zh" ? "配额" : "Quota"}</p>
<p className="font-semibold text-foreground">
{channelAllocatedResources.customAgentQuota.cpuAllocatedToTenants || 0} / {channelAllocatedResources.customAgentQuota.cpuQuota || 0} {language === "zh" ? "核" : "cores"}
</p>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "可用" : "Available"}: {channelAllocatedResources.customAgentQuota.cpuAvailable || 0} {language === "zh" ? "核" : "cores"}
</p>
</div>
<div>
<p className="text-muted-foreground mb-1">{language === "zh" ? "内存配额" : "Memory Quota"}</p>
<p className="font-semibold text-foreground">
{channelAllocatedResources.customAgentQuota.memoryAllocatedToTenants || 0} / {channelAllocatedResources.customAgentQuota.memoryQuota || 0} GB
</p>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "可用" : "Available"}: {channelAllocatedResources.customAgentQuota.memoryAvailable || 0} GB
</p>
</div>
<div className="flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 rounded-full border-4 border-purple-500/30 flex items-center justify-center">
<span className="text-lg font-bold text-purple-500">
{channelAllocatedResources.customAgentQuota.cpuQuota > 0
? Math.round((channelAllocatedResources.customAgentQuota.cpuAllocatedToTenants / channelAllocatedResources.customAgentQuota.cpuQuota) * 100)
: 0}%
</span>
</div>
<p className="text-xs text-muted-foreground mt-1">{language === "zh" ? "使用率" : "Usage"}</p>
</div>
</div>
</div>
</div>
)}
{/* Platform Agents */}
{channelAllocatedResources.platformAgents && channelAllocatedResources.platformAgents.length > 0 && (
<div className="p-4 border border-border rounded-lg">
<h5 className="font-medium text-foreground mb-3 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-blue-500"></span>
{language === "zh" ? "平台 Agent 配额" : "Platform Agent Quota"}
<Badge variant="secondary" className="text-xs ml-2">{channelAllocatedResources.platformAgents.length}</Badge>
</h5>
<div className="space-y-3 max-h-[200px] overflow-y-auto">
{channelAllocatedResources.platformAgents.map((agent: any, idx: number) => (
<div key={agent.templateName || idx} className="flex items-center justify-between p-3 bg-background rounded-lg">
<div>
<p className="font-medium text-foreground">{agent.templateDisplayName || agent.templateName}</p>
<p className="text-xs text-muted-foreground">
{agent.cpuPerPod} CPU | {agent.memoryPerPod} {language === "zh" ? "内存" : "Memory"}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-foreground">
{agent.podUsed || 0} / {agent.podQuota || 0} Pods
</p>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "已分配租户" : "Allocated to tenants"}: {agent.podAllocatedToTenants || 0}
</p>
</div>
</div>
))}
</div>
</div>
)}
{/* Model Providers */}
{channelAllocatedResources.modelProviders && channelAllocatedResources.modelProviders.length > 0 && (
<div className="p-4 border border-border rounded-lg">
<h5 className="font-medium text-foreground mb-3 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-orange-500"></span>
{language === "zh" ? "模型供应商" : "Model Providers"}
<Badge variant="secondary" className="text-xs ml-2">{channelAllocatedResources.modelProviders.length}</Badge>
</h5>
<div className="space-y-3 max-h-[200px] overflow-y-auto">
{channelAllocatedResources.modelProviders.map((provider: any, idx: number) => (
<div key={provider.providerId || idx} className="p-3 bg-background rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<p className="font-medium text-foreground">{provider.providerName}</p>
<Badge variant={provider.status === "active" ? "default" : "secondary"} className="text-xs">
{provider.status === "active" ? (language === "zh" ? "活跃" : "Active") : provider.status}
</Badge>
</div>
<span className="text-xs text-muted-foreground">{provider.providerType}</span>
</div>
{provider.models && provider.models.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{provider.models.map((model: any, midx: number) => (
<Badge key={midx} variant="outline" className="text-xs">
{model.modelName}
<span className="ml-1 text-muted-foreground">
({model.rpmLimit} RPM)
</span>
</Badge>
))}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Models */}
{channelAllocatedResources.models && channelAllocatedResources.models.length > 0 && (
<div className="p-4 border border-border rounded-lg">
<h5 className="font-medium text-foreground mb-3 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green-500"></span>
{language === "zh" ? "模型配额" : "Model Quota"}
<Badge variant="secondary" className="text-xs ml-2">{channelAllocatedResources.models.length}</Badge>
</h5>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 text-muted-foreground font-medium">{language === "zh" ? "模型" : "Model"}</th>
<th className="text-left py-2 text-muted-foreground font-medium">{language === "zh" ? "供应商" : "Provider"}</th>
<th className="text-center py-2 text-muted-foreground font-medium">RPM</th>
<th className="text-center py-2 text-muted-foreground font-medium">TPM</th>
<th className="text-center py-2 text-muted-foreground font-medium">{language === "zh" ? "已分配租户" : "Allocated"}</th>
</tr>
</thead>
<tbody>
{channelAllocatedResources.models.map((model: any, idx: number) => (
<tr key={model.modelName || idx} className="border-b border-border/50 last:border-0">
<td className="py-2 font-medium text-foreground">{model.modelName}</td>
<td className="py-2 text-muted-foreground">{model.providerName || '-'}</td>
<td className="py-2 text-center">{model.rpmLimit?.toLocaleString() || '-'}</td>
<td className="py-2 text-center">{model.tpmLimit?.toLocaleString() || '-'}</td>
<td className="py-2 text-center">{model.allocatedToTenants || 0}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* No resources allocated */}
{!channelAllocatedResources.customAgentQuota &&
(!channelAllocatedResources.platformAgents || channelAllocatedResources.platformAgents.length === 0) &&
(!channelAllocatedResources.models || channelAllocatedResources.models.length === 0) && (
<div className="text-center py-8 text-muted-foreground">
{language === "zh" ? "暂无资源分配" : "No resources allocated"}
</div>
)}
</div>
) : (
<div className="text-center py-4 text-muted-foreground">
{language === "zh" ? "暂无数据" : "No data available"}
</div>
)}
</div>
{/* Tenant Resource Allocation */}
<div className="border-t border-border pt-6">
<h4 className="text-base font-semibold text-foreground mb-4">
{language === "zh" ? "租户资源分配" : "Tenant Resource Allocation"}
</h4>
{channelTenantsResourcesLoading ? (
<div className="text-center py-4 text-muted-foreground">
{language === "zh" ? "加载中..." : "Loading..."}
</div>
) : channelTenantsResources ? (
<div className="space-y-4">
{/* Tenant Summary */}
{channelTenantsResources.summary && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 p-3 bg-muted/30 rounded-lg">
<div className="text-center">
<p className="text-lg font-bold text-foreground">{channelTenantsResources.summary.totalTenants || 0}</p>
<p className="text-xs text-muted-foreground">{language === "zh" ? "租户总数" : "Total Tenants"}</p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-foreground">{channelTenantsResources.summary.tenantsWithCustomAgents || 0}</p>
<p className="text-xs text-muted-foreground">{language === "zh" ? "有自定义Agent" : "With Custom Agents"}</p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-foreground">{channelTenantsResources.summary.tenantsWithPlatformAgents || 0}</p>
<p className="text-xs text-muted-foreground">{language === "zh" ? "有平台Agent" : "With Platform Agents"}</p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-foreground">{channelTenantsResources.summary.tenantsWithModels || 0}</p>
<p className="text-xs text-muted-foreground">{language === "zh" ? "有模型配额" : "With Models"}</p>
</div>
</div>
)}
{/* Tenants List */}
{channelTenantsResources.tenants && channelTenantsResources.tenants.length > 0 ? (
<div className="space-y-3 max-h-[300px] overflow-y-auto">
{channelTenantsResources.tenants.map((tenant: any) => (
<div key={tenant.tenantId} className="p-3 rounded-lg border border-border bg-background">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">{tenant.tenantName}</span>
<Badge variant={tenant.status === "active" ? "default" : "secondary"} className="text-xs">
{tenant.status === "active" ? (language === "zh" ? "活跃" : "Active") : (language === "zh" ? "已暂停" : "Suspended")}
</Badge>
</div>
<span className="text-xs text-muted-foreground">{tenant.tenantEmail}</span>
</div>
<div className="space-y-2 text-sm">
{/* Custom Agent Quota */}
{tenant.customAgentQuota && (
<div className="flex items-center gap-4 text-muted-foreground">
<span className="text-purple-500">●</span>
<span>{language === "zh" ? "自定义Agent" : "Custom Agent"}:</span>
<span>CPU {tenant.customAgentQuota.cpuUsed}/{tenant.customAgentQuota.cpuQuota}</span>
<span>{language === "zh" ? "内存" : "Mem"} {tenant.customAgentQuota.memoryUsed}/{tenant.customAgentQuota.memoryQuota}GB</span>
</div>
)}
{/* Platform Agents */}
{tenant.platformAgents && tenant.platformAgents.length > 0 && (
<div className="flex items-center gap-4 text-muted-foreground flex-wrap">
<span className="text-blue-500">●</span>
<span>{language === "zh" ? "平台Agent" : "Platform Agent"}:</span>
{tenant.platformAgents.map((agent: any, idx: number) => (
<span key={idx}>{agent.templateName} ({agent.podUsed}/{agent.podQuota})</span>
))}
</div>
)}
{/* Models */}
{tenant.models && tenant.models.length > 0 && (
<div className="flex items-center gap-4 text-muted-foreground flex-wrap">
<span className="text-green-500">●</span>
<span>{language === "zh" ? "模型" : "Models"}:</span>
{tenant.models.map((model: any, idx: number) => (
<span key={idx}>{model.modelName}</span>
))}
</div>
)}
{/* No resources */}
{!tenant.customAgentQuota &&
(!tenant.platformAgents || tenant.platformAgents.length === 0) &&
(!tenant.models || tenant.models.length === 0) && (
<div className="text-muted-foreground text-xs">
{language === "zh" ? "暂无资源分配" : "No resources allocated"}
</div>
)}
</div>
</div>
))}
</div>
) : (
<div className="text-center py-4 text-muted-foreground">
{language === "zh" ? "暂无租户" : "No tenants"}
</div>
)}
</div>
) : (
<div className="text-center py-4 text-muted-foreground">
{language === "zh" ? "暂无租户数据" : "No tenant data available"}
</div>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsChannelDetailsDialogOpen(false)}>
{language === "zh" ? "关闭" : "Close"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Channel Edit Dialog */}
<Dialog open={isChannelEditDialogOpen} onOpenChange={setIsChannelEditDialogOpen}>
<DialogContent className="bg-card border-border max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{language === "zh" ? "编辑渠道信息" : "Edit Channel"}</DialogTitle>
<DialogDescription>
{language === "zh" ? `修改渠道 "${selectedChannel?.name}" 的基本信息` : `Update basic information for channel "${selectedChannel?.name}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Basic Information Form */}
<div className="space-y-4">
<div>
<label className="text-sm font-semibold text-foreground block mb-2">
{language === "zh" ? "渠道名称" : "Channel Name"}
</label>
<Input
value={channelEditForm.name}
onChange={(e) => setChannelEditForm({ ...channelEditForm, name: e.target.value })}
placeholder={language === "zh" ? "例: 阿里云渠道" : "e.g., Alibaba Cloud Partner"}
className="bg-background border-border text-foreground"
/>
</div>
<div>
<label className="text-sm font-semibold text-foreground block mb-2">
{language === "zh" ? "联系人名称" : "Contact Person"}
</label>
<Input
value={channelEditForm.contactName}
onChange={(e) => setChannelEditForm({ ...channelEditForm, contactName: e.target.value })}
placeholder={language === "zh" ? "例: 张三" : "e.g., John Doe"}
className="bg-background border-border text-foreground"
/>
</div>
<div>
<label className="text-sm font-semibold text-foreground block mb-2">
{language === "zh" ? "联系邮箱" : "Email"}
</label>
<Input
type="email"
value={channelEditForm.email}
onChange={(e) => setChannelEditForm({ ...channelEditForm, email: e.target.value })}
placeholder={language === "zh" ? "例: contact@example.com" : "e.g., contact@example.com"}
className="bg-background border-border text-foreground"
/>
</div>
<div>
<label className="text-sm font-semibold text-foreground block mb-2">
{language === "zh" ? "联系电话" : "Phone Number"}
</label>
<Input
value={channelEditForm.phone}
onChange={(e) => setChannelEditForm({ ...channelEditForm, phone: e.target.value })}
placeholder={language === "zh" ? "例: +86-10-XXXXXX" : "e.g., +1-555-0000"}
className="bg-background border-border text-foreground"
/>
</div>
</div>
{/* Channel Admins Management */}
<div className="border-t border-border pt-6">
<h4 className="text-base font-semibold text-foreground mb-4">
{language === "zh" ? "管理员管理" : "Administrator Management"}
</h4>
<div className="space-y-3">
{channelAdmins && channelAdmins.length > 0 ? (
channelAdmins.map((admin: any, index: number) => (
<div key={index} className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
<div className="flex-1">
<p className="font-medium text-foreground">{admin.name}</p>
<p className="text-sm text-muted-foreground">{admin.email}</p>
<Badge variant="outline" className="mt-2">
{language === "zh"
? admin.role === "billing"
? "计费管理员"
: admin.role === "operations"
? "运营管理员"
: "管理员"
: admin.role === "billing"
? "Billing Admin"
: admin.role === "operations"
? "Operations Admin"
: "Administrator"}
</Badge>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => handleRemoveAdmin(admin.id)}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))
) : (
<p className="text-sm text-muted-foreground">
{language === "zh" ? "暂无管理员" : "No administrators"}
</p>
)}
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsChannelEditDialogOpen(false)}>
{language === "zh" ? "取消" : "Cancel"}
</Button>
<Button className="bg-primary text-primary-foreground" onClick={handleSaveChannelEdit}>
{language === "zh" ? "保存更改" : "Save Changes"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* View Tenants Dialog */}
<Dialog open={isViewTenantsDialogOpen} onOpenChange={setIsViewTenantsDialogOpen}>
<DialogContent className="bg-card border-border max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<div className="flex items-center justify-between">
<div>
<DialogTitle>{language === "zh" ? "渠道租户管理" : "Channel Tenants"}</DialogTitle>
<DialogDescription>
{language === "zh"
? `查看和管理渠道 "${selectedChannel?.name}" 下的租户信息`
: `View and manage tenants under channel "${selectedChannel?.name}"`}
</DialogDescription>
</div>
<Button
className="bg-primary text-primary-foreground"
onClick={() => setIsAddTenantDialogOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
{language === "zh" ? "添加租户" : "Add Tenant"}
</Button>
</div>
</DialogHeader>
<div className="space-y-4 py-4">
{channelTenants && channelTenants.length > 0 ? (
<div className="grid grid-cols-1 gap-4">
{channelTenants.map((tenant: any) => (
<div key={tenant.id} className="border border-border rounded-lg p-4 hover:bg-muted/50 transition-colors">
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h4 className="font-semibold text-foreground">{tenant.name}</h4>
<p className="text-sm text-muted-foreground">{tenant.email}</p>
<p className="text-sm text-muted-foreground">{tenant.phone}</p>
{tenant.permissions && tenant.permissions.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{tenant.permissions.map((perm: string) => (
<span key={perm} className="inline-block px-2 py-1 text-xs rounded bg-primary/20 text-primary">
{perm}
</span>
))}
</div>
)}
</div>
<Badge
variant="secondary"
className={`${
tenant.status === "active"
? "bg-green-500/10 text-green-600 border-green-500/20"
: "bg-red-500/10 text-red-600 border-red-500/20"
}`}
>
{language === "zh"
? tenant.status === "active"
? "活跃"
: "禁用"
: tenant.status === "active"
? "Active"
: "Disabled"}
</Badge>
</div>
<div className="text-xs text-muted-foreground mb-3">
{language === "zh" ? "创建时间" : "Created"}: {tenant.createdAt || "2024-01-15"}
</div>
<div className="flex gap-2 flex-wrap">
<Button
size="sm"
variant="outline"
onClick={() => handleTenantChangePassword(tenant.id)}
>
{language === "zh" ? "修改密码" : "Change Password"}
</Button>
{tenant.status === "active" && (
<Button
size="sm"
variant="outline"
onClick={() => handleDisableTenant(tenant.id)}
className="text-yellow-600 border-yellow-600/20 hover:bg-yellow-600/10"
>
{language === "zh" ? "禁用" : "Disable"}
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteTenant(tenant.id)}
className="text-destructive border-destructive/20 hover:bg-destructive/10"
>
<Trash2 className="h-3.5 w-3.5 mr-1" />
{language === "zh" ? "删除" : "Delete"}
</Button>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<p className="text-muted-foreground">
{language === "zh" ? "该渠道暂无租户" : "No tenants under this channel"}
</p>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsViewTenantsDialogOpen(false)}>
{language === "zh" ? "关闭" : "Close"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Add Tenant Dialog */}
<Dialog open={isAddTenantDialogOpen} onOpenChange={setIsAddTenantDialogOpen}>
<DialogContent className="bg-card border-border max-w-md">
<DialogHeader>
<DialogTitle>{language === "zh" ? "添加新租户" : "Add New Tenant"}</DialogTitle>
<DialogDescription>
{language === "zh" ? "为渠道添加新的租户账户" : "Add a new tenant account to the channel"}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="tenant-name">{language === "zh" ? "租户名称" : "Tenant Name"}</Label>
<Input
id="tenant-name"
placeholder="Acme Corp"
value={newTenantForm.name}
onChange={(e) => setNewTenantForm({ ...newTenantForm, name: e.target.value })}
className="bg-background"
/>
</div>
<div className="space-y-2">
<Label htmlFor="tenant-email">{language === "zh" ? "邮箱" : "Email"}</Label>
<Input
id="tenant-email"
type="email"
placeholder="tenant@example.com"
value={newTenantForm.email}
onChange={(e) => setNewTenantForm({ ...newTenantForm, email: e.target.value })}
className="bg-background"
/>
</div>
<div className="space-y-2">
<Label htmlFor="tenant-password">{language === "zh" ? "密码" : "Password"}</Label>
<Input
id="tenant-password"
type="password"
placeholder="••••••••"
value={newTenantForm.password}
onChange={(e) => setNewTenantForm({ ...newTenantForm, password: e.target.value })}
className="bg-background"
/>
</div>
<div className="space-y-2">
<Label htmlFor="system-role">{language === "zh" ? "系统权限" : "System Role"}</Label>
<Select
value={newTenantForm.systemRole}
onValueChange={(value: "tenant" | "billing-admin" | "operations-admin") =>
setNewTenantForm({ ...newTenantForm, systemRole: value })
}
>
<SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="tenant">
{language === "zh" ? "租户" : "Tenant"}
</SelectItem>
<SelectItem value="billing-admin">
{language === "zh" ? "计费管理员" : "Billing Admin"}
</SelectItem>
<SelectItem value="operations-admin">
{language === "zh" ? "运营管理员" : "Operations Admin"}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddTenantDialogOpen(false)}>
{language === "zh" ? "取消" : "Cancel"}
</Button>
<Button className="bg-primary text-primary-foreground" onClick={handleCreateTenant}>
{language === "zh" ? "创建租户" : "Create Tenant"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Change Password Dialog */}
<Dialog open={isChangePasswordDialogOpen} onOpenChange={setIsChangePasswordDialogOpen}>
<DialogContent className="bg-card border-border max-w-md">
<DialogHeader>
<DialogTitle>{language === "zh" ? "修改租户密码" : "Change Tenant Password"}</DialogTitle>
<DialogDescription>
{language === "zh"
? `为租户 "${selectedTenantForEdit?.name}" 设置新密码`
: `Set a new password for tenant "${selectedTenantForEdit?.name}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="new-password">{language === "zh" ? "新密码" : "New Password"}</Label>
<Input
id="new-password"
type="password"
placeholder={language === "zh" ? "输入新密码(至少6位)" : "Enter new password (min 6 chars)"}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="bg-background"
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password">{language === "zh" ? "确认密码" : "Confirm Password"}</Label>
<Input
id="confirm-password"
type="password"
placeholder={language === "zh" ? "再次输入新密码" : "Re-enter new password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="bg-background"
/>
</div>
{newPassword && confirmPassword && newPassword !== confirmPassword && (
<p className="text-sm text-destructive">
{language === "zh" ? "两次输入的密码不一致" : "Passwords do not match"}
</p>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setIsChangePasswordDialogOpen(false)
setNewPassword("")
setConfirmPassword("")
}}
>
{language === "zh" ? "取消" : "Cancel"}
</Button>
<Button
className="bg-primary text-primary-foreground"
onClick={handleConfirmChangePassword}
disabled={changePasswordLoading || !newPassword || newPassword !== confirmPassword}
>
{changePasswordLoading
? (language === "zh" ? "修改中..." : "Changing...")
: (language === "zh" ? "确认修改" : "Confirm Change")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<div className="mt-8">
<h3 className="text-lg font-semibold mb-4">
{language === "zh" ? "渠道申请审批" : "Channel Application Approvals"}
</h3>
<Card className="bg-card border-border">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b border-border">
<tr>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "渠道" : "Channel"}
</th>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "供应商" : "Provider"}
</th>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "期望模型" : "Models"}
</th>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "提交时间" : "Submitted"}
</th>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "状态" : "Status"}
</th>
<th className="text-right p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "操作" : "Actions"}
</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={6} className="p-4 text-center text-muted-foreground">
{language === "zh" ? "加载中..." : "Loading..."}
</td>
</tr>
) : providerApprovals.length === 0 ? (
<tr>
<td colSpan={6} className="p-4 text-center text-muted-foreground">
{language === "zh" ? "暂无待审批申请" : "No pending applications"}
</td>
</tr>
) : (
providerApprovals.map((approval) => (
<tr key={approval.id} className="border-b border-border last:border-0 hover:bg-muted/50">
<td className="p-4 text-sm text-foreground font-medium">{approval.channelName}</td>
<td className="p-4 text-sm text-foreground">{approval.providerName}</td>
<td className="p-4 text-sm text-muted-foreground">{approval.expectedModels}</td>
<td className="p-4 text-sm text-muted-foreground">{approval.submittedAt}</td>
<td className="p-4">
<Badge
variant="secondary"
className={
approval.status === "pending"
? "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
: approval.status === "approved"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-red-500/10 text-red-500 border-red-500/20"
}
>
{language === "zh"
? approval.status === "pending"
? "待审批"
: approval.status === "approved"
? "已批准"
: "已拒绝"
: approval.status === "pending"
? "Pending"
: approval.status === "approved"
? "Approved"
: "Rejected"}
</Badge>
</td>
<td className="p-4 text-right">
{approval.status === "pending" ? (
<Button
size="sm"
variant="outline"
onClick={async () => {
setSelectedApproval(approval)
setIsApprovalDialogOpen(true)
}}
>
{language === "zh" ? "审批" : "Review"}
</Button>
) : (
<span className="text-sm text-muted-foreground">
{language === "zh" ? "已处理" : "Processed"}
</span>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
</div>
{/* 平台Agent申请审批(新增) */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground">
{language === "zh" ? "平台Agent申请审批" : "Platform Agent Application Approvals"}
</h3>
<Card className="bg-card border-border">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b border-border">
<tr>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "渠道" : "Channel"}
</th>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "模板名称" : "Template Name"}
</th>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "申请Pod配额" : "Requested Pod Quota"}
</th>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "申请理由" : "Reason"}
</th>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "提交时间" : "Submitted"}
</th>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "状态" : "Status"}
</th>
<th className="text-right p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "操作" : "Actions"}
</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={7} className="p-4 text-center text-muted-foreground">
{language === "zh" ? "加载中..." : "Loading..."}
</td>
</tr>
) : platformAgentApplications.length === 0 ? (
<tr>
<td colSpan={7} className="p-4 text-center text-muted-foreground">
{language === "zh" ? "暂无待审批申请" : "No pending applications"}
</td>
</tr>
) : (
platformAgentApplications.map((app) => (
<tr key={app.id} className="border-b border-border last:border-0 hover:bg-muted/50">
<td className="p-4 text-sm text-foreground font-medium">{app.channelName || app.channelId}</td>
<td className="p-4 text-sm text-foreground">{app.templateDisplayName || app.templateName}</td>
<td className="p-4 text-sm text-muted-foreground">{app.requestedPodQuota || 0}</td>
<td className="p-4 text-sm text-muted-foreground max-w-[200px] truncate">{app.reason || "-"}</td>
<td className="p-4 text-sm text-muted-foreground">{app.createdAt}</td>
<td className="p-4">
<Badge
variant="secondary"
className={
app.status === "pending"
? "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
: app.status === "approved"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-red-500/10 text-red-500 border-red-500/20"
}
>
{language === "zh"
? app.status === "pending"
? "待审批"
: app.status === "approved"
? "已批准"
: "已拒绝"
: app.status === "pending"
? "Pending"
: app.status === "approved"
? "Approved"
: "Rejected"}
</Badge>
</td>
<td className="p-4 text-right">
{app.status === "pending" && (
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedPlatformAgentApplication(app)
setApprovedPodQuota(String(app.requestedPodQuota || 0))
setApprovalReason("")
setIsPlatformAgentApprovalDialogOpen(true)
}}
>
{language === "zh" ? "审批" : "Review"}
</Button>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
</div>
{/* 平台Agent申请审批对话框(新增) */}
<Dialog open={isPlatformAgentApprovalDialogOpen} onOpenChange={setIsPlatformAgentApprovalDialogOpen}>
<DialogContent className="bg-card border-border max-w-2xl">
<DialogHeader>
<DialogTitle>{language === "zh" ? "审批平台Agent申请" : "Review Platform Agent Application"}</DialogTitle>
<DialogDescription>
{language === "zh"
? `审批渠道 "${selectedPlatformAgentApplication?.channelName || selectedPlatformAgentApplication?.channelId}" 的平台Agent申请`
: `Review platform agent application from channel "${selectedPlatformAgentApplication?.channelName || selectedPlatformAgentApplication?.channelId}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "申请渠道" : "Channel"}
</label>
<p className="text-foreground mt-1">{selectedPlatformAgentApplication?.channelName || selectedPlatformAgentApplication?.channelId}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "提交时间" : "Submitted At"}
</label>
<p className="text-foreground mt-1">{selectedPlatformAgentApplication?.createdAt}</p>
</div>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "模板名称" : "Template Name"}
</label>
<p className="text-foreground mt-1">{selectedPlatformAgentApplication?.templateDisplayName || selectedPlatformAgentApplication?.templateName}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "申请Pod配额" : "Requested Pod Quota"}
</label>
<p className="text-foreground mt-1">{selectedPlatformAgentApplication?.requestedPodQuota || 0}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "申请理由" : "Reason"}
</label>
<p className="text-foreground mt-1">{selectedPlatformAgentApplication?.reason || "-"}</p>
</div>
<div className="border-t border-border pt-4">
<div className="space-y-2">
<Label htmlFor="approved-pod-quota">{language === "zh" ? "批准Pod配额" : "Approved Pod Quota"}</Label>
<Input
id="approved-pod-quota"
type="number"
min="1"
placeholder={language === "zh" ? "输入批准的Pod配额" : "Enter approved pod quota"}
value={approvedPodQuota}
onChange={(e) => setApprovedPodQuota(e.target.value)}
className="bg-background border-border"
/>
</div>
<div className="space-y-2 mt-4">
<Label htmlFor="approval-reason">{language === "zh" ? "审批理由" : "Approval Reason"}</Label>
<Input
id="approval-reason"
placeholder={language === "zh" ? "输入审批理由(可选)" : "Enter approval reason (optional)"}
value={approvalReason}
onChange={(e) => setApprovalReason(e.target.value)}
className="bg-background border-border"
/>
</div>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
className="border-destructive text-destructive hover:bg-destructive/10 bg-transparent"
onClick={async () => {
if (!selectedPlatformAgentApplication) return
try {
await TaijiAPIClient.reviewPlatformAgentApplication(
selectedPlatformAgentApplication.id,
"reject",
undefined,
approvalReason || (language === "zh" ? "申请被拒绝" : "Application rejected")
)
await loadDashboardData()
setIsPlatformAgentApprovalDialogOpen(false)
setApprovedPodQuota("")
setApprovalReason("")
} catch (error) {
console.error("Failed to reject application:", error)
alert(language === "zh" ? "操作失败" : "Operation failed")
}
}}
>
{language === "zh" ? "拒绝" : "Reject"}
</Button>
<Button
className="bg-primary text-primary-foreground"
onClick={async () => {
if (!selectedPlatformAgentApplication) return
const podQuota = parseInt(approvedPodQuota) || selectedPlatformAgentApplication.requestedPodQuota || 1
try {
await TaijiAPIClient.reviewPlatformAgentApplication(
selectedPlatformAgentApplication.id,
"approve",
podQuota,
approvalReason || (language === "zh" ? "申请已批准" : "Application approved")
)
await loadDashboardData()
setIsPlatformAgentApprovalDialogOpen(false)
setApprovedPodQuota("")
setApprovalReason("")
} catch (error) {
console.error("Failed to approve application:", error)
alert(language === "zh" ? "操作失败" : "Operation failed")
}
}}
>
{language === "zh" ? "批准" : "Approve"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 供应商申请审批对话框 */}
<Dialog open={isApprovalDialogOpen} onOpenChange={setIsApprovalDialogOpen}>
<DialogContent className="bg-card border-border max-w-2xl">
<DialogHeader>
<DialogTitle>{language === "zh" ? "审批供应商申请" : "Review Provider Application"}</DialogTitle>
<DialogDescription>
{language === "zh"
? `审批渠道 "${selectedApproval?.channelName}" 的供应商申请`
: `Review provider application from channel "${selectedApproval?.channelName}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "申请渠道" : "Channel"}
</label>
<p className="text-foreground mt-1">{selectedApproval?.channelName}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "提交时间" : "Submitted At"}
</label>
<p className="text-foreground mt-1">{selectedApproval?.submittedAt}</p>
</div>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "供应商名称" : "Provider Name"}
</label>
<p className="text-foreground mt-1">{selectedApproval?.providerName}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "期望模型" : "Expected Models"}
</label>
<p className="text-foreground mt-1">{selectedApproval?.expectedModels}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "申请理由" : "Reason"}
</label>
<p className="text-foreground mt-1">{selectedApproval?.reason}</p>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
className="border-destructive text-destructive hover:bg-destructive/10 bg-transparent"
onClick={async () => {
if (!selectedApproval) return
try {
await TaijiAPIClient.reviewProviderApplication(selectedApproval.id, false, language === "zh" ? "申请被拒绝" : "Application rejected")
await loadDashboardData()
setIsApprovalDialogOpen(false)
} catch (error) {
console.error("Failed to reject application:", error)
alert(language === "zh" ? "操作失败" : "Operation failed")
}
}}
>
{language === "zh" ? "拒绝" : "Reject"}
</Button>
<Button
className="bg-primary text-primary-foreground"
onClick={async () => {
if (!selectedApproval) return
try {
await TaijiAPIClient.reviewProviderApplication(selectedApproval.id, true, language === "zh" ? "申请已批准" : "Application approved")
await loadDashboardData()
setIsApprovalDialogOpen(false)
} catch (error) {
console.error("Failed to approve application:", error)
alert(language === "zh" ? "操作失败" : "Operation failed")
}
}}
>
{language === "zh" ? "批准" : "Approve"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)}
{activeTab === "resources" && (
<div className="space-y-6">
<div>
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-2xl font-bold text-foreground">
{language === "zh" ? "平台Agent模板管理" : "Platform Agent Template Management"}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{language === "zh"
? "管理ACR模板的K8s资源配置(CPU/内存请求和限制)"
: "Manage K8s resource configuration for ACR templates (CPU/Memory requests and limits)"}
</p>
</div>
</div>
{/* Agent Resource Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
{resourcesLoading ? (
<Card className="p-4 bg-card border-border">
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "加载中..." : "Loading..."}</span>
</div>
</Card>
) : agentResources.length === 0 ? (
<Card className="p-4 bg-card border-border">
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "暂无Agent模板" : "No agent templates available"}</span>
</div>
</Card>
) : (
agentResources.map((agent) => (
<Card
key={agent.id || agent.name}
className="p-4 bg-card border-border cursor-pointer hover:border-primary transition-colors"
onClick={() => {
setSelectedAgent(agent)
// 初始化配置表单数据
setAgentConfigForm({
cpuRequest: agent.cpuRequest || "100m",
cpuLimit: agent.cpuLimit || "500m",
memoryRequest: agent.memoryRequest || "128Mi",
memoryLimit: agent.memoryLimit || "512Mi",
maxInstances: agent.maxInstances || 10,
})
setComputeConfigOpen(true)
}}
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="font-semibold text-foreground">{agent.displayName || agent.name}</h3>
<p className="text-xs text-muted-foreground">{agent.name}</p>
{agent.description && (
<p className="text-xs text-muted-foreground mt-1">{agent.description}</p>
)}
</div>
{agent.status && (
<Badge
variant="secondary"
className={agent.status === "available" || agent.status === "Running"
? "bg-green-500/10 text-green-500"
: "bg-yellow-500/10 text-yellow-500"}
>
{agent.status}
</Badge>
)}
</div>
<div className="space-y-2">
{/* CPU Request/Limit */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-blue-500" />
<span className="text-muted-foreground">CPU:</span>
</div>
<span className="font-medium text-foreground">
{agent.cpuRequest || agent.cpu || "100m"} / {agent.cpuLimit || "500m"}
</span>
</div>
{/* Memory Request/Limit */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<Server className="h-4 w-4 text-green-500" />
<span className="text-muted-foreground">{language === "zh" ? "内存" : "Memory"}:</span>
</div>
<span className="font-medium text-foreground">
{agent.memoryRequest || agent.memory || "128Mi"} / {agent.memoryLimit || "512Mi"}
</span>
</div>
</div>
<div className="flex gap-2 mt-3">
<Button variant="ghost" size="sm" className="w-full" onClick={(e) => {
e.stopPropagation()
setSelectedAgent(agent)
// 初始化配置表单数据
setAgentConfigForm({
cpuRequest: agent.cpuRequest || "100m",
cpuLimit: agent.cpuLimit || "500m",
memoryRequest: agent.memoryRequest || "128Mi",
memoryLimit: agent.memoryLimit || "512Mi",
maxInstances: agent.maxInstances || 10,
})
setComputeConfigOpen(true)
}}>
<Settings className="h-4 w-4 mr-2" />
{language === "zh" ? "配置资源" : "Configure Resources"}
</Button>
</div>
</Card>
))
)}
</div>
</div>
{/* Agent Resource Configuration Dialog - K8s资源配置 */}
<Dialog open={computeConfigOpen} onOpenChange={setComputeConfigOpen}>
<DialogContent className="bg-background border-border max-w-2xl">
<DialogHeader>
<DialogTitle className="text-foreground">
{language === "zh" ? "Agent模板资源配置" : "Agent Template Resource Configuration"}
</DialogTitle>
<DialogDescription>
{language === "zh"
? "配置K8s Pod的CPU和内存资源请求与限制"
: "Configure K8s Pod CPU and memory resource requests and limits"}
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{selectedAgent && (
<>
<div className="p-4 bg-muted/50 rounded-lg">
<h4 className="font-semibold text-foreground mb-1">{selectedAgent.displayName || selectedAgent.name}</h4>
<p className="text-sm text-muted-foreground">{selectedAgent.name}</p>
{selectedAgent.description && (
<p className="text-sm text-muted-foreground mt-1">{selectedAgent.description}</p>
)}
{selectedAgent.imageUrl && (
<p className="text-xs text-muted-foreground mt-2 font-mono">{selectedAgent.imageUrl}</p>
)}
</div>
<div className="space-y-4">
{/* CPU Request */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-foreground mb-1 block">
{language === "zh" ? "CPU 请求 (Request)" : "CPU Request"}
</label>
<Input
type="text"
placeholder={language === "zh" ? "例: 100m, 500m, 1" : "e.g., 100m, 500m, 1"}
value={agentConfigForm.cpuRequest}
onChange={(e) => setAgentConfigForm({ ...agentConfigForm, cpuRequest: e.target.value })}
className="bg-background border-border text-foreground"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "Pod启动时保证的CPU资源" : "Guaranteed CPU for pod startup"}
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground mb-1 block">
{language === "zh" ? "CPU 限制 (Limit)" : "CPU Limit"}
</label>
<Input
type="text"
placeholder={language === "zh" ? "例: 500m, 1, 2" : "e.g., 500m, 1, 2"}
value={agentConfigForm.cpuLimit}
onChange={(e) => setAgentConfigForm({ ...agentConfigForm, cpuLimit: e.target.value })}
className="bg-background border-border text-foreground"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "Pod可使用的最大CPU资源" : "Maximum CPU the pod can use"}
</p>
</div>
</div>
{/* Memory Request/Limit */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-foreground mb-1 block">
{language === "zh" ? "内存请求 (Request)" : "Memory Request"}
</label>
<Input
type="text"
placeholder={language === "zh" ? "例: 128Mi, 256Mi, 1Gi" : "e.g., 128Mi, 256Mi, 1Gi"}
value={agentConfigForm.memoryRequest}
onChange={(e) => setAgentConfigForm({ ...agentConfigForm, memoryRequest: e.target.value })}
className="bg-background border-border text-foreground"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "Pod启动时保证的内存资源" : "Guaranteed memory for pod startup"}
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground mb-1 block">
{language === "zh" ? "内存限制 (Limit)" : "Memory Limit"}
</label>
<Input
type="text"
placeholder={language === "zh" ? "例: 512Mi, 1Gi, 2Gi" : "e.g., 512Mi, 1Gi, 2Gi"}
value={agentConfigForm.memoryLimit}
onChange={(e) => setAgentConfigForm({ ...agentConfigForm, memoryLimit: e.target.value })}
className="bg-background border-border text-foreground"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "Pod可使用的最大内存资源" : "Maximum memory the pod can use"}
</p>
</div>
</div>
{/* Max Instances */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground mb-1 block">
{language === "zh" ? "最大实例数" : "Max Instances"}
</label>
<Input
type="number"
placeholder={language === "zh" ? "输入最大实例数" : "Enter max instances"}
value={agentConfigForm.maxInstances}
onChange={(e) => setAgentConfigForm({ ...agentConfigForm, maxInstances: parseInt(e.target.value) || 10 })}
min={1}
max={100}
className="bg-background border-border text-foreground"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "该模板可部署的最大Pod数量" : "Maximum number of pods for this template"}
</p>
</div>
{/* Info Box */}
<div className="p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg">
<p className="text-sm text-foreground font-medium mb-1">
{language === "zh" ? "K8s资源配置说明" : "K8s Resource Configuration"}
</p>
<ul className="text-xs text-muted-foreground space-y-1">
<li>• {language === "zh" ? "CPU: 1 = 1核, 500m = 0.5核, 100m = 0.1核" : "CPU: 1 = 1 core, 500m = 0.5 core, 100m = 0.1 core"}</li>
<li>• {language === "zh" ? "内存: 1Gi = 1GB, 512Mi = 512MB, 128Mi = 128MB" : "Memory: 1Gi = 1GB, 512Mi = 512MB, 128Mi = 128MB"}</li>
<li>• {language === "zh" ? "Request: 保证资源,Limit: 最大资源" : "Request: guaranteed, Limit: maximum"}</li>
</ul>
</div>
</div>
</>
)}
<div className="flex justify-end gap-3">
<Button variant="outline" onClick={() => setComputeConfigOpen(false)}>
{language === "zh" ? "取消" : "Cancel"}
</Button>
<Button
onClick={async () => {
if (!selectedAgent) return
try {
setResourcesLoading(true)
// 调用API更新Agent资源配置 - 使用新的平台Agent模板配置接口
// PUT /api/admin/platform-agents/templates/{name}/config
await TaijiAPIClient.configurePlatformAgentTemplate(selectedAgent.name, {
cpuRequest: agentConfigForm.cpuRequest,
cpuLimit: agentConfigForm.cpuLimit,
memoryRequest: agentConfigForm.memoryRequest,
memoryLimit: agentConfigForm.memoryLimit,
maxPods: agentConfigForm.maxInstances, // 映射 maxInstances 到 maxPods
})
alert(language === "zh" ? "配置已保存" : "Configuration saved")
setComputeConfigOpen(false)
await loadDashboardData()
} catch (error) {
console.error("Failed to save config:", error)
alert(language === "zh" ? "保存失败" : "Failed to save")
} finally {
setResourcesLoading(false)
}
}}
>
{language === "zh" ? "保存配置" : "Save Configuration"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-2xl font-bold text-foreground">
{language === "zh" ? "货源供应商管理" : "Goods Source Management"}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{language === "zh" ? "管理平台的模型供应商配置" : "Manage platform model provider configurations"}
</p>
</div>
<Button className="bg-primary text-primary-foreground" onClick={() => handleAddProvider("model")}>
<Plus className="h-4 w-4 mr-2" />
{language === "zh" ? "添加模型供应商" : "Add Model Provider"}
</Button>
</div>
{/* Model Providers Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{resourcesLoading ? (
<Card className="p-4 bg-card border-border">
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "加载中..." : "Loading..."}</span>
</div>
</Card>
) : modelProviders.length === 0 ? (
<Card className="p-4 bg-card border-border">
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "暂无模型供应商" : "No model providers available"}</span>
</div>
</Card>
) : (
modelProviders.map((provider) => (
<Card
key={provider.name}
className="p-4 bg-card border-border hover:border-primary/50 transition-colors"
>
<div className="flex items-start justify-between mb-3">
<div>
<h4 className="font-semibold text-foreground">{provider.name}</h4>
<p className="text-xs text-muted-foreground">{provider.type}</p>
</div>
<Badge
variant={provider.status === "active" ? "default" : "secondary"}
className="bg-green-500/10 text-green-500 border-green-500/20"
>
{provider.status === "active"
? language === "zh"
? "活跃"
: "Active"
: language === "zh"
? "离线"
: "Offline"}
</Badge>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">{language === "zh" ? "支持模型" : "Models"}</span>
<span className="text-foreground font-medium">{(provider.supportedModels || provider.models || []).length}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">RPM:</span>
<span className="text-foreground font-medium">{(provider.rpm || 0).toLocaleString()}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">TPM:</span>
<span className="text-foreground font-medium">{(provider.tpm || 0).toLocaleString()}</span>
</div>
</div>
<div className="flex gap-2 mt-4">
<Button
variant="outline"
size="sm"
className="flex-1 bg-transparent"
onClick={() => handleConfigProvider(provider, "model")}
>
<Settings className="h-3 w-3 mr-1" />
{language === "zh" ? "配置" : "Config"}
</Button>
<Button
variant="outline"
size="sm"
className="flex-1 bg-transparent"
onClick={async () => {
try {
await TaijiAPIClient.testProviderConnection(provider.id || provider.name)
alert(language === "zh" ? "连接测试成功" : "Connection test successful")
} catch (error) {
console.error("Failed to test connection:", error)
alert(language === "zh" ? "连接测试失败" : "Connection test failed")
}
}}
>
<Zap className="h-3 w-3 mr-1" />
{language === "zh" ? "测试延迟" : "Test Latency"}
</Button>
<Button
variant="outline"
size="sm"
className="flex-1 bg-transparent text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={async () => {
if (confirm(language === "zh" ? `确定要删除 ${provider.name} 吗?` : `Are you sure you want to delete ${provider.name}?`)) {
try {
setResourcesLoading(true)
await TaijiAPIClient.deleteProvider(provider.id || provider.name)
await loadDashboardData()
} catch (error) {
console.error("Failed to delete provider:", error)
alert(language === "zh" ? "删除失败" : "Failed to delete")
} finally {
setResourcesLoading(false)
}
}
}}
>
<Trash2 className="h-3 w-3 mr-1" />
{language === "zh" ? "删除" : "Delete"}
</Button>
</div>
</Card>
))
)}
</div>
</div>
)}
{activeTab === "monitoring" && (
<div className="space-y-6">
{/* Agent health monitoring */}
<div className="mb-6">
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<h2 className="text-2xl font-bold text-foreground">{text.agentHealthMonitoring}</h2>
<p className="text-sm text-muted-foreground mt-1">{text.agentHealthDesc}</p>
</div>
{/* Agent总数统计卡片 */}
<div className="flex items-center gap-3 flex-wrap">
<Card className="px-4 py-2 bg-card border-border">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10">
<Bot className="h-5 w-5 text-primary" />
</div>
<div>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "Agent总数" : "Total Agents"}
</p>
<p className="text-xl font-bold text-foreground">
{agentSummary?.total ?? agents.length}
</p>
</div>
</div>
</Card>
<Card className="px-4 py-2 bg-card border-border">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-green-500/10">
<Activity className="h-5 w-5 text-green-500" />
</div>
<div>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "健康Agent" : "Healthy Agents"}
</p>
<p className="text-xl font-bold text-green-500">
{agentSummary?.byHealthStatus?.healthy ?? agents.filter(a => a.healthStatus === "healthy" || a.status === "healthy" || a.status === "Running" || a.status === "available").length}
</p>
</div>
</div>
</Card>
<Card className="px-4 py-2 bg-card border-border">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-yellow-500/10">
<AlertTriangle className="h-5 w-5 text-yellow-500" />
</div>
<div>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "警告/异常" : "Warning/Critical"}
</p>
<p className="text-xl font-bold text-yellow-500">
{(agentSummary?.byHealthStatus?.warning ?? 0) + (agentSummary?.byHealthStatus?.critical ?? 0) ||
agents.filter(a => a.healthStatus === "warning" || a.healthStatus === "critical" || a.status === "warning" || a.status === "critical").length}
</p>
</div>
</div>
</Card>
</div>
</div>
</div>
{/* Agent health cards grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{loading ? (
<Card className="p-6 bg-card border-border">
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "加载中..." : "Loading..."}</span>
</div>
</Card>
) : agents.length === 0 ? (
<Card className="p-6 bg-card border-border">
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "暂无Agent" : "No agents available"}</span>
</div>
</Card>
) : (
agents.map((agent) => {
// 使用新的healthStatus字段,兼容旧的status字段
// 后端可能返回: healthy, Running, available, warning, critical
const healthStatus = agent.healthStatus || agent.status
const isHealthy = healthStatus === "healthy" || healthStatus === "Running" || healthStatus === "available"
const isWarning = healthStatus === "warning"
const statusColor = isHealthy
? "text-green-500 bg-green-500/10"
: isWarning
? "text-yellow-500 bg-yellow-500/10"
: "text-red-500 bg-red-500/10"
const statusText = isHealthy ? text.healthy : isWarning ? text.warning : text.critical
return (
<Card key={agent.id || agent.name} className="p-6 bg-card border-border hover:border-primary/50 transition-colors">
{/* Agent header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10">
<Bot className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="text-base font-semibold text-foreground">
{agent.name}
</h3>
<p className="text-xs text-muted-foreground">
{agent.type === "platform"
? (language === "zh" ? "平台Agent" : "Platform Agent")
: (language === "zh" ? "自定义Agent" : "Custom Agent")}
{agent.healthMessage && ` · ${agent.healthMessage}`}
</p>
</div>
</div>
<Badge className={statusColor}>{statusText}</Badge>
</div>
{/* Agent metrics */}
<div className="space-y-3">
{/* CPU Usage - 使用 cpuUtilization 百分比值控制进度条,显示 cpuUsage 实际使用量 */}
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-muted-foreground">{text.cpuUsageLabel}</span>
<span className="text-foreground font-medium">
{agent.cpuUsage || "0m"} / {agent.cpuLimit || "500m"}
</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className={`h-2 rounded-full ${
(agent.cpuUtilization ?? 0) > 80
? "bg-red-500"
: (agent.cpuUtilization ?? 0) > 60
? "bg-yellow-500"
: "bg-green-500"
}`}
style={{ width: `${Math.min(agent.cpuUtilization ?? 0, 100)}%` }}
/>
</div>
<div className="text-xs text-muted-foreground mt-1">
{(agent.cpuUtilization ?? 0).toFixed(1)}%
</div>
</div>
{/* Memory Usage - 使用 memoryUtilization 百分比值控制进度条,显示 memoryUsage 实际使用量 */}
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-muted-foreground">{text.memoryUsageLabel}</span>
<span className="text-foreground font-medium">
{agent.memoryUsage || "0Mi"} / {agent.memoryLimit || "512Mi"}
</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className={`h-2 rounded-full ${
(agent.memoryUtilization ?? 0) > 80
? "bg-red-500"
: (agent.memoryUtilization ?? 0) > 60
? "bg-yellow-500"
: "bg-green-500"
}`}
style={{ width: `${Math.min(agent.memoryUtilization ?? 0, 100)}%` }}
/>
</div>
<div className="text-xs text-muted-foreground mt-1">
{(agent.memoryUtilization ?? 0).toFixed(1)}%
</div>
</div>
{/* Stats grid */}
<div className="grid grid-cols-2 gap-3 pt-3 border-t border-border">
<div>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "CPU上限" : "CPU Limit"}
</p>
<p className="text-sm font-semibold text-foreground">
{agent.cpuLimit || "500m"}
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "内存上限" : "Memory Limit"}
</p>
<p className="text-sm font-semibold text-foreground">
{agent.memoryLimit || "512Mi"}
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "运行状态" : "Status"}
</p>
<p className="text-sm font-semibold text-foreground">
{agent.status || agent.k8sStatus || "-"}
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "数据来源" : "Source"}
</p>
<p className="text-sm font-semibold text-foreground">
{agent.source === "k8s" ? "K8s" : (agent.source === "database" ? "DB" : "-")}
</p>
</div>
</div>
</div>
</Card>
)
})
)}
</div>
</div>
)}
{activeTab === "billing" && (
<div className="space-y-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-foreground">{t("计费管理", "Billing Management")}</h2>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="gap-2 bg-transparent"
onClick={() => setShowDateDialog(true)}
>
<Calendar className="h-4 w-4" />
{t("时间查询", "Date Query")}
</Button>
<Button
variant="outline"
size="sm"
className="gap-2 bg-transparent"
onClick={() => setShowFilterDialog(true)}
>
<Filter className="h-4 w-4" />
{t("筛选", "Filter")}
</Button>
<Button
variant="outline"
size="sm"
className="gap-2 bg-transparent"
onClick={async () => {
try {
setBillingLoading(true)
const endTime = new Date().toISOString()
const startTime = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
const result = await TaijiAPIClient.getAdminBillingOverview({
startTime,
endTime,
export: "excel",
})
if (result?.data?.fileUrl) {
window.open(result.data.fileUrl, "_blank")
}
} catch (error) {
console.error("Failed to export:", error)
alert(language === "zh" ? "导出失败" : "Export failed")
} finally {
setBillingLoading(false)
}
}}
>
<Download className="h-4 w-4" />
{t("导出", "Export")}
</Button>
</div>
</div>
{/* 计费维度切换 */}
<div className="flex gap-2 border-b border-border">
<button
onClick={() => setBillingView("channel")}
className={`px-4 py-2 font-medium transition-colors ${
billingView === "channel"
? "border-b-2 border-primary text-primary"
: "text-muted-foreground hover:text-foreground"
}`}
>
{t("渠道维度", "Channel Dimension")}
</button>
<button
onClick={() => setBillingView("tenant")}
className={`px-4 py-2 font-medium transition-colors ${
billingView === "tenant"
? "border-b-2 border-primary text-primary"
: "text-muted-foreground hover:text-foreground"
}`}
>
{t("租户维度", "Tenant Dimension")}
</button>
<button
onClick={() => setBillingView("calls")}
className={`px-4 py-2 font-medium transition-colors ${
billingView === "calls"
? "border-b-2 border-primary text-primary"
: "text-muted-foreground hover:text-foreground"
}`}
>
{t("调用记录", "Call Records")}
</button>
</div>
{/* 渠道维度计费 */}
{billingView === "channel" && (
<div className="space-y-4">
{billingLoading ? (
<Card className="p-6 bg-card border-border">
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "加载中..." : "Loading..."}</span>
</div>
</Card>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="p-4 bg-card border-border">
<div className="text-sm text-muted-foreground">{t("渠道总数", "Total Channels")}</div>
<div className="text-2xl font-bold text-foreground mt-1">
{billingData?.channelStats?.length || 0}
</div>
</Card>
<Card className="p-4 bg-card border-border">
<div className="text-sm text-muted-foreground">{t("总计费额", "Total Billing")}</div>
<div className="text-2xl font-bold text-foreground mt-1">
${billingData?.channelStats?.reduce((sum: number, stat: any) => sum + (stat.totalCost || 0), 0).toFixed(2) || "0.00"}
</div>
</Card>
<Card className="p-4 bg-card border-border">
<div className="text-sm text-muted-foreground">{t("总EU消耗", "Total EU Consumption")}</div>
<div className="text-2xl font-bold text-foreground mt-1">
{billingData?.channelStats?.reduce((sum: number, stat: any) => sum + (stat.totalEU || 0), 0).toLocaleString() || 0} EU
</div>
</Card>
</div>
<Card className="p-6 bg-card border-border">
<h3 className="text-lg font-semibold text-foreground mb-4">
{t("渠道计费详情", "Channel Billing Details")}
</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b border-border">
<tr className="text-left text-sm text-muted-foreground">
<th className="pb-3 font-medium">{t("渠道名称", "Channel Name")}</th>
<th className="pb-3 font-medium">{t("调用次数", "Call Count")}</th>
<th className="pb-3 font-medium">{t("总EU", "Total EU")}</th>
<th className="pb-3 font-medium">{t("渠道总价", "Channel Total")}</th>
</tr>
</thead>
<tbody className="text-sm">
{billingData?.channelStats?.length === 0 ? (
<tr>
<td colSpan={4} className="py-8 text-center text-muted-foreground">
{language === "zh" ? "暂无数据" : "No data available"}
</td>
</tr>
) : (
billingData?.channelStats?.map((stat: any, idx: number) => (
<tr key={idx} className="border-b border-border/50">
<td className="py-3 text-foreground font-medium">{stat.channelName || stat.channelId}</td>
<td className="py-3 text-foreground">{stat.calls?.toLocaleString() || 0}</td>
<td className="py-3 text-foreground">{stat.totalEU?.toLocaleString() || 0} EU</td>
<td className="py-3 text-foreground font-semibold">${stat.totalCost?.toFixed(2) || "0.00"}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
</>
)}
</div>
)}
{/* 租户维度计费 */}
{billingView === "tenant" && (
<div className="space-y-4">
{billingLoading ? (
<Card className="p-6 bg-card border-border">
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "加载中..." : "Loading..."}</span>
</div>
</Card>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="p-4 bg-card border-border">
<div className="text-sm text-muted-foreground">{t("租户总数", "Total Tenants")}</div>
<div className="text-2xl font-bold text-foreground mt-1">
{billingData?.tenantStats?.length || 0}
</div>
</Card>
<Card className="p-4 bg-card border-border">
<div className="text-sm text-muted-foreground">{t("用户总价", "User Total")}</div>
<div className="text-2xl font-bold text-foreground mt-1">
${billingData?.tenantStats?.reduce((sum: number, stat: any) => sum + (stat.totalCost || 0), 0).toFixed(2) || "0.00"}
</div>
</Card>
<Card className="p-4 bg-card border-border">
<div className="text-sm text-muted-foreground">{t("平均消费", "Average Spending")}</div>
<div className="text-2xl font-bold text-foreground mt-1">
${billingData?.tenantStats?.length > 0
? (billingData.tenantStats.reduce((sum: number, stat: any) => sum + (stat.totalCost || 0), 0) / billingData.tenantStats.length).toFixed(2)
: "0.00"}
</div>
</Card>
</div>
<Card className="p-6 bg-card border-border">
<h3 className="text-lg font-semibold text-foreground mb-4">
{t("租户计费详情", "Tenant Billing Details")}
</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b border-border">
<tr className="text-left text-sm text-muted-foreground">
<th className="pb-3 font-medium">{t("租户名称", "Tenant Name")}</th>
<th className="pb-3 font-medium">{t("所属渠道", "Channel")}</th>
<th className="pb-3 font-medium">{t("调用次数", "Call Count")}</th>
<th className="pb-3 font-medium">{t("总EU", "Total EU")}</th>
<th className="pb-3 font-medium">{t("用户总价", "User Total")}</th>
</tr>
</thead>
<tbody className="text-sm">
{billingData?.tenantStats?.length === 0 ? (
<tr>
<td colSpan={5} className="py-8 text-center text-muted-foreground">
{language === "zh" ? "暂无数据" : "No data available"}
</td>
</tr>
) : (
billingData?.tenantStats?.map((stat: any, idx: number) => (
<tr key={idx} className="border-b border-border/50">
<td className="py-3 text-foreground font-medium">{stat.tenantName || stat.tenantId}</td>
<td className="py-3 text-muted-foreground">{stat.channelName || stat.channelId}</td>
<td className="py-3 text-foreground">{stat.calls?.toLocaleString() || 0}</td>
<td className="py-3 text-foreground">{stat.totalEU?.toLocaleString() || 0} EU</td>
<td className="py-3 text-foreground font-semibold">${stat.totalCost?.toFixed(2) || "0.00"}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
</>
)}
</div>
)}
{/* 调用记录 */}
{billingView === "calls" && (
<div className="space-y-4">
<Card className="p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground">
{t("调用记录明细", "Call Records Detail")}
</h3>
<div className="text-sm text-muted-foreground">
{t("EU计算规则:1 EU = 10秒调用时间", "EU Calculation: 1 EU = 10 seconds call time")}
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b border-border">
<tr className="text-left text-sm text-muted-foreground">
<th className="pb-3 font-medium">{t("调用ID", "Call ID")}</th>
<th className="pb-3 font-medium">{t("租户", "Tenant")}</th>
<th className="pb-3 font-medium">{t("渠道", "Channel")}</th>
<th className="pb-3 font-medium">{t("调用时间", "Call Time")}</th>
<th className="pb-3 font-medium">{t("时长(秒)", "Duration(s)")}</th>
<th className="pb-3 font-medium">{t("EU", "EU")}</th>
<th className="pb-3 font-medium">{t("单次调用总价", "Single Call Cost")}</th>
<th className="pb-3 font-medium">{t("时间戳", "Timestamp")}</th>
</tr>
</thead>
<tbody className="text-sm">
{[
{
id: "CALL-2024-001",
tenant: "Tech Corp",
channel: "Partner A",
time: "2024-01-15 14:23:45",
duration: 45,
eu: 4.5,
cost: "$0.45",
},
{
id: "CALL-2024-002",
tenant: "AI Solutions",
channel: "Partner A",
time: "2024-01-15 14:24:12",
duration: 30,
eu: 3.0,
cost: "$0.30",
},
{
id: "CALL-2024-003",
tenant: "Data Inc",
channel: "Partner B",
time: "2024-01-15 14:25:03",
duration: 120,
eu: 12.0,
cost: "$1.20",
},
{
id: "CALL-2024-004",
tenant: "Cloud Systems",
channel: "Partner C",
time: "2024-01-15 14:26:31",
duration: 65,
eu: 6.5,
cost: "$0.65",
},
{
id: "CALL-2024-005",
tenant: "Tech Corp",
channel: "Partner A",
time: "2024-01-15 14:27:15",
duration: 90,
eu: 9.0,
cost: "$0.90",
},
].map((call, idx) => (
<tr key={idx} className="border-b border-border/50">
<td className="py-3 text-foreground font-mono text-xs">{call.id}</td>
<td className="py-3 text-foreground">{call.tenant}</td>
<td className="py-3 text-muted-foreground">{call.channel}</td>
<td className="py-3 text-foreground">{call.time}</td>
<td className="py-3 text-foreground">{call.duration}s</td>
<td className="py-3 text-foreground">{call.eu} EU</td>
<td className="py-3 text-foreground font-semibold">{call.cost}</td>
<td className="py-3 text-muted-foreground text-xs">{call.time}</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
)}
<Dialog open={showFilterDialog} onOpenChange={setShowFilterDialog}>
<DialogContent className="bg-background border-border">
<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")}
className="bg-background border-border text-foreground"
/>
</div>
<div>
<Label>{t("最小调用次数", "Min Calls")}</Label>
<Input type="number" placeholder="0" className="bg-background border-border text-foreground" />
</div>
<div>
<Label>{t("最大调用次数", "Max Calls")}</Label>
<Input type="number" placeholder="10000" className="bg-background border-border text-foreground" />
</div>
<Button className="w-full">{t("应用筛选", "Apply Filter")}</Button>
</div>
</DialogContent>
</Dialog>
<Dialog open={showDateDialog} onOpenChange={setShowDateDialog}>
<DialogContent className="bg-background border-border">
<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" className="bg-background border-border text-foreground" />
</div>
<div>
<Label>{t("结束时间 (年-月-日 时:分)", "End Date & Time (YYYY-MM-DD HH:MM)")}</Label>
<Input type="datetime-local" className="bg-background border-border text-foreground" />
</div>
<Button className="w-full">{t("查询", "Query")}</Button>
</div>
</DialogContent>
</Dialog>
<Dialog open={showExportDialog} onOpenChange={setShowExportDialog}>
<DialogContent className="bg-background border-border">
<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>
</div>
)}
{activeTab === "settings" && (
<div className="space-y-8">
{/* 管理员列表管理 */}
<SettingsTab language={language} />
{/* 用户身份权限设置 */}
<div className="space-y-6">
<div>
<div className="mb-4">
<h2 className="text-2xl font-bold text-foreground">{t("角色权限配置", "Role Permission Configuration")}</h2>
<p className="text-sm text-muted-foreground mt-1">
{t("配置不同角色的标签页访问权限", "Configure tab access permissions for different roles")}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
{/* Billing Admin Role Card */}
<Card
className={`p-6 cursor-pointer transition-all ${selectedRole === "billing-admin" ? "border-primary bg-primary/5" : "bg-card border-border hover:border-primary/50"}`}
onClick={() => setSelectedRole("billing-admin")}
>
<div className="flex items-start gap-3">
<DollarSign className="w-8 h-8 text-green-500" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-foreground">
{t("计费管理员", "Billing Administrator")}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t(
"负责平台计费、账单和财务管理",
"Responsible for billing, invoices, and financial management",
)}
</p>
<div className="mt-3 flex items-center gap-2">
<Badge variant="secondary" className="text-xs">
{rolePermissions["billing-admin"].length} {t("个权限", "permissions")}
</Badge>
</div>
</div>
</div>
</Card>
{/* Operations Admin Role Card */}
<Card
className={`p-6 cursor-pointer transition-all ${selectedRole === "operations-admin" ? "border-primary bg-primary/5" : "bg-card border-border hover:border-primary/50"}`}
onClick={() => setSelectedRole("operations-admin")}
>
<div className="flex items-start gap-3">
<Settings className="w-8 h-8 text-blue-500" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-foreground">
{t("运营管理员", "Operations Administrator")}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t("负责渠道、资源和系统监控管理", "Responsible for channels, resources, and monitoring")}
</p>
<div className="mt-3 flex items-center gap-2">
<Badge variant="secondary" className="text-xs">
{rolePermissions["operations-admin"].length} {t("个权限", "permissions")}
</Badge>
</div>
</div>
</div>
</Card>
{/* Super Admin Role Card */}
<Card
className={`p-6 cursor-pointer transition-all ${selectedRole === "super-admin" ? "border-primary bg-primary/5" : "bg-card border-border hover:border-primary/50"}`}
onClick={() => setSelectedRole("super-admin")}
>
<div className="flex items-start gap-3">
<Shield className="w-8 h-8 text-purple-500" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-foreground">
{t("超级管理员", "Super Administrator")}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t("拥有所有权限,管理整个平台", "Full access to all platform features")}
</p>
<div className="mt-3 flex items-center gap-2">
<Badge variant="secondary" className="text-xs">
{rolePermissions["super-admin"].length} {t("个权限", "permissions")}
</Badge>
</div>
</div>
</div>
</Card>
</div>
{selectedRole && (
<Card className="p-6 bg-card border-border">
<h3 className="text-lg font-semibold text-foreground mb-4">
{t("标签页访问权限", "Tab Access Permissions")}
</h3>
<p className="text-sm text-muted-foreground mb-6">
{t("选择该角色可以访问的标签页", "Select which tabs this role can access")}
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{ id: "overview", label: t("概览", "Overview"), icon: "📊" },
{ id: "channels", label: t("渠道管理", "Channel Management"), icon: "🏢" },
{ id: "resources", label: t("资源管理", "Resource Management"), icon: "⚙️" },
{ id: "monitoring", label: t("监控", "Monitoring"), icon: "📈" },
{ id: "billing", label: t("计费", "Billing"), icon: "💰" },
{ id: "provider-backend", label: t("供应商管理中心后台", "Provider Backend"), icon: "🔧" },
{ id: "channel-backend", label: t("渠道管理中心后台", "Channel Backend"), icon: "🏪" },
{ id: "settings", label: t("设置", "Settings"), icon: "⚙️" },
].map((tab) => {
const hasPermission = rolePermissions[selectedRole]?.includes(tab.id)
return (
<div
key={tab.id}
className={`p-4 rounded-lg border-2 cursor-pointer transition-all ${
hasPermission
? "border-primary bg-primary/10"
: "border-border bg-background hover:border-primary/30"
}`}
onClick={() => {
setRolePermissions((prev) => ({
...prev,
[selectedRole]: hasPermission
? prev[selectedRole].filter((p) => p !== tab.id)
: [...prev[selectedRole], tab.id],
}))
}}
>
<div className="flex items-center gap-2 mb-2">
<span className="text-xl">{tab.icon}</span>
{hasPermission && <Check className="w-4 h-4 text-primary ml-auto" />}
</div>
<p className="text-sm font-medium text-foreground">{tab.label}</p>
</div>
)
})}
</div>
<Button
className="mt-6"
onClick={async () => {
if (!selectedRole) return
try {
const token = localStorage.getItem('admin_token') || localStorage.getItem('auth_token')
// 注意:此接口需要后端实现 PUT /api/admin/roles/{role_id}/permissions
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/admin/roles/${selectedRole}/permissions`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ permissions: rolePermissions[selectedRole] })
})
if (response.ok) {
const data = await response.json()
if (data.success) {
alert(language === "zh" ? "权限配置已保存" : "Permissions saved successfully")
} else {
alert(data.message || (language === "zh" ? "保存失败" : "Failed to save"))
}
} else {
// 如果接口未实现,显示提示
alert(language === "zh" ? "权限配置接口暂未实现" : "Permissions API not implemented yet")
}
} catch (error) {
console.error("Failed to save permissions:", error)
alert(language === "zh" ? "保存权限配置出错" : "Error saving permissions")
}
}}
>
{t("保存权限配置", "Save Permissions")}
</Button>
</Card>
)}
</div>
</div>
</div>
)}
</main>
{/* System role assignment dialog */}
<Dialog open={isSubscriptionDialogOpen} onOpenChange={setIsSubscriptionDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{text.assignSubscriptionLevel}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
{language === "zh" ? "为租户选择系统权限:" : "Select a system role for the tenant:"}
</p>
<div className="space-y-3">
{systemRoles.map((role) => (
<div
key={role.id}
onClick={() => setSelectedSubscriptionLevel(role.id)}
className={`p-4 rounded-lg border-2 cursor-pointer transition-colors ${
selectedSubscriptionLevel === role.id
? "border-primary bg-primary/5"
: "border-border hover:border-primary/50"
}`}
>
<div className="flex items-center justify-between">
<div>
<h4 className="font-semibold text-foreground">{role.name}</h4>
<p className="text-sm text-muted-foreground">{role.description}</p>
</div>
<div
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
selectedSubscriptionLevel === role.id ? "border-primary bg-primary" : "border-border"
}`}
>
{selectedSubscriptionLevel === role.id && <div className="w-2 h-2 rounded-full bg-white" />}
</div>
</div>
</div>
))}
</div>
<div className="flex gap-2 justify-end pt-4">
<Button variant="outline" onClick={() => setIsSubscriptionDialogOpen(false)}>
{language === "zh" ? "取消" : "Cancel"}
</Button>
<Button
onClick={async () => {
if (!selectedTenant || !selectedSubscriptionLevel) {
alert(language === "zh" ? "请选择租户和系统权限" : "Please select tenant and system role")
return
}
try {
const token = localStorage.getItem('admin_token') || localStorage.getItem('auth_token')
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/admin/tenants/${selectedTenant}/role`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ role: selectedSubscriptionLevel })
})
if (response.ok) {
const data = await response.json()
if (data.success) {
alert(language === "zh" ? "系统权限分配成功" : "System role assigned successfully")
setIsSubscriptionDialogOpen(false)
setSelectedSubscriptionLevel(null)
setSelectedTenant(null)
// 重新加载数据
await loadDashboardData()
} else {
alert(data.message || (language === "zh" ? "分配失败" : "Failed to assign"))
}
} else {
// 如果接口未实现,显示提示
alert(language === "zh" ? "系统权限分配接口暂未实现" : "System role assignment API not implemented yet")
}
} catch (error) {
console.error("Failed to assign system role:", error)
alert(language === "zh" ? "分配系统权限出错" : "Error assigning system role")
}
}}
>
{language === "zh" ? "确认分配" : "Confirm Assignment"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Dialogs for Add/Config Provider */}
<Dialog open={isAddProviderOpen} onOpenChange={setIsAddProviderOpen}>
<DialogContent className="bg-card border-border max-w-2xl">
<DialogHeader>
<DialogTitle>
{language === "zh"
? providerType === "model"
? "添加模型供应商"
: "添加数据供应商"
: providerType === "model"
? "Add Model Provider"
: "Add Data Provider"}
</DialogTitle>
<DialogDescription>
{language === "zh" ? "填写供应商信息以添加新的货源" : "Fill in provider information to add a new source"}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="provider-name">{language === "zh" ? "供应商名称" : "Provider Name"}</Label>
<Input
id="provider-name"
placeholder={language === "zh" ? "例: OpenAI" : "e.g., OpenAI"}
value={providerForm.name}
onChange={(e) => setProviderForm({ ...providerForm, name: e.target.value })}
className="bg-background border-border text-foreground"
/>
</div>
<div className="space-y-2">
<Label htmlFor="provider-url">{language === "zh" ? "API URL / 服务端点" : "API URL / Endpoint"}</Label>
<Input
id="provider-url"
placeholder={language === "zh" ? "例: https://api.openai.com/v1" : "e.g., https://api.openai.com/v1"}
value={providerForm.url}
onChange={(e) => setProviderForm({ ...providerForm, url: e.target.value })}
className="bg-background border-border text-foreground"
/>
</div>
{providerType === "model" && (
<>
<div className="space-y-2">
<Label htmlFor="provider-models">{language === "zh" ? "可调用模型" : "Available Models"}</Label>
{litellmModelsLoading ? (
<div className="text-sm text-muted-foreground py-2">
{language === "zh" ? "加载模型列表中..." : "Loading models..."}
</div>
) : (
<div className="border border-border rounded-md p-3 max-h-48 overflow-y-auto bg-background">
{litellmModels.length === 0 ? (
<div className="text-sm text-muted-foreground">
{language === "zh" ? "暂无可用模型" : "No models available"}
</div>
) : (
<div className="space-y-2">
{litellmModels.map((model) => (
<div key={model} className="flex items-center space-x-2">
<input
type="checkbox"
id={`add-model-${model}`}
checked={providerForm.models.includes(model)}
onChange={(e) => {
if (e.target.checked) {
setProviderForm({ ...providerForm, models: [...providerForm.models, model] })
} else {
setProviderForm({ ...providerForm, models: providerForm.models.filter((m: string) => m !== model) })
}
}}
className="w-4 h-4 rounded border-border"
/>
<label htmlFor={`add-model-${model}`} className="text-sm font-medium cursor-pointer flex-1 text-foreground">
{model}
</label>
</div>
))}
</div>
)}
</div>
)}
<p className="text-xs text-muted-foreground">
{language === "zh"
? `已选择 ${providerForm.models.length} 个模型`
: `${providerForm.models.length} model(s) selected`}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="provider-rpm">
{language === "zh" ? "RPM (每分钟请求数)" : "RPM (Requests Per Minute)"}
</Label>
<Input
id="provider-rpm"
type="number"
placeholder={language === "zh" ? "例: 10000" : "e.g., 10000"}
value={providerForm.rpm}
onChange={(e) => setProviderForm({ ...providerForm, rpm: e.target.value })}
className="bg-background border-border text-foreground"
/>
</div>
<div className="space-y-2">
<Label htmlFor="provider-tpm">
{language === "zh" ? "TPM (每分钟令牌数)" : "TPM (Tokens Per Minute)"}
</Label>
<Input
id="provider-tpm"
type="number"
placeholder={language === "zh" ? "例: 500000" : "e.g., 500000"}
value={providerForm.tpm}
onChange={(e) => setProviderForm({ ...providerForm, tpm: e.target.value })}
className="bg-background border-border text-foreground"
/>
</div>
</div>
</>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddProviderOpen(false)}>
{text.cancel}
</Button>
<Button className="bg-primary text-primary-foreground" onClick={handleSaveProvider}>
{language === "zh" ? "添加供应商" : "Add Provider"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isConfigProviderOpen} onOpenChange={setIsConfigProviderOpen}>
<DialogContent className="bg-card border-border max-w-2xl">
<DialogHeader>
<DialogTitle>{language === "zh" ? "配置供应商" : "Configure Provider"}</DialogTitle>
<DialogDescription>
{language === "zh"
? `修改 "${selectedProvider?.name}" 的配置信息`
: `Modify configuration for "${selectedProvider?.name}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="config-provider-name">{language === "zh" ? "供应商名称" : "Provider Name"}</Label>
<Input
id="config-provider-name"
value={providerForm.name}
onChange={(e) => setProviderForm({ ...providerForm, name: e.target.value })}
className="bg-background border-border text-foreground"
/>
</div>
<div className="space-y-2">
<Label htmlFor="config-provider-url">
{language === "zh" ? "API URL / 服务端点" : "API URL / Endpoint"}
</Label>
<Input
id="config-provider-url"
value={providerForm.url}
onChange={(e) => setProviderForm({ ...providerForm, url: e.target.value })}
className="bg-background border-border text-foreground"
/>
</div>
{providerType === "model" && (
<>
<div className="space-y-2">
<Label htmlFor="config-provider-models">
{language === "zh" ? "可调用模型" : "Available Models"}
</Label>
{litellmModelsLoading ? (
<div className="text-sm text-muted-foreground py-2">
{language === "zh" ? "加载模型列表中..." : "Loading models..."}
</div>
) : (
<div className="border border-border rounded-md p-3 max-h-48 overflow-y-auto bg-background">
{litellmModels.length === 0 ? (
<div className="text-sm text-muted-foreground">
{language === "zh" ? "暂无可用模型" : "No models available"}
</div>
) : (
<div className="space-y-2">
{litellmModels.map((model) => (
<div key={model} className="flex items-center space-x-2">
<input
type="checkbox"
id={`config-model-${model}`}
checked={providerForm.models.includes(model)}
onChange={(e) => {
if (e.target.checked) {
setProviderForm({ ...providerForm, models: [...providerForm.models, model] })
} else {
setProviderForm({ ...providerForm, models: providerForm.models.filter((m: string) => m !== model) })
}
}}
className="w-4 h-4 rounded border-border"
/>
<label htmlFor={`config-model-${model}`} className="text-sm font-medium cursor-pointer flex-1 text-foreground">
{model}
</label>
</div>
))}
</div>
)}
</div>
)}
<p className="text-xs text-muted-foreground">
{language === "zh"
? `已选择 ${providerForm.models.length} 个模型`
: `${providerForm.models.length} model(s) selected`}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="config-provider-rpm">
{language === "zh" ? "RPM (每分钟请求数)" : "RPM (Requests Per Minute)"}
</Label>
<Input
id="config-provider-rpm"
type="number"
value={providerForm.rpm}
onChange={(e) => setProviderForm({ ...providerForm, rpm: e.target.value })}
className="bg-background border-border text-foreground"
/>
</div>
<div className="space-y-2">
<Label htmlFor="config-provider-tpm">
{language === "zh" ? "TPM (每分钟令牌数)" : "TPM (Tokens Per Minute)"}
</Label>
<Input
id="config-provider-tpm"
type="number"
value={providerForm.tpm}
onChange={(e) => setProviderForm({ ...providerForm, tpm: e.target.value })}
className="bg-background border-border text-foreground"
/>
</div>
</div>
</>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsConfigProviderOpen(false)}>
{text.cancel}
</Button>
<Button className="bg-primary text-primary-foreground" onClick={handleSaveProvider}>
{language === "zh" ? "保存修改" : "Save Changes"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Dialog for Add New Admin */}
<Dialog open={showAddAdminDialog} onOpenChange={setShowAddAdminDialog}>
<DialogContent className="max-w-md bg-background border-border">
<DialogHeader>
<DialogTitle className="text-foreground">{t("添加新管理员", "Add New Administrator")}</DialogTitle>
<DialogDescription className="text-muted-foreground">
{t("创建新的管理员账户并分配角色", "Create a new administrator account and assign a role")}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-foreground mb-1 block">{t("姓名", "Name")}</label>
<Input
placeholder={t("输入管理员姓名", "Enter administrator name")}
value={newAdminForm.name}
onChange={(e) => setNewAdminForm({ ...newAdminForm, name: e.target.value })}
className="bg-background border-border text-foreground"
/>
</div>
<div>
<label className="text-sm font-medium text-foreground mb-1 block">{t("邮箱", "Email")}</label>
<Input
type="email"
placeholder={t("输入邮箱地址", "Enter email address")}
value={newAdminForm.email}
onChange={(e) => setNewAdminForm({ ...newAdminForm, email: e.target.value })}
className="bg-background border-border text-foreground"
/>
</div>
<div>
<label className="text-sm font-medium text-foreground mb-1 block">{t("密码", "Password")}</label>
<Input
type="password"
placeholder={t("设置登录密码", "Set login password")}
value={newAdminForm.password}
onChange={(e) => setNewAdminForm({ ...newAdminForm, password: e.target.value })}
className="bg-background border-border text-foreground"
/>
</div>
<div>
<label className="text-sm font-medium text-foreground mb-1 block">{t("角色", "Role")}</label>
<Select
value={newAdminForm.role}
onValueChange={(value) => setNewAdminForm({ ...newAdminForm, role: value })}
>
<SelectTrigger className="bg-background border-border text-foreground">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-background border-border">
<SelectItem value="billing-admin" className="text-foreground">
{t("计费管理员", "Billing Administrator")}
</SelectItem>
<SelectItem value="operations-admin" className="text-foreground">
{t("运营管理员", "Operations Administrator")}
</SelectItem>
<SelectItem value="super-admin" className="text-foreground">
{t("超级管理员", "Super Administrator")}
</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground mt-1">
{newAdminForm.role === "billing-admin" &&
t("负责平台计费、账单和财务管理", "Responsible for billing, invoices, and financial management")}
{newAdminForm.role === "operations-admin" &&
t("负责渠道、资源和系统监控管理", "Responsible for channels, resources, and monitoring")}
{newAdminForm.role === "super-admin" &&
t("拥有所有权限,管理整个平台", "Full access to all platform features")}
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAddAdminDialog(false)}>
{t("取消", "Cancel")}
</Button>
<Button
onClick={async () => {
try {
// 角色映射:billing-admin -> billing_admin, operations-admin -> operations_admin
const roleMap: Record<string, "billing_admin" | "operations_admin"> = {
"billing-admin": "billing_admin",
"operations-admin": "operations_admin",
"billing_admin": "billing_admin",
"operations_admin": "operations_admin",
}
const result = await TaijiAPIClient.createAdmin({
name: newAdminForm.name,
email: newAdminForm.email,
password: newAdminForm.password,
role: roleMap[newAdminForm.role] || "billing_admin",
})
if (result?.success) {
alert(language === "zh" ? "管理员创建成功" : "Admin created successfully")
setShowAddAdminDialog(false)
setNewAdminForm({ name: "", email: "", password: "", role: "billing_admin" })
// 重新加载数据
await loadDashboardData()
} else {
alert(result?.message || (language === "zh" ? "创建失败" : "Failed to create"))
}
} catch (error: any) {
console.error("Failed to create admin:", error)
alert(error?.message || (language === "zh" ? "创建管理员出错" : "Error creating admin"))
}
}}
disabled={!newAdminForm.name || !newAdminForm.email || !newAdminForm.password}
>
{t("创建管理员", "Create Administrator")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}