hyapp-admin-platform/ops-center/src/components/LuckyGiftConfigDialog.jsx
zhx d57a8bdbd9 merge: latest test lucky gift thresholds
# Conflicts:
#	ops-center/src/components/LuckyGiftConfigSections.jsx
#	ops-center/src/components/LuckyGiftStageDesigner.jsx
2026-07-15 18:37:58 +08:00

232 lines
10 KiB
JavaScript

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 { useEffect, useMemo, useRef, useState } from "react";
import { Button } from "@/shared/ui/Button.jsx";
import {
applyDynamicStrategyDefaults,
configToForm,
correctAllStageProbabilities,
formToConfig,
nextStageMultiplier,
stageSummary,
validateLuckyGiftForm,
} from "../configForm.js";
import { formatNumber, formatPercent } from "../format.js";
import { LuckyGiftConfigSections } from "./LuckyGiftConfigSections.jsx";
import { LuckyGiftStageDesigner } from "./LuckyGiftStageDesigner.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 validationRef = useRef(null);
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 normalStageConfig = form.stages.find((stage) => stage.stage === "normal");
useEffect(() => {
if (!validationErrors.length) return;
// 保存按钮位于固定底栏;失败后把焦点送回错误摘要,避免键盘或读屏用户停留在无变化的按钮上。
validationRef.current?.focus();
}, [validationErrors]);
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 (
<Dialog
aria-labelledby="ops-config-dialog-title"
fullWidth
maxWidth={false}
open
scroll="paper"
slotProps={{ paper: { className: "ops-config-dialog-paper" } }}
onClose={saving ? undefined : onClose}
>
<form className="ops-config-form" onSubmit={handleSubmit}>
<DialogTitle className="ops-config-title" id="ops-config-dialog-title">
<span>{title}</span>
<small>
{form.app_code || "-"} / {form.pool_id || "default"} /{" "}
{config.is_default || isCreate ? "默认草稿" : `当前版本 v${config.rule_version ?? "-"}`}
</small>
</DialogTitle>
<dl className="ops-config-summary" aria-label="配置摘要">
<ConfigMetric label="应用" value={form.app_code || "-"} />
<ConfigMetric label="奖池" value={form.pool_id || "default"} />
<ConfigMetric label="开奖策略" value={form.strategy_version} />
<ConfigMetric label="基础返还目标" value={formatPercent(Number(form.target_rtp_percent))} />
<ConfigMetric
label={form.strategy_version === "dynamic_v3" ? "资金拆分" : "奖池比例"}
value={
form.strategy_version === "dynamic_v3"
? `${form.pool_rate_percent}% / ${form.profit_rate_percent}% / ${form.anchor_rate_percent}%`
: `${form.pool_rate_percent}%`
}
/>
<ConfigMetric
label={form.strategy_version === "dynamic_v3" ? "每轮观察流水" : "V2 窗口配置"}
value={formatNumber(Number(form.settlement_window_wager))}
/>
<ConfigMetric label="发布状态" value={form.enabled ? "启用" : "停用"} />
</dl>
<DialogContent className="ops-config-dialog-content" dividers>
{validationErrors.length ? (
<div
aria-live="assertive"
className="ops-config-validation"
ref={validationRef}
role="alert"
tabIndex={-1}
>
<strong>配置不能发布</strong>
<ul>
{validationErrors.map((message) => (
<li key={message}>{message}</li>
))}
</ul>
</div>
) : null}
<div className="ops-config-layout">
<LuckyGiftConfigSections
appOptions={appOptions}
form={form}
isCreate={isCreate}
onChange={updateField}
/>
<LuckyGiftStageDesigner
activeStageConfig={activeStageConfig}
activeStageKey={activeStageKey}
activeSummary={activeSummary}
normalStageConfig={normalStageConfig}
stageSummaries={stageSummaries}
strategyVersion={form.strategy_version}
onAddTier={addStageTier}
onRemoveTier={removeStageTier}
onStageChange={setActiveStage}
onStageFieldChange={updateStageField}
onTierChange={updateStageTier}
/>
</div>
</DialogContent>
<DialogActions className="ops-config-dialog-actions">
<Button disabled={saving} onClick={onClose}>
取消
</Button>
<Button disabled={saving} type="submit" variant="primary">
{saving ? "保存中" : "保存配置"}
</Button>
</DialogActions>
</form>
</Dialog>
);
}
function ConfigMetric({ label, value }) {
return (
<div className="ops-config-metric">
<dt>{label}</dt>
<dd>{value}</dd>
</div>
);
}