Files
browser/platform_web/components/GamePlayerIntelligencePanel.tsx
T

21 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { RefreshCw, ShieldAlert, UsersRound } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { platformApiClient } from "../api/client";
import type { GamePlayerProfileResponse, GamePlayerResponse } from "../api/types";
import { ErrorState, LoadingState } from "./StateViews";
type State = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; players: GamePlayerResponse[]; selected?: GamePlayerProfileResponse };
export function GamePlayerIntelligencePanel({ serverInstanceId }: { serverInstanceId: string }) {
const [state, setState] = useState<State>({ status: "loading" });
const load = useCallback(async () => { setState({ status: "loading" }); try { const result = await platformApiClient.listGamePlayers(serverInstanceId); setState({ status: "ready", players: result.items }); } catch (error) { setState({ status: "error", reason: error instanceof Error ? error.message : "玩家档案读取失败。" }); } }, [serverInstanceId]);
useEffect(() => { void load(); }, [load]);
async function select(player: GamePlayerResponse) { try { const selected = await platformApiClient.getGamePlayerProfile(serverInstanceId, player.id); setState((current) => current.status === "ready" ? { ...current, selected } : current); } catch (error) { setState({ status: "error", reason: error instanceof Error ? error.message : "玩家档案读取失败。" }); } }
if (state.status === "loading") return <LoadingState label="正在加载本地游戏玩家档案…" />;
if (state.status === "error") return <ErrorState title="玩家档案不可用" reason={state.reason} onRetry={() => void load()} />;
const profile = state.selected;
return <section className="console-panel" aria-label="SCUM 本地玩家档案与风险信号"><div className="panel-header"><div><h2><UsersRound size={16} /> 本地玩家档案与登录轨迹</h2><p className="provider-id">游戏身份独立于平台账号;网络数据仅用于服务器内不可逆关联,绝不展示或持久化原始值。</p></div><button type="button" className="icon-command" onClick={() => void load()}><RefreshCw size={14} /><span>刷新</span></button></div><div className="console-record-list">{state.players.length ? state.players.map((player) => <button type="button" className="console-record" key={player.id} onClick={() => void select(player)}><strong>{player.displayName}</strong><span>游戏 ID{player.gamePlayerId}</span><small>最近登录:{formatTime(player.lastSeenAt)}</small></button>) : <p className="page-status">暂无已投影的成功登录记录。</p>}</div>{profile && <div className="console-module"><div className="panel-header"><h3>{profile.player.displayName} 的审阅记录</h3><span className="page-status">别名 {profile.aliases.length} · 会话 {profile.sessions.length}</span></div><div className="console-row-list"><div className="console-row"><strong>别名</strong><span>{profile.aliases.map((alias) => alias.alias).join(" / ") || "无"}</span></div>{profile.sessions.slice(0, 10).map((session) => <div className="console-row" key={session.id}><strong>会话</strong><span>{formatTime(session.startedAt)} {session.endedAt ? formatTime(session.endedAt) : "仍在线"} {session.endReason ? `(${session.endReason})` : ""}</span></div>)}</div><div className="console-record-list">{profile.securitySignals.length ? profile.securitySignals.map((signal) => <div className="console-record" key={signal.id}><ShieldAlert size={15} aria-hidden="true" /><strong>{signal.ruleKey}</strong><span>{signal.status === "review-required" ? "需要人工审核" : signal.status}</span><small>{signal.summary} · 证据 {signal.evidenceCount} </small></div>) : <p className="page-status">未发现需要人工审核的风险信号。</p>}</div>{profile.accessAttempts.length > 0 && <p className="page-status">失败尝试:{profile.accessAttempts.length} 条(仅显示结果与时间,不显示网络标识)。</p>}</div>}</section>;
}
function formatTime(value: string) { const date = new Date(value); return Number.isNaN(date.valueOf()) ? "--" : date.toLocaleString("zh-CN", { hour12: false }); }