火箭重新修改

This commit is contained in:
zhx 2026-06-05 19:53:39 +08:00
parent af9da06615
commit 825fe47367
16 changed files with 250 additions and 251 deletions

View File

@ -326,26 +326,26 @@
"x-permissions": ["registration-reward:update"] "x-permissions": ["registration-reward:update"]
} }
}, },
"/admin/activity/room-treasure/config": { "/admin/activity/room-rocket/config": {
"get": { "get": {
"operationId": "getRoomTreasureConfig", "operationId": "getRoomRocketConfig",
"responses": { "responses": {
"200": { "200": {
"$ref": "#/components/responses/EmptyResponse" "$ref": "#/components/responses/EmptyResponse"
} }
}, },
"x-permission": "room-treasure:view", "x-permission": "room-rocket:view",
"x-permissions": ["room-treasure:view"] "x-permissions": ["room-rocket:view"]
}, },
"put": { "put": {
"operationId": "updateRoomTreasureConfig", "operationId": "updateRoomRocketConfig",
"responses": { "responses": {
"200": { "200": {
"$ref": "#/components/responses/EmptyResponse" "$ref": "#/components/responses/EmptyResponse"
} }
}, },
"x-permission": "room-treasure:update", "x-permission": "room-rocket:update",
"x-permissions": ["room-treasure:update"] "x-permissions": ["room-rocket:update"]
} }
}, },
"/admin/activity/seven-day-checkin/claims": { "/admin/activity/seven-day-checkin/claims": {

View File

@ -163,7 +163,7 @@ export const fallbackNavigation = [
routeNavItem("registration-reward", { icon: CardGiftcardOutlined }), routeNavItem("registration-reward", { icon: CardGiftcardOutlined }),
routeNavItem("first-recharge-reward", { icon: CardGiftcardOutlined }), routeNavItem("first-recharge-reward", { icon: CardGiftcardOutlined }),
routeNavItem("seven-day-checkin", { icon: EventAvailableOutlined }), routeNavItem("seven-day-checkin", { icon: EventAvailableOutlined }),
routeNavItem("room-treasure", { icon: RedeemOutlined }), routeNavItem("room-rocket", { icon: RedeemOutlined }),
routeNavItem("user-leaderboard", { icon: LeaderboardOutlined }), routeNavItem("user-leaderboard", { icon: LeaderboardOutlined }),
routeNavItem("red-packet", { icon: RedeemOutlined }), routeNavItem("red-packet", { icon: RedeemOutlined }),
routeNavItem("vip-config", { icon: WorkspacePremiumOutlined }), routeNavItem("vip-config", { icon: WorkspacePremiumOutlined }),

View File

@ -127,8 +127,8 @@ export const PERMISSIONS = {
sevenDayCheckInUpdate: "seven-day-checkin:update", sevenDayCheckInUpdate: "seven-day-checkin:update",
luckyGiftView: "lucky-gift:view", luckyGiftView: "lucky-gift:view",
luckyGiftUpdate: "lucky-gift:update", luckyGiftUpdate: "lucky-gift:update",
roomTreasureView: "room-treasure:view", roomRocketView: "room-rocket:view",
roomTreasureUpdate: "room-treasure:update", roomRocketUpdate: "room-rocket:update",
userLeaderboardView: "user-leaderboard:view", userLeaderboardView: "user-leaderboard:view",
redPacketView: "red-packet:view", redPacketView: "red-packet:view",
redPacketUpdate: "red-packet:update", redPacketUpdate: "red-packet:update",
@ -186,7 +186,7 @@ export const MENU_CODES = {
firstRechargeReward: "first-recharge-reward", firstRechargeReward: "first-recharge-reward",
achievementConfig: "achievement-config", achievementConfig: "achievement-config",
sevenDayCheckIn: "seven-day-checkin", sevenDayCheckIn: "seven-day-checkin",
roomTreasure: "room-treasure", roomRocket: "room-rocket",
userLeaderboard: "user-leaderboard", userLeaderboard: "user-leaderboard",
redPacket: "red-packet", redPacket: "red-packet",
vipConfig: "vip-config", vipConfig: "vip-config",

View File

@ -20,7 +20,7 @@ import { regionBlockRoutes } from "@/features/region-blocks/routes.js";
import { resourceRoutes } from "@/features/resources/routes.js"; import { resourceRoutes } from "@/features/resources/routes.js";
import { rolesRoutes } from "@/features/roles/routes.js"; import { rolesRoutes } from "@/features/roles/routes.js";
import { roomRoutes } from "@/features/rooms/routes.js"; import { roomRoutes } from "@/features/rooms/routes.js";
import { roomTreasureRoutes } from "@/features/room-treasure/routes.js"; import { roomRocketRoutes } from "@/features/room-rocket/routes.js";
import { sevenDayCheckInRoutes } from "@/features/seven-day-checkin/routes.js"; import { sevenDayCheckInRoutes } from "@/features/seven-day-checkin/routes.js";
import { userLeaderboardRoutes } from "@/features/user-leaderboard/routes.js"; import { userLeaderboardRoutes } from "@/features/user-leaderboard/routes.js";
import { usersRoutes } from "@/features/users/routes.js"; import { usersRoutes } from "@/features/users/routes.js";
@ -42,7 +42,7 @@ export const adminRoutes: AdminRoute[] = [
...registrationRewardRoutes, ...registrationRewardRoutes,
...firstRechargeRewardRoutes, ...firstRechargeRewardRoutes,
...sevenDayCheckInRoutes, ...sevenDayCheckInRoutes,
...roomTreasureRoutes, ...roomRocketRoutes,
...userLeaderboardRoutes, ...userLeaderboardRoutes,
...redPacketRoutes, ...redPacketRoutes,
...vipConfigRoutes, ...vipConfigRoutes,

View File

@ -1,7 +1,7 @@
import { API_ENDPOINTS, API_OPERATIONS, apiEndpointPath } from "@/shared/api/generated/endpoints"; import { API_ENDPOINTS, API_OPERATIONS, apiEndpointPath } from "@/shared/api/generated/endpoints";
import { apiRequest } from "@/shared/api/request"; import { apiRequest } from "@/shared/api/request";
export interface RoomTreasureRewardItemDto { export interface RoomRocketRewardItemDto {
rewardItemId: string; rewardItemId: string;
resourceGroupId: number; resourceGroupId: number;
weight: number; weight: number;
@ -9,60 +9,60 @@ export interface RoomTreasureRewardItemDto {
iconUrl?: string; iconUrl?: string;
} }
export interface RoomTreasureLevelDto { export interface RoomRocketLevelDto {
level: number; level: number;
energyThreshold: number; fuelThreshold: number;
coverUrl?: string; coverUrl?: string;
animationUrl?: string; animationUrl?: string;
openingAnimationUrl?: string; launchAnimationUrl?: string;
openedImageUrl?: string; launchedImageUrl?: string;
inRoomRewards: RoomTreasureRewardItemDto[]; inRoomRewards: RoomRocketRewardItemDto[];
top1Rewards: RoomTreasureRewardItemDto[]; top1Rewards: RoomRocketRewardItemDto[];
igniterRewards: RoomTreasureRewardItemDto[]; igniterRewards: RoomRocketRewardItemDto[];
} }
export interface GiftEnergyRuleDto { export interface GiftFuelRuleDto {
ruleId: string; ruleId: string;
giftId?: string; giftId?: string;
giftTypeCode?: string; giftTypeCode?: string;
multiplierPpm: number; multiplierPpm: number;
fixedEnergy: number; fixedFuel: number;
excluded: boolean; excluded: boolean;
} }
export interface RoomTreasureConfigDto { export interface RoomRocketConfigDto {
appCode?: string; appCode?: string;
enabled: boolean; enabled: boolean;
configVersion: number; configVersion: number;
energySource: string; fuelSource: string;
openDelayMs: number; launchDelayMs: number;
broadcastEnabled: boolean; broadcastEnabled: boolean;
broadcastScope: string; broadcastScope: string;
broadcastDelayMs: number; broadcastDelayMs: number;
rewardStackPolicy: string; rewardStackPolicy: string;
levels: RoomTreasureLevelDto[]; levels: RoomRocketLevelDto[];
giftEnergyRules: GiftEnergyRuleDto[]; giftFuelRules: GiftFuelRuleDto[];
updatedByAdminId?: number; updatedByAdminId?: number;
createdAtMs?: number; createdAtMs?: number;
updatedAtMs?: number; updatedAtMs?: number;
} }
export type RoomTreasureConfigPayload = Pick< export type RoomRocketConfigPayload = Pick<
RoomTreasureConfigDto, RoomRocketConfigDto,
| "broadcastDelayMs" | "broadcastDelayMs"
| "broadcastEnabled" | "broadcastEnabled"
| "broadcastScope" | "broadcastScope"
| "enabled" | "enabled"
| "energySource" | "fuelSource"
| "giftEnergyRules" | "giftFuelRules"
| "levels" | "levels"
| "openDelayMs" | "launchDelayMs"
| "rewardStackPolicy" | "rewardStackPolicy"
>; >;
type RawReward = RoomTreasureRewardItemDto & Record<string, unknown>; type RawReward = RoomRocketRewardItemDto & Record<string, unknown>;
type RawLevel = Omit< type RawLevel = Omit<
RoomTreasureLevelDto, RoomRocketLevelDto,
"igniterRewards" | "inRoomRewards" | "top1Rewards" "igniterRewards" | "inRoomRewards" | "top1Rewards"
> & { > & {
igniterRewards?: RawReward[]; igniterRewards?: RawReward[];
@ -72,96 +72,95 @@ type RawLevel = Omit<
top1Rewards?: RawReward[]; top1Rewards?: RawReward[];
top1_rewards?: RawReward[]; top1_rewards?: RawReward[];
} & Record<string, unknown>; } & Record<string, unknown>;
type RawGiftEnergyRule = GiftEnergyRuleDto & Record<string, unknown>; type RawGiftFuelRule = GiftFuelRuleDto & Record<string, unknown>;
type RawConfig = Omit<RoomTreasureConfigDto, "giftEnergyRules" | "levels"> & { type RawConfig = Omit<RoomRocketConfigDto, "giftFuelRules" | "levels"> & {
giftEnergyRules?: RawGiftEnergyRule[]; giftFuelRules?: RawGiftFuelRule[];
gift_energy_rules?: RawGiftEnergyRule[];
levels?: RawLevel[]; levels?: RawLevel[];
} & Record<string, unknown>; } & Record<string, unknown>;
export function getRoomTreasureConfig(): Promise<RoomTreasureConfigDto> { export function getRoomRocketConfig(): Promise<RoomRocketConfigDto> {
const endpoint = API_ENDPOINTS.getRoomTreasureConfig; const endpoint = API_ENDPOINTS.getRoomRocketConfig;
return apiRequest<RawConfig>(apiEndpointPath(API_OPERATIONS.getRoomTreasureConfig), { return apiRequest<RawConfig>(apiEndpointPath(API_OPERATIONS.getRoomRocketConfig), {
method: endpoint.method, method: endpoint.method,
}).then(normalizeConfig); }).then(normalizeConfig);
} }
export function updateRoomTreasureConfig(payload: RoomTreasureConfigPayload): Promise<RoomTreasureConfigDto> { export function updateRoomRocketConfig(payload: RoomRocketConfigPayload): Promise<RoomRocketConfigDto> {
const endpoint = API_ENDPOINTS.updateRoomTreasureConfig; const endpoint = API_ENDPOINTS.updateRoomRocketConfig;
return apiRequest<RawConfig, RoomTreasureConfigPayload>(apiEndpointPath(API_OPERATIONS.updateRoomTreasureConfig), { return apiRequest<RawConfig, RoomRocketConfigPayload>(apiEndpointPath(API_OPERATIONS.updateRoomRocketConfig), {
body: payload, body: payload,
method: endpoint.method, method: endpoint.method,
}).then(normalizeConfig); }).then(normalizeConfig);
} }
function normalizeConfig(item: RawConfig): RoomTreasureConfigDto { function normalizeConfig(item: RawConfig): RoomRocketConfigDto {
return { return {
appCode: stringValue(item.appCode ?? item.app_code), appCode: stringValue(item.appCode ?? item.app_code),
enabled: Boolean(item.enabled), enabled: Boolean(item.enabled),
configVersion: numberValue(item.configVersion ?? item.config_version), configVersion: numberValue(item.configVersion ?? item.config_version),
energySource: normalizeEnergySource(item.energySource ?? item.energy_source), fuelSource: normalizeFuelSource(item.fuelSource),
openDelayMs: numberValue(item.openDelayMs ?? item.open_delay_ms), launchDelayMs: numberValue(item.launchDelayMs ?? item.launch_delay_ms),
broadcastEnabled: Boolean(item.broadcastEnabled ?? item.broadcast_enabled), broadcastEnabled: Boolean(item.broadcastEnabled ?? item.broadcast_enabled),
broadcastScope: stringValue(item.broadcastScope ?? item.broadcast_scope) || "region", broadcastScope: stringValue(item.broadcastScope ?? item.broadcast_scope) || "region",
broadcastDelayMs: numberValue(item.broadcastDelayMs ?? item.broadcast_delay_ms), broadcastDelayMs: numberValue(item.broadcastDelayMs ?? item.broadcast_delay_ms),
rewardStackPolicy: stringValue(item.rewardStackPolicy ?? item.reward_stack_policy) || "allow_stack", rewardStackPolicy: stringValue(item.rewardStackPolicy ?? item.reward_stack_policy) || "allow_stack",
levels: normalizeLevels(item.levels), levels: normalizeLevels(item.levels),
giftEnergyRules: normalizeGiftEnergyRules(item.giftEnergyRules ?? item.gift_energy_rules), giftFuelRules: normalizeGiftFuelRules(item.giftFuelRules),
updatedByAdminId: numberValue(item.updatedByAdminId ?? item.updated_by_admin_id), updatedByAdminId: numberValue(item.updatedByAdminId ?? item.updated_by_admin_id),
createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms), createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms),
updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms), updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms),
}; };
} }
function normalizeEnergySource(value: unknown) { function normalizeFuelSource(value: unknown) {
const source = stringValue(value); const source = stringValue(value);
if (!source || source === "gift_point_added") { if (!source) {
return "heat_value"; return "heat_value";
} }
return source; return source;
} }
function normalizeLevels(levels?: RawLevel[]): RoomTreasureLevelDto[] { function normalizeLevels(levels?: RawLevel[]): RoomRocketLevelDto[] {
const byLevel = new Map<number, RoomTreasureLevelDto>(); const byLevel = new Map<number, RoomRocketLevelDto>();
for (const level of levels || []) { for (const level of levels || []) {
const levelNo = numberValue(level.level); const levelNo = numberValue(level.level);
if (levelNo < 1 || levelNo > 7) { if (levelNo < 1 || levelNo > 5) {
continue; continue;
} }
byLevel.set(levelNo, { byLevel.set(levelNo, {
level: levelNo, level: levelNo,
energyThreshold: numberValue(level.energyThreshold ?? level.energy_threshold), fuelThreshold: numberValue(level.fuelThreshold),
coverUrl: stringValue(level.coverUrl ?? level.cover_url), coverUrl: stringValue(level.coverUrl ?? level.cover_url),
animationUrl: stringValue(level.animationUrl ?? level.animation_url), animationUrl: stringValue(level.animationUrl ?? level.animation_url),
openingAnimationUrl: stringValue(level.openingAnimationUrl ?? level.opening_animation_url), launchAnimationUrl: stringValue(level.launchAnimationUrl ?? level.launch_animation_url),
openedImageUrl: stringValue(level.openedImageUrl ?? level.opened_image_url), launchedImageUrl: stringValue(level.launchedImageUrl ?? level.launched_image_url),
inRoomRewards: normalizeRewards(level.inRoomRewards ?? level.in_room_rewards), inRoomRewards: normalizeRewards(level.inRoomRewards ?? level.in_room_rewards),
top1Rewards: normalizeRewards(level.top1Rewards ?? level.top1_rewards), top1Rewards: normalizeRewards(level.top1Rewards ?? level.top1_rewards),
igniterRewards: normalizeRewards(level.igniterRewards ?? level.igniter_rewards), igniterRewards: normalizeRewards(level.igniterRewards ?? level.igniter_rewards),
}); });
} }
return Array.from({ length: 7 }, (_, index) => { return Array.from({ length: 5 }, (_, index) => {
const level = index + 1; const level = index + 1;
return byLevel.get(level) || defaultLevel(level); return byLevel.get(level) || defaultLevel(level);
}); });
} }
function defaultLevel(level: number): RoomTreasureLevelDto { function defaultLevel(level: number): RoomRocketLevelDto {
const thresholds = [1000, 3000, 6000, 10000, 15000, 21000, 28000]; const thresholds = [1000, 3000, 6000, 10000, 15000];
return { return {
level, level,
energyThreshold: thresholds[level - 1] || 0, fuelThreshold: thresholds[level - 1] || 0,
coverUrl: "", coverUrl: "",
animationUrl: "", animationUrl: "",
openingAnimationUrl: "", launchAnimationUrl: "",
openedImageUrl: "", launchedImageUrl: "",
inRoomRewards: [], inRoomRewards: [],
top1Rewards: [], top1Rewards: [],
igniterRewards: [], igniterRewards: [],
}; };
} }
function normalizeRewards(rewards?: RawReward[]): RoomTreasureRewardItemDto[] { function normalizeRewards(rewards?: RawReward[]): RoomRocketRewardItemDto[] {
return (rewards || []) return (rewards || [])
.map((reward, index) => ({ .map((reward, index) => ({
rewardItemId: stringValue(reward.rewardItemId ?? reward.reward_item_id) || `reward_${index + 1}`, rewardItemId: stringValue(reward.rewardItemId ?? reward.reward_item_id) || `reward_${index + 1}`,
@ -173,17 +172,17 @@ function normalizeRewards(rewards?: RawReward[]): RoomTreasureRewardItemDto[] {
.filter((reward) => reward.resourceGroupId > 0 || reward.weight > 0 || reward.displayName || reward.iconUrl); .filter((reward) => reward.resourceGroupId > 0 || reward.weight > 0 || reward.displayName || reward.iconUrl);
} }
function normalizeGiftEnergyRules(rules?: RawGiftEnergyRule[]): GiftEnergyRuleDto[] { function normalizeGiftFuelRules(rules?: RawGiftFuelRule[]): GiftFuelRuleDto[] {
return (rules || []) return (rules || [])
.map((rule, index) => ({ .map((rule, index) => ({
ruleId: stringValue(rule.ruleId ?? rule.rule_id) || `gift_energy_rule_${index + 1}`, ruleId: stringValue(rule.ruleId ?? rule.rule_id) || `gift_fuel_rule_${index + 1}`,
giftId: stringValue(rule.giftId ?? rule.gift_id), giftId: stringValue(rule.giftId ?? rule.gift_id),
giftTypeCode: stringValue(rule.giftTypeCode ?? rule.gift_type_code), giftTypeCode: stringValue(rule.giftTypeCode ?? rule.gift_type_code),
multiplierPpm: numberValue(rule.multiplierPpm ?? rule.multiplier_ppm), multiplierPpm: numberValue(rule.multiplierPpm ?? rule.multiplier_ppm),
fixedEnergy: numberValue(rule.fixedEnergy ?? rule.fixed_energy), fixedFuel: numberValue(rule.fixedFuel),
excluded: Boolean(rule.excluded), excluded: Boolean(rule.excluded),
})) }))
.filter((rule) => rule.giftId || rule.giftTypeCode || rule.multiplierPpm > 0 || rule.fixedEnergy > 0 || rule.excluded); .filter((rule) => rule.giftId || rule.giftTypeCode || rule.multiplierPpm > 0 || rule.fixedFuel > 0 || rule.excluded);
} }
function stringValue(value: unknown) { function stringValue(value: unknown) {

View File

@ -13,9 +13,9 @@ import { Button } from "@/shared/ui/Button.jsx";
import { IconButton } from "@/shared/ui/IconButton.jsx"; import { IconButton } from "@/shared/ui/IconButton.jsx";
import { ResourceGroupSelectField } from "@/shared/ui/ResourceGroupSelectDrawer.jsx"; import { ResourceGroupSelectField } from "@/shared/ui/ResourceGroupSelectDrawer.jsx";
import { UploadField } from "@/shared/ui/UploadField.jsx"; import { UploadField } from "@/shared/ui/UploadField.jsx";
import styles from "@/features/room-treasure/room-treasure.module.css"; import styles from "@/features/room-rocket/room-rocket.module.css";
const energySourceOptions = [ const fuelSourceOptions = [
["heat_value", "礼物贡献"], ["heat_value", "礼物贡献"],
]; ];
@ -36,7 +36,7 @@ const rewardTabs = [
["igniterRewards", "点火人"], ["igniterRewards", "点火人"],
]; ];
export function RoomTreasureConfigEditor({ export function RoomRocketConfigEditor({
abilities, abilities,
configLoading, configLoading,
configSaving, configSaving,
@ -64,8 +64,8 @@ export function RoomTreasureConfigEditor({
返回 返回
</Button> </Button>
<div className={styles.stack}> <div className={styles.stack}>
<span className={styles.title}>房间宝箱配置</span> <span className={styles.title}>房间火箭配置</span>
<span className={styles.meta}>编辑 7 级宝箱物料奖励池和礼物能量规则</span> <span className={styles.meta}>编辑 5 级火箭物料奖励池和礼物燃料规则</span>
</div> </div>
</div> </div>
<div className={styles.editorActions}> <div className={styles.editorActions}>
@ -85,30 +85,30 @@ export function RoomTreasureConfigEditor({
<SwitchField <SwitchField
checked={form.enabled} checked={form.enabled}
disabled={disabled} disabled={disabled}
label="宝箱状态" label="火箭状态"
onChange={(event) => updateRoot(setForm, "enabled", event.target.checked)} onChange={(event) => updateRoot(setForm, "enabled", event.target.checked)}
/> />
<SwitchField <SwitchField
checked={form.broadcastEnabled} checked={form.broadcastEnabled}
disabled={disabled} disabled={disabled}
label="开箱广播" label="发射广播"
onChange={(event) => updateRoot(setForm, "broadcastEnabled", event.target.checked)} onChange={(event) => updateRoot(setForm, "broadcastEnabled", event.target.checked)}
/> />
<SelectField <SelectField
disabled={disabled} disabled={disabled}
label="能量来源" label="燃料来源"
options={energySourceOptions} options={fuelSourceOptions}
value={form.energySource} value={form.fuelSource}
onChange={(value) => updateRoot(setForm, "energySource", value)} onChange={(value) => updateRoot(setForm, "fuelSource", value)}
/> />
<TextField <TextField
required required
disabled={disabled} disabled={disabled}
inputProps={{ min: 0, step: 1000 }} inputProps={{ min: 0, step: 1000 }}
label="开箱倒计时 ms" label="发射倒计时 ms"
type="number" type="number"
value={form.openDelayMs} value={form.launchDelayMs}
onChange={(event) => updateRoot(setForm, "openDelayMs", event.target.value)} onChange={(event) => updateRoot(setForm, "launchDelayMs", event.target.value)}
/> />
<SelectField <SelectField
disabled={disabled} disabled={disabled}
@ -139,7 +139,7 @@ export function RoomTreasureConfigEditor({
<section className={styles.editorSection}> <section className={styles.editorSection}>
<Tabs value={editorTab} onChange={(_, value) => setEditorTab(value)}> <Tabs value={editorTab} onChange={(_, value) => setEditorTab(value)}>
<Tab label="等级配置" value="levels" /> <Tab label="等级配置" value="levels" />
<Tab label="礼物能量规则" value="energyRules" /> <Tab label="礼物燃料规则" value="fuelRules" />
</Tabs> </Tabs>
{editorTab === "levels" ? ( {editorTab === "levels" ? (
<div className={styles.levelWorkspace}> <div className={styles.levelWorkspace}>
@ -161,17 +161,17 @@ export function RoomTreasureConfigEditor({
<div className={styles.levelDetailHeader}> <div className={styles.levelDetailHeader}>
<div className={styles.stack}> <div className={styles.stack}>
<span className={styles.title}>L{level.level}</span> <span className={styles.title}>L{level.level}</span>
<span className={styles.meta}>当前等级送礼溢出不进入下一级倒计时期间送礼不累计能量</span> <span className={styles.meta}>当前等级溢出燃料作废点火后下一等级立即开始</span>
</div> </div>
<TextField <TextField
required required
disabled={disabled} disabled={disabled}
inputProps={{ min: 1, step: 1 }} inputProps={{ min: 1, step: 1 }}
label="能量阈值" label="燃料阈值"
type="number" type="number"
value={level.energyThreshold} value={level.fuelThreshold}
onChange={(event) => onChange={(event) =>
updateLevel(setForm, activeLevelIndex, { energyThreshold: event.target.value }) updateLevel(setForm, activeLevelIndex, { fuelThreshold: event.target.value })
} }
/> />
</div> </div>
@ -186,7 +186,7 @@ export function RoomTreasureConfigEditor({
<UploadField <UploadField
disabled={disabled} disabled={disabled}
kind="file" kind="file"
label="宝箱动效" label="火箭动效"
uploadKind="file" uploadKind="file"
value={level.animationUrl} value={level.animationUrl}
onChange={(value) => updateLevel(setForm, activeLevelIndex, { animationUrl: value })} onChange={(value) => updateLevel(setForm, activeLevelIndex, { animationUrl: value })}
@ -194,19 +194,19 @@ export function RoomTreasureConfigEditor({
<UploadField <UploadField
disabled={disabled} disabled={disabled}
kind="file" kind="file"
label="开箱动效" label="发射动效"
uploadKind="file" uploadKind="file"
value={level.openingAnimationUrl} value={level.launchAnimationUrl}
onChange={(value) => onChange={(value) =>
updateLevel(setForm, activeLevelIndex, { openingAnimationUrl: value }) updateLevel(setForm, activeLevelIndex, { launchAnimationUrl: value })
} }
/> />
<UploadField <UploadField
disabled={disabled} disabled={disabled}
kind="image" kind="image"
label="打开图" label="发射后图"
value={level.openedImageUrl} value={level.launchedImageUrl}
onChange={(value) => updateLevel(setForm, activeLevelIndex, { openedImageUrl: value })} onChange={(value) => updateLevel(setForm, activeLevelIndex, { launchedImageUrl: value })}
/> />
</div> </div>
<div className={styles.rewardPanel}> <div className={styles.rewardPanel}>
@ -242,7 +242,7 @@ export function RoomTreasureConfigEditor({
) : null} ) : null}
</div> </div>
) : ( ) : (
<EnergyRuleEditor disabled={disabled} form={form} setForm={setForm} /> <FuelRuleEditor disabled={disabled} form={form} setForm={setForm} />
)} )}
</section> </section>
</div> </div>
@ -254,15 +254,15 @@ function LevelNavItem({ active, level, onClick }) {
const materialCount = [ const materialCount = [
level.coverUrl, level.coverUrl,
level.animationUrl, level.animationUrl,
level.openingAnimationUrl, level.launchAnimationUrl,
level.openedImageUrl, level.launchedImageUrl,
].filter(Boolean).length; ].filter(Boolean).length;
const rewardCount = rewardTabs.filter(([key]) => (level[key] || []).length > 0).length; const rewardCount = rewardTabs.filter(([key]) => (level[key] || []).length > 0).length;
return ( return (
<button className={[styles.levelNavItem, active ? styles.levelNavItemActive : ""].join(" ")} type="button" onClick={onClick}> <button className={[styles.levelNavItem, active ? styles.levelNavItemActive : ""].join(" ")} type="button" onClick={onClick}>
<span className={styles.levelNavMain}>L{level.level}</span> <span className={styles.levelNavMain}>L{level.level}</span>
<span className={styles.levelNavMeta}>阈值 {formatNumber(level.energyThreshold)}</span> <span className={styles.levelNavMeta}>阈值 {formatNumber(level.fuelThreshold)}</span>
<span className={styles.levelNavMeta}>物料 {materialCount}/4 · 奖励 {rewardCount}/3</span> <span className={styles.levelNavMeta}>物料 {materialCount}/4 · 奖励 {rewardCount}/3</span>
</button> </button>
); );
@ -334,38 +334,38 @@ function RewardTable({ disabled, levelIndex, poolKey, resourceGroups, rewards, s
); );
} }
function EnergyRuleEditor({ disabled, form, setForm }) { function FuelRuleEditor({ disabled, form, setForm }) {
return ( return (
<div className={styles.energyRuleEditor}> <div className={styles.fuelRuleEditor}>
<div className={styles.energyRuleToolbar}> <div className={styles.fuelRuleToolbar}>
<span className={styles.meta}>未命中特殊规则的礼物按基础能量来源计算</span> <span className={styles.meta}>未命中特殊规则的礼物按基础燃料来源计算</span>
<Button disabled={disabled} startIcon={<AddOutlined fontSize="small" />} type="button" onClick={() => addEnergyRule(setForm)}> <Button disabled={disabled} startIcon={<AddOutlined fontSize="small" />} type="button" onClick={() => addFuelRule(setForm)}>
添加规则 添加规则
</Button> </Button>
</div> </div>
{form.giftEnergyRules.length ? ( {form.giftFuelRules.length ? (
<div className={styles.energyRuleTable}> <div className={styles.fuelRuleTable}>
<div className={styles.energyRuleTableHead}> <div className={styles.fuelRuleTableHead}>
<span>礼物 ID</span> <span>礼物 ID</span>
<span>礼物类型</span> <span>礼物类型</span>
<span>倍率 ppm</span> <span>倍率 ppm</span>
<span>固定能量</span> <span>固定燃料</span>
<span>排除</span> <span>排除</span>
<span /> <span />
</div> </div>
{form.giftEnergyRules.map((rule, index) => ( {form.giftFuelRules.map((rule, index) => (
<div className={styles.energyRuleTableRow} key={rule.ruleId || index}> <div className={styles.fuelRuleTableRow} key={rule.ruleId || index}>
<TextField <TextField
disabled={disabled} disabled={disabled}
label="礼物 ID" label="礼物 ID"
value={rule.giftId} value={rule.giftId}
onChange={(event) => updateEnergyRule(setForm, index, { giftId: event.target.value })} onChange={(event) => updateFuelRule(setForm, index, { giftId: event.target.value })}
/> />
<TextField <TextField
disabled={disabled} disabled={disabled}
label="礼物类型" label="礼物类型"
value={rule.giftTypeCode} value={rule.giftTypeCode}
onChange={(event) => updateEnergyRule(setForm, index, { giftTypeCode: event.target.value })} onChange={(event) => updateFuelRule(setForm, index, { giftTypeCode: event.target.value })}
/> />
<TextField <TextField
disabled={disabled} disabled={disabled}
@ -373,21 +373,21 @@ function EnergyRuleEditor({ disabled, form, setForm }) {
label="倍率 ppm" label="倍率 ppm"
type="number" type="number"
value={rule.multiplierPpm} value={rule.multiplierPpm}
onChange={(event) => updateEnergyRule(setForm, index, { multiplierPpm: event.target.value })} onChange={(event) => updateFuelRule(setForm, index, { multiplierPpm: event.target.value })}
/> />
<TextField <TextField
disabled={disabled} disabled={disabled}
inputProps={{ min: 0, step: 1 }} inputProps={{ min: 0, step: 1 }}
label="固定能量" label="固定燃料"
type="number" type="number"
value={rule.fixedEnergy} value={rule.fixedFuel}
onChange={(event) => updateEnergyRule(setForm, index, { fixedEnergy: event.target.value })} onChange={(event) => updateFuelRule(setForm, index, { fixedFuel: event.target.value })}
/> />
<SwitchField <SwitchField
checked={rule.excluded} checked={rule.excluded}
disabled={disabled} disabled={disabled}
label="排除" label="排除"
onChange={(event) => updateEnergyRule(setForm, index, { excluded: event.target.checked })} onChange={(event) => updateFuelRule(setForm, index, { excluded: event.target.checked })}
/> />
<Tooltip arrow title="删除规则"> <Tooltip arrow title="删除规则">
<span> <span>
@ -396,7 +396,7 @@ function EnergyRuleEditor({ disabled, form, setForm }) {
label="删除规则" label="删除规则"
tone="danger" tone="danger"
type="button" type="button"
onClick={() => removeEnergyRule(setForm, index)} onClick={() => removeFuelRule(setForm, index)}
> >
<DeleteOutlineOutlined fontSize="small" /> <DeleteOutlineOutlined fontSize="small" />
</IconButton> </IconButton>
@ -501,36 +501,36 @@ function removeReward(setForm, levelIndex, poolKey, rewardIndex) {
})); }));
} }
function addEnergyRule(setForm) { function addFuelRule(setForm) {
setForm((current) => ({ setForm((current) => ({
...current, ...current,
giftEnergyRules: [ giftFuelRules: [
...current.giftEnergyRules, ...current.giftFuelRules,
{ {
excluded: false, excluded: false,
fixedEnergy: "0", fixedFuel: "0",
giftId: "", giftId: "",
giftTypeCode: "", giftTypeCode: "",
multiplierPpm: "1000000", multiplierPpm: "1000000",
ruleId: `gift_energy_rule_${Date.now()}`, ruleId: `gift_fuel_rule_${Date.now()}`,
}, },
], ],
})); }));
} }
function updateEnergyRule(setForm, index, patch) { function updateFuelRule(setForm, index, patch) {
setForm((current) => ({ setForm((current) => ({
...current, ...current,
giftEnergyRules: current.giftEnergyRules.map((rule, itemIndex) => giftFuelRules: current.giftFuelRules.map((rule, itemIndex) =>
itemIndex === index ? { ...rule, ...patch } : rule, itemIndex === index ? { ...rule, ...patch } : rule,
), ),
})); }));
} }
function removeEnergyRule(setForm, index) { function removeFuelRule(setForm, index) {
setForm((current) => ({ setForm((current) => ({
...current, ...current,
giftEnergyRules: current.giftEnergyRules.filter((_, itemIndex) => itemIndex !== index), giftFuelRules: current.giftFuelRules.filter((_, itemIndex) => itemIndex !== index),
})); }));
} }

View File

@ -3,7 +3,7 @@ import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
import { Button } from "@/shared/ui/Button.jsx"; import { Button } from "@/shared/ui/Button.jsx";
import { AdminActionIconButton } from "@/shared/ui/AdminListLayout.jsx"; import { AdminActionIconButton } from "@/shared/ui/AdminListLayout.jsx";
import { formatMillis } from "@/shared/utils/time.js"; import { formatMillis } from "@/shared/utils/time.js";
import styles from "@/features/room-treasure/room-treasure.module.css"; import styles from "@/features/room-rocket/room-rocket.module.css";
const broadcastScopeLabels = { const broadcastScopeLabels = {
global: "全局广播", global: "全局广播",
@ -16,18 +16,18 @@ const rewardStackPolicyLabels = {
priority_only: "优先级单份", priority_only: "优先级单份",
}; };
export function RoomTreasureConfigSummary({ canUpdate, config, configLoading, onEdit, onRefresh }) { export function RoomRocketConfigSummary({ canUpdate, config, configLoading, onEdit, onRefresh }) {
return ( return (
<section className={styles.summaryPanel}> <section className={styles.summaryPanel}>
<div className={styles.summaryScrollArea}> <div className={styles.summaryScrollArea}>
<div className={styles.summaryContent}> <div className={styles.summaryContent}>
<div className={styles.summaryHeader}> <div className={styles.summaryHeader}>
<span className={styles.title}>房间宝箱配置</span> <span className={styles.title}>房间火箭配置</span>
</div> </div>
<div className={styles.summaryItems}> <div className={styles.summaryItems}>
<ConfigStatus config={config} loading={configLoading} /> <ConfigStatus config={config} loading={configLoading} />
<SummaryItem label="配置版本">{config?.configVersion || "-"}</SummaryItem> <SummaryItem label="配置版本">{config?.configVersion || "-"}</SummaryItem>
<SummaryItem label="开箱倒计时">{formatSeconds(config?.openDelayMs)}</SummaryItem> <SummaryItem label="发射倒计时">{formatSeconds(config?.launchDelayMs)}</SummaryItem>
<SummaryItem label="广播"> <SummaryItem label="广播">
{config?.broadcastEnabled ? broadcastScopeLabels[config.broadcastScope] || config.broadcastScope : "关闭"} {config?.broadcastEnabled ? broadcastScopeLabels[config.broadcastScope] || config.broadcastScope : "关闭"}
</SummaryItem> </SummaryItem>
@ -93,7 +93,7 @@ function LevelPreview({ config, loading }) {
{(config?.levels || []).map((level) => ( {(config?.levels || []).map((level) => (
<span className={styles.levelPill} key={level.level}> <span className={styles.levelPill} key={level.level}>
<span>L{level.level}</span> <span>L{level.level}</span>
<span>{formatNumber(level.energyThreshold)}</span> <span>{formatNumber(level.fuelThreshold)}</span>
</span> </span>
))} ))}
</div> </div>

View File

@ -2,20 +2,20 @@ import { useCallback, useEffect, useState } from "react";
import { parseForm } from "@/shared/forms/validation"; import { parseForm } from "@/shared/forms/validation";
import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx";
import { listResourceGroups } from "@/features/resources/api"; import { listResourceGroups } from "@/features/resources/api";
import { getRoomTreasureConfig, updateRoomTreasureConfig } from "@/features/room-treasure/api"; import { getRoomRocketConfig, updateRoomRocketConfig } from "@/features/room-rocket/api";
import { useRoomTreasureAbilities } from "@/features/room-treasure/permissions.js"; import { useRoomRocketAbilities } from "@/features/room-rocket/permissions.js";
import { roomTreasureConfigFormSchema } from "@/features/room-treasure/schema"; import { roomRocketConfigFormSchema } from "@/features/room-rocket/schema";
const defaultThresholds = [1000, 3000, 6000, 10000, 15000, 21000, 28000]; const defaultThresholds = [1000, 3000, 6000, 10000, 15000];
export function useRoomTreasurePage() { export function useRoomRocketPage() {
const abilities = useRoomTreasureAbilities(); const abilities = useRoomRocketAbilities();
const { showToast } = useToast(); const { showToast } = useToast();
const [config, setConfig] = useState(null); const [config, setConfig] = useState(null);
const [configError, setConfigError] = useState(""); const [configError, setConfigError] = useState("");
const [configLoading, setConfigLoading] = useState(false); const [configLoading, setConfigLoading] = useState(false);
const [configSaving, setConfigSaving] = useState(false); const [configSaving, setConfigSaving] = useState(false);
const [form, setForm] = useState(emptyRoomTreasureForm()); const [form, setForm] = useState(emptyRoomRocketForm());
const [resourceGroups, setResourceGroups] = useState([]); const [resourceGroups, setResourceGroups] = useState([]);
const reloadConfig = useCallback(async () => { const reloadConfig = useCallback(async () => {
@ -23,14 +23,14 @@ export function useRoomTreasurePage() {
setConfigError(""); setConfigError("");
try { try {
const [nextConfig, groups] = await Promise.all([ const [nextConfig, groups] = await Promise.all([
getRoomTreasureConfig(), getRoomRocketConfig(),
listResourceGroups({ page: 1, page_size: 100, status: "active" }), listResourceGroups({ page: 1, page_size: 100, status: "active" }),
]); ]);
setConfig(nextConfig); setConfig(nextConfig);
setForm(formFromConfig(nextConfig)); setForm(formFromConfig(nextConfig));
setResourceGroups(groups.items || []); setResourceGroups(groups.items || []);
} catch (err) { } catch (err) {
const message = err.message || "加载房间宝箱配置失败"; const message = err.message || "加载房间火箭配置失败";
setConfigError(message); setConfigError(message);
showToast(message, "error"); showToast(message, "error");
} finally { } finally {
@ -43,7 +43,7 @@ export function useRoomTreasurePage() {
}, [reloadConfig]); }, [reloadConfig]);
const resetForm = useCallback(() => { const resetForm = useCallback(() => {
setForm(config ? formFromConfig(config) : emptyRoomTreasureForm()); setForm(config ? formFromConfig(config) : emptyRoomRocketForm());
}, [config]); }, [config]);
const submitConfig = async (event, onSaved) => { const submitConfig = async (event, onSaved) => {
@ -53,14 +53,14 @@ export function useRoomTreasurePage() {
} }
setConfigSaving(true); setConfigSaving(true);
try { try {
const parsed = parseForm(roomTreasureConfigFormSchema, form); const parsed = parseForm(roomRocketConfigFormSchema, form);
const saved = await updateRoomTreasureConfig(payloadFromForm(parsed)); const saved = await updateRoomRocketConfig(payloadFromForm(parsed));
setConfig(saved); setConfig(saved);
setForm(formFromConfig(saved)); setForm(formFromConfig(saved));
showToast("房间宝箱配置已保存", "success"); showToast("房间火箭配置已保存", "success");
onSaved?.(saved); onSaved?.(saved);
} catch (err) { } catch (err) {
showToast(err.message || "保存房间宝箱配置失败", "error"); showToast(err.message || "保存房间火箭配置失败", "error");
} finally { } finally {
setConfigSaving(false); setConfigSaving(false);
} }
@ -81,61 +81,61 @@ export function useRoomTreasurePage() {
}; };
} }
export function emptyRoomTreasureForm() { export function emptyRoomRocketForm() {
return { return {
broadcastDelayMs: "0", broadcastDelayMs: "0",
broadcastEnabled: true, broadcastEnabled: true,
broadcastScope: "region", broadcastScope: "region",
enabled: false, enabled: false,
energySource: "heat_value", fuelSource: "heat_value",
giftEnergyRules: [], giftFuelRules: [],
levels: Array.from({ length: 7 }, (_, index) => ({ levels: Array.from({ length: 5 }, (_, index) => ({
animationUrl: "", animationUrl: "",
coverUrl: "", coverUrl: "",
energyThreshold: String(defaultThresholds[index]), fuelThreshold: String(defaultThresholds[index]),
igniterRewards: [], igniterRewards: [],
inRoomRewards: [], inRoomRewards: [],
level: index + 1, level: index + 1,
openedImageUrl: "", launchedImageUrl: "",
openingAnimationUrl: "", launchAnimationUrl: "",
top1Rewards: [], top1Rewards: [],
})), })),
openDelayMs: "30000", launchDelayMs: "30000",
rewardStackPolicy: "allow_stack", rewardStackPolicy: "allow_stack",
}; };
} }
function formFromConfig(config) { function formFromConfig(config) {
const fallback = emptyRoomTreasureForm(); const fallback = emptyRoomRocketForm();
return { return {
broadcastDelayMs: String(config?.broadcastDelayMs ?? fallback.broadcastDelayMs), broadcastDelayMs: String(config?.broadcastDelayMs ?? fallback.broadcastDelayMs),
broadcastEnabled: Boolean(config?.broadcastEnabled), broadcastEnabled: Boolean(config?.broadcastEnabled),
broadcastScope: config?.broadcastScope || fallback.broadcastScope, broadcastScope: config?.broadcastScope || fallback.broadcastScope,
enabled: Boolean(config?.enabled), enabled: Boolean(config?.enabled),
energySource: config?.energySource || fallback.energySource, fuelSource: config?.fuelSource || fallback.fuelSource,
giftEnergyRules: (config?.giftEnergyRules || []).map((rule, index) => ({ giftFuelRules: (config?.giftFuelRules || []).map((rule, index) => ({
excluded: Boolean(rule.excluded), excluded: Boolean(rule.excluded),
fixedEnergy: String(rule.fixedEnergy || 0), fixedFuel: String(rule.fixedFuel || 0),
giftId: rule.giftId || "", giftId: rule.giftId || "",
giftTypeCode: rule.giftTypeCode || "", giftTypeCode: rule.giftTypeCode || "",
multiplierPpm: String(rule.multiplierPpm || 0), multiplierPpm: String(rule.multiplierPpm || 0),
ruleId: rule.ruleId || `gift_energy_rule_${index + 1}`, ruleId: rule.ruleId || `gift_fuel_rule_${index + 1}`,
})), })),
levels: fallback.levels.map((fallbackLevel) => { levels: fallback.levels.map((fallbackLevel) => {
const level = config?.levels?.find((item) => Number(item.level) === fallbackLevel.level) || fallbackLevel; const level = config?.levels?.find((item) => Number(item.level) === fallbackLevel.level) || fallbackLevel;
return { return {
animationUrl: level.animationUrl || "", animationUrl: level.animationUrl || "",
coverUrl: level.coverUrl || "", coverUrl: level.coverUrl || "",
energyThreshold: String(level.energyThreshold || fallbackLevel.energyThreshold), fuelThreshold: String(level.fuelThreshold || fallbackLevel.fuelThreshold),
igniterRewards: rewardsToForm(level.igniterRewards), igniterRewards: rewardsToForm(level.igniterRewards),
inRoomRewards: rewardsToForm(level.inRoomRewards), inRoomRewards: rewardsToForm(level.inRoomRewards),
level: fallbackLevel.level, level: fallbackLevel.level,
openedImageUrl: level.openedImageUrl || "", launchedImageUrl: level.launchedImageUrl || "",
openingAnimationUrl: level.openingAnimationUrl || "", launchAnimationUrl: level.launchAnimationUrl || "",
top1Rewards: rewardsToForm(level.top1Rewards), top1Rewards: rewardsToForm(level.top1Rewards),
}; };
}), }),
openDelayMs: String(config?.openDelayMs ?? fallback.openDelayMs), launchDelayMs: String(config?.launchDelayMs ?? fallback.launchDelayMs),
rewardStackPolicy: config?.rewardStackPolicy || fallback.rewardStackPolicy, rewardStackPolicy: config?.rewardStackPolicy || fallback.rewardStackPolicy,
}; };
} }
@ -156,29 +156,29 @@ function payloadFromForm(form) {
broadcastEnabled: Boolean(form.broadcastEnabled), broadcastEnabled: Boolean(form.broadcastEnabled),
broadcastScope: form.broadcastScope, broadcastScope: form.broadcastScope,
enabled: Boolean(form.enabled), enabled: Boolean(form.enabled),
energySource: form.energySource, fuelSource: form.fuelSource,
giftEnergyRules: form.giftEnergyRules.map((rule, index) => ({ giftFuelRules: form.giftFuelRules.map((rule, index) => ({
excluded: Boolean(rule.excluded), excluded: Boolean(rule.excluded),
fixedEnergy: Number(rule.fixedEnergy || 0), fixedFuel: Number(rule.fixedFuel || 0),
giftId: String(rule.giftId || "").trim(), giftId: String(rule.giftId || "").trim(),
giftTypeCode: String(rule.giftTypeCode || "").trim(), giftTypeCode: String(rule.giftTypeCode || "").trim(),
multiplierPpm: Number(rule.multiplierPpm || 0), multiplierPpm: Number(rule.multiplierPpm || 0),
ruleId: String(rule.ruleId || "").trim() || `gift_energy_rule_${index + 1}`, ruleId: String(rule.ruleId || "").trim() || `gift_fuel_rule_${index + 1}`,
})), })),
levels: form.levels levels: form.levels
.map((level) => ({ .map((level) => ({
animationUrl: String(level.animationUrl || "").trim(), animationUrl: String(level.animationUrl || "").trim(),
coverUrl: String(level.coverUrl || "").trim(), coverUrl: String(level.coverUrl || "").trim(),
energyThreshold: Number(level.energyThreshold || 0), fuelThreshold: Number(level.fuelThreshold || 0),
igniterRewards: rewardsFromForm(level.igniterRewards, level.level, "igniter"), igniterRewards: rewardsFromForm(level.igniterRewards, level.level, "igniter"),
inRoomRewards: rewardsFromForm(level.inRoomRewards, level.level, "in_room"), inRoomRewards: rewardsFromForm(level.inRoomRewards, level.level, "in_room"),
level: Number(level.level), level: Number(level.level),
openedImageUrl: String(level.openedImageUrl || "").trim(), launchedImageUrl: String(level.launchedImageUrl || "").trim(),
openingAnimationUrl: String(level.openingAnimationUrl || "").trim(), launchAnimationUrl: String(level.launchAnimationUrl || "").trim(),
top1Rewards: rewardsFromForm(level.top1Rewards, level.level, "top1"), top1Rewards: rewardsFromForm(level.top1Rewards, level.level, "top1"),
})) }))
.sort((left, right) => left.level - right.level), .sort((left, right) => left.level - right.level),
openDelayMs: Number(form.openDelayMs || 0), launchDelayMs: Number(form.launchDelayMs || 0),
rewardStackPolicy: form.rewardStackPolicy, rewardStackPolicy: form.rewardStackPolicy,
}; };
} }

View File

@ -2,10 +2,10 @@ import { useSearchParams } from "react-router-dom";
import { DataState } from "@/shared/ui/DataState.jsx"; import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx";
import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx"; import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
import { RoomTreasureConfigEditor } from "@/features/room-treasure/components/RoomTreasureConfigEditor.jsx"; import { RoomRocketConfigEditor } from "@/features/room-rocket/components/RoomRocketConfigEditor.jsx";
import { RoomTreasureConfigSummary } from "@/features/room-treasure/components/RoomTreasureConfigSummary.jsx"; import { RoomRocketConfigSummary } from "@/features/room-rocket/components/RoomRocketConfigSummary.jsx";
import { useRoomTreasurePage } from "@/features/room-treasure/hooks/useRoomTreasurePage.js"; import { useRoomRocketPage } from "@/features/room-rocket/hooks/useRoomRocketPage.js";
import styles from "@/features/room-treasure/room-treasure.module.css"; import styles from "@/features/room-rocket/room-rocket.module.css";
const columns = [ const columns = [
{ {
@ -15,7 +15,7 @@ const columns = [
render: (level) => ( render: (level) => (
<div className={styles.stack}> <div className={styles.stack}>
<span className={styles.name}>L{level.level}</span> <span className={styles.name}>L{level.level}</span>
<span className={styles.meta}>阈值 {formatNumber(level.energyThreshold)}</span> <span className={styles.meta}>阈值 {formatNumber(level.fuelThreshold)}</span>
</div> </div>
), ),
}, },
@ -49,8 +49,8 @@ const columns = [
}, },
]; ];
export function RoomTreasurePage() { export function RoomRocketPage() {
const page = useRoomTreasurePage(); const page = useRoomRocketPage();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const editing = searchParams.get("mode") === "edit"; const editing = searchParams.get("mode") === "edit";
@ -68,7 +68,7 @@ export function RoomTreasurePage() {
return ( return (
<AdminListPage> <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}>
<RoomTreasureConfigEditor <RoomRocketConfigEditor
abilities={page.abilities} abilities={page.abilities}
configLoading={page.configLoading} configLoading={page.configLoading}
configSaving={page.configSaving} configSaving={page.configSaving}
@ -85,7 +85,7 @@ export function RoomTreasurePage() {
return ( return (
<AdminListPage> <AdminListPage>
<RoomTreasureConfigSummary <RoomRocketConfigSummary
canUpdate={page.abilities.canUpdate} canUpdate={page.abilities.canUpdate}
config={page.config} config={page.config}
configLoading={page.configLoading} configLoading={page.configLoading}
@ -111,8 +111,8 @@ function MaterialState({ level }) {
const items = [ const items = [
["封面", level.coverUrl], ["封面", level.coverUrl],
["动效", level.animationUrl], ["动效", level.animationUrl],
["开箱", level.openingAnimationUrl], ["发射", level.launchAnimationUrl],
["打开图", level.openedImageUrl], ["发射后图", level.launchedImageUrl],
]; ];
return ( return (
<div className={styles.assetBadges}> <div className={styles.assetBadges}>

View File

@ -1,11 +1,11 @@
import { useAuth } from "@/app/auth/AuthProvider.jsx"; import { useAuth } from "@/app/auth/AuthProvider.jsx";
import { PERMISSIONS } from "@/app/permissions"; import { PERMISSIONS } from "@/app/permissions";
export function useRoomTreasureAbilities() { export function useRoomRocketAbilities() {
const { can } = useAuth(); const { can } = useAuth();
return { return {
canUpdate: can(PERMISSIONS.roomTreasureUpdate), canUpdate: can(PERMISSIONS.roomRocketUpdate),
canView: can(PERMISSIONS.roomTreasureView), canView: can(PERMISSIONS.roomRocketView),
}; };
} }

View File

@ -246,7 +246,7 @@
} }
.rewardPanelHead, .rewardPanelHead,
.energyRuleToolbar { .fuelRuleToolbar {
display: flex; display: flex;
min-width: 0; min-width: 0;
align-items: center; align-items: center;
@ -255,8 +255,8 @@
} }
.rewardTable, .rewardTable,
.energyRuleEditor, .fuelRuleEditor,
.energyRuleTable { .fuelRuleTable {
display: grid; display: grid;
min-width: 0; min-width: 0;
gap: 10px; gap: 10px;
@ -272,7 +272,7 @@
} }
.rewardTableHead, .rewardTableHead,
.energyRuleTableHead { .fuelRuleTableHead {
align-items: center; align-items: center;
color: var(--text-secondary); color: var(--text-secondary);
font-size: 12px; font-size: 12px;
@ -280,13 +280,13 @@
} }
.rewardTableRow, .rewardTableRow,
.energyRuleTableRow { .fuelRuleTableRow {
padding: 12px 0; padding: 12px 0;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.energyRuleTableHead, .fuelRuleTableHead,
.energyRuleTableRow { .fuelRuleTableRow {
display: grid; display: grid;
min-width: 0; min-width: 0;
grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) 130px 130px 120px var(--control-height); grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) 130px 130px 120px var(--control-height);
@ -384,7 +384,7 @@
.editorTitleGroup, .editorTitleGroup,
.editorActions, .editorActions,
.rewardPanelHead, .rewardPanelHead,
.energyRuleToolbar { .fuelRuleToolbar {
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;
} }
@ -403,8 +403,8 @@
.materialGrid, .materialGrid,
.rewardTableHead, .rewardTableHead,
.rewardTableRow, .rewardTableRow,
.energyRuleTableHead, .fuelRuleTableHead,
.energyRuleTableRow { .fuelRuleTableRow {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View File

@ -0,0 +1,12 @@
import { MENU_CODES, PERMISSIONS } from "@/app/permissions";
export const roomRocketRoutes = [
{
label: "房间火箭",
loader: () => import("./pages/RoomRocketPage.jsx").then((module) => module.RoomRocketPage),
menuCode: MENU_CODES.roomRocket,
pageKey: "room-rocket",
path: "/activities/room-rocket",
permission: PERMISSIONS.roomRocketView,
},
];

View File

@ -13,39 +13,39 @@ const rewardSchema = z.object({
const levelSchema = z.object({ const levelSchema = z.object({
animationUrl: z.string().optional(), animationUrl: z.string().optional(),
coverUrl: z.string().optional(), coverUrl: z.string().optional(),
energyThreshold: numberInput, fuelThreshold: numberInput,
igniterRewards: z.array(rewardSchema), igniterRewards: z.array(rewardSchema),
inRoomRewards: z.array(rewardSchema), inRoomRewards: z.array(rewardSchema),
level: z.number().int().min(1).max(7), level: z.number().int().min(1).max(5),
openedImageUrl: z.string().optional(), launchedImageUrl: z.string().optional(),
openingAnimationUrl: z.string().optional(), launchAnimationUrl: z.string().optional(),
top1Rewards: z.array(rewardSchema), top1Rewards: z.array(rewardSchema),
}); });
const giftEnergyRuleSchema = z.object({ const giftFuelRuleSchema = z.object({
excluded: z.boolean(), excluded: z.boolean(),
fixedEnergy: numberInput, fixedFuel: numberInput,
giftId: z.string().optional(), giftId: z.string().optional(),
giftTypeCode: z.string().optional(), giftTypeCode: z.string().optional(),
multiplierPpm: numberInput, multiplierPpm: numberInput,
ruleId: z.string().optional(), ruleId: z.string().optional(),
}); });
export const roomTreasureConfigFormSchema = z export const roomRocketConfigFormSchema = z
.object({ .object({
broadcastDelayMs: numberInput, broadcastDelayMs: numberInput,
broadcastEnabled: z.boolean(), broadcastEnabled: z.boolean(),
broadcastScope: z.enum(["none", "region", "global"]), broadcastScope: z.enum(["none", "region", "global"]),
enabled: z.boolean(), enabled: z.boolean(),
energySource: z.literal("heat_value"), fuelSource: z.literal("heat_value"),
giftEnergyRules: z.array(giftEnergyRuleSchema), giftFuelRules: z.array(giftFuelRuleSchema),
levels: z.array(levelSchema).length(7, "必须配置 7 级宝箱"), levels: z.array(levelSchema).length(5, "必须配置 5 级火箭"),
openDelayMs: numberInput, launchDelayMs: numberInput,
rewardStackPolicy: z.enum(["allow_stack", "priority_only"]), rewardStackPolicy: z.enum(["allow_stack", "priority_only"]),
}) })
.superRefine((value, context) => { .superRefine((value, context) => {
if (!validNonNegativeInteger(value.openDelayMs)) { if (!validNonNegativeInteger(value.launchDelayMs)) {
context.addIssue({ code: "custom", message: "开箱倒计时必须大于等于 0", path: ["openDelayMs"] }); context.addIssue({ code: "custom", message: "发射倒计时必须大于等于 0", path: ["launchDelayMs"] });
} }
if (!validNonNegativeInteger(value.broadcastDelayMs)) { if (!validNonNegativeInteger(value.broadcastDelayMs)) {
context.addIssue({ code: "custom", message: "广播延迟必须大于等于 0", path: ["broadcastDelayMs"] }); context.addIssue({ code: "custom", message: "广播延迟必须大于等于 0", path: ["broadcastDelayMs"] });
@ -54,14 +54,14 @@ export const roomTreasureConfigFormSchema = z
const seen = new Set<number>(); const seen = new Set<number>();
for (const level of value.levels) { for (const level of value.levels) {
if (seen.has(level.level)) { if (seen.has(level.level)) {
context.addIssue({ code: "custom", message: "宝箱等级不能重复", path: ["levels"] }); context.addIssue({ code: "custom", message: "火箭等级不能重复", path: ["levels"] });
} }
seen.add(level.level); seen.add(level.level);
if (!positiveInteger(level.energyThreshold)) { if (!positiveInteger(level.fuelThreshold)) {
context.addIssue({ context.addIssue({
code: "custom", code: "custom",
message: `${level.level}能量阈值必须大于 0`, message: `${level.level}燃料阈值必须大于 0`,
path: ["levels", level.level - 1, "energyThreshold"], path: ["levels", level.level - 1, "fuelThreshold"],
}); });
} }
validateRewards(context, level.level, "inRoomRewards", level.inRoomRewards); validateRewards(context, level.level, "inRoomRewards", level.inRoomRewards);
@ -69,36 +69,36 @@ export const roomTreasureConfigFormSchema = z
validateRewards(context, level.level, "igniterRewards", level.igniterRewards); validateRewards(context, level.level, "igniterRewards", level.igniterRewards);
} }
value.giftEnergyRules.forEach((rule, index) => { value.giftFuelRules.forEach((rule, index) => {
const giftId = String(rule.giftId || "").trim(); const giftId = String(rule.giftId || "").trim();
const giftTypeCode = String(rule.giftTypeCode || "").trim(); const giftTypeCode = String(rule.giftTypeCode || "").trim();
const multiplierPpm = Number(rule.multiplierPpm || 0); const multiplierPpm = Number(rule.multiplierPpm || 0);
const fixedEnergy = Number(rule.fixedEnergy || 0); const fixedFuel = Number(rule.fixedFuel || 0);
if (!giftId && !giftTypeCode) { if (!giftId && !giftTypeCode) {
context.addIssue({ context.addIssue({
code: "custom", code: "custom",
message: "礼物能量规则必须填写礼物 ID 或礼物类型", message: "礼物燃料规则必须填写礼物 ID 或礼物类型",
path: ["giftEnergyRules", index, "giftId"], path: ["giftFuelRules", index, "giftId"],
}); });
} }
if (!Number.isInteger(multiplierPpm) || multiplierPpm < 0 || !Number.isInteger(fixedEnergy) || fixedEnergy < 0) { if (!Number.isInteger(multiplierPpm) || multiplierPpm < 0 || !Number.isInteger(fixedFuel) || fixedFuel < 0) {
context.addIssue({ context.addIssue({
code: "custom", code: "custom",
message: "礼物能量规则数值必须大于等于 0", message: "礼物燃料规则数值必须大于等于 0",
path: ["giftEnergyRules", index, "multiplierPpm"], path: ["giftFuelRules", index, "multiplierPpm"],
}); });
} }
if (!rule.excluded && multiplierPpm === 0 && fixedEnergy === 0) { if (!rule.excluded && multiplierPpm === 0 && fixedFuel === 0) {
context.addIssue({ context.addIssue({
code: "custom", code: "custom",
message: "非排除规则必须配置倍率或固定能量", message: "非排除规则必须配置倍率或固定燃料",
path: ["giftEnergyRules", index, "multiplierPpm"], path: ["giftFuelRules", index, "multiplierPpm"],
}); });
} }
}); });
}); });
export type RoomTreasureConfigForm = z.infer<typeof roomTreasureConfigFormSchema>; 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) => { rewards.forEach((reward, index) => {

View File

@ -1,12 +0,0 @@
import { MENU_CODES, PERMISSIONS } from "@/app/permissions";
export const roomTreasureRoutes = [
{
label: "房间宝箱",
loader: () => import("./pages/RoomTreasurePage.jsx").then((module) => module.RoomTreasurePage),
menuCode: MENU_CODES.roomTreasure,
pageKey: "room-treasure",
path: "/activities/room-treasure",
permission: PERMISSIONS.roomTreasureView,
},
];

View File

@ -98,7 +98,7 @@ export const API_OPERATIONS = {
getResourceGroup: "getResourceGroup", getResourceGroup: "getResourceGroup",
getRoleDataScopes: "getRoleDataScopes", getRoleDataScopes: "getRoleDataScopes",
getRoomConfig: "getRoomConfig", getRoomConfig: "getRoomConfig",
getRoomTreasureConfig: "getRoomTreasureConfig", getRoomRocketConfig: "getRoomRocketConfig",
getSevenDayCheckInConfig: "getSevenDayCheckInConfig", getSevenDayCheckInConfig: "getSevenDayCheckInConfig",
getUser: "getUser", getUser: "getUser",
grantResource: "grantResource", grantResource: "grantResource",
@ -204,7 +204,7 @@ export const API_OPERATIONS = {
updateRole: "updateRole", updateRole: "updateRole",
updateRoom: "updateRoom", updateRoom: "updateRoom",
updateRoomConfig: "updateRoomConfig", updateRoomConfig: "updateRoomConfig",
updateRoomTreasureConfig: "updateRoomTreasureConfig", updateRoomRocketConfig: "updateRoomRocketConfig",
updateSevenDayCheckInConfig: "updateSevenDayCheckInConfig", updateSevenDayCheckInConfig: "updateSevenDayCheckInConfig",
updateTaskDefinition: "updateTaskDefinition", updateTaskDefinition: "updateTaskDefinition",
updateTeamSalaryPolicy: "updateTeamSalaryPolicy", updateTeamSalaryPolicy: "updateTeamSalaryPolicy",
@ -815,12 +815,12 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "room-config:view", permission: "room-config:view",
permissions: ["room-config:view"] permissions: ["room-config:view"]
}, },
getRoomTreasureConfig: { getRoomRocketConfig: {
method: "GET", method: "GET",
operationId: API_OPERATIONS.getRoomTreasureConfig, operationId: API_OPERATIONS.getRoomRocketConfig,
path: "/v1/admin/activity/room-treasure/config", path: "/v1/admin/activity/room-rocket/config",
permission: "room-treasure:view", permission: "room-rocket:view",
permissions: ["room-treasure:view"] permissions: ["room-rocket:view"]
}, },
getSevenDayCheckInConfig: { getSevenDayCheckInConfig: {
method: "GET", method: "GET",
@ -1543,12 +1543,12 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "room-config:update", permission: "room-config:update",
permissions: ["room-config:update"] permissions: ["room-config:update"]
}, },
updateRoomTreasureConfig: { updateRoomRocketConfig: {
method: "PUT", method: "PUT",
operationId: API_OPERATIONS.updateRoomTreasureConfig, operationId: API_OPERATIONS.updateRoomRocketConfig,
path: "/v1/admin/activity/room-treasure/config", path: "/v1/admin/activity/room-rocket/config",
permission: "room-treasure:update", permission: "room-rocket:update",
permissions: ["room-treasure:update"] permissions: ["room-rocket:update"]
}, },
updateSevenDayCheckInConfig: { updateSevenDayCheckInConfig: {
method: "PUT", method: "PUT",

View File

@ -292,15 +292,15 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/admin/activity/room-treasure/config": { "/admin/activity/room-rocket/config": {
parameters: { parameters: {
query?: never; query?: never;
header?: never; header?: never;
path?: never; path?: never;
cookie?: never; cookie?: never;
}; };
get: operations["getRoomTreasureConfig"]; get: operations["getRoomRocketConfig"];
put: operations["updateRoomTreasureConfig"]; put: operations["updateRoomRocketConfig"];
post?: never; post?: never;
delete?: never; delete?: never;
options?: never; options?: never;
@ -3038,7 +3038,7 @@ export interface operations {
200: components["responses"]["EmptyResponse"]; 200: components["responses"]["EmptyResponse"];
}; };
}; };
getRoomTreasureConfig: { getRoomRocketConfig: {
parameters: { parameters: {
query?: never; query?: never;
header?: never; header?: never;
@ -3050,7 +3050,7 @@ export interface operations {
200: components["responses"]["EmptyResponse"]; 200: components["responses"]["EmptyResponse"];
}; };
}; };
updateRoomTreasureConfig: { updateRoomRocketConfig: {
parameters: { parameters: {
query?: never; query?: never;
header?: never; header?: never;