feat room rocket reward quantity
This commit is contained in:
parent
ebb4545ad6
commit
53b74f9dc2
@ -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;
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}` : "-");
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user