feat: 重构授权管理为层级模型 - 管理员直接对代理授权应用卡密权限
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
package admin
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -17,6 +17,7 @@ func SetupAgentAppRoutes(r *gin.RouterGroup) {
|
||||
{
|
||||
agentApps.GET("", handleGetAgentApps)
|
||||
agentApps.GET("/requests", handleGetAgentRequests)
|
||||
agentApps.POST("/authorize", handleDirectAuthorize)
|
||||
agentApps.POST("/invite", handleInviteAgent)
|
||||
agentApps.PUT("/requests/:id/approve", handleApproveRequest)
|
||||
agentApps.PUT("/requests/:id/reject", handleRejectRequest)
|
||||
@@ -66,61 +67,34 @@ func checkAgentPermission(userID uint) bool {
|
||||
|
||||
func handleGetAgentApps(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
log.Printf("[DEBUG] handleGetAgentApps called for Admin %d\n", userID)
|
||||
|
||||
var myAuthorizations []model.AgentApplication
|
||||
if err := database.DB.Where("admin_id = ?", userID).
|
||||
Preload("CardTypes.CardType").
|
||||
Find(&myAuthorizations).Error; err != nil {
|
||||
response.Error(c, 500, "获取授权列表失败")
|
||||
var currentUser model.User
|
||||
if err := database.DB.First(¤tUser, userID).Error; err != nil {
|
||||
response.Error(c, 500, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
var receivedAuthorizations []model.AgentApplication
|
||||
if err := database.DB.Where("agent_id = ?", userID).
|
||||
Preload("CardTypes.CardType").
|
||||
Find(&receivedAuthorizations).Error; err != nil {
|
||||
response.Error(c, 500, "获取授权列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found %d my authorizations and %d received authorizations for Admin %d\n",
|
||||
len(myAuthorizations), len(receivedAuthorizations), userID)
|
||||
|
||||
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)
|
||||
var authorizations []model.AgentApplication
|
||||
if currentUser.Role == "admin" {
|
||||
if err := database.DB.Where("admin_id = ?", userID).
|
||||
Preload("CardTypes.CardType").
|
||||
Preload("Agent").
|
||||
Preload("Application").
|
||||
Order("created_at DESC").
|
||||
Find(&authorizations).Error; err != nil {
|
||||
response.Error(c, 500, "获取授权列表失败")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Printf("[DEBUG] Found %d users\n", len(users))
|
||||
}
|
||||
|
||||
var applications []model.Application
|
||||
if err := database.DB.Where("id IN ?", applicationIDs).Find(&applications).Error; err != nil {
|
||||
log.Printf("[ERROR] Failed to query applications: %v\n", err)
|
||||
}
|
||||
|
||||
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
|
||||
if err := database.DB.Where("admin_id = ?", userID).
|
||||
Preload("CardTypes.CardType").
|
||||
Preload("Agent").
|
||||
Preload("Application").
|
||||
Order("created_at DESC").
|
||||
Find(&authorizations).Error; err != nil {
|
||||
response.Error(c, 500, "获取授权列表失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type CardTypeResponse struct {
|
||||
@@ -139,25 +113,27 @@ func handleGetAgentApps(c *gin.Context) {
|
||||
AppName string `json:"app_name"`
|
||||
Discount float64 `json:"discount"`
|
||||
Status string `json:"status"`
|
||||
Balance float64 `json:"balance"`
|
||||
CardTypes []CardTypeResponse `json:"card_types"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
IsReceived bool `json:"is_received"`
|
||||
}
|
||||
|
||||
var result []AgentAppResponse
|
||||
for _, aa := range allAgentApps {
|
||||
for _, aa := range authorizations {
|
||||
agentName := ""
|
||||
agentEmail := ""
|
||||
if user, exists := userMap[aa.AgentID]; exists {
|
||||
agentName = user.Username
|
||||
if user.Email != nil {
|
||||
agentEmail = *user.Email
|
||||
var agentBalance float64
|
||||
if aa.Agent.ID != 0 {
|
||||
agentName = aa.Agent.Username
|
||||
if aa.Agent.Email != nil {
|
||||
agentEmail = *aa.Agent.Email
|
||||
}
|
||||
agentBalance = aa.Agent.Balance
|
||||
}
|
||||
|
||||
appName := ""
|
||||
if app, exists := applicationMap[aa.ApplicationID]; exists {
|
||||
appName = app.Name
|
||||
if aa.Application.ID != 0 {
|
||||
appName = aa.Application.Name
|
||||
}
|
||||
|
||||
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{
|
||||
ID: aa.ID,
|
||||
AgentID: aa.AgentID,
|
||||
@@ -182,13 +155,12 @@ func handleGetAgentApps(c *gin.Context) {
|
||||
AppName: appName,
|
||||
Discount: aa.Discount,
|
||||
Status: aa.Status,
|
||||
Balance: agentBalance,
|
||||
CardTypes: cardTypes,
|
||||
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{
|
||||
"agent_apps": result,
|
||||
"total": len(result),
|
||||
@@ -765,3 +737,125 @@ func handleCancelRequest(c *gin.Context) {
|
||||
"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,
|
||||
},
|
||||
{
|
||||
title: '代理授权',
|
||||
title: '授权管理',
|
||||
url: '/admin/agent-apps',
|
||||
icon: Share2,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ const breadcrumbs = computed(() => {
|
||||
announcements: '公告管理',
|
||||
versions: '版本管理',
|
||||
'card-types': '卡类管理',
|
||||
'agent-apps': '代理授权',
|
||||
'agent-apps': '授权管理',
|
||||
agents: '代理管理',
|
||||
finance: '财务管理',
|
||||
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'
|
||||
|
||||
export const agentAppStatusSchema = z.enum(['active', 'inactive', 'pending', 'approved', 'rejected'])
|
||||
export const agentAppStatusSchema = z.enum(['active', 'inactive'])
|
||||
|
||||
export const agentAppSchema = z.object({
|
||||
id: z.number(),
|
||||
agent_id: z.number(),
|
||||
agent_name: z.string(),
|
||||
agent_email: z.string().optional(),
|
||||
application_id: z.number(),
|
||||
app_name: z.string(),
|
||||
status: agentAppStatusSchema,
|
||||
discount: z.number(),
|
||||
balance: z.number(),
|
||||
card_types: z.array(z.object({
|
||||
id: z.number().optional(),
|
||||
card_type_id: z.number(),
|
||||
name: z.string().optional(),
|
||||
can_generate: z.boolean(),
|
||||
})).optional(),
|
||||
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 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">
|
||||
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 { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
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 BulkActions from '@/components/data-table/bulk-actions.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/admin/agent-apps/components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -18,213 +16,174 @@ const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const agentApps = ref<AgentApp[]>([])
|
||||
const requests = ref<AgentRequest[]>([])
|
||||
const myRequests = ref<AgentRequest[]>([])
|
||||
const tableRef = ref()
|
||||
const showRejectDialog = ref(false)
|
||||
const showRemoveDialog = ref(false)
|
||||
const showBatchRejectDialog = ref(false)
|
||||
const showBatchRemoveDialog = ref(false)
|
||||
const rejectingItem = ref<CombinedItem | null>(null)
|
||||
const removingItem = ref<CombinedItem | null>(null)
|
||||
const batchRejectIds = ref<(string | number)[]>([])
|
||||
const batchRemoveIds = ref<(string | number)[]>([])
|
||||
const removingItem = ref<AgentApp | null>(null)
|
||||
const showAuthorizeDialog = ref(false)
|
||||
const authorizing = ref(false)
|
||||
|
||||
const agents = ref<Array<{ id: number, username: string, email?: string }>>([])
|
||||
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 pendingCount = computed(() => requests.value.filter(r => r.status === 'pending').length + myRequests.value.filter(r => r.status === 'pending').length)
|
||||
const totalBalance = computed(() => agentApps.value.reduce((sum, app) => sum + app.balance, 0))
|
||||
const inactiveCount = computed(() => agentApps.value.filter(app => app.status === 'inactive').length)
|
||||
const totalBalance = computed(() => agentApps.value.reduce((sum, app) => sum + (app.balance || 0), 0))
|
||||
|
||||
const allItems = computed<CombinedItem[]>(() => {
|
||||
const items = agentApps.value.map(item => ({
|
||||
...item,
|
||||
itemType: 'authorization' as const,
|
||||
status: item.status,
|
||||
created_at: item.created_at,
|
||||
hasAuthorization: item.status === 'active' || item.status === 'inactive',
|
||||
authorizationId: item.id,
|
||||
}))
|
||||
const searchFilter = ref('')
|
||||
const statusFilter = ref('')
|
||||
|
||||
const requestItems = requests.value
|
||||
.filter(item => item.status === 'pending')
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
agent_id: item.agent_id,
|
||||
agent_name: item.agent_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: 'request' as const,
|
||||
hasAuthorization: false,
|
||||
authorizationId: null,
|
||||
}))
|
||||
const filteredApps = computed(() => {
|
||||
let result = agentApps.value
|
||||
if (statusFilter.value) {
|
||||
result = result.filter(app => app.status === statusFilter.value)
|
||||
}
|
||||
if (searchFilter.value) {
|
||||
const search = searchFilter.value.toLowerCase()
|
||||
result = result.filter(app =>
|
||||
app.agent_name?.toLowerCase().includes(search)
|
||||
|| app.app_name?.toLowerCase().includes(search)
|
||||
|| app.agent_email?.toLowerCase().includes(search),
|
||||
)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const myRequestItems = myRequests.value
|
||||
.filter(item => item.status === 'pending')
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
agent_id: item.admin_id,
|
||||
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()
|
||||
})
|
||||
const selectedAppCardTypes = computed(() => {
|
||||
if (!authorizeForm.value.application_id)
|
||||
return []
|
||||
const appId = Number(authorizeForm.value.application_id)
|
||||
return cardTypes.value.filter(ct => ct.id && true)
|
||||
})
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [appsData, requestsData, myRequestsData] = await Promise.all([
|
||||
api.get<{ agent_apps: AgentApp[] }>('/dev/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 || []
|
||||
const data = await api.get<{ agent_apps: AgentApp[] }>('/dev/agent-apps')
|
||||
agentApps.value = data?.agent_apps || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
toast.error(t('common.fetchFailed'))
|
||||
toast.error('获取授权列表失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToRequest() {
|
||||
router.push('/admin/agent-apps/request')
|
||||
}
|
||||
|
||||
function goToInvite() {
|
||||
router.push('/admin/agent-apps/invite')
|
||||
}
|
||||
|
||||
async function approveRequest(item: CombinedItem) {
|
||||
async function fetchDialogData() {
|
||||
try {
|
||||
await api.post(`/dev/agent-apps/requests/${item.id}/approve`)
|
||||
toast.success(t('admin.agentApps.approveSuccess'))
|
||||
fetchData()
|
||||
const [agentsData, appsData] = await Promise.all([
|
||||
api.get<{ agents: Array<{ id: number, username: string, email?: string }> }>('/dev/agents'),
|
||||
api.get<{ applications: Array<{ id: number, name: string }> }>('/dev/applications'),
|
||||
])
|
||||
agents.value = agentsData?.agents || []
|
||||
applications.value = appsData?.applications || []
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批准申请失败:', error)
|
||||
toast.error(error.message || t('admin.agentApps.approveFailed'))
|
||||
catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectRequest() {
|
||||
if (!rejectingItem.value)
|
||||
async function onApplicationChange() {
|
||||
const appId = Number(authorizeForm.value.application_id)
|
||||
if (!appId) {
|
||||
cardTypes.value = []
|
||||
authorizeForm.value.card_types = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await api.post(`/dev/agent-apps/requests/${rejectingItem.value.id}/reject`, { reason: '' })
|
||||
toast.success(t('admin.agentApps.rejectSuccess'))
|
||||
showRejectDialog.value = false
|
||||
fetchData()
|
||||
const data = await api.get<{ card_types: Array<{ id: number, name: string, billing_type: string, price: number }> }>('/dev/card-types')
|
||||
const allTypes = data?.card_types || []
|
||||
cardTypes.value = allTypes
|
||||
authorizeForm.value.card_types = allTypes.map(ct => ({
|
||||
card_type_id: ct.id,
|
||||
can_generate: false,
|
||||
}))
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('拒绝申请失败:', error)
|
||||
toast.error(error.message || t('admin.agentApps.rejectFailed'))
|
||||
catch (error) {
|
||||
console.error('获取卡类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function confirmReject(item: CombinedItem) {
|
||||
rejectingItem.value = item
|
||||
showRejectDialog.value = true
|
||||
function openAuthorizeDialog() {
|
||||
authorizeForm.value = {
|
||||
agent_id: '',
|
||||
application_id: '',
|
||||
discount: 1.0,
|
||||
card_types: [],
|
||||
}
|
||||
cardTypes.value = []
|
||||
showAuthorizeDialog.value = true
|
||||
fetchDialogData()
|
||||
}
|
||||
|
||||
async function removeItem() {
|
||||
if (!removingItem.value)
|
||||
async function handleAuthorize() {
|
||||
if (!authorizeForm.value.agent_id) {
|
||||
toast.error('请选择代理')
|
||||
return
|
||||
}
|
||||
if (!authorizeForm.value.application_id) {
|
||||
toast.error('请选择应用')
|
||||
return
|
||||
}
|
||||
|
||||
authorizing.value = true
|
||||
try {
|
||||
const endpoint = removingItem.value.itemType === 'my_request'
|
||||
? `/dev/agent-apps/my-requests/${removingItem.value.id}`
|
||||
: `/dev/agent-apps/${removingItem.value.authorizationId}`
|
||||
|
||||
await api.delete(endpoint)
|
||||
toast.success(t('common.success'))
|
||||
showRemoveDialog.value = false
|
||||
await api.post('/dev/agent-apps/authorize', {
|
||||
agent_id: Number(authorizeForm.value.agent_id),
|
||||
application_id: Number(authorizeForm.value.application_id),
|
||||
discount: authorizeForm.value.discount,
|
||||
card_types: authorizeForm.value.card_types,
|
||||
})
|
||||
toast.success('授权成功')
|
||||
showAuthorizeDialog.value = false
|
||||
fetchData()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('操作失败:', error)
|
||||
toast.error(error.message || t('common.failed'))
|
||||
console.error('授权失败:', error)
|
||||
toast.error(error.message || '授权失败')
|
||||
}
|
||||
finally {
|
||||
authorizing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmRemove(item: CombinedItem) {
|
||||
function confirmRemove(item: AgentApp) {
|
||||
removingItem.value = item
|
||||
showRemoveDialog.value = true
|
||||
}
|
||||
|
||||
async function batchApprove(ids: (string | number)[]) {
|
||||
try {
|
||||
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)
|
||||
async function handleRemove() {
|
||||
if (!removingItem.value)
|
||||
return
|
||||
|
||||
try {
|
||||
await api.post('/dev/agent-apps/requests/batch-reject', { ids: batchRejectIds.value, reason: '' })
|
||||
toast.success(t('admin.agentApps.batchRejectSuccess'))
|
||||
showBatchRejectDialog.value = false
|
||||
await api.delete(`/dev/agent-apps/${removingItem.value.id}`)
|
||||
toast.success('移除授权成功')
|
||||
showRemoveDialog.value = false
|
||||
fetchData()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量拒绝失败:', error)
|
||||
toast.error(error.message || t('admin.agentApps.batchRejectFailed'))
|
||||
console.error('移除失败:', error)
|
||||
toast.error(error.message || '移除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function confirmBatchReject(ids: (string | number)[]) {
|
||||
batchRejectIds.value = ids
|
||||
showBatchRejectDialog.value = true
|
||||
}
|
||||
|
||||
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()
|
||||
function getBillingTypeText(type: string) {
|
||||
const map: Record<string, string> = {
|
||||
subscription: '订阅',
|
||||
time: '计时',
|
||||
point: '点卡',
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('批量移除失败:', error)
|
||||
toast.error(error.message || t('admin.agentApps.batchRemoveFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
function confirmBatchRemove(ids: (string | number)[]) {
|
||||
batchRemoveIds.value = ids
|
||||
showBatchRemoveDialog.value = true
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -234,18 +193,14 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('admin.agentApps.title')"
|
||||
:description="t('admin.agentApps.description')"
|
||||
title="授权管理"
|
||||
description="管理代理的应用卡密权限授权"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton variant="outline" @click="goToRequest">
|
||||
<Send 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 @click="openAuthorizeDialog">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
新建授权
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
@@ -254,7 +209,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.agentApps.totalAuth') }}
|
||||
授权总数
|
||||
</UiCardTitle>
|
||||
<Share2 class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -268,12 +223,12 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.agentApps.activeAuth') }}
|
||||
已启用
|
||||
</UiCardTitle>
|
||||
<CheckCircle class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
<div class="text-2xl font-bold text-green-600">
|
||||
{{ activeCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
@@ -282,13 +237,13 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.agentApps.pendingRequests') }}
|
||||
已禁用
|
||||
</UiCardTitle>
|
||||
<Clock class="size-4 text-muted-foreground" />
|
||||
<XCircle class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ pendingCount }}
|
||||
<div class="text-2xl font-bold text-muted-foreground">
|
||||
{{ inactiveCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
@@ -296,7 +251,7 @@ onMounted(() => {
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('admin.agentApps.agentBalance') }}
|
||||
代理余额总计
|
||||
</UiCardTitle>
|
||||
<Wallet class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
@@ -310,80 +265,272 @@ onMounted(() => {
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<DataTable
|
||||
ref="tableRef"
|
||||
:loading
|
||||
:data="allItems"
|
||||
:on-approve="approveRequest"
|
||||
:on-reject="confirmReject"
|
||||
:on-remove="confirmRemove"
|
||||
@refresh="fetchData"
|
||||
/>
|
||||
<div class="flex flex-wrap items-center gap-2 mb-4">
|
||||
<UiInput
|
||||
v-model="searchFilter"
|
||||
placeholder="搜索代理名称、应用名称..."
|
||||
class="h-9 w-[200px]"
|
||||
/>
|
||||
<UiSelect v-model="statusFilter">
|
||||
<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>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<BulkActions
|
||||
v-if="tableRef?.table"
|
||||
:table="tableRef.table"
|
||||
:entity-name="t('admin.agentApps.auth')"
|
||||
>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@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>
|
||||
<UiDialog v-model:open="showAuthorizeDialog">
|
||||
<UiDialogContent class="max-w-2xl max-h-[85vh] overflow-y-auto">
|
||||
<UiDialogHeader>
|
||||
<UiDialogTitle>新建授权</UiDialogTitle>
|
||||
<UiDialogDescription>
|
||||
为代理分配应用卡密权限
|
||||
</UiDialogDescription>
|
||||
</UiDialogHeader>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="showRejectDialog"
|
||||
:title="t('admin.agentApps.reject')"
|
||||
:description="t('admin.agentApps.rejectConfirm')"
|
||||
confirm-text="确定"
|
||||
@confirm="rejectRequest"
|
||||
/>
|
||||
<div class="space-y-6 py-4">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>选择代理</UiLabel>
|
||||
<UiSelect v-model="authorizeForm.agent_id">
|
||||
<UiSelectTrigger>
|
||||
<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
|
||||
v-model:open="showRemoveDialog"
|
||||
:title="t('admin.agentApps.remove')"
|
||||
:description="removingItem?.itemType === 'my_request' ? t('admin.agentApps.withdrawConfirm') : t('admin.agentApps.removeConfirm')"
|
||||
confirm-text="确定"
|
||||
@confirm="removeItem"
|
||||
/>
|
||||
|
||||
<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"
|
||||
title="移除授权"
|
||||
:description="`确定要移除 ${removingItem?.agent_name} 对 ${removingItem?.app_name} 的授权吗?`"
|
||||
confirm-text="确定移除"
|
||||
@confirm="handleRemove"
|
||||
/>
|
||||
</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({
|
||||
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',
|
||||
name: 'AdminAgentApps',
|
||||
component: () => import('@/pages/admin/agent-apps/index.vue'),
|
||||
meta: { title: '代理授权 - 管理后台' },
|
||||
meta: { title: '授权管理 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'agents',
|
||||
@@ -191,18 +191,6 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/pages/admin/agents/[id].vue'),
|
||||
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',
|
||||
name: 'AdminAgentAppEdit',
|
||||
|
||||
Reference in New Issue
Block a user