feat: 创建独立的授权管理页面
- 在左侧导航添加授权管理链接(代理管理后) - 创建独立的授权管理页面,参考版本管理页面样式 - 添加统计卡片显示授权应用总数、活跃授权、授权卡类 - 创建添加/编辑授权页面 - 从代理编辑页面移除授权管理部分 - 添加路由配置
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { Boxes, Code, CreditCard, DollarSign, FileLock, Gauge, GitBranch, HardDrive, Hash, Key, Mail, Megaphone, MessageSquare, Monitor, Network, Plug, ScrollText, Settings, Shield, Smartphone, Users, Variable } from 'lucide-vue-next'
|
||||
import { Boxes, Code, CreditCard, DollarSign, FileLock, Gauge, GitBranch, HardDrive, Hash, Key, KeyRound, Mail, Megaphone, MessageSquare, Monitor, Network, Plug, ScrollText, Settings, Shield, Smartphone, Users, Variable } from 'lucide-vue-next'
|
||||
import { computed, onMounted, onUnmounted, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -137,6 +137,11 @@ const navMain = computed(() => [
|
||||
url: '/admin/agents',
|
||||
icon: Network,
|
||||
},
|
||||
{
|
||||
title: t('nav.agentApps'),
|
||||
url: '/admin/agent-apps',
|
||||
icon: KeyRound,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, CheckCircle, Loader2 } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import api from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Agent {
|
||||
id: number
|
||||
username: string
|
||||
}
|
||||
|
||||
interface Application {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
interface CardType {
|
||||
id: number
|
||||
name: string
|
||||
billing_type: string
|
||||
price: number
|
||||
}
|
||||
|
||||
const agentAppId = computed(() => route.params.id as string)
|
||||
const isEdit = computed(() => !!agentAppId.value)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
const agents = ref<Agent[]>([])
|
||||
const applications = ref<Application[]>([])
|
||||
const cardTypes = ref<CardType[]>([])
|
||||
|
||||
const form = ref({
|
||||
agent_id: '',
|
||||
application_id: '',
|
||||
card_type_prices: {} as Record<string, number>,
|
||||
card_type_auth: {} as Record<string, boolean>,
|
||||
})
|
||||
|
||||
async function fetchAgents() {
|
||||
try {
|
||||
const data = await api.get<{ agents?: Agent[] }>('/dev/agents')
|
||||
agents.value = data?.agents || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取代理列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
const data = await api.get<{ applications: Application[] }>('/dev/applications')
|
||||
applications.value = data?.applications || []
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function fetchAgentApp() {
|
||||
if (!agentAppId.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<{ agent_app?: any }>(`/dev/agent-apps/${agentAppId.value}`)
|
||||
const agentApp = data?.agent_app || data
|
||||
|
||||
if (agentApp) {
|
||||
form.value.agent_id = String(agentApp.agent_id)
|
||||
form.value.application_id = String(agentApp.application_id)
|
||||
|
||||
agentApp.card_types.forEach((ct: any) => {
|
||||
form.value.card_type_auth[String(ct.card_type_id)] = ct.can_generate
|
||||
})
|
||||
|
||||
await fetchCardTypes(String(agentApp.application_id))
|
||||
|
||||
cardTypes.value.forEach((ct) => {
|
||||
const existingCard = agentApp.card_types.find((c: any) => c.card_type_id === ct.id)
|
||||
if (existingCard) {
|
||||
form.value.card_type_prices[String(ct.id)] = existingCard.price || ct.price
|
||||
} else {
|
||||
form.value.card_type_prices[String(ct.id)] = ct.price
|
||||
form.value.card_type_auth[String(ct.id)] = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取授权信息失败:', error)
|
||||
toast.error('获取授权信息失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCardTypes(appId: string) {
|
||||
if (!appId) return
|
||||
try {
|
||||
const data = await api.get<{ card_types: CardType[] }>(`/dev/card-types?application_id=${appId}`)
|
||||
cardTypes.value = data?.card_types || []
|
||||
}
|
||||
catch {
|
||||
cardTypes.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function onAppChange(appId: string) {
|
||||
cardTypes.value = []
|
||||
form.value.card_type_prices = {}
|
||||
form.value.card_type_auth = {}
|
||||
if (appId) fetchCardTypes(appId)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.agent_id) {
|
||||
toast.error('请选择代理')
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.application_id) {
|
||||
toast.error('请选择应用')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const cardTypesPayload = cardTypes.value.map(ct => ({
|
||||
card_type_id: ct.id,
|
||||
can_generate: form.value.card_type_auth[String(ct.id)] || false,
|
||||
price: form.value.card_type_prices[String(ct.id)] !== undefined
|
||||
? Number(form.value.card_type_prices[String(ct.id)])
|
||||
: ct.price,
|
||||
}))
|
||||
|
||||
if (isEdit.value) {
|
||||
await api.put(`/dev/agent-apps/${agentAppId.value}`, {
|
||||
card_types: cardTypesPayload,
|
||||
discount: 0,
|
||||
})
|
||||
toast.success('授权更新成功')
|
||||
} else {
|
||||
await api.post('/dev/agent-apps', {
|
||||
agent_id: Number(form.value.agent_id),
|
||||
application_id: Number(form.value.application_id),
|
||||
card_types: cardTypesPayload,
|
||||
discount: 0,
|
||||
})
|
||||
toast.success('授权添加成功')
|
||||
}
|
||||
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
catch (error: any) {
|
||||
toast.error(error.message || (isEdit.value ? '更新授权失败' : '添加授权失败'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchAgents()
|
||||
fetchApplications()
|
||||
if (isEdit.value) {
|
||||
fetchAgentApp()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="isEdit ? '编辑授权' : '添加授权'"
|
||||
:description="isEdit ? '修改代理的应用授权配置' : '为代理添加新的应用授权'"
|
||||
:breadcrumbs="[
|
||||
{ title: t('nav.agentApps'), href: '/admin/agent-apps' },
|
||||
{ title: isEdit ? '编辑授权' : '添加授权' },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Loader2 class="size-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div v-else class="max-w-4xl mx-auto space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>基本信息</UiCardTitle>
|
||||
<UiCardDescription>选择代理和应用</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>选择代理</UiLabel>
|
||||
<UiSelect
|
||||
v-model="form.agent_id"
|
||||
:disabled="isEdit"
|
||||
>
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue placeholder="请选择代理" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="agent in agents" :key="agent.id" :value="String(agent.id)">
|
||||
{{ agent.username }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel>选择应用</UiLabel>
|
||||
<UiSelect
|
||||
v-model="form.application_id"
|
||||
@update:model-value="onAppChange"
|
||||
:disabled="isEdit"
|
||||
>
|
||||
<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>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard v-if="form.application_id && cardTypes.length > 0">
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>卡类权限</UiCardTitle>
|
||||
<UiCardDescription>配置代理可以生成的卡类和价格</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/50">
|
||||
<tr>
|
||||
<th class="text-left p-3 font-medium w-12">授权</th>
|
||||
<th class="text-left p-3 font-medium">卡类名称</th>
|
||||
<th class="text-left p-3 font-medium">计费类型</th>
|
||||
<th class="text-right p-3 font-medium w-32">代理价格</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="ct in cardTypes" :key="ct.id" class="border-t hover:bg-muted/30 transition-colors">
|
||||
<td class="p-3">
|
||||
<UiCheckbox
|
||||
:checked="form.card_type_auth[String(ct.id)] ?? true"
|
||||
@update:checked="form.card_type_auth[String(ct.id)] = $event"
|
||||
/>
|
||||
</td>
|
||||
<td class="p-3 font-medium">{{ ct.name }}</td>
|
||||
<td class="p-3 text-muted-foreground">{{ ct.billing_type }}</td>
|
||||
<td class="p-3">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<span class="text-muted-foreground">¥</span>
|
||||
<UiInput
|
||||
class="w-20 text-right"
|
||||
:model-value="String(form.card_type_prices[String(ct.id)] ?? ct.price)"
|
||||
@input="form.card_type_prices[String(ct.id)] = Number(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-3">
|
||||
提示:取消勾选"授权"列可以禁止代理生成该类型的卡密
|
||||
</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<UiButton
|
||||
:disabled="saving || !form.agent_id || !form.application_id"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<CheckCircle v-else class="mr-2 h-4 w-4" />
|
||||
{{ isEdit ? '保存修改' : '确认授权' }}
|
||||
</UiButton>
|
||||
<UiButton variant="outline" @click="router.back()">
|
||||
取消
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</BasicPage>
|
||||
</template>
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import type { Composer } from 'vue-i18n'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Check, Pencil, Trash2, X } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { AgentApp } from '../data/schema'
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { MoreHorizontal } from 'lucide-vue-next'
|
||||
|
||||
export function getColumns(actions: {
|
||||
onEdit: (row: AgentApp) => void
|
||||
onDelete: (row: AgentApp) => void
|
||||
onToggleStatus: (row: AgentApp) => void
|
||||
}, t: Composer['t']): ColumnDef<AgentApp>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'agent_name',
|
||||
header: () => h('span', {}, '代理'),
|
||||
cell: ({ row }) => {
|
||||
const agentName = row.getValue('agent_name') as string
|
||||
return h('div', { class: 'flex items-center gap-2' }, [
|
||||
h('span', { class: 'font-medium' }, agentName || '-'),
|
||||
])
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'app_name',
|
||||
header: () => h('span', {}, '应用'),
|
||||
cell: ({ row }) => {
|
||||
const appName = row.getValue('app_name') as string
|
||||
return h('span', { class: 'font-medium' }, appName || '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'card_types',
|
||||
header: () => h('span', {}, '卡密类型'),
|
||||
cell: ({ row }) => {
|
||||
const cardTypes = row.getValue('card_types') as AgentApp['card_types']
|
||||
const total = cardTypes?.length || 0
|
||||
const authorized = cardTypes?.filter(c => c.can_generate).length || 0
|
||||
return h('div', { class: 'flex items-center gap-2' }, [
|
||||
h('span', { class: 'text-sm' }, `${authorized} / ${total}`),
|
||||
])
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'balance',
|
||||
header: () => h('span', {}, '余额'),
|
||||
cell: ({ row }) => {
|
||||
const balance = row.getValue('balance') as number
|
||||
return h('span', { class: 'text-sm font-medium' }, `¥${(balance || 0).toFixed(2)}`)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: () => h('span', {}, '状态'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as string
|
||||
const isActive = status === 'active'
|
||||
return h(Badge, { variant: isActive ? 'default' : 'secondary' }, () =>
|
||||
isActive ? '已启用' : '已禁用'
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => h('span', { class: 'sr-only' }, '操作'),
|
||||
cell: ({ row }) => {
|
||||
const app = row.original
|
||||
const isActive = app.status === 'active'
|
||||
|
||||
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.onEdit(app) }, () => [
|
||||
h(Pencil, { class: 'mr-2 h-4 w-4' }),
|
||||
'编辑',
|
||||
]),
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(app) }, () => [
|
||||
h(Check, { class: 'mr-2 h-4 w-4' }),
|
||||
isActive ? '禁用' : '启用',
|
||||
]),
|
||||
h(DropdownMenuSeparator),
|
||||
h(DropdownMenuItem, { class: 'text-destructive', onClick: () => actions.onDelete(app) }, () => [
|
||||
h(Trash2, { class: 'mr-2 h-4 w-4' }),
|
||||
'删除',
|
||||
]),
|
||||
],
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import type { DataTableProps } from '@/components/data-table/types'
|
||||
import type { AgentApp } from '../data/schema'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DataTable from '@/components/data-table/data-table.vue'
|
||||
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||
import { getColumns } from './columns'
|
||||
|
||||
const props = defineProps<Omit<DataTableProps<AgentApp>, 'columns'> & {
|
||||
onEdit: (row: AgentApp) => void
|
||||
onDelete: (row: AgentApp) => void
|
||||
onToggleStatus: (row: AgentApp) => void
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const columns = computed(() => getColumns({
|
||||
onEdit: props.onEdit,
|
||||
onDelete: props.onDelete,
|
||||
onToggleStatus: props.onToggleStatus,
|
||||
}, t))
|
||||
|
||||
const table = generateVueTable<AgentApp>({
|
||||
get data() { return props.data },
|
||||
get loading() { return props.loading },
|
||||
columns: columns.value,
|
||||
})
|
||||
|
||||
const columnLabels: Record<string, string> = {
|
||||
agent_name: '代理',
|
||||
app_name: '应用',
|
||||
card_types: '卡密类型',
|
||||
balance: '余额',
|
||||
status: '状态',
|
||||
actions: '操作',
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
table,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
||||
<template #toolbar>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
共 {{ data.length }} 条授权记录
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
</template>
|
||||
@@ -0,0 +1,243 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, CheckCircle, Loader2 } 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 { BasicPage } from '@/components/global-layout'
|
||||
import api from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Agent {
|
||||
id: number
|
||||
username: string
|
||||
}
|
||||
|
||||
interface Application {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
interface CardType {
|
||||
id: number
|
||||
name: string
|
||||
billing_type: string
|
||||
price: number
|
||||
}
|
||||
|
||||
const saving = ref(false)
|
||||
|
||||
const agents = ref<Agent[]>([])
|
||||
const applications = ref<Application[]>([])
|
||||
const cardTypes = ref<CardType[]>([])
|
||||
|
||||
const form = ref({
|
||||
agent_id: '',
|
||||
application_id: '',
|
||||
card_type_prices: {} as Record<string, number>,
|
||||
card_type_auth: {} as Record<string, boolean>,
|
||||
})
|
||||
|
||||
async function fetchAgents() {
|
||||
try {
|
||||
const data = await api.get<{ agents?: Agent[] }>('/dev/agents')
|
||||
agents.value = data?.agents || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取代理列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
const data = await api.get<{ applications: Application[] }>('/dev/applications')
|
||||
applications.value = data?.applications || []
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function fetchCardTypes(appId: string) {
|
||||
if (!appId) return
|
||||
try {
|
||||
const data = await api.get<{ card_types: CardType[] }>(`/dev/card-types?application_id=${appId}`)
|
||||
cardTypes.value = data?.card_types || []
|
||||
cardTypes.value.forEach((ct) => {
|
||||
form.value.card_type_prices[String(ct.id)] = ct.price
|
||||
form.value.card_type_auth[String(ct.id)] = true
|
||||
})
|
||||
}
|
||||
catch {
|
||||
cardTypes.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function onAppChange(appId: string) {
|
||||
cardTypes.value = []
|
||||
form.value.card_type_prices = {}
|
||||
form.value.card_type_auth = {}
|
||||
if (appId) fetchCardTypes(appId)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.agent_id) {
|
||||
toast.error('请选择代理')
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.application_id) {
|
||||
toast.error('请选择应用')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const cardTypesPayload = cardTypes.value.map(ct => ({
|
||||
card_type_id: ct.id,
|
||||
can_generate: form.value.card_type_auth[String(ct.id)] || false,
|
||||
price: form.value.card_type_prices[String(ct.id)] !== undefined
|
||||
? Number(form.value.card_type_prices[String(ct.id)])
|
||||
: ct.price,
|
||||
}))
|
||||
|
||||
await api.post('/dev/agent-apps', {
|
||||
agent_id: Number(form.value.agent_id),
|
||||
application_id: Number(form.value.application_id),
|
||||
card_types: cardTypesPayload,
|
||||
discount: 0,
|
||||
})
|
||||
toast.success('授权添加成功')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
catch (error: any) {
|
||||
toast.error(error.message || '添加授权失败')
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchAgents()
|
||||
fetchApplications()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
title="添加授权"
|
||||
description="为代理添加新的应用授权"
|
||||
:breadcrumbs="[
|
||||
{ title: t('nav.agentApps'), href: '/admin/agent-apps' },
|
||||
{ title: '添加授权' },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>基本信息</UiCardTitle>
|
||||
<UiCardDescription>选择代理和应用</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>选择代理</UiLabel>
|
||||
<UiSelect v-model="form.agent_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue placeholder="请选择代理" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="agent in agents" :key="agent.id" :value="String(agent.id)">
|
||||
{{ agent.username }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel>选择应用</UiLabel>
|
||||
<UiSelect
|
||||
v-model="form.application_id"
|
||||
@update:model-value="onAppChange"
|
||||
>
|
||||
<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>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard v-if="form.application_id && cardTypes.length > 0">
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>卡类权限</UiCardTitle>
|
||||
<UiCardDescription>配置代理可以生成的卡类和价格</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/50">
|
||||
<tr>
|
||||
<th class="text-left p-3 font-medium w-12">授权</th>
|
||||
<th class="text-left p-3 font-medium">卡类名称</th>
|
||||
<th class="text-left p-3 font-medium">计费类型</th>
|
||||
<th class="text-right p-3 font-medium w-32">代理价格</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="ct in cardTypes" :key="ct.id" class="border-t hover:bg-muted/30 transition-colors">
|
||||
<td class="p-3">
|
||||
<UiCheckbox
|
||||
:checked="form.card_type_auth[String(ct.id)] ?? true"
|
||||
@update:checked="form.card_type_auth[String(ct.id)] = $event"
|
||||
/>
|
||||
</td>
|
||||
<td class="p-3 font-medium">{{ ct.name }}</td>
|
||||
<td class="p-3 text-muted-foreground">{{ ct.billing_type }}</td>
|
||||
<td class="p-3">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<span class="text-muted-foreground">¥</span>
|
||||
<UiInput
|
||||
class="w-20 text-right"
|
||||
:model-value="String(form.card_type_prices[String(ct.id)] ?? ct.price)"
|
||||
@input="form.card_type_prices[String(ct.id)] = Number(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-3">
|
||||
提示:取消勾选"授权"列可以禁止代理生成该类型的卡密
|
||||
</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<UiButton
|
||||
:disabled="saving || !form.agent_id || !form.application_id"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<CheckCircle v-else class="mr-2 h-4 w-4" />
|
||||
确认授权
|
||||
</UiButton>
|
||||
<UiButton variant="outline" @click="router.back()">
|
||||
取消
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</BasicPage>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface AgentApp {
|
||||
id: number
|
||||
agent_id: number
|
||||
agent_name?: string
|
||||
application_id: number
|
||||
app_name: string
|
||||
status: string
|
||||
discount: number
|
||||
balance: number
|
||||
card_types: { card_type_id: number, can_generate: boolean, name?: string }[]
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckCircle, KeyRound, Network, Plus } 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 } from './data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from './components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const agentApps = ref<AgentApp[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteTarget = ref<AgentApp | null>(null)
|
||||
|
||||
const totalAuthApps = computed(() => agentApps.value.length)
|
||||
const activeAuthApps = computed(() => agentApps.value.filter(app => app.status === 'active').length)
|
||||
const totalCardTypes = computed(() => {
|
||||
return agentApps.value.reduce((sum, app) => {
|
||||
return sum + (app.card_types?.filter(c => c.can_generate).length || 0)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
async function fetchAgentApps() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<{ agent_apps?: AgentApp[] }>('/dev/agent-apps')
|
||||
agentApps.value = data?.agent_apps || data || []
|
||||
}
|
||||
catch {
|
||||
agentApps.value = []
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToCreate() {
|
||||
router.push('/admin/agent-apps/create')
|
||||
}
|
||||
|
||||
function goToEdit(app: AgentApp) {
|
||||
router.push(`/admin/agent-apps/${app.id}`)
|
||||
}
|
||||
|
||||
function confirmDelete(app: AgentApp) {
|
||||
deleteTarget.value = app
|
||||
deleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!deleteTarget.value) return
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/agent-apps/${deleteTarget.value.id}`)
|
||||
toast.success('删除授权成功')
|
||||
fetchAgentApps()
|
||||
}
|
||||
catch (error: any) {
|
||||
toast.error(error.message || '删除失败')
|
||||
}
|
||||
finally {
|
||||
deleteTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleStatus(app: AgentApp) {
|
||||
try {
|
||||
const newStatus = app.status === 'active' ? 'inactive' : 'active'
|
||||
await api.put(`/dev/agent-apps/${app.id}/status`, { status: newStatus })
|
||||
toast.success('状态更新成功')
|
||||
fetchAgentApps()
|
||||
}
|
||||
catch (error: any) {
|
||||
toast.error(error.message || '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchAgentApps()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('nav.agentApps')"
|
||||
description="管理代理的应用授权和卡类权限"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton @click="goToCreate">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
添加授权
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
授权应用总数
|
||||
</UiCardTitle>
|
||||
<KeyRound class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ totalAuthApps }}
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
已授权的应用数量
|
||||
</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
活跃授权
|
||||
</UiCardTitle>
|
||||
<CheckCircle class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ activeAuthApps }}
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
状态为启用的授权
|
||||
</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
授权卡类
|
||||
</UiCardTitle>
|
||||
<Network class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ totalCardTypes }}
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
已授权的卡类总数
|
||||
</p>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<DataTable
|
||||
ref="tableRef"
|
||||
:loading
|
||||
:data="agentApps"
|
||||
:on-edit="goToEdit"
|
||||
:on-delete="confirmDelete"
|
||||
:on-toggle-status="handleToggleStatus"
|
||||
@refresh="fetchAgentApps"
|
||||
/>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="deleteDialogOpen"
|
||||
destructive
|
||||
confirm-button-text="删除"
|
||||
cancel-button-text="取消"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #title>
|
||||
删除授权
|
||||
</template>
|
||||
<template #description>
|
||||
确定要删除代理「{{ deleteTarget?.agent_name }}」对应用「{{ deleteTarget?.app_name }}」的授权吗?此操作不可撤销。
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Ban, Check, CheckCircle, Loader2, Plus, Receipt, Settings2, Trash2, UserPlus, X } from 'lucide-vue-next'
|
||||
import { Check, Loader2, Receipt, Settings2, UserPlus } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -19,44 +19,10 @@ interface Agent {
|
||||
status: string
|
||||
}
|
||||
|
||||
interface Application {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
interface CardType {
|
||||
id: number
|
||||
name: string
|
||||
billing_type: string
|
||||
price: number
|
||||
}
|
||||
|
||||
interface AgentApp {
|
||||
id: number
|
||||
agent_id: number
|
||||
application_id: number
|
||||
app_name: string
|
||||
status: string
|
||||
discount: number
|
||||
balance: number
|
||||
card_types: { card_type_id: number, can_generate: boolean, name?: string }[]
|
||||
}
|
||||
|
||||
const agentId = computed(() => route.params.id as string)
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const agents = ref<Agent[]>([])
|
||||
const authApps = ref<AgentApp[]>([])
|
||||
const applications = ref<Application[]>([])
|
||||
const cardTypes = ref<CardType[]>([])
|
||||
|
||||
const authLoading = ref(false)
|
||||
const showAddAuth = ref(false)
|
||||
const newAuthForm = ref({
|
||||
application_id: '',
|
||||
card_type_prices: {} as Record<string, number>,
|
||||
card_type_auth: {} as Record<string, boolean>,
|
||||
})
|
||||
|
||||
const form = ref({
|
||||
username: '',
|
||||
@@ -99,109 +65,6 @@ async function fetchAgents() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAuthApps() {
|
||||
authLoading.value = true
|
||||
try {
|
||||
const data = await api.get<{ agent_apps?: AgentApp[] }>(`/dev/agent-apps?agent_id=${agentId.value}`)
|
||||
authApps.value = data?.agent_apps || data || []
|
||||
}
|
||||
catch {
|
||||
authApps.value = []
|
||||
}
|
||||
finally {
|
||||
authLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
const data = await api.get<{ applications: Application[] }>('/dev/applications')
|
||||
applications.value = data?.applications || []
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function fetchCardTypes(appId: string) {
|
||||
if (!appId) return
|
||||
try {
|
||||
const data = await api.get<{ card_types: CardType[] }>(`/dev/applications/${appId}/card-types`)
|
||||
cardTypes.value = data?.card_types || []
|
||||
}
|
||||
catch {
|
||||
cardTypes.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function openAddAuth() {
|
||||
newAuthForm.value = { application_id: '', card_type_prices: {}, card_type_auth: {} }
|
||||
showAddAuth.value = true
|
||||
}
|
||||
|
||||
function onAppChange(appId: string) {
|
||||
cardTypes.value = []
|
||||
newAuthForm.value.card_type_prices = {}
|
||||
newAuthForm.value.card_type_auth = {}
|
||||
if (appId) fetchCardTypes(appId)
|
||||
}
|
||||
|
||||
async function handleAddAuth() {
|
||||
if (!newAuthForm.value.application_id) {
|
||||
toast.error('请选择应用')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const cardTypesPayload = cardTypes.value.map(ct => ({
|
||||
card_type_id: ct.id,
|
||||
can_generate: newAuthForm.value.card_type_auth[String(ct.id)] || false,
|
||||
price: newAuthForm.value.card_type_prices[String(ct.id)] !== undefined
|
||||
? Number(newAuthForm.value.card_type_prices[String(ct.id)])
|
||||
: ct.price,
|
||||
}))
|
||||
|
||||
await api.post('/dev/agent-apps', {
|
||||
agent_id: Number(agentId.value),
|
||||
application_id: Number(newAuthForm.value.application_id),
|
||||
card_types: cardTypesPayload,
|
||||
discount: 0,
|
||||
})
|
||||
|
||||
toast.success('授权成功')
|
||||
showAddAuth.value = false
|
||||
fetchAuthApps()
|
||||
}
|
||||
catch (error: any) {
|
||||
toast.error(error.message || '授权失败')
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveAuth(app: AgentApp) {
|
||||
try {
|
||||
await api.delete(`/dev/agent-apps/${app.id}`)
|
||||
toast.success('移除授权成功')
|
||||
fetchAuthApps()
|
||||
}
|
||||
catch (error: any) {
|
||||
toast.error(error.message || '移除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleAuthStatus(app: AgentApp) {
|
||||
try {
|
||||
const newStatus = app.status === 'active' ? 'inactive' : 'active'
|
||||
await api.put(`/dev/agent-apps/${app.id}/status`, { status: newStatus })
|
||||
toast.success('状态更新成功')
|
||||
fetchAuthApps()
|
||||
}
|
||||
catch (error: any) {
|
||||
toast.error(error.message || '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
const selectedParentAgent = computed(() => {
|
||||
if (form.value.parent_agent_id && form.value.parent_agent_id !== 'none') {
|
||||
return agents.value.find(a => String(a.id) === form.value.parent_agent_id)
|
||||
@@ -251,8 +114,6 @@ async function handleSave() {
|
||||
onMounted(() => {
|
||||
fetchAgents()
|
||||
fetchAgent()
|
||||
fetchAuthApps()
|
||||
fetchApplications()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -386,98 +247,6 @@ onMounted(() => {
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<CheckCircle class="size-5" />
|
||||
授权应用
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>管理该代理可生成卡密的应用和卡类权限</UiCardDescription>
|
||||
</div>
|
||||
<UiButton size="sm" @click="openAddAuth">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
添加授权
|
||||
</UiButton>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="p-0">
|
||||
<div v-if="authLoading" class="flex items-center justify-center py-12">
|
||||
<Loader2 class="size-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<div v-else-if="authApps.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
<CheckCircle class="size-12 mx-auto mb-3 opacity-30" />
|
||||
<p>暂无授权应用</p>
|
||||
<p class="text-sm mt-1">点击上方按钮为代理添加应用授权</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/50">
|
||||
<tr>
|
||||
<th class="text-left p-3 font-medium">应用</th>
|
||||
<th class="text-left p-3 font-medium">卡密类型</th>
|
||||
<th class="text-left p-3 font-medium">余额</th>
|
||||
<th class="text-left p-3 font-medium">状态</th>
|
||||
<th class="text-right p-3 font-medium w-20">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="app in authApps" :key="app.id">
|
||||
<tr class="border-t">
|
||||
<td class="p-3 font-medium" :rowspan="app.card_types && app.card_types.length > 0 ? app.card_types.length : 1">
|
||||
{{ app.app_name }}
|
||||
</td>
|
||||
<td class="p-3">
|
||||
<div v-if="app.card_types && app.card_types.length > 0" class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs"
|
||||
:class="app.card_types[0].can_generate ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'"
|
||||
>
|
||||
<Check v-if="app.card_types[0].can_generate" class="size-3" />
|
||||
<X v-else class="size-3" />
|
||||
{{ app.card_types[0].name || `卡类#${app.card_types[0].card_type_id}` }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="text-muted-foreground">-</span>
|
||||
</td>
|
||||
<td class="p-3 font-medium" :rowspan="app.card_types && app.card_types.length > 0 ? app.card_types.length : 1">
|
||||
¥{{ (app.balance || 0).toFixed(2) }}
|
||||
</td>
|
||||
<td class="p-3" :rowspan="app.card_types && app.card_types.length > 0 ? app.card_types.length : 1">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs cursor-pointer"
|
||||
:class="app.status === 'active' ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'"
|
||||
@click="handleToggleAuthStatus(app)"
|
||||
>
|
||||
{{ app.status === 'active' ? '已启用' : '已禁用' }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="p-3 text-right" :rowspan="app.card_types && app.card_types.length > 0 ? app.card_types.length : 1">
|
||||
<UiButton variant="ghost" size="icon" class="text-destructive" @click="handleRemoveAuth(app)">
|
||||
<Trash2 class="size-4" />
|
||||
</UiButton>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="(ct, index) in app.card_types?.slice(1) || []" :key="ct.card_type_id" class="border-t">
|
||||
<td class="p-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs"
|
||||
:class="ct.can_generate ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'"
|
||||
>
|
||||
<Check v-if="ct.can_generate" class="size-3" />
|
||||
<X v-else class="size-3" />
|
||||
{{ ct.name || `卡类#${ct.card_type_id}` }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
@@ -540,73 +309,5 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UiDialog v-model:open="showAddAuth">
|
||||
<UiDialogContent class="sm:max-w-xl max-h-[80vh] overflow-y-auto">
|
||||
<UiDialogHeader>
|
||||
<UiDialogTitle>添加授权</UiDialogTitle>
|
||||
<UiDialogDescription>为该代理添加应用和卡类权限</UiDialogDescription>
|
||||
</UiDialogHeader>
|
||||
<div class="space-y-4 py-4">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>选择应用</UiLabel>
|
||||
<UiSelect v-model="newAuthForm.application_id" @update:model-value="onAppChange">
|
||||
<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 v-if="newAuthForm.application_id && cardTypes.length > 0" class="space-y-3">
|
||||
<UiLabel>卡类权限</UiLabel>
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/50">
|
||||
<tr>
|
||||
<th class="text-left p-2 font-medium">卡类</th>
|
||||
<th class="text-left p-2 font-medium">类型</th>
|
||||
<th class="text-right p-2 font-medium w-28">售价</th>
|
||||
<th class="text-center p-2 font-medium w-20">可生成</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="ct in cardTypes" :key="ct.id" class="border-t">
|
||||
<td class="p-2">{{ ct.name }}</td>
|
||||
<td class="p-2 text-muted-foreground">{{ ct.billing_type }}</td>
|
||||
<td class="p-2 text-right">
|
||||
<UiInput
|
||||
class="w-24 text-right"
|
||||
:model-value="String(newAuthForm.card_type_prices[String(ct.id)] ?? ct.price)"
|
||||
@input="newAuthForm.card_type_prices[String(ct.id)] = Number(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</td>
|
||||
<td class="p-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="newAuthForm.card_type_auth[String(ct.id)] ?? true"
|
||||
class="size-4"
|
||||
@change="newAuthForm.card_type_auth[String(ct.id)] = ($event.target as HTMLInputElement).checked"
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UiDialogFooter>
|
||||
<UiButton variant="outline" @click="showAddAuth = false">取消</UiButton>
|
||||
<UiButton :disabled="saving || !newAuthForm.application_id" @click="handleAddAuth">
|
||||
<CheckCircle v-if="!saving" class="mr-2 h-4 w-4" />
|
||||
确认授权
|
||||
</UiButton>
|
||||
</UiDialogFooter>
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -185,6 +185,24 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/pages/admin/agents/[id].vue'),
|
||||
meta: { title: '编辑代理 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'agent-apps',
|
||||
name: 'AdminAgentApps',
|
||||
component: () => import('@/pages/admin/agent-apps/index.vue'),
|
||||
meta: { title: '授权管理 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'agent-apps/create',
|
||||
name: 'AdminAgentAppCreate',
|
||||
component: () => import('@/pages/admin/agent-apps/create.vue'),
|
||||
meta: { title: '添加授权 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'agent-apps/:id',
|
||||
name: 'AdminAgentAppEdit',
|
||||
component: () => import('@/pages/admin/agent-apps/[id].vue'),
|
||||
meta: { title: '编辑授权 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'finance',
|
||||
name: 'AdminFinance',
|
||||
|
||||
Reference in New Issue
Block a user