Tighten SCUM user table display

This commit is contained in:
npc0-hue
2026-08-28 14:32:28 +08:00
parent 3ca67907a1
commit bb187f6007
4 changed files with 25 additions and 11 deletions
@@ -203,14 +203,14 @@ function playersSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, in
const selectedPlayer = data.players.find((player) => playerKey(player) === view.playerPanel.playerId);
return e("div", { className: "console-record-list scum-surface" },
e("div", { className: "resource-filter-bar scum-filter-bar" },
labeledField(e, "搜索用户", e("input", { type: "search", value: view.playerSearch, "aria-label": "搜索用户", placeholder: "名称 / UUID / Steam ID / 登录 IP / 队伍", onChange: (event: InputEvent) => view.setPlayerSearch(inputValue(event)) })),
labeledField(e, "搜索用户", e("input", { type: "search", value: view.playerSearch, "aria-label": "搜索用户", placeholder: "名称 / Steam ID / UUID / 登录 IP / 队伍", onChange: (event: InputEvent) => view.setPlayerSearch(inputValue(event)) })),
labeledField(e, "在线状态", e("select", { value: view.playerStatus, "aria-label": "在线状态", onChange: (event: InputEvent) => view.setPlayerStatus(inputValue(event)) }, e("option", { value: "all" }, "全部状态"), e("option", { value: "online" }, "在线"), e("option", { value: "offline" }, "离线/未知"))),
e("span", { className: "scum-online-summary" }, "在线用户 ", e("strong", null, `${onlineCount} / ${data.players.length}`)),
players.length !== data.players.length ? e("span", { className: "page-status" }, `当前显示 ${players.length}`) : null
),
e("div", { className: "provider-table-wrap scum-table-wrap" },
e("table", { className: "resource-table scum-user-table" },
e("thead", null, e("tr", null, ["用户名", "UUID / Steam", "渣币", "登录 IP", "队伍", "状态", "最后活动", "操作"].map((label) => e("th", { key: label, scope: "col" }, label)))),
e("thead", null, e("tr", null, ["用户名", "Steam ID", "渣币", "登录 IP", "队伍", "状态", "最后活动", "操作"].map((label) => e("th", { key: label, scope: "col" }, label)))),
e("tbody", null, players.length ? players.map((player, index) => playerTableRow(e, player, index, data, input, view)) : e("tr", null, e("td", { colSpan: 8 }, e("p", { className: "page-status" }, "没有符合筛选条件的真实用户记录。"))))
)
),
@@ -220,13 +220,13 @@ function playersSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, in
function playerTableRow(e: ReactLike["createElement"], player: RecordMap, index: number, data: SCUMSurfaceData, input: SCUMPageContext, view: ViewState) {
const name = textField(player, "displayName", "playerName", "name") || textField(player, "gamePlayerId", "playerId", "steamId") || "未知用户";
const uuid = textField(player, "gamePlayerId", "playerId", "userProfileId", "profileId") || "未同步";
const steamId = textField(player, "steamId", "providerId");
const uuid = playerUuid(player);
const open = (kind: PlayerPanelKind) => { view.setPlayerPanel({ kind, playerId: playerKey(player) }); if (kind === "attributes") view.setAttributeDrafts(playerAttributeDrafts(player)); };
return e("tr", { key: idOf(player, `player-${index}`) },
e("td", null, e("strong", null, name)),
e("td", null, e("span", { className: "provider-id" }, uuid), steamId ? e("span", { className: "provider-id" }, `Steam ${steamId}`) : null),
e("td", null, e("strong", null, numField(player, "normalBalance", "moneyBalance")), e("span", { className: "provider-id" }, `Gold ${numField(player, "goldBalance")}`)),
e("td", null, e("span", { className: "provider-id scum-mono-cell", title: uuid ? `UUID ${uuid}` : undefined }, steamId || "未同步")),
e("td", null, e("strong", null, playerCurrency(player))),
e("td", null, e("span", { className: "provider-id" }, textField(player, "lastLoginIp", "loginIp", "ipAddress", "ip") || "未同步")),
e("td", null, e("span", null, textField(player, "squadName", "squadId") || "未加入"), e("span", { className: "provider-id" }, textField(player, "squadId") ? `ID ${textField(player, "squadId")}` : "")),
e("td", null, e("span", { className: `status-pill ${playerOnline(player) ? "status-active" : "status-disabled"}` }, playerOnline(player) ? "在线" : "离线/未知"), e("span", { className: "provider-id" }, numField(player, "pingMs") === "--" ? "" : `${numField(player, "pingMs")} ms`)),
@@ -259,11 +259,11 @@ function playerDrawer(e: ReactLike["createElement"], player: RecordMap, data: SC
e("div", { className: "panel-header" }, e("div", null, e("h2", null, title), e("span", { className: "provider-id" }, `${name} · Steam ${textField(player, "steamId", "providerId") || "未同步"}`)), e("button", { type: "button", className: "drawer-close", onClick: close }, "关闭")),
e("div", { className: "console-record-meta scum-player-overview" },
e("span", null, `Profile ${textField(player, "userProfileId", "profileId") || "未同步"}`),
e("span", null, `UUID ${textField(player, "gamePlayerId", "playerId") || "未同步"}`),
e("span", null, `UUID ${playerUuid(player) || "未同步"}`),
e("span", null, `登录 IP ${textField(player, "lastLoginIp", "loginIp", "ipAddress", "ip") || "未同步"}`),
e("span", null, `网络相关 ${shortHash(textField(player, "networkCorrelation")) || "未同步"}`),
e("span", null, `Fame ${numField(player, "famePoints")}`),
e("span", null, `渣币 ${numField(player, "normalBalance", "moneyBalance")} · Gold ${numField(player, "goldBalance")}`),
e("span", null, `渣币 ${playerCurrency(player)} · Gold ${playerGold(player)}`),
e("span", null, `上次登录 ${userDateField(player, "lastLoginTime", "lastLoginAt", "lastLoginObservedAt")}`)),
content
)
@@ -648,6 +648,10 @@ const playerActivityKeys = ["lastSeenAt", "lastLoginObservedAt", "onlineObserved
const playerOnlineWindowMs = 5 * 60 * 1000;
function playerOnline(player: RecordMap): boolean { const status = textField(player, "status").toLowerCase(); const active = boolField(player, "online") || ["online", "active", "connected"].includes(status); if (!active) return false; const lastActivity = field(player, ...playerActivityKeys); if (lastActivity === undefined || lastActivity === null || lastActivity === "") return true; const timestamp = Date.parse(String(lastActivity)); return Number.isNaN(timestamp) || Date.now() - timestamp <= playerOnlineWindowMs; }
function numField(row: RecordMap, ...keys: string[]): string { const value = field(row, ...keys); return value === undefined || value === null || value === "" ? "--" : String(value); }
function playerUuid(player: RecordMap): string { const steamId = textField(player, "steamId", "providerId"); const uuid = textField(player, "gamePlayerId", "playerId"); return uuid && uuid !== steamId ? uuid : textField(player, "userProfileId", "profileId"); }
function playerCurrency(player: RecordMap): string { return valueField(player, "normalBalance", "moneyBalance", "balance", "cash", "cashBalance", "currencyBalance") || "未同步"; }
function playerGold(player: RecordMap): string { return valueField(player, "goldBalance", "gold") || "未同步"; }
function valueField(row: RecordMap, ...keys: string[]): string { const value = field(row, ...keys); return value === undefined || value === null || value === "" ? "" : String(value); }
function idOf(row: RecordMap | undefined, fallback: string): string { return textField(row, "_recordKey", "id", "gamePlayerId", "squadId", "vehicleId", "flagId", "eventId", "code") || fallback; }
function freshness(row: RecordMap): string { const fresh = field(row, "freshness"); return isRecord(fresh) ? textField(fresh, "status") || "unknown" : textField(row, "freshnessStatus", "updatedAt") || "unknown"; }
function coords(row?: RecordMap): string { if (!row || !hasCoordinates(row)) return "坐标 unknown"; return `X ${numField(row, "x", "locationX")} / Y ${numField(row, "y", "locationY")} / Z ${numField(row, "z", "locationZ")}`; }
+3 -2
View File
@@ -186,7 +186,7 @@ describe("SCUM plugin feature module", () => {
expect(view.nodes).toContain("section:用户管理");
expect(view.texts).not.toContain("SCUM 用户真实记录");
expect(view.texts).toEqual(expect.arrayContaining(["在线用户 ", "1 / 1"]));
expect(view.texts).toEqual(expect.arrayContaining(["用户名", "UUID / Steam", "渣币", "登录 IP", "队伍", "状态", "最后活动", "操作"]));
expect(view.texts).toEqual(expect.arrayContaining(["用户名", "Steam ID", "渣币", "登录 IP", "队伍", "状态", "最后活动", "操作"]));
expect(view.texts).not.toContain("筛选结果");
expect(view.texts).not.toContain("通用数据/机器动作可用");
expect(view.buttons.map((button) => button.label)).not.toEqual(expect.arrayContaining(["同步 SCUM.db", "重新读取"]));
@@ -196,7 +196,8 @@ describe("SCUM plugin feature module", () => {
expect(pageSource).toContain("resource-filter-bar scum-filter-bar");
expect(pageSource).toContain("provider-table-wrap scum-table-wrap");
expect(view.texts).toContain("Mira");
expect(view.texts).toEqual(expect.arrayContaining(["steam-1", "Steam 76561198000000001", "1000", "Gold 3", "203.0.113.7"]));
expect(view.texts).toEqual(expect.arrayContaining(["76561198000000001", "1000", "203.0.113.7"]));
expect(view.texts).not.toEqual(expect.arrayContaining(["UUID / Steam", "Gold 3"]));
const ipSearch = renderAndCollect({ playerSearch: "203.0.113.7" });
expect(ipSearch.texts).toContain("Mira");
const uuidSearch = renderAndCollect({ playerSearch: "profile-1" });