2026-05-21 10:43:36 +08:00

291 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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),
}));
}