feat: 代理管理添加批量操作功能,用户管理添加到期时间列
- 代理管理页面添加批量解封、批量封禁、批量删除功能 - 后端添加代理批量操作API - 用户管理添加独立的到期时间列 - 修复应用管理和日志记录页面顶部卡片图标颜色
This commit is contained in:
@@ -23,6 +23,8 @@ func SetupAgentsRoutes(r *gin.RouterGroup) {
|
|||||||
agents.DELETE("/:id", handleDeleteAgent)
|
agents.DELETE("/:id", handleDeleteAgent)
|
||||||
agents.GET("/:id/cards", handleGetAgentCards)
|
agents.GET("/:id/cards", handleGetAgentCards)
|
||||||
agents.PUT("/:id/cards", handleUpdateAgentCards)
|
agents.PUT("/:id/cards", handleUpdateAgentCards)
|
||||||
|
agents.POST("/batch/status", handleBatchUpdateAgentStatus)
|
||||||
|
agents.DELETE("/batch", handleBatchDeleteAgents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,3 +585,63 @@ func handleUpdateAgentCards(c *gin.Context) {
|
|||||||
"message": "更新成功",
|
"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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import type { DataTableProps } from '@/components/data-table/types'
|
import type { DataTableProps } from '@/components/data-table/types'
|
||||||
import type { Agent } from '../data/schema'
|
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 DataTable from '@/components/data-table/data-table.vue'
|
||||||
import { SelectColumn } from '@/components/data-table/table-columns'
|
import { SelectColumn } from '@/components/data-table/table-columns'
|
||||||
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||||
@@ -26,6 +27,9 @@ const emit = defineEmits<{
|
|||||||
'refresh': []
|
'refresh': []
|
||||||
'toggleView': []
|
'toggleView': []
|
||||||
'update:searchFilter': [value: string]
|
'update:searchFilter': [value: string]
|
||||||
|
'batchUnban': []
|
||||||
|
'batchBan': []
|
||||||
|
'batchDelete': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -68,6 +72,20 @@ defineExpose({
|
|||||||
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
<BulkActions :table="table" entity-name="agents">
|
||||||
|
<UiButton variant="outline" size="sm" @click="emit('batchUnban')">
|
||||||
|
{{ t('developer.agents.batchUnbanBtn') }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="outline" size="sm" @click="emit('batchBan')">
|
||||||
|
{{ t('developer.agents.batchBanBtn') }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
|
||||||
|
{{ t('developer.agents.batchDeleteBtn') }}
|
||||||
|
</UiButton>
|
||||||
|
</BulkActions>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ const viewMode = ref<'tree' | 'list'>('tree')
|
|||||||
|
|
||||||
const deleteDialogOpen = ref(false)
|
const deleteDialogOpen = ref(false)
|
||||||
const deleteTarget = ref<Agent | null>(null)
|
const deleteTarget = ref<Agent | null>(null)
|
||||||
|
const batchDeleteDialogOpen = ref(false)
|
||||||
|
const batchDeleteIds = ref<number[]>([])
|
||||||
|
|
||||||
const filteredAgents = computed(() => {
|
const filteredAgents = computed(() => {
|
||||||
let result = agents.value
|
let result = agents.value
|
||||||
@@ -185,6 +187,35 @@ async function handleDelete() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmBatchDelete(ids: number[]) {
|
||||||
|
batchDeleteIds.value = ids
|
||||||
|
batchDeleteDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBatchDelete() {
|
||||||
|
try {
|
||||||
|
await api.delete('/dev/agents/batch', { agent_ids: batchDeleteIds.value } as any)
|
||||||
|
toast.success(t('developer.agents.batchDeleteSuccess'))
|
||||||
|
fetchAgents()
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('批量删除失败:', error)
|
||||||
|
toast.error(error.message || t('developer.agents.batchDeleteFailed'))
|
||||||
|
} finally {
|
||||||
|
batchDeleteIds.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchToggleStatus(ids: number[], status: string) {
|
||||||
|
try {
|
||||||
|
await api.post('/dev/agents/batch/status', { agent_ids: ids, status })
|
||||||
|
toast.success(t('developer.agents.batchUpdateSuccess'))
|
||||||
|
fetchAgents()
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('批量更新状态失败:', error)
|
||||||
|
toast.error(error.message || t('developer.agents.batchUpdateFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchAgents()
|
fetchAgents()
|
||||||
})
|
})
|
||||||
@@ -276,6 +307,9 @@ onMounted(() => {
|
|||||||
@refresh="fetchAgents"
|
@refresh="fetchAgents"
|
||||||
@toggle-view="toggleViewMode"
|
@toggle-view="toggleViewMode"
|
||||||
@update:search-filter="searchFilter = $event"
|
@update:search-filter="searchFilter = $event"
|
||||||
|
@batch-unban="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'banned').map((r: any) => r.original.id) || [], 'active')"
|
||||||
|
@batch-ban="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status !== 'banned').map((r: any) => r.original.id) || [], 'banned')"
|
||||||
|
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
|
||||||
>
|
>
|
||||||
<template #filters>
|
<template #filters>
|
||||||
<SingleFilter
|
<SingleFilter
|
||||||
@@ -303,5 +337,20 @@ onMounted(() => {
|
|||||||
{{ t('developer.agents.deleteAgentConfirm', { username: deleteTarget?.username }) }}
|
{{ t('developer.agents.deleteAgentConfirm', { username: deleteTarget?.username }) }}
|
||||||
</template>
|
</template>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
v-model:open="batchDeleteDialogOpen"
|
||||||
|
destructive
|
||||||
|
:confirm-button-text="t('common.delete')"
|
||||||
|
:cancel-button-text="t('common.cancel')"
|
||||||
|
@confirm="handleBatchDelete"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
{{ t('developer.agents.batchDeleteAgents') }}
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
{{ t('developer.agents.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||||
|
</template>
|
||||||
|
</ConfirmDialog>
|
||||||
</BasicPage>
|
</BasicPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ onMounted(() => {
|
|||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
{{ t('developer.applications.activeApps') }}
|
{{ t('developer.applications.activeApps') }}
|
||||||
</UiCardTitle>
|
</UiCardTitle>
|
||||||
<Icon icon="lucide:check-circle" class="size-4 text-green-500" />
|
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
<UiCardContent>
|
<UiCardContent>
|
||||||
<div class="text-2xl font-bold">
|
<div class="text-2xl font-bold">
|
||||||
@@ -165,7 +165,7 @@ onMounted(() => {
|
|||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
{{ t('developer.applications.totalUsers') }}
|
{{ t('developer.applications.totalUsers') }}
|
||||||
</UiCardTitle>
|
</UiCardTitle>
|
||||||
<Icon icon="lucide:users" class="size-4 text-blue-500" />
|
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
<UiCardContent>
|
<UiCardContent>
|
||||||
<div class="text-2xl font-bold">
|
<div class="text-2xl font-bold">
|
||||||
@@ -179,7 +179,7 @@ onMounted(() => {
|
|||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
{{ t('developer.applications.totalVerifyCount') }}
|
{{ t('developer.applications.totalVerifyCount') }}
|
||||||
</UiCardTitle>
|
</UiCardTitle>
|
||||||
<Icon icon="lucide:activity" class="size-4 text-purple-500" />
|
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
<UiCardContent>
|
<UiCardContent>
|
||||||
<div class="text-2xl font-bold">
|
<div class="text-2xl font-bold">
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ onMounted(() => {
|
|||||||
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
<UiCardContent>
|
<UiCardContent>
|
||||||
<div class="text-2xl font-bold text-green-600">
|
<div class="text-2xl font-bold">
|
||||||
{{ logs.filter(log => log.status === 'success').length }}
|
{{ logs.filter(log => log.status === 'success').length }}
|
||||||
</div>
|
</div>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
@@ -167,7 +167,7 @@ onMounted(() => {
|
|||||||
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
|
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
<UiCardContent>
|
<UiCardContent>
|
||||||
<div class="text-2xl font-bold text-red-600">
|
<div class="text-2xl font-bold">
|
||||||
{{ logs.filter(log => log.status === 'failed').length }}
|
{{ logs.filter(log => log.status === 'failed').length }}
|
||||||
</div>
|
</div>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ export function getColumns(actions: {
|
|||||||
header: () => t('developer.users.columns.balance'),
|
header: () => t('developer.users.columns.balance'),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const balance = row.getValue('balance') as number
|
const balance = row.getValue('balance') as number
|
||||||
const expiryAt = row.original.expiry_at
|
|
||||||
const billingType = row.original.application?.billing_type
|
const billingType = row.original.application?.billing_type
|
||||||
|
|
||||||
if (balance === -1) {
|
if (balance === -1) {
|
||||||
@@ -84,25 +83,9 @@ export function getColumns(actions: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (billingType === 'subscription') {
|
if (billingType === 'subscription') {
|
||||||
if (!expiryAt)
|
if (balance === null || balance === undefined)
|
||||||
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 {
|
|
||||||
return '-'
|
return '-'
|
||||||
}
|
return h('span', { class: balance > 0 ? 'text-green-600 font-medium' : 'text-muted-foreground' }, balance.toFixed(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (balance === null || balance === undefined)
|
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))
|
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',
|
accessorKey: 'last_login_at',
|
||||||
header: () => t('developer.users.columns.lastLoginAt'),
|
header: () => t('developer.users.columns.lastLoginAt'),
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ const columnLabels: Record<string, string> = {
|
|||||||
'email': 'developer.users.columns.email',
|
'email': 'developer.users.columns.email',
|
||||||
'application.name': 'developer.users.columns.application',
|
'application.name': 'developer.users.columns.application',
|
||||||
'online_status': 'developer.users.columns.status',
|
'online_status': 'developer.users.columns.status',
|
||||||
|
'balance': 'developer.users.columns.balance',
|
||||||
|
'expiry_at': 'developer.users.columns.expiryAt',
|
||||||
'last_login_at': 'developer.users.columns.lastLoginAt',
|
'last_login_at': 'developer.users.columns.lastLoginAt',
|
||||||
'created_at': 'developer.users.columns.createdAt',
|
'created_at': 'developer.users.columns.createdAt',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ onMounted(() => {
|
|||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
{{ t('developer.applications.activeApps') }}
|
{{ t('developer.applications.activeApps') }}
|
||||||
</UiCardTitle>
|
</UiCardTitle>
|
||||||
<Icon icon="lucide:check-circle" class="size-4 text-green-500" />
|
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
<UiCardContent>
|
<UiCardContent>
|
||||||
<div class="text-2xl font-bold">
|
<div class="text-2xl font-bold">
|
||||||
@@ -188,7 +188,7 @@ onMounted(() => {
|
|||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
{{ t('developer.applications.totalUsers') }}
|
{{ t('developer.applications.totalUsers') }}
|
||||||
</UiCardTitle>
|
</UiCardTitle>
|
||||||
<Icon icon="lucide:users" class="size-4 text-blue-500" />
|
<Icon icon="lucide:users" class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
<UiCardContent>
|
<UiCardContent>
|
||||||
<div class="text-2xl font-bold">
|
<div class="text-2xl font-bold">
|
||||||
@@ -202,7 +202,7 @@ onMounted(() => {
|
|||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
{{ t('developer.applications.totalVerifyCount') }}
|
{{ t('developer.applications.totalVerifyCount') }}
|
||||||
</UiCardTitle>
|
</UiCardTitle>
|
||||||
<Icon icon="lucide:activity" class="size-4 text-purple-500" />
|
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
<UiCardContent>
|
<UiCardContent>
|
||||||
<div class="text-2xl font-bold">
|
<div class="text-2xl font-bold">
|
||||||
|
|||||||
@@ -468,6 +468,15 @@
|
|||||||
"statusUpdateFailed": "状态更新失败",
|
"statusUpdateFailed": "状态更新失败",
|
||||||
"deleteSuccess": "删除成功",
|
"deleteSuccess": "删除成功",
|
||||||
"deleteFailed": "删除失败",
|
"deleteFailed": "删除失败",
|
||||||
|
"batchUnbanBtn": "批量解封",
|
||||||
|
"batchBanBtn": "批量封禁",
|
||||||
|
"batchDeleteBtn": "批量删除",
|
||||||
|
"batchDeleteAgents": "批量删除代理",
|
||||||
|
"batchDeleteConfirm": "确定要删除选中的 {count} 个代理吗?此操作不可撤销。",
|
||||||
|
"batchDeleteSuccess": "批量删除成功",
|
||||||
|
"batchDeleteFailed": "批量删除失败",
|
||||||
|
"batchUpdateSuccess": "批量更新成功",
|
||||||
|
"batchUpdateFailed": "批量更新失败",
|
||||||
"edit": {
|
"edit": {
|
||||||
"title": "编辑代理",
|
"title": "编辑代理",
|
||||||
"description": "修改代理信息",
|
"description": "修改代理信息",
|
||||||
@@ -793,6 +802,7 @@
|
|||||||
"status": "状态",
|
"status": "状态",
|
||||||
"deviceCount": "绑定设备",
|
"deviceCount": "绑定设备",
|
||||||
"balance": "余额",
|
"balance": "余额",
|
||||||
|
"expiryAt": "到期时间",
|
||||||
"lastLoginAt": "最后登录",
|
"lastLoginAt": "最后登录",
|
||||||
"createdAt": "注册时间",
|
"createdAt": "注册时间",
|
||||||
"actions": "操作"
|
"actions": "操作"
|
||||||
|
|||||||
Reference in New Issue
Block a user