fix: 修复代理编辑页面翻译问题和代理卡密生成页面

- 添加 admin.agents.create.saveBtn 和 admin.agents.editSuccess 翻译
- 修复代理卡密生成页面API路径错误,使用/agent/apps/{id}获取卡类
- 重构代理卡密生成页面样式,参考管理员后台卡密生成页面
- 添加代理卡密生成页面完整i18n国际化支持

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 22:25:06 +08:00
parent 914e3d5127
commit e7dc05c257
3 changed files with 287 additions and 132 deletions
+223 -127
View File
@@ -1,189 +1,285 @@
<script setup lang="ts">
import { Loader2 } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { Loader2, Receipt, Settings2, Sparkles } from 'lucide-vue-next'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import { BasicPage } from '@/components/global-layout'
import api from '@/services/api'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const loading = ref(false)
const apps = ref<any[]>([])
const cardTypes = ref<any[]>([])
interface CardType {
id: number
name: string
billing_type: string
price: number
value: number
duration_days: number
}
interface Application {
id: number
name: string
}
const saving = ref(false)
const applications = ref<Application[]>([])
const cardTypes = ref<CardType[]>([])
const form = ref({
app_id: route.query.app_id || '',
card_type_id: route.query.card_type_id || '',
quantity: 1,
application_id: '',
card_type_id: '',
count: 1,
})
const availableCardTypes = computed(() => {
return cardTypes.value.filter(ct => ct.app_id === Number(form.value.app_id))
})
const totalPrice = computed(() => {
const ct = cardTypes.value.find(c => c.id === Number(form.value.card_type_id))
return ct ? ct.price * form.value.quantity : 0
})
onMounted(async () => {
async function fetchApplications() {
try {
const data = await api.get<any>('/agent/apps')
if (Array.isArray(data)) {
apps.value = data
applications.value = data
} else if (data?.apps) {
apps.value = data.apps
applications.value = data.apps
} else if (data?.data) {
apps.value = Array.isArray(data.data) ? data.data : []
applications.value = Array.isArray(data.data) ? data.data : []
} else {
apps.value = []
}
if (apps.value.length > 0) {
const typesData = await api.get<any>('/agent/card-types')
if (Array.isArray(typesData)) {
cardTypes.value = typesData
} else if (typesData?.card_types) {
cardTypes.value = typesData.card_types
} else if (typesData?.data) {
cardTypes.value = Array.isArray(typesData.data) ? typesData.data : []
} else {
cardTypes.value = []
}
applications.value = []
}
}
catch (error) {
console.error('Load data failed:', error)
console.error('获取应用列表失败:', error)
}
}
async function fetchCardTypes(appId: string) {
if (!appId) {
cardTypes.value = []
return
}
try {
const data = await api.get<{ app: any, cardTypes: CardType[] }>(`/agent/apps/${appId}`)
cardTypes.value = data?.cardTypes || []
}
catch (error) {
console.error('获取卡类列表失败:', error)
cardTypes.value = []
}
}
watch(() => form.value.application_id, (appId) => {
form.value.card_type_id = ''
fetchCardTypes(appId)
})
const selectedCardType = computed(() => {
if (form.value.card_type_id) {
return cardTypes.value.find(ct => String(ct.id) === form.value.card_type_id)
}
return null
})
const totalCost = computed(() => {
if (selectedCardType.value) {
return selectedCardType.value.price * form.value.count
}
return 0
})
async function handleGenerate() {
if (!form.value.app_id) {
toast.error('请选择应用')
if (!form.value.application_id) {
toast.error(t('agent.cards.create.selectAppFirst'))
return
}
if (!form.value.card_type_id) {
toast.error('请选择卡类')
toast.error(t('agent.cards.create.selectCardTypeFirst'))
return
}
if (form.value.quantity < 1 || form.value.quantity > 100) {
toast.error('数量必须在1-100之间')
if (form.value.count < 1 || form.value.count > 100) {
toast.error(t('agent.cards.create.countRange'))
return
}
loading.value = true
saving.value = true
try {
const data = await api.post<{ codes: string[], cards: any[] }>('/agent/cards/generate', {
app_id: Number(form.value.app_id),
app_id: Number(form.value.application_id),
card_type_id: Number(form.value.card_type_id),
quantity: form.value.quantity,
quantity: form.value.count,
})
toast.success(`成功生成 ${data.codes.length} 张卡密`)
toast.success(t('agent.cards.create.generateSuccess', { count: data.codes.length }))
const codesText = data.codes.join('\n')
await navigator.clipboard.writeText(codesText)
toast.success('卡密已复制到剪贴板')
toast.success(t('agent.cards.create.copiedToClipboard'))
router.push('/agent/cards')
}
catch (error: any) {
console.error('Generate cards failed:', error)
toast.error(error.message || '生成卡密失败')
console.error('生成卡密失败:', error)
toast.error(error.message || t('agent.cards.create.generateFailed'))
}
finally {
loading.value = false
saving.value = false
}
}
onMounted(() => {
fetchApplications()
})
</script>
<template>
<BasicPage
title="生成卡密"
description="批量生成卡密"
:title="t('agent.cards.create.title')"
:description="t('agent.cards.create.description')"
:breadcrumbs="[
{ title: '卡密管理', href: '/agent/cards' },
{ title: '生成卡密' },
{ title: t('agent.cards.title'), href: '/agent/cards' },
{ title: t('agent.cards.create.title') },
]"
sticky
>
<UiCard class="max-w-2xl">
<UiCardHeader>
<UiCardTitle>生成设置</UiCardTitle>
<UiCardDescription>
选择应用和卡类设置生成数量
</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel>选择应用 <span class="text-destructive">*</span></UiLabel>
<UiSelect v-model="form.app_id">
<UiSelectTrigger>
<UiSelectValue placeholder="请选择应用" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="app in apps" :key="app.id" :value="String(app.id)">
{{ app.name }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<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">
<Settings2 class="size-5" />
{{ t('agent.cards.create.config') }}
</UiCardTitle>
<UiCardDescription>{{ t('agent.cards.create.configDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="application">
{{ t('agent.cards.create.application') }}
</UiLabel>
<UiSelect v-model="form.application_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('agent.cards.create.selectApplication')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
{{ app.name }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<div class="space-y-2">
<UiLabel for="cardType">
{{ t('agent.cards.create.cardType') }}
</UiLabel>
<UiSelect v-model="form.card_type_id" :disabled="!form.application_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('agent.cards.create.selectCardType')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="type in cardTypes" :key="type.id" :value="String(type.id)">
{{ type.name }}<template v-if="type.billing_type"> ({{ type.billing_type }})</template>
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<p v-if="form.application_id && cardTypes.length === 0" class="text-xs text-muted-foreground">
{{ t('agent.cards.create.noCardType') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="count">
{{ t('agent.cards.create.count') }}
</UiLabel>
<div class="flex items-center gap-4">
<UiNumberField v-model="form.count" :min="1" :max="100" class="flex-1 max-w-[200px]">
<UiNumberFieldContent>
<UiNumberFieldDecrement />
<UiNumberFieldInput />
<UiNumberFieldIncrement />
</UiNumberFieldContent>
</UiNumberField>
<div class="flex gap-2">
<UiButton variant="outline" size="sm" @click="form.count = 10">
10
</UiButton>
<UiButton variant="outline" size="sm" @click="form.count = 50">
50
</UiButton>
<UiButton variant="outline" size="sm" @click="form.count = 100">
100
</UiButton>
</div>
</div>
<p class="text-xs text-muted-foreground">
{{ t('agent.cards.create.maxCount') }}
</p>
</div>
</UiCardContent>
</UiCard>
</div>
<div v-if="form.app_id" class="space-y-2">
<UiLabel>选择卡类 <span class="text-destructive">*</span></UiLabel>
<UiSelect v-model="form.card_type_id">
<UiSelectTrigger>
<UiSelectValue placeholder="请选择卡类" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="ct in availableCardTypes" :key="ct.id" :value="String(ct.id)">
{{ ct.name }} - {{ ct.duration_days }} - ¥{{ ct.price }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<div class="space-y-6">
<UiCard>
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Receipt class="size-5" />
{{ t('agent.cards.create.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('agent.cards.create.app') }}</span>
<span>{{ applications.find(a => String(a.id) === form.application_id)?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('agent.cards.create.type') }}</span>
<span>{{ selectedCardType?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('agent.cards.create.unitPrice') }}</span>
<span>¥{{ selectedCardType?.price?.toFixed(2) || '0.00' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('agent.cards.create.quantity') }}</span>
<span>{{ form.count }} {{ t('agent.cards.create.units') }}</span>
</div>
<div class="border-t pt-3 mt-3">
<div class="flex justify-between items-center">
<span class="text-muted-foreground">{{ t('agent.cards.create.totalAmount') }}</span>
<span class="text-xl font-bold text-primary">¥{{ totalCost.toFixed(2) }}</span>
</div>
</div>
</div>
</UiCardContent>
</UiCard>
<div class="space-y-2">
<UiLabel>生成数量 <span class="text-destructive">*</span></UiLabel>
<UiInput
v-model.number="form.quantity"
type="number"
:min="1"
:max="100"
placeholder="请输入生成数量"
/>
<p class="text-xs text-muted-foreground">
单次最多生成100张卡密
</p>
<UiCard>
<UiCardContent class="pt-6">
<div class="flex flex-col gap-3">
<UiButton
class="w-full"
size="lg"
:disabled="saving || !form.application_id || !form.card_type_id"
@click="handleGenerate"
>
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Sparkles v-else class="mr-2 h-4 w-4" />
{{ t('agent.cards.create.generateNow') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('agent.cards.create.cancel') }}
</UiButton>
</div>
</UiCardContent>
</UiCard>
</div>
<div v-if="totalPrice > 0" class="p-4 rounded-lg bg-muted/50">
<div class="flex items-center justify-between">
<span class="text-muted-foreground">预计费用</span>
<span class="text-xl font-bold">¥{{ totalPrice.toFixed(2) }}</span>
</div>
</div>
<div class="flex gap-3">
<UiButton variant="outline" as-child>
<router-link to="/agent/cards">
取消
</router-link>
</UiButton>
<UiButton
:disabled="loading || !form.app_id || !form.card_type_id"
@click="handleGenerate"
>
<Loader2 v-if="loading" class="mr-2 h-4 w-4 animate-spin" />
{{ loading ? '生成中...' : '生成卡密' }}
</UiButton>
</div>
</UiCardContent>
</UiCard>
</div>
</div>
</BasicPage>
</template>
</template>