291 lines
13 KiB
JavaScript
291 lines
13 KiB
JavaScript
import AddOutlined from "@mui/icons-material/AddOutlined";
|
||
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||
import SaveOutlined from "@mui/icons-material/SaveOutlined";
|
||
import Drawer from "@mui/material/Drawer";
|
||
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 {
|
||
atmosphereLevelOptions,
|
||
controlStrengthOptions,
|
||
jackpotStrategyOptions,
|
||
noviceStrengthOptions,
|
||
poolModeOptions,
|
||
riskLevelOptions,
|
||
} from "@/features/lucky-gift/constants.js";
|
||
import styles from "@/features/lucky-gift/lucky-gift.module.css";
|
||
|
||
const helpText = {
|
||
enabled: "控制全站幸运礼物抽奖是否生效,关闭后用户仍可送礼但不触发幸运抽奖。",
|
||
poolId: "奖池 ID 是请求接口传入的隔离键,不同奖池的 RTP、预算、风控和用户阶段互不影响。",
|
||
targetRtpPercent: "基础返奖成本比例,95 表示长期每消耗 100 金币,基础奖池目标返还 95 金币。",
|
||
controlStrength: "控制 RTP 收敛窗口大小,越精准越快贴近目标,越自然短期波动越大。",
|
||
poolMode: "决定平台池、房间池、整体幸运礼物池分别承担多少基础返奖责任。",
|
||
noviceStrength: "控制新手池持续抽数,越强表示用户前期更长时间处于低波动体验。",
|
||
multiplierValues: "配置用户可能命中的倍率,例如 0、0.5、1、2、10、100;每个倍率的概率由后端按 RTP 窗口自动生成。",
|
||
jackpotStrategy: "控制高倍奖档开放强度,保守需要更高水位,关闭则不出高倍大奖。",
|
||
riskLevel: "控制单用户、设备、房间和主播关联的奖励上限,越严格越防套利。",
|
||
atmosphereLevel: "控制房间气氛池入账强度,用于房间共享爆点,不计入基础 RTP。",
|
||
activitySubsidyEnabled: "活动补贴是独立预算,开启后会提高用户可见奖励,但不污染基础 RTP。",
|
||
activityBudget: "活动期最多可额外补贴的金币总额。",
|
||
activityDailyLimit: "活动补贴每天最多释放的金币额度。",
|
||
};
|
||
|
||
export function LuckyGiftConfigDrawer({
|
||
abilities,
|
||
configLoading,
|
||
configSaving,
|
||
form,
|
||
onClose,
|
||
onSubmit,
|
||
open,
|
||
setForm,
|
||
}) {
|
||
const disabled = !abilities.canUpdate || configLoading || configSaving;
|
||
|
||
return (
|
||
<Drawer anchor="right" open={open} onClose={configSaving ? undefined : onClose}>
|
||
<form className={["form-drawer", styles.configDrawer].join(" ")} onSubmit={onSubmit}>
|
||
<h2>幸运礼物配置</h2>
|
||
<section className="form-drawer__section">
|
||
<div className="form-drawer__section-title">基础控制</div>
|
||
<div className={styles.configGrid}>
|
||
<TextField
|
||
required
|
||
disabled
|
||
label={<FieldLabel help={helpText.poolId} text="奖池 ID" />}
|
||
value={form.poolId}
|
||
/>
|
||
<TextField
|
||
required
|
||
disabled={disabled}
|
||
inputProps={{ max: 100, min: 1, step: 0.01 }}
|
||
label={<FieldLabel help={helpText.targetRtpPercent} text="RTP" />}
|
||
type="number"
|
||
value={form.targetRtpPercent}
|
||
onChange={(event) => updateForm(setForm, "targetRtpPercent", event.target.value)}
|
||
/>
|
||
<SelectField
|
||
disabled={disabled}
|
||
help={helpText.controlStrength}
|
||
label="RTP 控制强度"
|
||
options={controlStrengthOptions}
|
||
value={form.controlStrength}
|
||
onChange={(value) => updateForm(setForm, "controlStrength", value)}
|
||
/>
|
||
<SwitchField
|
||
checked={form.enabled}
|
||
className={styles.configWide}
|
||
disabled={disabled}
|
||
help={helpText.enabled}
|
||
label="幸运礼物状态"
|
||
onChange={(event) => updateForm(setForm, "enabled", event.target.checked)}
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="form-drawer__section">
|
||
<div className="form-drawer__section-title">体验策略</div>
|
||
<div className={styles.configGrid}>
|
||
<SelectField
|
||
disabled={disabled}
|
||
help={helpText.poolMode}
|
||
label="奖池模式"
|
||
options={poolModeOptions}
|
||
value={form.poolMode}
|
||
onChange={(value) => updateForm(setForm, "poolMode", value)}
|
||
/>
|
||
<SelectField
|
||
disabled={disabled}
|
||
help={helpText.noviceStrength}
|
||
label="新手保护"
|
||
options={noviceStrengthOptions}
|
||
value={form.noviceStrength}
|
||
onChange={(value) => updateForm(setForm, "noviceStrength", value)}
|
||
/>
|
||
<MultiplierField
|
||
disabled={disabled}
|
||
help={helpText.multiplierValues}
|
||
setForm={setForm}
|
||
values={form.multiplierValues || []}
|
||
/>
|
||
<SelectField
|
||
disabled={disabled}
|
||
help={helpText.jackpotStrategy}
|
||
label="大奖策略"
|
||
options={jackpotStrategyOptions}
|
||
value={form.jackpotStrategy}
|
||
onChange={(value) => updateForm(setForm, "jackpotStrategy", value)}
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="form-drawer__section">
|
||
<div className="form-drawer__section-title">保护和补贴</div>
|
||
<div className={styles.configGrid}>
|
||
<SelectField
|
||
disabled={disabled}
|
||
help={helpText.riskLevel}
|
||
label="风控等级"
|
||
options={riskLevelOptions}
|
||
value={form.riskLevel}
|
||
onChange={(value) => updateForm(setForm, "riskLevel", value)}
|
||
/>
|
||
<SelectField
|
||
disabled={disabled}
|
||
help={helpText.atmosphereLevel}
|
||
label="房间爆点强度"
|
||
options={atmosphereLevelOptions}
|
||
value={form.atmosphereLevel}
|
||
onChange={(value) => updateForm(setForm, "atmosphereLevel", value)}
|
||
/>
|
||
<div className={styles.subsidyBlock}>
|
||
<SwitchField
|
||
checked={form.activitySubsidyEnabled}
|
||
className={styles.subsidyToggle}
|
||
disabled={disabled}
|
||
help={helpText.activitySubsidyEnabled}
|
||
label="活动补贴"
|
||
onChange={(event) => updateForm(setForm, "activitySubsidyEnabled", event.target.checked)}
|
||
/>
|
||
{form.activitySubsidyEnabled ? (
|
||
<div className={styles.subsidyInputs}>
|
||
<TextField
|
||
disabled={disabled}
|
||
inputProps={{ min: 0, step: 1 }}
|
||
label={<FieldLabel help={helpText.activityBudget} text="活动总预算" />}
|
||
type="number"
|
||
value={form.activityBudget}
|
||
onChange={(event) => updateForm(setForm, "activityBudget", event.target.value)}
|
||
/>
|
||
<TextField
|
||
disabled={disabled}
|
||
inputProps={{ min: 0, step: 1 }}
|
||
label={<FieldLabel help={helpText.activityDailyLimit} text="活动每日预算" />}
|
||
type="number"
|
||
value={form.activityDailyLimit}
|
||
onChange={(event) => updateForm(setForm, "activityDailyLimit", event.target.value)}
|
||
/>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div className="form-drawer__actions">
|
||
<Button disabled={configSaving} type="button" onClick={onClose}>
|
||
取消
|
||
</Button>
|
||
<Button disabled={disabled} startIcon={<SaveOutlined fontSize="small" />} type="submit" variant="primary">
|
||
保存
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Drawer>
|
||
);
|
||
}
|
||
|
||
function MultiplierField({ disabled, help, setForm, values }) {
|
||
const list = values.length > 0 ? values : ["0"];
|
||
return (
|
||
<div className={[styles.multiplierBlock, styles.configWide].join(" ")}>
|
||
<div className={styles.multiplierHeader}>
|
||
<FieldLabel help={help} text="倍率配置" />
|
||
<Button
|
||
disabled={disabled}
|
||
startIcon={<AddOutlined fontSize="small" />}
|
||
type="button"
|
||
onClick={() => addMultiplier(setForm)}
|
||
>
|
||
添加倍率
|
||
</Button>
|
||
</div>
|
||
<div className={styles.multiplierList}>
|
||
{list.map((value, index) => (
|
||
<div key={index} className={styles.multiplierRow}>
|
||
<TextField
|
||
disabled={disabled}
|
||
inputProps={{ min: 0, step: 0.01 }}
|
||
label={`倍率 ${index + 1}`}
|
||
type="number"
|
||
value={value}
|
||
onChange={(event) => updateMultiplier(setForm, index, event.target.value)}
|
||
/>
|
||
<IconButton
|
||
aria-label="删除倍率"
|
||
disabled={disabled || list.length <= 1}
|
||
size="small"
|
||
onClick={() => removeMultiplier(setForm, index)}
|
||
>
|
||
<DeleteOutlineOutlined fontSize="small" />
|
||
</IconButton>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SelectField({ disabled, help, label, onChange, options, value }) {
|
||
return (
|
||
<TextField
|
||
select
|
||
disabled={disabled}
|
||
label={<FieldLabel help={help} text={label} />}
|
||
value={value}
|
||
onChange={(event) => onChange(event.target.value)}
|
||
>
|
||
{options.map(([optionValue, optionLabel]) => (
|
||
<MenuItem key={optionValue} value={optionValue}>
|
||
{optionLabel}
|
||
</MenuItem>
|
||
))}
|
||
</TextField>
|
||
);
|
||
}
|
||
|
||
function SwitchField({ checked, className, disabled, help, label, onChange }) {
|
||
return (
|
||
<div className={[styles.switchField, className].filter(Boolean).join(" ")}>
|
||
<FieldLabel help={help} text={label} />
|
||
<AdminSwitch
|
||
checked={checked}
|
||
checkedLabel="开启"
|
||
disabled={disabled}
|
||
label={label}
|
||
uncheckedLabel="关闭"
|
||
onChange={onChange}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function updateForm(setForm, key, value) {
|
||
setForm((current) => ({ ...current, [key]: value }));
|
||
}
|
||
|
||
function updateMultiplier(setForm, index, value) {
|
||
setForm((current) => {
|
||
const nextValues = [...(current.multiplierValues || [])];
|
||
nextValues[index] = value;
|
||
return { ...current, multiplierValues: nextValues };
|
||
});
|
||
}
|
||
|
||
function addMultiplier(setForm) {
|
||
setForm((current) => ({
|
||
...current,
|
||
multiplierValues: [...(current.multiplierValues || []), ""],
|
||
}));
|
||
}
|
||
|
||
function removeMultiplier(setForm, index) {
|
||
setForm((current) => ({
|
||
...current,
|
||
multiplierValues: (current.multiplierValues || []).filter((_, itemIndex) => itemIndex !== index),
|
||
}));
|
||
}
|