From 2da94473972ca8c4faab506dad5dabc0b5455ed0 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 30 Apr 2026 21:10:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BB=A3=E7=90=86=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=89=B9=E9=87=8F=E6=93=8D=E4=BD=9C=E5=8A=9F?= =?UTF-8?q?=E8=83=BD,=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=88=B0=E6=9C=9F=E6=97=B6=E9=97=B4=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 代理管理页面添加批量解封、批量封禁、批量删除功能 - 后端添加代理批量操作API - 用户管理添加独立的到期时间列 - 修复应用管理和日志记录页面顶部卡片图标颜色 --- backend/internal/router/developer/agents.go | 62 +++++++++++++++++++ .../admin/agents/components/data-table.vue | 18 ++++++ frontend/src/pages/admin/agents/index.vue | 49 +++++++++++++++ frontend/src/pages/admin/applications.vue | 6 +- frontend/src/pages/admin/logs/index.vue | 4 +- .../pages/admin/users/components/columns.ts | 57 +++++++++++------ .../admin/users/components/data-table.vue | 2 + frontend/src/pages/developer/applications.vue | 6 +- frontend/src/plugins/i18n/zh.json | 10 +++ 9 files changed, 187 insertions(+), 27 deletions(-) diff --git a/backend/internal/router/developer/agents.go b/backend/internal/router/developer/agents.go index c8c78fd..39980e0 100644 --- a/backend/internal/router/developer/agents.go +++ b/backend/internal/router/developer/agents.go @@ -23,6 +23,8 @@ func SetupAgentsRoutes(r *gin.RouterGroup) { agents.DELETE("/:id", handleDeleteAgent) agents.GET("/:id/cards", handleGetAgentCards) agents.PUT("/:id/cards", handleUpdateAgentCards) + agents.POST("/batch/status", handleBatchUpdateAgentStatus) + agents.DELETE("/batch", handleBatchDeleteAgents) } } @@ -583,3 +585,63 @@ func handleUpdateAgentCards(c *gin.Context) { "message": "更新成功", }) } + +func handleBatchUpdateAgentStatus(c *gin.Context) { + var req struct { + AgentIDs []uint `json:"agent_ids" binding:"required"` + Status string `json:"status" binding:"required,oneof=active inactive banned"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, 400, "参数错误") + return + } + + if len(req.AgentIDs) == 0 { + response.Error(c, 400, "请选择要操作的代理") + return + } + + if err := database.DB.Model(&model.User{}). + Where("id IN ? AND role = ?", req.AgentIDs, "agent"). + Update("status", req.Status).Error; err != nil { + response.Error(c, 500, "批量更新状态失败") + return + } + + service.LogOperation(c, "batch_update_status", "agent", nil, fmt.Sprintf("批量更新代理状态: %d个代理 -> %s", len(req.AgentIDs), req.Status), nil) + + response.Success(c, gin.H{ + "updated": len(req.AgentIDs), + }) +} + +func handleBatchDeleteAgents(c *gin.Context) { + var req struct { + AgentIDs []uint `json:"agent_ids" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, 400, "参数错误") + return + } + + if len(req.AgentIDs) == 0 { + response.Error(c, 400, "请选择要删除的代理") + return + } + + for _, agentID := range req.AgentIDs { + var user model.User + if err := database.DB.Where("id = ? AND role = ?", agentID, "agent").First(&user).Error; err != nil { + continue + } + + database.DB.Model(&model.User{}).Where("parent_agent_id = ?", user.ID).Update("parent_agent_id", nil) + database.DB.Delete(&user) + } + + service.LogOperation(c, "batch_delete", "agent", nil, fmt.Sprintf("批量删除代理: %d个", len(req.AgentIDs)), nil) + + response.Success(c, gin.H{ + "deleted": len(req.AgentIDs), + }) +} diff --git a/frontend/src/pages/admin/agents/components/data-table.vue b/frontend/src/pages/admin/agents/components/data-table.vue index c474893..3eb57fa 100644 --- a/frontend/src/pages/admin/agents/components/data-table.vue +++ b/frontend/src/pages/admin/agents/components/data-table.vue @@ -7,6 +7,7 @@ import { useI18n } from 'vue-i18n' import type { DataTableProps } from '@/components/data-table/types' import type { Agent } from '../data/schema' +import BulkActions from '@/components/data-table/bulk-actions.vue' import DataTable from '@/components/data-table/data-table.vue' import { SelectColumn } from '@/components/data-table/table-columns' import { generateVueTable } from '@/components/data-table/use-generate-vue-table' @@ -26,6 +27,9 @@ const emit = defineEmits<{ 'refresh': [] 'toggleView': [] 'update:searchFilter': [value: string] + 'batchUnban': [] + 'batchBan': [] + 'batchDelete': [] }>() const { t } = useI18n() @@ -68,6 +72,20 @@ defineExpose({ diff --git a/frontend/src/pages/admin/applications.vue b/frontend/src/pages/admin/applications.vue index 1c9afe1..6265840 100644 --- a/frontend/src/pages/admin/applications.vue +++ b/frontend/src/pages/admin/applications.vue @@ -151,7 +151,7 @@ onMounted(() => { {{ t('developer.applications.activeApps') }} - +
@@ -165,7 +165,7 @@ onMounted(() => { {{ t('developer.applications.totalUsers') }} - +
@@ -179,7 +179,7 @@ onMounted(() => { {{ t('developer.applications.totalVerifyCount') }} - +
diff --git a/frontend/src/pages/admin/logs/index.vue b/frontend/src/pages/admin/logs/index.vue index 3ecd4ca..536dfe9 100644 --- a/frontend/src/pages/admin/logs/index.vue +++ b/frontend/src/pages/admin/logs/index.vue @@ -153,7 +153,7 @@ onMounted(() => { -
+
{{ logs.filter(log => log.status === 'success').length }}
@@ -167,7 +167,7 @@ onMounted(() => { -
+
{{ logs.filter(log => log.status === 'failed').length }}
diff --git a/frontend/src/pages/admin/users/components/columns.ts b/frontend/src/pages/admin/users/components/columns.ts index 7b956b8..3ab5a08 100644 --- a/frontend/src/pages/admin/users/components/columns.ts +++ b/frontend/src/pages/admin/users/components/columns.ts @@ -76,7 +76,6 @@ export function getColumns(actions: { header: () => t('developer.users.columns.balance'), cell: ({ row }) => { const balance = row.getValue('balance') as number - const expiryAt = row.original.expiry_at const billingType = row.original.application?.billing_type if (balance === -1) { @@ -84,25 +83,9 @@ export function getColumns(actions: { } if (billingType === 'subscription') { - if (!expiryAt) - return h('span', { class: 'text-muted-foreground' }, '-') - try { - const date = new Date(expiryAt as string) - if (Number.isNaN(date.getTime())) - return '-' - const now = new Date() - if (date < now) { - return h(Badge, { variant: 'destructive' }, () => t('developer.users.expiry.expired')) - } - const daysLeft = Math.ceil((date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) - return h('div', { class: 'flex flex-col' }, [ - h('span', { class: 'text-green-600 font-medium' }, `${daysLeft} ${t('developer.users.expiry.daysLeft')}`), - h('span', { class: 'text-xs text-muted-foreground' }, date.toLocaleDateString('zh-CN')), - ]) - } - catch { + if (balance === null || balance === undefined) return '-' - } + return h('span', { class: balance > 0 ? 'text-green-600 font-medium' : 'text-muted-foreground' }, balance.toFixed(2)) } if (balance === null || balance === undefined) @@ -111,6 +94,42 @@ export function getColumns(actions: { return h('span', { class: balance > 0 ? 'text-green-600 font-medium' : 'text-muted-foreground' }, balance.toFixed(2)) }, }, + { + accessorKey: 'expiry_at', + header: () => t('developer.users.columns.expiryAt'), + cell: ({ row }) => { + const expiryAt = row.getValue('expiry_at') as string + const billingType = row.original.application?.billing_type + + if (billingType !== 'subscription') { + return h('span', { class: 'text-muted-foreground' }, '-') + } + + if (!expiryAt) { + return h('span', { class: 'text-muted-foreground' }, '-') + } + + try { + const date = new Date(expiryAt) + if (Number.isNaN(date.getTime())) + return '-' + + const now = new Date() + if (date < now) { + return h(Badge, { variant: 'destructive' }, () => t('developer.users.expiry.expired')) + } + + const daysLeft = Math.ceil((date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) + return h('div', { class: 'flex flex-col' }, [ + h('span', { class: 'text-green-600 font-medium' }, `${daysLeft} ${t('developer.users.expiry.daysLeft')}`), + h('span', { class: 'text-xs text-muted-foreground' }, date.toLocaleDateString('zh-CN')), + ]) + } + catch { + return '-' + } + }, + }, { accessorKey: 'last_login_at', header: () => t('developer.users.columns.lastLoginAt'), diff --git a/frontend/src/pages/admin/users/components/data-table.vue b/frontend/src/pages/admin/users/components/data-table.vue index f005c75..13241dd 100644 --- a/frontend/src/pages/admin/users/components/data-table.vue +++ b/frontend/src/pages/admin/users/components/data-table.vue @@ -53,6 +53,8 @@ const columnLabels: Record = { 'email': 'developer.users.columns.email', 'application.name': 'developer.users.columns.application', 'online_status': 'developer.users.columns.status', + 'balance': 'developer.users.columns.balance', + 'expiry_at': 'developer.users.columns.expiryAt', 'last_login_at': 'developer.users.columns.lastLoginAt', 'created_at': 'developer.users.columns.createdAt', } diff --git a/frontend/src/pages/developer/applications.vue b/frontend/src/pages/developer/applications.vue index 64ce6b1..9f2c6a6 100644 --- a/frontend/src/pages/developer/applications.vue +++ b/frontend/src/pages/developer/applications.vue @@ -174,7 +174,7 @@ onMounted(() => { {{ t('developer.applications.activeApps') }} - +
@@ -188,7 +188,7 @@ onMounted(() => { {{ t('developer.applications.totalUsers') }} - +
@@ -202,7 +202,7 @@ onMounted(() => { {{ t('developer.applications.totalVerifyCount') }} - +
diff --git a/frontend/src/plugins/i18n/zh.json b/frontend/src/plugins/i18n/zh.json index 4b091a8..e7590a9 100644 --- a/frontend/src/plugins/i18n/zh.json +++ b/frontend/src/plugins/i18n/zh.json @@ -468,6 +468,15 @@ "statusUpdateFailed": "状态更新失败", "deleteSuccess": "删除成功", "deleteFailed": "删除失败", + "batchUnbanBtn": "批量解封", + "batchBanBtn": "批量封禁", + "batchDeleteBtn": "批量删除", + "batchDeleteAgents": "批量删除代理", + "batchDeleteConfirm": "确定要删除选中的 {count} 个代理吗?此操作不可撤销。", + "batchDeleteSuccess": "批量删除成功", + "batchDeleteFailed": "批量删除失败", + "batchUpdateSuccess": "批量更新成功", + "batchUpdateFailed": "批量更新失败", "edit": { "title": "编辑代理", "description": "修改代理信息", @@ -793,6 +802,7 @@ "status": "状态", "deviceCount": "绑定设备", "balance": "余额", + "expiryAt": "到期时间", "lastLoginAt": "最后登录", "createdAt": "注册时间", "actions": "操作"