refactor: 删除独立的授权管理页面和导航,授权功能合并到代理详情页
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, Share2 } from 'lucide-vue-next'
|
||||
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 { computed, onMounted, onUnmounted, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -137,11 +137,6 @@ const navMain = computed(() => [
|
||||
url: '/admin/agents',
|
||||
icon: Network,
|
||||
},
|
||||
{
|
||||
title: t('nav.agentApps'),
|
||||
url: '/admin/agent-apps',
|
||||
icon: Share2,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeft, Check, CheckCheck, Eye, Loader2, X } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { AgentApp, CardTypeAuth } from '@/pages/admin/agent-apps/data/schema'
|
||||
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import api from '@/services/api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const agentApp = ref<AgentApp | null>(null)
|
||||
|
||||
const form = ref({
|
||||
status: 'active',
|
||||
discount: 1.0,
|
||||
card_types: [] as Array<{
|
||||
card_type_id: number
|
||||
name: string
|
||||
billing_type: string
|
||||
origin_price: number
|
||||
price: number
|
||||
can_generate: boolean
|
||||
}>,
|
||||
})
|
||||
|
||||
const agentAppId = computed(() => route.params.id as string)
|
||||
|
||||
function getBillingTypeText(type: string) {
|
||||
const map: Record<string, string> = {
|
||||
subscription: '订阅',
|
||||
time: '计时',
|
||||
point: '点卡',
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
async function fetchAgentApp() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [agentAppRes, cardTypesRes] = await Promise.all([
|
||||
api.get<{ agent_app: AgentApp }>(`/dev/agent-apps/${agentAppId.value}`),
|
||||
api.get<{ card_types: Array<{ id: number, name: string, billing_type: string, price: number }> }>('/dev/card-types'),
|
||||
])
|
||||
|
||||
if (agentAppRes?.agent_app) {
|
||||
agentApp.value = agentAppRes.agent_app
|
||||
form.value.status = agentAppRes.agent_app.status || 'active'
|
||||
form.value.discount = agentAppRes.agent_app.discount || 1.0
|
||||
|
||||
const existingPerms = agentAppRes.agent_app.card_types || []
|
||||
const allCardTypes = cardTypesRes?.card_types || []
|
||||
|
||||
form.value.card_types = allCardTypes.map((ct) => {
|
||||
const existing = existingPerms.find((p: CardTypeAuth) => (p.card_type_id || p.id) === ct.id)
|
||||
return {
|
||||
card_type_id: ct.id,
|
||||
name: ct.name,
|
||||
billing_type: ct.billing_type,
|
||||
origin_price: ct.price,
|
||||
price: existing?.price || ct.price,
|
||||
can_generate: existing ? existing.can_generate : false,
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
toast.error('获取授权信息失败')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取授权信息失败:', error)
|
||||
toast.error('获取授权信息失败')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
form.value.card_types.forEach(ct => ct.can_generate = true)
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
form.value.card_types.forEach(ct => ct.can_generate = false)
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (form.value.discount < 0 || form.value.discount > 1) {
|
||||
toast.error('折扣必须在0-1之间')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await api.put(`/dev/agent-apps/${agentAppId.value}`, {
|
||||
status: form.value.status,
|
||||
discount: form.value.discount,
|
||||
})
|
||||
|
||||
await api.put(`/dev/agent-apps/${agentAppId.value}/card-types`, {
|
||||
card_types: form.value.card_types.map(ct => ({
|
||||
card_type_id: ct.card_type_id,
|
||||
can_generate: ct.can_generate,
|
||||
price: ct.price,
|
||||
})),
|
||||
})
|
||||
|
||||
toast.success('保存成功')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('保存失败:', error)
|
||||
toast.error(error.message || '保存失败')
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
|
||||
function goToView() {
|
||||
router.push(`/admin/agent-apps/${agentAppId.value}/view`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchAgentApp()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
title="编辑授权"
|
||||
description="修改代理商授权信息"
|
||||
:breadcrumbs="[
|
||||
{ title: '授权管理', href: '/admin/agent-apps' },
|
||||
{ title: '编辑授权' },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton variant="outline" @click="goBack">
|
||||
<ArrowLeft class="mr-2 h-4 w-4" />
|
||||
返回
|
||||
</UiButton>
|
||||
<UiButton variant="outline" @click="goToView">
|
||||
<Eye class="mr-2 h-4 w-4" />
|
||||
查看授权
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="agentApp" class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>基本信息</UiCardTitle>
|
||||
<UiCardDescription>
|
||||
代理: {{ agentApp.agent_name }} | 应用: {{ agentApp.app_name }}
|
||||
</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>代理</UiLabel>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">{{ agentApp.agent_name }}</span>
|
||||
<span v-if="agentApp.agent_email" class="text-xs text-muted-foreground">
|
||||
({{ agentApp.agent_email }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<UiLabel>应用</UiLabel>
|
||||
<div class="flex items-center gap-2">
|
||||
<UiBadge variant="secondary">
|
||||
{{ agentApp.app_name }}
|
||||
</UiBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="status">状态</UiLabel>
|
||||
<UiSelect v-model="form.status">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue placeholder="请选择状态" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="active">
|
||||
已启用
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="inactive">
|
||||
已禁用
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="discount">折扣</UiLabel>
|
||||
<div class="flex items-center gap-4">
|
||||
<UiNumberField
|
||||
v-model="form.discount"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
class="flex-1"
|
||||
>
|
||||
<UiNumberFieldContent>
|
||||
<UiNumberFieldDecrement />
|
||||
<UiNumberFieldInput />
|
||||
<UiNumberFieldIncrement />
|
||||
</UiNumberFieldContent>
|
||||
</UiNumberField>
|
||||
<span class="text-sm text-muted-foreground w-16">
|
||||
{{ (form.discount * 10).toFixed(1) }}折
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
代理商购买卡密的折扣,范围0-1
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel>当前余额</UiLabel>
|
||||
<div class="text-2xl font-bold text-primary">
|
||||
¥{{ (agentApp.balance || 0).toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<UiCardTitle>卡密权限配置</UiCardTitle>
|
||||
<UiCardDescription>配置代理商可生成的卡密类型及价格</UiCardDescription>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<UiButton variant="outline" size="sm" @click="selectAll">
|
||||
<CheckCheck class="mr-2 h-4 w-4" />
|
||||
全选
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="clearAll">
|
||||
<X class="mr-2 h-4 w-4" />
|
||||
清空
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div v-if="form.card_types.length > 0">
|
||||
<UiTable>
|
||||
<UiTableHeader>
|
||||
<UiTableRow>
|
||||
<UiTableHead class="w-12" />
|
||||
<UiTableHead>卡类名称</UiTableHead>
|
||||
<UiTableHead>计费类型</UiTableHead>
|
||||
<UiTableHead>原价</UiTableHead>
|
||||
<UiTableHead>授权价格</UiTableHead>
|
||||
</UiTableRow>
|
||||
</UiTableHeader>
|
||||
<UiTableBody>
|
||||
<UiTableRow
|
||||
v-for="ct in form.card_types"
|
||||
:key="ct.card_type_id"
|
||||
:class="ct.can_generate ? 'bg-primary/5' : ''"
|
||||
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">
|
||||
{{ ct.name }}
|
||||
</UiTableCell>
|
||||
<UiTableCell>
|
||||
<UiBadge variant="outline">
|
||||
{{ getBillingTypeText(ct.billing_type) }}
|
||||
</UiBadge>
|
||||
</UiTableCell>
|
||||
<UiTableCell>
|
||||
¥{{ ct.origin_price }}
|
||||
</UiTableCell>
|
||||
<UiTableCell @click.stop>
|
||||
<UiNumberField v-model="ct.price" :min="0" class="w-32">
|
||||
<UiNumberFieldContent>
|
||||
<UiNumberFieldDecrement />
|
||||
<UiNumberFieldInput />
|
||||
<UiNumberFieldIncrement />
|
||||
</UiNumberFieldContent>
|
||||
</UiNumberField>
|
||||
</UiTableCell>
|
||||
</UiTableRow>
|
||||
</UiTableBody>
|
||||
</UiTable>
|
||||
</div>
|
||||
<div v-else class="text-center py-8 text-muted-foreground">
|
||||
暂无卡密类型
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>统计信息</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">卡类总数</span>
|
||||
<span class="font-medium">{{ form.card_types.length }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">已授权</span>
|
||||
<span class="font-medium text-green-600">
|
||||
{{ form.card_types.filter(ct => ct.can_generate).length }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">未授权</span>
|
||||
<span class="font-medium text-muted-foreground">
|
||||
{{ form.card_types.filter(ct => !ct.can_generate).length }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="saving"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Check v-else class="mr-2 h-4 w-4" />
|
||||
保存修改
|
||||
</UiButton>
|
||||
<UiButton variant="outline" class="w-full" @click="goBack">
|
||||
取消
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasicPage>
|
||||
</template>
|
||||
@@ -1,185 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeft, Loader2 } from 'lucide-vue-next'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import { API_BASE } from '@/utils/config'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const currentBalance = ref(0)
|
||||
const agentAppName = ref('')
|
||||
const rechargeAmount = ref(0)
|
||||
|
||||
async function fetchAgentApp() {
|
||||
loading.value = true
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await fetch(`${API_BASE}/dev/agent-apps/${route.params.id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.code === 200 && data.agent_app) {
|
||||
currentBalance.value = data.agent_app.balance
|
||||
agentAppName.value = `${data.agent_app.agent_name} - ${data.agent_app.app_name}`
|
||||
}
|
||||
else {
|
||||
toast.error('获取授权信息失败')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取授权信息失败:', error)
|
||||
toast.error('获取授权信息失败')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (rechargeAmount.value <= 0) {
|
||||
toast.error('请输入有效的充值金额')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await fetch(`${API_BASE}/dev/agent-apps/${route.params.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
balance: currentBalance.value + rechargeAmount.value,
|
||||
}),
|
||||
})
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
const quickAmounts = [100, 500, 1000, 5000]
|
||||
|
||||
function setAmount(amount: number) {
|
||||
rechargeAmount.value = amount
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchAgentApp()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
title="充值余额"
|
||||
description="为代理商充值余额"
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton variant="outline" @click="goBack">
|
||||
<ArrowLeft class="mr-2 h-4 w-4" />
|
||||
返回
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div v-else class="max-w-2xl">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>充值信息</UiCardTitle>
|
||||
<UiCardDescription>
|
||||
{{ agentAppName }}
|
||||
</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel>当前余额</UiLabel>
|
||||
<div class="text-3xl font-bold text-primary">
|
||||
¥{{ currentBalance.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="amount">
|
||||
充值金额
|
||||
</UiLabel>
|
||||
<UiInput
|
||||
id="amount"
|
||||
v-model.number="rechargeAmount"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="请输入充值金额"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UiButton
|
||||
v-for="amount in quickAmounts"
|
||||
:key="amount"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="setAmount(amount)"
|
||||
>
|
||||
¥{{ amount }}
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
<div v-if="rechargeAmount > 0" class="p-4 bg-muted rounded-lg">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span>当前余额</span>
|
||||
<span>¥{{ currentBalance.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-1">
|
||||
<span>充值金额</span>
|
||||
<span class="text-green-600">+¥{{ rechargeAmount.toFixed(2) }}</span>
|
||||
</div>
|
||||
<UiSeparator class="my-2" />
|
||||
<div class="flex justify-between font-medium">
|
||||
<span>充值后余额</span>
|
||||
<span>¥{{ (currentBalance + rechargeAmount).toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
<UiCardFooter class="flex justify-end gap-2">
|
||||
<UiButton variant="outline" @click="goBack">
|
||||
取消
|
||||
</UiButton>
|
||||
<UiButton :disabled="saving || rechargeAmount <= 0" @click="handleSubmit">
|
||||
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
|
||||
确认充值
|
||||
</UiButton>
|
||||
</UiCardFooter>
|
||||
</UiCard>
|
||||
</div>
|
||||
</BasicPage>
|
||||
</template>
|
||||
@@ -1,232 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeft, CheckCircle, Key, Loader2, Pencil, XCircle } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { AgentApp } from '@/pages/admin/agent-apps/data/schema'
|
||||
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import api from '@/services/api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const agentApp = ref<AgentApp | null>(null)
|
||||
|
||||
const agentAppId = computed(() => route.params.id as string)
|
||||
|
||||
function getBillingTypeText(type: string) {
|
||||
const map: Record<string, string> = {
|
||||
subscription: '订阅',
|
||||
time: '计时',
|
||||
point: '点卡',
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
async function fetchAgentApp() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<{ agent_app: AgentApp }>(`/dev/agent-apps/${agentAppId.value}`)
|
||||
if (data?.agent_app) {
|
||||
agentApp.value = data.agent_app
|
||||
}
|
||||
else {
|
||||
toast.error('获取授权信息失败')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取授权信息失败:', error)
|
||||
toast.error('获取授权信息失败')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
|
||||
function goToEdit() {
|
||||
router.push(`/admin/agent-apps/${agentAppId.value}/edit`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchAgentApp()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
title="查看授权"
|
||||
description="查看授权详情信息"
|
||||
:breadcrumbs="[
|
||||
{ title: '授权管理', href: '/admin/agent-apps' },
|
||||
{ title: '查看授权' },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton variant="outline" @click="goBack">
|
||||
<ArrowLeft class="mr-2 h-4 w-4" />
|
||||
返回
|
||||
</UiButton>
|
||||
<UiButton @click="goToEdit">
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
编辑授权
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="agentApp" class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>基本信息</UiCardTitle>
|
||||
<UiCardDescription>授权的基本信息</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="space-y-1">
|
||||
<span class="text-sm text-muted-foreground">代理</span>
|
||||
<p class="font-medium">{{ agentApp.agent_name }}</p>
|
||||
<p v-if="agentApp.agent_email" class="text-xs text-muted-foreground">
|
||||
{{ agentApp.agent_email }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<span class="text-sm text-muted-foreground">应用</span>
|
||||
<p class="font-medium">{{ agentApp.app_name }}</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<span class="text-sm text-muted-foreground">状态</span>
|
||||
<div>
|
||||
<UiBadge :variant="agentApp.status === 'active' ? 'default' : 'secondary'">
|
||||
<CheckCircle v-if="agentApp.status === 'active'" class="mr-1 h-3 w-3" />
|
||||
<XCircle v-else class="mr-1 h-3 w-3" />
|
||||
{{ agentApp.status === 'active' ? '已启用' : '已禁用' }}
|
||||
</UiBadge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<span class="text-sm text-muted-foreground">折扣</span>
|
||||
<p class="font-medium">{{ (agentApp.discount * 10).toFixed(1) }}折</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<span class="text-sm text-muted-foreground">余额</span>
|
||||
<p class="font-medium text-primary">¥{{ (agentApp.balance || 0).toFixed(2) }}</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<span class="text-sm text-muted-foreground">授权时间</span>
|
||||
<p class="font-medium">{{ agentApp.created_at }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Key class="size-5" />
|
||||
卡密权限
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>代理可生成的卡密类型及价格</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div v-if="agentApp.card_types && agentApp.card_types.length > 0">
|
||||
<UiTable>
|
||||
<UiTableHeader>
|
||||
<UiTableRow>
|
||||
<UiTableHead>卡类名称</UiTableHead>
|
||||
<UiTableHead>计费类型</UiTableHead>
|
||||
<UiTableHead>原价</UiTableHead>
|
||||
<UiTableHead>授权价格</UiTableHead>
|
||||
<UiTableHead>状态</UiTableHead>
|
||||
</UiTableRow>
|
||||
</UiTableHeader>
|
||||
<UiTableBody>
|
||||
<UiTableRow v-for="ct in agentApp.card_types" :key="ct.card_type_id || ct.id">
|
||||
<UiTableCell class="font-medium">
|
||||
{{ ct.name }}
|
||||
</UiTableCell>
|
||||
<UiTableCell>
|
||||
<UiBadge variant="outline">
|
||||
{{ getBillingTypeText(ct.billing_type || '') }}
|
||||
</UiBadge>
|
||||
</UiTableCell>
|
||||
<UiTableCell>
|
||||
¥{{ ct.origin_price || 0 }}
|
||||
</UiTableCell>
|
||||
<UiTableCell>
|
||||
¥{{ ct.price || 0 }}
|
||||
</UiTableCell>
|
||||
<UiTableCell>
|
||||
<UiBadge :variant="ct.can_generate ? 'default' : 'secondary'">
|
||||
{{ ct.can_generate ? '已授权' : '未授权' }}
|
||||
</UiBadge>
|
||||
</UiTableCell>
|
||||
</UiTableRow>
|
||||
</UiTableBody>
|
||||
</UiTable>
|
||||
</div>
|
||||
<div v-else class="text-center py-8 text-muted-foreground">
|
||||
暂无卡密权限
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle>统计信息</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">卡类总数</span>
|
||||
<span class="font-medium">{{ agentApp.card_types?.length || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">已授权</span>
|
||||
<span class="font-medium text-green-600">
|
||||
{{ agentApp.card_types?.filter(ct => ct.can_generate).length || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">未授权</span>
|
||||
<span class="font-medium text-muted-foreground">
|
||||
{{ agentApp.card_types?.filter(ct => !ct.can_generate).length || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton class="w-full" @click="goToEdit">
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
编辑授权
|
||||
</UiButton>
|
||||
<UiButton variant="outline" class="w-full" @click="goBack">
|
||||
返回列表
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasicPage>
|
||||
</template>
|
||||
@@ -1,144 +0,0 @@
|
||||
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,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<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,
|
||||
})
|
||||
|
||||
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,349 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckCheck, Eye, Key, Loader2, Share2, X } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import api from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface Agent {
|
||||
id: number
|
||||
username: string
|
||||
email?: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface Application {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
interface CardType {
|
||||
id: number
|
||||
name: string
|
||||
recharge_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_types: [] as Array<{ card_type_id: number, can_generate: boolean, price: number }>,
|
||||
})
|
||||
|
||||
async function fetchAgents() {
|
||||
try {
|
||||
const data = await api.get<{ agents: Agent[] }>('/dev/agents')
|
||||
agents.value = Array.isArray(data?.agents) ? data.agents : []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取代理列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
const data = await api.get<{ applications: Application[] }>('/dev/applications')
|
||||
applications.value = Array.isArray(data?.applications) ? data.applications : []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取应用列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCardTypes(applicationId: string) {
|
||||
if (!applicationId) {
|
||||
cardTypes.value = []
|
||||
form.value.card_types = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await api.get<{ card_types: CardType[] }>(`/dev/card-types?application_id=${applicationId}`)
|
||||
cardTypes.value = Array.isArray(data?.card_types) ? data.card_types : []
|
||||
form.value.card_types = cardTypes.value.map(ct => ({
|
||||
card_type_id: ct.id,
|
||||
can_generate: false,
|
||||
price: ct.price,
|
||||
}))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取卡类列表失败:', error)
|
||||
cardTypes.value = []
|
||||
form.value.card_types = []
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => form.value.application_id, (newVal) => {
|
||||
fetchCardTypes(newVal)
|
||||
})
|
||||
|
||||
const selectedAgent = computed(() => {
|
||||
if (form.value.agent_id) {
|
||||
return agents.value.find(a => String(a.id) === form.value.agent_id)
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const selectedApplication = computed(() => {
|
||||
if (form.value.application_id) {
|
||||
return applications.value.find(app => String(app.id) === form.value.application_id)
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const enabledCardTypesCount = computed(() => {
|
||||
return form.value.card_types.filter(ct => ct.can_generate).length
|
||||
})
|
||||
|
||||
function getRechargeTypeText(type: string) {
|
||||
const map: Record<string, string> = {
|
||||
balance: '余额充值',
|
||||
subscription: '订阅充值',
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
function selectAllCardTypes() {
|
||||
form.value.card_types.forEach(ct => ct.can_generate = true)
|
||||
}
|
||||
|
||||
function clearAllCardTypes() {
|
||||
form.value.card_types.forEach(ct => ct.can_generate = false)
|
||||
}
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return form.value.agent_id && form.value.application_id
|
||||
})
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.agent_id) {
|
||||
toast.error('请选择代理')
|
||||
return
|
||||
}
|
||||
if (!form.value.application_id) {
|
||||
toast.error('请选择应用')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await api.post('/dev/agent-apps/authorize', {
|
||||
agent_id: Number(form.value.agent_id),
|
||||
application_id: Number(form.value.application_id),
|
||||
card_types: form.value.card_types,
|
||||
})
|
||||
toast.success('授权成功')
|
||||
router.push('/admin/agent-apps')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('授权失败:', error)
|
||||
toast.error(error.message || '授权失败')
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchAgents()
|
||||
fetchApplications()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
title="新建授权"
|
||||
description="为代理分配应用卡密权限"
|
||||
:breadcrumbs="[
|
||||
{ title: '授权管理', href: '/admin/agent-apps' },
|
||||
{ title: '新建授权' },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Share2 class="size-5" />
|
||||
基本信息
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>选择要授权的代理和应用</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<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 }}
|
||||
<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="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>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard v-if="cardTypes.length > 0">
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Key class="size-5" />
|
||||
卡密权限
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>配置代理可生成的卡密类型及价格</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="flex gap-2">
|
||||
<UiButton variant="outline" size="sm" @click="selectAllCardTypes">
|
||||
<CheckCheck class="mr-2 h-4 w-4" />
|
||||
全选
|
||||
</UiButton>
|
||||
<UiButton variant="outline" size="sm" @click="clearAllCardTypes">
|
||||
<X class="mr-2 h-4 w-4" />
|
||||
清空
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
<div class="border rounded-lg">
|
||||
<UiTable>
|
||||
<UiTableHeader>
|
||||
<UiTableRow>
|
||||
<UiTableHead class="w-12" />
|
||||
<UiTableHead>卡类名称</UiTableHead>
|
||||
<UiTableHead>充值类型</UiTableHead>
|
||||
<UiTableHead>原价</UiTableHead>
|
||||
<UiTableHead>授权价格</UiTableHead>
|
||||
</UiTableRow>
|
||||
</UiTableHeader>
|
||||
<UiTableBody>
|
||||
<UiTableRow
|
||||
v-for="(ct, index) in form.card_types"
|
||||
:key="ct.card_type_id"
|
||||
:class="ct.can_generate ? 'bg-primary/10' : ''"
|
||||
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">
|
||||
{{ getRechargeTypeText(cardTypes[index]?.recharge_type || '') }}
|
||||
</UiBadge>
|
||||
</UiTableCell>
|
||||
<UiTableCell>
|
||||
¥{{ cardTypes[index]?.price || 0 }}
|
||||
</UiTableCell>
|
||||
<UiTableCell @click.stop>
|
||||
<UiNumberField v-model="ct.price" :min="0" class="w-28">
|
||||
<UiNumberFieldContent>
|
||||
<UiNumberFieldDecrement />
|
||||
<UiNumberFieldInput />
|
||||
<UiNumberFieldIncrement />
|
||||
</UiNumberFieldContent>
|
||||
</UiNumberField>
|
||||
</UiTableCell>
|
||||
</UiTableRow>
|
||||
</UiTableBody>
|
||||
</UiTable>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Eye class="size-5" />
|
||||
预览
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">代理</span>
|
||||
<span>{{ selectedAgent?.username || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">应用</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div v-if="cardTypes.length > 0" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">卡密权限</span>
|
||||
<span>{{ enabledCardTypesCount }}/{{ cardTypes.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="saving || !isFormValid"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Share2 v-else class="mr-2 h-4 w-4" />
|
||||
确认授权
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
取消
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasicPage>
|
||||
</template>
|
||||
@@ -1,30 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const agentAppStatusSchema = z.enum(['active', 'inactive'])
|
||||
|
||||
export const cardTypeSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
card_type_id: z.number(),
|
||||
name: z.string().optional(),
|
||||
billing_type: z.string().optional(),
|
||||
can_generate: z.boolean(),
|
||||
price: z.number().optional(),
|
||||
origin_price: z.number().optional(),
|
||||
})
|
||||
|
||||
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(cardTypeSchema).optional(),
|
||||
created_at: z.string(),
|
||||
})
|
||||
|
||||
export type CardTypeAuth = z.infer<typeof cardTypeSchema>
|
||||
export type AgentApp = z.infer<typeof agentAppSchema>
|
||||
@@ -1,254 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckCircle, Key, Plus, Share2, XCircle } from 'lucide-vue-next'
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { AgentApp } from '@/pages/admin/agent-apps/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/admin/agent-apps/components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const agentApps = ref<AgentApp[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
const statusFilter = ref<string>('')
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteTarget = ref<AgentApp | null>(null)
|
||||
const batchDeleteDialogOpen = ref(false)
|
||||
const batchDeleteIds = ref<(string | number)[]>([])
|
||||
|
||||
const filteredApps = computed(() => {
|
||||
let result = agentApps.value
|
||||
|
||||
if (statusFilter.value) {
|
||||
result = result.filter(app => app.status === statusFilter.value)
|
||||
}
|
||||
|
||||
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() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<{ agent_apps: AgentApp[] }>('/dev/agent-apps')
|
||||
agentApps.value = data?.agent_apps || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
toast.error('获取授权列表失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToCreate() {
|
||||
router.push('/admin/agent-apps/create')
|
||||
}
|
||||
|
||||
function handleView(item: AgentApp) {
|
||||
router.push(`/admin/agent-apps/${item.id}/view`)
|
||||
}
|
||||
|
||||
function handleEdit(item: AgentApp) {
|
||||
router.push(`/admin/agent-apps/${item.id}/edit`)
|
||||
}
|
||||
|
||||
function confirmDelete(item: AgentApp) {
|
||||
deleteTarget.value = item
|
||||
deleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!deleteTarget.value)
|
||||
return
|
||||
|
||||
try {
|
||||
await api.delete(`/dev/agent-apps/${deleteTarget.value.id}`)
|
||||
toast.success('移除授权成功')
|
||||
fetchData()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('移除失败:', error)
|
||||
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(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
title="授权管理"
|
||||
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-4">
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
授权总数
|
||||
</UiCardTitle>
|
||||
<Share2 class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ filteredApps.length }}
|
||||
</div>
|
||||
</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">
|
||||
{{ activeCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
已禁用
|
||||
</UiCardTitle>
|
||||
<XCircle class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ inactiveCount }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
代理余额总计
|
||||
</UiCardTitle>
|
||||
<Key class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
¥{{ totalBalance.toFixed(2) }}
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<DataTable
|
||||
ref="tableRef"
|
||||
:loading
|
||||
:data="filteredApps"
|
||||
:on-view="handleView"
|
||||
:on-edit="handleEdit"
|
||||
:on-delete="confirmDelete"
|
||||
@refresh="fetchData"
|
||||
@batch-delete="confirmBatchDelete"
|
||||
>
|
||||
<template #filters>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
title="状态"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</template>
|
||||
</DataTable>
|
||||
</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>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="batchDeleteDialogOpen"
|
||||
destructive
|
||||
confirm-button-text="确定移除"
|
||||
cancel-button-text="取消"
|
||||
@confirm="handleBatchDelete"
|
||||
>
|
||||
<template #title>
|
||||
批量移除授权
|
||||
</template>
|
||||
<template #description>
|
||||
确定要移除选中的 {{ batchDeleteIds.length }} 项授权吗?
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, Loader2, Receipt, Settings2, UserPlus } from 'lucide-vue-next'
|
||||
import { Ban, Check, CheckCircle, Loader2, Plus, Receipt, Settings2, Trash2, UserPlus, X } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -19,10 +19,44 @@ 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: '',
|
||||
@@ -65,6 +99,109 @@ 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)
|
||||
@@ -114,6 +251,8 @@ async function handleSave() {
|
||||
onMounted(() => {
|
||||
fetchAgents()
|
||||
fetchAgent()
|
||||
fetchAuthApps()
|
||||
fetchApplications()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -247,6 +386,78 @@ 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>
|
||||
<tr v-for="app in authApps" :key="app.id" class="border-t">
|
||||
<td class="p-3 font-medium">{{ app.app_name }}</td>
|
||||
<td class="p-3">
|
||||
<div v-if="app.card_types && app.card_types.length > 0" class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="ct in app.card_types"
|
||||
:key="ct.card_type_id"
|
||||
class="inline-flex items-center px-2 py-0.5 rounded text-xs"
|
||||
:class="ct.can_generate ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'"
|
||||
>
|
||||
{{ ct.name || `卡类#${ct.card_type_id}` }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="text-muted-foreground">-</span>
|
||||
</td>
|
||||
<td class="p-3 font-medium">¥{{ (app.balance || 0).toFixed(2) }}</td>
|
||||
<td class="p-3">
|
||||
<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' : 'bg-gray-100 text-gray-500'"
|
||||
@click="handleToggleAuthStatus(app)"
|
||||
>
|
||||
{{ app.status === 'active' ? '已启用' : '已禁用' }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="p-3 text-right">
|
||||
<UiButton variant="ghost" size="icon" class="text-destructive" @click="handleRemoveAuth(app)">
|
||||
<Trash2 class="size-4" />
|
||||
</UiButton>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
@@ -309,5 +520,73 @@ 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>
|
||||
|
||||
@@ -167,18 +167,6 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/pages/admin/sessions/index.vue'),
|
||||
meta: { title: '在线实例 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'agent-apps',
|
||||
name: 'AdminAgentApps',
|
||||
component: () => import('@/pages/admin/agent-apps/index.vue'),
|
||||
meta: { title: '授权管理 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'agent-apps/create',
|
||||
name: 'AdminAgentAppsCreate',
|
||||
component: () => import('@/pages/admin/agent-apps/create.vue'),
|
||||
meta: { title: '新建授权 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'agents',
|
||||
name: 'AdminAgents',
|
||||
@@ -197,24 +185,6 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/pages/admin/agents/[id].vue'),
|
||||
meta: { title: '编辑代理 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'agent-apps/:id/edit',
|
||||
name: 'AdminAgentAppEdit',
|
||||
component: () => import('@/pages/admin/agent-apps/[id]/edit.vue'),
|
||||
meta: { title: '编辑授权 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'agent-apps/:id/view',
|
||||
name: 'AdminAgentAppView',
|
||||
component: () => import('@/pages/admin/agent-apps/[id]/view.vue'),
|
||||
meta: { title: '查看授权 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'agent-apps/:id/recharge',
|
||||
name: 'AdminAgentAppRecharge',
|
||||
component: () => import('@/pages/admin/agent-apps/[id]/recharge.vue'),
|
||||
meta: { title: '充值余额 - 管理后台' },
|
||||
},
|
||||
{
|
||||
path: 'finance',
|
||||
name: 'AdminFinance',
|
||||
|
||||
Reference in New Issue
Block a user