# Conflicts: # ops-center/src/components/LuckyGiftConfigSections.jsx # ops-center/src/components/LuckyGiftStageDesigner.jsx
232 lines
10 KiB
JavaScript
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>
|
|
);
|
|
}
|