import { useEffect, useId, useMemo, useState, type FormEvent } from "react"; import "./admin-users.css"; interface AdminSession { csrf_token: string; } interface CreditBalance { available_balance: number; reserved_balance: number; updated_at: string; } async function readJson(url: string, init?: RequestInit) { const response = await fetch(url, { credentials: "same-origin", ...init }); if (response.status === 401) throw new Error("admin_session_invalid"); if (!response.ok) throw new Error("request_failed"); return response.json() as Promise; } function idempotencyKey() { return crypto.randomUUID().replaceAll("-", "") + crypto.randomUUID().replaceAll("-", ""); } export function AdminUsersPage() { const amountId = useId(); const reasonId = useId(); const userId = new URLSearchParams(window.location.search).get("userId") ?? ""; const [session, setSession] = useState(); const [balance, setBalance] = useState(); const [failed, setFailed] = useState(false); const [adjusting, setAdjusting] = useState(false); const [direction, setDirection] = useState<"increase" | "decrease">("increase"); const [amount, setAmount] = useState(""); const [reason, setReason] = useState(""); const [adjustmentId, setAdjustmentId] = useState(""); const [adjustmentKey, setAdjustmentKey] = useState(""); const [submitting, setSubmitting] = useState(false); const [notice, setNotice] = useState(""); useEffect(() => { if (!/^[0-9a-f-]{36}$/i.test(userId)) return; Promise.all([ readJson("/api/v1/admin-auth/session"), readJson(`/api/v1/admin/users/${userId}/credits`), ]).then(([nextSession, nextBalance]) => { setSession(nextSession); setBalance(nextBalance); }).catch(() => setFailed(true)); }, [userId]); const signedAmount = useMemo(() => { const parsed = Number(amount); if (!Number.isSafeInteger(parsed) || parsed <= 0) return 0; return direction === "increase" ? parsed : -parsed; }, [amount, direction]); const projected = balance ? balance.available_balance + signedAmount : 0; function openAdjustment() { setDirection("increase"); setAmount(""); setReason(""); setAdjustmentId(crypto.randomUUID()); setAdjustmentKey(idempotencyKey()); setAdjusting(true); setNotice(""); } async function submitAdjustment(event: FormEvent) { event.preventDefault(); if (!session || !balance || !adjustmentId || !adjustmentKey || signedAmount === 0 || !reason.trim() || submitting) return; setSubmitting(true); try { const result = await readJson( `/api/v1/admin/users/${userId}/credit-adjustments`, { body: JSON.stringify({ adjustment_id: adjustmentId, amount: signedAmount, reason: reason.trim() }), headers: { "Content-Type": "application/json", "Idempotency-Key": adjustmentKey, "X-CSRF-Token": session.csrf_token, }, method: "POST", }, ); setBalance({ available_balance: result.available_balance, reserved_balance: result.reserved_balance, updated_at: new Date().toISOString(), }); setAdjusting(false); setNotice("点数调整已完成并记录审计"); } catch { setNotice("点数调整未完成,请检查输入后重试"); } finally { setSubmitting(false); } } return (

USER OPERATIONS

用户点数

{balance ? : null}
{!userId ?

从用户列表选择用户后调整点数。

: null} {failed ?

用户点数暂时无法读取。

: null} {userId && !balance && !failed ?

正在读取用户点数

: null} {balance ? (
内部用户标识{userId}
可用点数{balance.available_balance}
冻结点数{balance.reserved_balance}
) : null} {notice ?

{notice}

: null}
{adjusting && balance ? (

CREDIT ADJUSTMENT

调整点数

setAmount(event.target.value.replace(/\D/g, ""))} type="number" value={amount} />