火箭重新修改
This commit is contained in:
parent
af9da06615
commit
825fe47367
@ -326,26 +326,26 @@
|
||||
"x-permissions": ["registration-reward:update"]
|
||||
}
|
||||
},
|
||||
"/admin/activity/room-treasure/config": {
|
||||
"/admin/activity/room-rocket/config": {
|
||||
"get": {
|
||||
"operationId": "getRoomTreasureConfig",
|
||||
"operationId": "getRoomRocketConfig",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "room-treasure:view",
|
||||
"x-permissions": ["room-treasure:view"]
|
||||
"x-permission": "room-rocket:view",
|
||||
"x-permissions": ["room-rocket:view"]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "updateRoomTreasureConfig",
|
||||
"operationId": "updateRoomRocketConfig",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "room-treasure:update",
|
||||
"x-permissions": ["room-treasure:update"]
|
||||
"x-permission": "room-rocket:update",
|
||||
"x-permissions": ["room-rocket:update"]
|
||||
}
|
||||
},
|
||||
"/admin/activity/seven-day-checkin/claims": {
|
||||
|
||||
@ -163,7 +163,7 @@ export const fallbackNavigation = [
|
||||
routeNavItem("registration-reward", { icon: CardGiftcardOutlined }),
|
||||
routeNavItem("first-recharge-reward", { icon: CardGiftcardOutlined }),
|
||||
routeNavItem("seven-day-checkin", { icon: EventAvailableOutlined }),
|
||||
routeNavItem("room-treasure", { icon: RedeemOutlined }),
|
||||
routeNavItem("room-rocket", { icon: RedeemOutlined }),
|
||||
routeNavItem("user-leaderboard", { icon: LeaderboardOutlined }),
|
||||
routeNavItem("red-packet", { icon: RedeemOutlined }),
|
||||
routeNavItem("vip-config", { icon: WorkspacePremiumOutlined }),
|
||||
|
||||
@ -127,8 +127,8 @@ export const PERMISSIONS = {
|
||||
sevenDayCheckInUpdate: "seven-day-checkin:update",
|
||||
luckyGiftView: "lucky-gift:view",
|
||||
luckyGiftUpdate: "lucky-gift:update",
|
||||
roomTreasureView: "room-treasure:view",
|
||||
roomTreasureUpdate: "room-treasure:update",
|
||||
roomRocketView: "room-rocket:view",
|
||||
roomRocketUpdate: "room-rocket:update",
|
||||
userLeaderboardView: "user-leaderboard:view",
|
||||
redPacketView: "red-packet:view",
|
||||
redPacketUpdate: "red-packet:update",
|
||||
@ -186,7 +186,7 @@ export const MENU_CODES = {
|
||||
firstRechargeReward: "first-recharge-reward",
|
||||
achievementConfig: "achievement-config",
|
||||
sevenDayCheckIn: "seven-day-checkin",
|
||||
roomTreasure: "room-treasure",
|
||||
roomRocket: "room-rocket",
|
||||
userLeaderboard: "user-leaderboard",
|
||||
redPacket: "red-packet",
|
||||
vipConfig: "vip-config",
|
||||
|
||||
@ -20,7 +20,7 @@ import { regionBlockRoutes } from "@/features/region-blocks/routes.js";
|
||||
import { resourceRoutes } from "@/features/resources/routes.js";
|
||||
import { rolesRoutes } from "@/features/roles/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 { userLeaderboardRoutes } from "@/features/user-leaderboard/routes.js";
|
||||
import { usersRoutes } from "@/features/users/routes.js";
|
||||
@ -42,7 +42,7 @@ export const adminRoutes: AdminRoute[] = [
|
||||
...registrationRewardRoutes,
|
||||
...firstRechargeRewardRoutes,
|
||||
...sevenDayCheckInRoutes,
|
||||
...roomTreasureRoutes,
|
||||
...roomRocketRoutes,
|
||||
...userLeaderboardRoutes,
|
||||
...redPacketRoutes,
|
||||
...vipConfigRoutes,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { API_ENDPOINTS, API_OPERATIONS, apiEndpointPath } from "@/shared/api/generated/endpoints";
|
||||
import { apiRequest } from "@/shared/api/request";
|
||||
|
||||
export interface RoomTreasureRewardItemDto {
|
||||
export interface RoomRocketRewardItemDto {
|
||||
rewardItemId: string;
|
||||
resourceGroupId: number;
|
||||
weight: number;
|
||||
@ -9,60 +9,60 @@ export interface RoomTreasureRewardItemDto {
|
||||
iconUrl?: string;
|
||||
}
|
||||
|
||||
export interface RoomTreasureLevelDto {
|
||||
export interface RoomRocketLevelDto {
|
||||
level: number;
|
||||
energyThreshold: number;
|
||||
fuelThreshold: number;
|
||||
coverUrl?: string;
|
||||
animationUrl?: string;
|
||||
openingAnimationUrl?: string;
|
||||
openedImageUrl?: string;
|
||||
inRoomRewards: RoomTreasureRewardItemDto[];
|
||||
top1Rewards: RoomTreasureRewardItemDto[];
|
||||
igniterRewards: RoomTreasureRewardItemDto[];
|
||||
launchAnimationUrl?: string;
|
||||
launchedImageUrl?: string;
|
||||
inRoomRewards: RoomRocketRewardItemDto[];
|
||||
top1Rewards: RoomRocketRewardItemDto[];
|
||||
igniterRewards: RoomRocketRewardItemDto[];
|
||||
}
|
||||
|
||||
export interface GiftEnergyRuleDto {
|
||||
export interface GiftFuelRuleDto {
|
||||
ruleId: string;
|
||||
giftId?: string;
|
||||
giftTypeCode?: string;
|
||||
multiplierPpm: number;
|
||||
fixedEnergy: number;
|
||||
fixedFuel: number;
|
||||
excluded: boolean;
|
||||
}
|
||||
|
||||
export interface RoomTreasureConfigDto {
|
||||
export interface RoomRocketConfigDto {
|
||||
appCode?: string;
|
||||
enabled: boolean;
|
||||
configVersion: number;
|
||||
energySource: string;
|
||||
openDelayMs: number;
|
||||
fuelSource: string;
|
||||
launchDelayMs: number;
|
||||
broadcastEnabled: boolean;
|
||||
broadcastScope: string;
|
||||
broadcastDelayMs: number;
|
||||
rewardStackPolicy: string;
|
||||
levels: RoomTreasureLevelDto[];
|
||||
giftEnergyRules: GiftEnergyRuleDto[];
|
||||
levels: RoomRocketLevelDto[];
|
||||
giftFuelRules: GiftFuelRuleDto[];
|
||||
updatedByAdminId?: number;
|
||||
createdAtMs?: number;
|
||||
updatedAtMs?: number;
|
||||
}
|
||||
|
||||
export type RoomTreasureConfigPayload = Pick<
|
||||
RoomTreasureConfigDto,
|
||||
export type RoomRocketConfigPayload = Pick<
|
||||
RoomRocketConfigDto,
|
||||
| "broadcastDelayMs"
|
||||
| "broadcastEnabled"
|
||||
| "broadcastScope"
|
||||
| "enabled"
|
||||
| "energySource"
|
||||
| "giftEnergyRules"
|
||||
| "fuelSource"
|
||||
| "giftFuelRules"
|
||||
| "levels"
|
||||
| "openDelayMs"
|
||||
| "launchDelayMs"
|
||||
| "rewardStackPolicy"
|
||||
>;
|
||||
|
||||
type RawReward = RoomTreasureRewardItemDto & Record<string, unknown>;
|
||||
type RawReward = RoomRocketRewardItemDto & Record<string, unknown>;
|
||||
type RawLevel = Omit<
|
||||
RoomTreasureLevelDto,
|
||||
RoomRocketLevelDto,
|
||||
"igniterRewards" | "inRoomRewards" | "top1Rewards"
|
||||
> & {
|
||||
igniterRewards?: RawReward[];
|
||||
@ -72,96 +72,95 @@ type RawLevel = Omit<
|
||||
top1Rewards?: RawReward[];
|
||||
top1_rewards?: RawReward[];
|
||||
} & Record<string, unknown>;
|
||||
type RawGiftEnergyRule = GiftEnergyRuleDto & Record<string, unknown>;
|
||||
type RawConfig = Omit<RoomTreasureConfigDto, "giftEnergyRules" | "levels"> & {
|
||||
giftEnergyRules?: RawGiftEnergyRule[];
|
||||
gift_energy_rules?: RawGiftEnergyRule[];
|
||||
type RawGiftFuelRule = GiftFuelRuleDto & Record<string, unknown>;
|
||||
type RawConfig = Omit<RoomRocketConfigDto, "giftFuelRules" | "levels"> & {
|
||||
giftFuelRules?: RawGiftFuelRule[];
|
||||
levels?: RawLevel[];
|
||||
} & Record<string, unknown>;
|
||||
|
||||
export function getRoomTreasureConfig(): Promise<RoomTreasureConfigDto> {
|
||||
const endpoint = API_ENDPOINTS.getRoomTreasureConfig;
|
||||
return apiRequest<RawConfig>(apiEndpointPath(API_OPERATIONS.getRoomTreasureConfig), {
|
||||
export function getRoomRocketConfig(): Promise<RoomRocketConfigDto> {
|
||||
const endpoint = API_ENDPOINTS.getRoomRocketConfig;
|
||||
return apiRequest<RawConfig>(apiEndpointPath(API_OPERATIONS.getRoomRocketConfig), {
|
||||
method: endpoint.method,
|
||||
}).then(normalizeConfig);
|
||||
}
|
||||
|
||||
export function updateRoomTreasureConfig(payload: RoomTreasureConfigPayload): Promise<RoomTreasureConfigDto> {
|
||||
const endpoint = API_ENDPOINTS.updateRoomTreasureConfig;
|
||||
return apiRequest<RawConfig, RoomTreasureConfigPayload>(apiEndpointPath(API_OPERATIONS.updateRoomTreasureConfig), {
|
||||
export function updateRoomRocketConfig(payload: RoomRocketConfigPayload): Promise<RoomRocketConfigDto> {
|
||||
const endpoint = API_ENDPOINTS.updateRoomRocketConfig;
|
||||
return apiRequest<RawConfig, RoomRocketConfigPayload>(apiEndpointPath(API_OPERATIONS.updateRoomRocketConfig), {
|
||||
body: payload,
|
||||
method: endpoint.method,
|
||||
}).then(normalizeConfig);
|
||||
}
|
||||
|
||||
function normalizeConfig(item: RawConfig): RoomTreasureConfigDto {
|
||||
function normalizeConfig(item: RawConfig): RoomRocketConfigDto {
|
||||
return {
|
||||
appCode: stringValue(item.appCode ?? item.app_code),
|
||||
enabled: Boolean(item.enabled),
|
||||
configVersion: numberValue(item.configVersion ?? item.config_version),
|
||||
energySource: normalizeEnergySource(item.energySource ?? item.energy_source),
|
||||
openDelayMs: numberValue(item.openDelayMs ?? item.open_delay_ms),
|
||||
fuelSource: normalizeFuelSource(item.fuelSource),
|
||||
launchDelayMs: numberValue(item.launchDelayMs ?? item.launch_delay_ms),
|
||||
broadcastEnabled: Boolean(item.broadcastEnabled ?? item.broadcast_enabled),
|
||||
broadcastScope: stringValue(item.broadcastScope ?? item.broadcast_scope) || "region",
|
||||
broadcastDelayMs: numberValue(item.broadcastDelayMs ?? item.broadcast_delay_ms),
|
||||
rewardStackPolicy: stringValue(item.rewardStackPolicy ?? item.reward_stack_policy) || "allow_stack",
|
||||
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),
|
||||
createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms),
|
||||
updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeEnergySource(value: unknown) {
|
||||
function normalizeFuelSource(value: unknown) {
|
||||
const source = stringValue(value);
|
||||
if (!source || source === "gift_point_added") {
|
||||
if (!source) {
|
||||
return "heat_value";
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
function normalizeLevels(levels?: RawLevel[]): RoomTreasureLevelDto[] {
|
||||
const byLevel = new Map<number, RoomTreasureLevelDto>();
|
||||
function normalizeLevels(levels?: RawLevel[]): RoomRocketLevelDto[] {
|
||||
const byLevel = new Map<number, RoomRocketLevelDto>();
|
||||
for (const level of levels || []) {
|
||||
const levelNo = numberValue(level.level);
|
||||
if (levelNo < 1 || levelNo > 7) {
|
||||
if (levelNo < 1 || levelNo > 5) {
|
||||
continue;
|
||||
}
|
||||
byLevel.set(levelNo, {
|
||||
level: levelNo,
|
||||
energyThreshold: numberValue(level.energyThreshold ?? level.energy_threshold),
|
||||
fuelThreshold: numberValue(level.fuelThreshold),
|
||||
coverUrl: stringValue(level.coverUrl ?? level.cover_url),
|
||||
animationUrl: stringValue(level.animationUrl ?? level.animation_url),
|
||||
openingAnimationUrl: stringValue(level.openingAnimationUrl ?? level.opening_animation_url),
|
||||
openedImageUrl: stringValue(level.openedImageUrl ?? level.opened_image_url),
|
||||
launchAnimationUrl: stringValue(level.launchAnimationUrl ?? level.launch_animation_url),
|
||||
launchedImageUrl: stringValue(level.launchedImageUrl ?? level.launched_image_url),
|
||||
inRoomRewards: normalizeRewards(level.inRoomRewards ?? level.in_room_rewards),
|
||||
top1Rewards: normalizeRewards(level.top1Rewards ?? level.top1_rewards),
|
||||
igniterRewards: normalizeRewards(level.igniterRewards ?? level.igniter_rewards),
|
||||
});
|
||||
}
|
||||
return Array.from({ length: 7 }, (_, index) => {
|
||||
return Array.from({ length: 5 }, (_, index) => {
|
||||
const level = index + 1;
|
||||
return byLevel.get(level) || defaultLevel(level);
|
||||
});
|
||||
}
|
||||
|
||||
function defaultLevel(level: number): RoomTreasureLevelDto {
|
||||
const thresholds = [1000, 3000, 6000, 10000, 15000, 21000, 28000];
|
||||
function defaultLevel(level: number): RoomRocketLevelDto {
|
||||
const thresholds = [1000, 3000, 6000, 10000, 15000];
|
||||
return {
|
||||
level,
|
||||
energyThreshold: thresholds[level - 1] || 0,
|
||||
fuelThreshold: thresholds[level - 1] || 0,
|
||||
coverUrl: "",
|
||||
animationUrl: "",
|
||||
openingAnimationUrl: "",
|
||||
openedImageUrl: "",
|
||||
launchAnimationUrl: "",
|
||||
launchedImageUrl: "",
|
||||
inRoomRewards: [],
|
||||
top1Rewards: [],
|
||||
igniterRewards: [],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRewards(rewards?: RawReward[]): RoomTreasureRewardItemDto[] {
|
||||
function normalizeRewards(rewards?: RawReward[]): RoomRocketRewardItemDto[] {
|
||||
return (rewards || [])
|
||||
.map((reward, index) => ({
|
||||
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);
|
||||
}
|
||||
|
||||
function normalizeGiftEnergyRules(rules?: RawGiftEnergyRule[]): GiftEnergyRuleDto[] {
|
||||
function normalizeGiftFuelRules(rules?: RawGiftFuelRule[]): GiftFuelRuleDto[] {
|
||||
return (rules || [])
|
||||
.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),
|
||||
giftTypeCode: stringValue(rule.giftTypeCode ?? rule.gift_type_code),
|
||||
multiplierPpm: numberValue(rule.multiplierPpm ?? rule.multiplier_ppm),
|
||||
fixedEnergy: numberValue(rule.fixedEnergy ?? rule.fixed_energy),
|
||||
fixedFuel: numberValue(rule.fixedFuel),
|
||||
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) {
|
||||
@ -13,9 +13,9 @@ import { Button } from "@/shared/ui/Button.jsx";
|
||||
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
||||
import { ResourceGroupSelectField } from "@/shared/ui/ResourceGroupSelectDrawer.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", "礼物贡献"],
|
||||
];
|
||||
|
||||
@ -36,7 +36,7 @@ const rewardTabs = [
|
||||
["igniterRewards", "点火人"],
|
||||
];
|
||||
|
||||
export function RoomTreasureConfigEditor({
|
||||
export function RoomRocketConfigEditor({
|
||||
abilities,
|
||||
configLoading,
|
||||
configSaving,
|
||||
@ -64,8 +64,8 @@ export function RoomTreasureConfigEditor({
|
||||
返回
|
||||
</Button>
|
||||
<div className={styles.stack}>
|
||||
<span className={styles.title}>房间宝箱配置</span>
|
||||
<span className={styles.meta}>编辑 7 级宝箱物料、奖励池和礼物能量规则</span>
|
||||
<span className={styles.title}>房间火箭配置</span>
|
||||
<span className={styles.meta}>编辑 5 级火箭物料、奖励池和礼物燃料规则</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.editorActions}>
|
||||
@ -85,30 +85,30 @@ export function RoomTreasureConfigEditor({
|
||||
<SwitchField
|
||||
checked={form.enabled}
|
||||
disabled={disabled}
|
||||
label="宝箱状态"
|
||||
label="火箭状态"
|
||||
onChange={(event) => updateRoot(setForm, "enabled", event.target.checked)}
|
||||
/>
|
||||
<SwitchField
|
||||
checked={form.broadcastEnabled}
|
||||
disabled={disabled}
|
||||
label="开箱广播"
|
||||
label="发射广播"
|
||||
onChange={(event) => updateRoot(setForm, "broadcastEnabled", event.target.checked)}
|
||||
/>
|
||||
<SelectField
|
||||
disabled={disabled}
|
||||
label="能量来源"
|
||||
options={energySourceOptions}
|
||||
value={form.energySource}
|
||||
onChange={(value) => updateRoot(setForm, "energySource", value)}
|
||||
label="燃料来源"
|
||||
options={fuelSourceOptions}
|
||||
value={form.fuelSource}
|
||||
onChange={(value) => updateRoot(setForm, "fuelSource", value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
disabled={disabled}
|
||||
inputProps={{ min: 0, step: 1000 }}
|
||||
label="开箱倒计时 ms"
|
||||
label="发射倒计时 ms"
|
||||
type="number"
|
||||
value={form.openDelayMs}
|
||||
onChange={(event) => updateRoot(setForm, "openDelayMs", event.target.value)}
|
||||
value={form.launchDelayMs}
|
||||
onChange={(event) => updateRoot(setForm, "launchDelayMs", event.target.value)}
|
||||
/>
|
||||
<SelectField
|
||||
disabled={disabled}
|
||||
@ -139,7 +139,7 @@ export function RoomTreasureConfigEditor({
|
||||
<section className={styles.editorSection}>
|
||||
<Tabs value={editorTab} onChange={(_, value) => setEditorTab(value)}>
|
||||
<Tab label="等级配置" value="levels" />
|
||||
<Tab label="礼物能量规则" value="energyRules" />
|
||||
<Tab label="礼物燃料规则" value="fuelRules" />
|
||||
</Tabs>
|
||||
{editorTab === "levels" ? (
|
||||
<div className={styles.levelWorkspace}>
|
||||
@ -161,17 +161,17 @@ export function RoomTreasureConfigEditor({
|
||||
<div className={styles.levelDetailHeader}>
|
||||
<div className={styles.stack}>
|
||||
<span className={styles.title}>L{level.level}</span>
|
||||
<span className={styles.meta}>当前等级送礼溢出不进入下一级,倒计时期间送礼不累计能量</span>
|
||||
<span className={styles.meta}>当前等级溢出燃料作废,点火后下一等级立即开始</span>
|
||||
</div>
|
||||
<TextField
|
||||
required
|
||||
disabled={disabled}
|
||||
inputProps={{ min: 1, step: 1 }}
|
||||
label="能量阈值"
|
||||
label="燃料阈值"
|
||||
type="number"
|
||||
value={level.energyThreshold}
|
||||
value={level.fuelThreshold}
|
||||
onChange={(event) =>
|
||||
updateLevel(setForm, activeLevelIndex, { energyThreshold: event.target.value })
|
||||
updateLevel(setForm, activeLevelIndex, { fuelThreshold: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -186,7 +186,7 @@ export function RoomTreasureConfigEditor({
|
||||
<UploadField
|
||||
disabled={disabled}
|
||||
kind="file"
|
||||
label="宝箱动效"
|
||||
label="火箭动效"
|
||||
uploadKind="file"
|
||||
value={level.animationUrl}
|
||||
onChange={(value) => updateLevel(setForm, activeLevelIndex, { animationUrl: value })}
|
||||
@ -194,19 +194,19 @@ export function RoomTreasureConfigEditor({
|
||||
<UploadField
|
||||
disabled={disabled}
|
||||
kind="file"
|
||||
label="开箱动效"
|
||||
label="发射动效"
|
||||
uploadKind="file"
|
||||
value={level.openingAnimationUrl}
|
||||
value={level.launchAnimationUrl}
|
||||
onChange={(value) =>
|
||||
updateLevel(setForm, activeLevelIndex, { openingAnimationUrl: value })
|
||||
updateLevel(setForm, activeLevelIndex, { launchAnimationUrl: value })
|
||||
}
|
||||
/>
|
||||
<UploadField
|
||||
disabled={disabled}
|
||||
kind="image"
|
||||
label="打开图"
|
||||
value={level.openedImageUrl}
|
||||
onChange={(value) => updateLevel(setForm, activeLevelIndex, { openedImageUrl: value })}
|
||||
label="发射后图"
|
||||
value={level.launchedImageUrl}
|
||||
onChange={(value) => updateLevel(setForm, activeLevelIndex, { launchedImageUrl: value })}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.rewardPanel}>
|
||||
@ -242,7 +242,7 @@ export function RoomTreasureConfigEditor({
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<EnergyRuleEditor disabled={disabled} form={form} setForm={setForm} />
|
||||
<FuelRuleEditor disabled={disabled} form={form} setForm={setForm} />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
@ -254,15 +254,15 @@ function LevelNavItem({ active, level, onClick }) {
|
||||
const materialCount = [
|
||||
level.coverUrl,
|
||||
level.animationUrl,
|
||||
level.openingAnimationUrl,
|
||||
level.openedImageUrl,
|
||||
level.launchAnimationUrl,
|
||||
level.launchedImageUrl,
|
||||
].filter(Boolean).length;
|
||||
const rewardCount = rewardTabs.filter(([key]) => (level[key] || []).length > 0).length;
|
||||
|
||||
return (
|
||||
<button className={[styles.levelNavItem, active ? styles.levelNavItemActive : ""].join(" ")} type="button" onClick={onClick}>
|
||||
<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>
|
||||
</button>
|
||||
);
|
||||
@ -334,38 +334,38 @@ function RewardTable({ disabled, levelIndex, poolKey, resourceGroups, rewards, s
|
||||
);
|
||||
}
|
||||
|
||||
function EnergyRuleEditor({ disabled, form, setForm }) {
|
||||
function FuelRuleEditor({ disabled, form, setForm }) {
|
||||
return (
|
||||
<div className={styles.energyRuleEditor}>
|
||||
<div className={styles.energyRuleToolbar}>
|
||||
<span className={styles.meta}>未命中特殊规则的礼物按基础能量来源计算</span>
|
||||
<Button disabled={disabled} startIcon={<AddOutlined fontSize="small" />} type="button" onClick={() => addEnergyRule(setForm)}>
|
||||
<div className={styles.fuelRuleEditor}>
|
||||
<div className={styles.fuelRuleToolbar}>
|
||||
<span className={styles.meta}>未命中特殊规则的礼物按基础燃料来源计算</span>
|
||||
<Button disabled={disabled} startIcon={<AddOutlined fontSize="small" />} type="button" onClick={() => addFuelRule(setForm)}>
|
||||
添加规则
|
||||
</Button>
|
||||
</div>
|
||||
{form.giftEnergyRules.length ? (
|
||||
<div className={styles.energyRuleTable}>
|
||||
<div className={styles.energyRuleTableHead}>
|
||||
{form.giftFuelRules.length ? (
|
||||
<div className={styles.fuelRuleTable}>
|
||||
<div className={styles.fuelRuleTableHead}>
|
||||
<span>礼物 ID</span>
|
||||
<span>礼物类型</span>
|
||||
<span>倍率 ppm</span>
|
||||
<span>固定能量</span>
|
||||
<span>固定燃料</span>
|
||||
<span>排除</span>
|
||||
<span />
|
||||
</div>
|
||||
{form.giftEnergyRules.map((rule, index) => (
|
||||
<div className={styles.energyRuleTableRow} key={rule.ruleId || index}>
|
||||
{form.giftFuelRules.map((rule, index) => (
|
||||
<div className={styles.fuelRuleTableRow} key={rule.ruleId || index}>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="礼物 ID"
|
||||
value={rule.giftId}
|
||||
onChange={(event) => updateEnergyRule(setForm, index, { giftId: event.target.value })}
|
||||
onChange={(event) => updateFuelRule(setForm, index, { giftId: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="礼物类型"
|
||||
value={rule.giftTypeCode}
|
||||
onChange={(event) => updateEnergyRule(setForm, index, { giftTypeCode: event.target.value })}
|
||||
onChange={(event) => updateFuelRule(setForm, index, { giftTypeCode: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
@ -373,21 +373,21 @@ function EnergyRuleEditor({ disabled, form, setForm }) {
|
||||
label="倍率 ppm"
|
||||
type="number"
|
||||
value={rule.multiplierPpm}
|
||||
onChange={(event) => updateEnergyRule(setForm, index, { multiplierPpm: event.target.value })}
|
||||
onChange={(event) => updateFuelRule(setForm, index, { multiplierPpm: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label="固定能量"
|
||||
label="固定燃料"
|
||||
type="number"
|
||||
value={rule.fixedEnergy}
|
||||
onChange={(event) => updateEnergyRule(setForm, index, { fixedEnergy: event.target.value })}
|
||||
value={rule.fixedFuel}
|
||||
onChange={(event) => updateFuelRule(setForm, index, { fixedFuel: event.target.value })}
|
||||
/>
|
||||
<SwitchField
|
||||
checked={rule.excluded}
|
||||
disabled={disabled}
|
||||
label="排除"
|
||||
onChange={(event) => updateEnergyRule(setForm, index, { excluded: event.target.checked })}
|
||||
onChange={(event) => updateFuelRule(setForm, index, { excluded: event.target.checked })}
|
||||
/>
|
||||
<Tooltip arrow title="删除规则">
|
||||
<span>
|
||||
@ -396,7 +396,7 @@ function EnergyRuleEditor({ disabled, form, setForm }) {
|
||||
label="删除规则"
|
||||
tone="danger"
|
||||
type="button"
|
||||
onClick={() => removeEnergyRule(setForm, index)}
|
||||
onClick={() => removeFuelRule(setForm, index)}
|
||||
>
|
||||
<DeleteOutlineOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
@ -501,36 +501,36 @@ function removeReward(setForm, levelIndex, poolKey, rewardIndex) {
|
||||
}));
|
||||
}
|
||||
|
||||
function addEnergyRule(setForm) {
|
||||
function addFuelRule(setForm) {
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
giftEnergyRules: [
|
||||
...current.giftEnergyRules,
|
||||
giftFuelRules: [
|
||||
...current.giftFuelRules,
|
||||
{
|
||||
excluded: false,
|
||||
fixedEnergy: "0",
|
||||
fixedFuel: "0",
|
||||
giftId: "",
|
||||
giftTypeCode: "",
|
||||
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) => ({
|
||||
...current,
|
||||
giftEnergyRules: current.giftEnergyRules.map((rule, itemIndex) =>
|
||||
giftFuelRules: current.giftFuelRules.map((rule, itemIndex) =>
|
||||
itemIndex === index ? { ...rule, ...patch } : rule,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
function removeEnergyRule(setForm, index) {
|
||||
function removeFuelRule(setForm, index) {
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
giftEnergyRules: current.giftEnergyRules.filter((_, itemIndex) => itemIndex !== index),
|
||||
giftFuelRules: current.giftFuelRules.filter((_, itemIndex) => itemIndex !== index),
|
||||
}));
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
||||
import { Button } from "@/shared/ui/Button.jsx";
|
||||
import { AdminActionIconButton } from "@/shared/ui/AdminListLayout.jsx";
|
||||
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 = {
|
||||
global: "全局广播",
|
||||
@ -16,18 +16,18 @@ const rewardStackPolicyLabels = {
|
||||
priority_only: "优先级单份",
|
||||
};
|
||||
|
||||
export function RoomTreasureConfigSummary({ canUpdate, config, configLoading, onEdit, onRefresh }) {
|
||||
export function RoomRocketConfigSummary({ canUpdate, config, configLoading, onEdit, onRefresh }) {
|
||||
return (
|
||||
<section className={styles.summaryPanel}>
|
||||
<div className={styles.summaryScrollArea}>
|
||||
<div className={styles.summaryContent}>
|
||||
<div className={styles.summaryHeader}>
|
||||
<span className={styles.title}>房间宝箱配置</span>
|
||||
<span className={styles.title}>房间火箭配置</span>
|
||||
</div>
|
||||
<div className={styles.summaryItems}>
|
||||
<ConfigStatus config={config} loading={configLoading} />
|
||||
<SummaryItem label="配置版本">{config?.configVersion || "-"}</SummaryItem>
|
||||
<SummaryItem label="开箱倒计时">{formatSeconds(config?.openDelayMs)}</SummaryItem>
|
||||
<SummaryItem label="发射倒计时">{formatSeconds(config?.launchDelayMs)}</SummaryItem>
|
||||
<SummaryItem label="广播">
|
||||
{config?.broadcastEnabled ? broadcastScopeLabels[config.broadcastScope] || config.broadcastScope : "关闭"}
|
||||
</SummaryItem>
|
||||
@ -93,7 +93,7 @@ function LevelPreview({ config, loading }) {
|
||||
{(config?.levels || []).map((level) => (
|
||||
<span className={styles.levelPill} key={level.level}>
|
||||
<span>L{level.level}</span>
|
||||
<span>{formatNumber(level.energyThreshold)}</span>
|
||||
<span>{formatNumber(level.fuelThreshold)}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@ -2,20 +2,20 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { parseForm } from "@/shared/forms/validation";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { listResourceGroups } from "@/features/resources/api";
|
||||
import { getRoomTreasureConfig, updateRoomTreasureConfig } from "@/features/room-treasure/api";
|
||||
import { useRoomTreasureAbilities } from "@/features/room-treasure/permissions.js";
|
||||
import { roomTreasureConfigFormSchema } from "@/features/room-treasure/schema";
|
||||
import { getRoomRocketConfig, updateRoomRocketConfig } from "@/features/room-rocket/api";
|
||||
import { useRoomRocketAbilities } from "@/features/room-rocket/permissions.js";
|
||||
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() {
|
||||
const abilities = useRoomTreasureAbilities();
|
||||
export function useRoomRocketPage() {
|
||||
const abilities = useRoomRocketAbilities();
|
||||
const { showToast } = useToast();
|
||||
const [config, setConfig] = useState(null);
|
||||
const [configError, setConfigError] = useState("");
|
||||
const [configLoading, setConfigLoading] = useState(false);
|
||||
const [configSaving, setConfigSaving] = useState(false);
|
||||
const [form, setForm] = useState(emptyRoomTreasureForm());
|
||||
const [form, setForm] = useState(emptyRoomRocketForm());
|
||||
const [resourceGroups, setResourceGroups] = useState([]);
|
||||
|
||||
const reloadConfig = useCallback(async () => {
|
||||
@ -23,14 +23,14 @@ export function useRoomTreasurePage() {
|
||||
setConfigError("");
|
||||
try {
|
||||
const [nextConfig, groups] = await Promise.all([
|
||||
getRoomTreasureConfig(),
|
||||
getRoomRocketConfig(),
|
||||
listResourceGroups({ page: 1, page_size: 100, status: "active" }),
|
||||
]);
|
||||
setConfig(nextConfig);
|
||||
setForm(formFromConfig(nextConfig));
|
||||
setResourceGroups(groups.items || []);
|
||||
} catch (err) {
|
||||
const message = err.message || "加载房间宝箱配置失败";
|
||||
const message = err.message || "加载房间火箭配置失败";
|
||||
setConfigError(message);
|
||||
showToast(message, "error");
|
||||
} finally {
|
||||
@ -43,7 +43,7 @@ export function useRoomTreasurePage() {
|
||||
}, [reloadConfig]);
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setForm(config ? formFromConfig(config) : emptyRoomTreasureForm());
|
||||
setForm(config ? formFromConfig(config) : emptyRoomRocketForm());
|
||||
}, [config]);
|
||||
|
||||
const submitConfig = async (event, onSaved) => {
|
||||
@ -53,14 +53,14 @@ export function useRoomTreasurePage() {
|
||||
}
|
||||
setConfigSaving(true);
|
||||
try {
|
||||
const parsed = parseForm(roomTreasureConfigFormSchema, form);
|
||||
const saved = await updateRoomTreasureConfig(payloadFromForm(parsed));
|
||||
const parsed = parseForm(roomRocketConfigFormSchema, form);
|
||||
const saved = await updateRoomRocketConfig(payloadFromForm(parsed));
|
||||
setConfig(saved);
|
||||
setForm(formFromConfig(saved));
|
||||
showToast("房间宝箱配置已保存", "success");
|
||||
showToast("房间火箭配置已保存", "success");
|
||||
onSaved?.(saved);
|
||||
} catch (err) {
|
||||
showToast(err.message || "保存房间宝箱配置失败", "error");
|
||||
showToast(err.message || "保存房间火箭配置失败", "error");
|
||||
} finally {
|
||||
setConfigSaving(false);
|
||||
}
|
||||
@ -81,61 +81,61 @@ export function useRoomTreasurePage() {
|
||||
};
|
||||
}
|
||||
|
||||
export function emptyRoomTreasureForm() {
|
||||
export function emptyRoomRocketForm() {
|
||||
return {
|
||||
broadcastDelayMs: "0",
|
||||
broadcastEnabled: true,
|
||||
broadcastScope: "region",
|
||||
enabled: false,
|
||||
energySource: "heat_value",
|
||||
giftEnergyRules: [],
|
||||
levels: Array.from({ length: 7 }, (_, index) => ({
|
||||
fuelSource: "heat_value",
|
||||
giftFuelRules: [],
|
||||
levels: Array.from({ length: 5 }, (_, index) => ({
|
||||
animationUrl: "",
|
||||
coverUrl: "",
|
||||
energyThreshold: String(defaultThresholds[index]),
|
||||
fuelThreshold: String(defaultThresholds[index]),
|
||||
igniterRewards: [],
|
||||
inRoomRewards: [],
|
||||
level: index + 1,
|
||||
openedImageUrl: "",
|
||||
openingAnimationUrl: "",
|
||||
launchedImageUrl: "",
|
||||
launchAnimationUrl: "",
|
||||
top1Rewards: [],
|
||||
})),
|
||||
openDelayMs: "30000",
|
||||
launchDelayMs: "30000",
|
||||
rewardStackPolicy: "allow_stack",
|
||||
};
|
||||
}
|
||||
|
||||
function formFromConfig(config) {
|
||||
const fallback = emptyRoomTreasureForm();
|
||||
const fallback = emptyRoomRocketForm();
|
||||
return {
|
||||
broadcastDelayMs: String(config?.broadcastDelayMs ?? fallback.broadcastDelayMs),
|
||||
broadcastEnabled: Boolean(config?.broadcastEnabled),
|
||||
broadcastScope: config?.broadcastScope || fallback.broadcastScope,
|
||||
enabled: Boolean(config?.enabled),
|
||||
energySource: config?.energySource || fallback.energySource,
|
||||
giftEnergyRules: (config?.giftEnergyRules || []).map((rule, index) => ({
|
||||
fuelSource: config?.fuelSource || fallback.fuelSource,
|
||||
giftFuelRules: (config?.giftFuelRules || []).map((rule, index) => ({
|
||||
excluded: Boolean(rule.excluded),
|
||||
fixedEnergy: String(rule.fixedEnergy || 0),
|
||||
fixedFuel: String(rule.fixedFuel || 0),
|
||||
giftId: rule.giftId || "",
|
||||
giftTypeCode: rule.giftTypeCode || "",
|
||||
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) => {
|
||||
const level = config?.levels?.find((item) => Number(item.level) === fallbackLevel.level) || fallbackLevel;
|
||||
return {
|
||||
animationUrl: level.animationUrl || "",
|
||||
coverUrl: level.coverUrl || "",
|
||||
energyThreshold: String(level.energyThreshold || fallbackLevel.energyThreshold),
|
||||
fuelThreshold: String(level.fuelThreshold || fallbackLevel.fuelThreshold),
|
||||
igniterRewards: rewardsToForm(level.igniterRewards),
|
||||
inRoomRewards: rewardsToForm(level.inRoomRewards),
|
||||
level: fallbackLevel.level,
|
||||
openedImageUrl: level.openedImageUrl || "",
|
||||
openingAnimationUrl: level.openingAnimationUrl || "",
|
||||
launchedImageUrl: level.launchedImageUrl || "",
|
||||
launchAnimationUrl: level.launchAnimationUrl || "",
|
||||
top1Rewards: rewardsToForm(level.top1Rewards),
|
||||
};
|
||||
}),
|
||||
openDelayMs: String(config?.openDelayMs ?? fallback.openDelayMs),
|
||||
launchDelayMs: String(config?.launchDelayMs ?? fallback.launchDelayMs),
|
||||
rewardStackPolicy: config?.rewardStackPolicy || fallback.rewardStackPolicy,
|
||||
};
|
||||
}
|
||||
@ -156,29 +156,29 @@ function payloadFromForm(form) {
|
||||
broadcastEnabled: Boolean(form.broadcastEnabled),
|
||||
broadcastScope: form.broadcastScope,
|
||||
enabled: Boolean(form.enabled),
|
||||
energySource: form.energySource,
|
||||
giftEnergyRules: form.giftEnergyRules.map((rule, index) => ({
|
||||
fuelSource: form.fuelSource,
|
||||
giftFuelRules: form.giftFuelRules.map((rule, index) => ({
|
||||
excluded: Boolean(rule.excluded),
|
||||
fixedEnergy: Number(rule.fixedEnergy || 0),
|
||||
fixedFuel: Number(rule.fixedFuel || 0),
|
||||
giftId: String(rule.giftId || "").trim(),
|
||||
giftTypeCode: String(rule.giftTypeCode || "").trim(),
|
||||
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
|
||||
.map((level) => ({
|
||||
animationUrl: String(level.animationUrl || "").trim(),
|
||||
coverUrl: String(level.coverUrl || "").trim(),
|
||||
energyThreshold: Number(level.energyThreshold || 0),
|
||||
fuelThreshold: Number(level.fuelThreshold || 0),
|
||||
igniterRewards: rewardsFromForm(level.igniterRewards, level.level, "igniter"),
|
||||
inRoomRewards: rewardsFromForm(level.inRoomRewards, level.level, "in_room"),
|
||||
level: Number(level.level),
|
||||
openedImageUrl: String(level.openedImageUrl || "").trim(),
|
||||
openingAnimationUrl: String(level.openingAnimationUrl || "").trim(),
|
||||
launchedImageUrl: String(level.launchedImageUrl || "").trim(),
|
||||
launchAnimationUrl: String(level.launchAnimationUrl || "").trim(),
|
||||
top1Rewards: rewardsFromForm(level.top1Rewards, level.level, "top1"),
|
||||
}))
|
||||
.sort((left, right) => left.level - right.level),
|
||||
openDelayMs: Number(form.openDelayMs || 0),
|
||||
launchDelayMs: Number(form.launchDelayMs || 0),
|
||||
rewardStackPolicy: form.rewardStackPolicy,
|
||||
};
|
||||
}
|
||||
@ -2,10 +2,10 @@ import { useSearchParams } from "react-router-dom";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { RoomTreasureConfigEditor } from "@/features/room-treasure/components/RoomTreasureConfigEditor.jsx";
|
||||
import { RoomTreasureConfigSummary } from "@/features/room-treasure/components/RoomTreasureConfigSummary.jsx";
|
||||
import { useRoomTreasurePage } from "@/features/room-treasure/hooks/useRoomTreasurePage.js";
|
||||
import styles from "@/features/room-treasure/room-treasure.module.css";
|
||||
import { RoomRocketConfigEditor } from "@/features/room-rocket/components/RoomRocketConfigEditor.jsx";
|
||||
import { RoomRocketConfigSummary } from "@/features/room-rocket/components/RoomRocketConfigSummary.jsx";
|
||||
import { useRoomRocketPage } from "@/features/room-rocket/hooks/useRoomRocketPage.js";
|
||||
import styles from "@/features/room-rocket/room-rocket.module.css";
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@ -15,7 +15,7 @@ const columns = [
|
||||
render: (level) => (
|
||||
<div className={styles.stack}>
|
||||
<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>
|
||||
),
|
||||
},
|
||||
@ -49,8 +49,8 @@ const columns = [
|
||||
},
|
||||
];
|
||||
|
||||
export function RoomTreasurePage() {
|
||||
const page = useRoomTreasurePage();
|
||||
export function RoomRocketPage() {
|
||||
const page = useRoomRocketPage();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const editing = searchParams.get("mode") === "edit";
|
||||
|
||||
@ -68,7 +68,7 @@ export function RoomTreasurePage() {
|
||||
return (
|
||||
<AdminListPage>
|
||||
<DataState error={page.configError} loading={page.configLoading && !page.config} onRetry={page.reloadConfig}>
|
||||
<RoomTreasureConfigEditor
|
||||
<RoomRocketConfigEditor
|
||||
abilities={page.abilities}
|
||||
configLoading={page.configLoading}
|
||||
configSaving={page.configSaving}
|
||||
@ -85,7 +85,7 @@ export function RoomTreasurePage() {
|
||||
|
||||
return (
|
||||
<AdminListPage>
|
||||
<RoomTreasureConfigSummary
|
||||
<RoomRocketConfigSummary
|
||||
canUpdate={page.abilities.canUpdate}
|
||||
config={page.config}
|
||||
configLoading={page.configLoading}
|
||||
@ -111,8 +111,8 @@ function MaterialState({ level }) {
|
||||
const items = [
|
||||
["封面", level.coverUrl],
|
||||
["动效", level.animationUrl],
|
||||
["开箱", level.openingAnimationUrl],
|
||||
["打开图", level.openedImageUrl],
|
||||
["发射", level.launchAnimationUrl],
|
||||
["发射后图", level.launchedImageUrl],
|
||||
];
|
||||
return (
|
||||
<div className={styles.assetBadges}>
|
||||
@ -1,11 +1,11 @@
|
||||
import { useAuth } from "@/app/auth/AuthProvider.jsx";
|
||||
import { PERMISSIONS } from "@/app/permissions";
|
||||
|
||||
export function useRoomTreasureAbilities() {
|
||||
export function useRoomRocketAbilities() {
|
||||
const { can } = useAuth();
|
||||
|
||||
return {
|
||||
canUpdate: can(PERMISSIONS.roomTreasureUpdate),
|
||||
canView: can(PERMISSIONS.roomTreasureView),
|
||||
canUpdate: can(PERMISSIONS.roomRocketUpdate),
|
||||
canView: can(PERMISSIONS.roomRocketView),
|
||||
};
|
||||
}
|
||||
@ -246,7 +246,7 @@
|
||||
}
|
||||
|
||||
.rewardPanelHead,
|
||||
.energyRuleToolbar {
|
||||
.fuelRuleToolbar {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
@ -255,8 +255,8 @@
|
||||
}
|
||||
|
||||
.rewardTable,
|
||||
.energyRuleEditor,
|
||||
.energyRuleTable {
|
||||
.fuelRuleEditor,
|
||||
.fuelRuleTable {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 10px;
|
||||
@ -272,7 +272,7 @@
|
||||
}
|
||||
|
||||
.rewardTableHead,
|
||||
.energyRuleTableHead {
|
||||
.fuelRuleTableHead {
|
||||
align-items: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
@ -280,13 +280,13 @@
|
||||
}
|
||||
|
||||
.rewardTableRow,
|
||||
.energyRuleTableRow {
|
||||
.fuelRuleTableRow {
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.energyRuleTableHead,
|
||||
.energyRuleTableRow {
|
||||
.fuelRuleTableHead,
|
||||
.fuelRuleTableRow {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) 130px 130px 120px var(--control-height);
|
||||
@ -384,7 +384,7 @@
|
||||
.editorTitleGroup,
|
||||
.editorActions,
|
||||
.rewardPanelHead,
|
||||
.energyRuleToolbar {
|
||||
.fuelRuleToolbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
@ -403,8 +403,8 @@
|
||||
.materialGrid,
|
||||
.rewardTableHead,
|
||||
.rewardTableRow,
|
||||
.energyRuleTableHead,
|
||||
.energyRuleTableRow {
|
||||
.fuelRuleTableHead,
|
||||
.fuelRuleTableRow {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
12
src/features/room-rocket/routes.js
Normal file
12
src/features/room-rocket/routes.js
Normal 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,
|
||||
},
|
||||
];
|
||||
@ -13,39 +13,39 @@ const rewardSchema = z.object({
|
||||
const levelSchema = z.object({
|
||||
animationUrl: z.string().optional(),
|
||||
coverUrl: z.string().optional(),
|
||||
energyThreshold: numberInput,
|
||||
fuelThreshold: numberInput,
|
||||
igniterRewards: z.array(rewardSchema),
|
||||
inRoomRewards: z.array(rewardSchema),
|
||||
level: z.number().int().min(1).max(7),
|
||||
openedImageUrl: z.string().optional(),
|
||||
openingAnimationUrl: z.string().optional(),
|
||||
level: z.number().int().min(1).max(5),
|
||||
launchedImageUrl: z.string().optional(),
|
||||
launchAnimationUrl: z.string().optional(),
|
||||
top1Rewards: z.array(rewardSchema),
|
||||
});
|
||||
|
||||
const giftEnergyRuleSchema = z.object({
|
||||
const giftFuelRuleSchema = z.object({
|
||||
excluded: z.boolean(),
|
||||
fixedEnergy: numberInput,
|
||||
fixedFuel: numberInput,
|
||||
giftId: z.string().optional(),
|
||||
giftTypeCode: z.string().optional(),
|
||||
multiplierPpm: numberInput,
|
||||
ruleId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const roomTreasureConfigFormSchema = z
|
||||
export const roomRocketConfigFormSchema = z
|
||||
.object({
|
||||
broadcastDelayMs: numberInput,
|
||||
broadcastEnabled: z.boolean(),
|
||||
broadcastScope: z.enum(["none", "region", "global"]),
|
||||
enabled: z.boolean(),
|
||||
energySource: z.literal("heat_value"),
|
||||
giftEnergyRules: z.array(giftEnergyRuleSchema),
|
||||
levels: z.array(levelSchema).length(7, "必须配置 7 级宝箱"),
|
||||
openDelayMs: numberInput,
|
||||
fuelSource: z.literal("heat_value"),
|
||||
giftFuelRules: z.array(giftFuelRuleSchema),
|
||||
levels: z.array(levelSchema).length(5, "必须配置 5 级火箭"),
|
||||
launchDelayMs: numberInput,
|
||||
rewardStackPolicy: z.enum(["allow_stack", "priority_only"]),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
if (!validNonNegativeInteger(value.openDelayMs)) {
|
||||
context.addIssue({ code: "custom", message: "开箱倒计时必须大于等于 0", path: ["openDelayMs"] });
|
||||
if (!validNonNegativeInteger(value.launchDelayMs)) {
|
||||
context.addIssue({ code: "custom", message: "发射倒计时必须大于等于 0", path: ["launchDelayMs"] });
|
||||
}
|
||||
if (!validNonNegativeInteger(value.broadcastDelayMs)) {
|
||||
context.addIssue({ code: "custom", message: "广播延迟必须大于等于 0", path: ["broadcastDelayMs"] });
|
||||
@ -54,14 +54,14 @@ export const roomTreasureConfigFormSchema = z
|
||||
const seen = new Set<number>();
|
||||
for (const level of value.levels) {
|
||||
if (seen.has(level.level)) {
|
||||
context.addIssue({ code: "custom", message: "宝箱等级不能重复", path: ["levels"] });
|
||||
context.addIssue({ code: "custom", message: "火箭等级不能重复", path: ["levels"] });
|
||||
}
|
||||
seen.add(level.level);
|
||||
if (!positiveInteger(level.energyThreshold)) {
|
||||
if (!positiveInteger(level.fuelThreshold)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: `第 ${level.level} 级能量阈值必须大于 0`,
|
||||
path: ["levels", level.level - 1, "energyThreshold"],
|
||||
message: `第 ${level.level} 级燃料阈值必须大于 0`,
|
||||
path: ["levels", level.level - 1, "fuelThreshold"],
|
||||
});
|
||||
}
|
||||
validateRewards(context, level.level, "inRoomRewards", level.inRoomRewards);
|
||||
@ -69,36 +69,36 @@ export const roomTreasureConfigFormSchema = z
|
||||
validateRewards(context, level.level, "igniterRewards", level.igniterRewards);
|
||||
}
|
||||
|
||||
value.giftEnergyRules.forEach((rule, index) => {
|
||||
value.giftFuelRules.forEach((rule, index) => {
|
||||
const giftId = String(rule.giftId || "").trim();
|
||||
const giftTypeCode = String(rule.giftTypeCode || "").trim();
|
||||
const multiplierPpm = Number(rule.multiplierPpm || 0);
|
||||
const fixedEnergy = Number(rule.fixedEnergy || 0);
|
||||
const fixedFuel = Number(rule.fixedFuel || 0);
|
||||
if (!giftId && !giftTypeCode) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "礼物能量规则必须填写礼物 ID 或礼物类型",
|
||||
path: ["giftEnergyRules", index, "giftId"],
|
||||
message: "礼物燃料规则必须填写礼物 ID 或礼物类型",
|
||||
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({
|
||||
code: "custom",
|
||||
message: "礼物能量规则数值必须大于等于 0",
|
||||
path: ["giftEnergyRules", index, "multiplierPpm"],
|
||||
message: "礼物燃料规则数值必须大于等于 0",
|
||||
path: ["giftFuelRules", index, "multiplierPpm"],
|
||||
});
|
||||
}
|
||||
if (!rule.excluded && multiplierPpm === 0 && fixedEnergy === 0) {
|
||||
if (!rule.excluded && multiplierPpm === 0 && fixedFuel === 0) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "非排除规则必须配置倍率或固定能量",
|
||||
path: ["giftEnergyRules", index, "multiplierPpm"],
|
||||
message: "非排除规则必须配置倍率或固定燃料",
|
||||
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>[]) {
|
||||
rewards.forEach((reward, index) => {
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
@ -98,7 +98,7 @@ export const API_OPERATIONS = {
|
||||
getResourceGroup: "getResourceGroup",
|
||||
getRoleDataScopes: "getRoleDataScopes",
|
||||
getRoomConfig: "getRoomConfig",
|
||||
getRoomTreasureConfig: "getRoomTreasureConfig",
|
||||
getRoomRocketConfig: "getRoomRocketConfig",
|
||||
getSevenDayCheckInConfig: "getSevenDayCheckInConfig",
|
||||
getUser: "getUser",
|
||||
grantResource: "grantResource",
|
||||
@ -204,7 +204,7 @@ export const API_OPERATIONS = {
|
||||
updateRole: "updateRole",
|
||||
updateRoom: "updateRoom",
|
||||
updateRoomConfig: "updateRoomConfig",
|
||||
updateRoomTreasureConfig: "updateRoomTreasureConfig",
|
||||
updateRoomRocketConfig: "updateRoomRocketConfig",
|
||||
updateSevenDayCheckInConfig: "updateSevenDayCheckInConfig",
|
||||
updateTaskDefinition: "updateTaskDefinition",
|
||||
updateTeamSalaryPolicy: "updateTeamSalaryPolicy",
|
||||
@ -815,12 +815,12 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "room-config:view",
|
||||
permissions: ["room-config:view"]
|
||||
},
|
||||
getRoomTreasureConfig: {
|
||||
getRoomRocketConfig: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.getRoomTreasureConfig,
|
||||
path: "/v1/admin/activity/room-treasure/config",
|
||||
permission: "room-treasure:view",
|
||||
permissions: ["room-treasure:view"]
|
||||
operationId: API_OPERATIONS.getRoomRocketConfig,
|
||||
path: "/v1/admin/activity/room-rocket/config",
|
||||
permission: "room-rocket:view",
|
||||
permissions: ["room-rocket:view"]
|
||||
},
|
||||
getSevenDayCheckInConfig: {
|
||||
method: "GET",
|
||||
@ -1543,12 +1543,12 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "room-config:update",
|
||||
permissions: ["room-config:update"]
|
||||
},
|
||||
updateRoomTreasureConfig: {
|
||||
updateRoomRocketConfig: {
|
||||
method: "PUT",
|
||||
operationId: API_OPERATIONS.updateRoomTreasureConfig,
|
||||
path: "/v1/admin/activity/room-treasure/config",
|
||||
permission: "room-treasure:update",
|
||||
permissions: ["room-treasure:update"]
|
||||
operationId: API_OPERATIONS.updateRoomRocketConfig,
|
||||
path: "/v1/admin/activity/room-rocket/config",
|
||||
permission: "room-rocket:update",
|
||||
permissions: ["room-rocket:update"]
|
||||
},
|
||||
updateSevenDayCheckInConfig: {
|
||||
method: "PUT",
|
||||
|
||||
10
src/shared/api/generated/schema.d.ts
vendored
10
src/shared/api/generated/schema.d.ts
vendored
@ -292,15 +292,15 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/activity/room-treasure/config": {
|
||||
"/admin/activity/room-rocket/config": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["getRoomTreasureConfig"];
|
||||
put: operations["updateRoomTreasureConfig"];
|
||||
get: operations["getRoomRocketConfig"];
|
||||
put: operations["updateRoomRocketConfig"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
@ -3038,7 +3038,7 @@ export interface operations {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
getRoomTreasureConfig: {
|
||||
getRoomRocketConfig: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
@ -3050,7 +3050,7 @@ export interface operations {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
updateRoomTreasureConfig: {
|
||||
updateRoomRocketConfig: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user