add admin

This commit is contained in:
songtianlun 2025-08-03 11:04:56 +08:00
parent 2e44c5865d
commit b052bbedf5
5 changed files with 217 additions and 149 deletions

View File

@ -218,6 +218,7 @@
},
"admin": {
"dashboard": "Admin Dashboard",
"dashboardDesc": "Manage users, prompts, and system settings",
"totalUsers": "Total Users",
"totalPrompts": "Total Prompts",
"sharedPrompts": "Shared Prompts",
@ -238,7 +239,10 @@
"approve": "Approve",
"reject": "Reject",
"allPrompts": "All Prompts",
"allPromptsDesc": "Review all shared prompts"
"allPromptsDesc": "Review all shared prompts",
"loadingAdmin": "Loading admin panel...",
"loadingDashboard": "Loading dashboard statistics...",
"loadingPrompts": "Loading prompts for review..."
},
"errors": {
"generic": "Something went wrong. Please try again.",

View File

@ -218,6 +218,7 @@
},
"admin": {
"dashboard": "管理员后台",
"dashboardDesc": "管理用户、提示词和系统设置",
"totalUsers": "用户总数",
"totalPrompts": "提示词总数",
"sharedPrompts": "用户共享提示词",
@ -238,7 +239,10 @@
"approve": "通过",
"reject": "拒绝",
"allPrompts": "所有提示词",
"allPromptsDesc": "审核所有共享提示词"
"allPromptsDesc": "审核所有共享提示词",
"loadingAdmin": "加载管理员后台中...",
"loadingDashboard": "加载统计数据中...",
"loadingPrompts": "加载审核提示词中..."
},
"errors": {
"generic": "出现错误,请重试。",

View File

@ -4,6 +4,7 @@ import { useUser } from '@/hooks/useUser'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { Header } from '@/components/layout/Header'
export default function AdminLayout({
children,
@ -22,9 +23,15 @@ export default function AdminLayout({
if (loading) {
return (
<div className="min-h-screen bg-background">
<div className="flex items-center justify-center h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
<div className="min-h-screen">
<Header />
<div className="max-w-7xl mx-auto px-4 py-8">
<div className="flex items-center justify-center min-h-96">
<div className="flex flex-col items-center gap-3">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
<p className="text-sm text-muted-foreground">{t('loadingAdmin')}</p>
</div>
</div>
</div>
</div>
)
@ -35,17 +42,9 @@ export default function AdminLayout({
}
return (
<div className="min-h-screen bg-background">
<div className="border-b border-border bg-card">
<div className="container mx-auto px-4 py-4">
<h1 className="text-2xl font-bold text-foreground">
{t('dashboard')}
</h1>
</div>
</div>
<div className="container mx-auto px-4 py-8">
{children}
</div>
<div className="min-h-screen">
<Header />
{children}
</div>
)
}

View File

@ -38,15 +38,22 @@ export default function AdminDashboard() {
if (loading) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[...Array(4)].map((_, i) => (
<Card key={i} className="p-6">
<div className="animate-pulse">
<div className="h-4 bg-muted rounded w-3/4 mb-2"></div>
<div className="h-8 bg-muted rounded w-1/2"></div>
</div>
</Card>
))}
<div className="max-w-7xl mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">{t('dashboard')}</h1>
<p className="text-muted-foreground">
{t('dashboardDesc') || 'Manage users, prompts, and system settings'}
</p>
</div>
{/* Loading State */}
<div className="flex items-center justify-center min-h-96">
<div className="flex flex-col items-center gap-3">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
<p className="text-sm text-muted-foreground">{t('loadingDashboard')}</p>
</div>
</div>
</div>
)
}
@ -79,78 +86,112 @@ export default function AdminDashboard() {
]
return (
<div className="space-y-8">
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{statCards.map((stat, index) => {
const Icon = stat.icon
return (
<Card key={index} className="p-6 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">
{stat.title}
</p>
<p className="text-3xl font-bold text-foreground">
{stat.value.toLocaleString()}
</p>
</div>
<Icon className={`h-8 w-8 ${stat.color}`} />
</div>
</Card>
)
})}
<div className="max-w-7xl mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">{t('dashboard')}</h1>
<p className="text-muted-foreground">
{t('dashboardDesc') || 'Manage users, prompts, and system settings'}
</p>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-foreground mb-4">
{t('quickActions')}
</h3>
<div className="space-y-3">
<Link
href="/admin/review"
className="block p-3 rounded-lg border border-border hover:bg-accent hover:text-accent-foreground transition-colors"
>
<div className="flex items-center gap-3">
<CheckCircle className="h-5 w-5 text-primary" />
<div>
<div className="font-medium">
{t('allPrompts')}
<div className="space-y-6 lg:space-y-8">
{/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
{statCards.map((stat, index) => {
const Icon = stat.icon
return (
<Card key={index} className="p-4 lg:p-6 hover:shadow-md transition-all duration-200 border-border hover:border-primary/20">
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<p className="text-xs sm:text-sm font-medium text-muted-foreground mb-1 truncate">
{stat.title}
</p>
<p className="text-xl sm:text-2xl lg:text-3xl font-bold text-foreground">
{stat.value.toLocaleString()}
</p>
</div>
<div className="text-sm text-muted-foreground">
{t('allPromptsDesc')}
<div className="flex-shrink-0 ml-3">
<div className="p-2 lg:p-3 rounded-full bg-muted/50">
<Icon className={`h-5 w-5 lg:h-6 lg:w-6 ${stat.color}`} />
</div>
</div>
</div>
</div>
</Link>
</div>
</Card>
</Card>
)
})}
</div>
<Card className="p-6">
<h3 className="text-lg font-semibold text-foreground mb-4">
{t('systemStatus')}
</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{t('databaseStatus')}
</span>
<span className="text-sm font-medium text-green-600">
{t('healthy')}
</span>
{/* Quick Actions and System Status */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Quick Actions */}
<Card className="p-4 lg:p-6">
<div className="flex items-center gap-3 mb-4 lg:mb-6">
<div className="p-2 rounded-lg bg-primary/10">
<CheckCircle className="h-5 w-5 text-primary" />
</div>
<h3 className="text-lg font-semibold text-foreground">
{t('quickActions')}
</h3>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{t('authStatus')}
</span>
<span className="text-sm font-medium text-green-600">
{t('healthy')}
</span>
<div className="space-y-3">
<Link
href="/admin/review"
className="group block p-3 lg:p-4 rounded-lg border border-border hover:border-primary/30 hover:bg-accent/50 transition-all duration-200"
>
<div className="flex items-center gap-3">
<div className="p-2 rounded-md bg-primary/5 group-hover:bg-primary/10 transition-colors">
<FileText className="h-4 w-4 text-primary" />
</div>
<div className="min-w-0 flex-1">
<div className="font-medium text-foreground group-hover:text-primary transition-colors">
{t('allPrompts')}
</div>
<div className="text-sm text-muted-foreground mt-0.5">
{t('allPromptsDesc')}
</div>
</div>
</div>
</Link>
</div>
</div>
</Card>
</Card>
{/* System Status */}
<Card className="p-4 lg:p-6">
<div className="flex items-center gap-3 mb-4 lg:mb-6">
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-900/30">
<CheckCircle className="h-5 w-5 text-green-600" />
</div>
<h3 className="text-lg font-semibold text-foreground">
{t('systemStatus')}
</h3>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/30">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<span className="text-sm font-medium text-foreground">
{t('databaseStatus')}
</span>
</div>
<span className="text-sm font-medium text-green-600">
{t('healthy')}
</span>
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/30">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<span className="text-sm font-medium text-foreground">
{t('authStatus')}
</span>
</div>
<span className="text-sm font-medium text-green-600">
{t('healthy')}
</span>
</div>
</div>
</Card>
</div>
</div>
</div>
)

View File

@ -73,104 +73,123 @@ export default function AdminReviewPage() {
if (loading) {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-foreground">
{t('reviewPrompts') || 'Review Prompts'}
</h1>
<div className="max-w-7xl mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
{t('allPrompts')}
</h1>
<p className="text-muted-foreground">
{t('reviewPromptsDesc')}
</p>
</div>
</div>
</div>
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<Card key={i} className="p-6">
<div className="animate-pulse space-y-4">
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-20 bg-muted rounded"></div>
<div className="flex gap-2">
<div className="h-9 bg-muted rounded w-20"></div>
<div className="h-9 bg-muted rounded w-20"></div>
</div>
</div>
</Card>
))}
{/* Loading State */}
<div className="flex items-center justify-center min-h-96">
<div className="flex flex-col items-center gap-3">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
<p className="text-sm text-muted-foreground">{t('loadingPrompts')}</p>
</div>
</div>
</div>
)
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-foreground">
{t('allPrompts')}
</h1>
<div className="flex items-center gap-2">
<Badge variant="secondary">
{prompts.filter(p => !p.visibility || p.visibility === 'under_review').length} {t('pending')}
</Badge>
<Badge variant="outline">
{prompts.filter(p => p.visibility === 'published').length} {t('published')}
</Badge>
<div className="max-w-7xl mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">
{t('allPrompts')}
</h1>
<p className="text-muted-foreground">
{t('reviewPromptsDesc')}
</p>
</div>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs sm:text-sm">
{prompts.filter(p => !p.visibility || p.visibility === 'under_review').length} {t('pending')}
</Badge>
<Badge variant="outline" className="text-xs sm:text-sm">
{prompts.filter(p => p.visibility === 'published').length} {t('published')}
</Badge>
</div>
</div>
</div>
{/* Content */}
{prompts.length === 0 ? (
<Card className="p-8 text-center">
<CheckCircle className="h-12 w-12 text-green-500 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-foreground mb-2">
{t('noPromptsPending')}
</h3>
<p className="text-muted-foreground">
{t('allPromptsReviewed')}
</p>
<Card className="p-8 lg:p-12 text-center">
<div className="max-w-md mx-auto">
<CheckCircle className="h-12 w-12 lg:h-16 lg:w-16 text-green-500 mx-auto mb-4" />
<h3 className="text-lg lg:text-xl font-semibold text-foreground mb-2">
{t('noPromptsPending')}
</h3>
<p className="text-muted-foreground">
{t('allPromptsReviewed')}
</p>
</div>
</Card>
) : (
<div className="space-y-4">
<div className="space-y-4 lg:space-y-6">
{prompts.map((prompt) => (
<Card key={prompt.id} className="p-6">
<Card key={prompt.id} className="p-4 lg:p-6 border-border hover:border-primary/20 transition-all duration-200">
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="space-y-1">
<h3 className="text-lg font-semibold text-foreground">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<div className="space-y-1 min-w-0 flex-1">
<h3 className="text-lg lg:text-xl font-semibold text-foreground break-words">
{prompt.name}
</h3>
{prompt.description && (
<p className="text-sm text-muted-foreground">
<p className="text-sm text-muted-foreground line-clamp-2 break-words">
{prompt.description}
</p>
)}
</div>
<Badge
variant={prompt.visibility === 'published' ? 'default' : 'outline'}
className={prompt.visibility === 'published' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : ''}
>
{prompt.visibility === 'published' ? t('published') : t('underReview')}
</Badge>
<div className="flex-shrink-0">
<Badge
variant={prompt.visibility === 'published' ? 'default' : 'outline'}
className={`text-xs sm:text-sm ${
prompt.visibility === 'published'
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: ''
}`}
>
{prompt.visibility === 'published' ? t('published') : t('underReview')}
</Badge>
</div>
</div>
{/* Metadata */}
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<UserIcon className="h-4 w-4" />
<span>{prompt.user.username || prompt.user.email}</span>
<UserIcon className="h-4 w-4 flex-shrink-0" />
<span className="truncate">{prompt.user.username || prompt.user.email}</span>
</div>
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<span>
<Calendar className="h-4 w-4 flex-shrink-0" />
<span className="whitespace-nowrap">
{formatDistanceToNow(new Date(prompt.createdAt), { addSuffix: true })}
</span>
</div>
</div>
{/* Content Preview */}
<div className="border border-border rounded-lg p-4 bg-muted/50">
<div className="border border-border rounded-lg p-3 lg:p-4 bg-muted/30">
<div className="flex items-center gap-2 mb-2">
<Eye className="h-4 w-4 text-muted-foreground" />
<Eye className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<span className="text-sm font-medium text-muted-foreground">
{t('promptContent')}
</span>
</div>
<div className="text-sm text-foreground max-h-32 overflow-y-auto">
<div className="text-sm text-foreground max-h-32 lg:max-h-40 overflow-y-auto break-words">
{prompt.content.length > 500
? `${prompt.content.slice(0, 500)}...`
: prompt.content
@ -179,11 +198,11 @@ export default function AdminReviewPage() {
</div>
{/* Actions */}
<div className="flex items-center gap-3 pt-2">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 pt-2 border-t border-border/50">
{prompt.visibility !== 'published' && (
<Button
onClick={() => handleApprove(prompt.id)}
className="bg-green-600 hover:bg-green-700 text-white"
className="bg-green-600 hover:bg-green-700 text-white w-full sm:w-auto"
size="sm"
>
<CheckCircle className="h-4 w-4 mr-2" />
@ -194,6 +213,7 @@ export default function AdminReviewPage() {
onClick={() => handleReject(prompt.id)}
variant="destructive"
size="sm"
className="w-full sm:w-auto"
>
<XCircle className="h-4 w-4 mr-2" />
{t('reject')}