fix: make SCUM gift management gift-first
This commit is contained in:
@@ -85,11 +85,13 @@ type SCUMPlatformSurfaceKey = "players" | "vehicles" | "trajectories" | "vehicle
|
||||
|
||||
// Call budget per live-map load/15s refresh: 7 bounded plugin collections + 1 typed SCUM surface.
|
||||
// Squads and members load once per refresh; hovering, selecting and playback issue 0 API calls.
|
||||
// Call budget per gifts load/15s refresh: 5 bounded plugin collections + 1 typed SCUM surface.
|
||||
// Searching, viewing a definition and opening the editor issue 0 additional API calls.
|
||||
const pageCollections: Record<PageKey, PluginCollectionSurfaceKey[]> = {
|
||||
players: ["members", "activityEvents", "giftClaims", "pendingGifts", "giftDeliveries"],
|
||||
squads: ["squads", "members", "flags"],
|
||||
"live-map": ["mapPoints", "mapRegions", "mapSettings", "flags", "squads", "members", "tradeGoods"],
|
||||
gifts: ["gifts", "giftClaims", "pendingGifts", "giftDeliveries", "timedGiftEvents", "tradeGoods"],
|
||||
gifts: ["gifts", "giftClaims", "pendingGifts", "giftDeliveries", "timedGiftEvents"],
|
||||
workflows: ["events", "eventProduces", "eventRuns", "nativeEventRounds", "tasks", "activityEvents", "tradeGoods", "tradeEvents"]
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import type { MapPlaybackFrame, MapPlaybackState, MapSceneData, MapTimeWindow, M
|
||||
type StateSetter<T> = (next: T | ((previous: T) => T)) => void;
|
||||
type InputEvent = { target?: { value?: string; checked?: boolean; open?: boolean }; currentTarget?: { open?: boolean }; stopPropagation?: () => void };
|
||||
type GiftTab = "definitions" | "claims" | "deliveries" | "timed";
|
||||
type GiftDialogState = { mode: "closed" } | { mode: "view" | "edit"; gift?: RecordMap };
|
||||
type MapLayer = "players" | "vehicles" | "flags" | "regions" | "other";
|
||||
type MapVehicleFilter = "all" | "land" | "water" | "air" | "other";
|
||||
type ScumMapLayer = "terrain" | "topo" | "night";
|
||||
@@ -145,7 +146,7 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
const mapFollowRef = react.useRef ? react.useRef(true) : { current: true };
|
||||
const [eventEditorOpen, setEventEditorOpen] = usePluginState(react, false);
|
||||
const [produceEditorOpen, setProduceEditorOpen] = usePluginState(react, false);
|
||||
const [giftEditorOpen, setGiftEditorOpen] = usePluginState(react, false);
|
||||
const [giftDialog, setGiftDialog] = usePluginState<GiftDialogState>(react, { mode: "closed" });
|
||||
const [mapBaseLayer, setMapBaseLayer] = usePluginState<ScumMapLayer | undefined>(react, undefined);
|
||||
const [mapZonesOverlay, setMapZonesOverlay] = usePluginState<boolean | undefined>(react, undefined);
|
||||
const [catalogSearch, setCatalogSearch] = usePluginState(react, "");
|
||||
@@ -218,11 +219,11 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
setSelectedMapPoint("");
|
||||
}, [input.serverInstanceId, pageKey, mapUsers, mapTimeFrom, mapTimeTo]);
|
||||
if (react.useEffect) react.useEffect(() => {
|
||||
if (!giftEditorOpen || typeof document === "undefined") return;
|
||||
const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") setGiftEditorOpen(false); };
|
||||
if (giftDialog.mode === "closed" || typeof document === "undefined") return;
|
||||
const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") setGiftDialog({ mode: "closed" }); };
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, [giftEditorOpen]);
|
||||
}, [giftDialog.mode]);
|
||||
if (react.useEffect) react.useEffect(() => {
|
||||
if (pageKey !== "live-map") return;
|
||||
if (!focusUser) {
|
||||
@@ -263,7 +264,7 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
hoverMapPoint, setHoverMapPoint,
|
||||
mapView, setMapView, mapViewportRef, mapPlaybackFrameRef, mapFollowRef, mapTimePreset, setMapTimePreset, mapTimeFrom, setMapTimeFrom, mapTimeTo, setMapTimeTo,
|
||||
mapUsers, setMapUsers, mapTrackKey, setMapTrackKey, mapPlayback, setMapPlayback,
|
||||
eventEditorOpen, setEventEditorOpen, produceEditorOpen, setProduceEditorOpen, giftEditorOpen, setGiftEditorOpen,
|
||||
eventEditorOpen, setEventEditorOpen, produceEditorOpen, setProduceEditorOpen, giftDialog, setGiftDialog,
|
||||
mapBaseLayer, setMapBaseLayer, mapZonesOverlay, setMapZonesOverlay, catalogSearch, setCatalogSearch, catalogKind, setCatalogKind, catalogOpen, setCatalogOpen,
|
||||
setAction, refresh: () => setRefreshSignal((value) => value + 1),
|
||||
mapData, mapSceneContents: null, mapStaticContents: null, mapSelectedTrails: [], mapSelectedLocks: []
|
||||
@@ -285,7 +286,7 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
const renderMapScene = () => mapData ? mapSceneContents(e, data, view, mapData) : null;
|
||||
// Pan/zoom only changes the parent transform. Keep the geometry and marker elements stable.
|
||||
view.mapSceneContents = react.useMemo ? react.useMemo(renderMapScene, [mapData, mapLayers, mapPlayback.value, mapPlayback.startedAt, hoverMapPoint]) : renderMapScene();
|
||||
return e("section", { className: "console-panel scum-workbench", "aria-label": input.pageTitle ?? surfaceTitle(pageKey) },
|
||||
return e("section", { className: `console-panel scum-workbench${giftDialog.mode === "closed" ? "" : " has-management-dialog"}`, "aria-label": input.pageTitle ?? surfaceTitle(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,
|
||||
@@ -318,7 +319,7 @@ type ViewState = {
|
||||
mapFollowRef: { current: boolean };
|
||||
mapTimePreset: string; setMapTimePreset: StateSetter<string>; mapTimeFrom: string; setMapTimeFrom: StateSetter<string>; mapTimeTo: string; setMapTimeTo: StateSetter<string>;
|
||||
mapUsers: string[] | undefined; setMapUsers: StateSetter<string[] | undefined>; mapTrackKey: string; setMapTrackKey: StateSetter<string>; mapPlayback: MapPlaybackState; setMapPlayback: StateSetter<MapPlaybackState>;
|
||||
eventEditorOpen: boolean; setEventEditorOpen: StateSetter<boolean>; produceEditorOpen: boolean; setProduceEditorOpen: StateSetter<boolean>; giftEditorOpen: boolean; setGiftEditorOpen: StateSetter<boolean>;
|
||||
eventEditorOpen: boolean; setEventEditorOpen: StateSetter<boolean>; produceEditorOpen: boolean; setProduceEditorOpen: StateSetter<boolean>; giftDialog: GiftDialogState; setGiftDialog: StateSetter<GiftDialogState>;
|
||||
mapBaseLayer: ScumMapLayer | undefined; setMapBaseLayer: StateSetter<ScumMapLayer | undefined>; mapZonesOverlay: boolean | undefined; setMapZonesOverlay: StateSetter<boolean | undefined>;
|
||||
catalogSearch: string; setCatalogSearch: StateSetter<string>; catalogKind: string; setCatalogKind: StateSetter<string>; catalogOpen: boolean; setCatalogOpen: StateSetter<boolean>;
|
||||
};
|
||||
@@ -601,10 +602,10 @@ function activitiesSurface(e: ReactLike["createElement"], data: SCUMSurfaceData,
|
||||
function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: SCUMPageContext, view: ViewState) {
|
||||
const actions = input.workspaceActions;
|
||||
const openNewGift = () => {
|
||||
view.setGiftRecordKey(""); view.setGiftCode(""); view.setGiftName(""); view.setGiftItems(""); view.setGiftCommands(""); view.setGiftClass("5"); view.setGiftAudience("all"); view.setGiftNumber("1"); view.setGiftAchievement("0"); view.setGiftAchievementNumber("0"); view.setGiftEditorOpen(true);
|
||||
view.setGiftRecordKey(""); view.setGiftCode(""); view.setGiftName(""); view.setGiftItems(""); view.setGiftCommands(""); view.setGiftClass("5"); view.setGiftAudience("all"); view.setGiftNumber("1"); view.setGiftAchievement("0"); view.setGiftAchievementNumber("0"); view.setGiftDialog({ mode: "edit" });
|
||||
};
|
||||
const openGiftEditor = (gift: RecordMap) => {
|
||||
view.setGiftRecordKey(textField(gift, "_recordKey", "code", "id")); view.setGiftCode(textField(gift, "code")); view.setGiftName(textField(gift, "name")); view.setGiftClass(numField(gift, "class")); view.setGiftAudience(textField(gift, "audience") || "all"); view.setGiftNumber(numField(gift, "number")); view.setGiftAchievement(numField(gift, "achievement")); view.setGiftAchievementNumber(numField(gift, "achievementNumber", "achievement_number")); view.setGiftItems(giftItemsInput(gift)); view.setGiftCommands(giftCommandsInput(gift)); view.setGiftEditorOpen(true);
|
||||
const openGiftDialog = (gift: RecordMap, mode: "view" | "edit") => {
|
||||
view.setGiftRecordKey(textField(gift, "_recordKey", "code", "id")); view.setGiftCode(textField(gift, "code")); view.setGiftName(textField(gift, "name")); view.setGiftClass(numField(gift, "class")); view.setGiftAudience(textField(gift, "audience") || "all"); view.setGiftNumber(numField(gift, "number")); view.setGiftAchievement(numField(gift, "achievement")); view.setGiftAchievementNumber(numField(gift, "achievementNumber", "achievement_number")); view.setGiftItems(giftItemsInput(gift)); view.setGiftCommands(giftCommandsInput(gift)); view.setGiftDialog({ mode, gift });
|
||||
};
|
||||
const saveGift = () => runAction(view.setAction, "正在保存礼包定义…", async () => {
|
||||
const code = view.giftCode.trim();
|
||||
@@ -623,10 +624,11 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
status: "active", updatedAt: new Date().toISOString()
|
||||
});
|
||||
if (view.giftRecordKey && view.giftRecordKey !== code) await deleteGiftDefinition(actions ?? {}, view.giftRecordKey);
|
||||
view.setGiftEditorOpen(false); view.setGiftRecordKey("");
|
||||
view.setGiftDialog({ mode: "closed" }); view.setGiftRecordKey("");
|
||||
view.refresh();
|
||||
return `礼包 ${name} 已保存。`;
|
||||
});
|
||||
const selectedGift = view.giftDialog.mode === "closed" ? undefined : view.giftDialog.gift;
|
||||
const queueDelivery = () => runAction(view.setAction, "正在创建发放记录…", async () => {
|
||||
const giftCode = view.deliveryGift || textField(data.gifts[0], "code", "id");
|
||||
const playerId = view.deliveryPlayer || textField(data.players[0], "gamePlayerId", "steamId", "id");
|
||||
@@ -644,7 +646,7 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
return matchesText(gift, search, "code", "name", "audience", "status") && (view.giftStatus === "all" || statusFilter === view.giftStatus);
|
||||
});
|
||||
return e("div", { className: "console-record-list" },
|
||||
statsStrip(e, [["礼包定义", data.gifts.length], ["领取/待领", data.giftClaims.length + data.pendingGifts.length], ["发放记录", data.giftDeliveries.length], ["物品列表", data.tradeGoods.length]]),
|
||||
statsStrip(e, [["礼包定义", data.gifts.length], ["领取/待领", data.giftClaims.length + data.pendingGifts.length], ["发放记录", data.giftDeliveries.length]]),
|
||||
e("div", { className: "console-row-actions", role: "tablist", "aria-label": "礼包视图" },
|
||||
giftTabButton(e, view, "definitions", "礼包定义"), giftTabButton(e, view, "claims", "领取/待领"), giftTabButton(e, view, "deliveries", "发放记录"), giftTabButton(e, view, "timed", "游戏定时记录")
|
||||
),
|
||||
@@ -669,10 +671,11 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
e("td", null, e("span", { className: `status-pill ${activeStatus(status) ? "status-active" : "status-disabled"}` }, giftStatusLabel(status))),
|
||||
e("td", null, e("span", { className: "provider-id" }, dateField(gift, "updatedAt", "createdAt"))),
|
||||
e("td", { className: "provider-actions-cell" }, e("div", { className: "scum-gift-table-actions" },
|
||||
e("button", { type: "button", className: "icon-command", onClick: () => openGiftEditor(gift) }, "编辑"),
|
||||
e("button", { type: "button", className: "icon-command", onClick: () => openGiftDialog(gift, "view") }, "查看配置"),
|
||||
e("details", { className: "inline-action-control" },
|
||||
e("summary", { className: "icon-command", "aria-label": `打开${textField(gift, "name") || key}更多操作` }, "更多"),
|
||||
e("div", { className: "inline-action-menu", role: "menu", "aria-label": "礼包操作" },
|
||||
e("button", { type: "button", className: "runtime-action-item", role: "menuitem", onClick: () => openGiftDialog(gift, "edit") }, "编辑礼包"),
|
||||
e("button", { type: "button", className: "runtime-action-item", role: "menuitem", onClick: () => { view.setDeliveryGift(key); view.setGiftTab("deliveries"); } }, "选择发放"),
|
||||
e("button", { type: "button", className: "runtime-action-item danger-command", role: "menuitem", disabled: !actions?.pluginData, onClick: () => runAction(view.setAction, "正在删除礼包…", async () => { await deleteGiftDefinition(actions ?? {}, key); view.refresh(); return "礼包定义已删除。"; }) }, "删除")
|
||||
)
|
||||
@@ -683,7 +686,6 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
)
|
||||
),
|
||||
) : null,
|
||||
view.giftTab === "definitions" ? itemCatalogPanel(e, data.tradeGoods) : null,
|
||||
view.giftTab === "claims" ? e("div", { className: "overview-two-col" },
|
||||
resettableGiftPanel(e, "领取记录", data.giftClaims, "重置领取", (claim) => runAction(view.setAction, "正在重置领取记录…", async () => { await resetGiftClaim(actions ?? {}, claim); view.refresh(); return "领取记录已重置。"; }), actions),
|
||||
resettableGiftPanel(e, "待领礼包", data.pendingGifts, "重置待领", (pending) => runAction(view.setAction, "正在重置待领记录…", async () => { await resetPendingGift(actions ?? {}, pending); view.refresh(); return "待领状态已重置。"; }), actions)
|
||||
@@ -700,15 +702,47 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
e("p", { className: "page-status" }, "这里是 SCUM 原生 finished_timed_gift_spawner 完成记录,不是插件运营礼包定义。"),
|
||||
tablePanel(e, "游戏原生定时礼包", data.timedGiftEvents, (event) => [textField(event, "userProfileId") || "unknown", textField(event, "mapId") || "unknown", textField(event, "spawnTime") || "unknown", dateField(event, "spawnAt")])
|
||||
) : null,
|
||||
view.giftEditorOpen ? giftEditorDialog(e, view, actions, saveGift) : null
|
||||
view.giftDialog.mode === "view" && selectedGift ? giftDetailsDialog(e, selectedGift, view, () => openGiftDialog(selectedGift, "edit")) : null,
|
||||
view.giftDialog.mode === "edit" ? giftEditorDialog(e, view, actions, saveGift) : null
|
||||
);
|
||||
}
|
||||
|
||||
function giftDetailsDialog(e: ReactLike["createElement"], gift: RecordMap, view: ViewState, editGift: () => void) {
|
||||
const items = giftItemRecords(gift);
|
||||
const commands = giftCommandValues(gift);
|
||||
const status = textField(gift, "status").toLowerCase() || "active";
|
||||
return e("div", { className: "confirm-backdrop management-dialog-backdrop gift-dialog-backdrop", role: "presentation", onClick: () => view.setGiftDialog({ mode: "closed" }) },
|
||||
e("div", { className: "drawer-panel management-dialog-panel management-dialog-wide", role: "dialog", "aria-modal": "true", "aria-label": "查看礼包配置", onClick: (event: InputEvent) => event.stopPropagation?.() },
|
||||
e("div", { className: "panel-header dialog-panel-header" }, e("div", { className: "gift-dialog-heading" }, e("h2", null, textField(gift, "name") || textField(gift, "code") || "礼包配置"), e("span", { className: "page-status" }, "礼包配置详情")), e("button", { type: "button", className: "theme-upload drawer-close", "aria-label": "关闭礼包配置", onClick: () => view.setGiftDialog({ mode: "closed" }) }, "关闭")),
|
||||
e("div", { className: "gift-config-details dialog-form" },
|
||||
e("dl", { className: "gift-config-grid" },
|
||||
giftConfigEntry(e, "礼包编号", textField(gift, "code", "id") || "未配置"),
|
||||
giftConfigEntry(e, "状态", giftStatusLabel(status)),
|
||||
giftConfigEntry(e, "领取周期", giftClassLabel(numField(gift, "class"))),
|
||||
giftConfigEntry(e, "适用玩家", giftAudienceLabel(textField(gift, "audience"))),
|
||||
giftConfigEntry(e, "发放次数", numField(gift, "number")),
|
||||
giftConfigEntry(e, "成就门槛", `${numField(gift, "achievement")} / ${numField(gift, "achievementNumber", "achievement_number")}`)
|
||||
),
|
||||
e("section", { className: "gift-form-section gift-config-section", "aria-label": "礼包内容" },
|
||||
e("div", { className: "gift-form-section-title" }, e("strong", null, "礼包内容"), e("span", null, `${items.length} 项物品,${commands.length} 条命令`)),
|
||||
items.length ? e("div", { className: "gift-config-list" }, items.map((item, index) => {
|
||||
const code = textField(item, "catalogCode", "catalogItemKey", "key", "className");
|
||||
const entry = scumCatalogEntryFor(code);
|
||||
return e("div", { key: `${code}-${index}`, className: "gift-config-row" }, catalogIconStrip(e, catalogIconsForValue(code)), e("span", null, entry?.name || textField(item, "label") || code || "未命名物品"), e("code", null, code || "unknown"), e("strong", null, `× ${numField(item, "quantity")}`));
|
||||
})) : e("p", { className: "page-status" }, "未配置物品。"),
|
||||
commands.length ? e("div", { className: "gift-command-list", "aria-label": "礼包命令" }, commands.map((command, index) => e("code", { key: `${command}-${index}` }, command))) : e("p", { className: "page-status" }, "未配置附加命令。")
|
||||
),
|
||||
e("div", { className: "confirm-actions gift-dialog-actions" }, e("button", { type: "button", onClick: () => view.setGiftDialog({ mode: "closed" }) }, "关闭"), e("button", { type: "button", className: "confirm-primary", onClick: editGift }, "编辑礼包"))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function giftEditorDialog(e: ReactLike["createElement"], view: ViewState, actions: SCUMWorkspaceActions | undefined, saveGift: () => void) {
|
||||
const editing = Boolean(view.giftRecordKey);
|
||||
return e("div", { className: "confirm-backdrop management-dialog-backdrop gift-dialog-backdrop", role: "presentation", onClick: () => view.setGiftEditorOpen(false) },
|
||||
return e("div", { className: "confirm-backdrop management-dialog-backdrop gift-dialog-backdrop", role: "presentation", onClick: () => view.setGiftDialog({ mode: "closed" }) },
|
||||
e("div", { className: "drawer-panel management-dialog-panel management-dialog-wide", role: "dialog", "aria-modal": "true", "aria-label": editing ? "编辑礼包" : "新建礼包", onClick: (event: InputEvent) => event.stopPropagation?.() },
|
||||
e("div", { className: "panel-header dialog-panel-header" }, e("div", { className: "gift-dialog-heading" }, e("h2", null, editing ? "编辑礼包" : "新建礼包"), e("span", { className: "page-status" }, editing ? "更新现有礼包定义" : "创建可领取的运营礼包")), e("button", { type: "button", className: "theme-upload drawer-close", "aria-label": "关闭礼包编辑器", onClick: () => view.setGiftEditorOpen(false) }, "关闭")),
|
||||
e("div", { className: "panel-header dialog-panel-header" }, e("div", { className: "gift-dialog-heading" }, e("h2", null, editing ? "编辑礼包" : "新建礼包"), e("span", { className: "page-status" }, editing ? "更新现有礼包定义" : "创建可领取的运营礼包")), e("button", { type: "button", className: "theme-upload drawer-close", "aria-label": "关闭礼包编辑器", onClick: () => view.setGiftDialog({ mode: "closed" }) }, "关闭")),
|
||||
e("div", { className: "gift-form dialog-form management-form" },
|
||||
e("div", { className: "form-guidance management-form-wide" }, e("strong", null, "先定义规则,再配置内容"), e("span", null, "礼包编号用于数据主键,只支持字母、数字、点、下划线和短横线;礼包至少要包含一项物品或一条命令。")),
|
||||
labeledField(e, "礼包编号", e("input", { value: view.giftCode, "aria-label": "礼包编号", placeholder: "例如 welcome_pack", onChange: (event: InputEvent) => view.setGiftCode(inputValue(event)) })),
|
||||
@@ -720,7 +754,7 @@ function giftEditorDialog(e: ReactLike["createElement"], view: ViewState, action
|
||||
labeledField(e, "成就值", e("input", { value: view.giftAchievementNumber, "aria-label": "成就值", type: "number", min: "0", placeholder: "0", onChange: (event: InputEvent) => view.setGiftAchievementNumber(inputValue(event)) })),
|
||||
e("div", { className: "gift-form-wide gift-form-section" }, e("div", { className: "gift-form-section-title" }, e("strong", null, "礼包物品"), e("span", null, "可用目录选择器快速加入,格式为 目录代码:数量")), labeledField(e, "物品清单", e("input", { value: view.giftItems, "aria-label": "礼包物品", placeholder: "例如 BP_Cash_01:2, Water-Bottle.01:1", onChange: (event: InputEvent) => view.setGiftItems(inputValue(event)) })), catalogPicker(e, view, "gift-items")),
|
||||
e("div", { className: "gift-form-wide gift-form-section" }, e("div", { className: "gift-form-section-title" }, e("strong", null, "礼包命令"), e("span", null, "每行一条 SCUM RCON 命令,可选")), labeledField(e, "命令清单", e("textarea", { value: view.giftCommands, "aria-label": "礼包命令", placeholder: "例如 #announce Welcome\n每行一条命令", onChange: (event: InputEvent) => view.setGiftCommands(inputValue(event)) }))),
|
||||
e("div", { className: "confirm-actions gift-dialog-actions" }, e("button", { type: "button", onClick: () => view.setGiftEditorOpen(false) }, "取消"), e("button", { type: "button", className: "confirm-primary", disabled: !actions?.pluginData, onClick: saveGift }, "保存礼包"))
|
||||
e("div", { className: "confirm-actions gift-dialog-actions" }, e("button", { type: "button", onClick: () => view.setGiftDialog({ mode: "closed" }) }, "取消"), e("button", { type: "button", className: "confirm-primary", disabled: !actions?.pluginData, onClick: saveGift }, "保存礼包"))
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -1500,6 +1534,9 @@ function giftItemsSummary(gift: RecordMap): string { const items = field(gift, "
|
||||
function giftCommandsSummary(gift: RecordMap): string { const commands = field(gift, "commands"); return Array.isArray(commands) && commands.length ? `${commands.length} 条命令` : "无命令"; }
|
||||
function giftItemsInput(gift: RecordMap): string { const items = field(gift, "items"); return Array.isArray(items) ? items.map((item) => isRecord(item) ? `${textField(item, "catalogCode", "catalogItemKey", "key")}:${numField(item, "quantity")}` : "").filter(Boolean).join(", ") : ""; }
|
||||
function giftCommandsInput(gift: RecordMap): string { const commands = field(gift, "commands"); return Array.isArray(commands) ? commands.map((item) => isRecord(item) ? textField(item, "command", "value") : String(item)).filter(Boolean).join("\n") : ""; }
|
||||
function giftItemRecords(gift: RecordMap): RecordMap[] { const items = field(gift, "items"); return Array.isArray(items) ? items.filter(isRecord) : []; }
|
||||
function giftCommandValues(gift: RecordMap): string[] { const commands = field(gift, "commands"); return Array.isArray(commands) ? commands.map((item) => isRecord(item) ? textField(item, "command", "value") : String(item)).filter(Boolean) : []; }
|
||||
function giftConfigEntry(e: ReactLike["createElement"], label: string, value: string) { return e("div", { className: "gift-config-entry" }, e("dt", null, label), e("dd", null, value)); }
|
||||
function giftClassLabel(value: string): string { return ({ "1": "每日", "2": "每周", "3": "每月", "4": "每年", "5": "一次", "6": "每日五次" } as Record<string, string>)[value] ?? value; }
|
||||
function giftAudienceLabel(value: string): string { return (({ all: "全部玩家", pve: "PVE 玩家", pvp: "PVP 玩家" } as Record<string, string>)[value] ?? value) || "全部玩家"; }
|
||||
function giftStatusLabel(value: string): string { return (({ active: "启用", enabled: "启用", running: "启用", scheduled: "启用", queued: "启用", disabled: "停用", inactive: "停用" } as Record<string, string>)[value] ?? value) || "启用"; }
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("SCUM plugin feature module", () => {
|
||||
const list = vi.fn(async (collection: string) => ({ items: [{ key: `${collection}-1`, value: { collection } }], count: 1 }));
|
||||
const scum = scumActions();
|
||||
const data = await loadSCUMSurface({ pluginData: pluginDataActions({ list }), scum }, "gifts");
|
||||
expect(list.mock.calls.map(([collection]) => collection)).toEqual([scumCollections.gifts, scumCollections.giftClaims, scumCollections.pendingGifts, scumCollections.giftDeliveries, scumCollections.timedGiftEvents, scumCollections.tradeGoods]);
|
||||
expect(list.mock.calls.map(([collection]) => collection)).toEqual([scumCollections.gifts, scumCollections.giftClaims, scumCollections.pendingGifts, scumCollections.giftDeliveries, scumCollections.timedGiftEvents]);
|
||||
expect(scum.surface).toHaveBeenCalledTimes(1);
|
||||
expect(data.gifts[0]).toMatchObject({ collection: scumCollections.gifts, _recordKey: `${scumCollections.gifts}-1` });
|
||||
});
|
||||
@@ -271,11 +271,12 @@ describe("SCUM plugin feature module", () => {
|
||||
it("renders gift definitions, claims, and delivery records", () => {
|
||||
const definitions = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理" });
|
||||
expect(definitions.texts).toContain("礼包定义");
|
||||
expect(definitions.texts).toContain("物品列表");
|
||||
expect(definitions.texts).not.toContain("物品列表");
|
||||
expect(definitions.texts).toContain("Starter Pack");
|
||||
expect(definitions.texts).toContain("Cargo Drop");
|
||||
expect(definitions.texts).not.toContain("Cargo Drop");
|
||||
expect(definitions.texts).toEqual(expect.arrayContaining(["礼包", "领取规则", "礼包内容", "次数 / 门槛", "状态", "最近更新", "操作"]));
|
||||
expect(definitions.buttons.find((button) => button.label === "新建礼包")?.disabled).toBe(false);
|
||||
expect(definitions.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["查看配置", "编辑礼包"]));
|
||||
expect(definitions.buttons.find((button) => button.label === "保存礼包")).toBeUndefined();
|
||||
expect(definitions.texts).not.toContain("先定义规则,再配置内容");
|
||||
expect(definitions.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["搜索礼包", "礼包状态"]));
|
||||
@@ -287,6 +288,9 @@ describe("SCUM plugin feature module", () => {
|
||||
expect(deliveries.texts).toContain("发放记录");
|
||||
expect(deliveries.texts).toContain("delivered");
|
||||
expect(deliveries.buttons.find((button) => button.label === "立即发放")?.disabled).toBe(false);
|
||||
const details = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理", giftDialog: { mode: "view", gift: surfaceData.gifts[0] } });
|
||||
expect(details.texts).toEqual(expect.arrayContaining(["礼包配置详情", "礼包编号", "starter-pack", "领取周期", "一次", "礼包内容", "BP_Cash_01", "#announce Starter pack"]));
|
||||
expect(details.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["关闭", "编辑礼包"]));
|
||||
});
|
||||
|
||||
it("renders map layers, filter controls, points, and selected-point details", () => {
|
||||
@@ -504,7 +508,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; pageQuery?: Record<string, string> } = {}) {
|
||||
function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: string[]; pageKey?: string; pageTitle?: string; giftTab?: "definitions" | "claims" | "deliveries" | "timed"; giftDialog?: { mode: "closed" | "view" | "edit"; gift?: RecordMap }; playerSearch?: string; pageQuery?: Record<string, string> } = {}) {
|
||||
const nodes: string[] = [];
|
||||
const texts: string[] = [];
|
||||
const buttons: Array<{ label: string; disabled: boolean; onClick?: () => void }> = [];
|
||||
@@ -528,6 +532,7 @@ function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: strin
|
||||
stateCall += 1;
|
||||
if (stateCall === 1) return [{ status: "ready", data: options.data ?? surfaceData } as T, () => undefined];
|
||||
if (stateCall === 3 && options.playerSearch !== undefined) return [options.playerSearch as T, () => undefined];
|
||||
if (options.giftDialog && typeof initial === "object" && initial !== null && "mode" in initial && (initial as { mode?: string }).mode === "closed") return [options.giftDialog as T, () => undefined];
|
||||
const value = typeof initial === "function" ? (initial as () => T)() : initial;
|
||||
if (options.giftTab && value === "definitions") return [options.giftTab as T, () => undefined];
|
||||
return [value, () => undefined];
|
||||
|
||||
Reference in New Issue
Block a user