- 在渠道管理页面添加删除租户按钮和确认对话框 - 在渠道设置页面添加删除管理员按钮和确认对话框 - 在渠道编辑对话框中添加删除管理员功能 - 添加完整的删除确认流程,包含警告提示 - 删除成功后自动刷新列表 - 添加API调用调试日志,便于排查问题 - 支持中英文双语显示
2081 lines
90 KiB
TypeScript
2081 lines
90 KiB
TypeScript
"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>
|
||
|
||
{/* 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)}
|
||
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" | "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
|