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:
@@ -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>
|
||||
@@ -1322,8 +1322,11 @@
|
||||
"passwordMinLength": "Password must be at least 6 characters",
|
||||
"createSuccess": "Agent created successfully",
|
||||
"createFailed": "Failed to create agent",
|
||||
"cancel": "Cancel"
|
||||
"cancel": "Cancel",
|
||||
"saveBtn": "Save Changes"
|
||||
},
|
||||
"editSuccess": "Agent updated successfully",
|
||||
"editFailed": "Failed to update agent",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"deleteAgent": "Delete Agent",
|
||||
@@ -1341,7 +1344,8 @@
|
||||
"save": "Save",
|
||||
"usernameHint": "Username cannot be changed",
|
||||
"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": {
|
||||
@@ -2857,7 +2861,27 @@
|
||||
"estimatedCost": "Estimated Cost",
|
||||
"cancel": "Cancel",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1280,8 +1280,23 @@
|
||||
"passwordMinLength": "密码长度至少6位",
|
||||
"createSuccess": "创建代理成功",
|
||||
"createFailed": "创建代理失败",
|
||||
"cancel": "取消"
|
||||
"cancel": "取消",
|
||||
"saveBtn": "保存修改"
|
||||
},
|
||||
"edit": {
|
||||
"title": "编辑代理",
|
||||
"description": "修改代理信息",
|
||||
"fetchFailed": "获取代理信息失败",
|
||||
"saveSuccess": "保存成功",
|
||||
"saveFailed": "保存失败",
|
||||
"save": "保存",
|
||||
"usernameHint": "用户名不可修改",
|
||||
"passwordPlaceholder": "留空则不修改密码",
|
||||
"passwordHint": "输入新密码以修改,留空保持原密码",
|
||||
"saveBtn": "保存修改"
|
||||
},
|
||||
"editSuccess": "更新代理成功",
|
||||
"editFailed": "更新代理失败",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"deleteAgent": "删除代理",
|
||||
@@ -2847,7 +2862,27 @@
|
||||
"estimatedCost": "预计费用",
|
||||
"cancel": "取消",
|
||||
"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": "立即生成"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user