diff --git a/platform_web/theme/base.css b/platform_web/theme/base.css index 8ffa4f2..7e00234 100644 --- a/platform_web/theme/base.css +++ b/platform_web/theme/base.css @@ -440,6 +440,8 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))} .field-help{color:var(--ink-faint);font-size:11.5px;font-weight:600;line-height:1.4} .form-helper-actions{display:flex;gap:8px;flex-wrap:wrap} .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:700}.console-field>span{line-height:1.25}.console-field input,.console-field select,.console-field textarea{min-width:0} +.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)} .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 effd988..579de38 100644 --- a/plugins/examples/scum-server-plugin/features/page.ts +++ b/plugins/examples/scum-server-plugin/features/page.ts @@ -116,16 +116,14 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext) }; if (react.useEffect) react.useEffect(() => { - refresh(); - const interval = setInterval(refresh, 3000); + if (playerPanel.kind === "closed") refresh(); + if (playerPanel.kind !== "closed") return; + const interval = setInterval(refresh, 10000); return () => clearInterval(interval); - }, [input.serverInstanceId, pageKey, input.workspaceActions]); + }, [input.serverInstanceId, pageKey, input.workspaceActions, playerPanel.kind]); const data = state.status === "ready" ? state.data : emptySCUMSurfaceData; return e("section", { className: "console-panel", "aria-label": input.pageTitle ?? surfaceTitle(pageKey) }, - e("div", { className: "panel-header" }, - e("div", null, e("h2", null, input.pageTitle ?? surfaceTitle(pageKey)), e("p", { className: "provider-id" }, surfaceSummary(pageKey))) - ), action.status !== "idle" ? e("p", { className: "page-status", "data-state": action.status }, action.message) : null, state.status === "loading" ? e("p", { className: "page-status" }, "正在读取插件自有 SCUM 集合…") : null, state.status === "error" ? e("p", { className: "page-status", "data-state": "error" }, state.reason) : null, @@ -184,10 +182,10 @@ function playersSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, in 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("div", { className: "provider-table-wrap" }, - e("table", { className: "resource-table scum-user-table", style: { minWidth: "1180px" } }, + e("table", { className: "resource-table scum-user-table" }, e("caption", { className: "provider-id" }, "SCUM 用户真实记录"), - e("thead", null, e("tr", null, ["用户名", "Steam", "队伍", "状态", "上次登录", "登录 IP", "最后活动", "概况", "操作"].map((label) => e("th", { key: label, scope: "col" }, label)))), - e("tbody", null, players.length ? players.slice(0, 500).map((player, index) => playerTableRow(e, player, index, data, input, view)) : e("tr", null, e("td", { colSpan: 9 }, e("p", { className: "page-status" }, "没有符合筛选条件的真实用户记录。")))) + e("thead", null, e("tr", null, ["用户名", "队伍", "状态", "最后活动", "操作"].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: 5 }, e("p", { className: "page-status" }, "没有符合筛选条件的真实用户记录。")))) ) ), selectedPlayer && view.playerPanel.kind !== "closed" ? playerDrawer(e, selectedPlayer, data, input, view) : null @@ -196,17 +194,12 @@ 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 steamId = textField(player, "steamId", "providerId") || "未同步"; 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("span", { className: "provider-id" }, `Profile ${textField(player, "userProfileId", "profileId") || "未同步"}`)), - e("td", null, e("code", null, steamId)), + e("td", null, e("strong", null, name)), 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`)), - e("td", null, userDateField(player, "lastLoginTime", "lastLoginAt", "lastLoginObservedAt")), - e("td", null, e("span", null, textField(player, "lastLoginIp", "loginIp", "ipAddress", "lastIp") || "未同步"), e("span", { className: "provider-id" }, textField(player, "lastLoginIp", "loginIp", "ipAddress", "lastIp") ? "日志同步" : "等待日志字段")), e("td", null, userDateField(player, "lastSeenAt", "lastLoginObservedAt", "onlineObservedAt", "updatedAt")), - e("td", null, e("span", null, `Fame ${numField(player, "famePoints")}`), e("span", { className: "provider-id" }, `Cash ${numField(player, "normalBalance", "moneyBalance")} · Gold ${numField(player, "goldBalance")}`)), e("td", { className: "provider-actions-cell" }, playerActionMenu(e, player, view, open)) ); } @@ -233,6 +226,12 @@ function playerDrawer(e: ReactLike["createElement"], player: RecordMap, data: SC return e("div", { className: "confirm-backdrop", role: "presentation", onClick: close }, e("aside", { className: "drawer-panel", role: "dialog", "aria-modal": "true", "aria-label": `${name} / ${title}`, onClick: (event: InputEvent) => event.stopPropagation?.() }, 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, `登录 IP ${textField(player, "lastLoginIp", "loginIp", "ipAddress", "lastIp") || "未同步"}`), + e("span", null, `Fame ${numField(player, "famePoints")}`), + e("span", null, `Cash ${numField(player, "normalBalance", "moneyBalance")} · Gold ${numField(player, "goldBalance")}`), + e("span", null, `上次登录 ${userDateField(player, "lastLoginTime", "lastLoginAt", "lastLoginObservedAt")}`)), content ) ); @@ -357,19 +356,19 @@ function activitiesSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, return e("div", { className: "console-record-list" }, statsStrip(e, [["活动定义", data.events.length], ["运行记录", data.eventRuns.length], ["原生赛事轮次", data.nativeEventRounds.length], ["任务", data.tasks.length]]), e("div", { className: "overview-two-col" }, - e("article", { className: "console-module" }, e("h2", null, "新建或更新活动"), - e("input", { value: view.eventId, "aria-label": "活动编号", placeholder: "活动编号", onChange: (event: InputEvent) => view.setEventId(inputValue(event)) }), - e("input", { value: view.eventName, "aria-label": "活动名称", placeholder: "活动名称", onChange: (event: InputEvent) => view.setEventName(inputValue(event)) }), - e("select", { value: view.eventClass, "aria-label": "生成类型", onChange: (event: InputEvent) => view.setEventClass(inputValue(event)) }, e("option", { value: "1" }, "范围生成"), e("option", { value: "2" }, "固定坐标生成")), - e("input", { value: view.eventSchedule, "aria-label": "活动计划", placeholder: "Cron", onChange: (event: InputEvent) => view.setEventSchedule(inputValue(event)) }), - e("input", { value: view.eventPlacard, "aria-label": "活动公告", placeholder: "活动开始公告", onChange: (event: InputEvent) => view.setEventPlacard(inputValue(event)) }), - e("input", { value: view.eventPercent, "aria-label": "活动概率", type: "number", placeholder: "触发概率 %", onChange: (event: InputEvent) => view.setEventPercent(inputValue(event)) }), - e("input", { value: view.eventDuration, "aria-label": "活动持续秒数", type: "number", min: 1, placeholder: "持续秒数", onChange: (event: InputEvent) => view.setEventDuration(inputValue(event)) }), + e("details", { className: "console-module scum-editor", open: Boolean(view.eventId || view.eventName) }, e("summary", null, e("strong", null, "新建或更新活动"), e("span", { className: "page-status" }, view.eventId || view.eventName ? "编辑中" : "点击展开")), + labeledField(e, "活动编号", e("input", { value: view.eventId, "aria-label": "活动编号", placeholder: "例如 event_supply_drop", onChange: (event: InputEvent) => view.setEventId(inputValue(event)) })), + labeledField(e, "活动名称", e("input", { value: view.eventName, "aria-label": "活动名称", placeholder: "例如 空投补给", onChange: (event: InputEvent) => view.setEventName(inputValue(event)) })), + labeledField(e, "生成类型", e("select", { value: view.eventClass, "aria-label": "生成类型", onChange: (event: InputEvent) => view.setEventClass(inputValue(event)) }, e("option", { value: "1" }, "范围生成"), e("option", { value: "2" }, "固定坐标生成"))), + labeledField(e, "活动计划", e("input", { value: view.eventSchedule, "aria-label": "活动计划", placeholder: "Cron(留空为手动)", onChange: (event: InputEvent) => view.setEventSchedule(inputValue(event)) })), + labeledField(e, "活动公告", e("input", { value: view.eventPlacard, "aria-label": "活动公告", placeholder: "活动开始时显示的公告", onChange: (event: InputEvent) => view.setEventPlacard(inputValue(event)) })), + labeledField(e, "触发概率 (%)", e("input", { value: view.eventPercent, "aria-label": "活动概率", type: "number", placeholder: "100", onChange: (event: InputEvent) => view.setEventPercent(inputValue(event)) })), + labeledField(e, "持续秒数", e("input", { value: view.eventDuration, "aria-label": "活动持续秒数", type: "number", min: 1, placeholder: "1800", onChange: (event: InputEvent) => view.setEventDuration(inputValue(event)) })), e("div", { className: "console-row-actions" }, - e("input", { value: view.eventNpc, "aria-label": "NPC 数量", type: "number", placeholder: "NPC", onChange: (event: InputEvent) => view.setEventNpc(inputValue(event)) }), - e("input", { value: view.eventItem, "aria-label": "物品数量", type: "number", placeholder: "物品", onChange: (event: InputEvent) => view.setEventItem(inputValue(event)) }), - e("input", { value: view.eventZombie, "aria-label": "僵尸数量", type: "number", placeholder: "僵尸", onChange: (event: InputEvent) => view.setEventZombie(inputValue(event)) }), - e("input", { value: view.eventAnimal, "aria-label": "动物数量", type: "number", placeholder: "动物", onChange: (event: InputEvent) => view.setEventAnimal(inputValue(event)) })), + labeledField(e, "NPC 数量", e("input", { value: view.eventNpc, "aria-label": "NPC 数量", type: "number", placeholder: "0", onChange: (event: InputEvent) => view.setEventNpc(inputValue(event)) })), + labeledField(e, "物品数量", e("input", { value: view.eventItem, "aria-label": "物品数量", type: "number", placeholder: "0", onChange: (event: InputEvent) => view.setEventItem(inputValue(event)) })), + labeledField(e, "僵尸数量", e("input", { value: view.eventZombie, "aria-label": "僵尸数量", type: "number", placeholder: "0", onChange: (event: InputEvent) => view.setEventZombie(inputValue(event)) })), + labeledField(e, "动物数量", e("input", { value: view.eventAnimal, "aria-label": "动物数量", type: "number", placeholder: "0", onChange: (event: InputEvent) => view.setEventAnimal(inputValue(event)) }))), e("button", { type: "button", className: "primary-command", disabled: !actions?.pluginData, onClick: saveEvent }, "保存活动") ), e("article", { className: "console-module" }, e("h2", null, "状态筛选"), @@ -377,18 +376,18 @@ function activitiesSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, e("p", { className: "page-status" }, "活动定义属于插件;SCUM 原生 event_round 和 quest/task 只作为运行事实展示。") ) ), - e("article", { className: "console-module" }, e("h2", null, "活动生成项"), + e("details", { className: "console-module scum-editor", open: Boolean(view.produceEventId || view.produceId || view.produceTradeGoodsId) }, e("summary", null, e("strong", null, "活动生成项"), e("span", { className: "page-status" }, view.produceEventId || view.produceId ? "编辑中" : "点击展开")), e("div", { className: "console-row-actions" }, - e("input", { value: view.produceEventId, "aria-label": "生成项活动编号", placeholder: "活动编号", onChange: (event: InputEvent) => view.setProduceEventId(inputValue(event)) }), - e("input", { value: view.produceId, "aria-label": "生成项编号", placeholder: "生成项编号(更新时填写)", onChange: (event: InputEvent) => view.setProduceId(inputValue(event)) }), - e("input", { value: view.produceTradeGoodsId, "aria-label": "生成物品编号", placeholder: "物品 / TradeGoods ID", onChange: (event: InputEvent) => view.setProduceTradeGoodsId(inputValue(event)) }), - e("input", { value: view.producePercent, "aria-label": "生成概率", type: "number", placeholder: "概率 %", onChange: (event: InputEvent) => view.setProducePercent(inputValue(event)) }), - e("input", { value: view.produceValue, "aria-label": "生成数量", type: "number", placeholder: "数量", onChange: (event: InputEvent) => view.setProduceValue(inputValue(event)) })), + labeledField(e, "活动编号", e("input", { value: view.produceEventId, "aria-label": "生成项活动编号", placeholder: "默认使用上方活动", onChange: (event: InputEvent) => view.setProduceEventId(inputValue(event)) })), + labeledField(e, "生成项编号", e("input", { value: view.produceId, "aria-label": "生成项编号", placeholder: "更新时填写", onChange: (event: InputEvent) => view.setProduceId(inputValue(event)) })), + labeledField(e, "物品编号", e("input", { value: view.produceTradeGoodsId, "aria-label": "生成物品编号", placeholder: "TradeGoods ID", onChange: (event: InputEvent) => view.setProduceTradeGoodsId(inputValue(event)) })), + labeledField(e, "概率 (%)", e("input", { value: view.producePercent, "aria-label": "生成概率", type: "number", placeholder: "100", onChange: (event: InputEvent) => view.setProducePercent(inputValue(event)) })), + labeledField(e, "数量", e("input", { value: view.produceValue, "aria-label": "生成数量", type: "number", placeholder: "1", onChange: (event: InputEvent) => view.setProduceValue(inputValue(event)) }))), e("div", { className: "console-row-actions" }, - e("input", { value: view.produceRadius, "aria-label": "生成半径", type: "number", placeholder: "半径", onChange: (event: InputEvent) => view.setProduceRadius(inputValue(event)) }), - e("input", { value: view.produceX, "aria-label": "生成 X", type: "number", placeholder: "X", onChange: (event: InputEvent) => view.setProduceX(inputValue(event)) }), - e("input", { value: view.produceY, "aria-label": "生成 Y", type: "number", placeholder: "Y", onChange: (event: InputEvent) => view.setProduceY(inputValue(event)) }), - e("input", { value: view.produceZ, "aria-label": "生成 Z", type: "number", placeholder: "Z", onChange: (event: InputEvent) => view.setProduceZ(inputValue(event)) }), + labeledField(e, "半径", e("input", { value: view.produceRadius, "aria-label": "生成半径", type: "number", placeholder: "0", onChange: (event: InputEvent) => view.setProduceRadius(inputValue(event)) })), + labeledField(e, "X", e("input", { value: view.produceX, "aria-label": "生成 X", type: "number", placeholder: "0", onChange: (event: InputEvent) => view.setProduceX(inputValue(event)) })), + labeledField(e, "Y", e("input", { value: view.produceY, "aria-label": "生成 Y", type: "number", placeholder: "0", onChange: (event: InputEvent) => view.setProduceY(inputValue(event)) })), + labeledField(e, "Z", e("input", { value: view.produceZ, "aria-label": "生成 Z", type: "number", placeholder: "0", onChange: (event: InputEvent) => view.setProduceZ(inputValue(event)) })), e("button", { type: "button", className: "primary-command", disabled: !actions?.pluginData, onClick: saveProduce }, "保存生成项")), e("div", { className: "console-row-list" }, data.eventProduces.length ? data.eventProduces.map((produce, index) => e("div", { key: idOf(produce, `produce-${index}`), className: "console-row" }, e("span", null, `${textField(produce, "eventId", "event")} / ${textField(produce, "tradeGoodsId", "trade_goods_id")}`), @@ -452,15 +451,15 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu giftTabButton(e, view, "definitions", "礼包定义"), giftTabButton(e, view, "claims", "领取/待领"), giftTabButton(e, view, "deliveries", "发放记录"), giftTabButton(e, view, "timed", "游戏定时记录") ), view.giftTab === "definitions" ? e("div", { className: "overview-two-col" }, - e("article", { className: "console-module" }, e("h2", null, "新建或更新礼包"), - e("input", { value: view.giftCode, "aria-label": "礼包编号", placeholder: "礼包编号", onChange: (event: InputEvent) => view.setGiftCode(inputValue(event)) }), - e("input", { value: view.giftName, "aria-label": "礼包名称", placeholder: "礼包名称", onChange: (event: InputEvent) => view.setGiftName(inputValue(event)) }), - e("select", { value: view.giftClass, "aria-label": "礼包周期", onChange: (event: InputEvent) => view.setGiftClass(inputValue(event)) }, [["1", "每日"], ["2", "每周"], ["3", "每月"], ["4", "每年"], ["5", "一次"], ["6", "每日五次"]].map(([value, label]) => e("option", { key: value, value }, label))), - e("select", { value: view.giftAudience, "aria-label": "适用玩家", onChange: (event: InputEvent) => view.setGiftAudience(inputValue(event)) }, e("option", { value: "all" }, "全部玩家"), e("option", { value: "pve" }, "PVE 玩家"), e("option", { value: "pvp" }, "PVP 玩家")), - e("input", { value: view.giftNumber, "aria-label": "发放次数", type: "number", placeholder: "发放次数", onChange: (event: InputEvent) => view.setGiftNumber(inputValue(event)) }), - e("div", { className: "console-row-actions" }, e("input", { value: view.giftAchievement, "aria-label": "成就类型", type: "number", placeholder: "成就类型", onChange: (event: InputEvent) => view.setGiftAchievement(inputValue(event)) }), e("input", { value: view.giftAchievementNumber, "aria-label": "成就值", type: "number", placeholder: "成就值", onChange: (event: InputEvent) => view.setGiftAchievementNumber(inputValue(event)) })), - e("input", { value: view.giftItems, "aria-label": "礼包物品", placeholder: "SCUM目录代码:数量, SCUM目录代码:数量", onChange: (event: InputEvent) => view.setGiftItems(inputValue(event)) }), - e("textarea", { value: view.giftCommands, "aria-label": "礼包命令", placeholder: "每行一条命令", onChange: (event: InputEvent) => view.setGiftCommands(inputValue(event)) }), + e("details", { className: "console-module scum-editor", open: Boolean(view.giftCode || view.giftName) }, e("summary", null, e("strong", null, "新建或更新礼包"), e("span", { className: "page-status" }, view.giftCode || view.giftName ? "编辑中" : "点击展开")), + labeledField(e, "礼包编号", e("input", { value: view.giftCode, "aria-label": "礼包编号", placeholder: "例如 welcome_pack", onChange: (event: InputEvent) => view.setGiftCode(inputValue(event)) })), + labeledField(e, "礼包名称", e("input", { value: view.giftName, "aria-label": "礼包名称", placeholder: "例如 新人礼包", onChange: (event: InputEvent) => view.setGiftName(inputValue(event)) })), + labeledField(e, "礼包周期", e("select", { value: view.giftClass, "aria-label": "礼包周期", onChange: (event: InputEvent) => view.setGiftClass(inputValue(event)) }, [["1", "每日"], ["2", "每周"], ["3", "每月"], ["4", "每年"], ["5", "一次"], ["6", "每日五次"]].map(([value, label]) => e("option", { key: value, value }, label)))), + labeledField(e, "适用玩家", e("select", { value: view.giftAudience, "aria-label": "适用玩家", onChange: (event: InputEvent) => view.setGiftAudience(inputValue(event)) }, e("option", { value: "all" }, "全部玩家"), e("option", { value: "pve" }, "PVE 玩家"), e("option", { value: "pvp" }, "PVP 玩家"))), + labeledField(e, "发放次数", e("input", { value: view.giftNumber, "aria-label": "发放次数", type: "number", placeholder: "1", onChange: (event: InputEvent) => view.setGiftNumber(inputValue(event)) })), + e("div", { className: "console-row-actions" }, labeledField(e, "成就类型", e("input", { value: view.giftAchievement, "aria-label": "成就类型", type: "number", placeholder: "0", onChange: (event: InputEvent) => view.setGiftAchievement(inputValue(event)) })), labeledField(e, "成就值", e("input", { value: view.giftAchievementNumber, "aria-label": "成就值", type: "number", placeholder: "0", onChange: (event: InputEvent) => view.setGiftAchievementNumber(inputValue(event)) }))), + labeledField(e, "礼包物品", e("input", { value: view.giftItems, "aria-label": "礼包物品", placeholder: "SCUM 目录代码:数量", onChange: (event: InputEvent) => view.setGiftItems(inputValue(event)) })), + labeledField(e, "礼包命令", e("textarea", { value: view.giftCommands, "aria-label": "礼包命令", placeholder: "每行一条命令", onChange: (event: InputEvent) => view.setGiftCommands(inputValue(event)) })), e("button", { type: "button", className: "primary-command", disabled: !actions?.pluginData, onClick: saveGift }, "保存礼包") ), e("div", { className: "console-record-list" }, data.gifts.length ? data.gifts.map((gift, index) => { @@ -480,8 +479,8 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu ) : null, view.giftTab === "deliveries" ? e("div", { className: "console-record-list" }, e("article", { className: "console-module" }, e("h2", null, "创建发放记录"), - e("select", { value: view.deliveryGift, "aria-label": "选择礼包", onChange: (event: InputEvent) => view.setDeliveryGift(inputValue(event)) }, e("option", { value: "" }, "选择礼包"), data.gifts.map((gift, index) => { const key = textField(gift, "code", "id"); return e("option", { key: idOf(gift, `gift-option-${index}`), value: key }, textField(gift, "name") || key); })), - e("select", { value: view.deliveryPlayer, "aria-label": "选择用户", onChange: (event: InputEvent) => view.setDeliveryPlayer(inputValue(event)) }, e("option", { value: "" }, "选择用户"), data.players.map((player, index) => { const key = textField(player, "gamePlayerId", "steamId", "id"); return e("option", { key: idOf(player, `player-option-${index}`), value: key }, textField(player, "displayName") || key); })), + labeledField(e, "礼包", e("select", { value: view.deliveryGift, "aria-label": "选择礼包", onChange: (event: InputEvent) => view.setDeliveryGift(inputValue(event)) }, e("option", { value: "" }, "选择礼包"), data.gifts.map((gift, index) => { const key = textField(gift, "code", "id"); return e("option", { key: idOf(gift, `gift-option-${index}`), value: key }, textField(gift, "name") || key); }))), + labeledField(e, "用户", e("select", { value: view.deliveryPlayer, "aria-label": "选择用户", onChange: (event: InputEvent) => view.setDeliveryPlayer(inputValue(event)) }, e("option", { value: "" }, "选择用户"), data.players.map((player, index) => { const key = textField(player, "gamePlayerId", "steamId", "id"); return e("option", { key: idOf(player, `player-option-${index}`), value: key }, textField(player, "displayName") || key); }))), e("button", { type: "button", className: "primary-command", disabled: !actions?.pluginData || !actions?.gameClient, onClick: queueDelivery }, "立即发放") ), tablePanel(e, "发放记录", data.giftDeliveries, (delivery) => [textField(delivery, "playerName", "playerId") || "unknown", textField(delivery, "giftName", "giftCode") || "unknown", textField(delivery, "status") || "unknown", dateField(delivery, "deliveredAt", "createdAt")]) @@ -509,16 +508,16 @@ function mapSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: return e("div", { className: "console-record-list" }, statsStrip(e, [["地图点", points.length], ["用户", data.players.length], ["载具", data.vehicles.length], ["旗帜/区域", data.flags.length + data.mapRegions.length]]), e("div", { className: "console-row-actions" }, - e("input", { value: view.mapSearch, "aria-label": "筛选地图点", placeholder: "名称 / 类型 / ID", onChange: (event: InputEvent) => view.setMapSearch(inputValue(event)) }), + labeledField(e, "筛选地图点", e("input", { value: view.mapSearch, "aria-label": "筛选地图点", placeholder: "名称 / 类型 / ID", onChange: (event: InputEvent) => view.setMapSearch(inputValue(event)) })), (["players", "vehicles", "flags", "regions", "other"] as MapLayer[]).map((layer) => e("label", { key: layer }, e("input", { type: "checkbox", checked: view.mapLayers[layer], onChange: (event: InputEvent) => view.setMapLayers((previous) => ({ ...previous, [layer]: Boolean(event.target?.checked) })) }), layerLabel(layer))) ), - e("article", { className: "console-module" }, e("div", { className: "panel-header" }, e("h2", null, "地图范围"), e("span", { className: "page-status" }, customEnabled ? "自定义范围" : "SCUM 默认范围")), + e("details", { className: "console-module scum-editor" }, e("summary", null, e("strong", null, "地图范围"), e("span", { className: "page-status" }, customEnabled ? "自定义范围" : "SCUM 默认范围")), e("div", { className: "console-row-actions" }, e("label", null, e("input", { type: "checkbox", checked: customEnabled, "aria-label": "启用自定义地图", onChange: (event: InputEvent) => view.setMapCustomEnabled(Boolean(event.target?.checked)) }), "启用自定义地图"), - e("input", { value: centerX, "aria-label": "地图中心 X", type: "number", onChange: (event: InputEvent) => view.setMapCenterX(inputValue(event)) }), - e("input", { value: centerY, "aria-label": "地图中心 Y", type: "number", onChange: (event: InputEvent) => view.setMapCenterY(inputValue(event)) }), - e("input", { value: widthKm, "aria-label": "地图宽度公里", type: "number", onChange: (event: InputEvent) => view.setMapWidthKm(inputValue(event)) }), - e("input", { value: heightKm, "aria-label": "地图高度公里", type: "number", onChange: (event: InputEvent) => view.setMapHeightKm(inputValue(event)) }), + labeledField(e, "中心 X", e("input", { value: centerX, "aria-label": "地图中心 X", type: "number", onChange: (event: InputEvent) => view.setMapCenterX(inputValue(event)) })), + labeledField(e, "中心 Y", e("input", { value: centerY, "aria-label": "地图中心 Y", type: "number", onChange: (event: InputEvent) => view.setMapCenterY(inputValue(event)) })), + labeledField(e, "宽度 (公里)", e("input", { value: widthKm, "aria-label": "地图宽度公里", type: "number", onChange: (event: InputEvent) => view.setMapWidthKm(inputValue(event)) })), + labeledField(e, "高度 (公里)", e("input", { value: heightKm, "aria-label": "地图高度公里", type: "number", onChange: (event: InputEvent) => view.setMapHeightKm(inputValue(event)) })), e("button", { type: "button", className: "primary-command", disabled: !actions?.pluginData, onClick: () => runAction(view.setAction, "正在保存地图范围…", async () => { await saveMapSettings(actions ?? {}, { customMapEnabled: customEnabled, centerX: numberInput(centerX, 0), centerY: numberInput(centerY, 0), widthKm: numberInput(widthKm, 15.24), heightKm: numberInput(heightKm, 15.24) }); view.refresh(); return "地图范围已保存。"; }) }, "保存地图范围")) ), e("div", { className: "overview-two-col" }, @@ -528,6 +527,7 @@ function mapSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: ); } +function labeledField(e: ReactLike["createElement"], label: string, control: unknown) { return e("label", { className: "console-field" }, e("span", null, label), control); } function giftTabButton(e: ReactLike["createElement"], view: ViewState, tab: GiftTab, label: string) { return e("button", { type: "button", role: "tab", "aria-selected": view.giftTab === tab, className: view.giftTab === tab ? "primary-command" : "icon-command", onClick: () => view.setGiftTab(tab) }, label); } function statsStrip(e: ReactLike["createElement"], items: Array<[string, number]>) { return e("div", { className: "console-stat-strip" }, items.map(([label, value]) => e("span", { key: label, className: "server-card-stat" }, e("span", null, label), e("strong", null, String(value))))); } function tablePanel(e: ReactLike["createElement"], title: string, rows: RecordMap[], render: (row: RecordMap) => unknown[]) { return e("article", { className: "console-module" }, e("div", { className: "panel-header" }, e("h2", null, title), e("span", { className: "page-status" }, `${rows.length} 条`)), e("div", { className: "console-row-list" }, rows.length ? rows.map((row, index) => e("div", { key: idOf(row, `${title}-${index}`), className: "console-row" }, render(row).map((part, partIndex) => partIndex === 0 ? e("span", { key: partIndex }, String(part ?? "unknown")) : e("strong", { key: partIndex }, String(part ?? "unknown"))))) : e("p", { className: "page-status" }, "暂无真实记录。"))); } @@ -590,4 +590,3 @@ function numberInput(value: string, fallback: number): number { const parsed = N function isRecord(value: unknown): value is RecordMap { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function errorMessage(error: unknown, fallback: string): string { return error instanceof Error ? error.message : fallback; } function surfaceTitle(pageKey: string): string { return pageKey === "squads" ? "队伍管理" : pageKey === "live-map" ? "实时地图" : pageKey === "gifts" ? "礼包管理" : pageKey === "workflows" || pageKey === "activity" ? "活动管理" : "用户管理"; } -function surfaceSummary(pageKey: string): string { return pageKey === "live-map" ? "地图点、用户、载具、旗帜与区域由 SCUM 插件集合提供。" : pageKey === "gifts" ? "礼包定义、领取和发放记录由 SCUM 插件自有集合管理。" : pageKey === "squads" ? "队伍与成员关系由插件集合管理,可按队伍查看 roster。" : pageKey === "workflows" || pageKey === "activity" ? "活动定义、运行状态和事件记录由插件集合管理。" : "用户列表来自插件声明的 SCUM.db 查询与日志同步,不显示样例数据。"; } diff --git a/plugins/tests/scum-feature-module.test.ts b/plugins/tests/scum-feature-module.test.ts index fddc787..82e079c 100644 --- a/plugins/tests/scum-feature-module.test.ts +++ b/plugins/tests/scum-feature-module.test.ts @@ -175,16 +175,17 @@ describe("SCUM plugin feature module", () => { it("renders searchable user management from real collection values", () => { const view = renderAndCollect(); expect(view.nodes).toContain("section:用户管理"); - expect(view.texts.join("\n")).toContain("插件声明的 SCUM.db 查询与日志同步"); - expect(view.texts).toEqual(expect.arrayContaining(["用户名", "Steam", "队伍", "上次登录", "登录 IP", "操作"])); + expect(view.texts).toContain("SCUM 用户真实记录"); + expect(view.texts).toEqual(expect.arrayContaining(["用户名", "队伍", "状态", "最后活动", "操作"])); + expect(view.texts).not.toEqual(expect.arrayContaining(["Steam", "上次登录", "登录 IP", "概况"])); expect(view.texts).not.toContain("筛选结果"); 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(view.inputs.map((input) => input.label)).toContain("搜索用户"); expect(view.texts).toContain("Mira"); - expect(view.texts).toContain("76561198000000001"); - expect(view.texts.join("\n")).toContain("Fame 42"); + expect(view.texts).not.toContain("76561198000000001"); + expect(view.texts.join("\n")).not.toContain("Fame 42"); }); it("prepares player attribute SQL execution through platform-to-Run remote access", async () => { @@ -272,7 +273,7 @@ describe("SCUM plugin feature module", () => { expect(source).toContain("remote.access.request"); expect(source).not.toContain("input.templateKey"); expect(source).not.toContain("requestSCUMPageQueries"); - expect(pageSource).toContain("setInterval(refresh, 3000)"); + expect(pageSource).toContain("setInterval(refresh, 10000)"); expect(pageSource).toContain("clearInterval(interval)"); }); });