feat: 添加删除租户和管理员功能

- 在渠道管理页面添加删除租户按钮和确认对话框
- 在渠道设置页面添加删除管理员按钮和确认对话框
- 在渠道编辑对话框中添加删除管理员功能
- 添加完整的删除确认流程,包含警告提示
- 删除成功后自动刷新列表
- 添加API调用调试日志,便于排查问题
- 支持中英文双语显示
This commit is contained in:
xiaohei
2025-12-28 16:35:05 +00:00
parent eceb2f6ea8
commit 3e148c390e
3 changed files with 351 additions and 8 deletions
+100 -4
View File
@@ -101,6 +101,9 @@ export function ChannelsTab({
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)
@@ -480,12 +483,35 @@ export function ChannelsTab({
console.log("Change password for tenant:", tenantId)
}
const handleRemoveAdmin = async (adminId: string) => {
if (!selectedChannel) return
const handleRemoveAdmin = (admin: any) => {
setSelectedAdminForDelete(admin)
setShowDeleteChannelAdminDialog(true)
}
const handleConfirmDeleteChannelAdmin = async () => {
if (!selectedAdminForDelete || !selectedChannel) return
try {
console.log(`Removing admin from channel:`, selectedChannel.id)
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)
}
}
@@ -1353,7 +1379,7 @@ export function ChannelsTab({
<Button
size="sm"
variant="ghost"
onClick={() => handleRemoveAdmin(admin.id)}
onClick={() => handleRemoveAdmin(admin)}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<Trash2 className="h-4 w-4" />
@@ -1977,6 +2003,76 @@ export function ChannelsTab({
</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>
)
}
+245 -1
View File
@@ -99,6 +99,9 @@ export default function ChannelDashboard() {
const [isAddAdminOpen, setIsAddAdminOpen] = useState(false)
const [channelAdmins, setChannelAdmins] = useState<any[]>([])
const [isLoadingAdmins, setIsLoadingAdmins] = useState(false)
const [showDeleteAdminDialog, setShowDeleteAdminDialog] = useState(false)
const [selectedAdminForDelete, setSelectedAdminForDelete] = useState<any>(null)
const [isDeletingAdmin, setIsDeletingAdmin] = useState(false)
const [createAdminForm, setCreateAdminForm] = useState({
name: "",
email: "",
@@ -130,6 +133,8 @@ export default function ChannelDashboard() {
const [showTenantDetailsDialog, setShowTenantDetailsDialog] = useState(false)
const [selectedTenantForDetails, setSelectedTenantForDetails] = useState<any>(null)
const [isDeletingTenant, setIsDeletingTenant] = useState(false)
const [showDeleteTenantDialog, setShowDeleteTenantDialog] = useState(false)
const [selectedTenantForDelete, setSelectedTenantForDelete] = useState<any>(null)
// API Data states
const [tenantsData, setTenantsData] = useState<any[]>([])
@@ -341,16 +346,100 @@ export default function ChannelDashboard() {
}
}
// Handle delete tenant
const handleDeleteTenant = async () => {
if (!selectedTenantForDelete) return
try {
setIsDeletingTenant(true)
const response = await TaijiAPIClient.deleteTenant(selectedTenantForDelete.id)
if (response.success) {
toast({
title: language === "zh" ? "删除成功" : "Deleted Successfully",
description: language === "zh"
? `租户 "${selectedTenantForDelete.name}" 已被删除`
: `Tenant "${selectedTenantForDelete.name}" has been deleted`,
})
// Refresh the tenants list
await loadChannelData()
} else {
toast({
title: language === "zh" ? "删除失败" : "Delete Failed",
description: response.error?.message || (language === "zh" ? "无法删除租户" : "Failed to delete tenant"),
variant: "destructive",
})
}
} catch (error: any) {
toast({
title: language === "zh" ? "删除失败" : "Delete Failed",
description: error.message || (language === "zh" ? "删除租户时发生错误" : "Error occurred while deleting tenant"),
variant: "destructive",
})
} finally {
setIsDeletingTenant(false)
setShowDeleteTenantDialog(false)
setSelectedTenantForDelete(null)
}
}
// Handle delete admin
const handleDeleteAdmin = async () => {
if (!selectedAdminForDelete) return
try {
setIsDeletingAdmin(true)
const response = await TaijiAPIClient.deleteAdmin(selectedAdminForDelete.id)
if (response.success) {
toast({
title: language === "zh" ? "删除成功" : "Deleted Successfully",
description: language === "zh"
? `管理员 "${selectedAdminForDelete.name}" 已被删除`
: `Administrator "${selectedAdminForDelete.name}" has been deleted`,
})
// Refresh the admins list
await loadChannelAdmins()
} else {
toast({
title: language === "zh" ? "删除失败" : "Delete Failed",
description: response.error?.message || (language === "zh" ? "无法删除管理员" : "Failed to delete administrator"),
variant: "destructive",
})
}
} catch (error: any) {
toast({
title: language === "zh" ? "删除失败" : "Delete Failed",
description: error.message || (language === "zh" ? "删除管理员时发生错误" : "Error occurred while deleting administrator"),
variant: "destructive",
})
} finally {
setIsDeletingAdmin(false)
setShowDeleteAdminDialog(false)
setSelectedAdminForDelete(null)
}
}
// Load channel administrators
const loadChannelAdmins = async () => {
try {
setIsLoadingAdmins(true)
console.log("🔍 正在加载渠道管理员列表...")
const response = await TaijiAPIClient.getChannelAdmins()
console.log("📥 获取管理员列表响应:", response)
if (response.success && response.data?.admins) {
console.log("✅ 成功获取管理员列表:", response.data.admins)
setChannelAdmins(response.data.admins)
} else {
console.warn("⚠️ 响应格式异常或无管理员数据:", response)
}
} catch (error) {
console.error("Failed to load channel admins:", error)
console.error("❌ 加载渠道管理员失败:", error)
toast({
title: language === "zh" ? "加载失败" : "Failed to Load",
description: language === "zh"
? "无法加载管理员列表,请检查网络连接和登录状态"
: "Failed to load administrators. Please check network connection and login status.",
variant: "destructive",
})
} finally {
setIsLoadingAdmins(false)
}
@@ -957,6 +1046,16 @@ export default function ChannelDashboard() {
<DollarSign className="mr-2 h-4 w-4" />
{language === "zh" ? "管理计费" : "Manage Billing"}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelectedTenantForDelete(tenant)
setShowDeleteTenantDialog(true)
}}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
{language === "zh" ? "删除租户" : "Delete Tenant"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -1716,6 +1815,17 @@ export default function ChannelDashboard() {
: (language === "zh" ? "已停用" : "Inactive")}
</Badge>
)}
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedAdminForDelete(admin)
setShowDeleteAdminDialog(true)
}}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</Card>
@@ -2628,6 +2738,140 @@ export default function ChannelDashboard() {
</DialogContent>
</Dialog>
{/* Delete Tenant Confirmation Dialog */}
<Dialog open={showDeleteTenantDialog} onOpenChange={setShowDeleteTenantDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-destructive">
{language === "zh" ? "删除租户" : "Delete Tenant"}
</DialogTitle>
<DialogDescription>
{language === "zh"
? "此操作将永久删除该租户及其所有数据。此操作无法撤销。"
: "This action will permanently delete this tenant and all its data. This action cannot be undone."}
</DialogDescription>
</DialogHeader>
{selectedTenantForDelete && (
<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">
{language === "zh" ? "将要删除的租户:" : "Tenant to be deleted:"}
</p>
<p className="text-lg font-bold mt-1">{selectedTenantForDelete.name}</p>
<div className="mt-3 space-y-1 text-sm text-muted-foreground">
<p>• {language === "zh" ? "用户数" : "Users"}: {selectedTenantForDelete.users}</p>
<p>• {language === "zh" ? "当前方案" : "Current Plan"}: {selectedTenantForDelete.plan}</p>
<p>• {language === "zh" ? "状态" : "Status"}: {selectedTenantForDelete.status}</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: Tenant data cannot be recovered after deletion. Please proceed with caution."}
</p>
</div>
</div>
)}
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowDeleteTenantDialog(false)}
disabled={isDeletingTenant}
>
{language === "zh" ? "取消" : "Cancel"}
</Button>
<Button
variant="destructive"
onClick={handleDeleteTenant}
disabled={isDeletingTenant}
>
{isDeletingTenant
? (language === "zh" ? "删除中..." : "Deleting...")
: (language === "zh" ? "确认删除" : "Confirm Delete")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Admin Confirmation Dialog */}
<Dialog open={showDeleteAdminDialog} onOpenChange={setShowDeleteAdminDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-destructive">
{language === "zh" ? "删除管理员" : "Delete 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">
{language === "zh" ? "将要删除的管理员:" : "Administrator to be deleted:"}
</p>
<p className="text-lg font-bold mt-1">{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_admin"
? (language === "zh" ? "计费管理员" : "Billing Admin")
: (language === "zh" ? "运营管理员" : "Operations Admin")
}</p>
{selectedAdminForDelete.status && (
<p>• {language === "zh" ? "状态" : "Status"}: {
selectedAdminForDelete.status === "active"
? (language === "zh" ? "活跃" : "Active")
: (language === "zh" ? "已停用" : "Inactive")
}</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={() => setShowDeleteAdminDialog(false)}
disabled={isDeletingAdmin}
>
{language === "zh" ? "取消" : "Cancel"}
</Button>
<Button
variant="destructive"
onClick={handleDeleteAdmin}
disabled={isDeletingAdmin}
>
{isDeletingAdmin
? (language === "zh" ? "删除中..." : "Deleting...")
: (language === "zh" ? "确认删除" : "Confirm Delete")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</main>
</div>
)
+6 -3
View File
@@ -699,9 +699,12 @@ export class TaijiAPIClient {
* GET /api/channel/admins
*/
static async getChannelAdmins() {
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/channel/admins`, {
headers: buildHeaders(),
})
const url = `${API_BASE_URLS.mcpServer}/api/channel/admins`
console.log("🌐 API请求: GET", url)
const headers = buildHeaders()
console.log("📋 请求头:", headers)
const response = await fetch(url, { headers })
console.log("📨 响应状态:", response.status, response.statusText)
return handleResponse(response)
}