Initial commit: 网络验证平台

This commit is contained in:
Admin
2026-04-27 17:22:56 +08:00
commit afe67d704e
780 changed files with 88960 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import api from '@/services/api'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const apps = ref<any[]>([])
const cardTypes = ref<any[]>([])
const form = ref({
app_id: route.query.app_id || '',
card_type_id: route.query.card_type_id || '',
quantity: 1,
})
const selectedApp = computed(() => {
return apps.value.find(a => a.id === Number(form.value.app_id))
})
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 () => {
try {
const data = await api.get('/agent/apps')
apps.value = data || []
if (apps.value.length > 0) {
const typesData = await api.get('/agent/card-types')
cardTypes.value = typesData || []
}
}
catch (error) {
console.error('获取数据失败:', error)
}
})
async function handleGenerate() {
if (!form.value.app_id) {
toast.error('请选择应用')
return
}
if (!form.value.card_type_id) {
toast.error('请选择卡类')
return
}
if (form.value.quantity < 1 || form.value.quantity > 100) {
toast.error('生成数量需要在 1-100 之间')
return
}
loading.value = true
try {
const data = await api.post<{ codes: string[], cards: any[] }>('/agent/cards/generate', {
app_id: Number(form.value.app_id),
card_type_id: Number(form.value.card_type_id),
quantity: form.value.quantity,
})
toast.success(`成功生成 ${data.codes.length} 张卡密`)
const codesText = data.codes.join('\n')
await navigator.clipboard.writeText(codesText)
toast.success('卡密已复制到剪贴板')
router.push('/agent/cards')
}
catch (error: any) {
console.error('生成卡密失败:', error)
toast.error(error.message || '生成失败')
}
finally {
loading.value = false
}
}
</script>
<template>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold">
生成卡密
</h1>
<p class="text-muted-foreground">
为授权应用生成卡密
</p>
</div>
<UiCard class="max-w-xl">
<UiCardHeader>
<UiCardTitle>生成设置</UiCardTitle>
<UiCardDescription>
选择应用和卡类设置生成数量
</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel>选择应用</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>
<div v-if="form.app_id" class="space-y-2">
<UiLabel>选择卡类</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-2">
<UiLabel>生成数量</UiLabel>
<UiInput
v-model.number="form.quantity"
type="number"
:min="1"
:max="100"
placeholder="请输入生成数量"
/>
<p class="text-xs text-muted-foreground">
单次最多生成 100 张卡密
</p>
</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"
>
<Icon v-if="loading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
{{ loading ? '生成中...' : '生成卡密' }}
</UiButton>
</div>
</UiCardContent>
</UiCard>
</div>
</template>
+138
View File
@@ -0,0 +1,138 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { onMounted, ref } from 'vue'
import api from '@/services/api'
interface Card {
id: number
code: string
app_name: string
card_type_name: string
status: string
created_at: string
used_at: string | null
}
const loading = ref(true)
const cards = ref<Card[]>([])
onMounted(async () => {
try {
const data = await api.get<Card[]>('/agent/cards')
cards.value = data || []
}
catch (error) {
console.error('获取卡密列表失败:', error)
}
finally {
loading.value = false
}
})
function formatDate(dateStr: string) {
if (!dateStr)
return '-'
return new Date(dateStr).toLocaleDateString('zh-CN')
}
function getStatusBadge(status: string) {
const map: Record<string, { label: string, class: string }> = {
unused: { label: '未使用', class: 'bg-green-500/10 text-green-500' },
used: { label: '已使用', class: 'bg-blue-500/10 text-blue-500' },
expired: { label: '已过期', class: 'bg-red-500/10 text-red-500' },
disabled: { label: '已禁用', class: 'bg-gray-500/10 text-gray-500' },
}
return map[status] || { label: status, class: '' }
}
</script>
<template>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">
卡密管理
</h1>
<p class="text-muted-foreground">
管理您生成的卡密
</p>
</div>
<UiButton as-child>
<router-link to="/agent/cards/create">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
生成卡密
</router-link>
</UiButton>
</div>
<UiCard>
<UiCardContent class="p-0">
<div v-if="loading" class="flex items-center justify-center py-12">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
</div>
<div v-else-if="cards.length === 0" class="text-center py-12 text-muted-foreground">
<Icon icon="lucide:key" class="size-16 mx-auto mb-4 opacity-30" />
<p>暂无卡密记录</p>
<UiButton class="mt-4" as-child>
<router-link to="/agent/cards/create">
生成卡密
</router-link>
</UiButton>
</div>
<div v-else class="overflow-x-auto">
<table class="w-full">
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-3 text-left text-sm font-medium">
卡密
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
应用
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
卡类
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
状态
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
创建时间
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
使用时间
</th>
</tr>
</thead>
<tbody class="divide-y">
<tr v-for="card in cards" :key="card.id" class="hover:bg-muted/30">
<td class="px-4 py-3 font-mono text-sm">
{{ card.code }}
</td>
<td class="px-4 py-3 text-sm">
{{ card.app_name }}
</td>
<td class="px-4 py-3 text-sm">
{{ card.card_type_name }}
</td>
<td class="px-4 py-3">
<UiBadge :class="getStatusBadge(card.status).class" variant="outline">
{{ getStatusBadge(card.status).label }}
</UiBadge>
</td>
<td class="px-4 py-3 text-sm text-muted-foreground">
{{ formatDate(card.created_at) }}
</td>
<td class="px-4 py-3 text-sm text-muted-foreground">
{{ formatDate(card.used_at) }}
</td>
</tr>
</tbody>
</table>
</div>
</UiCardContent>
</UiCard>
</div>
</template>