"use client"; import { FormEvent, useEffect, useState } from "react"; import { adjustQuota, changeUserStatus, createUser, deleteUser, fetchCsrf, grantMembership, getUserDetail, getUserQuota, listUsers, revokeMembership, updateUserProfile, type AdminUser, type AdminUserDetail, } from "@/lib/admin"; import { StatusCard } from "@/components/ui"; type UserRow = AdminUser; export function UsersPanel() { const [rows, setRows] = useState([]); const [total, setTotal] = useState(0); const [pages, setPages] = useState(1); const [page, setPage] = useState(1); const [email, setEmail] = useState(""); const [phone, setPhone] = useState(""); const [status, setStatus] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [selected, setSelected] = useState(null); const [notice, setNotice] = useState(""); const [showCreate, setShowCreate] = useState(false); async function load(pageNum = page) { setLoading(true); setError(""); try { const res = await listUsers({ email, phone, status, page: pageNum, limit: 20 }); setRows(res.items); setTotal(res.total); setPages(res.pages); setPage(res.page); } catch (e) { setError(e instanceof Error ? e.message : "加载失败"); } finally { setLoading(false); } } useEffect(() => { void load(1); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); async function openDetail(id: string) { setError(""); try { const detail = await getUserDetail(id); setSelected(detail); } catch (e) { setError(e instanceof Error ? e.message : "加载详情失败"); } } return (
{showCreate ? { setShowCreate(false); setNotice("用户已创建"); void load(1); }} /> : null}
setEmail(e.target.value)} placeholder="按邮箱筛选" value={email} /> setPhone(e.target.value)} placeholder="按手机号筛选" value={phone} />
{error ?
{error}
: null}
{loading ? ( ) : rows.length === 0 ? ( ) : ( rows.map((row) => ( )) )}
用户 有效 Plan 角色 状态 注册时间
加载中…
无匹配用户

{row.username}

{row.email ?? "无邮箱"} · {row.phone ?? "无手机号"}

{row.effective_plan.toUpperCase()} {row.role} {row.status === "active" ? "正常" : "已禁用"} {new Date(row.created_at).toLocaleDateString()}
第 {page} / {pages} 页
{selected ? void openDetail(selected.user.id)} setNotice={setNotice} onClose={() => setSelected(null)} /> : null} {notice ?
{notice}
: null}
); } function UserDetailPanel({ detail, onRefresh, setNotice, onClose }: { detail: AdminUserDetail; onRefresh: () => void; setNotice: (msg: string) => void; onClose: () => void }) { const [csrf, setCsrf] = useState(""); const [reason, setReason] = useState(""); const [vipDays, setVipDays] = useState("30"); const [amount, setAmount] = useState("100"); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); const [username, setUsername] = useState(detail.user.username); const [email, setEmail] = useState(detail.user.email ?? ""); const [phone, setPhone] = useState(detail.user.phone ?? ""); const [emailVerified, setEmailVerified] = useState(detail.user.email_verified); const [phoneVerified, setPhoneVerified] = useState(detail.user.phone_verified); useEffect(() => { void fetchCsrf().then(setCsrf); }, []); async function run(fn: () => Promise, okMsg: string) { if (!reason.trim()) { setMessage("请填写原因"); return; } setBusy(true); setMessage(""); try { await fn(); setNotice(okMsg); onRefresh(); } catch (e) { setMessage(e instanceof Error ? e.message : "操作失败"); } finally { setBusy(false); } } function toggleStatus() { const target: "active" | "disabled" = detail.user.status === "active" ? "disabled" : "active"; return run(() => changeUserStatus(detail.user.id, target, reason, csrf), target === "disabled" ? "已禁用并撤销会话" : "已恢复"); } function doGrant() { const days = Number(vipDays); const expiresAt = days > 0 ? new Date(Date.now() + days * 86400000).toISOString() : null; return run(() => grantMembership(detail.user.id, "vip", expiresAt, reason, csrf), "已授予 VIP"); } function doRevoke() { return run(() => revokeMembership(detail.user.id, reason, csrf), "已撤销 VIP"); } function doAdjust() { const amt = Number(amount); if (!Number.isFinite(amt)) { setMessage("额度需为数字"); return; } return run(() => adjustQuota(detail.user.id, amt, reason, `admin-${Date.now()}-${Math.random()}`, csrf), "额度已调整"); } function saveProfile() { if (!username.trim()) { setMessage("用户名不能为空"); return; } if (!email.trim() && !phone.trim()) { setMessage("邮箱或手机号至少填写一项"); return; } return run(() => updateUserProfile(detail.user.id, { username: username.trim(), email: email.trim() || null, phone: phone.trim() || null, email_verified: emailVerified, phone_verified: phoneVerified, reason: reason.trim() }, csrf), "用户信息已保存"); } async function removeUser() { if (!reason.trim()) { setMessage("请填写原因"); return; } if (!window.confirm("确定删除该用户?删除后不可恢复。")) return; setBusy(true); try { await deleteUser(detail.user.id, reason, csrf); setNotice("用户已删除"); onClose(); } catch (e) { setMessage(e instanceof Error ? e.message : "删除失败"); } finally { setBusy(false); } } return (

{detail.user.username}

有效 Plan:{detail.effective_plan.toUpperCase()} · 状态:{detail.user.status === "active" ? "正常" : "已禁用"}

额度周期

{new Date(detail.quota.period_start).toLocaleDateString()} → {new Date(detail.quota.period_end).toLocaleDateString()}

上限
{detail.quota.limit.toLocaleString()}
已用
{detail.quota.used.toLocaleString()}
冻结
{detail.quota.reserved.toLocaleString()}
调整
{detail.quota.adjustment.toLocaleString()}
可用
{detail.quota.available.toLocaleString()}

当前会员

{detail.effective_grant ? (

{detail.effective_grant.plan.toUpperCase()}

至 {detail.effective_grant.expires_at ? new Date(detail.effective_grant.expires_at).toLocaleDateString() : "长期"}

) : (

无有效会员

)} {detail.membership_history.length ? (
    {detail.membership_history.slice(0, 4).map((g) => (
  • {g.plan.toUpperCase()} · {new Date(g.starts_at).toLocaleDateString()} → {g.expires_at ? new Date(g.expires_at).toLocaleDateString() : "长期"}{g.revoked_at ? " · 已撤销" : ""}
  • ))}
) : null}

额度调整记录

{detail.adjustment_records.length ? (
    {detail.adjustment_records.map((r) => (
  • {r.amount > 0 ? "+" : ""}{r.amount.toLocaleString()} · {r.reason ?? "—"}
  • ))}
) : (

暂无调整记录

)}

用户信息

setUsername(e.target.value)} value={username} />
setEmail(e.target.value)} type="email" value={email} />
setPhone(e.target.value)} value={phone} />