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
+110 -70
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { Check, Eye, Info, Loader2, Shield } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue'
import { Check, Eye, Info, Loader2, Plus, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -29,6 +29,13 @@ interface CardType {
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 loading = ref(false)
const saving = ref(false)
@@ -41,10 +48,11 @@ const form = ref({
agent_id: '',
application_id: '',
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(() => {
if (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
})
const authorizedCount = computed(() => {
return Object.values(form.value.card_type_auth).filter(Boolean).length
})
async function fetchAgents() {
try {
const data = await api.get<{ agents?: Agent[] }>('/dev/agents')
@@ -94,29 +98,28 @@ async function fetchCardTypes(appId: string) {
async function fetchAgentApp() {
if (!agentAppId.value) return
loading.value = true
try {
const data = await api.get<{ agent_app?: any }>(`/dev/agent-apps/${agentAppId.value}`)
const agentApp = data?.agent_app || data
if (agentApp) {
form.value.agent_id = String(agentApp.agent_id)
form.value.application_id = String(agentApp.application_id)
form.value.remark = agentApp.remark || ''
await fetchCardTypes(String(agentApp.application_id))
cardTypes.value.forEach((ct) => {
const existingCard = agentApp.card_types?.find((c: any) => c.card_type_id === ct.id)
if (existingCard) {
form.value.card_type_prices[String(ct.id)] = existingCard.price || ct.price
form.value.card_type_auth[String(ct.id)] = existingCard.can_generate
} else {
form.value.card_type_prices[String(ct.id)] = ct.price
form.value.card_type_auth[String(ct.id)] = false
permissions.value = agentApp.card_types?.map((c: any) => {
const cardType = cardTypes.value.find(ct => ct.id === c.card_type_id)
return {
card_type_id: c.card_type_id,
card_type_name: cardType?.name || `卡类 #${c.card_type_id}`,
billing_type: cardType?.billing_type || '',
price: c.price || 0,
}
})
}) || []
}
}
catch (error) {
@@ -128,25 +131,55 @@ 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() {
if (!form.value.agent_id) {
toast.error('请选择代理')
return
}
if (!form.value.application_id) {
toast.error('请选择应用')
return
}
if (permissions.value.length === 0) {
toast.error('请至少添加一个卡类权限')
return
}
saving.value = true
try {
const cardTypesPayload = cardTypes.value.map(ct => ({
card_type_id: ct.id,
can_generate: form.value.card_type_auth[String(ct.id)] || false,
price: form.value.card_type_prices[String(ct.id)] !== undefined
? Number(form.value.card_type_prices[String(ct.id)])
: ct.price,
const cardTypesPayload = permissions.value.map(p => ({
card_type_id: p.card_type_id,
can_generate: true,
price: Number(p.price),
}))
await api.put(`/dev/agent-apps/${agentAppId.value}`, {
@@ -242,50 +275,57 @@ onMounted(() => {
<UiCard v-if="form.application_id && cardTypes.length > 0">
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Shield class="size-5" />
<Plus class="size-5" />
卡类权限
</UiCardTitle>
<UiCardDescription>配置代理可以生成的卡类和价格</UiCardDescription>
<UiCardDescription>为代理添加卡类授权</UiCardDescription>
</UiCardHeader>
<UiCardContent>
<div class="border rounded-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="text-left p-3 font-medium w-12">授权</th>
<th class="text-left p-3 font-medium">卡类名称</th>
<th class="text-left p-3 font-medium">计费类型</th>
<th class="text-right p-3 font-medium w-32">代理价格</th>
</tr>
</thead>
<tbody>
<tr v-for="ct in cardTypes" :key="ct.id" class="border-t hover:bg-muted/30 transition-colors">
<td class="p-3">
<UiCheckbox
:checked="form.card_type_auth[String(ct.id)] ?? true"
@update:checked="form.card_type_auth[String(ct.id)] = $event"
/>
</td>
<td class="p-3 font-medium">{{ ct.name }}</td>
<td class="p-3 text-muted-foreground">{{ ct.billing_type }}</td>
<td class="p-3">
<div class="flex items-center justify-end gap-1">
<span class="text-muted-foreground">¥</span>
<UiInput
class="w-20 text-right"
:model-value="String(form.card_type_prices[String(ct.id)] ?? ct.price)"
@input="form.card_type_prices[String(ct.id)] = Number(($event.target as HTMLInputElement).value)"
/>
</div>
</td>
</tr>
</tbody>
</table>
<UiCardContent class="space-y-4">
<div class="flex gap-3">
<div class="flex-1">
<UiSelect v-model="selectedCardTypeId">
<UiSelectTrigger>
<UiSelectValue placeholder="选择卡类" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
v-for="ct in cardTypes.filter(c => !permissions.some(p => p.card_type_id === c.id))"
:key="ct.id"
:value="String(ct.id)"
>
{{ ct.name }} ({{ ct.billing_type }})
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<UiButton @click="addPermission">
<Plus class="mr-1 h-4 w-4" />
添加
</UiButton>
</div>
<div v-if="permissions.length > 0" class="border rounded-lg divide-y">
<div v-for="(p, index) in permissions" :key="p.card_type_id" class="flex items-center justify-between p-3">
<div class="flex-1">
<div class="font-medium">{{ p.card_type_name }}</div>
<div class="text-sm text-muted-foreground">{{ p.billing_type }}</div>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-1">
<span class="text-muted-foreground">¥</span>
<UiInput
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>
<p class="text-xs text-muted-foreground mt-3">
提示取消勾选"授权"列可以禁止代理生成该类型的卡密
<p v-else class="text-sm text-muted-foreground text-center py-4">
请选择卡类添加到授权列表
</p>
</UiCardContent>
</UiCard>
@@ -311,7 +351,7 @@ onMounted(() => {
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">授权卡类</span>
<span>{{ authorizedCount }} / {{ cardTypes.length }}</span>
<span>{{ permissions.length }} </span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">备注</span>
@@ -327,7 +367,7 @@ onMounted(() => {
<UiButton
class="w-full"
size="lg"
:disabled="saving || !form.agent_id || !form.application_id"
:disabled="saving || !form.agent_id || !form.application_id || permissions.length === 0"
@click="handleSave"
>
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
@@ -348,4 +388,4 @@ onMounted(() => {
</div>
</div>
</BasicPage>
</template>
</template>
+102 -65
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { CheckCircle, Eye, Info, Loader2, Plus, Shield } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue'
import { CheckCircle, Eye, Info, Loader2, Plus, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -28,6 +28,13 @@ interface CardType {
price: number
}
interface PermissionItem {
card_type_id: number
card_type_name: string
billing_type: string
price: number
}
const saving = ref(false)
const agents = ref<Agent[]>([])
const applications = ref<Application[]>([])
@@ -37,10 +44,11 @@ const form = ref({
agent_id: '',
application_id: '',
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(() => {
if (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
})
const authorizedCount = computed(() => {
return Object.values(form.value.card_type_auth).filter(Boolean).length
const selectedCardType = computed(() => {
if (selectedCardTypeId.value) {
return cardTypes.value.find(ct => String(ct.id) === selectedCardTypeId.value)
}
return null
})
async function fetchAgents() {
@@ -82,42 +93,61 @@ async function fetchCardTypes(appId: string) {
try {
const data = await api.get<{ card_types: CardType[] }>(`/dev/card-types?application_id=${appId}`)
cardTypes.value = data?.card_types || []
cardTypes.value.forEach((ct) => {
form.value.card_type_prices[String(ct.id)] = ct.price
form.value.card_type_auth[String(ct.id)] = true
})
}
catch {
cardTypes.value = []
}
}
watch(() => form.value.application_id, (appId) => {
cardTypes.value = []
form.value.card_type_prices = {}
form.value.card_type_auth = {}
if (appId) fetchCardTypes(appId)
})
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() {
if (!form.value.agent_id) {
toast.error('请选择代理')
return
}
if (!form.value.application_id) {
toast.error('请选择应用')
return
}
if (permissions.value.length === 0) {
toast.error('请至少添加一个卡类权限')
return
}
saving.value = true
try {
const cardTypesPayload = cardTypes.value.map(ct => ({
card_type_id: ct.id,
can_generate: form.value.card_type_auth[String(ct.id)] || false,
price: form.value.card_type_prices[String(ct.id)] !== undefined
? Number(form.value.card_type_prices[String(ct.id)])
: ct.price,
const cardTypesPayload = permissions.value.map(p => ({
card_type_id: p.card_type_id,
can_generate: true,
price: Number(p.price),
}))
await api.post('/dev/agent-apps', {
@@ -182,7 +212,7 @@ onMounted(() => {
<div class="space-y-2">
<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>
<UiSelectValue placeholder="请选择应用" />
</UiSelectTrigger>
@@ -208,50 +238,57 @@ onMounted(() => {
<UiCard v-if="form.application_id && cardTypes.length > 0">
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Shield class="size-5" />
<CheckCircle class="size-5" />
卡类权限
</UiCardTitle>
<UiCardDescription>配置代理可以生成的卡类和价格</UiCardDescription>
<UiCardDescription>为代理添加卡类授权</UiCardDescription>
</UiCardHeader>
<UiCardContent>
<div class="border rounded-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="text-left p-3 font-medium w-12">授权</th>
<th class="text-left p-3 font-medium">卡类名称</th>
<th class="text-left p-3 font-medium">计费类型</th>
<th class="text-right p-3 font-medium w-32">代理价格</th>
</tr>
</thead>
<tbody>
<tr v-for="ct in cardTypes" :key="ct.id" class="border-t hover:bg-muted/30 transition-colors">
<td class="p-3">
<UiCheckbox
:checked="form.card_type_auth[String(ct.id)] ?? true"
@update:checked="form.card_type_auth[String(ct.id)] = $event"
/>
</td>
<td class="p-3 font-medium">{{ ct.name }}</td>
<td class="p-3 text-muted-foreground">{{ ct.billing_type }}</td>
<td class="p-3">
<div class="flex items-center justify-end gap-1">
<span class="text-muted-foreground">¥</span>
<UiInput
class="w-20 text-right"
:model-value="String(form.card_type_prices[String(ct.id)] ?? ct.price)"
@input="form.card_type_prices[String(ct.id)] = Number(($event.target as HTMLInputElement).value)"
/>
</div>
</td>
</tr>
</tbody>
</table>
<UiCardContent class="space-y-4">
<div class="flex gap-3">
<div class="flex-1">
<UiSelect v-model="selectedCardTypeId">
<UiSelectTrigger>
<UiSelectValue placeholder="选择卡类" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
v-for="ct in cardTypes.filter(c => !permissions.some(p => p.card_type_id === c.id))"
:key="ct.id"
:value="String(ct.id)"
>
{{ ct.name }} ({{ ct.billing_type }})
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<UiButton @click="addPermission">
<Plus class="mr-1 h-4 w-4" />
添加
</UiButton>
</div>
<div v-if="permissions.length > 0" class="border rounded-lg divide-y">
<div v-for="(p, index) in permissions" :key="p.card_type_id" class="flex items-center justify-between p-3">
<div class="flex-1">
<div class="font-medium">{{ p.card_type_name }}</div>
<div class="text-sm text-muted-foreground">{{ p.billing_type }}</div>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-1">
<span class="text-muted-foreground">¥</span>
<UiInput
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>
<p class="text-xs text-muted-foreground mt-3">
提示取消勾选"授权"列可以禁止代理生成该类型的卡密
<p v-else class="text-sm text-muted-foreground text-center py-4">
请选择卡类添加到授权列表
</p>
</UiCardContent>
</UiCard>
@@ -277,7 +314,7 @@ onMounted(() => {
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">授权卡类</span>
<span>{{ authorizedCount }} / {{ cardTypes.length }}</span>
<span>{{ permissions.length }} </span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">备注</span>
@@ -293,7 +330,7 @@ onMounted(() => {
<UiButton
class="w-full"
size="lg"
:disabled="saving || !form.agent_id || !form.application_id"
:disabled="saving || !form.agent_id || !form.application_id || permissions.length === 0"
@click="handleSave"
>
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />