Files
evanpage/frontend/app/(main)/admin/page.tsx
evan b0b85f4d3a Initial fullstack project setup with Next.js 15, Gin, PostgreSQL and Docker Compose
- Frontend: Next.js 15 (App Router), Auth.js v5, shadcn/ui, MagicUI
- Backend: Go + Gin + GORM with layered architecture
- Auth: Local credentials login with optional Keycloak OAuth binding
- Admin: RBAC user management for admin role
- Dev: Docker Compose with hot reload for both frontend and backend
- Docker: 3-service orchestration (frontend, backend, postgres)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 15:11:20 +00:00

187 lines
5.2 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface User {
id: number;
username: string;
email: string;
role: string;
createdAt: string;
}
export default function AdminPage() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [open, setOpen] = useState(false);
const [form, setForm] = useState({
username: "",
email: "",
password: "",
role: "user",
});
async function fetchUsers() {
const res = await fetch("/api/proxy/admin/users");
if (res.ok) {
const data = await res.json();
setUsers(data.users || []);
}
setLoading(false);
}
useEffect(() => {
fetchUsers();
}, []);
async function handleCreate(e: React.FormEvent) {
e.preventDefault();
const res = await fetch("/api/proxy/admin/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (res.ok) {
setOpen(false);
setForm({ username: "", email: "", password: "", role: "user" });
fetchUsers();
}
}
async function handleDelete(id: number) {
if (!confirm("确定删除该用户?")) return;
const res = await fetch(`/api/proxy/admin/users/${id}`, {
method: "DELETE",
});
if (res.ok) {
fetchUsers();
}
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button></Button>} />
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<form onSubmit={handleCreate} className="space-y-4">
<div>
<Label></Label>
<Input
value={form.username}
onChange={(e) =>
setForm({ ...form, username: e.target.value })
}
required
/>
</div>
<div>
<Label></Label>
<Input
type="email"
value={form.email}
onChange={(e) =>
setForm({ ...form, email: e.target.value })
}
required
/>
</div>
<div>
<Label></Label>
<Input
type="password"
value={form.password}
onChange={(e) =>
setForm({ ...form, password: e.target.value })
}
required
/>
</div>
<div>
<Label></Label>
<select
className="w-full rounded-md border px-3 py-2 text-sm"
value={form.role}
onChange={(e) => setForm({ ...form, role: e.target.value })}
>
<option value="user">user</option>
<option value="admin">admin</option>
</select>
</div>
<Button type="submit" className="w-full">
</Button>
</form>
</DialogContent>
</Dialog>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<p>...</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.id}</TableCell>
<TableCell>{user.username}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
<TableCell>
<Button
variant="destructive"
size="sm"
onClick={() => handleDelete(user.id)}
>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
);
}