Send user trajectories from the user list into a focused live map
- Open 「用户轨迹」 straight on the live map instead of a drawer, carrying the server, the focused user and a start/end window through the plugin page hash. - Default that window to one hour before the user's last seen time and keep the plugin page query available to bundles through the host context. - Replace the time range preset gate with always visible start/end datetime fields plus a quick-range select, so the user list rebuilds from the window. - Drop the bulky trajectory list and draw one compact colour legend under the map, keeping a stable colour per identity and restricting vehicle tracks to the vehicles the selected users actually rode.
This commit is contained in:
@@ -357,7 +357,7 @@ describe("plugin manifest validation", () => {
|
||||
const serialized = JSON.stringify(manifest).toLowerCase();
|
||||
|
||||
expect(serialized).not.toContain("local-proof");
|
||||
expect(manifest.version).toBe("0.1.22");
|
||||
expect(manifest.version).toBe("0.1.23");
|
||||
expect(installAction.environment?.SERVER_TEMPLATE).toBe("scum-server");
|
||||
expect(manifest.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"]));
|
||||
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining([
|
||||
|
||||
@@ -14,6 +14,8 @@ const featureRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../example
|
||||
const pageSource = readFileSync(resolve(featureRoot, "page.ts"), "utf8");
|
||||
const dataClientSource = readFileSync(resolve(featureRoot, "page-data.ts"), "utf8");
|
||||
|
||||
const mapTestWindow = { from: "2026-08-09T23:00:00Z", to: "2026-08-10T01:00:00Z" };
|
||||
|
||||
const surfaceData: SCUMSurfaceData = {
|
||||
players: [{ gamePlayerId: "steam-1", steamId: "76561198000000001", userProfileId: "profile-1", displayName: "Mira", squadName: "Wolves", squadId: "squad-1", online: true, famePoints: 42, normalBalance: 1000, goldBalance: 3, lastLoginIp: "203.0.113.7", position: { x: 10, y: 20, z: 3 }, freshness: { status: "fresh" } }],
|
||||
squads: [{ squadId: "squad-1", name: "Wolves", memberCount: 1, memberLimit: 12, leaderProfileId: "profile-1", score: 88, message: "Hold the north", freshness: { status: "fresh" } }],
|
||||
@@ -202,6 +204,8 @@ describe("SCUM plugin feature module", () => {
|
||||
expect(view.texts).not.toContain("通用数据/机器动作可用");
|
||||
expect(view.buttons.map((button) => button.label)).not.toEqual(expect.arrayContaining([`同步 ${"SCUM"}.db`, "重新读取", "编辑属性"]));
|
||||
expect(view.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["礼包状态", "他的物品", "登录历史", "用户轨迹"]));
|
||||
expect(pageSource).not.toContain('open("trajectory")');
|
||||
expect(pageSource).toContain("/live-map?${query.toString()}");
|
||||
expect(view.inputs.map((input) => input.label)).toContain("搜索用户");
|
||||
expect(pageSource).toContain("console-panel scum-workbench");
|
||||
expect(pageSource).toContain("resource-filter-bar scum-filter-bar");
|
||||
@@ -224,7 +228,7 @@ describe("SCUM plugin feature module", () => {
|
||||
|
||||
it("keeps user management off SCUM database SQL execution paths", () => {
|
||||
const source = `${pageSource}\n${dataClientSource}`;
|
||||
expect(source).toContain("平台 scum_user_trajectory 表");
|
||||
expect(source).toContain("平台 scum_user_trajectory 与 scum_vehicle_trajectory 表");
|
||||
expect(source).toContain("平台 SCUM 用户与载具表");
|
||||
expect(source).not.toContain("remote.run.db.sqlite.execute");
|
||||
expect(source).not.toContain("scum-database");
|
||||
@@ -283,7 +287,7 @@ describe("SCUM plugin feature module", () => {
|
||||
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图" });
|
||||
expect(view.nodes).toContain("div:SCUM 地图图层");
|
||||
expect(view.nodes).toContain("div:SCUM 地图视图");
|
||||
expect(view.inputs.map((input) => input.label)).toContain("轨迹时间范围");
|
||||
expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["轨迹快捷时间", "轨迹开始时间", "轨迹结束时间"]));
|
||||
expect(view.inputs.map((input) => input.label)).not.toContain("筛选地图点");
|
||||
for (const layer of ["用户", "载具", "旗帜", "区域", "其他"]) expect(view.texts).toContain(layer);
|
||||
expect(view.texts).toContain("地图点详情");
|
||||
@@ -302,12 +306,13 @@ describe("SCUM plugin feature module", () => {
|
||||
expect(view.texts.some((text) => /\d+ \/ \d+ 个点 · \d+ 条轨迹/.test(text))).toBe(true);
|
||||
for (const layer of ["地形", "等高线", "夜间", "建筑区"]) expect(view.texts).toContain(layer);
|
||||
expect(view.buttons.map((button) => button.label)).not.toEqual(expect.arrayContaining(["地形", "等高线", "夜间"]));
|
||||
expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["轨迹时间范围", "地图底图", "轨迹回放进度"]));
|
||||
expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["轨迹快捷时间", "轨迹开始时间", "轨迹结束时间", "地图底图", "轨迹回放进度"]));
|
||||
expect(view.inputs.map((input) => input.label)).not.toContain("启用自定义地图");
|
||||
expect(view.texts).not.toContain("地图范围与图层");
|
||||
expect(view.texts).not.toContain("自定义范围");
|
||||
expect(view.elements.map((element) => element.label)).toEqual(expect.arrayContaining(["轨迹用户筛选", "地图图层筛选"]));
|
||||
expect(view.texts).toContain("轨迹列表");
|
||||
expect(view.texts).not.toContain("轨迹列表");
|
||||
expect(view.elements.map((element) => element.label)).toContain("轨迹颜色图例");
|
||||
expect(view.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["全选", "清空", "不限", "回放", "回到最新"]));
|
||||
expect(pageSource).toContain('new URL("../assets/map/scum-map-building-zones-4096.png", import.meta.url).href');
|
||||
expect(pageSource).toContain("mapBoardStyle(baseLayer, zonesOverlay)");
|
||||
@@ -320,12 +325,12 @@ describe("SCUM plugin feature module", () => {
|
||||
{ subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 120, y: 140, z: 3, riddenVehicleId: "veh-1", gameVehicleId: "veh-1", sampledAt: "2026-08-10T00:00:03Z" },
|
||||
{ subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 180, y: 140, z: 3, riddenVehicleId: "veh-1", gameVehicleId: "veh-1", sampledAt: "2026-08-10T00:00:04Z" }
|
||||
] };
|
||||
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data: riding });
|
||||
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data: riding, pageQuery: mapTestWindow });
|
||||
expect(view.nodes).toContain("polyline:");
|
||||
expect(view.texts).toEqual(expect.arrayContaining(["用户 · 4 点 · 步行 2 / 乘车 2"]));
|
||||
expect(view.elements.filter((element) => element.className.includes("map-ride-marker")).length).toBeGreaterThan(0);
|
||||
expect(view.elements.filter((element) => element.className.includes("map-track-endpoint")).length).toBeGreaterThan(1);
|
||||
expect(view.texts).toContain("Mira · 4");
|
||||
expect(view.texts).toContain("Mira · 4 点");
|
||||
});
|
||||
|
||||
it("keeps the extracted icon catalog closed until an editor asks for it", () => {
|
||||
@@ -337,8 +342,33 @@ describe("SCUM plugin feature module", () => {
|
||||
expect(workflows.inputs.map((input) => input.label)).not.toContain("搜索生成物品");
|
||||
});
|
||||
|
||||
it("focuses the live map on the user who was picked from the user list", () => {
|
||||
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", pageQuery: { focus: "76561198000000001", from: "2026-08-09T23:00:00Z", to: "2026-08-10T01:00:00Z" } });
|
||||
expect(view.texts).toContain("用户 1/1");
|
||||
expect(view.inputs.find((input) => input.label === "轨迹开始时间")?.value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
|
||||
expect(view.inputs.find((input) => input.label === "轨迹结束时间")?.value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
|
||||
expect(pageSource).toContain('"lastSeenAt", "lastLoginTime", "lastLoginAt", "lastLoginObservedAt", "onlineObservedAt", "updatedAt"');
|
||||
expect(pageSource).toContain("scumMapDefaultWindowMs");
|
||||
});
|
||||
|
||||
it("keeps only the selected user's tracks and the vehicles that user rode", () => {
|
||||
const data: SCUMSurfaceData = { ...surfaceData, trajectories: [
|
||||
{ subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 10, y: 20, z: 3, riddenVehicleId: "veh-1", gameVehicleId: "veh-1", sampledAt: "2026-08-10T00:00:01Z" },
|
||||
{ subjectType: "player", subjectId: "76561198000000009", steamId: "76561198000000009", displayName: "Nova", x: 30, y: 40, z: 3, sampledAt: "2026-08-10T00:00:02Z" },
|
||||
{ subjectType: "vehicle", subjectId: "veh-1", vehicleId: "veh-1", gameVehicleId: "veh-1", label: "Laika", className: "BPC_Laika_C", x: 400, y: 200, z: 0, sampledAt: "2026-08-10T00:00:03Z" },
|
||||
{ subjectType: "vehicle", subjectId: "veh-2", vehicleId: "veh-2", gameVehicleId: "veh-2", label: "WolfsWagen", className: "BPC_WolfsWagen_C", x: 500, y: 300, z: 0, sampledAt: "2026-08-10T00:00:04Z" }
|
||||
] };
|
||||
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data, pageQuery: { ...mapTestWindow, focus: "76561198000000001" } });
|
||||
expect(view.texts).toContain("Mira");
|
||||
expect(view.texts).toContain("Laika");
|
||||
expect(view.texts).not.toContain("Nova");
|
||||
expect(view.texts).not.toContain("WolfsWagen");
|
||||
const all = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data, pageQuery: mapTestWindow });
|
||||
expect(all.texts).toEqual(expect.arrayContaining(["Mira", "Nova", "Laika", "WolfsWagen"]));
|
||||
});
|
||||
|
||||
it("loads the vehicle icon for trajectory rows and gift items", () => {
|
||||
expect(pageSource).toContain('trajectoryVehicleIcon(row) ? e("img", { className: "scum-catalog-icon"');
|
||||
expect(pageSource).toContain("trajectoryVehicleIcon(middle.row)");
|
||||
expect(pageSource).toContain("catalogIconStrip(e, giftCatalogIcons(gift))");
|
||||
});
|
||||
|
||||
@@ -395,7 +425,7 @@ function scumActions(overrides: Partial<NonNullable<SCUMWorkspaceActions["scum"]
|
||||
};
|
||||
}
|
||||
|
||||
function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: string[]; pageKey?: string; pageTitle?: string; giftTab?: "definitions" | "claims" | "deliveries" | "timed"; playerSearch?: string } = {}) {
|
||||
function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: string[]; pageKey?: string; pageTitle?: string; giftTab?: "definitions" | "claims" | "deliveries" | "timed"; playerSearch?: string; pageQuery?: Record<string, string> } = {}) {
|
||||
const nodes: string[] = [];
|
||||
const texts: string[] = [];
|
||||
const buttons: Array<{ label: string; disabled: boolean; onClick?: () => void }> = [];
|
||||
@@ -425,7 +455,7 @@ function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: strin
|
||||
const actions: SCUMWorkspaceActions = { pluginData: pluginDataActions(), scum: scumActions(), dispatch: async () => ({ status: "queued", result: { jobId: "job-1" } }) };
|
||||
renderPluginPage(react, {
|
||||
page: { key: options.pageKey ?? "players", title: options.pageTitle ?? "用户管理" },
|
||||
context: { serverInstanceId: "server-1", permissions: options.permissions ?? ["server.read", "server.remote.access"] },
|
||||
context: { pluginId: "game.scum", serverInstanceId: "server-1", pageQuery: options.pageQuery ?? {}, permissions: options.permissions ?? ["server.read", "server.remote.access"] },
|
||||
availability: { available: true, features: [{ key: "player.intelligence", available: true }] },
|
||||
workspaceActions: actions
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user