Files
taiji-pda-v0/app/admin/dashboard/page.tsx
T
xiaohei e4ee876f57 fix: 修复最近登录租户功能的数据加载逻辑
- 优化数据加载逻辑,优先使用 data.recentTenants 字段
- 兼容 data.tenants 字段(向后兼容)
- 支持直接数组格式
- 移除调试日志,保持代码整洁

测试验证:后端API返回正确的租户登录记录
2026-01-04 05:24:27 +00:00

4476 lines
210 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 [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 [isChannelAccessDialogOpen, setIsChannelAccessDialogOpen] = useState(false)
const [channelAccessList, setChannelAccessList] = useState<any[]>([])
const [channelAccessLoading, setChannelAccessLoading] = useState(false)
const [revokeAccessConfirm, setRevokeAccessConfirm] = useState<{ open: boolean; access: any | null }>({
open: false,
access: null,
})
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: "",
rpm: "",
tpm: "",
})
const [selectedAgent, setSelectedAgent] = useState<any>(null) // State for selected agent in resources tab
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 [agentApprovals, setAgentApprovals] = useState<any[]>([])
const [selectedAgentApproval, setSelectedAgentApproval] = useState<any>(null)
const [isAgentApprovalDialogOpen, setIsAgentApprovalDialogOpen] = useState(false)
// 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 [stats, setStats] = useState<any[]>([])
const [systemMetrics, setSystemMetrics] = useState<any[]>([])
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
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",
},
])
}
// 加载系统性能指标 - 使用正确的监控接口 GET /api/v1/monitoring/metrics
try {
const metricsResponse = await TaijiAPIClient.getMonitoringMetrics()
const metricsData = metricsResponse?.data || metricsResponse
if (metricsData?.system) {
setSystemMetrics([
{ label: text.cpuUsage, value: metricsData.system.cpu_usage_percent || 0, max: 100, color: "bg-blue-500" },
{ label: text.memoryUsage, value: metricsData.system.memory_usage_percent || 0, max: 100, color: "bg-green-500" },
{ label: text.storageUsage, value: metricsData.system.disk_usage_percent || 0, max: 100, color: "bg-yellow-500" },
{ label: text.activeAgents, value: 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)
}
// 加载申请列表
const applicationsData = await TaijiAPIClient.getAdminApplications()
if (Array.isArray(applicationsData)) {
setProviderApprovals(applicationsData.filter((app: any) => app.type === "provider"))
setAgentApprovals(applicationsData.filter((app: any) => app.type === "agent"))
}
// 加载Agent监控数据
const agentMonitoringData = await TaijiAPIClient.getAdminAgentMonitoring()
if (agentMonitoringData?.data?.agents && Array.isArray(agentMonitoringData.data.agents)) {
setAgents(agentMonitoringData.data.agents)
} else if (Array.isArray(agentMonitoringData)) {
setAgents(agentMonitoringData)
}
// 加载模型提供商(用于渠道资源分配)
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资源
const agentResourcesData = await TaijiAPIClient.getAdminAgentResources()
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 = (type: "model" | "data") => {
setProviderType(type)
setProviderForm({
name: "",
url: "",
apiKey: "",
models: "",
rpm: "",
tpm: "",
})
setIsAddProviderOpen(true)
}
const handleConfigProvider = (provider: any, type: "model" | "data") => {
setProviderType(type)
setSelectedProvider(provider)
setProviderForm({
name: provider.name,
url: provider.url || "",
apiKey: "••••••••",
models: provider.models?.join(", ") || "",
rpm: provider.rpm?.toString() || "",
tpm: provider.tpm?.toString() || "",
})
setIsConfigProviderOpen(true)
}
const handleSaveProvider = async () => {
try {
// 验证必填字段
if (!providerForm.name || !providerForm.url || !providerForm.apiKey || !providerForm.models) {
alert(language === "zh" ? "请填写所有必填字段" : "Please fill in all required fields")
return
}
const supportedModels = providerForm.models
.split(",")
.map((m) => m.trim())
.filter((m) => m.length > 0)
if (supportedModels.length === 0) {
alert(language === "zh" ? "请至少输入一个模型" : "Please enter 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({})
// }
// 新增:查看渠道供应商授权
const handleViewChannelAccess = async (channel: (typeof channels)[0]) => {
setSelectedChannel(channel)
setChannelAccessLoading(true)
setIsChannelAccessDialogOpen(true)
try {
const result = await TaijiAPIClient.getChannelProviderAccess({ channelId: channel.id })
if (result.success && result.data?.accessList) {
setChannelAccessList(result.data.accessList)
} else {
setChannelAccessList([])
}
} catch (error) {
console.error("Failed to load channel access list:", error)
setChannelAccessList([])
} finally {
setChannelAccessLoading(false)
}
}
// 新增:撤销渠道供应商授权
const handleRevokeAccess = async (access: any) => {
try {
const result = await TaijiAPIClient.revokeChannelProviderAccess(access.id)
if (result.success) {
alert(language === "zh" ? "授权已撤销" : "Access revoked successfully")
setRevokeAccessConfirm({ open: false, access: null })
// 重新加载授权列表
if (selectedChannel) {
const refreshResult = await TaijiAPIClient.getChannelProviderAccess({ channelId: selectedChannel.id })
if (refreshResult.success && refreshResult.data?.accessList) {
setChannelAccessList(refreshResult.data.accessList)
}
}
}
} catch (error) {
console.error("Failed to revoke access:", error)
alert(language === "zh" ? "撤销授权失败" : "Failed to revoke access")
}
}
// 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()
if (data.success && data.data) {
// 加载已选择的模型
if (data.data.models && Array.isArray(data.data.models)) {
setSelectedModels(data.data.models)
}
// 加载已分配的Agent
if (data.data.agents && Array.isArray(data.data.agents)) {
const agentIds = data.data.agents.map((a: any) => a.agentId)
setSelectedAgents(agentIds)
const quantities: { [key: string]: number } = {}
data.data.agents.forEach((a: any) => {
quantities[a.agentId] = a.quantity || 1
})
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))
}
}
}
} 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 {
await TaijiAPIClient.manageChannelResources(selectedChannel.id, {
models: selectedModels,
agents: selectedAgents.map((agentId) => ({
agentId,
quantity: agentQuantities[agentId] || 1,
})),
customAgentResources: {
cpu: parseFloat(customAgentCpu) || 2,
memory: parseFloat(customAgentMemory) || 4,
},
channelCredit: parseFloat(creditLimit) || 0,
})
// 重新加载数据
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)
// 可选:从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)
}
}
// 新增:编辑渠道信息
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')
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/channel/tenants`, {
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
const data = await response.json()
if (data.success && data.data.tenants) {
setChannelTenants(data.data.tenants)
} 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 {
const result = await TaijiAPIClient.changeTenantPassword(selectedTenantForEdit.id, newPassword)
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 {
const token = localStorage.getItem('channel_token') || localStorage.getItem('auth_token')
const response = await fetch(`/api/channel/tenants/${tenantId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
const data = await response.json()
if (data.success) {
setChannelTenants(channelTenants.filter(t => t.id !== tenantId))
console.log('Tenant deleted successfully')
}
} else {
console.error('Failed to delete tenant:', response.statusText)
}
} 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 (adminType: "billing" | "operations" | "admin") => {
if (!selectedChannel) return
try {
// TODO: 调用API删除管理员
console.log(`Removing ${adminType} admin from channel:`, selectedChannel.id)
setChannelAdmins({
...channelAdmins,
[adminType === "billing" ? "billingAdmin" : adminType === "operations" ? "operationsAdmin" : "admin"]: "",
})
} catch (error) {
console.error("Failed to remove admin:", error)
}
}
// 新增:创建租户或管理员
// 根据系统权限选择不同的后端接口:
// - 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">
{agentResources.reduce((sum, agent) => sum + (agent.cpu || 0), 0).toFixed(1)} {language === "zh" ? "核" : "Cores"}
</p>
</div>
</div>
<div className="text-xs text-muted-foreground">
{language === "zh"
? `共 ${agentResources.length} 个 Agent`
: `${agentResources.length} 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">
{agentResources.reduce((sum, agent) => sum + (agent.memory || 0), 0).toFixed(1)} GB
</p>
</div>
</div>
<div className="text-xs text-muted-foreground">
{language === "zh"
? `平均 ${agentResources.length > 0 ? (agentResources.reduce((sum, agent) => sum + (agent.memory || 0), 0) / agentResources.length).toFixed(1) : 0} GB/Agent`
: `Avg ${agentResources.length > 0 ? (agentResources.reduce((sum, agent) => sum + (agent.memory || 0), 0) / agentResources.length).toFixed(1) : 0} GB/Agent`}
</div>
</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>
<DropdownMenuItem onClick={() => handleViewChannelAccess(channel)}>
<Shield className="h-4 w-4 mr-2" />
{language === "zh" ? "管理授权" : "Manage Access"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">
<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>
{/* Channel Provider Access Management Dialog */}
<Dialog open={isChannelAccessDialogOpen} onOpenChange={setIsChannelAccessDialogOpen}>
<DialogContent className="bg-card border-border max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{language === "zh" ? "管理渠道授权" : "Manage Channel Access"}</DialogTitle>
<DialogDescription>
{language === "zh"
? `查看和管理渠道 "${selectedChannel?.name}" 的供应商授权`
: `View and manage provider access for channel "${selectedChannel?.name}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{channelAccessLoading ? (
<div className="text-center py-8">
<span className="text-muted-foreground">{language === "zh" ? "加载中..." : "Loading..."}</span>
</div>
) : channelAccessList.length === 0 ? (
<div className="text-center py-8">
<span className="text-muted-foreground">
{language === "zh" ? "该渠道暂无供应商授权" : "No provider access for this channel"}
</span>
</div>
) : (
<div className="space-y-3">
{channelAccessList.map((access) => (
<Card key={access.id} className="p-4 bg-background border-border">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-3">
<h4 className="font-semibold text-foreground">{access.providerName}</h4>
<span
className={`px-2 py-0.5 rounded text-xs font-medium ${
access.status === "active"
? "bg-green-500/10 text-green-500"
: access.status === "suspended"
? "bg-red-500/10 text-red-500"
: "bg-gray-500/10 text-gray-500"
}`}
>
{access.status === "active"
? language === "zh"
? "已授权"
: "Active"
: access.status === "suspended"
? language === "zh"
? "已暂停"
: "Suspended"
: language === "zh"
? "已过期"
: "Expired"}
</span>
</div>
<div className="mt-2 text-sm text-muted-foreground space-y-1">
<p>
{language === "zh" ? "供应商类型" : "Provider"}: {access.providerType || access.provider}
</p>
<p>
RPM: {access.rpmLimit?.toLocaleString() || "N/A"} | TPM: {access.tpmLimit?.toLocaleString() || "N/A"}
</p>
<p>
{language === "zh" ? "授权时间" : "Approved"}: {access.approvedAt ? new Date(access.approvedAt).toLocaleDateString() : "N/A"}
</p>
</div>
</div>
<div className="flex gap-2">
{access.status === "active" && (
<Button
variant="destructive"
size="sm"
onClick={() => setRevokeAccessConfirm({ open: true, access })}
>
{language === "zh" ? "撤销授权" : "Revoke"}
</Button>
)}
</div>
</div>
</Card>
))}
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsChannelAccessDialogOpen(false)}>
{language === "zh" ? "关闭" : "Close"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Revoke Access Confirmation Dialog */}
<Dialog open={revokeAccessConfirm.open} onOpenChange={(open) => setRevokeAccessConfirm({ open, access: open ? revokeAccessConfirm.access : null })}>
<DialogContent className="bg-card border-border">
<DialogHeader>
<DialogTitle className="text-destructive">
{language === "zh" ? "确认撤销授权" : "Confirm Revoke Access"}
</DialogTitle>
<DialogDescription>
{language === "zh"
? `确定要撤销渠道 "${selectedChannel?.name}" 对供应商 "${revokeAccessConfirm.access?.providerName}" 的授权吗?撤销后该渠道将无法使用该供应商的服务。`
: `Are you sure you want to revoke access to provider "${revokeAccessConfirm.access?.providerName}" for channel "${selectedChannel?.name}"? The channel will no longer be able to use this provider's services.`}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setRevokeAccessConfirm({ open: false, access: null })}>
{language === "zh" ? "取消" : "Cancel"}
</Button>
<Button
variant="destructive"
onClick={() => revokeAccessConfirm.access && handleRevokeAccess(revokeAccessConfirm.access)}
>
{language === "zh" ? "确认撤销" : "Revoke Access"}
</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) => (
<div
key={agent.id}
className={`border rounded-lg p-4 transition-all ${
selectedAgents.includes(agent.id) ? "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 (selectedAgents.includes(agent.id)) {
setSelectedAgents(selectedAgents.filter((a) => a !== agent.id))
const newQuantities = { ...agentQuantities }
delete newQuantities[agent.id]
setAgentQuantities(newQuantities)
} else {
setSelectedAgents([...selectedAgents, agent.id])
setAgentQuantities({ ...agentQuantities, [agent.id]: 1 })
}
}}
>
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center ${
selectedAgents.includes(agent.id) ? "bg-primary/20" : "bg-muted"
}`}
>
<Bot
className={`h-5 w-5 ${selectedAgents.includes(agent.id) ? "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>
{selectedAgents.includes(agent.id) && (
<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[agent.id] || 1}
onChange={(e) => {
const value = Math.max(1, Math.min(100, Number.parseInt(e.target.value) || 1))
setAgentQuantities({ ...agentQuantities, [agent.id]: value })
}}
className="w-20 h-9"
onClick={(e) => e.stopPropagation()}
/>
</div>
)}
<input
type="checkbox"
checked={selectedAgents.includes(agent.id)}
onChange={() => {}}
className="w-5 h-5 rounded border-border"
/>
</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-2xl">
<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>
{/* Resource Allocation */}
<div className="border-t border-border pt-6">
<h4 className="text-base font-semibold text-foreground mb-4">
{language === "zh" ? "资源配置" : "Resource Configuration"}
</h4>
<div className="grid grid-cols-2 gap-4">
<div className="bg-muted/50 rounded-lg p-4 text-center">
<p className="text-3xl font-bold text-primary mb-2">{selectedChannel?.cpuCores || 0}</p>
<p className="text-sm text-muted-foreground">
{language === "zh" ? "CPU核心" : "CPU Cores"}
</p>
</div>
<div className="bg-muted/50 rounded-lg p-4 text-center">
<p className="text-3xl font-bold text-primary mb-2">{selectedChannel?.memory || "0GB"}</p>
<p className="text-sm text-muted-foreground">
{language === "zh" ? "内存" : "Memory"}
</p>
</div>
</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>
</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={() => {
setSelectedTenantForPermission(tenant)
setTenantPermissions(tenant.permissions || [])
setIsTenantPermissionDialogOpen(true)
}}
>
{language === "zh" ? "管理权限" : "Manage Permissions"}
</Button>
<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>
{/* Manage Tenant Permissions Dialog */}
<Dialog open={isTenantPermissionDialogOpen} onOpenChange={setIsTenantPermissionDialogOpen}>
<DialogContent className="bg-card border-border max-w-md">
<DialogHeader>
<DialogTitle>{language === "zh" ? "管理租户权限" : "Manage Tenant Permissions"}</DialogTitle>
<DialogDescription>
{language === "zh"
? `为租户 "${selectedTenantForPermission?.name}" 分配权限`
: `Assign permissions to tenant "${selectedTenantForPermission?.name}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<p className="text-sm text-muted-foreground">
{language === "zh" ? "选择此租户可以访问的功能" : "Select the features this tenant can access"}
</p>
<div className="space-y-3">
{[
{ id: "dashboard", label: language === "zh" ? "仪表板" : "Dashboard" },
{ id: "agents", label: language === "zh" ? "Agent管理" : "Agent Management" },
{ id: "models", label: language === "zh" ? "模型配置" : "Model Configuration" },
{ id: "billing", label: language === "zh" ? "计费管理" : "Billing Management" },
{ id: "resources", label: language === "zh" ? "资源配置" : "Resource Configuration" },
{ id: "data-tools", label: language === "zh" ? "数据工具" : "Data Tools" },
{ id: "api-gateway", label: language === "zh" ? "API网关" : "API Gateway" },
].map((permission) => (
<div key={permission.id} className="flex items-center space-x-2">
<input
type="checkbox"
id={`perm-${permission.id}`}
checked={tenantPermissions.includes(permission.id)}
onChange={(e) => {
if (e.target.checked) {
setTenantPermissions([...tenantPermissions, permission.id])
} else {
setTenantPermissions(tenantPermissions.filter((p) => p !== permission.id))
}
}}
className="rounded border-border"
/>
<Label htmlFor={`perm-${permission.id}`} className="cursor-pointer font-normal">
{permission.label}
</Label>
</div>
))}
</div>
<div className="p-3 rounded bg-muted/30 text-sm text-muted-foreground">
{language === "zh"
? `已选择 ${tenantPermissions.length} 项权限`
: `${tenantPermissions.length} permission(s) selected`}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsTenantPermissionDialogOpen(false)}>
{language === "zh" ? "取消" : "Cancel"}
</Button>
<Button className="bg-primary text-primary-foreground" onClick={handleUpdateTenantPermissions}>
{language === "zh" ? "保存权限" : "Save Permissions"}
</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="bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
>
{language === "zh" ? "待审批" : "Pending"}
</Badge>
</td>
<td className="p-4 text-right">
<Button
size="sm"
variant="outline"
onClick={async () => {
setSelectedApproval(approval)
setIsApprovalDialogOpen(true)
}}
>
{language === "zh" ? "审批" : "Review"}
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
</div>
{/* Agent申请审批 */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground">
{language === "zh" ? "Agent申请审批" : "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" ? "Agent类型" : "Agent Type"}
</th>
<th className="text-left p-4 text-sm font-medium text-muted-foreground">
{language === "zh" ? "申请数量" : "Quantity"}
</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>
) : agentApprovals.length === 0 ? (
<tr>
<td colSpan={6} className="p-4 text-center text-muted-foreground">
{language === "zh" ? "暂无待审批申请" : "No pending applications"}
</td>
</tr>
) : (
agentApprovals.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 || approval.channelId}</td>
<td className="p-4 text-sm text-foreground">{approval.agentType || approval.details?.agentType || "-"}</td>
<td className="p-4 text-sm text-muted-foreground">{approval.requestedQuantity || approval.details?.quantity || 0}</td>
<td className="p-4 text-sm text-muted-foreground">{approval.submittedAt || approval.createdAt}</td>
<td className="p-4">
<Badge
variant="secondary"
className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
>
{language === "zh" ? "待审批" : "Pending"}
</Badge>
</td>
<td className="p-4 text-right">
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedAgentApproval(approval)
setIsAgentApprovalDialogOpen(true)
}}
>
{language === "zh" ? "审批" : "Review"}
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
</div>
<Dialog open={isAgentApprovalDialogOpen} onOpenChange={setIsAgentApprovalDialogOpen}>
<DialogContent className="bg-card border-border max-w-2xl">
<DialogHeader>
<DialogTitle>{language === "zh" ? "审批Agent申请" : "Review Agent Application"}</DialogTitle>
<DialogDescription>
{language === "zh"
? `审批渠道 "${selectedAgentApproval?.channelName}" 的Agent申请`
: `Review agent application from channel "${selectedAgentApproval?.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">{selectedAgentApproval?.channelName}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "提交时间" : "Submitted At"}
</label>
<p className="text-foreground mt-1">{selectedAgentApproval?.submittedAt}</p>
</div>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "Agent类型" : "Agent Type"}
</label>
<p className="text-foreground mt-1">{selectedAgentApproval?.agentType}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "申请数量" : "Requested Quantity"}
</label>
<p className="text-foreground mt-1">{selectedAgentApproval?.requestedQuantity}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">
{language === "zh" ? "申请理由" : "Reason"}
</label>
<p className="text-foreground mt-1">{selectedAgentApproval?.reason}</p>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
className="border-destructive text-destructive hover:bg-destructive/10 bg-transparent"
onClick={async () => {
if (!selectedAgentApproval) return
try {
await TaijiAPIClient.reviewApplication(selectedAgentApproval.id, false, language === "zh" ? "申请被拒绝" : "Application rejected")
await loadDashboardData()
setIsAgentApprovalDialogOpen(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 (!selectedAgentApproval) return
try {
await TaijiAPIClient.reviewApplication(selectedAgentApproval.id, true, language === "zh" ? "申请已批准" : "Application approved")
await loadDashboardData()
setIsAgentApprovalDialogOpen(false)
} 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.reviewApplication(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.reviewApplication(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计算资源分配" : "Agent Compute Resource Allocation"}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{language === "zh"
? "为每个Agent单独配置CPU和内存资源"
: "Configure CPU and memory resources for each agent individually"}
</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 resources available"}</span>
</div>
</Card>
) : (
agentResources.map((agent) => (
<Card
key={agent.id}
className="p-4 bg-card border-border cursor-pointer hover:border-primary transition-colors"
onClick={() => {
setSelectedAgent(agent)
setComputeConfigOpen(true)
}}
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="font-semibold text-foreground">{agent.name}</h3>
<p className="text-xs text-muted-foreground">{agent.id}</p>
</div>
<div
className={`px-2 py-1 rounded text-xs ${
agent.status === "active" ? "bg-green-500/20 text-green-500" : "bg-gray-500/20 text-gray-500"
}`}
>
{agent.status === "active"
? language === "zh"
? "活跃"
: "Active"
: language === "zh"
? "空闲"
: "Idle"}
</div>
</div>
<div className="space-y-3">
<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.cpu} {language === "zh" ? "核" : "Cores"}
</span>
</div>
<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.memory} GB</span>
</div>
<div className="pt-2 border-t border-border">
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">CPU {language === "zh" ? "使用率" : "Usage"}</span>
<span className="text-foreground">{agent.usage?.cpu ?? 0}%</span>
</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div className="bg-blue-500 h-1.5 rounded-full" style={{ width: `${agent.usage?.cpu ?? 0}%` }} />
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">
{language === "zh" ? "内存使用率" : "Memory Usage"}
</span>
<span className="text-foreground">{agent.usage?.memory ?? 0}%</span>
</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div
className="bg-green-500 h-1.5 rounded-full"
style={{ width: `${agent.usage?.memory ?? 0}%` }}
/>
</div>
</div>
</div>
</div>
<div className="flex gap-2 mt-3">
<Button variant="ghost" size="sm" className="flex-1" onClick={(e) => {
e.stopPropagation()
setSelectedAgent(agent)
setComputeConfigOpen(true)
}}>
<Settings className="h-4 w-4 mr-2" />
{language === "zh" ? "配置" : "Config"}
</Button>
<Button
variant="ghost"
size="sm"
className="flex-1 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={async (e) => {
e.stopPropagation()
if (confirm(language === "zh" ? `确定要删除 ${agent.name} 吗?` : `Are you sure you want to delete ${agent.name}?`)) {
try {
setResourcesLoading(true)
await TaijiAPIClient.deleteAgentResource(agent.id)
await loadDashboardData()
alert(language === "zh" ? "Agent已删除" : "Agent deleted successfully")
} catch (error) {
console.error("Failed to delete agent:", error)
alert(language === "zh" ? "删除失败" : "Failed to delete")
} finally {
setResourcesLoading(false)
}
}
}}
>
<Trash2 className="h-4 w-4 mr-2" />
{language === "zh" ? "删除" : "Delete"}
</Button>
</div>
</Card>
))
)}
</div>
</div>
{/* Agent Resource Configuration Dialog */}
<Dialog open={computeConfigOpen} onOpenChange={setComputeConfigOpen}>
<DialogContent className="bg-background border-border max-w-2xl">
<DialogHeader>
<DialogTitle className="text-foreground">
{language === "zh" ? "Agent资源配置" : "Agent Resource Configuration"}
</DialogTitle>
</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.name}</h4>
<p className="text-sm text-muted-foreground">{selectedAgent.id}</p>
</div>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium text-foreground mb-1 block">
{language === "zh" ? "CPU核数" : "CPU Cores"}
</label>
<Input
type="number"
placeholder={language === "zh" ? "输入CPU核数" : "Enter CPU cores"}
defaultValue={selectedAgent.cpu}
min={1}
max={16}
className="bg-background border-border text-foreground"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "推荐范围: 1-16核" : "Recommended range: 1-16 cores"}
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground mb-1 block">
{language === "zh" ? "内存大小(GB)" : "Memory Size (GB)"}
</label>
<Input
type="number"
placeholder={language === "zh" ? "输入内存大小" : "Enter memory size"}
defaultValue={selectedAgent.memory}
min={1}
max={32}
className="bg-background border-border text-foreground"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "推荐范围: 1-32GB" : "Recommended range: 1-32GB"}
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground mb-1 block">
{language === "zh" ? "平台可用Agent个数" : "Available Agent Instances"}
</label>
<Input
type="number"
placeholder={language === "zh" ? "输入可用Agent个数" : "Enter available agents"}
defaultValue={10}
min={1}
max={100}
className="bg-background border-border text-foreground"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "推荐范围: 1-100个" : "Recommended range: 1-100 instances"}
</p>
</div>
<div className="p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg">
<p className="text-sm text-foreground">
{language === "zh" ? "当前使用率: " : "Current Usage: "}
CPU {selectedAgent?.usage?.cpu ?? 0}% · {language === "zh" ? "内存" : "Memory"}{" "}
{selectedAgent?.usage?.memory ?? 0}%
</p>
</div>
</div>
</>
)}
<div className="flex justify-end gap-3">
<Button variant="outline" onClick={() => setComputeConfigOpen(false)}>
{language === "zh" ? "取消" : "Cancel"}
</Button>
<Button onClick={() => setComputeConfigOpen(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">
<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 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) => {
const statusColor =
agent.status === "healthy"
? "text-green-500 bg-green-500/10"
: agent.status === "warning"
? "text-yellow-500 bg-yellow-500/10"
: "text-red-500 bg-red-500/10"
const statusText =
agent.status === "healthy" ? text.healthy : agent.status === "warning" ? text.warning : text.critical
return (
<Card key={agent.id} 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">
{language === "zh" ? agent.nameCn : agent.name}
</h3>
<p className="text-xs text-muted-foreground">{agent.lastActive}</p>
</div>
</div>
<Badge className={statusColor}>{statusText}</Badge>
</div>
{/* Agent metrics */}
<div className="space-y-3">
{/* CPU Usage */}
<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}%</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className={`h-2 rounded-full ${
agent.cpuUsage > 80
? "bg-red-500"
: agent.cpuUsage > 60
? "bg-yellow-500"
: "bg-green-500"
}`}
style={{ width: `${agent.cpuUsage}%` }}
/>
</div>
</div>
{/* Memory Usage */}
<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}%</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className={`h-2 rounded-full ${
agent.memoryUsage > 80
? "bg-red-500"
: agent.memoryUsage > 60
? "bg-yellow-500"
: "bg-green-500"
}`}
style={{ width: `${agent.memoryUsage}%` }}
/>
</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">{text.responseTime}</p>
<p className="text-sm font-semibold text-foreground">
{agent.responseTime} {language === "zh" ? "ms" : "ms"}
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">{text.responseTime}</p>
<p className="text-sm font-semibold text-foreground">
{agent.requestCount?.toLocaleString() || 0} {language === "zh" ? "请求" : "requests"}
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">{text.errorRate}</p>
<p className="text-sm font-semibold text-foreground">{agent.errorRate}%</p>
</div>
<div>
<p className="text-xs text-muted-foreground">{text.uptime}</p>
<p className="text-sm font-semibold text-foreground">{agent.uptime}</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={() => {
console.log("[v0] Saving permissions for", selectedRole, rolePermissions[selectedRole])
}}
>
{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={() => {
// Handle subscription level assignment
console.log(`Assigning subscription level ${selectedSubscriptionLevel} to tenant ${selectedTenant}`)
setIsSubscriptionDialogOpen(false)
}}
>
{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>
<div className="space-y-2">
<Label htmlFor="provider-key">{language === "zh" ? "API 密钥" : "API Key"}</Label>
<Input
id="provider-key"
type="password"
placeholder={language === "zh" ? "输入 API 密钥" : "Enter API key"}
value={providerForm.apiKey}
onChange={(e) => setProviderForm({ ...providerForm, apiKey: 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>
<Input
id="provider-models"
placeholder={
language === "zh"
? "例: gpt-4, gpt-3.5-turbo (逗号分隔)"
: "e.g., gpt-4, gpt-3.5-turbo (comma separated)"
}
value={providerForm.models}
onChange={(e) => setProviderForm({ ...providerForm, models: e.target.value })}
className="bg-background border-border text-foreground"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "多个模型请用逗号分隔" : "Separate multiple models with commas"}
</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>
<div className="space-y-2">
<Label htmlFor="config-provider-key">{language === "zh" ? "API 密钥" : "API Key"}</Label>
<Input
id="config-provider-key"
type="password"
value={providerForm.apiKey}
onChange={(e) => setProviderForm({ ...providerForm, apiKey: e.target.value })}
className="bg-background border-border text-foreground"
/>
<p className="text-xs text-muted-foreground">
{language === "zh" ? "留空则不修改密钥" : "Leave blank to keep existing key"}
</p>
</div>
{providerType === "model" && (
<>
<div className="space-y-2">
<Label htmlFor="config-provider-models">
{language === "zh" ? "可调用模型" : "Available Models"}
</Label>
<Input
id="config-provider-models"
value={providerForm.models}
onChange={(e) => setProviderForm({ ...providerForm, models: e.target.value })}
className="bg-background border-border text-foreground"
/>
</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={() => {
console.log("[v0] Creating admin:", newAdminForm)
setShowAddAdminDialog(false)
setNewAdminForm({ name: "", email: "", password: "", role: "billing-admin" })
}}
disabled={!newAdminForm.name || !newAdminForm.email || !newAdminForm.password}
>
{t("创建管理员", "Create Administrator")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}