feat: 实现前端卡密管理界面

- 卡密列表展示与分页功能

- 单个/批量创建卡密

- 卡密删除与批量删除

- 卡密导出功能 (file-saver)

- 启用/禁用状态切换

- 状态判断 (有效/已使用/已失效)

- Toast 通知系统 (vue-sonner)

- 登录页面错误提示优化

- 后端登录错误消息中文化
This commit is contained in:
sar
2026-01-13 21:34:56 +08:00
parent 42c423bd32
commit 8d60704eda
143 changed files with 6646 additions and 91 deletions

View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { toast } from 'vue-sonner'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { Button } from '@/components/ui/button'
import { useAccountsStore } from '@/stores/accounts'
import { Users, CheckCircle, XCircle, Armchair, RefreshCw } from 'lucide-vue-next'
const accountsStore = useAccountsStore()
onMounted(() => {
loadData()
})
async function loadData() {
try {
await accountsStore.fetchAccounts()
} catch (e: any) {
toast.error(e.message || '加载数据失败')
}
}
</script>
<template>
<div class="space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold">Dashboard</h1>
<Button variant="outline" size="sm" @click="loadData" :disabled="accountsStore.loading">
<RefreshCw :class="['h-4 w-4 mr-2', accountsStore.loading && 'animate-spin']" />
刷新
</Button>
</div>
<!-- Stats Cards -->
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- Total Teams -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Team 总数</CardTitle>
<Users class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<Skeleton v-if="accountsStore.loading" class="h-8 w-16" />
<div v-else class="text-2xl font-bold">{{ accountsStore.totalTeams }}</div>
</CardContent>
</Card>
<!-- Valid Teams -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">有效订阅</CardTitle>
<CheckCircle class="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<Skeleton v-if="accountsStore.loading" class="h-8 w-16" />
<div v-else class="text-2xl font-bold text-green-600">{{ accountsStore.validTeams }}</div>
</CardContent>
</Card>
<!-- Invalid Teams -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">无效订阅</CardTitle>
<XCircle class="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<Skeleton v-if="accountsStore.loading" class="h-8 w-16" />
<div v-else class="text-2xl font-bold text-red-600">{{ accountsStore.invalidTeams }}</div>
</CardContent>
</Card>
<!-- Available Seats -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">剩余席位</CardTitle>
<Armchair class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<Skeleton v-if="accountsStore.loading" class="h-8 w-16" />
<div v-else class="text-2xl font-bold">{{ accountsStore.totalAvailableSeats }}</div>
</CardContent>
</Card>
</div>
<!-- Error state -->
<Card v-if="accountsStore.error" class="border-destructive">
<CardContent class="pt-6">
<div class="flex items-center justify-between">
<p class="text-destructive">{{ accountsStore.error }}</p>
<Button variant="outline" size="sm" @click="loadData">
重试
</Button>
</div>
</CardContent>
</Card>
</div>
</template>