feat room rocket reward quantity

This commit is contained in:
zhx 2026-06-11 18:07:57 +08:00
parent ebb4545ad6
commit 53b74f9dc2
6 changed files with 110 additions and 19 deletions

View File

@ -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<string, unknown>;
type RawLevel = Omit<
RoomRocketLevelDto,
"igniterRewards" | "inRoomRewards" | "top1Rewards"
> & {
type RawLevel = Omit<RoomRocketLevelDto, "igniterRewards" | "inRoomRewards" | "top1Rewards"> & {
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;
}

View File

@ -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 <div className={styles.emptyState}>未配置奖项</div>;
}
return (
<div className={styles.rewardTable}>
<div className={styles.rewardTableHead}>
<div className={[styles.rewardTableHead, quantityEnabled ? styles.rewardTableWithQuantity : ""].join(" ")}>
<span>资源组</span>
<span>权重</span>
{quantityEnabled ? <span>数量</span> : null}
<span>展示名</span>
<span>图标</span>
<span />
</div>
{rewards.map((reward, rewardIndex) => (
<div className={styles.rewardTableRow} key={reward.rewardItemId || rewardIndex}>
<div
className={[styles.rewardTableRow, quantityEnabled ? styles.rewardTableWithQuantity : ""].join(" ")}
key={reward.rewardItemId || rewardIndex}
>
<ResourceGroupSelectField
disabled={disabled}
groups={resourceGroups}
@ -328,6 +334,21 @@ function RewardTable({ disabled, levelIndex, poolKey, resourceGroups, rewards, s
updateReward(setForm, levelIndex, poolKey, rewardIndex, { weight: event.target.value })
}
/>
{quantityEnabled ? (
<TextField
required
disabled={disabled}
inputProps={{ min: 1, step: 1 }}
label="数量"
type="number"
value={reward.quantity}
onChange={(event) =>
updateReward(setForm, levelIndex, poolKey, rewardIndex, {
quantity: event.target.value,
})
}
/>
) : null}
<TextField
disabled={disabled}
label="展示名"
@ -509,6 +530,7 @@ function addReward(setForm, levelIndex, poolKey) {
{
displayName: "",
iconUrl: "",
quantity: "1",
resourceGroupId: "",
rewardItemId: `level_${level.level}_${poolKey}_${Date.now()}`,
weight: "100",

View File

@ -144,6 +144,7 @@ function rewardsToForm(rewards = []) {
return rewards.map((reward, index) => ({
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,
);
}

View File

@ -30,14 +30,16 @@ const columns = [
label: "在房奖励",
width: "minmax(220px, 1fr)",
render: (level, _index, context) => (
<RewardSummary groups={context.resourceGroups} rewards={level.inRoomRewards} />
<RewardSummary showQuantity groups={context.resourceGroups} rewards={level.inRoomRewards} />
),
},
{
key: "top1Rewards",
label: "贡献第一",
width: "minmax(220px, 1fr)",
render: (level, _index, context) => <RewardSummary groups={context.resourceGroups} rewards={level.top1Rewards} />,
render: (level, _index, context) => (
<RewardSummary groups={context.resourceGroups} rewards={level.top1Rewards} />
),
},
{
key: "igniterRewards",
@ -67,7 +69,11 @@ export function RoomRocketPage() {
if (editing) {
return (
<AdminListPage>
<DataState error={page.configError} loading={page.configLoading && !page.config} onRetry={page.reloadConfig}>
<DataState
error={page.configError}
loading={page.configLoading && !page.config}
onRetry={page.reloadConfig}
>
<RoomRocketConfigEditor
abilities={page.abilities}
configLoading={page.configLoading}
@ -92,7 +98,11 @@ export function RoomRocketPage() {
onEdit={enterEdit}
onRefresh={page.reloadConfig}
/>
<DataState error={page.configError} loading={page.configLoading && !page.config} onRetry={page.reloadConfig}>
<DataState
error={page.configError}
loading={page.configLoading && !page.config}
onRetry={page.reloadConfig}
>
<AdminListBody>
<DataTable
columns={columns}
@ -117,7 +127,10 @@ function MaterialState({ level }) {
return (
<div className={styles.assetBadges}>
{items.map(([label, url]) => (
<span className={["status-badge", url ? "status-badge--running" : "status-badge--stopped"].join(" ")} key={label}>
<span
className={["status-badge", url ? "status-badge--running" : "status-badge--stopped"].join(" ")}
key={label}
>
<span className="status-point" />
{label}
</span>
@ -126,18 +139,33 @@ function MaterialState({ level }) {
);
}
function RewardSummary({ groups, rewards = [] }) {
function RewardSummary({ groups, rewards = [], showQuantity = false }) {
if (!rewards.length) {
return <span className={styles.meta}>未配置</span>;
}
return (
<div className={styles.stack}>
<span className={styles.name}>{rewards.length} 个奖项</span>
<span className={styles.meta}>{rewards.slice(0, 2).map((reward) => resourceGroupName(groups, reward.resourceGroupId)).join("、")}</span>
<span className={styles.name}>
{rewards.length} 个奖项{showQuantity ? ` · ${rewardQuantityTotal(rewards)}` : ""}
</span>
<span className={styles.meta}>
{rewards
.slice(0, 2)
.map((reward) =>
showQuantity
? `${resourceGroupName(groups, reward.resourceGroupId)} *${Number(reward.quantity || 1)}`
: resourceGroupName(groups, reward.resourceGroupId),
)
.join("、")}
</span>
</div>
);
}
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}` : "-");

View File

@ -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;

View File

@ -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<typeof roomRocketConfigFormSchema>;
function validateRewards(context: z.RefinementCtx, level: number, key: string, rewards: z.infer<typeof rewardSchema>[]) {
function validateRewards(
context: z.RefinementCtx,
level: number,
key: string,
rewards: z.infer<typeof rewardSchema>[],
) {
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"],
});
}
});
}