feat: 实现前端卡密管理界面
- 卡密列表展示与分页功能 - 单个/批量创建卡密 - 卡密删除与批量删除 - 卡密导出功能 (file-saver) - 启用/禁用状态切换 - 状态判断 (有效/已使用/已失效) - Toast 通知系统 (vue-sonner) - 登录页面错误提示优化 - 后端登录错误消息中文化
This commit is contained in:
98
frontend/src/views/admin/DashboardPage.vue
Normal file
98
frontend/src/views/admin/DashboardPage.vue
Normal 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>
|
||||
Reference in New Issue
Block a user