import React, { useEffect, useMemo, useState } from "react"; import { API_BASE, emptyForm, formatIsoDateTimeReadable, parseEmailList } from "../config"; import icons from "../icons"; import { IconLabel, MailtoEmail, TableCell } from "./common"; export default function AdminPanel({ onReady }) { const queryToken = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("token") || ""; }, []); const [token, setToken] = useState(queryToken || localStorage.getItem("sproutgate_admin_token") || ""); const [users, setUsers] = useState([]); const [form, setForm] = useState(emptyForm); const [selectedAccount, setSelectedAccount] = useState(""); const [editorOpen, setEditorOpen] = useState(false); const [editorMode, setEditorMode] = useState("create"); const [loading, setLoading] = useState(false); const [configLoading, setConfigLoading] = useState(false); const [configSaving, setConfigSaving] = useState(false); const [error, setError] = useState(""); const [configError, setConfigError] = useState(""); const [message, setMessage] = useState(""); const [configMessage, setConfigMessage] = useState(""); const [checkInConfig, setCheckInConfig] = useState({ rewardCoins: 1 }); const [readySent, setReadySent] = useState(false); const [regLoading, setRegLoading] = useState(false); const [regSaving, setRegSaving] = useState(false); const [regInvCreating, setRegInvCreating] = useState(false); const [regError, setRegError] = useState(""); const [regMessage, setRegMessage] = useState(""); const [requireInviteReg, setRequireInviteReg] = useState(false); const [inviteList, setInviteList] = useState([]); const [newInvNote, setNewInvNote] = useState(""); const [newInvMax, setNewInvMax] = useState(0); const [newInvExp, setNewInvExp] = useState(""); useEffect(() => { if (token) { localStorage.setItem("sproutgate_admin_token", token); loadUsers(); loadCheckInConfig(); loadRegistration(); } else if (onReady && !readySent) { onReady(); setReadySent(true); } }, [token, onReady, readySent]); const loadUsers = async () => { if (!token) return; setLoading(true); setError(""); try { const res = await fetch(`${API_BASE}/api/admin/users`, { headers: { "X-Admin-Token": token } }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "加载用户失败"); setUsers(data.users || []); } catch (err) { setError(err.message); } finally { setLoading(false); if (onReady && !readySent) { onReady(); setReadySent(true); } } }; const loadCheckInConfig = async () => { if (!token) return; setConfigLoading(true); setConfigError(""); try { const res = await fetch(`${API_BASE}/api/admin/check-in/config`, { headers: { "X-Admin-Token": token } }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "加载签到配置失败"); setCheckInConfig({ rewardCoins: Number(data.rewardCoins) || 1 }); } catch (err) { setConfigError(err.message); } finally { setConfigLoading(false); } }; const loadRegistration = async () => { if (!token) return; setRegLoading(true); setRegError(""); try { const res = await fetch(`${API_BASE}/api/admin/registration`, { headers: { "X-Admin-Token": token } }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "加载注册策略失败"); setRequireInviteReg(Boolean(data.requireInviteCode)); setInviteList(data.invites || []); } catch (err) { setRegError(err.message); } finally { setRegLoading(false); } }; const saveRegPolicy = async () => { if (!token) return; setRegMessage(""); setRegError(""); setRegSaving(true); try { const res = await fetch(`${API_BASE}/api/admin/registration`, { method: "PUT", headers: { "Content-Type": "application/json", "X-Admin-Token": token }, body: JSON.stringify({ requireInviteCode: requireInviteReg }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "保存失败"); setRequireInviteReg(Boolean(data.requireInviteCode)); setRegMessage("注册策略已保存"); } catch (err) { setRegError(err.message); } finally { setRegSaving(false); } }; const handleCreateInvite = async () => { if (!token) return; setRegMessage(""); setRegError(""); let expiresAt = ""; if (newInvExp.trim()) { const d = new Date(newInvExp); if (Number.isNaN(d.getTime())) { setRegError("过期时间无效"); return; } expiresAt = d.toISOString(); } setRegInvCreating(true); try { const res = await fetch(`${API_BASE}/api/admin/registration/invites`, { method: "POST", headers: { "Content-Type": "application/json", "X-Admin-Token": token }, body: JSON.stringify({ note: newInvNote.trim(), maxUses: Number(newInvMax) || 0, expiresAt }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "生成失败"); const inv = data.invite; setInviteList((prev) => [...prev, inv]); setRegMessage(`已生成邀请码:${inv.code}(请复制保存)`); setNewInvNote(""); setNewInvMax(0); setNewInvExp(""); } catch (err) { setRegError(err.message); } finally { setRegInvCreating(false); } }; const handleDeleteInvite = async (code) => { if (!token || !code) return; if (!window.confirm(`确认删除邀请码 ${code} 吗?`)) return; setRegMessage(""); setRegError(""); try { const res = await fetch( `${API_BASE}/api/admin/registration/invites/${encodeURIComponent(code)}`, { method: "DELETE", headers: { "X-Admin-Token": token } } ); const data = await res.json(); if (!res.ok) throw new Error(data.error || "删除失败"); setInviteList((prev) => prev.filter((x) => x.code !== code)); setRegMessage("已删除邀请码"); } catch (err) { setRegError(err.message); } }; const selectUser = (user) => { setSelectedAccount(user.account); setForm({ account: user.account, password: "", username: user.username || "", email: user.email || "", level: user.level ?? 0, sproutCoins: user.sproutCoins || 0, secondaryEmails: (user.secondaryEmails || []).join(","), phone: user.phone || "", avatarUrl: user.avatarUrl || "", websiteUrl: user.websiteUrl || "", bio: user.bio || "", banned: Boolean(user.banned), banReason: user.banReason || "" }); setMessage(""); setError(""); setEditorMode("edit"); setEditorOpen(true); }; const clearSelection = () => { setSelectedAccount(""); setForm(emptyForm); }; const openCreateUser = () => { clearSelection(); setEditorMode("create"); setMessage(""); setError(""); setEditorOpen(true); }; const closeEditor = () => { setEditorOpen(false); clearSelection(); setError(""); }; const handleChange = (field, value) => { setForm((prev) => ({ ...prev, [field]: value })); }; const handleCheckInConfigChange = (value) => { setCheckInConfig({ rewardCoins: value }); }; const handleSaveCheckInConfig = async () => { setConfigMessage(""); setConfigError(""); const rewardCoins = Number(checkInConfig.rewardCoins) || 0; if (rewardCoins <= 0) { setConfigError("奖励萌芽币必须大于 0"); return; } try { setConfigSaving(true); const res = await fetch(`${API_BASE}/api/admin/check-in/config`, { method: "PUT", headers: { "Content-Type": "application/json", "X-Admin-Token": token }, body: JSON.stringify({ rewardCoins }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "保存签到配置失败"); setCheckInConfig({ rewardCoins: Number(data.rewardCoins) || rewardCoins }); setConfigMessage("签到配置已保存"); } catch (err) { setConfigError(err.message); } finally { setConfigSaving(false); } }; const handleCreate = async () => { setMessage(""); setError(""); if (!form.account || !form.password) { setError("新建用户需要账户和密码"); return; } try { const res = await fetch(`${API_BASE}/api/admin/users`, { method: "POST", headers: { "Content-Type": "application/json", "X-Admin-Token": token }, body: JSON.stringify({ account: form.account, password: form.password, username: form.username, email: form.email, level: Number(form.level) || 0, sproutCoins: Number(form.sproutCoins) || 0, secondaryEmails: parseEmailList(form.secondaryEmails), phone: form.phone, avatarUrl: form.avatarUrl, websiteUrl: form.websiteUrl, bio: form.bio }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "创建失败"); setMessage("创建成功"); closeEditor(); loadUsers(); } catch (err) { setError(err.message); } }; const handleUpdate = async () => { if (!selectedAccount) { setError("请选择需要更新的账户"); return; } setMessage(""); setError(""); const payload = { username: form.username, email: form.email, level: Number(form.level) || 0, sproutCoins: Number(form.sproutCoins) || 0, secondaryEmails: parseEmailList(form.secondaryEmails), phone: form.phone, avatarUrl: form.avatarUrl, websiteUrl: form.websiteUrl, bio: form.bio, banned: Boolean(form.banned), banReason: (form.banReason || "").trim() }; if (form.password) payload.password = form.password; try { const res = await fetch(`${API_BASE}/api/admin/users/${encodeURIComponent(selectedAccount)}`, { method: "PUT", headers: { "Content-Type": "application/json", "X-Admin-Token": token }, body: JSON.stringify(payload) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "更新失败"); setMessage("更新成功"); setForm((prev) => ({ ...prev, password: "" })); closeEditor(); loadUsers(); } catch (err) { setError(err.message); } }; const handleDelete = async (account) => { if (!account) return; if (!window.confirm(`确认删除账户 ${account} 吗?`)) return; setMessage(""); setError(""); try { const res = await fetch(`${API_BASE}/api/admin/users/${encodeURIComponent(account)}`, { method: "DELETE", headers: { "X-Admin-Token": token } }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "删除失败"); setMessage("删除成功"); if (account === selectedAccount) clearSelection(); loadUsers(); } catch (err) { setError(err.message); } }; const editingUser = users.find((u) => u.account === selectedAccount); const editingAuthClients = editingUser?.authClients || []; return (
管理员控制台

管理员 Token

签到设置

用户每天首次签到会获得这里设置的奖励。
{configError &&
{configError}
} {configMessage &&
{configMessage}
}

注册与邀请码

公开接口 GET /api/public/registration-policy 供前端判断是否显示邀请码输入框。
{regError &&
{regError}
} {regMessage &&
{regMessage}
}

生成新邀请码

已有邀请码

{inviteList.length === 0 ? (
暂无邀请码
) : (
{inviteList.map((inv) => (
{inv.code} {inv.note ? · {inv.note} : null}
已用 {inv.uses ?? 0} {inv.maxUses > 0 ? ` / 上限 ${inv.maxUses}` : " / 不限次数"} {inv.expiresAt ? ` · 过期 ${formatIsoDateTimeReadable(inv.expiresAt)}` : ""}
))}
)}

用户列表

{message &&
{message}
} {users.length === 0 &&
暂无用户
}
账户 用户名 邮箱 等级 萌芽币 状态 最近 IP 最近位置 操作
{users.map((u) => (
selectUser(u)}>{u.account} {u.username || "-"} {u.email ? ( {u.email} ) : ( - )} {(u.secondaryEmails || []).length > 0 && ( <> / {(u.secondaryEmails || []).map((em, idx) => ( {idx > 0 ? , : null} {em} ))} )} {u.level ?? 0} 级 {u.sproutCoins} {u.banned ? ( 封禁 ) : ( "正常" )} {u.lastVisitIp || "-"} {u.lastVisitDisplayLocation || "-"}
))}
{editorOpen && (
e.stopPropagation()}>

{editorMode === "edit" ? "编辑用户" : "新建用户配置"}

{editorMode === "edit" ? "修改账户资料后保存" : "点击保存后创建新用户"}