feat: 优化授权管理页面卡类权限交互方式

将批量表格选择改为逐个添加卡类的交互,每个卡类对应一条独立权限记录

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 10:07:07 +08:00
parent 17d7d4c68d
commit 38f6788062
2 changed files with 212 additions and 135 deletions
+104 -64
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Check, Eye, Info, Loader2, Shield } from 'lucide-vue-next' import { Check, Eye, Info, Loader2, Plus, X } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref } 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'
@@ -29,6 +29,13 @@ interface CardType {
price: number price: number
} }
interface PermissionItem {
card_type_id: number
card_type_name: string
billing_type: string
price: number
}
const agentAppId = computed(() => route.params.id as string) const agentAppId = computed(() => route.params.id as string)
const loading = ref(false) const loading = ref(false)
const saving = ref(false) const saving = ref(false)
@@ -41,10 +48,11 @@ const form = ref({
agent_id: '', agent_id: '',
application_id: '', application_id: '',
remark: '', remark: '',
card_type_prices: {} as Record<string, number>,
card_type_auth: {} as Record<string, boolean>,
}) })
const selectedCardTypeId = ref<string>('')
const permissions = ref<PermissionItem[]>([])
const selectedAgent = computed(() => { const selectedAgent = computed(() => {
if (form.value.agent_id) { if (form.value.agent_id) {
return agents.value.find(a => String(a.id) === form.value.agent_id) return agents.value.find(a => String(a.id) === form.value.agent_id)
@@ -59,10 +67,6 @@ const selectedApplication = computed(() => {
return null return null
}) })
const authorizedCount = computed(() => {
return Object.values(form.value.card_type_auth).filter(Boolean).length
})
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')
@@ -107,16 +111,15 @@ async function fetchAgentApp() {
await fetchCardTypes(String(agentApp.application_id)) await fetchCardTypes(String(agentApp.application_id))
cardTypes.value.forEach((ct) => { permissions.value = agentApp.card_types?.map((c: any) => {
const existingCard = agentApp.card_types?.find((c: any) => c.card_type_id === ct.id) const cardType = cardTypes.value.find(ct => ct.id === c.card_type_id)
if (existingCard) { return {
form.value.card_type_prices[String(ct.id)] = existingCard.price || ct.price card_type_id: c.card_type_id,
form.value.card_type_auth[String(ct.id)] = existingCard.can_generate card_type_name: cardType?.name || `卡类 #${c.card_type_id}`,
} else { billing_type: cardType?.billing_type || '',
form.value.card_type_prices[String(ct.id)] = ct.price price: c.price || 0,
form.value.card_type_auth[String(ct.id)] = false
} }
}) }) || []
} }
} }
catch (error) { catch (error) {
@@ -128,6 +131,33 @@ async function fetchAgentApp() {
} }
} }
function addPermission() {
if (!selectedCardTypeId.value) {
toast.error('请选择卡类')
return
}
const cardType = cardTypes.value.find(ct => String(ct.id) === selectedCardTypeId.value)
if (!cardType) return
if (permissions.value.some(p => p.card_type_id === cardType.id)) {
toast.error('该卡类已添加')
return
}
permissions.value.push({
card_type_id: cardType.id,
card_type_name: cardType.name,
billing_type: cardType.billing_type,
price: cardType.price,
})
selectedCardTypeId.value = ''
}
function removePermission(index: number) {
permissions.value.splice(index, 1)
}
async function handleSave() { async function handleSave() {
if (!form.value.agent_id) { if (!form.value.agent_id) {
toast.error('请选择代理') toast.error('请选择代理')
@@ -139,14 +169,17 @@ async function handleSave() {
return return
} }
if (permissions.value.length === 0) {
toast.error('请至少添加一个卡类权限')
return
}
saving.value = true saving.value = true
try { try {
const cardTypesPayload = cardTypes.value.map(ct => ({ const cardTypesPayload = permissions.value.map(p => ({
card_type_id: ct.id, card_type_id: p.card_type_id,
can_generate: form.value.card_type_auth[String(ct.id)] || false, can_generate: true,
price: form.value.card_type_prices[String(ct.id)] !== undefined price: Number(p.price),
? Number(form.value.card_type_prices[String(ct.id)])
: ct.price,
})) }))
await api.put(`/dev/agent-apps/${agentAppId.value}`, { await api.put(`/dev/agent-apps/${agentAppId.value}`, {
@@ -242,50 +275,57 @@ onMounted(() => {
<UiCard v-if="form.application_id && cardTypes.length > 0"> <UiCard v-if="form.application_id && cardTypes.length > 0">
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Shield class="size-5" /> <Plus class="size-5" />
卡类权限 卡类权限
</UiCardTitle> </UiCardTitle>
<UiCardDescription>配置代理可以生成的卡类和价格</UiCardDescription> <UiCardDescription>为代理添加卡类授权</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent class="space-y-4">
<div class="border rounded-lg overflow-hidden"> <div class="flex gap-3">
<div class="overflow-x-auto"> <div class="flex-1">
<table class="w-full text-sm"> <UiSelect v-model="selectedCardTypeId">
<thead class="bg-muted/50"> <UiSelectTrigger>
<tr> <UiSelectValue placeholder="选择卡类" />
<th class="text-left p-3 font-medium w-12">授权</th> </UiSelectTrigger>
<th class="text-left p-3 font-medium">卡类名称</th> <UiSelectContent>
<th class="text-left p-3 font-medium">计费类型</th> <UiSelectItem
<th class="text-right p-3 font-medium w-32">代理价格</th> v-for="ct in cardTypes.filter(c => !permissions.some(p => p.card_type_id === c.id))"
</tr> :key="ct.id"
</thead> :value="String(ct.id)"
<tbody> >
<tr v-for="ct in cardTypes" :key="ct.id" class="border-t hover:bg-muted/30 transition-colors"> {{ ct.name }} ({{ ct.billing_type }})
<td class="p-3"> </UiSelectItem>
<UiCheckbox </UiSelectContent>
:checked="form.card_type_auth[String(ct.id)] ?? true" </UiSelect>
@update:checked="form.card_type_auth[String(ct.id)] = $event" </div>
/> <UiButton @click="addPermission">
</td> <Plus class="mr-1 h-4 w-4" />
<td class="p-3 font-medium">{{ ct.name }}</td> 添加
<td class="p-3 text-muted-foreground">{{ ct.billing_type }}</td> </UiButton>
<td class="p-3"> </div>
<div class="flex items-center justify-end gap-1">
<span class="text-muted-foreground">¥</span> <div v-if="permissions.length > 0" class="border rounded-lg divide-y">
<UiInput <div v-for="(p, index) in permissions" :key="p.card_type_id" class="flex items-center justify-between p-3">
class="w-20 text-right" <div class="flex-1">
:model-value="String(form.card_type_prices[String(ct.id)] ?? ct.price)" <div class="font-medium">{{ p.card_type_name }}</div>
@input="form.card_type_prices[String(ct.id)] = Number(($event.target as HTMLInputElement).value)" <div class="text-sm text-muted-foreground">{{ p.billing_type }}</div>
/> </div>
</div> <div class="flex items-center gap-2">
</td> <div class="flex items-center gap-1">
</tr> <span class="text-muted-foreground">¥</span>
</tbody> <UiInput
</table> class="w-20 text-right"
v-model.number="p.price"
/>
</div>
<UiButton variant="ghost" size="icon" @click="removePermission(index)">
<X class="h-4 w-4" />
</UiButton>
</div>
</div> </div>
</div> </div>
<p class="text-xs text-muted-foreground mt-3"> <p v-else class="text-sm text-muted-foreground text-center py-4">
提示取消勾选"授权"列可以禁止代理生成该类型的卡密 请选择卡类添加到授权列表
</p> </p>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -311,7 +351,7 @@ onMounted(() => {
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
<span class="text-muted-foreground">授权卡类</span> <span class="text-muted-foreground">授权卡类</span>
<span>{{ authorizedCount }} / {{ cardTypes.length }}</span> <span>{{ permissions.length }} </span>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
<span class="text-muted-foreground">备注</span> <span class="text-muted-foreground">备注</span>
@@ -327,7 +367,7 @@ onMounted(() => {
<UiButton <UiButton
class="w-full" class="w-full"
size="lg" size="lg"
:disabled="saving || !form.agent_id || !form.application_id" :disabled="saving || !form.agent_id || !form.application_id || permissions.length === 0"
@click="handleSave" @click="handleSave"
> >
<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" />
+101 -64
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { CheckCircle, Eye, Info, Loader2, Plus, Shield } from 'lucide-vue-next' import { CheckCircle, Eye, Info, Loader2, Plus, X } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref } 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'
@@ -28,6 +28,13 @@ interface CardType {
price: number price: number
} }
interface PermissionItem {
card_type_id: number
card_type_name: string
billing_type: string
price: number
}
const saving = ref(false) const saving = ref(false)
const agents = ref<Agent[]>([]) const agents = ref<Agent[]>([])
const applications = ref<Application[]>([]) const applications = ref<Application[]>([])
@@ -37,10 +44,11 @@ const form = ref({
agent_id: '', agent_id: '',
application_id: '', application_id: '',
remark: '', remark: '',
card_type_prices: {} as Record<string, number>,
card_type_auth: {} as Record<string, boolean>,
}) })
const selectedCardTypeId = ref<string>('')
const permissions = ref<PermissionItem[]>([])
const selectedAgent = computed(() => { const selectedAgent = computed(() => {
if (form.value.agent_id) { if (form.value.agent_id) {
return agents.value.find(a => String(a.id) === form.value.agent_id) return agents.value.find(a => String(a.id) === form.value.agent_id)
@@ -55,8 +63,11 @@ const selectedApplication = computed(() => {
return null return null
}) })
const authorizedCount = computed(() => { const selectedCardType = computed(() => {
return Object.values(form.value.card_type_auth).filter(Boolean).length if (selectedCardTypeId.value) {
return cardTypes.value.find(ct => String(ct.id) === selectedCardTypeId.value)
}
return null
}) })
async function fetchAgents() { async function fetchAgents() {
@@ -82,22 +93,38 @@ async function fetchCardTypes(appId: string) {
try { try {
const data = await api.get<{ card_types: CardType[] }>(`/dev/card-types?application_id=${appId}`) const data = await api.get<{ card_types: CardType[] }>(`/dev/card-types?application_id=${appId}`)
cardTypes.value = data?.card_types || [] 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 { catch {
cardTypes.value = [] cardTypes.value = []
} }
} }
watch(() => form.value.application_id, (appId) => { function addPermission() {
cardTypes.value = [] if (!selectedCardTypeId.value) {
form.value.card_type_prices = {} toast.error('请选择卡类')
form.value.card_type_auth = {} return
if (appId) fetchCardTypes(appId) }
})
const cardType = cardTypes.value.find(ct => String(ct.id) === selectedCardTypeId.value)
if (!cardType) return
if (permissions.value.some(p => p.card_type_id === cardType.id)) {
toast.error('该卡类已添加')
return
}
permissions.value.push({
card_type_id: cardType.id,
card_type_name: cardType.name,
billing_type: cardType.billing_type,
price: cardType.price,
})
selectedCardTypeId.value = ''
}
function removePermission(index: number) {
permissions.value.splice(index, 1)
}
async function handleSave() { async function handleSave() {
if (!form.value.agent_id) { if (!form.value.agent_id) {
@@ -110,14 +137,17 @@ async function handleSave() {
return return
} }
if (permissions.value.length === 0) {
toast.error('请至少添加一个卡类权限')
return
}
saving.value = true saving.value = true
try { try {
const cardTypesPayload = cardTypes.value.map(ct => ({ const cardTypesPayload = permissions.value.map(p => ({
card_type_id: ct.id, card_type_id: p.card_type_id,
can_generate: form.value.card_type_auth[String(ct.id)] || false, can_generate: true,
price: form.value.card_type_prices[String(ct.id)] !== undefined price: Number(p.price),
? Number(form.value.card_type_prices[String(ct.id)])
: ct.price,
})) }))
await api.post('/dev/agent-apps', { await api.post('/dev/agent-apps', {
@@ -182,7 +212,7 @@ onMounted(() => {
<div class="space-y-2"> <div class="space-y-2">
<UiLabel>选择应用 <span class="text-destructive">*</span></UiLabel> <UiLabel>选择应用 <span class="text-destructive">*</span></UiLabel>
<UiSelect v-model="form.application_id"> <UiSelect v-model="form.application_id" @update:model-value="fetchCardTypes($event)">
<UiSelectTrigger> <UiSelectTrigger>
<UiSelectValue placeholder="请选择应用" /> <UiSelectValue placeholder="请选择应用" />
</UiSelectTrigger> </UiSelectTrigger>
@@ -208,50 +238,57 @@ onMounted(() => {
<UiCard v-if="form.application_id && cardTypes.length > 0"> <UiCard v-if="form.application_id && cardTypes.length > 0">
<UiCardHeader> <UiCardHeader>
<UiCardTitle class="flex items-center gap-2"> <UiCardTitle class="flex items-center gap-2">
<Shield class="size-5" /> <CheckCircle class="size-5" />
卡类权限 卡类权限
</UiCardTitle> </UiCardTitle>
<UiCardDescription>配置代理可以生成的卡类和价格</UiCardDescription> <UiCardDescription>为代理添加卡类授权</UiCardDescription>
</UiCardHeader> </UiCardHeader>
<UiCardContent> <UiCardContent class="space-y-4">
<div class="border rounded-lg overflow-hidden"> <div class="flex gap-3">
<div class="overflow-x-auto"> <div class="flex-1">
<table class="w-full text-sm"> <UiSelect v-model="selectedCardTypeId">
<thead class="bg-muted/50"> <UiSelectTrigger>
<tr> <UiSelectValue placeholder="选择卡类" />
<th class="text-left p-3 font-medium w-12">授权</th> </UiSelectTrigger>
<th class="text-left p-3 font-medium">卡类名称</th> <UiSelectContent>
<th class="text-left p-3 font-medium">计费类型</th> <UiSelectItem
<th class="text-right p-3 font-medium w-32">代理价格</th> v-for="ct in cardTypes.filter(c => !permissions.some(p => p.card_type_id === c.id))"
</tr> :key="ct.id"
</thead> :value="String(ct.id)"
<tbody> >
<tr v-for="ct in cardTypes" :key="ct.id" class="border-t hover:bg-muted/30 transition-colors"> {{ ct.name }} ({{ ct.billing_type }})
<td class="p-3"> </UiSelectItem>
<UiCheckbox </UiSelectContent>
:checked="form.card_type_auth[String(ct.id)] ?? true" </UiSelect>
@update:checked="form.card_type_auth[String(ct.id)] = $event" </div>
/> <UiButton @click="addPermission">
</td> <Plus class="mr-1 h-4 w-4" />
<td class="p-3 font-medium">{{ ct.name }}</td> 添加
<td class="p-3 text-muted-foreground">{{ ct.billing_type }}</td> </UiButton>
<td class="p-3"> </div>
<div class="flex items-center justify-end gap-1">
<span class="text-muted-foreground">¥</span> <div v-if="permissions.length > 0" class="border rounded-lg divide-y">
<UiInput <div v-for="(p, index) in permissions" :key="p.card_type_id" class="flex items-center justify-between p-3">
class="w-20 text-right" <div class="flex-1">
:model-value="String(form.card_type_prices[String(ct.id)] ?? ct.price)" <div class="font-medium">{{ p.card_type_name }}</div>
@input="form.card_type_prices[String(ct.id)] = Number(($event.target as HTMLInputElement).value)" <div class="text-sm text-muted-foreground">{{ p.billing_type }}</div>
/> </div>
</div> <div class="flex items-center gap-2">
</td> <div class="flex items-center gap-1">
</tr> <span class="text-muted-foreground">¥</span>
</tbody> <UiInput
</table> class="w-20 text-right"
v-model.number="p.price"
/>
</div>
<UiButton variant="ghost" size="icon" @click="removePermission(index)">
<X class="h-4 w-4" />
</UiButton>
</div>
</div> </div>
</div> </div>
<p class="text-xs text-muted-foreground mt-3"> <p v-else class="text-sm text-muted-foreground text-center py-4">
提示取消勾选"授权"列可以禁止代理生成该类型的卡密 请选择卡类添加到授权列表
</p> </p>
</UiCardContent> </UiCardContent>
</UiCard> </UiCard>
@@ -277,7 +314,7 @@ onMounted(() => {
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
<span class="text-muted-foreground">授权卡类</span> <span class="text-muted-foreground">授权卡类</span>
<span>{{ authorizedCount }} / {{ cardTypes.length }}</span> <span>{{ permissions.length }} </span>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
<span class="text-muted-foreground">备注</span> <span class="text-muted-foreground">备注</span>
@@ -293,7 +330,7 @@ onMounted(() => {
<UiButton <UiButton
class="w-full" class="w-full"
size="lg" size="lg"
:disabled="saving || !form.agent_id || !form.application_id" :disabled="saving || !form.agent_id || !form.application_id || permissions.length === 0"
@click="handleSave" @click="handleSave"
> >
<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" />