feat: 优化授权管理页面卡类权限交互方式
将批量表格选择改为逐个添加卡类的交互,每个卡类对应一条独立权限记录 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user