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"> <script setup lang="ts">
import { Loader2 } from 'lucide-vue-next' import { Loader2, Receipt, Settings2, Sparkles } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { BasicPage } from '@/components/global-layout' import { BasicPage } from '@/components/global-layout'
import api from '@/services/api' import api from '@/services/api'
const { t } = useI18n()
const route = useRoute()
const router = useRouter() const router = useRouter()
const { t } = useI18n()
const loading = ref(false) interface CardType {
const apps = ref<any[]>([]) id: number
const cardTypes = ref<any[]>([]) 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({ const form = ref({
app_id: route.query.app_id || '', application_id: '',
card_type_id: route.query.card_type_id || '', card_type_id: '',
quantity: 1, count: 1,
}) })
const availableCardTypes = computed(() => { async function fetchApplications() {
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 () => {
try { try {
const data = await api.get<any>('/agent/apps') const data = await api.get<any>('/agent/apps')
if (Array.isArray(data)) { if (Array.isArray(data)) {
apps.value = data applications.value = data
} else if (data?.apps) { } else if (data?.apps) {
apps.value = data.apps applications.value = data.apps
} else if (data?.data) { } else if (data?.data) {
apps.value = Array.isArray(data.data) ? data.data : [] applications.value = Array.isArray(data.data) ? data.data : []
} else { } else {
apps.value = [] applications.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 = []
}
} }
} }
catch (error) { 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() { async function handleGenerate() {
if (!form.value.app_id) { if (!form.value.application_id) {
toast.error('请选择应用') toast.error(t('agent.cards.create.selectAppFirst'))
return return
} }
if (!form.value.card_type_id) { if (!form.value.card_type_id) {
toast.error('请选择卡类') toast.error(t('agent.cards.create.selectCardTypeFirst'))
return return
} }
if (form.value.quantity < 1 || form.value.quantity > 100) { if (form.value.count < 1 || form.value.count > 100) {
toast.error('数量必须在1-100之间') toast.error(t('agent.cards.create.countRange'))
return return
} }
loading.value = true saving.value = true
try { try {
const data = await api.post<{ codes: string[], cards: any[] }>('/agent/cards/generate', { 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), card_type_id: Number(form.value.card_type_id),
quantity: form.value.quantity, quantity: form.value.count,
}) })
toast.success(t('agent.cards.create.generateSuccess', { count: data.codes.length }))
toast.success(`成功生成 ${data.codes.length} 张卡密`)
const codesText = data.codes.join('\n') const codesText = data.codes.join('\n')
await navigator.clipboard.writeText(codesText) await navigator.clipboard.writeText(codesText)
toast.success('卡密已复制到剪贴板') toast.success(t('agent.cards.create.copiedToClipboard'))
router.push('/agent/cards') router.push('/agent/cards')
} }
catch (error: any) { catch (error: any) {
console.error('Generate cards failed:', error) console.error('生成卡密失败:', error)
toast.error(error.message || '生成卡密失败') toast.error(error.message || t('agent.cards.create.generateFailed'))
} }
finally { finally {
loading.value = false saving.value = false
} }
} }
onMounted(() => {
fetchApplications()
})
</script> </script>
<template> <template>
<BasicPage <BasicPage
title="生成卡密" :title="t('agent.cards.create.title')"
description="批量生成卡密" :description="t('agent.cards.create.description')"
:breadcrumbs="[ :breadcrumbs="[
{ title: '卡密管理', href: '/agent/cards' }, { title: t('agent.cards.title'), href: '/agent/cards' },
{ title: '生成卡密' }, { title: t('agent.cards.create.title') },
]" ]"
sticky sticky
> >
<UiCard class="max-w-2xl"> <div class="space-y-6">
<UiCardHeader> <div class="grid gap-6 lg:grid-cols-3">
<UiCardTitle>生成设置</UiCardTitle> <div class="lg:col-span-2 space-y-6">
<UiCardDescription> <UiCard>
选择应用和卡类设置生成数量 <UiCardHeader>
</UiCardDescription> <UiCardTitle class="flex items-center gap-2">
</UiCardHeader> <Settings2 class="size-5" />
<UiCardContent class="space-y-6"> {{ t('agent.cards.create.config') }}
<div class="space-y-2"> </UiCardTitle>
<UiLabel>选择应用 <span class="text-destructive">*</span></UiLabel> <UiCardDescription>{{ t('agent.cards.create.configDesc') }}</UiCardDescription>
<UiSelect v-model="form.app_id"> </UiCardHeader>
<UiSelectTrigger> <UiCardContent class="space-y-6">
<UiSelectValue placeholder="请选择应用" /> <div class="space-y-2">
</UiSelectTrigger> <UiLabel for="application">
<UiSelectContent> {{ t('agent.cards.create.application') }}
<UiSelectItem v-for="app in apps" :key="app.id" :value="String(app.id)"> </UiLabel>
{{ app.name }} <UiSelect v-model="form.application_id">
</UiSelectItem> <UiSelectTrigger>
</UiSelectContent> <UiSelectValue :placeholder="t('agent.cards.create.selectApplication')" />
</UiSelect> </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>
<div v-if="form.app_id" class="space-y-2"> <div class="space-y-6">
<UiLabel>选择卡类 <span class="text-destructive">*</span></UiLabel> <UiCard>
<UiSelect v-model="form.card_type_id"> <UiCardHeader>
<UiSelectTrigger> <UiCardTitle class="flex items-center gap-2">
<UiSelectValue placeholder="请选择卡类" /> <Receipt class="size-5" />
</UiSelectTrigger> {{ t('agent.cards.create.preview') }}
<UiSelectContent> </UiCardTitle>
<UiSelectItem v-for="ct in availableCardTypes" :key="ct.id" :value="String(ct.id)"> </UiCardHeader>
{{ ct.name }} - {{ ct.duration_days }} - ¥{{ ct.price }} <UiCardContent class="space-y-4">
</UiSelectItem> <div class="space-y-3">
</UiSelectContent> <div class="flex justify-between text-sm">
</UiSelect> <span class="text-muted-foreground">{{ t('agent.cards.create.app') }}</span>
</div> <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"> <UiCard>
<UiLabel>生成数量 <span class="text-destructive">*</span></UiLabel> <UiCardContent class="pt-6">
<UiInput <div class="flex flex-col gap-3">
v-model.number="form.quantity" <UiButton
type="number" class="w-full"
:min="1" size="lg"
:max="100" :disabled="saving || !form.application_id || !form.card_type_id"
placeholder="请输入生成数量" @click="handleGenerate"
/> >
<p class="text-xs text-muted-foreground"> <Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
单次最多生成100张卡密 <Sparkles v-else class="mr-2 h-4 w-4" />
</p> {{ 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>
</div>
<div v-if="totalPrice > 0" class="p-4 rounded-lg bg-muted/50"> </div>
<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>
</BasicPage> </BasicPage>
</template> </template>
+27 -3
View File
@@ -1322,8 +1322,11 @@
"passwordMinLength": "Password must be at least 6 characters", "passwordMinLength": "Password must be at least 6 characters",
"createSuccess": "Agent created successfully", "createSuccess": "Agent created successfully",
"createFailed": "Failed to create agent", "createFailed": "Failed to create agent",
"cancel": "Cancel" "cancel": "Cancel",
"saveBtn": "Save Changes"
}, },
"editSuccess": "Agent updated successfully",
"editFailed": "Failed to update agent",
"enable": "Enable", "enable": "Enable",
"disable": "Disable", "disable": "Disable",
"deleteAgent": "Delete Agent", "deleteAgent": "Delete Agent",
@@ -1341,7 +1344,8 @@
"save": "Save", "save": "Save",
"usernameHint": "Username cannot be changed", "usernameHint": "Username cannot be changed",
"passwordPlaceholder": "Leave empty to keep current password", "passwordPlaceholder": "Leave empty to keep current password",
"passwordHint": "Enter new password to change, leave empty to keep current" "passwordHint": "Enter new password to change, leave empty to keep current",
"saveBtn": "Save Changes"
} }
}, },
"announcements": { "announcements": {
@@ -2857,7 +2861,27 @@
"estimatedCost": "Estimated Cost", "estimatedCost": "Estimated Cost",
"cancel": "Cancel", "cancel": "Cancel",
"generating": "Generating...", "generating": "Generating...",
"generateBtn": "Generate Cards" "generateBtn": "Generate Cards",
"selectAppFirst": "Please select an application first",
"selectCardTypeFirst": "Please select a card type first",
"countRange": "Quantity must be between 1 and 100",
"config": "Generation Config",
"configDesc": "Select authorized application and available card type, set quantity",
"application": "Select Application",
"selectApplication": "Please select an application",
"cardType": "Select Card Type",
"selectCardType": "Please select a card type",
"noCardType": "No available card types for this application",
"count": "Quantity",
"maxCount": "Maximum 100 cards per generation",
"preview": "Generation Preview",
"app": "Application",
"type": "Card Type",
"unitPrice": "Unit Price",
"quantity": "Quantity",
"units": "cards",
"totalAmount": "Estimated Cost",
"generateNow": "Generate Now"
} }
} }
}, },
+37 -2
View File
@@ -1280,8 +1280,23 @@
"passwordMinLength": "密码长度至少6位", "passwordMinLength": "密码长度至少6位",
"createSuccess": "创建代理成功", "createSuccess": "创建代理成功",
"createFailed": "创建代理失败", "createFailed": "创建代理失败",
"cancel": "取消" "cancel": "取消",
"saveBtn": "保存修改"
}, },
"edit": {
"title": "编辑代理",
"description": "修改代理信息",
"fetchFailed": "获取代理信息失败",
"saveSuccess": "保存成功",
"saveFailed": "保存失败",
"save": "保存",
"usernameHint": "用户名不可修改",
"passwordPlaceholder": "留空则不修改密码",
"passwordHint": "输入新密码以修改,留空保持原密码",
"saveBtn": "保存修改"
},
"editSuccess": "更新代理成功",
"editFailed": "更新代理失败",
"enable": "启用", "enable": "启用",
"disable": "禁用", "disable": "禁用",
"deleteAgent": "删除代理", "deleteAgent": "删除代理",
@@ -2847,7 +2862,27 @@
"estimatedCost": "预计费用", "estimatedCost": "预计费用",
"cancel": "取消", "cancel": "取消",
"generating": "生成中...", "generating": "生成中...",
"generateBtn": "生成卡密" "generateBtn": "生成卡密",
"selectAppFirst": "请先选择应用",
"selectCardTypeFirst": "请先选择卡类",
"countRange": "生成数量需要在 1-100 之间",
"config": "生成配置",
"configDesc": "选择授权应用和可用卡类,设置生成数量",
"application": "选择应用",
"selectApplication": "请选择应用",
"cardType": "选择卡类",
"selectCardType": "请选择卡类",
"noCardType": "该应用暂无可用卡类",
"count": "生成数量",
"maxCount": "单次最多生成 100 张卡密",
"preview": "生成预览",
"app": "应用",
"type": "卡类",
"unitPrice": "单价",
"quantity": "数量",
"units": "张",
"totalAmount": "预计费用",
"generateNow": "立即生成"
} }
} }
}, },