feat: 重构授权管理为层级模型 - 管理员直接对代理授权应用卡密权限
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -17,6 +17,7 @@ func SetupAgentAppRoutes(r *gin.RouterGroup) {
|
|||||||
{
|
{
|
||||||
agentApps.GET("", handleGetAgentApps)
|
agentApps.GET("", handleGetAgentApps)
|
||||||
agentApps.GET("/requests", handleGetAgentRequests)
|
agentApps.GET("/requests", handleGetAgentRequests)
|
||||||
|
agentApps.POST("/authorize", handleDirectAuthorize)
|
||||||
agentApps.POST("/invite", handleInviteAgent)
|
agentApps.POST("/invite", handleInviteAgent)
|
||||||
agentApps.PUT("/requests/:id/approve", handleApproveRequest)
|
agentApps.PUT("/requests/:id/approve", handleApproveRequest)
|
||||||
agentApps.PUT("/requests/:id/reject", handleRejectRequest)
|
agentApps.PUT("/requests/:id/reject", handleRejectRequest)
|
||||||
@@ -66,61 +67,34 @@ func checkAgentPermission(userID uint) bool {
|
|||||||
|
|
||||||
func handleGetAgentApps(c *gin.Context) {
|
func handleGetAgentApps(c *gin.Context) {
|
||||||
userID := c.GetUint("user_id")
|
userID := c.GetUint("user_id")
|
||||||
log.Printf("[DEBUG] handleGetAgentApps called for Admin %d\n", userID)
|
|
||||||
|
|
||||||
var myAuthorizations []model.AgentApplication
|
var currentUser model.User
|
||||||
if err := database.DB.Where("admin_id = ?", userID).
|
if err := database.DB.First(¤tUser, userID).Error; err != nil {
|
||||||
Preload("CardTypes.CardType").
|
response.Error(c, 500, "获取用户信息失败")
|
||||||
Find(&myAuthorizations).Error; err != nil {
|
|
||||||
response.Error(c, 500, "获取授权列表失败")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var receivedAuthorizations []model.AgentApplication
|
var authorizations []model.AgentApplication
|
||||||
if err := database.DB.Where("agent_id = ?", userID).
|
if currentUser.Role == "admin" {
|
||||||
Preload("CardTypes.CardType").
|
if err := database.DB.Where("admin_id = ?", userID).
|
||||||
Find(&receivedAuthorizations).Error; err != nil {
|
Preload("CardTypes.CardType").
|
||||||
response.Error(c, 500, "获取授权列表失败")
|
Preload("Agent").
|
||||||
return
|
Preload("Application").
|
||||||
}
|
Order("created_at DESC").
|
||||||
|
Find(&authorizations).Error; err != nil {
|
||||||
log.Printf("[DEBUG] Found %d my authorizations and %d received authorizations for Admin %d\n",
|
response.Error(c, 500, "获取授权列表失败")
|
||||||
len(myAuthorizations), len(receivedAuthorizations), userID)
|
return
|
||||||
|
}
|
||||||
var allAgentApps []model.AgentApplication
|
|
||||||
allAgentApps = append(allAgentApps, myAuthorizations...)
|
|
||||||
allAgentApps = append(allAgentApps, receivedAuthorizations...)
|
|
||||||
|
|
||||||
var agentIDs []uint
|
|
||||||
var AdminIDs []uint
|
|
||||||
var applicationIDs []uint
|
|
||||||
|
|
||||||
for _, aa := range allAgentApps {
|
|
||||||
agentIDs = append(agentIDs, aa.AgentID)
|
|
||||||
AdminIDs = append(AdminIDs, aa.AdminID)
|
|
||||||
applicationIDs = append(applicationIDs, aa.ApplicationID)
|
|
||||||
}
|
|
||||||
|
|
||||||
var users []model.User
|
|
||||||
if err := database.DB.Where("id IN ?", append(agentIDs, AdminIDs...)).Find(&users).Error; err != nil {
|
|
||||||
log.Printf("[ERROR] Failed to query users: %v\n", err)
|
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[DEBUG] Found %d users\n", len(users))
|
if err := database.DB.Where("admin_id = ?", userID).
|
||||||
}
|
Preload("CardTypes.CardType").
|
||||||
|
Preload("Agent").
|
||||||
var applications []model.Application
|
Preload("Application").
|
||||||
if err := database.DB.Where("id IN ?", applicationIDs).Find(&applications).Error; err != nil {
|
Order("created_at DESC").
|
||||||
log.Printf("[ERROR] Failed to query applications: %v\n", err)
|
Find(&authorizations).Error; err != nil {
|
||||||
}
|
response.Error(c, 500, "获取授权列表失败")
|
||||||
|
return
|
||||||
userMap := make(map[uint]model.User)
|
}
|
||||||
for _, user := range users {
|
|
||||||
userMap[user.ID] = user
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationMap := make(map[uint]model.Application)
|
|
||||||
for _, app := range applications {
|
|
||||||
applicationMap[app.ID] = app
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CardTypeResponse struct {
|
type CardTypeResponse struct {
|
||||||
@@ -139,25 +113,27 @@ func handleGetAgentApps(c *gin.Context) {
|
|||||||
AppName string `json:"app_name"`
|
AppName string `json:"app_name"`
|
||||||
Discount float64 `json:"discount"`
|
Discount float64 `json:"discount"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
Balance float64 `json:"balance"`
|
||||||
CardTypes []CardTypeResponse `json:"card_types"`
|
CardTypes []CardTypeResponse `json:"card_types"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
IsReceived bool `json:"is_received"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []AgentAppResponse
|
var result []AgentAppResponse
|
||||||
for _, aa := range allAgentApps {
|
for _, aa := range authorizations {
|
||||||
agentName := ""
|
agentName := ""
|
||||||
agentEmail := ""
|
agentEmail := ""
|
||||||
if user, exists := userMap[aa.AgentID]; exists {
|
var agentBalance float64
|
||||||
agentName = user.Username
|
if aa.Agent.ID != 0 {
|
||||||
if user.Email != nil {
|
agentName = aa.Agent.Username
|
||||||
agentEmail = *user.Email
|
if aa.Agent.Email != nil {
|
||||||
|
agentEmail = *aa.Agent.Email
|
||||||
}
|
}
|
||||||
|
agentBalance = aa.Agent.Balance
|
||||||
}
|
}
|
||||||
|
|
||||||
appName := ""
|
appName := ""
|
||||||
if app, exists := applicationMap[aa.ApplicationID]; exists {
|
if aa.Application.ID != 0 {
|
||||||
appName = app.Name
|
appName = aa.Application.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
var cardTypes []CardTypeResponse
|
var cardTypes []CardTypeResponse
|
||||||
@@ -170,9 +146,6 @@ func handleGetAgentApps(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] Processing AgentApp: ID=%d, AgentID=%d, ApplicationID=%d, CardTypes count=%d, IsReceived=%v\n",
|
|
||||||
aa.ID, aa.AgentID, aa.ApplicationID, len(cardTypes), aa.AgentID == userID)
|
|
||||||
|
|
||||||
result = append(result, AgentAppResponse{
|
result = append(result, AgentAppResponse{
|
||||||
ID: aa.ID,
|
ID: aa.ID,
|
||||||
AgentID: aa.AgentID,
|
AgentID: aa.AgentID,
|
||||||
@@ -182,13 +155,12 @@ func handleGetAgentApps(c *gin.Context) {
|
|||||||
AppName: appName,
|
AppName: appName,
|
||||||
Discount: aa.Discount,
|
Discount: aa.Discount,
|
||||||
Status: aa.Status,
|
Status: aa.Status,
|
||||||
|
Balance: agentBalance,
|
||||||
CardTypes: cardTypes,
|
CardTypes: cardTypes,
|
||||||
CreatedAt: aa.CreatedAt.Format("2006-01-02 15:04:05"),
|
CreatedAt: aa.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
IsReceived: aa.AgentID == userID,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] Returning %d agent apps for user %d\n", len(result), userID)
|
|
||||||
response.Success(c, gin.H{
|
response.Success(c, gin.H{
|
||||||
"agent_apps": result,
|
"agent_apps": result,
|
||||||
"total": len(result),
|
"total": len(result),
|
||||||
@@ -765,3 +737,125 @@ func handleCancelRequest(c *gin.Context) {
|
|||||||
"id": req.ID,
|
"id": req.ID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleDirectAuthorize(c *gin.Context) {
|
||||||
|
userID := c.GetUint("user_id")
|
||||||
|
|
||||||
|
var reqBody struct {
|
||||||
|
AgentID uint `json:"agent_id" binding:"required"`
|
||||||
|
ApplicationID uint `json:"application_id" binding:"required"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
CardTypes []struct {
|
||||||
|
CardTypeID uint `json:"card_type_id" binding:"required"`
|
||||||
|
CanGenerate bool `json:"can_generate"`
|
||||||
|
} `json:"card_types"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&reqBody); err != nil {
|
||||||
|
response.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqBody.Discount <= 0 || reqBody.Discount > 1 {
|
||||||
|
reqBody.Discount = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
var agent model.User
|
||||||
|
if err := database.DB.Where("id = ? AND role = ?", reqBody.AgentID, "agent").First(&agent).Error; err != nil {
|
||||||
|
response.Error(c, 404, "代理不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if agent.ParentAgentID != nil && *agent.ParentAgentID != userID {
|
||||||
|
var currentUser model.User
|
||||||
|
if err := database.DB.First(¤tUser, userID).Error; err != nil {
|
||||||
|
response.Error(c, 403, "无权授权该代理")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if currentUser.Role != "admin" {
|
||||||
|
response.Error(c, 403, "只能授权自己的下级代理")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var app model.Application
|
||||||
|
if err := database.DB.Where("id = ?", reqBody.ApplicationID).First(&app).Error; err != nil {
|
||||||
|
response.Error(c, 404, "应用不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.UserID != userID {
|
||||||
|
var currentUser model.User
|
||||||
|
if err := database.DB.First(¤tUser, userID).Error; err != nil {
|
||||||
|
response.Error(c, 403, "无权授权该应用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if currentUser.Role != "admin" {
|
||||||
|
response.Error(c, 403, "只能授权自己的应用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing model.AgentApplication
|
||||||
|
if err := database.DB.Where("agent_id = ? AND application_id = ?", reqBody.AgentID, reqBody.ApplicationID).First(&existing).Error; err == nil {
|
||||||
|
response.Error(c, 400, "该代理已获得此应用的授权")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := database.DB.Begin()
|
||||||
|
|
||||||
|
agentApp := model.AgentApplication{
|
||||||
|
AgentID: reqBody.AgentID,
|
||||||
|
ApplicationID: reqBody.ApplicationID,
|
||||||
|
AdminID: userID,
|
||||||
|
Discount: reqBody.Discount,
|
||||||
|
Status: "active",
|
||||||
|
IsReceived: true,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&agentApp).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
response.Error(c, 500, "创建授权失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(reqBody.CardTypes) > 0 {
|
||||||
|
for _, ct := range reqBody.CardTypes {
|
||||||
|
agentCardType := model.AgentApplicationCardType{
|
||||||
|
AgentApplicationID: agentApp.ID,
|
||||||
|
CardTypeID: ct.CardTypeID,
|
||||||
|
CanGenerate: ct.CanGenerate,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&agentCardType).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
response.Error(c, 500, "创建卡密权限失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var cardTypes []model.CardType
|
||||||
|
database.DB.Where("application_id = ?", reqBody.ApplicationID).Find(&cardTypes)
|
||||||
|
for _, ct := range cardTypes {
|
||||||
|
agentCardType := model.AgentApplicationCardType{
|
||||||
|
AgentApplicationID: agentApp.ID,
|
||||||
|
CardTypeID: ct.ID,
|
||||||
|
CanGenerate: false,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&agentCardType).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
response.Error(c, 500, "创建卡密权限失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"id": agentApp.ID,
|
||||||
|
"agent_id": agentApp.AgentID,
|
||||||
|
"agent_name": agent.Username,
|
||||||
|
"app_id": agentApp.ApplicationID,
|
||||||
|
"app_name": app.Name,
|
||||||
|
"discount": agentApp.Discount,
|
||||||
|
"status": agentApp.Status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const navMain = [
|
|||||||
icon: Network,
|
icon: Network,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '代理授权',
|
title: '授权管理',
|
||||||
url: '/admin/agent-apps',
|
url: '/admin/agent-apps',
|
||||||
icon: Share2,
|
icon: Share2,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const breadcrumbs = computed(() => {
|
|||||||
announcements: '公告管理',
|
announcements: '公告管理',
|
||||||
versions: '版本管理',
|
versions: '版本管理',
|
||||||
'card-types': '卡类管理',
|
'card-types': '卡类管理',
|
||||||
'agent-apps': '代理授权',
|
'agent-apps': '授权管理',
|
||||||
agents: '代理管理',
|
agents: '代理管理',
|
||||||
finance: '财务管理',
|
finance: '财务管理',
|
||||||
logs: '日志记录',
|
logs: '日志记录',
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
import type { ColumnDef } from '@tanstack/vue-table'
|
|
||||||
|
|
||||||
import { h } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
import type { CombinedItem } from '@/pages/admin/agent-apps/data/schema'
|
|
||||||
|
|
||||||
import Badge from '@/components/ui/badge/Badge.vue'
|
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
|
|
||||||
export function getStatusVariant(status: string, hasAuthorization: boolean) {
|
|
||||||
if (hasAuthorization) {
|
|
||||||
return status === 'active' ? 'default' : 'secondary'
|
|
||||||
}
|
|
||||||
if (status === 'pending')
|
|
||||||
return 'outline'
|
|
||||||
if (status === 'approved')
|
|
||||||
return 'default'
|
|
||||||
return 'destructive'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStatusText(status: string, hasAuthorization: boolean) {
|
|
||||||
if (hasAuthorization) {
|
|
||||||
return status === 'active' ? '已授权' : '已禁用'
|
|
||||||
}
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
pending: '待处理',
|
|
||||||
approved: '已通过',
|
|
||||||
rejected: '已拒绝',
|
|
||||||
}
|
|
||||||
return map[status] || status
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getItemTypeText(itemType: string) {
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
authorization: '授权记录',
|
|
||||||
request: '收到的申请',
|
|
||||||
my_request: '发出的申请',
|
|
||||||
}
|
|
||||||
return map[itemType] || itemType
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getColumns(actions: {
|
|
||||||
onApprove: (row: CombinedItem) => void
|
|
||||||
onReject: (row: CombinedItem) => void
|
|
||||||
onRemove: (row: CombinedItem) => void
|
|
||||||
}): ColumnDef<CombinedItem>[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
accessorKey: 'itemType',
|
|
||||||
header: '类型',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const itemType = row.getValue('itemType') as string
|
|
||||||
const variants: Record<string, 'default' | 'secondary' | 'outline' | 'destructive'> = {
|
|
||||||
authorization: 'default',
|
|
||||||
request: 'outline',
|
|
||||||
my_request: 'secondary',
|
|
||||||
}
|
|
||||||
return h(Badge, { variant: variants[itemType] || 'default' }, () => getItemTypeText(itemType))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'agent_name',
|
|
||||||
header: '代理商/管理员',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const agentName = row.getValue('agent_name') as string
|
|
||||||
const itemType = row.original.itemType
|
|
||||||
const label = itemType === 'my_request' ? '管理员' : '代理商'
|
|
||||||
return h('div', {}, [
|
|
||||||
h('div', { class: 'font-medium' }, agentName || '-'),
|
|
||||||
h('div', { class: 'text-xs text-muted-foreground' }, label),
|
|
||||||
])
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'app_name',
|
|
||||||
header: '应用',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const appName = row.getValue('app_name') as string
|
|
||||||
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'status',
|
|
||||||
header: '状态',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.getValue('status') as string
|
|
||||||
const hasAuthorization = row.original.hasAuthorization
|
|
||||||
const variant = getStatusVariant(status, hasAuthorization)
|
|
||||||
const text = getStatusText(status, hasAuthorization)
|
|
||||||
return h(Badge, { variant }, () => text)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'discount',
|
|
||||||
header: '折扣',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const hasAuthorization = row.original.hasAuthorization
|
|
||||||
if (!hasAuthorization)
|
|
||||||
return '-'
|
|
||||||
const discount = row.getValue('discount') as number
|
|
||||||
return `${(discount * 10).toFixed(1)}折`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'balance',
|
|
||||||
header: '余额',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const hasAuthorization = row.original.hasAuthorization
|
|
||||||
if (!hasAuthorization)
|
|
||||||
return '-'
|
|
||||||
const balance = row.getValue('balance') as number
|
|
||||||
return h('span', { class: 'font-medium' }, `¥${balance.toFixed(2)}`)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'created_at',
|
|
||||||
header: '创建时间',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const createdAt = row.getValue('created_at')
|
|
||||||
if (!createdAt)
|
|
||||||
return '-'
|
|
||||||
try {
|
|
||||||
const date = new Date(createdAt as string)
|
|
||||||
if (Number.isNaN(date.getTime()))
|
|
||||||
return '-'
|
|
||||||
return date.toLocaleString('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
header: () => h('span', { class: 'sr-only' }, '操作'),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const item = row.original
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
return h(
|
|
||||||
DropdownMenu,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
default: () => [
|
|
||||||
h(DropdownMenuTrigger, { asChild: true }, () =>
|
|
||||||
h(Button, { variant: 'ghost', class: 'h-8 w-8 p-0' }, () =>
|
|
||||||
h('span', { class: 'sr-only' }, '打开菜单'))),
|
|
||||||
h(
|
|
||||||
DropdownMenuContent,
|
|
||||||
{ align: 'end' },
|
|
||||||
() => {
|
|
||||||
const items = []
|
|
||||||
|
|
||||||
if (item.itemType === 'request' && item.status === 'pending') {
|
|
||||||
items.push(
|
|
||||||
h(DropdownMenuItem, { onClick: () => actions.onApprove(item) }, () => [
|
|
||||||
h('span', { class: 'i-lucide-check mr-2 h-4 w-4' }),
|
|
||||||
'批准',
|
|
||||||
]),
|
|
||||||
h(DropdownMenuItem, { onClick: () => actions.onReject(item) }, () => [
|
|
||||||
h('span', { class: 'i-lucide-x mr-2 h-4 w-4' }),
|
|
||||||
'拒绝',
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.hasAuthorization) {
|
|
||||||
items.push(
|
|
||||||
h(DropdownMenuItem, { onClick: () => router.push(`/admin/agent-apps/${item.authorizationId}/edit`) }, () => [
|
|
||||||
h('span', { class: 'i-lucide-edit mr-2 h-4 w-4' }),
|
|
||||||
'编辑',
|
|
||||||
]),
|
|
||||||
h(DropdownMenuItem, { onClick: () => router.push(`/admin/agent-apps/${item.authorizationId}/card-types`) }, () => [
|
|
||||||
h('span', { class: 'i-lucide-key mr-2 h-4 w-4' }),
|
|
||||||
'卡密权限',
|
|
||||||
]),
|
|
||||||
h(DropdownMenuItem, { onClick: () => router.push(`/admin/agent-apps/${item.authorizationId}/recharge`) }, () => [
|
|
||||||
h('span', { class: 'i-lucide-wallet mr-2 h-4 w-4' }),
|
|
||||||
'充值余额',
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.itemType === 'my_request' && item.status === 'pending') {
|
|
||||||
items.push(
|
|
||||||
h(DropdownMenuSeparator),
|
|
||||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onRemove(item) }, () => [
|
|
||||||
h('span', { class: 'i-lucide-trash-2 mr-2 h-4 w-4' }),
|
|
||||||
'撤回申请',
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else if (item.hasAuthorization) {
|
|
||||||
items.push(
|
|
||||||
h(DropdownMenuSeparator),
|
|
||||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onRemove(item) }, () => [
|
|
||||||
h('span', { class: 'i-lucide-trash-2 mr-2 h-4 w-4' }),
|
|
||||||
'移除授权',
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
enableSorting: false,
|
|
||||||
enableHiding: false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { Table } from '@tanstack/vue-table'
|
|
||||||
|
|
||||||
import { X } from 'lucide-vue-next'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
import type { CombinedItem } from '@/pages/admin/agent-apps/data/schema'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
|
|
||||||
interface DataTableToolbarProps {
|
|
||||||
table: Table<CombinedItem>
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<DataTableToolbarProps>()
|
|
||||||
|
|
||||||
const isFiltered = computed(() => props.table.getState().columnFilters.length > 0)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex items-center flex-1 space-x-2">
|
|
||||||
<Input
|
|
||||||
placeholder="搜索代理商/管理员..."
|
|
||||||
:model-value="(table.getColumn('agent_name')?.getFilterValue() as string) ?? ''"
|
|
||||||
class="h-8 w-[150px] lg:w-[200px]"
|
|
||||||
@input="table.getColumn('agent_name')?.setFilterValue($event.target.value)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
v-if="isFiltered"
|
|
||||||
variant="ghost"
|
|
||||||
class="h-8 px-2 lg:px-3"
|
|
||||||
@click="table.resetColumnFilters()"
|
|
||||||
>
|
|
||||||
重置
|
|
||||||
<X class="size-4 ml-2" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { ColumnDef } from '@tanstack/vue-table'
|
|
||||||
|
|
||||||
import type { DataTableProps } from '@/components/data-table/types'
|
|
||||||
import type { CombinedItem } from '@/pages/admin/agent-apps/data/schema'
|
|
||||||
|
|
||||||
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'
|
|
||||||
import DataTableViewOptions from '@/components/data-table/view-options.vue'
|
|
||||||
import { getColumns } from '@/pages/admin/agent-apps/components/columns'
|
|
||||||
import DataTableToolbar from '@/pages/admin/agent-apps/components/data-table-toolbar.vue'
|
|
||||||
|
|
||||||
const props = defineProps<Omit<DataTableProps<CombinedItem>, 'columns'> & {
|
|
||||||
onApprove: (row: CombinedItem) => void
|
|
||||||
onReject: (row: CombinedItem) => void
|
|
||||||
onRemove: (row: CombinedItem) => void
|
|
||||||
}>()
|
|
||||||
const emit = defineEmits<{
|
|
||||||
refresh: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const columns = computed(() => [
|
|
||||||
SelectColumn as ColumnDef<CombinedItem>,
|
|
||||||
...getColumns({
|
|
||||||
onApprove: props.onApprove,
|
|
||||||
onReject: props.onReject,
|
|
||||||
onRemove: props.onRemove,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
const table = generateVueTable<CombinedItem>({
|
|
||||||
get data() { return props.data },
|
|
||||||
get loading() { return props.loading },
|
|
||||||
columns: columns.value,
|
|
||||||
}, columns.value)
|
|
||||||
|
|
||||||
const columnLabels: Record<string, string> = {
|
|
||||||
select: 'admin.agentApps.select',
|
|
||||||
itemType: 'admin.agentApps.columns.type',
|
|
||||||
agent_name: 'admin.agentApps.columns.agentName',
|
|
||||||
app_name: 'admin.agentApps.columns.appName',
|
|
||||||
status: 'admin.agentApps.columns.status',
|
|
||||||
created_at: 'admin.agentApps.columns.createdAt',
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
table,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
|
||||||
<template #toolbar>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="flex items-center justify-end">
|
|
||||||
<slot name="actions" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<DataTableToolbar :table />
|
|
||||||
<slot name="filters" />
|
|
||||||
</div>
|
|
||||||
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</DataTable>
|
|
||||||
</template>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
export const agentAppStatuses = [
|
|
||||||
{ label: '已授权', value: 'active' },
|
|
||||||
{ label: '已禁用', value: 'inactive' },
|
|
||||||
{ label: '待处理', value: 'pending' },
|
|
||||||
{ label: '已拒绝', value: 'rejected' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const itemTypes = [
|
|
||||||
{ label: '授权记录', value: 'authorization' },
|
|
||||||
{ label: '收到的申请', value: 'request' },
|
|
||||||
{ label: '发出的申请', value: 'my_request' },
|
|
||||||
]
|
|
||||||
@@ -1,54 +1,24 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
export const agentAppStatusSchema = z.enum(['active', 'inactive', 'pending', 'approved', 'rejected'])
|
export const agentAppStatusSchema = z.enum(['active', 'inactive'])
|
||||||
|
|
||||||
export const agentAppSchema = z.object({
|
export const agentAppSchema = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
agent_id: z.number(),
|
agent_id: z.number(),
|
||||||
agent_name: z.string(),
|
agent_name: z.string(),
|
||||||
|
agent_email: z.string().optional(),
|
||||||
application_id: z.number(),
|
application_id: z.number(),
|
||||||
app_name: z.string(),
|
app_name: z.string(),
|
||||||
status: agentAppStatusSchema,
|
status: agentAppStatusSchema,
|
||||||
discount: z.number(),
|
discount: z.number(),
|
||||||
balance: z.number(),
|
balance: z.number(),
|
||||||
card_types: z.array(z.object({
|
card_types: z.array(z.object({
|
||||||
|
id: z.number().optional(),
|
||||||
card_type_id: z.number(),
|
card_type_id: z.number(),
|
||||||
|
name: z.string().optional(),
|
||||||
can_generate: z.boolean(),
|
can_generate: z.boolean(),
|
||||||
})).optional(),
|
})).optional(),
|
||||||
created_at: z.string(),
|
created_at: z.string(),
|
||||||
updated_at: z.string().optional(),
|
|
||||||
is_received: z.boolean().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const agentRequestSchema = z.object({
|
|
||||||
id: z.number(),
|
|
||||||
admin_id: z.number(),
|
|
||||||
admin_name: z.string(),
|
|
||||||
agent_id: z.number(),
|
|
||||||
agent_name: z.string(),
|
|
||||||
application_id: z.number(),
|
|
||||||
app_name: z.string(),
|
|
||||||
status: z.enum(['pending', 'approved', 'rejected']),
|
|
||||||
message: z.string().optional(),
|
|
||||||
created_at: z.string(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export type AgentApp = z.infer<typeof agentAppSchema>
|
export type AgentApp = z.infer<typeof agentAppSchema>
|
||||||
export type AgentRequest = z.infer<typeof agentRequestSchema>
|
|
||||||
|
|
||||||
export interface CombinedItem {
|
|
||||||
id: number
|
|
||||||
agent_id: number
|
|
||||||
agent_name: string
|
|
||||||
application_id: number
|
|
||||||
app_name: string
|
|
||||||
status: string
|
|
||||||
discount: number
|
|
||||||
balance: number
|
|
||||||
card_types?: Array<{ card_type_id: number, can_generate: boolean }>
|
|
||||||
created_at: string
|
|
||||||
itemType: 'authorization' | 'request' | 'my_request'
|
|
||||||
hasAuthorization: boolean
|
|
||||||
authorizationId: number | null
|
|
||||||
is_received?: boolean
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Check, CheckCircle, Clock, Send, Share2, Trash2, UserPlus, Wallet, X } from 'lucide-vue-next'
|
import { CheckCircle, Key, Plus, Share2, Wallet, XCircle } from 'lucide-vue-next'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
|
|
||||||
import type { AgentApp, AgentRequest, CombinedItem } from '@/pages/admin/agent-apps/data/schema'
|
import type { AgentApp } from '@/pages/admin/agent-apps/data/schema'
|
||||||
|
|
||||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||||
import BulkActions from '@/components/data-table/bulk-actions.vue'
|
|
||||||
import { BasicPage } from '@/components/global-layout'
|
import { BasicPage } from '@/components/global-layout'
|
||||||
import DataTable from '@/pages/admin/agent-apps/components/data-table.vue'
|
|
||||||
import api from '@/services/api'
|
import api from '@/services/api'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -18,213 +16,174 @@ const router = useRouter()
|
|||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const agentApps = ref<AgentApp[]>([])
|
const agentApps = ref<AgentApp[]>([])
|
||||||
const requests = ref<AgentRequest[]>([])
|
|
||||||
const myRequests = ref<AgentRequest[]>([])
|
|
||||||
const tableRef = ref()
|
|
||||||
const showRejectDialog = ref(false)
|
|
||||||
const showRemoveDialog = ref(false)
|
const showRemoveDialog = ref(false)
|
||||||
const showBatchRejectDialog = ref(false)
|
const removingItem = ref<AgentApp | null>(null)
|
||||||
const showBatchRemoveDialog = ref(false)
|
const showAuthorizeDialog = ref(false)
|
||||||
const rejectingItem = ref<CombinedItem | null>(null)
|
const authorizing = ref(false)
|
||||||
const removingItem = ref<CombinedItem | null>(null)
|
|
||||||
const batchRejectIds = ref<(string | number)[]>([])
|
const agents = ref<Array<{ id: number, username: string, email?: string }>>([])
|
||||||
const batchRemoveIds = ref<(string | number)[]>([])
|
const applications = ref<Array<{ id: number, name: string }>>([])
|
||||||
|
const cardTypes = ref<Array<{ id: number, name: string, billing_type: string, price: number }>>([])
|
||||||
|
|
||||||
|
const authorizeForm = ref({
|
||||||
|
agent_id: '' as string | number,
|
||||||
|
application_id: '' as string | number,
|
||||||
|
discount: 1.0,
|
||||||
|
card_types: [] as Array<{ card_type_id: number, can_generate: boolean }>,
|
||||||
|
})
|
||||||
|
|
||||||
const activeCount = computed(() => agentApps.value.filter(app => app.status === 'active').length)
|
const activeCount = computed(() => agentApps.value.filter(app => app.status === 'active').length)
|
||||||
const pendingCount = computed(() => requests.value.filter(r => r.status === 'pending').length + myRequests.value.filter(r => r.status === 'pending').length)
|
const inactiveCount = computed(() => agentApps.value.filter(app => app.status === 'inactive').length)
|
||||||
const totalBalance = computed(() => agentApps.value.reduce((sum, app) => sum + app.balance, 0))
|
const totalBalance = computed(() => agentApps.value.reduce((sum, app) => sum + (app.balance || 0), 0))
|
||||||
|
|
||||||
const allItems = computed<CombinedItem[]>(() => {
|
const searchFilter = ref('')
|
||||||
const items = agentApps.value.map(item => ({
|
const statusFilter = ref('')
|
||||||
...item,
|
|
||||||
itemType: 'authorization' as const,
|
|
||||||
status: item.status,
|
|
||||||
created_at: item.created_at,
|
|
||||||
hasAuthorization: item.status === 'active' || item.status === 'inactive',
|
|
||||||
authorizationId: item.id,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const requestItems = requests.value
|
const filteredApps = computed(() => {
|
||||||
.filter(item => item.status === 'pending')
|
let result = agentApps.value
|
||||||
.map(item => ({
|
if (statusFilter.value) {
|
||||||
id: item.id,
|
result = result.filter(app => app.status === statusFilter.value)
|
||||||
agent_id: item.agent_id,
|
}
|
||||||
agent_name: item.agent_name,
|
if (searchFilter.value) {
|
||||||
application_id: item.application_id,
|
const search = searchFilter.value.toLowerCase()
|
||||||
app_name: item.app_name,
|
result = result.filter(app =>
|
||||||
status: item.status,
|
app.agent_name?.toLowerCase().includes(search)
|
||||||
discount: 1,
|
|| app.app_name?.toLowerCase().includes(search)
|
||||||
balance: 0,
|
|| app.agent_email?.toLowerCase().includes(search),
|
||||||
card_types: [],
|
)
|
||||||
created_at: item.created_at,
|
}
|
||||||
itemType: 'request' as const,
|
return result
|
||||||
hasAuthorization: false,
|
})
|
||||||
authorizationId: null,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const myRequestItems = myRequests.value
|
const selectedAppCardTypes = computed(() => {
|
||||||
.filter(item => item.status === 'pending')
|
if (!authorizeForm.value.application_id)
|
||||||
.map(item => ({
|
return []
|
||||||
id: item.id,
|
const appId = Number(authorizeForm.value.application_id)
|
||||||
agent_id: item.admin_id,
|
return cardTypes.value.filter(ct => ct.id && true)
|
||||||
agent_name: item.admin_name,
|
|
||||||
application_id: item.application_id,
|
|
||||||
app_name: item.app_name,
|
|
||||||
status: item.status,
|
|
||||||
discount: 1,
|
|
||||||
balance: 0,
|
|
||||||
card_types: [],
|
|
||||||
created_at: item.created_at,
|
|
||||||
itemType: 'my_request' as const,
|
|
||||||
hasAuthorization: false,
|
|
||||||
authorizationId: null,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return [...items, ...requestItems, ...myRequestItems].sort((a, b) => {
|
|
||||||
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [appsData, requestsData, myRequestsData] = await Promise.all([
|
const data = await api.get<{ agent_apps: AgentApp[] }>('/dev/agent-apps')
|
||||||
api.get<{ agent_apps: AgentApp[] }>('/dev/agent-apps'),
|
agentApps.value = data?.agent_apps || []
|
||||||
api.get<{ requests: AgentRequest[] }>('/dev/agent-apps/requests'),
|
|
||||||
api.get<{ requests: AgentRequest[] }>('/dev/agent-apps/my-requests'),
|
|
||||||
])
|
|
||||||
|
|
||||||
agentApps.value = appsData?.agent_apps || []
|
|
||||||
requests.value = requestsData?.requests || []
|
|
||||||
myRequests.value = myRequestsData?.requests || []
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('获取数据失败:', error)
|
console.error('获取数据失败:', error)
|
||||||
toast.error(t('common.fetchFailed'))
|
toast.error('获取授权列表失败')
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToRequest() {
|
async function fetchDialogData() {
|
||||||
router.push('/admin/agent-apps/request')
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToInvite() {
|
|
||||||
router.push('/admin/agent-apps/invite')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function approveRequest(item: CombinedItem) {
|
|
||||||
try {
|
try {
|
||||||
await api.post(`/dev/agent-apps/requests/${item.id}/approve`)
|
const [agentsData, appsData] = await Promise.all([
|
||||||
toast.success(t('admin.agentApps.approveSuccess'))
|
api.get<{ agents: Array<{ id: number, username: string, email?: string }> }>('/dev/agents'),
|
||||||
fetchData()
|
api.get<{ applications: Array<{ id: number, name: string }> }>('/dev/applications'),
|
||||||
|
])
|
||||||
|
agents.value = agentsData?.agents || []
|
||||||
|
applications.value = appsData?.applications || []
|
||||||
}
|
}
|
||||||
catch (error: any) {
|
catch (error) {
|
||||||
console.error('批准申请失败:', error)
|
console.error('获取数据失败:', error)
|
||||||
toast.error(error.message || t('admin.agentApps.approveFailed'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rejectRequest() {
|
async function onApplicationChange() {
|
||||||
if (!rejectingItem.value)
|
const appId = Number(authorizeForm.value.application_id)
|
||||||
|
if (!appId) {
|
||||||
|
cardTypes.value = []
|
||||||
|
authorizeForm.value.card_types = []
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.post(`/dev/agent-apps/requests/${rejectingItem.value.id}/reject`, { reason: '' })
|
const data = await api.get<{ card_types: Array<{ id: number, name: string, billing_type: string, price: number }> }>('/dev/card-types')
|
||||||
toast.success(t('admin.agentApps.rejectSuccess'))
|
const allTypes = data?.card_types || []
|
||||||
showRejectDialog.value = false
|
cardTypes.value = allTypes
|
||||||
fetchData()
|
authorizeForm.value.card_types = allTypes.map(ct => ({
|
||||||
|
card_type_id: ct.id,
|
||||||
|
can_generate: false,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
catch (error: any) {
|
catch (error) {
|
||||||
console.error('拒绝申请失败:', error)
|
console.error('获取卡类失败:', error)
|
||||||
toast.error(error.message || t('admin.agentApps.rejectFailed'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmReject(item: CombinedItem) {
|
function openAuthorizeDialog() {
|
||||||
rejectingItem.value = item
|
authorizeForm.value = {
|
||||||
showRejectDialog.value = true
|
agent_id: '',
|
||||||
|
application_id: '',
|
||||||
|
discount: 1.0,
|
||||||
|
card_types: [],
|
||||||
|
}
|
||||||
|
cardTypes.value = []
|
||||||
|
showAuthorizeDialog.value = true
|
||||||
|
fetchDialogData()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeItem() {
|
async function handleAuthorize() {
|
||||||
if (!removingItem.value)
|
if (!authorizeForm.value.agent_id) {
|
||||||
|
toast.error('请选择代理')
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
if (!authorizeForm.value.application_id) {
|
||||||
|
toast.error('请选择应用')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizing.value = true
|
||||||
try {
|
try {
|
||||||
const endpoint = removingItem.value.itemType === 'my_request'
|
await api.post('/dev/agent-apps/authorize', {
|
||||||
? `/dev/agent-apps/my-requests/${removingItem.value.id}`
|
agent_id: Number(authorizeForm.value.agent_id),
|
||||||
: `/dev/agent-apps/${removingItem.value.authorizationId}`
|
application_id: Number(authorizeForm.value.application_id),
|
||||||
|
discount: authorizeForm.value.discount,
|
||||||
await api.delete(endpoint)
|
card_types: authorizeForm.value.card_types,
|
||||||
toast.success(t('common.success'))
|
})
|
||||||
showRemoveDialog.value = false
|
toast.success('授权成功')
|
||||||
|
showAuthorizeDialog.value = false
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
catch (error: any) {
|
catch (error: any) {
|
||||||
console.error('操作失败:', error)
|
console.error('授权失败:', error)
|
||||||
toast.error(error.message || t('common.failed'))
|
toast.error(error.message || '授权失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
authorizing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmRemove(item: CombinedItem) {
|
function confirmRemove(item: AgentApp) {
|
||||||
removingItem.value = item
|
removingItem.value = item
|
||||||
showRemoveDialog.value = true
|
showRemoveDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function batchApprove(ids: (string | number)[]) {
|
async function handleRemove() {
|
||||||
try {
|
if (!removingItem.value)
|
||||||
await api.post('/dev/agent-apps/requests/batch-approve', { ids })
|
|
||||||
toast.success(t('admin.agentApps.batchApproveSuccess'))
|
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
catch (error: any) {
|
|
||||||
console.error('批量批准失败:', error)
|
|
||||||
toast.error(error.message || t('admin.agentApps.batchApproveFailed'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function batchReject() {
|
|
||||||
if (batchRejectIds.value.length === 0)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.post('/dev/agent-apps/requests/batch-reject', { ids: batchRejectIds.value, reason: '' })
|
await api.delete(`/dev/agent-apps/${removingItem.value.id}`)
|
||||||
toast.success(t('admin.agentApps.batchRejectSuccess'))
|
toast.success('移除授权成功')
|
||||||
showBatchRejectDialog.value = false
|
showRemoveDialog.value = false
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
catch (error: any) {
|
catch (error: any) {
|
||||||
console.error('批量拒绝失败:', error)
|
console.error('移除失败:', error)
|
||||||
toast.error(error.message || t('admin.agentApps.batchRejectFailed'))
|
toast.error(error.message || '移除失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmBatchReject(ids: (string | number)[]) {
|
function getBillingTypeText(type: string) {
|
||||||
batchRejectIds.value = ids
|
const map: Record<string, string> = {
|
||||||
showBatchRejectDialog.value = true
|
subscription: '订阅',
|
||||||
}
|
time: '计时',
|
||||||
|
point: '点卡',
|
||||||
async function batchRemove() {
|
|
||||||
if (batchRemoveIds.value.length === 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.delete('/dev/agent-apps/batch', { ids: batchRemoveIds.value })
|
|
||||||
toast.success(t('admin.agentApps.batchRemoveSuccess'))
|
|
||||||
showBatchRemoveDialog.value = false
|
|
||||||
fetchData()
|
|
||||||
}
|
}
|
||||||
catch (error: any) {
|
return map[type] || type
|
||||||
console.error('批量移除失败:', error)
|
|
||||||
toast.error(error.message || t('admin.agentApps.batchRemoveFailed'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmBatchRemove(ids: (string | number)[]) {
|
|
||||||
batchRemoveIds.value = ids
|
|
||||||
showBatchRemoveDialog.value = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -234,18 +193,14 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BasicPage
|
<BasicPage
|
||||||
:title="t('admin.agentApps.title')"
|
title="授权管理"
|
||||||
:description="t('admin.agentApps.description')"
|
description="管理代理的应用卡密权限授权"
|
||||||
sticky
|
sticky
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<UiButton variant="outline" @click="goToRequest">
|
<UiButton @click="openAuthorizeDialog">
|
||||||
<Send class="mr-2 h-4 w-4" />
|
<Plus class="mr-2 h-4 w-4" />
|
||||||
{{ t('admin.agentApps.requestAuth') }}
|
新建授权
|
||||||
</UiButton>
|
|
||||||
<UiButton @click="goToInvite">
|
|
||||||
<UserPlus class="mr-2 h-4 w-4" />
|
|
||||||
{{ t('admin.agentApps.inviteAuth') }}
|
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -254,7 +209,7 @@ onMounted(() => {
|
|||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
{{ t('admin.agentApps.totalAuth') }}
|
授权总数
|
||||||
</UiCardTitle>
|
</UiCardTitle>
|
||||||
<Share2 class="size-4 text-muted-foreground" />
|
<Share2 class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
@@ -268,12 +223,12 @@ onMounted(() => {
|
|||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
{{ t('admin.agentApps.activeAuth') }}
|
已启用
|
||||||
</UiCardTitle>
|
</UiCardTitle>
|
||||||
<CheckCircle class="size-4 text-muted-foreground" />
|
<CheckCircle class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
<UiCardContent>
|
<UiCardContent>
|
||||||
<div class="text-2xl font-bold">
|
<div class="text-2xl font-bold text-green-600">
|
||||||
{{ activeCount }}
|
{{ activeCount }}
|
||||||
</div>
|
</div>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
@@ -282,13 +237,13 @@ onMounted(() => {
|
|||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
{{ t('admin.agentApps.pendingRequests') }}
|
已禁用
|
||||||
</UiCardTitle>
|
</UiCardTitle>
|
||||||
<Clock class="size-4 text-muted-foreground" />
|
<XCircle class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
<UiCardContent>
|
<UiCardContent>
|
||||||
<div class="text-2xl font-bold">
|
<div class="text-2xl font-bold text-muted-foreground">
|
||||||
{{ pendingCount }}
|
{{ inactiveCount }}
|
||||||
</div>
|
</div>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
@@ -296,7 +251,7 @@ onMounted(() => {
|
|||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
{{ t('admin.agentApps.agentBalance') }}
|
代理余额总计
|
||||||
</UiCardTitle>
|
</UiCardTitle>
|
||||||
<Wallet class="size-4 text-muted-foreground" />
|
<Wallet class="size-4 text-muted-foreground" />
|
||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
@@ -310,80 +265,272 @@ onMounted(() => {
|
|||||||
|
|
||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardContent class="p-6">
|
<UiCardContent class="p-6">
|
||||||
<DataTable
|
<div class="flex flex-wrap items-center gap-2 mb-4">
|
||||||
ref="tableRef"
|
<UiInput
|
||||||
:loading
|
v-model="searchFilter"
|
||||||
:data="allItems"
|
placeholder="搜索代理名称、应用名称..."
|
||||||
:on-approve="approveRequest"
|
class="h-9 w-[200px]"
|
||||||
:on-reject="confirmReject"
|
/>
|
||||||
:on-remove="confirmRemove"
|
<UiSelect v-model="statusFilter">
|
||||||
@refresh="fetchData"
|
<UiSelectTrigger class="h-9 w-[120px]">
|
||||||
/>
|
<UiSelectValue placeholder="全部状态" />
|
||||||
|
</UiSelectTrigger>
|
||||||
|
<UiSelectContent>
|
||||||
|
<UiSelectItem value="">
|
||||||
|
全部状态
|
||||||
|
</UiSelectItem>
|
||||||
|
<UiSelectItem value="active">
|
||||||
|
已启用
|
||||||
|
</UiSelectItem>
|
||||||
|
<UiSelectItem value="inactive">
|
||||||
|
已禁用
|
||||||
|
</UiSelectItem>
|
||||||
|
</UiSelectContent>
|
||||||
|
</UiSelect>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||||
|
<div class="i-lucide-loader-2 h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="filteredApps.length === 0" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
|
<Share2 class="h-12 w-12 mb-4" />
|
||||||
|
<p>暂无授权记录</p>
|
||||||
|
<p class="text-sm mt-1">
|
||||||
|
点击"新建授权"为代理分配应用权限
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiTable v-else>
|
||||||
|
<UiTableHeader>
|
||||||
|
<UiTableRow>
|
||||||
|
<UiTableHead>代理</UiTableHead>
|
||||||
|
<UiTableHead>应用</UiTableHead>
|
||||||
|
<UiTableHead>卡密权限</UiTableHead>
|
||||||
|
<UiTableHead>折扣</UiTableHead>
|
||||||
|
<UiTableHead>余额</UiTableHead>
|
||||||
|
<UiTableHead>状态</UiTableHead>
|
||||||
|
<UiTableHead>授权时间</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
操作
|
||||||
|
</UiTableHead>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableHeader>
|
||||||
|
<UiTableBody>
|
||||||
|
<UiTableRow v-for="item in filteredApps" :key="item.id">
|
||||||
|
<UiTableCell>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">
|
||||||
|
{{ item.agent_name }}
|
||||||
|
</div>
|
||||||
|
<div v-if="item.agent_email" class="text-xs text-muted-foreground">
|
||||||
|
{{ item.agent_email }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>
|
||||||
|
<UiBadge variant="secondary">
|
||||||
|
{{ item.app_name }}
|
||||||
|
</UiBadge>
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>
|
||||||
|
<div v-if="item.card_types && item.card_types.length > 0" class="flex items-center gap-1">
|
||||||
|
<Key class="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ item.card_types.filter(ct => ct.can_generate).length }}/{{ item.card_types.length }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-muted-foreground text-sm">-</span>
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>
|
||||||
|
{{ (item.discount * 10).toFixed(1) }}折
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>
|
||||||
|
<span class="font-medium">¥{{ (item.balance || 0).toFixed(2) }}</span>
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>
|
||||||
|
<UiBadge :variant="item.status === 'active' ? 'default' : 'secondary'">
|
||||||
|
{{ item.status === 'active' ? '已启用' : '已禁用' }}
|
||||||
|
</UiBadge>
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-muted-foreground text-sm">
|
||||||
|
{{ item.created_at }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right">
|
||||||
|
<UiDropdownMenu>
|
||||||
|
<UiDropdownMenuTrigger as-child>
|
||||||
|
<UiButton variant="ghost" size="sm">
|
||||||
|
<span class="i-lucide-more-horizontal h-4 w-4" />
|
||||||
|
</UiButton>
|
||||||
|
</UiDropdownMenuTrigger>
|
||||||
|
<UiDropdownMenuContent align="end">
|
||||||
|
<UiDropdownMenuItem @click="router.push(`/admin/agent-apps/${item.id}/edit`)">
|
||||||
|
<span class="i-lucide-edit mr-2 h-4 w-4" />
|
||||||
|
编辑授权
|
||||||
|
</UiDropdownMenuItem>
|
||||||
|
<UiDropdownMenuItem @click="router.push(`/admin/agent-apps/${item.id}/card-types`)">
|
||||||
|
<span class="i-lucide-key mr-2 h-4 w-4" />
|
||||||
|
卡密权限
|
||||||
|
</UiDropdownMenuItem>
|
||||||
|
<UiDropdownMenuItem @click="router.push(`/admin/agent-apps/${item.id}/recharge`)">
|
||||||
|
<span class="i-lucide-wallet mr-2 h-4 w-4" />
|
||||||
|
充值余额
|
||||||
|
</UiDropdownMenuItem>
|
||||||
|
<UiDropdownMenuSeparator />
|
||||||
|
<UiDropdownMenuItem class="text-destructive" @click="confirmRemove(item)">
|
||||||
|
<span class="i-lucide-trash-2 mr-2 h-4 w-4" />
|
||||||
|
移除授权
|
||||||
|
</UiDropdownMenuItem>
|
||||||
|
</UiDropdownMenuContent>
|
||||||
|
</UiDropdownMenu>
|
||||||
|
</UiTableCell>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableBody>
|
||||||
|
</UiTable>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BulkActions
|
<UiDialog v-model:open="showAuthorizeDialog">
|
||||||
v-if="tableRef?.table"
|
<UiDialogContent class="max-w-2xl max-h-[85vh] overflow-y-auto">
|
||||||
:table="tableRef.table"
|
<UiDialogHeader>
|
||||||
:entity-name="t('admin.agentApps.auth')"
|
<UiDialogTitle>新建授权</UiDialogTitle>
|
||||||
>
|
<UiDialogDescription>
|
||||||
<UiButton
|
为代理分配应用卡密权限
|
||||||
variant="outline"
|
</UiDialogDescription>
|
||||||
size="sm"
|
</UiDialogHeader>
|
||||||
@click="batchApprove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
|
|
||||||
>
|
|
||||||
<Check class="mr-2 h-4 w-4" />
|
|
||||||
{{ t('admin.agentApps.batchApprove') }}
|
|
||||||
</UiButton>
|
|
||||||
<UiButton
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="confirmBatchReject(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
|
|
||||||
>
|
|
||||||
<X class="mr-2 h-4 w-4" />
|
|
||||||
{{ t('admin.agentApps.batchReject') }}
|
|
||||||
</UiButton>
|
|
||||||
<UiButton
|
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
@click="confirmBatchRemove(tableRef.table.getSelectedRowModel().rows.map((r: any) => r.original.id))"
|
|
||||||
>
|
|
||||||
<Trash2 class="mr-2 h-4 w-4" />
|
|
||||||
{{ t('admin.agentApps.batchRemove') }}
|
|
||||||
</UiButton>
|
|
||||||
</BulkActions>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
<div class="space-y-6 py-4">
|
||||||
v-model:open="showRejectDialog"
|
<div class="space-y-2">
|
||||||
:title="t('admin.agentApps.reject')"
|
<UiLabel>选择代理</UiLabel>
|
||||||
:description="t('admin.agentApps.rejectConfirm')"
|
<UiSelect v-model="authorizeForm.agent_id">
|
||||||
confirm-text="确定"
|
<UiSelectTrigger>
|
||||||
@confirm="rejectRequest"
|
<UiSelectValue placeholder="请选择代理" />
|
||||||
/>
|
</UiSelectTrigger>
|
||||||
|
<UiSelectContent>
|
||||||
|
<UiSelectItem
|
||||||
|
v-for="agent in agents"
|
||||||
|
:key="agent.id"
|
||||||
|
:value="String(agent.id)"
|
||||||
|
>
|
||||||
|
{{ agent.username }}
|
||||||
|
<span v-if="agent.email" class="text-muted-foreground ml-1">({{ agent.email }})</span>
|
||||||
|
</UiSelectItem>
|
||||||
|
</UiSelectContent>
|
||||||
|
</UiSelect>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel>选择应用</UiLabel>
|
||||||
|
<UiSelect v-model="authorizeForm.application_id" @update:model-value="onApplicationChange">
|
||||||
|
<UiSelectTrigger>
|
||||||
|
<UiSelectValue placeholder="请选择应用" />
|
||||||
|
</UiSelectTrigger>
|
||||||
|
<UiSelectContent>
|
||||||
|
<UiSelectItem
|
||||||
|
v-for="app in applications"
|
||||||
|
:key="app.id"
|
||||||
|
:value="String(app.id)"
|
||||||
|
>
|
||||||
|
{{ app.name }}
|
||||||
|
</UiSelectItem>
|
||||||
|
</UiSelectContent>
|
||||||
|
</UiSelect>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel>折扣</UiLabel>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<UiInput
|
||||||
|
v-model.number="authorizeForm.discount"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0.01"
|
||||||
|
max="1"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-muted-foreground w-20">
|
||||||
|
{{ (authorizeForm.discount * 10).toFixed(1) }}折
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
代理购买卡密的折扣,范围0.01-1
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="authorizeForm.card_types.length > 0" class="space-y-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<UiLabel>卡密权限</UiLabel>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UiButton variant="outline" size="sm" @click="authorizeForm.card_types.forEach(ct => ct.can_generate = true)">
|
||||||
|
全选
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="outline" size="sm" @click="authorizeForm.card_types.forEach(ct => ct.can_generate = false)">
|
||||||
|
清空
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="border rounded-lg">
|
||||||
|
<UiTable>
|
||||||
|
<UiTableHeader>
|
||||||
|
<UiTableRow>
|
||||||
|
<UiTableHead class="w-12" />
|
||||||
|
<UiTableHead>卡类名称</UiTableHead>
|
||||||
|
<UiTableHead>计费类型</UiTableHead>
|
||||||
|
<UiTableHead>价格</UiTableHead>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableHeader>
|
||||||
|
<UiTableBody>
|
||||||
|
<UiTableRow
|
||||||
|
v-for="(ct, index) in authorizeForm.card_types"
|
||||||
|
:key="ct.card_type_id"
|
||||||
|
:class="ct.can_generate ? 'bg-accent/50' : ''"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="ct.can_generate = !ct.can_generate"
|
||||||
|
>
|
||||||
|
<UiTableCell>
|
||||||
|
<UiCheckbox
|
||||||
|
:checked="ct.can_generate"
|
||||||
|
@update:checked="ct.can_generate = $event"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="font-medium">
|
||||||
|
{{ cardTypes[index]?.name || `卡类 #${ct.card_type_id}` }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>
|
||||||
|
<UiBadge variant="outline">
|
||||||
|
{{ getBillingTypeText(cardTypes[index]?.billing_type || '') }}
|
||||||
|
</UiBadge>
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>
|
||||||
|
¥{{ cardTypes[index]?.price || 0 }}
|
||||||
|
</UiTableCell>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableBody>
|
||||||
|
</UiTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiDialogFooter>
|
||||||
|
<UiButton variant="outline" @click="showAuthorizeDialog = false">
|
||||||
|
取消
|
||||||
|
</UiButton>
|
||||||
|
<UiButton :disabled="authorizing" @click="handleAuthorize">
|
||||||
|
<div v-if="authorizing" class="i-lucide-loader-2 mr-2 h-4 w-4 animate-spin" />
|
||||||
|
确认授权
|
||||||
|
</UiButton>
|
||||||
|
</UiDialogFooter>
|
||||||
|
</UiDialogContent>
|
||||||
|
</UiDialog>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
v-model:open="showRemoveDialog"
|
v-model:open="showRemoveDialog"
|
||||||
:title="t('admin.agentApps.remove')"
|
title="移除授权"
|
||||||
:description="removingItem?.itemType === 'my_request' ? t('admin.agentApps.withdrawConfirm') : t('admin.agentApps.removeConfirm')"
|
:description="`确定要移除 ${removingItem?.agent_name} 对 ${removingItem?.app_name} 的授权吗?`"
|
||||||
confirm-text="确定"
|
confirm-text="确定移除"
|
||||||
@confirm="removeItem"
|
@confirm="handleRemove"
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
v-model:open="showBatchRejectDialog"
|
|
||||||
:title="t('admin.agentApps.batchReject')"
|
|
||||||
:description="t('admin.agentApps.batchRejectConfirm', { count: batchRejectIds.length })"
|
|
||||||
confirm-text="确定"
|
|
||||||
@confirm="batchReject"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
v-model:open="showBatchRemoveDialog"
|
|
||||||
:title="t('admin.agentApps.batchRemove')"
|
|
||||||
:description="t('admin.agentApps.batchRemoveConfirm', { count: batchRemoveIds.length })"
|
|
||||||
confirm-text="确定"
|
|
||||||
@confirm="batchRemove"
|
|
||||||
/>
|
/>
|
||||||
</BasicPage>
|
</BasicPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ArrowLeft, Loader2 } from 'lucide-vue-next'
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
|
|
||||||
import { BasicPage } from '@/components/global-layout'
|
|
||||||
import { API_BASE } from '@/utils/config'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const saving = ref(false)
|
|
||||||
const applications = ref<Array<{ id: number, name: string }>>([])
|
|
||||||
|
|
||||||
const form = ref({
|
|
||||||
agent_id: '',
|
|
||||||
application_id: '',
|
|
||||||
message: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
async function fetchApplications() {
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
const response = await fetch(`${API_BASE}/dev/applications`, {
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
})
|
|
||||||
const data = await response.json()
|
|
||||||
if (data.code === 200) {
|
|
||||||
applications.value = data.applications || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('获取应用列表失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
if (!form.value.agent_id) {
|
|
||||||
toast.error('请输入代理商ID')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.value.application_id) {
|
|
||||||
toast.error('请选择应用')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
const response = await fetch(`${API_BASE}/dev/agent-apps/invite`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
agent_id: Number(form.value.agent_id),
|
|
||||||
application_id: Number(form.value.application_id),
|
|
||||||
message: form.value.message,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
if (data.code === 200) {
|
|
||||||
toast.success('邀请已发送')
|
|
||||||
router.push('/admin/agent-apps')
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
toast.error(data.message || '发送邀请失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('发送邀请失败:', error)
|
|
||||||
toast.error('发送邀请失败')
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
|
||||||
router.push('/admin/agent-apps')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchApplications()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BasicPage
|
|
||||||
title="邀请授权"
|
|
||||||
description="邀请代理商成为授权方"
|
|
||||||
>
|
|
||||||
<template #actions>
|
|
||||||
<UiButton variant="outline" @click="goBack">
|
|
||||||
<ArrowLeft class="mr-2 h-4 w-4" />
|
|
||||||
返回
|
|
||||||
</UiButton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="max-w-2xl">
|
|
||||||
<UiCard>
|
|
||||||
<UiCardHeader>
|
|
||||||
<UiCardTitle>邀请信息</UiCardTitle>
|
|
||||||
<UiCardDescription>
|
|
||||||
填写以下信息邀请代理商成为授权方
|
|
||||||
</UiCardDescription>
|
|
||||||
</UiCardHeader>
|
|
||||||
<UiCardContent class="space-y-6">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<UiLabel for="agent_id">
|
|
||||||
代理商ID
|
|
||||||
</UiLabel>
|
|
||||||
<UiInput
|
|
||||||
id="agent_id"
|
|
||||||
v-model="form.agent_id"
|
|
||||||
type="number"
|
|
||||||
placeholder="请输入代理商ID"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
请向代理商索取其ID
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<UiLabel for="application_id">
|
|
||||||
选择应用
|
|
||||||
</UiLabel>
|
|
||||||
<UiSelect v-model="form.application_id">
|
|
||||||
<UiSelectTrigger>
|
|
||||||
<UiSelectValue placeholder="请选择应用" />
|
|
||||||
</UiSelectTrigger>
|
|
||||||
<UiSelectContent>
|
|
||||||
<UiSelectItem
|
|
||||||
v-for="app in applications"
|
|
||||||
:key="app.id"
|
|
||||||
:value="String(app.id)"
|
|
||||||
>
|
|
||||||
{{ app.name }}
|
|
||||||
</UiSelectItem>
|
|
||||||
</UiSelectContent>
|
|
||||||
</UiSelect>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<UiLabel for="message">
|
|
||||||
邀请说明
|
|
||||||
</UiLabel>
|
|
||||||
<UiTextarea
|
|
||||||
id="message"
|
|
||||||
v-model="form.message"
|
|
||||||
placeholder="请输入邀请说明(可选)"
|
|
||||||
:rows="4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</UiCardContent>
|
|
||||||
<UiCardFooter class="flex justify-end gap-2">
|
|
||||||
<UiButton variant="outline" @click="goBack">
|
|
||||||
取消
|
|
||||||
</UiButton>
|
|
||||||
<UiButton :disabled="saving" @click="handleSubmit">
|
|
||||||
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
发送邀请
|
|
||||||
</UiButton>
|
|
||||||
</UiCardFooter>
|
|
||||||
</UiCard>
|
|
||||||
</div>
|
|
||||||
</BasicPage>
|
|
||||||
</template>
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ArrowLeft, Loader2 } from 'lucide-vue-next'
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
|
|
||||||
import { BasicPage } from '@/components/global-layout'
|
|
||||||
import { API_BASE } from '@/utils/config'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const saving = ref(false)
|
|
||||||
const applications = ref<Array<{ id: number, name: string }>>([])
|
|
||||||
|
|
||||||
const form = ref({
|
|
||||||
admin_id: '',
|
|
||||||
application_id: '',
|
|
||||||
message: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
async function fetchApplications() {
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
const response = await fetch(`${API_BASE}/dev/applications`, {
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
})
|
|
||||||
const data = await response.json()
|
|
||||||
if (data.code === 200) {
|
|
||||||
applications.value = data.applications || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('获取应用列表失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
if (!form.value.admin_id) {
|
|
||||||
toast.error('请输入管理员ID')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.value.application_id) {
|
|
||||||
toast.error('请选择应用')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
const response = await fetch(`${API_BASE}/dev/agent-apps/request`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
admin_id: Number(form.value.admin_id),
|
|
||||||
application_id: Number(form.value.application_id),
|
|
||||||
message: form.value.message,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
if (data.code === 200) {
|
|
||||||
toast.success('申请已提交')
|
|
||||||
router.push('/admin/agent-apps')
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
toast.error(data.message || '提交申请失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('提交申请失败:', error)
|
|
||||||
toast.error('提交申请失败')
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
|
||||||
router.push('/admin/agent-apps')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchApplications()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BasicPage
|
|
||||||
title="申请授权"
|
|
||||||
description="向管理员申请应用授权"
|
|
||||||
>
|
|
||||||
<template #actions>
|
|
||||||
<UiButton variant="outline" @click="goBack">
|
|
||||||
<ArrowLeft class="mr-2 h-4 w-4" />
|
|
||||||
返回
|
|
||||||
</UiButton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="max-w-2xl">
|
|
||||||
<UiCard>
|
|
||||||
<UiCardHeader>
|
|
||||||
<UiCardTitle>申请信息</UiCardTitle>
|
|
||||||
<UiCardDescription>
|
|
||||||
填写以下信息向管理员申请应用授权
|
|
||||||
</UiCardDescription>
|
|
||||||
</UiCardHeader>
|
|
||||||
<UiCardContent class="space-y-6">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<UiLabel for="admin_id">
|
|
||||||
管理员ID
|
|
||||||
</UiLabel>
|
|
||||||
<UiInput
|
|
||||||
id="admin_id"
|
|
||||||
v-model="form.admin_id"
|
|
||||||
type="number"
|
|
||||||
placeholder="请输入管理员ID"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
请向管理员索取其ID
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<UiLabel for="application_id">
|
|
||||||
选择应用
|
|
||||||
</UiLabel>
|
|
||||||
<UiSelect v-model="form.application_id">
|
|
||||||
<UiSelectTrigger>
|
|
||||||
<UiSelectValue placeholder="请选择应用" />
|
|
||||||
</UiSelectTrigger>
|
|
||||||
<UiSelectContent>
|
|
||||||
<UiSelectItem
|
|
||||||
v-for="app in applications"
|
|
||||||
:key="app.id"
|
|
||||||
:value="String(app.id)"
|
|
||||||
>
|
|
||||||
{{ app.name }}
|
|
||||||
</UiSelectItem>
|
|
||||||
</UiSelectContent>
|
|
||||||
</UiSelect>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<UiLabel for="message">
|
|
||||||
申请说明
|
|
||||||
</UiLabel>
|
|
||||||
<UiTextarea
|
|
||||||
id="message"
|
|
||||||
v-model="form.message"
|
|
||||||
placeholder="请输入申请说明(可选)"
|
|
||||||
:rows="4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</UiCardContent>
|
|
||||||
<UiCardFooter class="flex justify-end gap-2">
|
|
||||||
<UiButton variant="outline" @click="goBack">
|
|
||||||
取消
|
|
||||||
</UiButton>
|
|
||||||
<UiButton :disabled="saving" @click="handleSubmit">
|
|
||||||
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
提交申请
|
|
||||||
</UiButton>
|
|
||||||
</UiCardFooter>
|
|
||||||
</UiCard>
|
|
||||||
</div>
|
|
||||||
</BasicPage>
|
|
||||||
</template>
|
|
||||||
@@ -171,7 +171,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: 'agent-apps',
|
path: 'agent-apps',
|
||||||
name: 'AdminAgentApps',
|
name: 'AdminAgentApps',
|
||||||
component: () => import('@/pages/admin/agent-apps/index.vue'),
|
component: () => import('@/pages/admin/agent-apps/index.vue'),
|
||||||
meta: { title: '代理授权 - 管理后台' },
|
meta: { title: '授权管理 - 管理后台' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'agents',
|
path: 'agents',
|
||||||
@@ -191,18 +191,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/pages/admin/agents/[id].vue'),
|
component: () => import('@/pages/admin/agents/[id].vue'),
|
||||||
meta: { title: '编辑代理 - 管理后台' },
|
meta: { title: '编辑代理 - 管理后台' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'agent-apps/request',
|
|
||||||
name: 'AdminAgentAppRequest',
|
|
||||||
component: () => import('@/pages/admin/agent-apps/request.vue'),
|
|
||||||
meta: { title: '申请授权 - 管理后台' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'agent-apps/invite',
|
|
||||||
name: 'AdminAgentAppInvite',
|
|
||||||
component: () => import('@/pages/admin/agent-apps/invite.vue'),
|
|
||||||
meta: { title: '邀请授权 - 管理后台' },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'agent-apps/:id/edit',
|
path: 'agent-apps/:id/edit',
|
||||||
name: 'AdminAgentAppEdit',
|
name: 'AdminAgentAppEdit',
|
||||||
|
|||||||
Reference in New Issue
Block a user