fix: redesign SCUM gift item lists
This commit is contained in:
@@ -5,7 +5,6 @@ import {
|
||||
emptySCUMSurfaceData,
|
||||
loadSCUMSurface,
|
||||
parseGiftItems,
|
||||
parseGiftCommands,
|
||||
queueGiftDelivery,
|
||||
resetGiftClaim,
|
||||
resetPendingGift,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
type SCUMWorkspaceActions
|
||||
} from "./page-data.js";
|
||||
import { scumCatalogEntries, scumCatalogEntryFor, scumCatalogIconUrl, scumVehicleEntry, type ScumCatalogEntry } from "./scum-catalog.js";
|
||||
import { scumCatalogDisplayName, scumCatalogSearchText } from "./scum-catalog-zh.js";
|
||||
import { scumMapVehicleIconUrl } from "./map-vehicle-icons.js";
|
||||
import type { MapPlaybackFrame, MapPlaybackState, MapSceneData, MapTimeWindow, MapTrack, MapTrackPoint, MapTrackSegment, MapViewState } from "./contracts.js";
|
||||
|
||||
@@ -31,6 +31,8 @@ type MapLayer = "players" | "vehicles" | "flags" | "regions" | "other";
|
||||
type MapVehicleFilter = "all" | "land" | "water" | "air" | "other";
|
||||
type ScumMapLayer = "terrain" | "topo" | "night";
|
||||
type CatalogPickTarget = "gift-items" | "produce-item";
|
||||
type GiftItemDraft = { catalogCode: string; quantity: string; probability: string; durability: string };
|
||||
type GiftItemDraftField = keyof GiftItemDraft;
|
||||
type PlayerPanelKind = "closed" | "gifts" | "items" | "history";
|
||||
type PlayerPanelState = { kind: PlayerPanelKind; playerId: string };
|
||||
type PointerEventLike = { pointerId?: number; clientX?: number; clientY?: number; button?: number; currentTarget?: { setPointerCapture?: (pointerId?: number) => void; releasePointerCapture?: (pointerId?: number) => void }; preventDefault?: () => void; stopPropagation?: () => void };
|
||||
@@ -39,7 +41,7 @@ const scumMapBaseLayers: ReadonlyArray<readonly [ScumMapLayer, string]> = [["ter
|
||||
const scumMapVehicleFilters: ReadonlyArray<readonly [MapVehicleFilter, string]> = [["all", "全部载具"], ["land", "陆地载具"], ["water", "水上载具"], ["air", "飞行类载具"], ["other", "其他载具"]];
|
||||
const scumFlightVehicleKeys = new Set(["kinglet_duster", "kinglet_mariner", "kinglet_scout"]);
|
||||
const scumCatalogKinds: ReadonlyArray<readonly [string, string]> = [["all", "全部"], ["item", "物品"], ["vehicle", "载具"], ["zombie", "丧尸"], ["animal", "动物"], ["armed-npc", "NPC"]];
|
||||
const scumGiftCatalogCategories: ReadonlyArray<readonly [string, string]> = [["all", "全部目录"], ["weapons", "武器"], ["fishing", "钓鱼"], ["gear", "装备"], ["food", "食物"], ["medical", "医疗"], ["components", "组件"], ["blueprints", "蓝图"], ["tools", "工具"], ["farming", "农业"], ["misc", "其他"], ["vehicle", "载具"], ["animal", "动物"], ["zombie", "丧尸"], ["armed-npc", "NPC"]];
|
||||
const scumGiftCatalogCategories: ReadonlyArray<readonly [string, string]> = [["all", "全部目录"], ["hunting", "狩猎"], ["food", "食物"], ["metabolism", "代谢"], ["medical", "医疗"], ["weapons", "武器"], ["crafting", "制作"], ["gear", "装备"], ["fishing", "钓鱼"], ["building", "建筑"], ["tools", "工具"], ["farming", "农业"], ["misc", "其他"], ["vehicle", "载具"], ["animal", "动物"], ["zombie", "丧尸"], ["armed-npc", "NPC"]];
|
||||
const scumCatalogPickLimit = 48;
|
||||
const scumMapSize = 4096;
|
||||
const scumSurfaceRefreshMs = 15000;
|
||||
@@ -122,8 +124,7 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
const [giftRecordKey, setGiftRecordKey] = usePluginState(react, "");
|
||||
const [giftCode, setGiftCode] = usePluginState(react, "");
|
||||
const [giftName, setGiftName] = usePluginState(react, "");
|
||||
const [giftItems, setGiftItems] = usePluginState(react, "");
|
||||
const [giftCommands, setGiftCommands] = usePluginState(react, "");
|
||||
const [giftItems, setGiftItems] = usePluginState<GiftItemDraft[]>(react, []);
|
||||
const [giftClass, setGiftClass] = usePluginState(react, "5");
|
||||
const [giftAudience, setGiftAudience] = usePluginState(react, "all");
|
||||
const [giftNumber, setGiftNumber] = usePluginState(react, "1");
|
||||
@@ -260,7 +261,7 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
activityStatus, setActivityStatus, eventId, setEventId, eventName, setEventName, eventType, setEventType, eventSchedule, setEventSchedule,
|
||||
eventCommand, setEventCommand, eventClass, setEventClass, eventPlacard, setEventPlacard, eventPercent, setEventPercent, eventDuration, setEventDuration, eventNpc, setEventNpc, eventItem, setEventItem, eventZombie, setEventZombie, eventAnimal, setEventAnimal,
|
||||
produceEventId, setProduceEventId, produceId, setProduceId, produceTradeGoodsId, setProduceTradeGoodsId, producePercent, setProducePercent, produceValue, setProduceValue, produceRadius, setProduceRadius, produceX, setProduceX, produceY, setProduceY, produceZ, setProduceZ,
|
||||
giftTab, setGiftTab, giftSearch, setGiftSearch, giftStatus, setGiftStatus, giftRecordKey, setGiftRecordKey, giftCode, setGiftCode, giftName, setGiftName, giftItems, setGiftItems, giftCommands, setGiftCommands, giftClass, setGiftClass, giftAudience, setGiftAudience, giftNumber, setGiftNumber, giftAchievement, setGiftAchievement, giftAchievementNumber, setGiftAchievementNumber,
|
||||
giftTab, setGiftTab, giftSearch, setGiftSearch, giftStatus, setGiftStatus, giftRecordKey, setGiftRecordKey, giftCode, setGiftCode, giftName, setGiftName, giftItems, setGiftItems, giftClass, setGiftClass, giftAudience, setGiftAudience, giftNumber, setGiftNumber, giftAchievement, setGiftAchievement, giftAchievementNumber, setGiftAchievementNumber,
|
||||
deliveryGift, setDeliveryGift, deliveryPlayer, setDeliveryPlayer, mapLayers, setMapLayers, mapVehicleFilter, setMapVehicleFilter, selectedMapPoint, setSelectedMapPoint,
|
||||
hoverMapPoint, setHoverMapPoint,
|
||||
mapView, setMapView, mapViewportRef, mapPlaybackFrameRef, mapFollowRef, mapTimePreset, setMapTimePreset, mapTimeFrom, setMapTimeFrom, mapTimeTo, setMapTimeTo,
|
||||
@@ -310,7 +311,7 @@ type ViewState = {
|
||||
eventNpc: string; setEventNpc: StateSetter<string>; eventItem: string; setEventItem: StateSetter<string>; eventZombie: string; setEventZombie: StateSetter<string>; eventAnimal: string; setEventAnimal: StateSetter<string>;
|
||||
produceEventId: string; setProduceEventId: StateSetter<string>; produceId: string; setProduceId: StateSetter<string>; produceTradeGoodsId: string; setProduceTradeGoodsId: StateSetter<string>;
|
||||
producePercent: string; setProducePercent: StateSetter<string>; produceValue: string; setProduceValue: StateSetter<string>; produceRadius: string; setProduceRadius: StateSetter<string>; produceX: string; setProduceX: StateSetter<string>; produceY: string; setProduceY: StateSetter<string>; produceZ: string; setProduceZ: StateSetter<string>;
|
||||
giftCode: string; setGiftCode: StateSetter<string>; giftName: string; setGiftName: StateSetter<string>; giftItems: string; setGiftItems: StateSetter<string>; giftCommands: string; setGiftCommands: StateSetter<string>;
|
||||
giftCode: string; setGiftCode: StateSetter<string>; giftName: string; setGiftName: StateSetter<string>; giftItems: GiftItemDraft[]; setGiftItems: StateSetter<GiftItemDraft[]>;
|
||||
giftClass: string; setGiftClass: StateSetter<string>; giftAudience: string; setGiftAudience: StateSetter<string>; giftNumber: string; setGiftNumber: StateSetter<string>; giftAchievement: string; setGiftAchievement: StateSetter<string>; giftAchievementNumber: string; setGiftAchievementNumber: StateSetter<string>;
|
||||
deliveryGift: string; setDeliveryGift: StateSetter<string>; deliveryPlayer: string; setDeliveryPlayer: StateSetter<string>;
|
||||
mapLayers: Record<MapLayer, boolean>; setMapLayers: StateSetter<Record<MapLayer, boolean>>; mapVehicleFilter: MapVehicleFilter; setMapVehicleFilter: StateSetter<MapVehicleFilter>;
|
||||
@@ -603,10 +604,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.setGiftDialog({ mode: "edit" });
|
||||
view.setGiftRecordKey(""); view.setGiftCode(""); view.setGiftName(""); view.setGiftItems([]); view.setGiftClass("5"); view.setGiftAudience("all"); view.setGiftNumber("1"); view.setGiftAchievement("0"); view.setGiftAchievementNumber("0"); view.setGiftDialog({ mode: "edit" });
|
||||
};
|
||||
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 });
|
||||
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(giftItemDrafts(gift)); view.setGiftDialog({ mode, gift });
|
||||
};
|
||||
const saveGift = () => runAction(view.setAction, "正在保存礼包定义…", async () => {
|
||||
const code = view.giftCode.trim();
|
||||
@@ -614,14 +615,13 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
if (!code || !name) throw new Error("礼包编号和名称不能为空。");
|
||||
if (!/^[A-Za-z0-9_.-]{1,128}$/.test(code)) throw new Error("礼包编号只能使用字母、数字、点、下划线或短横线。");
|
||||
const items = parseGiftItems(view.giftItems);
|
||||
const commands = parseGiftCommands(view.giftCommands);
|
||||
if (!items.length && !commands.length) throw new Error("礼包至少需要一项物品或命令。");
|
||||
if (!items.length) throw new Error("礼包至少要包含一项物品。");
|
||||
const number = minimumIntegerInput(view.giftNumber, 1, "发放次数");
|
||||
const achievement = minimumIntegerInput(view.giftAchievement, 0, "成就类型");
|
||||
const achievementNumber = minimumIntegerInput(view.giftAchievementNumber, 0, "成就值");
|
||||
await saveGiftDefinition(actions ?? {}, {
|
||||
code, name, class: integerInput(view.giftClass, 5), audience: view.giftAudience, number,
|
||||
achievement, achievementNumber, items, commands,
|
||||
achievement, achievementNumber, items,
|
||||
status: "active", updatedAt: new Date().toISOString()
|
||||
});
|
||||
if (view.giftRecordKey && view.giftRecordKey !== code) await deleteGiftDefinition(actions ?? {}, view.giftRecordKey);
|
||||
@@ -638,7 +638,7 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
if (!gift || !player) throw new Error("请选择礼包和用户。");
|
||||
await queueGiftDelivery(actions ?? {}, gift, player);
|
||||
view.refresh();
|
||||
return "礼包发放命令已进入执行队列。";
|
||||
return "礼包发放已进入执行队列。";
|
||||
});
|
||||
const search = view.giftSearch.trim().toLowerCase();
|
||||
const gifts = data.gifts.filter((gift) => {
|
||||
@@ -666,7 +666,7 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
const status = textField(gift, "status").toLowerCase() || "active";
|
||||
return e("tr", { key: idOf(gift, `gift-${index}`) },
|
||||
e("td", null, e("strong", null, textField(gift, "name") || key), e("span", { className: "provider-id scum-gift-code" }, key)),
|
||||
e("td", null, e("div", { className: "scum-gift-content-cell" }, catalogIconStrip(e, giftCatalogIcons(gift)), e("span", null, giftItemsSummary(gift))), e("span", { className: "provider-id" }, giftCommandsSummary(gift))),
|
||||
e("td", null, e("div", { className: "scum-gift-content-cell" }, catalogIconStrip(e, giftCatalogIcons(gift)), e("span", null, giftItemsSummary(gift)))),
|
||||
e("td", null, e("span", { className: "gift-rule-primary" }, `${giftClassLabel(numField(gift, "class"))} · ${giftAudienceLabel(textField(gift, "audience"))}`), e("span", { className: "provider-id" }, `发放 ${numField(gift, "number")} 次 · 成就 ${numField(gift, "achievement")} / ${numField(gift, "achievementNumber", "achievement_number")}`)),
|
||||
e("td", null, e("div", { className: "gift-status-cell" }, e("span", { className: `status-pill ${activeStatus(status) ? "status-active" : "status-disabled"}` }, giftStatusLabel(status)), e("span", { className: "provider-id" }, dateField(gift, "updatedAt", "createdAt")))),
|
||||
e("td", { className: "provider-actions-cell" }, e("div", { className: "scum-gift-table-actions" },
|
||||
@@ -706,9 +706,33 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
);
|
||||
}
|
||||
|
||||
function giftItemEditorList(e: ReactLike["createElement"], view: ViewState) {
|
||||
if (!view.giftItems.length) return e("p", { className: "page-status" }, "尚未添加物品,请使用目录选择器加入礼包内容。");
|
||||
return e("div", { className: "gift-item-editor-list" }, view.giftItems.map((item, index) => {
|
||||
const entry = scumCatalogEntryFor(item.catalogCode);
|
||||
const supportsDurability = giftItemSupportsDurability(entry);
|
||||
const itemLabel = entry ? scumCatalogDisplayName(entry) : item.catalogCode || "未命名物品";
|
||||
const category = entry ? scumCatalogCategoryLabel(entry) : "物品";
|
||||
const itemNumber = index + 1;
|
||||
return e("div", { key: `${item.catalogCode}-${index}`, className: "gift-item-editor-row" },
|
||||
catalogIconStrip(e, catalogIconsForValue(item.catalogCode)),
|
||||
e("div", { className: "gift-item-editor-info" }, e("strong", null, itemLabel), e("span", { className: "provider-id" }, `${category} · ${item.catalogCode || "未配置代码"}`)),
|
||||
e("div", { className: "gift-item-editor-fields" },
|
||||
labeledField(e, "数量", e("input", { type: "number", min: "1", step: "1", value: item.quantity, "aria-label": `礼包物品 ${itemNumber} 数量`, onChange: (event: InputEvent) => updateGiftItemDraft(view, index, "quantity", inputValue(event)) })),
|
||||
labeledField(e, "概率", e("input", { type: "number", min: "0", max: "100", step: "1", value: item.probability, "aria-label": `礼包物品 ${itemNumber} 概率`, onChange: (event: InputEvent) => updateGiftItemDraft(view, index, "probability", inputValue(event)) })),
|
||||
labeledField(e, "耐久", e("input", { type: "number", min: "0", max: "100", step: "1", value: item.durability, disabled: !supportsDurability, placeholder: supportsDurability ? "默认" : "不适用", "aria-label": `礼包物品 ${itemNumber} 耐久`, onChange: (event: InputEvent) => updateGiftItemDraft(view, index, "durability", inputValue(event)) })),
|
||||
e("button", { type: "button", className: "gift-item-editor-remove icon-command", "aria-label": `删除礼包物品 ${itemNumber}`, onClick: () => view.setGiftItems((items) => items.filter((_, itemIndex) => itemIndex !== index)) }, "删除")
|
||||
)
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
function updateGiftItemDraft(view: ViewState, index: number, key: GiftItemDraftField, value: string): void {
|
||||
view.setGiftItems((items) => items.map((item, itemIndex) => itemIndex === index ? { ...item, [key]: value } : item));
|
||||
}
|
||||
|
||||
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?.() },
|
||||
@@ -723,13 +747,12 @@ function giftDetailsDialog(e: ReactLike["createElement"], gift: RecordMap, view:
|
||||
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} 条命令`)),
|
||||
e("div", { className: "gift-form-section-title" }, e("strong", null, "礼包内容"), e("span", null, `${items.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")}`));
|
||||
return e("div", { key: `${code}-${index}`, className: "gift-config-row" }, catalogIconStrip(e, catalogIconsForValue(code)), e("div", { className: "gift-config-item-info" }, e("strong", null, entry ? scumCatalogDisplayName(entry) : textField(item, "label") || code || "未命名物品"), e("span", { className: "provider-id" }, `${entry ? scumCatalogCategoryLabel(entry) : "物品"} · ${code || "unknown"}`)), e("span", null, `数量 × ${numField(item, "quantity")}`), e("span", null, `概率 ${giftItemProbability(item)}%`), e("span", null, `耐久 ${giftItemDurability(item, entry)}`));
|
||||
})) : 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 }, "编辑礼包"))
|
||||
)
|
||||
@@ -743,7 +766,7 @@ function giftEditorDialog(e: ReactLike["createElement"], view: ViewState, action
|
||||
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.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, "礼包编号用于数据主键,只支持字母、数字、点、下划线和短横线;礼包至少要包含一项物品或一条命令。")),
|
||||
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)) })),
|
||||
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)))),
|
||||
@@ -751,8 +774,7 @@ function giftEditorDialog(e: ReactLike["createElement"], view: ViewState, action
|
||||
labeledField(e, "发放次数", e("input", { value: view.giftNumber, "aria-label": "发放次数", type: "number", min: "1", placeholder: "1", onChange: (event: InputEvent) => view.setGiftNumber(inputValue(event)) })),
|
||||
labeledField(e, "成就类型", e("input", { value: view.giftAchievement, "aria-label": "成就类型", type: "number", min: "0", placeholder: "0", onChange: (event: InputEvent) => view.setGiftAchievement(inputValue(event)) })),
|
||||
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, "按 SCUM 目录分类筛选(物品、载具、动物、丧尸、NPC);格式为 目录代码:数量")), labeledField(e, "物品清单", e("input", { value: view.giftItems, "aria-label": "礼包物品", placeholder: "例如 Aloe_Vera:2, BPC_Laika:1, BP_Bear2:2", 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: "gift-form-wide gift-form-section" }, e("div", { className: "gift-form-section-title" }, e("strong", null, "礼包物品"), e("span", null, "可用目录选择器快速加入;每项配置数量、概率和耐久")), giftItemEditorList(e, view), catalogPicker(e, view, "gift-items")),
|
||||
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 }, "保存礼包"))
|
||||
)
|
||||
)
|
||||
@@ -1366,16 +1388,14 @@ function catalogMatches(query: string, kind: string, target: CatalogPickTarget):
|
||||
const search = query.trim().toLowerCase();
|
||||
return scumCatalogEntries.filter((entry) => {
|
||||
const matchesKind = target === "gift-items" ? kind === "all" || scumGiftCatalogFilterKey(entry) === kind : kind === "all" || entry.kind === kind;
|
||||
return matchesKind && (!search || entry.name.toLowerCase().includes(search) || entry.code.toLowerCase().includes(search) || entry.command.toLowerCase().includes(search));
|
||||
return matchesKind && (!search || scumCatalogSearchText(entry).includes(search));
|
||||
}).slice(0, scumCatalogPickLimit);
|
||||
}
|
||||
|
||||
function appendCatalogItem(current: string, code: string): string {
|
||||
const parts = current.split(",").map((part) => part.trim()).filter(Boolean);
|
||||
const index = parts.findIndex((part) => part.split(":")[0]?.trim().toLowerCase() === code.toLowerCase());
|
||||
if (index >= 0) { const [key, quantity] = parts[index].split(":"); parts[index] = `${key.trim()}:${Math.max(1, Number(quantity) || 1) + 1}`; }
|
||||
else parts.push(`${code}:1`);
|
||||
return parts.join(", ");
|
||||
function appendCatalogItem(items: GiftItemDraft[], code: string): GiftItemDraft[] {
|
||||
const index = items.findIndex((item) => item.catalogCode.toLowerCase() === code.toLowerCase());
|
||||
if (index >= 0) return items.map((item, itemIndex) => itemIndex === index ? { ...item, quantity: String(Math.max(1, Number(item.quantity) || 1) + 1) } : item);
|
||||
return [...items, { catalogCode: code, quantity: "1", probability: "100", durability: "" }];
|
||||
}
|
||||
|
||||
function catalogIconsForValue(value: string): string[] {
|
||||
@@ -1402,20 +1422,23 @@ function catalogPicker(e: ReactLike["createElement"], view: ViewState, target: C
|
||||
view.catalogOpen ? e("div", { className: "scum-catalog-picker-body" },
|
||||
e("input", { type: "search", value: view.catalogSearch, "aria-label": target === "gift-items" ? "搜索礼包目录" : "搜索生成物品", placeholder: "名称 / 代码,例如 Laika", onChange: (event: InputEvent) => view.setCatalogSearch(inputValue(event)) }),
|
||||
e("div", { className: "scum-catalog-kinds", role: "group", "aria-label": target === "gift-items" ? "礼包目录分类" : "目录类型" }, filters.map(([key, label]) => e("button", { key, type: "button", className: activeFilter === key ? "icon-command is-active" : "icon-command", "aria-pressed": activeFilter === key, onClick: () => view.setCatalogKind(key) }, label))),
|
||||
e("div", { className: "scum-catalog-grid" }, matches.length ? matches.map((entry) => { const icon = scumCatalogIconUrl(entry.icon); return e("button", { key: entry.code, type: "button", className: "scum-catalog-chip", title: `${entry.name} · ${entry.command}`, onClick: () => target === "gift-items" ? view.setGiftItems(appendCatalogItem(view.giftItems, entry.code)) : view.setProduceTradeGoodsId(entry.code) }, icon ? e("img", { className: "scum-catalog-icon", src: icon, alt: "" }) : e("span", { className: "scum-catalog-icon scum-catalog-icon-empty" }), e("span", { className: "scum-catalog-meta" }, e("strong", null, entry.name), e("span", { className: "provider-id" }, entry.code))); }) : e("p", { className: "page-status" }, "没有匹配的目录条目。"))
|
||||
e("div", { className: "scum-catalog-grid" }, matches.length ? matches.map((entry) => { const icon = scumCatalogIconUrl(entry.icon); const displayName = scumCatalogDisplayName(entry); return e("button", { key: entry.code, type: "button", className: "scum-catalog-chip", title: `${displayName} · ${scumCatalogCategoryLabel(entry)} · ${entry.command}`, onClick: () => target === "gift-items" ? view.setGiftItems((items) => appendCatalogItem(items, entry.code)) : view.setProduceTradeGoodsId(entry.code) }, icon ? e("img", { className: "scum-catalog-icon", src: icon, alt: "" }) : e("span", { className: "scum-catalog-icon scum-catalog-icon-empty" }), e("span", { className: "scum-catalog-meta" }, e("strong", null, displayName), e("span", { className: "provider-id" }, `${scumCatalogCategoryLabel(entry)} · ${entry.code}`))); }) : e("p", { className: "page-status" }, "没有匹配的目录条目。"))
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
||||
export function scumGiftCatalogCategory(entry: ScumCatalogEntry): string {
|
||||
const icon = entry.icon.toLowerCase();
|
||||
if (icon.includes("/weapons/") || icon.includes("/crafting/traps/")) return "weapons";
|
||||
const name = `${entry.name} ${entry.code}`.toLowerCase();
|
||||
if (icon.includes("/crafting/traps/") || icon.includes("/weapons/explosives/traps/") || /\b(hunting|bait|trap)\b/.test(name)) return "hunting";
|
||||
if (icon.includes("/fishing/")) return "fishing";
|
||||
if (icon.includes("/food/drinks/")) return "metabolism";
|
||||
if (icon.includes("/medicals/")) return "medical";
|
||||
if (icon.includes("/weapons/")) return "weapons";
|
||||
if (icon.includes("/clothes/")) return "gear";
|
||||
if (icon.includes("/food/")) return "food";
|
||||
if (icon.includes("/medicals/")) return "medical";
|
||||
if (icon.includes("/crafting/basebuilding/")) return "blueprints";
|
||||
if (icon.includes("/crafting/components/") || icon.includes("/misc/components/")) return "components";
|
||||
if (icon.includes("/crafting/basebuilding/") || icon.includes("/crafting/fortifications/")) return "building";
|
||||
if (icon.includes("/crafting/components/") || icon.includes("/misc/components/") || icon.includes("/crafting/recipes/")) return "crafting";
|
||||
if (icon.includes("/crafting/tools/") || icon.includes("/misc/tools/")) return "tools";
|
||||
if (icon.includes("/farming/")) return "farming";
|
||||
return "misc";
|
||||
@@ -1423,7 +1446,7 @@ export function scumGiftCatalogCategory(entry: ScumCatalogEntry): string {
|
||||
|
||||
export function scumGiftCatalogFilterKey(entry: ScumCatalogEntry): string { return entry.kind === "item" ? scumGiftCatalogCategory(entry) : entry.kind; }
|
||||
|
||||
export function scumCatalogCategoryLabel(entry: ScumCatalogEntry): string { if (entry.kind === "vehicle") return "载具"; if (entry.kind === "animal") return "动物"; if (entry.kind === "zombie") return "丧尸"; if (entry.kind === "armed-npc") return "NPC"; return ({ weapons: "武器", fishing: "钓鱼", gear: "装备", food: "食物", medical: "医疗", components: "组件", blueprints: "蓝图", tools: "工具", farming: "农业", misc: "其他" } as Record<string, string>)[scumGiftCatalogCategory(entry)] ?? "其他"; }
|
||||
export function scumCatalogCategoryLabel(entry: ScumCatalogEntry): string { if (entry.kind === "vehicle") return "载具"; if (entry.kind === "animal") return "动物"; if (entry.kind === "zombie") return "丧尸"; if (entry.kind === "armed-npc") return "NPC"; return ({ hunting: "狩猎", food: "食物", metabolism: "代谢", medical: "医疗", weapons: "武器", crafting: "制作", gear: "装备", fishing: "钓鱼", building: "建筑", tools: "工具", farming: "农业", misc: "其他" } as Record<string, string>)[scumGiftCatalogCategory(entry)] ?? "其他"; }
|
||||
function labeledField(e: ReactLike["createElement"], label: string, control: unknown) { return e("label", { className: "console-field" }, e("span", null, label), control); }
|
||||
function detailOpen(event: InputEvent): boolean { return Boolean(event.target?.open); }
|
||||
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); }
|
||||
@@ -1563,12 +1586,13 @@ function dateField(row: RecordMap | undefined, ...keys: string[]): string { cons
|
||||
function matchesText(row: RecordMap, search: string, ...keys: string[]): boolean { return !search || keys.some((key) => textField(row, key).toLowerCase().includes(search)); }
|
||||
function unique(values: string[]): string[] { return [...new Set(values)]; }
|
||||
function activeStatus(status: string): boolean { return ["active", "running", "scheduled", "enabled", "queued"].includes(status.toLowerCase()); }
|
||||
function giftItemsSummary(gift: RecordMap): string { const items = field(gift, "items"); if (!Array.isArray(items) || !items.length) return "未配置物品"; return items.map((item) => { if (!isRecord(item)) return String(item); const code = textField(item, "catalogCode", "catalogItemKey", "key", "className"); const entry = scumCatalogEntryFor(code); const label = entry?.name || textField(item, "label") || code || "未命名物品"; return `${label} × ${numField(item, "quantity")}`; }).join(" · "); }
|
||||
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 giftItemsSummary(gift: RecordMap): string { const items = giftItemRecords(gift); if (!items.length) return "未配置物品"; return items.map((item) => { const code = textField(item, "catalogCode", "catalogItemKey", "key", "className"); const entry = scumCatalogEntryFor(code); const label = entry ? scumCatalogDisplayName(entry) : textField(item, "label") || code || "未命名物品"; return `${label} × ${numField(item, "quantity")} · 概率 ${giftItemProbability(item)}%`; }).join(" · "); }
|
||||
function giftItemDrafts(gift: RecordMap): GiftItemDraft[] { return giftItemRecords(gift).map((item) => ({ catalogCode: textField(item, "catalogCode", "catalogItemKey", "key", "className"), quantity: String(field(item, "quantity") ?? "1"), probability: giftItemProbability(item), durability: giftItemRawDurability(item) })); }
|
||||
function giftItemRecords(gift: RecordMap): RecordMap[] { try { return parseGiftItems(field(gift, "items")).map((item) => ({ ...item })); } catch { return []; } }
|
||||
function giftItemProbability(item: RecordMap): string { const value = Number(field(item, "probability")); return Number.isFinite(value) && value >= 0 && value <= 100 ? String(value) : "100"; }
|
||||
function giftItemRawDurability(item: RecordMap): string { const value = field(item, "durability"); return value === undefined || value === null ? "" : String(value); }
|
||||
function giftItemDurability(item: RecordMap, entry: ScumCatalogEntry | undefined): string { if (!giftItemSupportsDurability(entry)) return "不适用"; const value = giftItemRawDurability(item); return value ? `${value}%` : "默认"; }
|
||||
function giftItemSupportsDurability(entry: ScumCatalogEntry | undefined): boolean { return !entry || entry.kind === "item"; }
|
||||
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) || "全部玩家"; }
|
||||
|
||||
Reference in New Issue
Block a user