diff --git a/platform_web/theme/base-css.test.js b/platform_web/theme/base-css.test.js index 598d983..60fb7a0 100644 --- a/platform_web/theme/base-css.test.js +++ b/platform_web/theme/base-css.test.js @@ -84,6 +84,15 @@ describe("platform web shared theme CSS", () => { expect(css).not.toContain("linear-gradient(145deg, rgba(255, 255, 255, 0.72), transparent 36%)"); }); + it("keeps SCUM user identity and economy columns readable", () => { + const css = compact(readThemeCss()); + + expect(css).toContain(".scum-user-table{min-width:1240px;table-layout:fixed}"); + expect(css).toContain(".scum-user-tableth,.scum-user-tabletd{padding:9px12px;border-bottom-color:color-mix(insrgb,var(--line)64%,transparent);white-space:nowrap}"); + expect(css).toContain(".scum-user-tableth:nth-child(7),.scum-user-tabletd:nth-child(7){width:190px}"); + expect(css).toContain(".scum-mono-cell{font-family:var(--font-mono);font-size:12px}"); + }); + it("uses shared orbital construct backdrop and button ring effects", () => { const themeCss = readThemeCss(); const constructRule = themeCss.slice(themeCss.indexOf(".global-orbital-construct"), themeCss.indexOf(".mobile-sidebar-backdrop")); diff --git a/platform_web/theme/base.css b/platform_web/theme/base.css index 1aeb949..4299358 100644 --- a/platform_web/theme/base.css +++ b/platform_web/theme/base.css @@ -442,9 +442,9 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))} .provider-form label{display:grid;gap:6px;color:var(--ink-soft);font-size:13px;font-weight:700} .console-field{display:grid;gap:5px;min-width:0;color:var(--ink-soft);font-size:12px;font-weight:760}.console-field>span{line-height:1.25}.console-field input:not([type=checkbox]):not([type=radio]),.console-field select,.console-field textarea{width:100%;min-width:0;min-height:36px;border:1px solid var(--line-strong);border-radius:6px;padding:0 10px;background:var(--surface-solid);color:var(--ink);font:inherit;font-size:13px;box-shadow:inset 0 1px 0 var(--crystal-rim)}.console-field textarea{min-height:112px;padding:9px;resize:vertical;font-family:var(--font-mono)}.console-field input::placeholder,.console-field textarea::placeholder{color:color-mix(in srgb,var(--ink-faint) 82%,transparent)}.console-field input:focus,.console-field select:focus,.console-field textarea:focus{border-color:var(--accent);outline:2px solid var(--accent-soft)} .scum-editor>summary{display:flex;align-items:center;justify-content:space-between;gap:10px;cursor:pointer;list-style:none}.scum-editor>summary::-webkit-details-marker{display:none}.scum-editor>summary::after{content:"+";color:var(--accent);font-size:18px;font-weight:400}.scum-editor[open]>summary::after{content:"-"}.scum-editor[open]>summary{margin-bottom:12px}.scum-editor .console-row-actions{align-items:flex-end}.scum-editor .console-row-actions>.console-field{flex:1 1 140px;max-width:260px}.scum-player-overview{margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid var(--line)} -.scum-workbench{padding:12px;overflow:visible}.scum-workbench>.console-record-list{gap:8px}.scum-filter-bar{margin:0;padding:0 0 10px;border-bottom:1px solid color-mix(in srgb,var(--line) 68%,transparent)}.scum-filter-bar .console-field{flex:0 1 210px}.scum-filter-bar .console-field:first-child{flex:1 1 320px}.scum-filter-bar .page-status{margin-left:auto}.scum-layer-toggle{display:inline-flex;align-items:center;gap:6px;min-height:34px;padding:0 9px;border:1px solid var(--line);border-radius:999px;background:var(--control-surface);color:var(--ink-soft);font-size:12px;font-weight:760}.scum-layer-toggle input{accent-color:var(--accent)}.scum-table-wrap{border-color:color-mix(in srgb,var(--line) 72%,transparent);background:linear-gradient(180deg,color-mix(in srgb,var(--surface-solid) 84%,transparent),color-mix(in srgb,var(--surface) 66%,transparent));-webkit-backdrop-filter:blur(8px) saturate(.92);backdrop-filter:blur(8px) saturate(.92);box-shadow:inset 0 1px 0 color-mix(in srgb,var(--crystal-rim) 46%,transparent)}.scum-table-wrap::before,.scum-table-wrap::after{content:none}.scum-user-table{min-width:720px;table-layout:fixed}.scum-user-table caption{caption-side:top;padding:0 0 8px;text-align:left;color:var(--ink-faint);font-size:12px;font-weight:800}.scum-user-table th,.scum-user-table td{padding:9px 14px;border-bottom-color:color-mix(in srgb,var(--line) 64%,transparent)}.scum-user-table th{background:color-mix(in srgb,var(--accent-soft) 52%,transparent);color:color-mix(in srgb,var(--ink) 70%,var(--accent-deep))}.scum-user-table tbody tr:hover{background:color-mix(in srgb,var(--accent-soft) 24%,transparent)}.scum-user-table th:nth-child(1),.scum-user-table td:nth-child(1){width:24%}.scum-user-table th:nth-child(2),.scum-user-table td:nth-child(2){width:22%}.scum-user-table th:nth-child(3),.scum-user-table td:nth-child(3){width:16%}.scum-user-table th:nth-child(4),.scum-user-table td:nth-child(4){width:26%}.scum-user-table th:last-child,.scum-user-table td:last-child{width:120px;text-align:right}.scum-user-table .icon-command{min-height:32px;padding:0 10px}.inline-action-control{position:relative;display:inline-block}.inline-action-control>summary{list-style:none}.inline-action-control>summary::-webkit-details-marker{display:none}.inline-action-control[open]>summary{border-color:var(--accent);color:var(--ink)}.inline-action-control .inline-action-menu{right:0} +.scum-workbench{padding:12px;overflow:visible}.scum-workbench>.console-record-list{gap:8px}.scum-filter-bar{margin:0;padding:0 0 10px;border-bottom:1px solid color-mix(in srgb,var(--line) 68%,transparent)}.scum-filter-bar .console-field{flex:0 1 210px}.scum-filter-bar .console-field:first-child{flex:1 1 320px}.scum-filter-bar .page-status{margin-left:auto}.scum-layer-toggle{display:inline-flex;align-items:center;gap:6px;min-height:34px;padding:0 9px;border:1px solid var(--line);border-radius:999px;background:var(--control-surface);color:var(--ink-soft);font-size:12px;font-weight:760}.scum-layer-toggle input{accent-color:var(--accent)}.scum-table-wrap{border-color:color-mix(in srgb,var(--line) 72%,transparent);background:linear-gradient(180deg,color-mix(in srgb,var(--surface-solid) 84%,transparent),color-mix(in srgb,var(--surface) 66%,transparent));-webkit-backdrop-filter:blur(8px) saturate(.92);backdrop-filter:blur(8px) saturate(.92);box-shadow:inset 0 1px 0 color-mix(in srgb,var(--crystal-rim) 46%,transparent)}.scum-table-wrap::before,.scum-table-wrap::after{content:none}.scum-user-table{min-width:1240px;table-layout:fixed}.scum-user-table caption{caption-side:top;padding:0 0 8px;text-align:left;color:var(--ink-faint);font-size:12px;font-weight:800}.scum-user-table th,.scum-user-table td{padding:9px 12px;border-bottom-color:color-mix(in srgb,var(--line) 64%,transparent);white-space:nowrap}.scum-user-table th{background:color-mix(in srgb,var(--accent-soft) 52%,transparent);color:color-mix(in srgb,var(--ink) 70%,var(--accent-deep))}.scum-user-table tbody tr:hover{background:color-mix(in srgb,var(--accent-soft) 24%,transparent)}.scum-user-table th:nth-child(1),.scum-user-table td:nth-child(1){width:180px}.scum-user-table th:nth-child(2),.scum-user-table td:nth-child(2){width:190px}.scum-user-table th:nth-child(3),.scum-user-table td:nth-child(3){width:90px}.scum-user-table th:nth-child(4),.scum-user-table td:nth-child(4){width:140px}.scum-user-table th:nth-child(5),.scum-user-table td:nth-child(5){width:130px}.scum-user-table th:nth-child(6),.scum-user-table td:nth-child(6){width:120px}.scum-user-table th:nth-child(7),.scum-user-table td:nth-child(7){width:190px}.scum-user-table th:last-child,.scum-user-table td:last-child{width:100px;text-align:right}.scum-user-table td>*{max-width:100%;overflow:hidden;text-overflow:ellipsis}.scum-mono-cell{font-family:var(--font-mono);font-size:12px}.scum-user-table .icon-command{min-height:32px;padding:0 10px}.inline-action-control{position:relative;display:inline-block}.inline-action-control>summary{list-style:none}.inline-action-control>summary::-webkit-details-marker{display:none}.inline-action-control[open]>summary{border-color:var(--accent);color:var(--ink)}.inline-action-control .inline-action-menu{right:0} :root[data-custom-background=true] .scum-workbench,:root[data-custom-background=true] .scum-table-wrap{background:linear-gradient(180deg,rgba(5,10,16,.76),rgba(5,10,16,.64));border-color:color-mix(in srgb,var(--line-strong) 54%,rgba(255,255,255,.18))}:root[data-custom-background=true][data-theme-palette=magical-girl] .scum-workbench,:root[data-custom-background=true][data-theme-palette=magical-girl] .scum-table-wrap{background:linear-gradient(180deg,rgba(18,19,26,.74),rgba(13,15,22,.64));border-color:rgba(250,240,249,.34)} -@media (max-width:760px){.scum-filter-bar .page-status{margin-left:0}.scum-user-table{min-width:640px}} +@media (max-width:760px){.scum-filter-bar .page-status{margin-left:0}.scum-user-table{min-width:1040px}} .provider-form input,.provider-form select,.provider-form textarea{width:100%;min-height:38px;border:1px solid var(--line-strong);border-radius:8px;padding:0 10px;background:var(--surface-solid);color:var(--ink);font:inherit} .provider-form textarea{min-height:120px;padding:10px;resize:vertical;font-family:var(--font-mono);font-size:13px} .provider-form input:focus,.provider-form select:focus,.provider-form textarea:focus{border-color:var(--accent);outline:2px solid var(--accent-soft)} diff --git a/plugins/examples/scum-server-plugin/features/page.ts b/plugins/examples/scum-server-plugin/features/page.ts index 1f93fa5..0182faf 100644 --- a/plugins/examples/scum-server-plugin/features/page.ts +++ b/plugins/examples/scum-server-plugin/features/page.ts @@ -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")}`; } diff --git a/plugins/tests/scum-feature-module.test.ts b/plugins/tests/scum-feature-module.test.ts index 7c400a1..f701c17 100644 --- a/plugins/tests/scum-feature-module.test.ts +++ b/plugins/tests/scum-feature-module.test.ts @@ -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" });