fix: 修复授权详情API返回格式,重构授权管理页面表格和批量操作
This commit is contained in:
@@ -568,9 +568,11 @@ func handleGetAgentAppDetail(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CardTypeResponse struct {
|
type CardTypeResponse struct {
|
||||||
ID uint `json:"id"`
|
CardTypeID uint `json:"card_type_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
BillingType string `json:"billing_type"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
|
OriginPrice float64 `json:"origin_price"`
|
||||||
CanGenerate bool `json:"can_generate"`
|
CanGenerate bool `json:"can_generate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,6 +585,7 @@ func handleGetAgentAppDetail(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"`
|
||||||
}
|
}
|
||||||
@@ -590,9 +593,11 @@ func handleGetAgentAppDetail(c *gin.Context) {
|
|||||||
var cardTypes []CardTypeResponse
|
var cardTypes []CardTypeResponse
|
||||||
for _, ct := range agentApp.CardTypes {
|
for _, ct := range agentApp.CardTypes {
|
||||||
cardTypes = append(cardTypes, CardTypeResponse{
|
cardTypes = append(cardTypes, CardTypeResponse{
|
||||||
ID: ct.CardTypeID,
|
CardTypeID: ct.CardTypeID,
|
||||||
Name: ct.CardType.Name,
|
Name: ct.CardType.Name,
|
||||||
Price: ct.CardType.Price,
|
BillingType: ct.CardType.RechargeType,
|
||||||
|
Price: ct.Price,
|
||||||
|
OriginPrice: ct.CardType.Price,
|
||||||
CanGenerate: ct.CanGenerate,
|
CanGenerate: ct.CanGenerate,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -605,6 +610,7 @@ func handleGetAgentAppDetail(c *gin.Context) {
|
|||||||
AppName: application.Name,
|
AppName: application.Name,
|
||||||
Discount: agentApp.Discount,
|
Discount: agentApp.Discount,
|
||||||
Status: agentApp.Status,
|
Status: agentApp.Status,
|
||||||
|
Balance: agent.Balance,
|
||||||
CardTypes: cardTypes,
|
CardTypes: cardTypes,
|
||||||
CreatedAt: agentApp.CreatedAt.Format("2006-01-02 15:04:05"),
|
CreatedAt: agentApp.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
@@ -613,7 +619,9 @@ func handleGetAgentAppDetail(c *gin.Context) {
|
|||||||
result.AgentEmail = *agent.Email
|
result.AgentEmail = *agent.Email
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, result)
|
response.Success(c, gin.H{
|
||||||
|
"agent_app": result,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdateAgentApp(c *gin.Context) {
|
func handleUpdateAgentApp(c *gin.Context) {
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import type { ColumnDef } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
import { Eye, MoreHorizontal, Pencil, Trash2 } from 'lucide-vue-next'
|
||||||
|
import { h } from 'vue'
|
||||||
|
|
||||||
|
import type { AgentApp } 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 getColumns(actions: {
|
||||||
|
onView: (row: AgentApp) => void
|
||||||
|
onEdit: (row: AgentApp) => void
|
||||||
|
onDelete: (row: AgentApp) => void
|
||||||
|
}): ColumnDef<AgentApp>[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: 'agent_name',
|
||||||
|
header: '代理',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const name = row.getValue('agent_name') as string
|
||||||
|
const email = row.original.agent_email
|
||||||
|
return h('div', { class: 'flex items-center space-x-3' }, [
|
||||||
|
h('div', { class: 'h-8 w-8 rounded bg-primary/10 flex items-center justify-center flex-shrink-0' }, [
|
||||||
|
h('span', { class: 'text-primary font-bold text-sm' }, name ? name.charAt(0).toUpperCase() : '?'),
|
||||||
|
]),
|
||||||
|
h('div', {}, [
|
||||||
|
h('p', { class: 'font-medium' }, name || '-'),
|
||||||
|
email ? h('p', { class: 'text-xs text-muted-foreground' }, email) : null,
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'app_name',
|
||||||
|
header: '应用',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const appName = row.getValue('app_name') as string
|
||||||
|
return appName ? h(Badge, { variant: 'secondary' }, () => appName) : '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'card_types',
|
||||||
|
header: '卡密权限',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const cardTypes = row.original.card_types || []
|
||||||
|
if (cardTypes.length === 0)
|
||||||
|
return h('span', { class: 'text-muted-foreground text-sm' }, '-')
|
||||||
|
const authorized = cardTypes.filter(ct => ct.can_generate).length
|
||||||
|
return h('span', { class: 'text-sm' }, `${authorized}/${cardTypes.length}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'balance',
|
||||||
|
header: '余额',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const balance = row.getValue('balance') as number
|
||||||
|
return h('span', { class: 'font-medium' }, `¥${(balance || 0).toFixed(2)}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: '状态',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.getValue('status') as string
|
||||||
|
const isActive = status === 'active'
|
||||||
|
return h(Badge, { variant: isActive ? 'default' : 'secondary' }, () => isActive ? '已启用' : '已禁用')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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
|
||||||
|
return h(
|
||||||
|
DropdownMenu,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
default: () => [
|
||||||
|
h(DropdownMenuTrigger, { asChild: true }, () =>
|
||||||
|
h(Button, { variant: 'ghost', class: 'h-8 w-8 p-0' }, () => [
|
||||||
|
h(MoreHorizontal, { class: 'h-4 w-4' }),
|
||||||
|
h('span', { class: 'sr-only' }, '打开菜单'),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
DropdownMenuContent,
|
||||||
|
{ align: 'end' },
|
||||||
|
() => [
|
||||||
|
h(DropdownMenuItem, { onClick: () => actions.onView(item) }, () => [
|
||||||
|
h(Eye, { class: 'mr-2 h-4 w-4' }),
|
||||||
|
'查看授权',
|
||||||
|
]),
|
||||||
|
h(DropdownMenuItem, { onClick: () => actions.onEdit(item) }, () => [
|
||||||
|
h(Pencil, { class: 'mr-2 h-4 w-4' }),
|
||||||
|
'编辑授权',
|
||||||
|
]),
|
||||||
|
h(DropdownMenuSeparator),
|
||||||
|
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(item) }, () => [
|
||||||
|
h(Trash2, { class: 'mr-2 h-4 w-4' }),
|
||||||
|
'移除授权',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ColumnDef } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import type { DataTableProps } from '@/components/data-table/types'
|
||||||
|
import type { AgentApp } from '@/pages/admin/agent-apps/data/schema'
|
||||||
|
|
||||||
|
import BulkActions from '@/components/data-table/bulk-actions.vue'
|
||||||
|
import DataTable from '@/components/data-table/data-table.vue'
|
||||||
|
import { SelectColumn } from '@/components/data-table/table-columns'
|
||||||
|
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||||
|
import DataTableViewOptions from '@/components/data-table/view-options.vue'
|
||||||
|
import { getColumns } from '@/pages/admin/agent-apps/components/columns'
|
||||||
|
|
||||||
|
const props = defineProps<Omit<DataTableProps<AgentApp>, 'columns'> & {
|
||||||
|
onView: (row: AgentApp) => void
|
||||||
|
onEdit: (row: AgentApp) => void
|
||||||
|
onDelete: (row: AgentApp) => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
refresh: []
|
||||||
|
batchDelete: [ids: (string | number)[]]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const columns = computed(() => [
|
||||||
|
SelectColumn as ColumnDef<AgentApp>,
|
||||||
|
...getColumns({
|
||||||
|
onView: props.onView,
|
||||||
|
onEdit: props.onEdit,
|
||||||
|
onDelete: props.onDelete,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
const table = generateVueTable<AgentApp>({
|
||||||
|
get data() { return props.data },
|
||||||
|
get loading() { return props.loading },
|
||||||
|
columns: columns.value,
|
||||||
|
}, columns.value)
|
||||||
|
|
||||||
|
const columnLabels: Record<string, string> = {
|
||||||
|
'select': '选择',
|
||||||
|
'agent_name': '代理',
|
||||||
|
'app_name': '应用',
|
||||||
|
'card_types': '卡密权限',
|
||||||
|
'balance': '余额',
|
||||||
|
'status': '状态',
|
||||||
|
'created_at': '授权时间',
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
<BulkActions :table="table" entity-name="agent-apps">
|
||||||
|
<UiButton variant="destructive" size="sm" @click="emit('batchDelete', table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])">
|
||||||
|
批量移除
|
||||||
|
</UiButton>
|
||||||
|
</BulkActions>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<slot name="filters" />
|
||||||
|
</div>
|
||||||
|
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</template>
|
||||||
@@ -1,45 +1,49 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CheckCircle, Eye, Key, MoreHorizontal, Pencil, Plus, Share2, Trash2, XCircle } from 'lucide-vue-next'
|
import { CheckCircle, Key, Plus, Share2, XCircle } from 'lucide-vue-next'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
|
|
||||||
import type { AgentApp } 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 SingleFilter from '@/components/data-table/single-filter.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 router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const agentApps = ref<AgentApp[]>([])
|
const agentApps = ref<AgentApp[]>([])
|
||||||
const showRemoveDialog = ref(false)
|
const tableRef = ref()
|
||||||
const removingItem = ref<AgentApp | null>(null)
|
|
||||||
|
|
||||||
const activeCount = computed(() => agentApps.value.filter(app => app.status === 'active').length)
|
const statusFilter = ref<string>('')
|
||||||
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 searchFilter = ref('')
|
const deleteDialogOpen = ref(false)
|
||||||
const statusFilter = ref('all')
|
const deleteTarget = ref<AgentApp | null>(null)
|
||||||
|
const batchDeleteDialogOpen = ref(false)
|
||||||
|
const batchDeleteIds = ref<(string | number)[]>([])
|
||||||
|
|
||||||
const filteredApps = computed(() => {
|
const filteredApps = computed(() => {
|
||||||
let result = agentApps.value
|
let result = agentApps.value
|
||||||
if (statusFilter.value && statusFilter.value !== 'all') {
|
|
||||||
|
if (statusFilter.value) {
|
||||||
result = result.filter(app => app.status === 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
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const activeCount = computed(() => filteredApps.value.filter(app => app.status === 'active').length)
|
||||||
|
const inactiveCount = computed(() => filteredApps.value.filter(app => app.status === 'inactive').length)
|
||||||
|
const totalBalance = computed(() => filteredApps.value.reduce((sum, app) => sum + (app.balance || 0), 0))
|
||||||
|
|
||||||
|
const statusOptions = computed(() => [
|
||||||
|
{ label: '已启用', value: 'active' },
|
||||||
|
{ label: '已禁用', value: 'inactive' },
|
||||||
|
])
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -67,30 +71,58 @@ function handleEdit(item: AgentApp) {
|
|||||||
router.push(`/admin/agent-apps/${item.id}/edit`)
|
router.push(`/admin/agent-apps/${item.id}/edit`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmRemove(item: AgentApp) {
|
function confirmDelete(item: AgentApp) {
|
||||||
removingItem.value = item
|
deleteTarget.value = item
|
||||||
showRemoveDialog.value = true
|
deleteDialogOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemove() {
|
async function handleDelete() {
|
||||||
if (!removingItem.value)
|
if (!deleteTarget.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.delete(`/dev/agent-apps/${removingItem.value.id}`)
|
await api.delete(`/dev/agent-apps/${deleteTarget.value.id}`)
|
||||||
toast.success('移除授权成功')
|
toast.success('移除授权成功')
|
||||||
showRemoveDialog.value = false
|
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
catch (error: any) {
|
catch (error: any) {
|
||||||
console.error('移除失败:', error)
|
console.error('移除失败:', error)
|
||||||
toast.error(error.message || '移除失败')
|
toast.error(error.message || '移除失败')
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
deleteTarget.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmBatchDelete(ids: (string | number)[]) {
|
||||||
|
batchDeleteIds.value = ids
|
||||||
|
batchDeleteDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBatchDelete() {
|
||||||
|
try {
|
||||||
|
for (const id of batchDeleteIds.value) {
|
||||||
|
await api.delete(`/dev/agent-apps/${id}`)
|
||||||
|
}
|
||||||
|
toast.success('批量移除成功')
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
console.error('批量移除失败:', error)
|
||||||
|
toast.error(error.message || '批量移除失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
batchDeleteIds.value = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -117,7 +149,7 @@ onMounted(() => {
|
|||||||
</UiCardHeader>
|
</UiCardHeader>
|
||||||
<UiCardContent>
|
<UiCardContent>
|
||||||
<div class="text-2xl font-bold">
|
<div class="text-2xl font-bold">
|
||||||
{{ agentApps.length }}
|
{{ filteredApps.length }}
|
||||||
</div>
|
</div>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
@@ -167,132 +199,56 @@ onMounted(() => {
|
|||||||
|
|
||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardContent class="p-6">
|
<UiCardContent class="p-6">
|
||||||
<div class="flex flex-wrap items-center gap-2 mb-4">
|
<DataTable
|
||||||
<UiInput
|
ref="tableRef"
|
||||||
v-model="searchFilter"
|
:loading
|
||||||
placeholder="搜索代理名称、应用名称..."
|
:data="filteredApps"
|
||||||
class="h-9 w-[200px]"
|
:on-view="handleView"
|
||||||
/>
|
:on-edit="handleEdit"
|
||||||
<UiSelect v-model="statusFilter">
|
:on-delete="confirmDelete"
|
||||||
<UiSelectTrigger class="h-9 w-[120px]">
|
@refresh="fetchData"
|
||||||
<UiSelectValue placeholder="全部状态" />
|
@batch-delete="confirmBatchDelete"
|
||||||
</UiSelectTrigger>
|
>
|
||||||
<UiSelectContent>
|
<template #filters>
|
||||||
<UiSelectItem value="all">
|
<SingleFilter
|
||||||
全部状态
|
v-model="statusFilter"
|
||||||
</UiSelectItem>
|
title="状态"
|
||||||
<UiSelectItem value="active">
|
:options="statusOptions"
|
||||||
已启用
|
/>
|
||||||
</UiSelectItem>
|
</template>
|
||||||
<UiSelectItem value="inactive">
|
</DataTable>
|
||||||
已禁用
|
|
||||||
</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 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>
|
|
||||||
<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">
|
|
||||||
<MoreHorizontal class="h-4 w-4" />
|
|
||||||
</UiButton>
|
|
||||||
</UiDropdownMenuTrigger>
|
|
||||||
<UiDropdownMenuContent align="end">
|
|
||||||
<UiDropdownMenuItem @click="handleView(item)">
|
|
||||||
<Eye class="mr-2 h-4 w-4" />
|
|
||||||
查看授权
|
|
||||||
</UiDropdownMenuItem>
|
|
||||||
<UiDropdownMenuItem @click="handleEdit(item)">
|
|
||||||
<Pencil class="mr-2 h-4 w-4" />
|
|
||||||
编辑授权
|
|
||||||
</UiDropdownMenuItem>
|
|
||||||
<UiDropdownMenuSeparator />
|
|
||||||
<UiDropdownMenuItem class="text-destructive" @click="confirmRemove(item)">
|
|
||||||
<Trash2 class="mr-2 h-4 w-4" />
|
|
||||||
移除授权
|
|
||||||
</UiDropdownMenuItem>
|
|
||||||
</UiDropdownMenuContent>
|
|
||||||
</UiDropdownMenu>
|
|
||||||
</UiTableCell>
|
|
||||||
</UiTableRow>
|
|
||||||
</UiTableBody>
|
|
||||||
</UiTable>
|
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
v-model:open="showRemoveDialog"
|
v-model:open="deleteDialogOpen"
|
||||||
title="移除授权"
|
destructive
|
||||||
:description="`确定要移除 ${removingItem?.agent_name} 对 ${removingItem?.app_name} 的授权吗?`"
|
confirm-button-text="确定移除"
|
||||||
confirm-text="确定移除"
|
cancel-button-text="取消"
|
||||||
@confirm="handleRemove"
|
@confirm="handleDelete"
|
||||||
/>
|
>
|
||||||
|
<template #title>
|
||||||
|
移除授权
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
确定要移除 {{ deleteTarget?.agent_name }} 对 {{ deleteTarget?.app_name }} 的授权吗?
|
||||||
|
</template>
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
v-model:open="batchDeleteDialogOpen"
|
||||||
|
destructive
|
||||||
|
confirm-button-text="确定移除"
|
||||||
|
cancel-button-text="取消"
|
||||||
|
@confirm="handleBatchDelete"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
批量移除授权
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
确定要移除选中的 {{ batchDeleteIds.length }} 项授权吗?
|
||||||
|
</template>
|
||||||
|
</ConfirmDialog>
|
||||||
</BasicPage>
|
</BasicPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user