import { CarFront, MapPinned, RefreshCw, UserRound } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { platformApiClient } from "../api/client"; import type { GameMapTrajectoryEntityResponse, GameMapTrajectoryResponse } from "../api/types"; import { ErrorState, LoadingState } from "./StateViews"; type State = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; value: GameMapTrajectoryResponse }; export function ScumMapTrajectoryPanel({ serverInstanceId }: { serverInstanceId: string }) { const [state, setState] = useState({ status: "loading" }); const [hours, setHours] = useState("1"); const [playerIds, setPlayerIds] = useState(""); const [vehicleIds, setVehicleIds] = useState(""); const [detail, setDetail] = useState(""); const load = useCallback(async () => { setState({ status: "loading" }); const to = new Date(); const from = new Date(to.valueOf() - Math.max(1, Math.min(24, Number(hours) || 1)) * 3600000); try { setState({ status: "ready", value: await platformApiClient.getGameMapTrajectories(serverInstanceId, { from: from.toISOString(), to: to.toISOString(), playerIds: ids(playerIds), vehicleIds: ids(vehicleIds) }) }); } catch (error) { setState({ status: "error", reason: error instanceof Error ? error.message : "地图轨迹读取失败。" }); } }, [hours, playerIds, serverInstanceId, vehicleIds]); useEffect(() => { void load(); }, [load]); async function openPlayer(entity: GameMapTrajectoryEntityResponse) { if (!entity.gamePlayerRecordId) return; try { const profile = await platformApiClient.getGamePlayerProfile(serverInstanceId, entity.gamePlayerRecordId); setDetail(`玩家详情:${profile.player.displayName}(${profile.player.gamePlayerId}),别名 ${profile.aliases.length} 条,会话 ${profile.sessions.length} 条。`); } catch (error) { setDetail(error instanceof Error ? error.message : "玩家详情不可用。"); } } if (state.status === "loading") return ; if (state.status === "error") return void load()} />; const value = state.value; return

SCUM 玩家与车辆地图轨迹

仅显示插件声明坐标转换后的安全投影;数据来源与采集时间始终可解释。

{value.status === "missing-map" ?

缺失地图声明:{value.reason}

: <>

地图 {value.map?.mapId} · 版本 {value.map?.mapVersion} · 精度 {value.map?.precision};窗口 {formatTime(value.from)} 至 {formatTime(value.to)}

{value.status === "empty" ?

{value.reason}

: setDetail(`车辆详情上下文:${entity.entityId}。此链接只使用服务器内安全车辆标识,不查询原始存储。`)} />}{value.rideSegments.length > 0 &&
{value.rideSegments.map((segment) =>
乘车区段玩家 {segment.gamePlayerRecordId} · 车辆 {segment.vehicleId}{formatTime(segment.startedAt)} → {segment.endedAt ? formatTime(segment.endedAt) : "采集窗口内仍在车上"}
)}
}}{detail &&

{detail}

}
; } export function trajectoryPath(points: { mapX: number; mapY: number }[]) { return points.map((point, index) => `${index ? "L" : "M"}${point.mapX},${1000 - point.mapY}`).join(" "); } function TrajectoryMap({ players, vehicles, onPlayer, onVehicle }: { players: GameMapTrajectoryEntityResponse[]; vehicles: GameMapTrajectoryEntityResponse[]; onPlayer: (entity: GameMapTrajectoryEntityResponse) => void; onVehicle: (entity: GameMapTrajectoryEntityResponse) => void }) { const entities = useMemo(() => [...players, ...vehicles], [players, vehicles]); return <>
{entities.map((entity) => {entity.points.map((point, index) => {`${entity.label} · ${formatTime(point.occurredAt)} · ${point.source}`})})}
{entities.map((entity) =>
{entity.kind === "player" ? : } {entity.label}{entity.points.length} 个采样点 · 来源 {entity.sources.join(" / ") || "--"}最后采集:{formatTime(entity.collectedAt)}
)}
; } function ids(value: string) { return value.split(",").map((item) => item.trim()).filter(Boolean).slice(0, 20); } function formatTime(value: string | undefined) { if (!value) return "--"; const date = new Date(value); return Number.isNaN(date.valueOf()) ? "--" : date.toLocaleString("zh-CN", { hour12: false }); }