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 (