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:
@@ -10,6 +10,7 @@ export interface PageParams {
|
||||
serverId?: string;
|
||||
pluginId?: string;
|
||||
routeKey?: string;
|
||||
pageQuery?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface PageRoute {
|
||||
|
||||
@@ -121,6 +121,7 @@ export interface PluginBridgeHostContext {
|
||||
pluginId: string;
|
||||
routeKey: string;
|
||||
serverInstanceId?: string;
|
||||
pageQuery: Record<string, string>;
|
||||
themeTokens: PluginBridgeThemeTokens;
|
||||
permissions: PluginPermission[];
|
||||
bridgeActions: PluginBridgeAction[];
|
||||
|
||||
@@ -58,12 +58,14 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
|
||||
const declaredPage = readyPlugin?.pages.find((candidate) => candidate.key === routeKey);
|
||||
const manifestContract = useMemo(() => (readyPlugin ? pluginBridgeManifestContractFromResponse(readyPlugin) : undefined), [readyPlugin]);
|
||||
const declaredBundlePage = manifestContract?.pages.find((candidate) => candidate.key === routeKey);
|
||||
const pageQuery = params.pageQuery;
|
||||
const hostContext = useMemo(() => manifestContract ? createPluginBridgeHostContext({
|
||||
plugin: manifestContract,
|
||||
routeKey,
|
||||
serverInstanceId: serverId || undefined,
|
||||
pageQuery,
|
||||
themeTokens: { colorScheme: "dark", accentColor: "#7dd3fc" }
|
||||
}) : undefined, [manifestContract, routeKey, serverId]);
|
||||
}) : undefined, [manifestContract, routeKey, serverId, pageQuery]);
|
||||
readyPluginRef.current = readyPlugin;
|
||||
hostContextRef.current = hostContext;
|
||||
const workspaceActions = useMemo<PluginPageWorkspaceActions | undefined>(() => {
|
||||
|
||||
@@ -51,6 +51,14 @@ describe("console shell routes", () => {
|
||||
expect(resolved.params).toEqual({ pluginId: "game.scum", routeKey: "operations", serverId: "server/scum-1" });
|
||||
});
|
||||
|
||||
it("carries hosted plugin page focus queries through the hash", () => {
|
||||
const pageQuery = { focus: "76561198000000001", from: "2026-09-16T09:30:00Z", to: "2026-09-16T11:00:00Z" };
|
||||
const hash = hashForPage("pluginPage", { pluginId: "game.scum", routeKey: "live-map", serverId: "server-1", pageQuery });
|
||||
const resolved = resolveRouteHash(hash, platformAdmin);
|
||||
expect(resolved.route.id).toBe("pluginPage");
|
||||
expect(resolved.params).toEqual({ pluginId: "game.scum", routeKey: "live-map", serverId: "server-1", pageQuery });
|
||||
});
|
||||
|
||||
it("keeps route metadata available for shell navigation", () => {
|
||||
expect(firstPartyRoutes.map((route) => route.id)).toContain("maintenance");
|
||||
expect(firstPartyRoutes.map((route) => route.id)).toContain("profileSettings");
|
||||
|
||||
@@ -110,6 +110,11 @@ export function hashForPage(pageId: PageId, params: PageParams = {}): string {
|
||||
if (params.serverId) {
|
||||
query.set("serverInstanceId", params.serverId);
|
||||
}
|
||||
for (const [key, value] of Object.entries(params.pageQuery ?? {})) {
|
||||
if (value) {
|
||||
query.set(key, value);
|
||||
}
|
||||
}
|
||||
const suffix = query.toString();
|
||||
return `#/plugin-pages/${encodeURIComponent(params.pluginId)}/${encodeURIComponent(params.routeKey)}${suffix ? `?${suffix}` : ""}`;
|
||||
}
|
||||
@@ -151,13 +156,22 @@ export function resolveRouteHash(hash: string, user?: CurrentUserView): Resolved
|
||||
return { route: routeForPage("serverDetail"), params: { serverId: decodeURIComponent(segments[1]), routeKey } };
|
||||
}
|
||||
if (segments[0] === "plugin-pages" && segments.length > 2) {
|
||||
const serverId = new URLSearchParams(rawQuery).get("serverInstanceId") ?? undefined;
|
||||
const query = new URLSearchParams(rawQuery);
|
||||
const serverId = query.get("serverInstanceId") ?? undefined;
|
||||
query.delete("serverInstanceId");
|
||||
const pageQuery: Record<string, string> = {};
|
||||
for (const [key, value] of query.entries()) {
|
||||
if (value) {
|
||||
pageQuery[key] = value;
|
||||
}
|
||||
}
|
||||
return {
|
||||
route: routeForPage("pluginPage"),
|
||||
params: {
|
||||
pluginId: decodeURIComponent(segments[1]),
|
||||
routeKey: decodeURIComponent(segments[2]),
|
||||
serverId
|
||||
serverId,
|
||||
...(Object.keys(pageQuery).length ? { pageQuery } : {})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -794,14 +794,21 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))}
|
||||
.scum-map-menu>summary{display:inline-flex;align-items:center;min-height:26px;padding:0 9px;font-size:11px;list-style:none;cursor:pointer}
|
||||
.scum-map-menu>summary::-webkit-details-marker{display:none}
|
||||
.scum-map-menu[open]>summary{border-color:var(--accent);color:var(--ink)}
|
||||
.scum-map-menu-body{position:absolute;top:calc(100% + 6px);left:0;z-index:9;display:grid;gap:6px;width:max-content;max-width:320px;padding:10px;border:1px solid var(--line-strong);border-radius:10px;background:var(--panel-material),var(--surface-solid);box-shadow:var(--panel-shadow)}
|
||||
.scum-map-menu-body{position:absolute;top:calc(100% + 6px);left:0;z-index:9;display:grid;gap:6px;width:max-content;max-width:320px;max-height:min(60vh,420px);overflow:auto;padding:10px;border:1px solid var(--line-strong);border-radius:10px;background:var(--panel-material),var(--surface-solid);box-shadow:var(--panel-shadow)}
|
||||
.scum-map-menu-body .scum-layer-toggle{justify-content:flex-start}
|
||||
.scum-map-menu-body .page-status{margin:0;font-size:11px;white-space:normal}
|
||||
.scum-map-bar{gap:6px}
|
||||
.scum-map-bar .console-field{flex:0 0 auto}
|
||||
.scum-map-bar select{min-height:26px;min-width:0;padding:0 8px;font-size:12px}
|
||||
.scum-map-bar input[type=datetime-local]{min-height:26px;min-width:0;padding:0 6px;font-size:12px;color:var(--ink)}
|
||||
.scum-map-bar .page-status{font-size:11px}
|
||||
.scum-map-tracks .console-row.is-selected{border-color:var(--accent);color:var(--ink)}
|
||||
.map-track-legend{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:6px 10px;border:1px solid color-mix(in srgb,var(--line) 70%,transparent);border-radius:10px;background:var(--control-surface)}
|
||||
.map-track-legend .page-status{margin:0}
|
||||
.map-track-chip{display:inline-flex;align-items:center;gap:6px;max-width:100%;min-height:24px;padding:0 9px;border:1px solid var(--line);border-radius:999px;background:color-mix(in srgb,var(--surface-solid) 72%,transparent);color:var(--ink-soft);font-size:11px;cursor:pointer}
|
||||
.map-track-chip:hover,.map-track-chip.is-selected{border-color:var(--accent);color:var(--ink)}
|
||||
.map-track-chip .map-track-swatch{margin-right:0}
|
||||
.map-track-chip strong{color:var(--ink);font-size:11px}
|
||||
.map-track-chip small{color:var(--ink-faint);font-size:10px}
|
||||
.console-row-list,.operations-endpoint-list,.operations-job-list{display:grid;gap:6px;margin-top:10px}
|
||||
.console-row,.operations-endpoint-row,.operations-job-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:10px;min-width:0;padding:8px 10px;border:1px solid var(--line);border-radius:6px;background:var(--control-surface);color:var(--ink-soft);text-align:left}
|
||||
.console-row-button,.operations-job-row{width:100%;cursor:pointer}
|
||||
|
||||
@@ -49,6 +49,7 @@ describe("plugin bridge host utilities", () => {
|
||||
pluginId: "game.example",
|
||||
routeKey: "logs",
|
||||
serverInstanceId: "server-1",
|
||||
pageQuery: {},
|
||||
themeTokens: { colorScheme: "dark", accentColor: "#22c55e" },
|
||||
permissions: ["server.logs.read", "server.files.read", "server.artifacts.read", "ai.invoke"],
|
||||
bridgeActions: ["logs.query", "files.request", "artifacts.open", "ai.invoke"],
|
||||
|
||||
@@ -13,6 +13,7 @@ export function createPluginBridgeHostContext(input: {
|
||||
plugin: PluginBridgeManifestContract;
|
||||
routeKey: string;
|
||||
serverInstanceId?: string;
|
||||
pageQuery?: Record<string, string>;
|
||||
themeTokens: PluginBridgeThemeTokens;
|
||||
}): PluginBridgeHostContext {
|
||||
const page = input.plugin.pages.find((candidate) => candidate.key === input.routeKey);
|
||||
@@ -26,6 +27,7 @@ export function createPluginBridgeHostContext(input: {
|
||||
pluginId: input.plugin.id,
|
||||
routeKey: input.routeKey,
|
||||
serverInstanceId: input.serverInstanceId,
|
||||
pageQuery: { ...(input.pageQuery ?? {}) },
|
||||
themeTokens: input.themeTokens,
|
||||
permissions,
|
||||
bridgeActions,
|
||||
|
||||
Reference in New Issue
Block a user