import { useEffect, useState } from "react";
import AddOutlined from "@mui/icons-material/AddOutlined";
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
import SaveOutlined from "@mui/icons-material/SaveOutlined";
import Checkbox from "@mui/material/Checkbox";
import Drawer from "@mui/material/Drawer";
import FormControlLabel from "@mui/material/FormControlLabel";
import IconButton from "@mui/material/IconButton";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
import { Button } from "@/shared/ui/Button.jsx";
import { FieldLabel } from "@/features/lucky-gift/components/FieldLabel.jsx";
import {
addTierToStage,
correctStageTierProbabilities,
removeTierFromStage,
stageExpectedRTP,
stageProbabilityTotal,
tierRTPContribution,
updateStageTier,
} from "@/features/lucky-gift/configModel.js";
import { broadcastLevelOptions, rewardSourceOptions, stageOptions, optionLabel } from "@/features/lucky-gift/constants.js";
import styles from "@/features/lucky-gift/lucky-gift.module.css";
const helpText = {
controlBandPercent: "RTP 正常波动带。1 表示允许当前窗口在目标值上下 1% 内自然波动。",
enabled: "控制全站幸运礼物抽奖是否生效。关闭后用户仍可送礼,但不会触发幸运抽奖。",
giftPriceReference: "配置奖档时使用的参考礼物价格。真实抽奖会按用户实际消耗金币等比缩放奖励。",
noviceMaxEquivalentDraws: "新手阶段截止等价抽数。等价抽数=累计消耗金币/参考价格,不同价格礼物会按金币流水推进。",
normalMaxEquivalentDraws: "正常阶段截止等价抽数。超过该值后进入高阶阶段,不能小于新手阶段阈值。",
maxSinglePayout: "单次基础返奖上限。超过该值的候选会被风控剪掉,真实抽奖按实际消耗金币等比缩放。",
poolId: "奖池 ID 是幸运礼物规则隔离键。不同奖池拥有独立 RTP、窗口和阶段奖档。",
poolRatePercent: "每笔幸运礼物扣费进入基础奖池的比例,必须大于等于 RTP,避免奖池长期亏空。",
userHourlyPayoutCap: "同一用户每小时基础返奖上限。达到上限后,该用户会优先进入 0x 或低倍率。",
userDailyPayoutCap: "同一用户每日基础返奖上限。按 UTC 自然日统计。",
deviceDailyPayoutCap: "同一设备每日基础返奖上限。用于限制多账号集中领取。",
roomHourlyPayoutCap: "同一房间每小时基础返奖上限。房间触顶后,新用户也会被该房间上限影响。",
anchorDailyPayoutCap: "同一收礼主播每日基础返奖上限。按 UTC 自然日统计。",
settlementWindowWager: "RTP 结算窗口按金币流水计算,不按抽数计算,低价和高价礼物会自然等比参与控制。",
stages: "新手、正常、高阶三个阶段必须各自配置概率,抽奖引擎按用户阶段读取对应奖档。",
targetRtpPercent: "基础返奖目标。95 表示长期每消耗 100 金币,基础 RTP 目标返还 95 金币。",
};
export function LuckyGiftConfigDrawer({
abilities,
configLoading,
configSaving,
form,
onClose,
onSubmit,
open,
setForm,
}) {
const disabled = !abilities.canUpdate || configLoading || configSaving;
return (
);
}
function StageTierEditor({ disabled, form, setForm }) {
const [stageTargetRTP, setStageTargetRTP] = useState(() => defaultStageTargetRTP(form.targetRtpPercent));
useEffect(() => {
setStageTargetRTP(defaultStageTargetRTP(form.targetRtpPercent));
}, [form.targetRtpPercent]);
return (
{stageOptions.map(([stageKey, stageLabel]) => {
const stage = (form.stages || []).find((item) => item.stage === stageKey) || { stage: stageKey, tiers: [] };
const expectedRTP = stageExpectedRTP(stage);
const lowerRTP = Number(form.targetRtpPercent || 0) - Number(form.controlBandPercent || 0);
const upperRTP = Number(form.targetRtpPercent || 0) + Number(form.controlBandPercent || 0);
const rtpInBand = expectedRTP >= lowerRTP && expectedRTP <= upperRTP;
return (
概率 {formatPercent(stageProbabilityTotal(stage))}
期望 RTP {formatPercent(expectedRTP)}
setStageTargetRTP((current) => ({ ...current, [stageKey]: event.target.value }))
}
/>
}
type="button"
onClick={() => updateStages(setForm, (stages) => addTierToStage(stages, stageKey))}
>
添加奖档
{stage.tiers.map((tier, index) => (
))}
);
})}
);
}
function defaultStageTargetRTP(targetRTPPercent) {
return Object.fromEntries(stageOptions.map(([stageKey]) => [stageKey, String(targetRTPPercent || 95)]));
}
function TierRow({ disabled, setForm, stageKey, stageLabel, tier, tierCount, tierIndex }) {
const updateTier = (patch) => updateStages(setForm, (stages) => updateStageTier(stages, stageKey, tierIndex, patch));
return (
updateTier({ tierId: event.target.value })}
/>
updateTier({ multiplier: event.target.value })}
/>
updateTier({ probabilityPercent: event.target.value })}
/>
贡献 {formatPercent(tierRTPContribution(tier))}
updateTier({ rewardSource: value })}
/>
updateTier({ broadcastLevel: value })}
/>
updateTier({ highWaterOnly: event.target.checked })}
/>
}
label="高水位"
/>
updateTier({ enabled: event.target.checked })}
/>
}
label="启用"
/>
updateStages(setForm, (stages) => removeTierFromStage(stages, stageKey, tierIndex))}
>
);
}
function SelectField({ disabled, label, onChange, options, value }) {
return (
onChange(event.target.value)}>
{options.map(([optionValue, optionLabelText]) => (
))}
);
}
function SwitchField({ checked, className, disabled, help, label, onChange }) {
return (
);
}
function updateForm(setForm, key, value) {
setForm((current) => ({ ...current, [key]: value }));
}
function updateStages(setForm, updater) {
setForm((current) => ({ ...current, stages: updater(current.stages || []) }));
}
function formatPercent(value) {
const number = Number(value || 0);
return `${number.toFixed(2).replace(/\.?0+$/, "")}%`;
}