import AddOutlined from "@mui/icons-material/AddOutlined"; import Checkbox from "@mui/material/Checkbox"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import DialogTitle from "@mui/material/DialogTitle"; import FormControlLabel from "@mui/material/FormControlLabel"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; import { useMemo, useState } from "react"; import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx"; import { Button } from "@/shared/ui/Button.jsx"; import { formatDecimal } from "@/shared/utils/rtpProbability.js"; import { applyDynamicStrategyDefaults, configToForm, correctAllStageProbabilities, formToConfig, nextStageMultiplier, stageSummary, validateLuckyGiftForm, } from "../configForm.js"; import { formatNumber, formatPercent } from "../format.js"; import { OpsStatusBadge } from "./OpsStatusBadge.jsx"; export function LuckyGiftConfigDialog({ appOptions = [], config, mode = "edit", onClose, onSubmit, saving }) { const [form, setForm] = useState(() => configToForm(config)); const [activeStage, setActiveStage] = useState("novice"); const [validationErrors, setValidationErrors] = useState([]); const stageSummaries = useMemo(() => form.stages.map((stage) => stageSummary(stage, form)), [form]); const activeStageKey = form.stages.some((stage) => stage.stage === activeStage) ? activeStage : form.stages[0]?.stage; const activeStageConfig = form.stages.find((stage) => stage.stage === activeStageKey); const activeSummary = stageSummaries.find((summary) => summary.stageKey === activeStageKey); const updateField = (field, value) => { setValidationErrors([]); setForm((current) => { const next = { ...current, [field]: value }; if (field === "strategy_version" && value === "dynamic_v3" && current.strategy_version !== "dynamic_v3") { return applyDynamicStrategyDefaults(next); } // 目标 RTP 决定各奖档概率的闭合解,改动后全部阶段重算,避免展示的概率和将要发布的值不一致。 return field === "target_rtp_percent" ? correctAllStageProbabilities(next) : next; }); }; const updateStageField = (stageKey, field, value) => { setValidationErrors([]); setForm((current) => ({ ...current, stages: current.stages.map((stage) => (stage.stage === stageKey ? { ...stage, [field]: value } : stage)), })); }; const updateStageTier = (stageKey, tierIndex, patch) => { setValidationErrors([]); setForm((current) => correctAllStageProbabilities({ ...current, stages: current.stages.map((stage) => stage.stage === stageKey ? { ...stage, tiers: stage.tiers.map((tier, index) => index === tierIndex ? { ...tier, ...patch } : tier, ), } : stage, ), }), ); }; const addStageTier = (stageKey) => { setValidationErrors([]); setForm((current) => correctAllStageProbabilities({ ...current, stages: current.stages.map((stage) => stage.stage === stageKey ? { ...stage, tiers: [ ...stage.tiers, { enabled: true, // 10 倍以上属于高倍档,默认只在奖池高水位时开放,防止新档位直接击穿保底线。 highWaterOnly: nextStageMultiplier(stage.tiers) >= 10, multiplier: String(nextStageMultiplier(stage.tiers)), probabilityPercent: "0", tierId: "", }, ], } : stage, ), }), ); }; const removeStageTier = (stageKey, tierIndex) => { setValidationErrors([]); setForm((current) => correctAllStageProbabilities({ ...current, stages: current.stages.map((stage) => stage.stage === stageKey ? { ...stage, tiers: stage.tiers.filter((_, index) => index !== tierIndex) } : stage, ), }), ); }; const handleSubmit = (event) => { event.preventDefault(); const errors = validateLuckyGiftForm(form); if (errors.length) { setValidationErrors(errors); return; } onSubmit(formToConfig(config, form)); }; const isCreate = mode === "create"; const title = isCreate ? "添加幸运礼物配置" : config.is_default ? "发布幸运礼物配置" : "新增幸运礼物版本"; return (
{title} {form.app_code || "-"} / {form.pool_id || "default"} /{" "} {config.is_default || isCreate ? "默认草稿" : `当前版本 v${config.rule_version ?? "-"}`}
{validationErrors.length ? (
配置不能发布
    {validationErrors.map((message) => (
  • {message}
  • ))}
) : null}
{stageSummaries.map((summary) => ( ))}
{activeStageConfig ? (

{activeSummary?.stageLabel}

{form.strategy_version === "dynamic_v3" ? (
updateStageField( activeStageConfig.stage, "min_recharge_7d_coins", event.target.value, ) } /> updateStageField( activeStageConfig.stage, "min_recharge_30d_coins", event.target.value, ) } />
) : null}
{activeStageConfig.tiers.map((tier, index) => (
updateStageTier(activeStageConfig.stage, index, { // 10 倍及以上强制只走高水位,输入时同步勾选并锁死,防止高倍档在低水位放开。 highWaterOnly: Number(event.target.value) >= 10 || tier.highWaterOnly, multiplier: event.target.value, }) } /> {/* 概率由目标 RTP 和倍率统一校正,保持只读可以避免运营手填后绕过服务端概率闭合校验。 */} = 10} size="small" slotProps={{ input: { "aria-label": "高水位" } }} onChange={(event) => updateStageTier(activeStageConfig.stage, index, { highWaterOnly: event.target.checked, }) } /> updateStageTier(activeStageConfig.stage, index, { enabled: event.target.checked, }) } />
))}
) : null}
); } function DynamicStrategySections({ form, onChange }) { return ( <>

动态水位

大奖控制

onChange("jackpot_multipliers", event.target.value)} />

六维返奖上限

); } function ConfigMetric({ label, value }) { return (
{label} {value}
); } function NumberField({ disabled = false, form, helperText, label, name, onChange, step = "1" }) { return ( onChange(name, event.target.value)} /> ); }