feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
status: string;
|
||||
memberPlan: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function AdminUsers() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
}, []);
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/v1/admin/users`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setUsers(data.items || []);
|
||||
}
|
||||
} catch {}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function toggleStatus(userId: number, currentStatus: string) {
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/v1/admin/users/${userId}/status`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ status: currentStatus === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }),
|
||||
});
|
||||
loadUsers();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div className="space-y-4 p-6">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-3/4" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b border-border bg-card px-4 py-4">
|
||||
<h1 className="text-2xl font-bold text-foreground">用户管理</h1>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="bg-card rounded-xl border border-border overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted/50 border-b border-border">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">ID</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">昵称</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">联系方式</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">状态</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">会员</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">注册时间</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{users.map(user => (
|
||||
<tr key={user.id} className="hover:bg-accent/50">
|
||||
<td className="px-6 py-4 text-sm text-foreground">{user.id}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm font-medium text-foreground">{user.nickname || '未设置'}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-muted-foreground">{user.email || user.phone || '-'}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
user.status === 'ACTIVE' ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' : 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400'
|
||||
}`}>
|
||||
{user.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
user.memberPlan === 'YEARLY' ? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400' :
|
||||
user.memberPlan === 'MONTHLY' ? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400' :
|
||||
'bg-muted text-muted-foreground'
|
||||
}`}>
|
||||
{user.memberPlan || 'NONE'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-muted-foreground">
|
||||
{new Date(user.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<button
|
||||
onClick={() => toggleStatus(user.id, user.status)}
|
||||
className={`text-xs px-3 py-1 rounded transition-colors ${
|
||||
user.status === 'ACTIVE'
|
||||
? 'text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20'
|
||||
: 'text-green-600 hover:bg-green-50 dark:hover:bg-green-900/20'
|
||||
}`}
|
||||
>
|
||||
{user.status === 'ACTIVE' ? '禁用' : '启用'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{users.length === 0 && (
|
||||
<div className="text-center py-20 text-muted-foreground">
|
||||
暂无用户数据
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user