chen.lin
昨天 b3a8cec76cd3d2d3aa6d470e1c28ec161bc1a16b
rsf-admin/src/layout/TabsBar.jsx
@@ -79,6 +79,8 @@
    return normalizePath(path1) === normalizePath(path2);
};
const LONG_PRESS_MS = 300;
const TabsBar = () => {
    const location = useLocation();
    const navigate = useNavigate();
@@ -91,6 +93,12 @@
    const [contextMenuTab, setContextMenuTab] = useState(null);
    const contextMenuOpenRef = useRef(false);
    contextMenuOpenRef.current = contextMenu !== null;
    const [draggingIndex, setDraggingIndex] = useState(null);
    const [dropIndicatorIndex, setDropIndicatorIndex] = useState(null);
    const longPressTimerRef = useRef(null);
    const longPressIndexRef = useRef(null);
    const justFinishedDragRef = useRef(false);
    // 在标签页右键,阻止浏览器默认菜单
    useEffect(() => {
@@ -226,6 +234,10 @@
    // 切换标签页
    const handleTabChange = (event, newValue) => {
        if (justFinishedDragRef.current) {
            justFinishedDragRef.current = false;
            return;
        }
        const targetTab = tabs[newValue];
        if (targetTab && targetTab.path !== location.pathname) {
            navigate(targetTab.path);
@@ -415,6 +427,91 @@
        return () => document.removeEventListener('mousedown', onDocClick, true);
    }, [contextMenu]);
    const clearLongPressTimer = useCallback(() => {
        if (longPressTimerRef.current) {
            clearTimeout(longPressTimerRef.current);
            longPressTimerRef.current = null;
        }
        longPressIndexRef.current = null;
    }, []);
    const handleTabPointerDown = useCallback((e, index) => {
        if (index < 0) return;
        longPressIndexRef.current = index;
        longPressTimerRef.current = setTimeout(() => {
            longPressTimerRef.current = null;
            setDraggingIndex(index);
            setDropIndicatorIndex(index);
        }, LONG_PRESS_MS);
    }, []);
    const handleTabPointerUp = useCallback(() => {
        clearLongPressTimer();
    }, [clearLongPressTimer]);
    useEffect(() => {
        if (draggingIndex === null) return;
        const getDropIndex = (clientX) => {
            const nodes = tabsBarRef.current?.querySelectorAll('[data-tab-index]');
            if (!nodes?.length) return draggingIndex;
            for (let i = 0; i < nodes.length; i++) {
                const rect = nodes[i].getBoundingClientRect();
                const mid = rect.left + rect.width / 2;
                if (clientX <= mid) {
                    const drop = i;
                    if (draggingIndex === 0) return drop <= 0 ? 0 : draggingIndex;
                    return drop <= 0 ? 1 : drop;
                }
            }
            const drop = nodes.length;
            return draggingIndex === 0 ? 0 : drop;
        };
        const clientXFromEvent = (e) => e.clientX ?? e.touches?.[0]?.clientX;
        const onMove = (e) => {
            const x = clientXFromEvent(e);
            if (x != null) setDropIndicatorIndex(getDropIndex(x));
        };
        const onTouchMove = (e) => {
            e.preventDefault();
            onMove(e);
        };
        const onUp = () => {
            justFinishedDragRef.current = true;
            setDraggingIndex((di) => {
                setDropIndicatorIndex((dropIdx) => {
                    if (di !== null && dropIdx !== null && di !== dropIdx) {
                        const newTabs = [...tabs];
                        const [item] = newTabs.splice(di, 1);
                        const insertAt = dropIdx > di ? dropIdx - 1 : dropIdx;
                        newTabs.splice(insertAt, 0, item);
                        const dashboard = newTabs.find((t) => t.path === '/dashboard');
                        if (dashboard && newTabs[0].path !== '/dashboard') {
                            const idx = newTabs.indexOf(dashboard);
                            newTabs.splice(idx, 1);
                            newTabs.unshift(dashboard);
                        }
                        saveTabs(newTabs);
                        setTabs(newTabs);
                    }
                    return null;
                });
                return null;
            });
        };
        document.addEventListener('mousemove', onMove, true);
        document.addEventListener('mouseup', onUp, true);
        document.addEventListener('touchmove', onTouchMove, { passive: false, capture: true });
        document.addEventListener('touchend', onUp, true);
        document.addEventListener('touchcancel', onUp, true);
        return () => {
            document.removeEventListener('mousemove', onMove, true);
            document.removeEventListener('mouseup', onUp, true);
            document.removeEventListener('touchmove', onTouchMove, true);
            document.removeEventListener('touchend', onUp, true);
            document.removeEventListener('touchcancel', onUp, true);
        };
    }, [draggingIndex, tabs]);
    return (
        <Box
            ref={tabsBarRef}
@@ -449,12 +546,20 @@
                        key={tab.path}
                        label={
                            <Box
                                data-tab-index={index}
                                onContextMenu={(e) => handleContextMenu(e, tab)}
                                onMouseDown={(e) => handleTabPointerDown(e, index)}
                                onMouseUp={handleTabPointerUp}
                                onMouseLeave={handleTabPointerUp}
                                onTouchStart={(e) => handleTabPointerDown(e, index)}
                                onTouchEnd={handleTabPointerUp}
                                onTouchCancel={handleTabPointerUp}
                                sx={{
                                    display: 'flex',
                                    alignItems: 'center',
                                    gap: 0.5,
                                    width: '100%',
                                    ...(draggingIndex === index && { opacity: 0.7 }),
                                }}
                            >
                                {tab.path === '/dashboard' && (