This commit is contained in:
Vaica committed 2026-06-22 14:59:24 +08:00
1 parent 4ab47a3e47
commit ba016efbe8
21 files changed
+921 -72

No files matched your search

+263
View File
@@ -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>
);
}