feat: 添加代理充值功能到代理管理页面, 移除授权管理页面的无效操作
This commit is contained in:
@@ -21,6 +21,7 @@ func SetupAgentsRoutes(r *gin.RouterGroup) {
|
||||
agents.GET("/:id", handleGetAgentDetail)
|
||||
agents.PUT("/:id", handleUpdateAgent)
|
||||
agents.PUT("/:id/status", handleUpdateAgentStatus)
|
||||
agents.POST("/:id/recharge", handleRechargeAgent)
|
||||
agents.DELETE("/:id", handleDeleteAgent)
|
||||
agents.GET("/:id/cards", handleGetAgentCards)
|
||||
agents.PUT("/:id/cards", handleUpdateAgentCards)
|
||||
@@ -77,6 +78,7 @@ func handleGetAgents(c *gin.Context) {
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastLoginAt string `json:"last_login_at"`
|
||||
Balance float64 `json:"balance"`
|
||||
TotalConsumption float64 `json:"total_consumption"`
|
||||
CanCreateAgent bool `json:"can_create_agent"`
|
||||
CardsCount int `json:"cards_count"`
|
||||
ChildAgentsCount int `json:"child_agents_count"`
|
||||
@@ -149,6 +151,7 @@ func handleGetAgents(c *gin.Context) {
|
||||
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
LastLoginAt: lastLoginAt,
|
||||
Balance: user.Balance,
|
||||
TotalConsumption: user.TotalConsumption,
|
||||
CanCreateAgent: user.CanCreateAgent,
|
||||
CardsCount: cardsCountMap[user.ID],
|
||||
ChildAgentsCount: childCountMap[user.ID],
|
||||
@@ -678,3 +681,41 @@ func handleBatchDeleteAgents(c *gin.Context) {
|
||||
"deleted": len(req.AgentIDs),
|
||||
})
|
||||
}
|
||||
|
||||
func handleRechargeAgent(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Where("id = ? AND role = ?", agentID, "agent").First(&user).Error; err != nil {
|
||||
response.Error(c, 404, "代理不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Amount float64 `json:"amount" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Amount <= 0 {
|
||||
response.Error(c, 400, "充值金额必须大于0")
|
||||
return
|
||||
}
|
||||
|
||||
newBalance := user.Balance + req.Amount
|
||||
if err := database.DB.Model(&user).Update("balance", newBalance).Error; err != nil {
|
||||
response.Error(c, 500, "充值失败")
|
||||
return
|
||||
}
|
||||
|
||||
service.LogOperation(c, "recharge", "agent", &user.ID, fmt.Sprintf("为代理充值: %.2f, 余额: %.2f -> %.2f", req.Amount, user.Balance, newBalance), nil)
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"balance": newBalance,
|
||||
"recharge": req.Amount,
|
||||
"agent_id": user.ID,
|
||||
"agent_name": user.Username,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckCircle, Edit, Key, MoreHorizontal, Plus, Share2, Trash2, Wallet, XCircle } from 'lucide-vue-next'
|
||||
import { CheckCircle, Key, MoreHorizontal, Plus, Share2, Trash2, XCircle } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
@@ -256,19 +256,6 @@ onMounted(() => {
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="end">
|
||||
<UiDropdownMenuItem @click="router.push(`/admin/agent-apps/${item.id}/edit`)">
|
||||
<Edit class="mr-2 h-4 w-4" />
|
||||
编辑授权
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="router.push(`/admin/agent-apps/${item.id}/card-types`)">
|
||||
<Key class="mr-2 h-4 w-4" />
|
||||
卡密权限
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="router.push(`/admin/agent-apps/${item.id}/recharge`)">
|
||||
<Wallet class="mr-2 h-4 w-4" />
|
||||
充值余额
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem class="text-destructive" @click="confirmRemove(item)">
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
移除授权
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ColumnDef, Row } from '@tanstack/vue-table'
|
||||
import type { Composer } from 'vue-i18n'
|
||||
|
||||
import { Ban, Check, MoreHorizontal, Pencil, Trash2, X } from 'lucide-vue-next'
|
||||
import { Ban, Check, MoreHorizontal, Pencil, Trash2, Wallet, X } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { Agent } from '../data/schema'
|
||||
@@ -27,6 +27,7 @@ export function getColumns(actions: {
|
||||
onToggleStatus: (row: Agent) => void
|
||||
onEdit: (row: Agent) => void
|
||||
onDelete: (row: Agent) => void
|
||||
onRecharge: (row: Agent) => void
|
||||
}, t: Composer['t']): ColumnDef<Agent>[] {
|
||||
return [
|
||||
{
|
||||
@@ -172,6 +173,10 @@ export function getColumns(actions: {
|
||||
h(Pencil, { class: 'mr-2 h-4 w-4' }),
|
||||
t('common.edit'),
|
||||
]),
|
||||
h(DropdownMenuItem, { onClick: () => actions.onRecharge(agent) }, () => [
|
||||
h(Wallet, { class: 'mr-2 h-4 w-4' }),
|
||||
t('admin.agents.recharge'),
|
||||
]),
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(agent) }, () => [
|
||||
h(Ban, { class: 'mr-2 h-4 w-4' }),
|
||||
isBanned ? t('admin.agents.enable') : t('admin.agents.disable'),
|
||||
|
||||
@@ -20,6 +20,7 @@ const props = defineProps<Omit<DataTableProps<Agent>, 'columns'> & {
|
||||
onToggleStatus: (row: Agent) => void
|
||||
onEdit: (row: Agent) => void
|
||||
onDelete: (row: Agent) => void
|
||||
onRecharge: (row: Agent) => void
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -39,6 +40,7 @@ const columns = computed(() => [
|
||||
onToggleStatus: props.onToggleStatus,
|
||||
onEdit: props.onEdit,
|
||||
onDelete: props.onDelete,
|
||||
onRecharge: props.onRecharge,
|
||||
}, t),
|
||||
])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Ban, CheckCircle, Clock, Plus, Users } from 'lucide-vue-next'
|
||||
import { Ban, CheckCircle, Clock, Plus, Users, Wallet } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -28,6 +28,11 @@ const deleteTarget = ref<Agent | null>(null)
|
||||
const batchDeleteDialogOpen = ref(false)
|
||||
const batchDeleteIds = ref<number[]>([])
|
||||
|
||||
const rechargeDialogOpen = ref(false)
|
||||
const rechargeTarget = ref<Agent | null>(null)
|
||||
const rechargeAmount = ref(0)
|
||||
const rechargeSaving = ref(false)
|
||||
|
||||
const filteredAgents = computed(() => {
|
||||
let result = agents.value
|
||||
|
||||
@@ -216,6 +221,38 @@ async function batchToggleStatus(ids: number[], status: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleRecharge(agent: Agent) {
|
||||
rechargeTarget.value = agent
|
||||
rechargeAmount.value = 0
|
||||
rechargeDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleRechargeSubmit() {
|
||||
if (!rechargeTarget.value || rechargeAmount.value <= 0) {
|
||||
toast.error('请输入有效的充值金额')
|
||||
return
|
||||
}
|
||||
|
||||
rechargeSaving.value = true
|
||||
try {
|
||||
await api.post(`/dev/agents/${rechargeTarget.value.id}/recharge`, { amount: rechargeAmount.value })
|
||||
toast.success('充值成功')
|
||||
rechargeDialogOpen.value = false
|
||||
fetchAgents()
|
||||
} catch (error: any) {
|
||||
console.error('充值失败:', error)
|
||||
toast.error(error.message || '充值失败')
|
||||
} finally {
|
||||
rechargeSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const quickAmounts = [100, 500, 1000, 5000]
|
||||
|
||||
function setRechargeAmount(amount: number) {
|
||||
rechargeAmount.value = amount
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchAgents()
|
||||
})
|
||||
@@ -304,6 +341,7 @@ onMounted(() => {
|
||||
:on-toggle-status="handleToggleStatus"
|
||||
:on-edit="handleEdit"
|
||||
:on-delete="confirmDelete"
|
||||
:on-recharge="handleRecharge"
|
||||
@refresh="fetchAgents"
|
||||
@toggle-view="toggleViewMode"
|
||||
@update:search-filter="searchFilter = $event"
|
||||
@@ -352,5 +390,69 @@ onMounted(() => {
|
||||
{{ t('admin.agents.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<UiDialog v-model:open="rechargeDialogOpen">
|
||||
<UiDialogContent class="sm:max-w-md">
|
||||
<UiDialogHeader>
|
||||
<UiDialogTitle>充值余额</UiDialogTitle>
|
||||
<UiDialogDescription>
|
||||
为代理 {{ rechargeTarget?.username }} 充值余额
|
||||
</UiDialogDescription>
|
||||
</UiDialogHeader>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>当前余额</UiLabel>
|
||||
<div class="text-2xl font-bold text-primary">
|
||||
¥{{ (rechargeTarget?.balance || 0).toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<UiLabel>充值金额</UiLabel>
|
||||
<UiInput
|
||||
v-model.number="rechargeAmount"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="请输入充值金额"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UiButton
|
||||
v-for="amount in quickAmounts"
|
||||
:key="amount"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="setRechargeAmount(amount)"
|
||||
>
|
||||
¥{{ amount }}
|
||||
</UiButton>
|
||||
</div>
|
||||
<div v-if="rechargeAmount > 0" class="p-4 bg-muted rounded-lg">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span>当前余额</span>
|
||||
<span>¥{{ (rechargeTarget?.balance || 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-1">
|
||||
<span>充值金额</span>
|
||||
<span class="text-green-600">+¥{{ rechargeAmount.toFixed(2) }}</span>
|
||||
</div>
|
||||
<UiSeparator class="my-2" />
|
||||
<div class="flex justify-between font-medium">
|
||||
<span>充值后余额</span>
|
||||
<span>¥{{ ((rechargeTarget?.balance || 0) + rechargeAmount).toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UiDialogFooter>
|
||||
<UiButton variant="outline" @click="rechargeDialogOpen = false">
|
||||
取消
|
||||
</UiButton>
|
||||
<UiButton :disabled="rechargeSaving || rechargeAmount <= 0" @click="handleRechargeSubmit">
|
||||
<span v-if="rechargeSaving" class="i-lucide-loader-2 mr-2 h-4 w-4 animate-spin" />
|
||||
确认充值
|
||||
</UiButton>
|
||||
</UiDialogFooter>
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -1205,6 +1205,7 @@
|
||||
"createdAt": "注册时间",
|
||||
"lastLoginAt": "最后登录"
|
||||
},
|
||||
"recharge": "充值",
|
||||
"form": {
|
||||
"username": "用户名",
|
||||
"usernamePlaceholder": "请输入用户名",
|
||||
|
||||
Reference in New Issue
Block a user