import FormControlLabel from "@mui/material/FormControlLabel"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; import { useEffect, useMemo, useRef, useState } from "react"; import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx"; import { LuckyGiftFieldLabel } from "./LuckyGiftFieldHelp.jsx"; const BASE_SECTIONS = [ ["identity", "规则"], ["funds", "资金"], ]; const DYNAMIC_SECTIONS = [ ["water", "水位"], ["jackpot", "大奖"], ["limits", "限额"], ]; export function LuckyGiftConfigSections({ appOptions, form, isCreate, onChange }) { const scrollRef = useRef(null); const sections = useMemo( () => (form.strategy_version === "dynamic_v3" ? [...BASE_SECTIONS, ...DYNAMIC_SECTIONS] : BASE_SECTIONS), [form.strategy_version], ); const [activeSection, setActiveSection] = useState(sections[0][0]); useEffect(() => { if (sections.some(([sectionKey]) => sectionKey === activeSection)) return; // dynamic_v3 的专属分区会在切回 fixed_v2 后卸载;同步回首个有效分区,避免导航继续指向不存在的面板。 setActiveSection(sections[0][0]); if (scrollRef.current) resolveConfigScroller(scrollRef.current).scrollTop = 0; }, [activeSection, sections]); useEffect(() => { const sectionList = scrollRef.current; if (!sectionList) return undefined; const view = sectionList.ownerDocument.defaultView; let boundScroller; const syncActiveSection = () => { if (!boundScroller) return; const readingLine = boundScroller.getBoundingClientRect().top + configNavInset(sectionList, boundScroller) + 32; let nextSection = sections[0][0]; for (const [sectionKey] of sections) { const section = sectionList.querySelector(`[data-config-section="${sectionKey}"]`); if (section && section.getBoundingClientRect().top <= readingLine) nextSection = sectionKey; } setActiveSection((current) => (current === nextSection ? current : nextSection)); }; const bindActualScroller = () => { const nextScroller = resolveConfigScroller(sectionList); if (nextScroller === boundScroller) return; boundScroller?.removeEventListener("scroll", syncActiveSection); boundScroller = nextScroller; // 桌面由参数面板独立滚动,平板/手机由 DialogContent 单滚动;只监听真实 scroller, // 避免小屏内层 overflow:visible 时导航状态更新、内容位置却完全没有变化。 boundScroller.addEventListener("scroll", syncActiveSection, { passive: true }); }; bindActualScroller(); view?.addEventListener("resize", bindActualScroller); const ResizeObserverClass = view?.ResizeObserver; const resizeObserver = ResizeObserverClass ? new ResizeObserverClass(bindActualScroller) : null; resizeObserver?.observe(sectionList); return () => { boundScroller?.removeEventListener("scroll", syncActiveSection); view?.removeEventListener("resize", bindActualScroller); resizeObserver?.disconnect(); }; }, [sections]); const jumpToSection = (sectionKey) => { const sectionList = scrollRef.current; const section = sectionList?.querySelector(`[data-config-section="${sectionKey}"]`); if (!sectionList || !section) return; const scroller = resolveConfigScroller(sectionList); setActiveSection(sectionKey); // offsetParent 不是可靠的滚动参考系;无论当前是桌面内层还是响应式外层滚动,都用视口坐标 // 换算同一个 scroller 的目标位点;窄屏横向导航是 sticky 覆盖层,目标还必须落到导航下方。 const navInset = configNavInset(sectionList, scroller); const top = Math.max( scroller.scrollTop + section.getBoundingClientRect().top - scroller.getBoundingClientRect().top - navInset - 8, 0, ); const reduceMotion = scroller.ownerDocument.defaultView?.matchMedia?.( "(prefers-reduced-motion: reduce)", ).matches; if (typeof scroller.scrollTo === "function") { scroller.scrollTo({ behavior: reduceMotion ? "auto" : "smooth", top }); } else { // jsdom 和部分旧 WebView 没有 Element.scrollTo,直接写 scrollTop 仍能保持导航可用。 scroller.scrollTop = top; } // 导航按钮只负责选择分区;焦点落到标题后,读屏会立即获得当前编辑上下文。 section.querySelector("h3")?.focus({ preventScroll: true }); }; const fundTotal = Number(form.pool_rate_percent || 0) + Number(form.profit_rate_percent || 0) + Number(form.anchor_rate_percent || 0); return ( ); } function SectionHeading({ detail, id, ok = false, title }) { return (

{title}

{detail ? {detail} : null}
); } function JackpotFlowSummary() { return (
1. 检查独立大奖资格 大盘 RTP 通过后,还要同时满足用户本轮、近 48 小时返还条件和当日大奖次数限制。
2. 尝试可支付大奖 资格通过后,从奖池余额和各项返奖上限都付得起的独立大奖倍率中选择一档。
3. 大奖成功直接结束 本抽直接采用独立大奖结果并累计一次大奖次数,不再叠加或继续抽普通奖档。
4. 未成功才走普通开奖 大奖资格未通过或没有可支付倍率时,才按当前阶段普通概率开奖;普通同倍率不需要大奖资格,也不计大奖次数。
); } function NumberField({ disabled = false, form, helperText, label, name, onChange, required = true, step = "1" }) { return ( } required={required} size="small" slotProps={{ htmlInput: { inputMode: "decimal", min: 0, step } }} type="number" value={form[name]} onChange={(event) => onChange(name, event.target.value)} /> ); } function formatCompactNumber(value) { return Number.isFinite(value) ? Number(value.toFixed(4)).toString() : "-"; } function resolveConfigScroller(sectionList) { // 桌面参数列限定高度后由自己滚动;响应式规则把它改为 overflow:visible,此时 scrollHeight 与 // clientHeight 相等,实际滚动责任上移到 DialogContent。判定能力而非 viewport 宽度可兼容缩放和嵌入场景。 if (sectionList.scrollHeight > sectionList.clientHeight) return sectionList; return sectionList.closest(".ops-config-dialog-content") || sectionList; } function configNavInset(sectionList, scroller) { if (scroller === sectionList) return 0; const nav = sectionList.previousElementSibling; if (!nav?.classList.contains("ops-config-section-nav")) return 0; // 响应式是否形成覆盖层由真实布局决定:390px 时 nav 为横向 row,1024px 时仍是左侧 column。 // 读取 computed style 而不是猜 innerWidth,才能兼容浏览器缩放、嵌入容器和未来断点调整。 const flexDirection = nav.ownerDocument.defaultView?.getComputedStyle(nav).flexDirection; return flexDirection === "row" ? nav.getBoundingClientRect().height : 0; }