feat: 优化授权管理和代理后台

- 授权管理页面显示具体卡密类型名称、授权时间、备注

- 重新设计添加/编辑授权页面,参考版本管理和卡类管理页面样式

- 添加备注字段支持

- 移除代理后台的应用管理页面和路由

- 从代理后台侧边栏删除应用管理导航
This commit is contained in:
2026-05-09 04:47:14 +08:00
parent c3536166b7
commit d7e479b5fa
6 changed files with 105 additions and 96 deletions
-6
View File
@@ -53,11 +53,6 @@ const navMain = computed(() => [
url: '/agent', url: '/agent',
icon: Gauge, icon: Gauge,
}, },
{
title: t('nav.applications'),
url: '/agent/apps',
icon: Boxes,
},
{ {
title: t('nav.cards'), title: t('nav.cards'),
url: '/agent/cards', url: '/agent/cards',
@@ -88,7 +83,6 @@ const breadcrumbs = computed(() => {
const crumbs = [{ title: t('nav.console'), path: '/agent' }] const crumbs = [{ title: t('nav.console'), path: '/agent' }]
const titleMap: Record<string, string> = { const titleMap: Record<string, string> = {
apps: t('nav.applications'),
cards: t('nav.cards'), cards: t('nav.cards'),
users: t('nav.users'), users: t('nav.users'),
finance: t('nav.finance'), finance: t('nav.finance'),
+42 -53
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Check, CheckCircle, Loader2 } from 'lucide-vue-next' import { CheckCircle, Loader2 } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
@@ -30,7 +30,6 @@ interface CardType {
} }
const agentAppId = computed(() => route.params.id as string) const agentAppId = computed(() => route.params.id as string)
const isEdit = computed(() => !!agentAppId.value)
const loading = ref(false) const loading = ref(false)
const saving = ref(false) const saving = ref(false)
@@ -41,6 +40,7 @@ const cardTypes = ref<CardType[]>([])
const form = ref({ const form = ref({
agent_id: '', agent_id: '',
application_id: '', application_id: '',
remark: '',
card_type_prices: {} as Record<string, number>, card_type_prices: {} as Record<string, number>,
card_type_auth: {} as Record<string, boolean>, card_type_auth: {} as Record<string, boolean>,
}) })
@@ -63,6 +63,17 @@ async function fetchApplications() {
catch { /* ignore */ } 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 || []
}
catch {
cardTypes.value = []
}
}
async function fetchAgentApp() { async function fetchAgentApp() {
if (!agentAppId.value) return if (!agentAppId.value) return
@@ -74,17 +85,15 @@ async function fetchAgentApp() {
if (agentApp) { if (agentApp) {
form.value.agent_id = String(agentApp.agent_id) form.value.agent_id = String(agentApp.agent_id)
form.value.application_id = String(agentApp.application_id) form.value.application_id = String(agentApp.application_id)
form.value.remark = agentApp.remark || ''
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)) await fetchCardTypes(String(agentApp.application_id))
cardTypes.value.forEach((ct) => { cardTypes.value.forEach((ct) => {
const existingCard = agentApp.card_types.find((c: any) => c.card_type_id === ct.id) const existingCard = agentApp.card_types?.find((c: any) => c.card_type_id === ct.id)
if (existingCard) { if (existingCard) {
form.value.card_type_prices[String(ct.id)] = existingCard.price || ct.price form.value.card_type_prices[String(ct.id)] = existingCard.price || ct.price
form.value.card_type_auth[String(ct.id)] = existingCard.can_generate
} else { } else {
form.value.card_type_prices[String(ct.id)] = ct.price form.value.card_type_prices[String(ct.id)] = ct.price
form.value.card_type_auth[String(ct.id)] = false form.value.card_type_auth[String(ct.id)] = false
@@ -101,24 +110,6 @@ async function fetchAgentApp() {
} }
} }
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() { async function handleSave() {
if (!form.value.agent_id) { if (!form.value.agent_id) {
toast.error('请选择代理') toast.error('请选择代理')
@@ -140,26 +131,16 @@ async function handleSave() {
: ct.price, : ct.price,
})) }))
if (isEdit.value) { await api.put(`/dev/agent-apps/${agentAppId.value}`, {
await api.put(`/dev/agent-apps/${agentAppId.value}`, { card_types: cardTypesPayload,
card_types: cardTypesPayload, discount: 0,
discount: 0, remark: form.value.remark,
}) })
toast.success('授权更新成功') 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') router.push('/admin/agent-apps')
} }
catch (error: any) { catch (error: any) {
toast.error(error.message || (isEdit.value ? '更新授权失败' : '添加授权失败')) toast.error(error.message || '更新授权失败')
} }
finally { finally {
saving.value = false saving.value = false
@@ -169,19 +150,17 @@ async function handleSave() {
onMounted(() => { onMounted(() => {
fetchAgents() fetchAgents()
fetchApplications() fetchApplications()
if (isEdit.value) { fetchAgentApp()
fetchAgentApp()
}
}) })
</script> </script>
<template> <template>
<BasicPage <BasicPage
:title="isEdit ? '编辑授权' : '添加授权'" title="编辑授权"
:description="isEdit ? '修改代理的应用授权配置' : '为代理添加新的应用授权'" description="修改代理的应用授权配置"
:breadcrumbs="[ :breadcrumbs="[
{ title: t('nav.agentApps'), href: '/admin/agent-apps' }, { title: t('nav.agentApps'), href: '/admin/agent-apps' },
{ title: isEdit ? '编辑授权' : '添加授权' }, { title: '编辑授权' },
]" ]"
sticky sticky
> >
@@ -201,7 +180,7 @@ onMounted(() => {
<UiLabel>选择代理</UiLabel> <UiLabel>选择代理</UiLabel>
<UiSelect <UiSelect
v-model="form.agent_id" v-model="form.agent_id"
:disabled="isEdit" disabled
> >
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue placeholder="请选择代理" /> <UiSelectValue placeholder="请选择代理" />
@@ -212,14 +191,14 @@ onMounted(() => {
</UiSelectItem> </UiSelectItem>
</UiSelectContent> </UiSelectContent>
</UiSelect> </UiSelect>
<p class="text-xs text-muted-foreground">编辑时不可修改代理</p>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>选择应用</UiLabel> <UiLabel>选择应用</UiLabel>
<UiSelect <UiSelect
v-model="form.application_id" v-model="form.application_id"
@update:model-value="onAppChange" disabled
:disabled="isEdit"
> >
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue placeholder="请选择应用" /> <UiSelectValue placeholder="请选择应用" />
@@ -230,8 +209,18 @@ onMounted(() => {
</UiSelectItem> </UiSelectItem>
</UiSelectContent> </UiSelectContent>
</UiSelect> </UiSelect>
<p class="text-xs text-muted-foreground">编辑时不可修改应用</p>
</div> </div>
</div> </div>
<div class="space-y-2">
<UiLabel>备注</UiLabel>
<UiTextarea
v-model="form.remark"
placeholder="输入备注信息(可选)"
rows="3"
/>
</div>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -290,7 +279,7 @@ onMounted(() => {
> >
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" /> <Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<CheckCircle v-else class="mr-2 h-4 w-4" /> <CheckCircle v-else class="mr-2 h-4 w-4" />
{{ isEdit ? '保存修改' : '确认授权' }} 保存修改
</UiButton> </UiButton>
<UiButton variant="outline" @click="router.back()"> <UiButton variant="outline" @click="router.back()">
取消 取消
@@ -3,7 +3,7 @@ import type { Composer } from 'vue-i18n'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Check, Pencil, Trash2, X } from 'lucide-vue-next' import { Check, Pencil, Trash2 } from 'lucide-vue-next'
import { h } from 'vue' import { h } from 'vue'
import type { AgentApp } from '../data/schema' import type { AgentApp } from '../data/schema'
@@ -46,19 +46,24 @@ export function getColumns(actions: {
header: () => h('span', {}, '卡密类型'), header: () => h('span', {}, '卡密类型'),
cell: ({ row }) => { cell: ({ row }) => {
const cardTypes = row.getValue('card_types') as AgentApp['card_types'] const cardTypes = row.getValue('card_types') as AgentApp['card_types']
const total = cardTypes?.length || 0 if (!cardTypes || cardTypes.length === 0) {
const authorized = cardTypes?.filter(c => c.can_generate).length || 0 return h('span', { class: 'text-muted-foreground' }, '-')
return h('div', { class: 'flex items-center gap-2' }, [ }
h('span', { class: 'text-sm' }, `${authorized} / ${total}`),
]) return h('div', { class: 'flex flex-wrap gap-1' },
}, cardTypes.map(ct =>
}, h('span', {
{ class: `inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs ${
accessorKey: 'balance', ct.can_generate
header: () => h('span', {}, '余额'), ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
cell: ({ row }) => { : 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'
const balance = row.getValue('balance') as number }`
return h('span', { class: 'text-sm font-medium' }, `¥${(balance || 0).toFixed(2)}`) }, [
ct.can_generate && h(Check, { class: 'size-3' }),
ct.name || `卡类#${ct.card_type_id}`
])
)
)
}, },
}, },
{ {
@@ -72,6 +77,24 @@ export function getColumns(actions: {
) )
}, },
}, },
{
accessorKey: 'created_at',
header: () => h('span', {}, '授权时间'),
cell: ({ row }) => {
const createdAt = row.getValue('created_at') as string
if (!createdAt) return h('span', { class: 'text-muted-foreground' }, '-')
const date = new Date(createdAt)
return h('span', { class: 'text-sm' }, date.toLocaleString('zh-CN'))
},
},
{
accessorKey: 'remark',
header: () => h('span', {}, '备注'),
cell: ({ row }) => {
const remark = row.getValue('remark') as string
return h('span', { class: 'text-sm text-muted-foreground' }, remark || '-')
},
},
{ {
id: 'actions', id: 'actions',
header: () => h('span', { class: 'sr-only' }, '操作'), header: () => h('span', { class: 'sr-only' }, '操作'),
+25 -11
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Check, CheckCircle, Loader2 } from 'lucide-vue-next' import { CheckCircle, Loader2 } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
@@ -29,7 +29,6 @@ interface CardType {
} }
const saving = ref(false) const saving = ref(false)
const agents = ref<Agent[]>([]) const agents = ref<Agent[]>([])
const applications = ref<Application[]>([]) const applications = ref<Application[]>([])
const cardTypes = ref<CardType[]>([]) const cardTypes = ref<CardType[]>([])
@@ -37,10 +36,18 @@ const cardTypes = ref<CardType[]>([])
const form = ref({ const form = ref({
agent_id: '', agent_id: '',
application_id: '', application_id: '',
remark: '',
card_type_prices: {} as Record<string, number>, card_type_prices: {} as Record<string, number>,
card_type_auth: {} as Record<string, boolean>, card_type_auth: {} as Record<string, boolean>,
}) })
const selectedApplication = computed(() => {
if (form.value.application_id) {
return applications.value.find(app => String(app.id) === form.value.application_id)
}
return null
})
async function fetchAgents() { async function fetchAgents() {
try { try {
const data = await api.get<{ agents?: Agent[] }>('/dev/agents') const data = await api.get<{ agents?: Agent[] }>('/dev/agents')
@@ -74,12 +81,12 @@ async function fetchCardTypes(appId: string) {
} }
} }
function onAppChange(appId: string) { watch(() => form.value.application_id, (appId) => {
cardTypes.value = [] cardTypes.value = []
form.value.card_type_prices = {} form.value.card_type_prices = {}
form.value.card_type_auth = {} form.value.card_type_auth = {}
if (appId) fetchCardTypes(appId) if (appId) fetchCardTypes(appId)
} })
async function handleSave() { async function handleSave() {
if (!form.value.agent_id) { if (!form.value.agent_id) {
@@ -107,6 +114,7 @@ async function handleSave() {
application_id: Number(form.value.application_id), application_id: Number(form.value.application_id),
card_types: cardTypesPayload, card_types: cardTypesPayload,
discount: 0, discount: 0,
remark: form.value.remark,
}) })
toast.success('授权添加成功') toast.success('授权添加成功')
router.push('/admin/agent-apps') router.push('/admin/agent-apps')
@@ -144,7 +152,7 @@ onMounted(() => {
<UiCardContent class="space-y-6"> <UiCardContent class="space-y-6">
<div class="grid gap-6 md:grid-cols-2"> <div class="grid gap-6 md:grid-cols-2">
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>选择代理</UiLabel> <UiLabel>选择代理 <span class="text-destructive">*</span></UiLabel>
<UiSelect v-model="form.agent_id"> <UiSelect v-model="form.agent_id">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue placeholder="请选择代理" /> <UiSelectValue placeholder="请选择代理" />
@@ -158,11 +166,8 @@ onMounted(() => {
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>选择应用</UiLabel> <UiLabel>选择应用 <span class="text-destructive">*</span></UiLabel>
<UiSelect <UiSelect v-model="form.application_id">
v-model="form.application_id"
@update:model-value="onAppChange"
>
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue placeholder="请选择应用" /> <UiSelectValue placeholder="请选择应用" />
</UiSelectTrigger> </UiSelectTrigger>
@@ -174,6 +179,15 @@ onMounted(() => {
</UiSelect> </UiSelect>
</div> </div>
</div> </div>
<div class="space-y-2">
<UiLabel>备注</UiLabel>
<UiTextarea
v-model="form.remark"
placeholder="输入备注信息(可选)"
rows="3"
/>
</div>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -10,4 +10,5 @@ export interface AgentApp {
card_types: { card_type_id: number, can_generate: boolean, name?: string }[] card_types: { card_type_id: number, can_generate: boolean, name?: string }[]
created_at?: string created_at?: string
updated_at?: string updated_at?: string
remark?: string
} }
-12
View File
@@ -426,18 +426,6 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/pages/agent/index.vue'), component: () => import('@/pages/agent/index.vue'),
meta: { title: '控制台 - 代理商后台' }, meta: { title: '控制台 - 代理商后台' },
}, },
{
path: 'apps',
name: 'AgentApps',
component: () => import('@/pages/agent/apps/index.vue'),
meta: { title: '应用管理 - 代理商后台' },
},
{
path: 'apps/:id',
name: 'AgentAppDetail',
component: () => import('@/pages/agent/apps/[id].vue'),
meta: { title: '应用详情 - 代理商后台' },
},
{ {
path: 'cards', path: 'cards',
name: 'AgentCards', name: 'AgentCards',