import { useCallback, useMemo, useRef, useState } from "react"; import { AdminPrettyDisplayID, AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx"; import { SideDrawer } from "@/shared/ui/SideDrawer.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { formatMillis } from "@/shared/utils/time.js"; import { getAppUser } from "@/features/app-users/api"; import { appUserStatusLabels } from "@/features/app-users/constants.js"; import { AppUserDetailContext } from "@/features/app-users/components/AppUserDetailContext.jsx"; import { BanCountdown, GenderValue, LevelValue, OperatorValue, RolesValue, formatUsdMinor, } from "@/features/app-users/components/AppUserValues.jsx"; import styles from "@/features/app-users/app-users.module.css"; export function AppUserDetailProvider({ children }) { const { showToast } = useToast(); const requestSeqRef = useRef(0); const [detailUser, setDetailUser] = useState(null); const [loadingUserId, setLoadingUserId] = useState(""); const closeAppUserDetail = useCallback(() => { requestSeqRef.current += 1; setDetailUser(null); setLoadingUserId(""); }, []); const openAppUserDetail = useCallback( async (source) => { const seedUser = normalizeDetailSeed(source); if (!seedUser.userId) { showToast("缺少用户 ID,无法打开用户详情", "error"); return; } const requestSeq = requestSeqRef.current + 1; requestSeqRef.current = requestSeq; setDetailUser(seedUser); setLoadingUserId(seedUser.userId); try { const latestUser = await getAppUser(seedUser.userId); if (requestSeqRef.current === requestSeq) { setDetailUser({ ...seedUser, ...latestUser }); } } catch (err) { if (requestSeqRef.current === requestSeq) { showToast(err.message || "加载用户详情失败", "error"); } } finally { if (requestSeqRef.current === requestSeq) { setLoadingUserId(""); } } }, [showToast], ); const value = useMemo(() => ({ closeAppUserDetail, openAppUserDetail }), [closeAppUserDetail, openAppUserDetail]); return ( {children} ); } function AppUserDetailDrawer({ loading, onClose, open, user }) { if (!open || !user) { return null; } const balances = user.balances || []; const equippedResources = user.equippedResources || []; const resources = user.resources || []; return (
{loading ?
正在刷新用户详情...
: null}

身份信息

} /> } /> } />

等级信息

} /> } /> } /> } />

账号状态

} /> } />

资产余额

当前佩戴

资源资产

); } function DetailItem({ label, value }) { const isElement = value && typeof value === "object"; return (
{label} {isElement ? value : value === undefined || value === null || value === "" ? "-" : value}
); } function PrettyValue({ user }) { if (!user.prettyId || !user.prettyDisplayUserId) { return "-"; } return ; } function VipDetailValue({ vip }) { const level = Number(vip?.level || 0); if (level <= 0 || !vip?.active) { return "VIP 0"; } return vip.name ? `VIP ${level} · ${vip.name}` : `VIP ${level}`; } function AssetBalanceList({ balances }) { if (!balances.length) { return
当前无数据
; } return (
{balances.map((balance) => (
{formatAssetType(balance.assetType)}
冻结 {formatNumber(balance.frozenAmount)}
{formatNumber(balance.availableAmount)}
))}
); } function ResourceList({ resources }) { if (!resources.length) { return
当前无数据
; } return ( ); } function ResourceThumb({ resource }) { const imageUrl = resource.previewUrl || resource.assetUrl || resource.animationUrl; if (!imageUrl) { return {resource.name?.slice(0, 1) || "资"}; } return ; } function AppUserStatus({ status }) { const tone = status === "active" ? "running" : "danger"; return ( {appUserStatusLabels[status] || status || "-"} ); } function normalizeDetailSeed(source) { if (source === undefined || source === null) { return {}; } if (typeof source === "string" || typeof source === "number") { return { userId: String(source).trim() }; } const userId = firstNonEmpty( source.userId, source.user_id, source.assignedUserId, source.assigned_user_id, source.targetUserId, source.target_user_id, ); return { ...source, avatar: firstNonEmpty(source.avatar), defaultDisplayUserId: firstNonEmpty(source.defaultDisplayUserId, source.default_display_user_id), displayUserId: firstNonEmpty(source.displayUserId, source.display_user_id), prettyDisplayUserId: firstNonEmpty(source.prettyDisplayUserId, source.pretty_display_user_id), prettyId: firstNonEmpty(source.prettyId, source.pretty_id), userId, username: firstNonEmpty(source.username, source.name, source.account), }; } function formatCountry(user) { return user.countryDisplayName || user.countryName || user.country || "-"; } function formatNumber(value) { const number = Number(value || 0); return Number.isFinite(number) ? number.toLocaleString() : "-"; } function formatAge(value) { if (value === undefined || value === null || value === "") { return "-"; } const age = Number(value); return Number.isFinite(age) && age >= 0 ? `${Math.trunc(age)} 岁` : "-"; } function formatVipExpire(vip) { if (Number(vip?.level || 0) <= 0) { return "-"; } return formatExpireTime(vip?.expiresAtMs); } function formatAssetType(value) { const normalized = String(value || "").toUpperCase(); const labels = { COIN: "金币", DIAMOND: "钻石", }; return labels[normalized] || normalized || "-"; } function formatResourceType(value) { const labels = { avatar_frame: "头像框", badge: "徽章", chat_bubble: "聊天气泡", emoji_pack: "表情包", floating_screen: "飘屏", gift: "礼物", mic_seat_animation: "麦位动画", mic_seat_icon: "麦位图标", profile_card: "资料卡", vehicle: "座驾", }; return labels[value] || value || "-"; } function formatExpireTime(value) { const expiresAtMs = Number(value || 0); if (!Number.isFinite(expiresAtMs) || expiresAtMs <= 0) { return "永久"; } return formatMillis(expiresAtMs); } function firstNonEmpty(...values) { for (const value of values) { const normalized = String(value ?? "").trim(); if (normalized) { return normalized; } } return ""; }