diff --git a/src/features/room-rocket/api.ts b/src/features/room-rocket/api.ts index b3e973c..99e58ab 100644 --- a/src/features/room-rocket/api.ts +++ b/src/features/room-rocket/api.ts @@ -5,6 +5,7 @@ export interface RoomRocketRewardItemDto { rewardItemId: string; resourceGroupId: number; weight: number; + quantity: number; displayName?: string; iconUrl?: string; } @@ -61,10 +62,7 @@ export type RoomRocketConfigPayload = Pick< >; type RawReward = RoomRocketRewardItemDto & Record; -type RawLevel = Omit< - RoomRocketLevelDto, - "igniterRewards" | "inRoomRewards" | "top1Rewards" -> & { +type RawLevel = Omit & { igniterRewards?: RawReward[]; igniter_rewards?: RawReward[]; inRoomRewards?: RawReward[]; @@ -166,10 +164,18 @@ function normalizeRewards(rewards?: RawReward[]): RoomRocketRewardItemDto[] { rewardItemId: stringValue(reward.rewardItemId ?? reward.reward_item_id) || `reward_${index + 1}`, resourceGroupId: numberValue(reward.resourceGroupId ?? reward.resource_group_id), weight: numberValue(reward.weight), + quantity: positiveNumberValue(reward.quantity) || 1, displayName: stringValue(reward.displayName ?? reward.display_name), iconUrl: stringValue(reward.iconUrl ?? reward.icon_url), })) - .filter((reward) => reward.resourceGroupId > 0 || reward.weight > 0 || reward.displayName || reward.iconUrl); + .filter( + (reward) => + reward.resourceGroupId > 0 || + reward.weight > 0 || + reward.quantity > 0 || + reward.displayName || + reward.iconUrl, + ); } function normalizeGiftFuelRules(rules?: RawGiftFuelRule[]): GiftFuelRuleDto[] { @@ -182,7 +188,9 @@ function normalizeGiftFuelRules(rules?: RawGiftFuelRule[]): GiftFuelRuleDto[] { fixedFuel: numberValue(rule.fixedFuel), excluded: Boolean(rule.excluded), })) - .filter((rule) => rule.giftId || rule.giftTypeCode || rule.multiplierPpm > 0 || rule.fixedFuel > 0 || rule.excluded); + .filter( + (rule) => rule.giftId || rule.giftTypeCode || rule.multiplierPpm > 0 || rule.fixedFuel > 0 || rule.excluded, + ); } function stringValue(value: unknown) { @@ -193,3 +201,8 @@ function numberValue(value: unknown) { const number = Number(value || 0); return Number.isFinite(number) ? number : 0; } + +function positiveNumberValue(value: unknown) { + const number = numberValue(value); + return number > 0 ? number : 0; +} diff --git a/src/features/room-rocket/components/RoomRocketConfigEditor.jsx b/src/features/room-rocket/components/RoomRocketConfigEditor.jsx index e9cac33..5c81f94 100644 --- a/src/features/room-rocket/components/RoomRocketConfigEditor.jsx +++ b/src/features/room-rocket/components/RoomRocketConfigEditor.jsx @@ -293,21 +293,27 @@ function LevelNavItem({ active, level, onClick }) { } function RewardTable({ disabled, levelIndex, poolKey, resourceGroups, rewards, setForm }) { + const quantityEnabled = poolKey === "inRoomRewards"; + if (!rewards.length) { return
未配置奖项
; } return (
-
+
资源组 权重 + {quantityEnabled ? 数量 : null} 展示名 图标
{rewards.map((reward, rewardIndex) => ( -
+
+ {quantityEnabled ? ( + + updateReward(setForm, levelIndex, poolKey, rewardIndex, { + quantity: event.target.value, + }) + } + /> + ) : null} ({ displayName: reward.displayName || "", iconUrl: reward.iconUrl || "", + quantity: String(reward.quantity || 1), resourceGroupId: reward.resourceGroupId ? String(reward.resourceGroupId) : "", rewardItemId: reward.rewardItemId || `reward_${index + 1}`, weight: String(reward.weight || 100), @@ -188,9 +189,13 @@ function rewardsFromForm(rewards = [], level, role) { .map((reward, index) => ({ displayName: String(reward.displayName || "").trim(), iconUrl: String(reward.iconUrl || "").trim(), + quantity: Number(reward.quantity || 1), resourceGroupId: Number(reward.resourceGroupId || 0), rewardItemId: String(reward.rewardItemId || "").trim() || `level_${level}_${role}_${index + 1}`, weight: Number(reward.weight || 0), })) - .filter((reward) => reward.resourceGroupId || reward.weight || reward.displayName || reward.iconUrl); + .filter( + (reward) => + reward.resourceGroupId || reward.weight || reward.quantity || reward.displayName || reward.iconUrl, + ); } diff --git a/src/features/room-rocket/pages/RoomRocketPage.jsx b/src/features/room-rocket/pages/RoomRocketPage.jsx index 04e024d..0cb725b 100644 --- a/src/features/room-rocket/pages/RoomRocketPage.jsx +++ b/src/features/room-rocket/pages/RoomRocketPage.jsx @@ -30,14 +30,16 @@ const columns = [ label: "在房奖励", width: "minmax(220px, 1fr)", render: (level, _index, context) => ( - + ), }, { key: "top1Rewards", label: "贡献第一", width: "minmax(220px, 1fr)", - render: (level, _index, context) => , + render: (level, _index, context) => ( + + ), }, { key: "igniterRewards", @@ -67,7 +69,11 @@ export function RoomRocketPage() { if (editing) { return ( - + - + {items.map(([label, url]) => ( - + {label} @@ -126,18 +139,33 @@ function MaterialState({ level }) { ); } -function RewardSummary({ groups, rewards = [] }) { +function RewardSummary({ groups, rewards = [], showQuantity = false }) { if (!rewards.length) { return 未配置; } return (
- {rewards.length} 个奖项 - {rewards.slice(0, 2).map((reward) => resourceGroupName(groups, reward.resourceGroupId)).join("、")} + + {rewards.length} 个奖项{showQuantity ? ` · ${rewardQuantityTotal(rewards)} 份` : ""} + + + {rewards + .slice(0, 2) + .map((reward) => + showQuantity + ? `${resourceGroupName(groups, reward.resourceGroupId)} *${Number(reward.quantity || 1)}` + : resourceGroupName(groups, reward.resourceGroupId), + ) + .join("、")} +
); } +function rewardQuantityTotal(rewards) { + return rewards.reduce((total, reward) => total + Number(reward.quantity || 1), 0); +} + function resourceGroupName(groups, id) { const group = groups.find((item) => Number(item.groupId) === Number(id)); return group?.name || group?.groupCode || (id ? `#${id}` : "-"); diff --git a/src/features/room-rocket/room-rocket.module.css b/src/features/room-rocket/room-rocket.module.css index 1a57b13..f9b0b4f 100644 --- a/src/features/room-rocket/room-rocket.module.css +++ b/src/features/room-rocket/room-rocket.module.css @@ -288,6 +288,10 @@ align-items: start; } +.rewardTableWithQuantity { + grid-template-columns: minmax(200px, 1.2fr) 88px 88px minmax(150px, 1fr) minmax(170px, 1fr) var(--control-height); +} + .rewardTableHead, .fuelRuleTableHead { align-items: center; diff --git a/src/features/room-rocket/schema.ts b/src/features/room-rocket/schema.ts index be4cc08..3444ecb 100644 --- a/src/features/room-rocket/schema.ts +++ b/src/features/room-rocket/schema.ts @@ -5,6 +5,7 @@ const numberInput = z.union([z.string(), z.number()]); const rewardSchema = z.object({ displayName: z.string().optional(), iconUrl: z.string().optional(), + quantity: numberInput, resourceGroupId: numberInput, rewardItemId: z.string().optional(), weight: numberInput, @@ -81,7 +82,12 @@ export const roomRocketConfigFormSchema = z path: ["giftFuelRules", index, "giftId"], }); } - if (!Number.isInteger(multiplierPpm) || multiplierPpm < 0 || !Number.isInteger(fixedFuel) || fixedFuel < 0) { + if ( + !Number.isInteger(multiplierPpm) || + multiplierPpm < 0 || + !Number.isInteger(fixedFuel) || + fixedFuel < 0 + ) { context.addIssue({ code: "custom", message: "礼物燃料规则数值必须大于等于 0", @@ -100,11 +106,17 @@ export const roomRocketConfigFormSchema = z export type RoomRocketConfigForm = z.infer; -function validateRewards(context: z.RefinementCtx, level: number, key: string, rewards: z.infer[]) { +function validateRewards( + context: z.RefinementCtx, + level: number, + key: string, + rewards: z.infer[], +) { rewards.forEach((reward, index) => { const empty = !Number(reward.resourceGroupId || 0) && !Number(reward.weight || 0) && + !Number(reward.quantity || 0) && !String(reward.displayName || "").trim() && !String(reward.iconUrl || "").trim(); if (empty) { @@ -124,6 +136,13 @@ function validateRewards(context: z.RefinementCtx, level: number, key: string, r path: ["levels", level - 1, key, index, "weight"], }); } + if (!positiveInteger(reward.quantity)) { + context.addIssue({ + code: "custom", + message: `第 ${level} 级奖励数量必须大于 0`, + path: ["levels", level - 1, key, index, "quantity"], + }); + } }); }