1459
This commit is contained in:
1 parent
4ab47a3e47
commit
ba016efbe8
21 files changed
+921
-72
No files matched your search
@@ -0,0 +1,263 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowLeft, Plus, Trash2, Save, Key, User } from "lucide-react";
|
||||
|
||||
interface UserInfo {
|
||||
username: string;
|
||||
name: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export default function UsersPage() {
|
||||
const router = useRouter();
|
||||
const [users, setUsers] = useState<UserInfo[]>([]);
|
||||
const [currentUser, setCurrentUser] = useState<UserInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [message, setMessage] = useState<{ type: "ok" | "err"; text: string } | null>(null);
|
||||
|
||||
// 修改密码
|
||||
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
||||
const [passwordTarget, setPasswordTarget] = useState("");
|
||||
const [oldPassword, setOldPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
|
||||
// 添加用户
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [newUsername, setNewUsername] = useState("");
|
||||
const [addPassword, setAddPassword] = useState("");
|
||||
const [newName, setNewName] = useState("");
|
||||
const [newRole, setNewRole] = useState("user");
|
||||
|
||||
const isAdmin = currentUser?.role === "admin";
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const u = JSON.parse(localStorage.getItem("wauth_user") || "{}");
|
||||
setCurrentUser(u);
|
||||
} catch {}
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
async function fetchUsers() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const token = localStorage.getItem("wauth_token");
|
||||
const res = await fetch("/api/users", {
|
||||
headers: { "X-Auth-User": token || "" },
|
||||
});
|
||||
const data = await res.json();
|
||||
setUsers(data.users || []);
|
||||
} catch {} finally { setLoading(false); }
|
||||
}
|
||||
|
||||
async function handleChangePassword(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setMessage(null);
|
||||
try {
|
||||
const token = localStorage.getItem("wauth_token");
|
||||
const res = await fetch("/api/users", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json", "X-Auth-User": token || "" },
|
||||
body: JSON.stringify({
|
||||
targetUsername: passwordTarget || currentUser?.username,
|
||||
oldPassword,
|
||||
newPassword,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
setMessage({ type: "ok", text: "密码修改成功" });
|
||||
setShowPasswordForm(false);
|
||||
setOldPassword("");
|
||||
setNewPassword("");
|
||||
} catch (e) {
|
||||
setMessage({ type: "err", text: e instanceof Error ? e.message : "修改失败" });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddUser(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setMessage(null);
|
||||
try {
|
||||
const token = localStorage.getItem("wauth_token");
|
||||
const res = await fetch("/api/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-Auth-User": token || "" },
|
||||
body: JSON.stringify({ username: newUsername, password: addPassword, name: newName, role: newRole }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
setMessage({ type: "ok", text: "用户添加成功" });
|
||||
setShowAddForm(false);
|
||||
setNewUsername("");
|
||||
setAddPassword("");
|
||||
setNewName("");
|
||||
fetchUsers();
|
||||
} catch (e) {
|
||||
setMessage({ type: "err", text: e instanceof Error ? e.message : "添加失败" });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteUser(username: string) {
|
||||
if (!confirm(`确定删除用户 ${username}?`)) return;
|
||||
setMessage(null);
|
||||
try {
|
||||
const token = localStorage.getItem("wauth_token");
|
||||
const res = await fetch(`/api/users?username=${username}`, {
|
||||
method: "DELETE",
|
||||
headers: { "X-Auth-User": token || "" },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
setMessage({ type: "ok", text: "用户已删除" });
|
||||
fetchUsers();
|
||||
} catch (e) {
|
||||
setMessage({ type: "err", text: e instanceof Error ? e.message : "删除失败" });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col px-4 py-4 md:px-8 md:py-6">
|
||||
<div className="flex items-center gap-3 mb-6 shrink-0">
|
||||
<button type="button" onClick={() => router.back()} className="p-1 -ml-1 rounded hover:bg-surface text-muted hover:text-text transition-colors">
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
<h2 className="text-lg font-bold text-text">用户管理</h2>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`mb-4 px-4 py-2.5 rounded-lg text-[13px] ${message.type === "ok" ? "bg-green-50 text-green-700" : "bg-red-50 text-red-600"}`}>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户列表 */}
|
||||
<div className="rounded-lg border border-line overflow-hidden mb-6">
|
||||
<table className="w-full text-[13px]">
|
||||
<thead className="bg-surface">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-2.5 font-medium text-muted">用户名</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium text-muted">昵称</th>
|
||||
<th className="text-left px-4 py-2.5 font-medium text-muted">角色</th>
|
||||
<th className="text-right px-4 py-2.5 font-medium text-muted">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map(u => (
|
||||
<tr key={u.username} className="border-t border-line">
|
||||
<td className="px-4 py-2.5">{u.username}</td>
|
||||
<td className="px-4 py-2.5">{u.name}</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<span className={`px-2 py-0.5 rounded text-[11px] ${u.role === "admin" ? "bg-accent/10 text-accent" : "bg-surface text-muted"}`}>
|
||||
{u.role === "admin" ? "管理员" : "用户"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-right">
|
||||
<button
|
||||
onClick={() => { setPasswordTarget(u.username); setShowPasswordForm(true); setMessage(null); }}
|
||||
className="p-1.5 rounded hover:bg-surface text-muted hover:text-accent transition-colors"
|
||||
title="修改密码"
|
||||
>
|
||||
<Key size={14} />
|
||||
</button>
|
||||
{isAdmin && u.username !== currentUser?.username && (
|
||||
<button
|
||||
onClick={() => handleDeleteUser(u.username)}
|
||||
className="p-1.5 rounded hover:bg-danger/8 text-muted hover:text-danger transition-colors ml-1"
|
||||
title="删除用户"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 添加用户按钮(仅 admin) */}
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => { setShowAddForm(true); setMessage(null); }}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg border border-line text-[13px] text-muted hover:text-accent hover:border-accent/30 transition-colors mb-6"
|
||||
>
|
||||
<Plus size={14} /> 添加用户
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 修改密码表单 */}
|
||||
{showPasswordForm && (
|
||||
<div className="rounded-lg border border-line p-5 mb-6">
|
||||
<h3 className="font-medium text-sm mb-4">修改密码 — {passwordTarget}</h3>
|
||||
<form onSubmit={handleChangePassword} className="space-y-4">
|
||||
{(!isAdmin || passwordTarget === currentUser?.username) && (
|
||||
<div>
|
||||
<label className="block text-[13px] text-muted mb-1">旧密码</label>
|
||||
<input type="password" value={oldPassword} onChange={e => setOldPassword(e.target.value)}
|
||||
className="w-full max-w-xs px-3 py-2 rounded-lg border border-line bg-bg text-sm outline-none focus:border-accent" required />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-[13px] text-muted mb-1">新密码</label>
|
||||
<input type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)}
|
||||
className="w-full max-w-xs px-3 py-2 rounded-lg border border-line bg-bg text-sm outline-none focus:border-accent" required />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" className="flex items-center gap-1.5 px-4 py-2 rounded-lg bg-accent text-white text-[13px] font-medium hover:opacity-90">
|
||||
<Save size={13} /> 保存
|
||||
</button>
|
||||
<button type="button" onClick={() => setShowPasswordForm(false)} className="px-4 py-2 rounded-lg border border-line text-[13px] text-muted hover:text-text transition-colors">
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 添加用户表单 */}
|
||||
{showAddForm && (
|
||||
<div className="rounded-lg border border-line p-5">
|
||||
<h3 className="font-medium text-sm mb-4">添加用户</h3>
|
||||
<form onSubmit={handleAddUser} className="space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-[13px] text-muted mb-1">用户名</label>
|
||||
<input type="text" value={newUsername} onChange={e => setNewUsername(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-line bg-bg text-sm outline-none focus:border-accent" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[13px] text-muted mb-1">昵称</label>
|
||||
<input type="text" value={newName} onChange={e => setNewName(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-line bg-bg text-sm outline-none focus:border-accent" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[13px] text-muted mb-1">密码</label>
|
||||
<input type="password" value={addPassword} onChange={e => setAddPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-line bg-bg text-sm outline-none focus:border-accent" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[13px] text-muted mb-1">角色</label>
|
||||
<select value={newRole} onChange={e => setNewRole(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-line bg-bg text-sm outline-none focus:border-accent">
|
||||
<option value="user">用户</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" className="flex items-center gap-1.5 px-4 py-2 rounded-lg bg-accent text-white text-[13px] font-medium hover:opacity-90">
|
||||
<User size={13} /> 添加
|
||||
</button>
|
||||
<button type="button" onClick={() => setShowAddForm(false)} className="px-4 py-2 rounded-lg border border-line text-[13px] text-muted hover:text-text transition-colors">
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user