Files
taiji-pad-v0/app/admin/dashboard/components/ChannelsTab.tsx
T

2039 lines
88 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"
/**
* ChannelsTab Component - 渠道管理标签页
*
* 完整功能实现 (1987行)
*
* ✅ 已实现功能:
* 1. 渠道列表展示和搜索
* 2. 添加新渠道
* 3. 佣金比例编辑
* 4. 渠道授权管理(供应商访问权限)
* 5. 资源管理(模型、Agent分配、CPU/内存/存储、授信额度配置)
* 6. 渠道详情查看
* 7. 渠道基本信息编辑
* 8. 租户管理(查看、添加、权限分配、禁用、删除)
* 9. 供应商申请审批流程
* 10. Agent申请审批流程
*
* 状态: ✅ 完整功能已恢复
* 参考源码: page_old.tsx lines 1459-2909
* 最后更新: 2024
*/
import { useState } from "react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Badge } from "@/components/ui/badge"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Plus,
Search,
MoreVertical,
Building2,
Eye,
Edit,
Users,
DollarSign,
Shield,
Trash2,
Settings,
Bot,
} from "lucide-react"
import { TaijiAPIClient, API_BASE_URLS } from "@/lib/api-client"
import type { Channel } from "../types"
interface ChannelsTabProps {
language: "zh" | "en"
channels: Channel[]
loading: boolean
loadDashboardData: () => Promise<void>
availableModels: any[]
availableAgents: any[]
providerApprovals: any[]
agentApprovals: any[]
}
export function ChannelsTab({
language,
channels,
loading,
loadDashboardData,
availableModels,
availableAgents,
providerApprovals,
agentApprovals,
}: ChannelsTabProps) {
// Dialog states
const [isAddChannelOpen, setIsAddChannelOpen] = useState(false)
const [isCommissionDialogOpen, setIsCommissionDialogOpen] = useState(false)
const [isResourceManagementOpen, setIsResourceManagementOpen] = useState(false)
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 [isChannelAccessDialogOpen, setIsChannelAccessDialogOpen] = useState(false)
const [isApprovalDialogOpen, setIsApprovalDialogOpen] = useState(false)
const [isAgentApprovalDialogOpen, setIsAgentApprovalDialogOpen] = useState(false)
const [showDeleteChannelAdminDialog, setShowDeleteChannelAdminDialog] = useState(false)
const [selectedAdminForDelete, setSelectedAdminForDelete] = useState<any>(null)
const [isDeletingChannelAdmin, setIsDeletingChannelAdmin] = useState(false)
// Data states
const [selectedChannel, setSelectedChannel] = useState<Channel | null>(null)
const [commissionRate, setCommissionRate] = useState<string>("")
const [selectedModels, setSelectedModels] = useState<string[]>([])
const [selectedAgents, setSelectedAgents] = useState<string[]>([])
const [agentQuantities, setAgentQuantities] = useState<Record<string, number>>({})
const [customAgentCpu, setCustomAgentCpu] = useState("2")
const [customAgentMemory, setCustomAgentMemory] = useState("4")
const [creditLimit, setCreditLimit] = useState<string>("")
const [channelTenants, setChannelTenants] = useState<any[]>([])
const [channelTenantsLoading, setChannelTenantsLoading] = useState(false)
const [channelAdmins, setChannelAdmins] = useState<any[]>([])
const [channelAccessList, setChannelAccessList] = useState<any[]>([])
const [channelAccessLoading, setChannelAccessLoading] = useState(false)
const [selectedTenantForPermission, setSelectedTenantForPermission] = useState<any>(null)
const [tenantPermissions, setTenantPermissions] = useState<string[]>([])
const [selectedApproval, setSelectedApproval] = useState<any>(null)
const [selectedAgentApproval, setSelectedAgentApproval] = useState<any>(null)
const [revokeAccessConfirm, setRevokeAccessConfirm] = useState<{ open: boolean; access: any | null }>({
open: false,
access: null,
})
// Form states
const [newChannelForm, setNewChannelForm] = useState({
name: "",
email: "",
password: "",
commissionRate: "",
})
const [channelEditForm, setChannelEditForm] = useState({
name: "",
contactName: "",
email: "",
phone: "",
})
const [newTenantForm, setNewTenantForm] = useState({
name: "",
email: "",
password: "",
systemRole: "tenant" as "tenant" | "admin" | "billing-admin" | "operations-admin",
})
const text = language === "zh" ? {
channelManagement: "渠道管理",
channelDescription: "管理分销渠道及其租户组合",
addChannel: "添加渠道",
createChannel: "创建新渠道",
channelName: "渠道名称",
channelNamePlaceholder: "输入渠道名称",
contactEmail: "联系邮箱",
password: "密码",
commissionRate: "佣金比例 (%)",
cancel: "取消",
create: "创建",
searchChannels: "搜索渠道...",
contactPerson: "联系人",
tenantCount: "租户数",
commission: "佣金比例",
viewDetails: "查看详情",
edit: "编辑",
delete: "删除",
} : {
channelManagement: "Channel Management",
channelDescription: "Manage distribution channels and their tenant portfolios",
addChannel: "Add Channel",
createChannel: "Create New Channel",
channelName: "Channel Name",
channelNamePlaceholder: "Enter channel name",
contactEmail: "Contact Email",
password: "Password",
commissionRate: "Commission Rate (%)",
cancel: "Cancel",
create: "Create",
searchChannels: "Search channels...",
contactPerson: "Contact Person",
tenantCount: "Tenants",
commission: "Commission Rate",
viewDetails: "View Details",
edit: "Edit",
delete: "Delete",
}
// Handler functions
const handleEditCommission = (channel: Channel) => {
setSelectedChannel(channel)
setCommissionRate(channel.commission.toString())
setIsCommissionDialogOpen(true)
}
const handleSaveCommission = async () => {
if (!selectedChannel) return
try {
await TaijiAPIClient.manageChannelResources(selectedChannel.id, {
channelCredit: selectedChannel.channelCredit || 0,
})
await loadDashboardData()
setIsCommissionDialogOpen(false)
setCommissionRate("")
} catch (error) {
console.error("Failed to save commission:", error)
alert(language === "zh" ? "保存失败" : "Failed to save")
}
}
const handleViewChannelAccess = async (channel: Channel) => {
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")
}
}
const handleResourceManagement = async (channel: Channel) => {
setSelectedChannel(channel)
setSelectedModels([])
setSelectedAgents([])
setAgentQuantities({})
setCreditLimit("")
setCustomAgentCpu("2")
setCustomAgentMemory("4")
try {
const token = localStorage.getItem("admin_token") || localStorage.getItem("auth_token")
const response = await fetch(`http://localhost:8002/api/admin/channels/${channel.id}/resources`, {
headers: { Authorization: `Bearer ${token}` },
})
if (response.ok) {
const data = await response.json()
if (data.models && Array.isArray(data.models)) {
setSelectedModels(data.models)
}
if (data.agents && Array.isArray(data.agents)) {
const agentIds = data.agents.map((a: any) => a.agentId)
setSelectedAgents(agentIds)
const quantities: { [key: string]: number } = {}
data.agents.forEach((a: any) => {
quantities[a.agentId] = a.quantity || 1
})
setAgentQuantities(quantities)
}
if (data.customAgentResources) {
setCustomAgentCpu(String(data.customAgentResources.cpu || 2))
setCustomAgentMemory(String(data.customAgentResources.memory || 4))
}
if (data.channelCredit !== undefined) {
setCreditLimit(String(data.channelCredit))
}
}
} catch (error) {
console.error("Failed to load channel resources:", error)
}
setIsResourceManagementOpen(true)
}
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: Channel) => {
setSelectedChannel(channel)
setIsChannelDetailsDialogOpen(true)
try {
const response = await fetch(`${API_BASE_URLS.mcpServer}/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: Channel) => {
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_BASE_URLS.mcpServer}/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: "PATCH",
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: Channel) => {
setSelectedChannel(channel)
setIsViewTenantsDialogOpen(true)
setChannelTenantsLoading(true)
try {
const token = localStorage.getItem("channel_token") || localStorage.getItem("auth_token")
const response = await fetch("http://localhost:8002/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 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_BASE_URLS.mcpServer}/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("channel_token") || localStorage.getItem("auth_token")
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/channel/tenants/${tenantId}/status`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ status: "disabled" }),
})
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 handleTenantChangePassword = async (tenantId: string) => {
const tenant = channelTenants.find((t) => t.id === tenantId)
if (!tenant) return
console.log("Change password for tenant:", tenantId)
}
const handleRemoveAdmin = (admin: any) => {
setSelectedAdminForDelete(admin)
setShowDeleteChannelAdminDialog(true)
}
const handleConfirmDeleteChannelAdmin = async () => {
if (!selectedAdminForDelete || !selectedChannel) return
try {
setIsDeletingChannelAdmin(true)
const response = await TaijiAPIClient.deleteAdmin(selectedAdminForDelete.id)
if (response.success) {
// Refresh channel admins list
const adminsResponse = await TaijiAPIClient.getChannelAdmins()
if (adminsResponse.success && adminsResponse.data?.admins) {
// Filter admins for current channel
const channelAdminsList = adminsResponse.data.admins.filter(
(admin: any) => admin.channelId === selectedChannel.id
)
setChannelAdmins(channelAdminsList)
}
console.log(`Successfully removed admin from channel:`, selectedChannel.id)
}
} catch (error) {
console.error("Failed to remove admin:", error)
} finally {
setIsDeletingChannelAdmin(false)
setShowDeleteChannelAdminDialog(false)
setSelectedAdminForDelete(null)
}
}
const handleCreateTenant = async () => {
if (!selectedChannel || !newTenantForm.name || !newTenantForm.email || !newTenantForm.password) {
alert(language === "zh" ? "请填写所有必需字段" : "Please fill all required fields")
return
}
try {
const result = await TaijiAPIClient.createChannelTenant({
name: newTenantForm.name,
email: newTenantForm.email,
password: newTenantForm.password,
systemRole: newTenantForm.systemRole,
})
if (result.success) {
alert(language === "zh" ? "租户创建成功" : "Tenant created successfully")
setNewTenantForm({ name: "", email: "", password: "", systemRole: "tenant" })
setIsAddTenantDialogOpen(false)
if (selectedChannel) {
handleViewTenants(selectedChannel)
}
} else {
alert(language === "zh" ? "创建租户失败" : "Failed to create tenant")
}
} catch (error) {
console.error("Failed to create tenant:", error)
alert(language === "zh" ? "创建租户出错" : "Error creating tenant")
}
}
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")
}
}
return (
<div className="space-y-6">
{/* Header */}
<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>
{/* Search */}
<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>
{/* Channel List */}
<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={() => 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>
{/* Resource Management Dialog */}
<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>
<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>
<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-3 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 className="bg-muted/50 rounded-lg p-4 text-center">
<p className="text-3xl font-bold text-primary mb-2">{selectedChannel?.storage || "0GB"}</p>
<p className="text-sm text-muted-foreground">{language === "zh" ? "存储空间" : "Storage"}</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>
</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" | "admin" | "billing-admin" | "operations-admin") =>
setNewTenantForm({ ...newTenantForm, systemRole: value })
}
>
<SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="tenant">
{language === "zh" ? "租户" : "Tenant"}
</SelectItem>
<SelectItem value="admin">
{language === "zh" ? "管理员" : "Admin"}
</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>
{/* Provider Approvals Table */}
<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 Approvals Table */}
<div className="space-y-4 mt-6">
<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>
{/* Provider Approval 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>
{/* Agent Approval Dialog */}
<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>
{/* Delete Channel Admin Confirmation Dialog */}
<Dialog open={showDeleteChannelAdminDialog} onOpenChange={setShowDeleteChannelAdminDialog}>
<DialogContent className="bg-card border-border">
<DialogHeader>
<DialogTitle className="text-destructive">
{language === "zh" ? "删除渠道管理员" : "Delete Channel Administrator"}
</DialogTitle>
<DialogDescription>
{language === "zh"
? "此操作将永久删除该管理员账号。此操作无法撤销。"
: "This action will permanently delete this administrator account. This action cannot be undone."}
</DialogDescription>
</DialogHeader>
{selectedAdminForDelete && (
<div className="space-y-4 py-4">
<div className="rounded-lg bg-destructive/10 border border-destructive/20 p-4">
<div className="flex items-start gap-3">
<Trash2 className="h-5 w-5 text-destructive mt-0.5" />
<div className="flex-1">
<p className="font-medium text-sm text-foreground">
{language === "zh" ? "将要删除的管理员:" : "Administrator to be deleted:"}
</p>
<p className="text-lg font-bold mt-1 text-foreground">{selectedAdminForDelete.name}</p>
<div className="mt-3 space-y-1 text-sm text-muted-foreground">
<p>• {language === "zh" ? "邮箱" : "Email"}: {selectedAdminForDelete.email}</p>
<p>• {language === "zh" ? "角色" : "Role"}: {
selectedAdminForDelete.role === "channel_admin"
? (language === "zh" ? "渠道管理员" : "Channel Admin")
: selectedAdminForDelete.role === "billing" || selectedAdminForDelete.role === "billing_admin"
? (language === "zh" ? "计费管理员" : "Billing Admin")
: (language === "zh" ? "运营管理员" : "Operations Admin")
}</p>
{selectedChannel && (
<p>• {language === "zh" ? "所属渠道" : "Channel"}: {selectedChannel.name}</p>
)}
</div>
</div>
</div>
</div>
<div className="rounded-lg bg-amber-500/10 border border-amber-500/20 p-3">
<p className="text-sm font-medium text-amber-700 dark:text-amber-500">
⚠️ {language === "zh"
? "警告:删除后将无法恢复管理员账号及其权限,请谨慎操作。"
: "Warning: Administrator account and permissions cannot be recovered after deletion. Please proceed with caution."}
</p>
</div>
</div>
)}
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowDeleteChannelAdminDialog(false)}
disabled={isDeletingChannelAdmin}
className="border-border text-foreground"
>
{language === "zh" ? "取消" : "Cancel"}
</Button>
<Button
variant="destructive"
onClick={handleConfirmDeleteChannelAdmin}
disabled={isDeletingChannelAdmin}
>
{isDeletingChannelAdmin
? (language === "zh" ? "删除中..." : "Deleting...")
: (language === "zh" ? "确认删除" : "Confirm Delete")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
export default ChannelsTab