174 lines
4.4 KiB
TypeScript
174 lines
4.4 KiB
TypeScript
|
|
import { IconButton } from "@/components/ui/button";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import { createContext, useContext, useEffect, useMemo, useCallback, useState, useRef } from "react";
|
||
|
|
|
||
|
|
// 管理所有抽屉的层级顺序
|
||
|
|
export const DrawerLayerContext = createContext<{
|
||
|
|
openOrder: string[];
|
||
|
|
registerDrawer: (id: string) => void;
|
||
|
|
unregisterDrawer: (id: string) => void;
|
||
|
|
bringToFront: (id: string) => void;
|
||
|
|
getZIndex: (id: string) => number;
|
||
|
|
}>({
|
||
|
|
openOrder: [],
|
||
|
|
registerDrawer: () => {},
|
||
|
|
unregisterDrawer: () => {},
|
||
|
|
bringToFront: () => {},
|
||
|
|
getZIndex: () => 0,
|
||
|
|
});
|
||
|
|
|
||
|
|
export const DrawerLayerProvider = ({
|
||
|
|
children,
|
||
|
|
baseZIndex = 10,
|
||
|
|
}: {
|
||
|
|
children: React.ReactNode;
|
||
|
|
baseZIndex?: number;
|
||
|
|
}) => {
|
||
|
|
const [openOrder, setOpenOrder] = useState<string[]>([]);
|
||
|
|
|
||
|
|
const registerDrawer = useCallback((id: string) => {
|
||
|
|
setOpenOrder((prev) => (prev.includes(id) ? prev : [...prev, id]));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const unregisterDrawer = useCallback((id: string) => {
|
||
|
|
setOpenOrder((prev) => prev.filter((item) => item !== id));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const bringToFront = useCallback((id: string) => {
|
||
|
|
setOpenOrder((prev) => {
|
||
|
|
// 如果该抽屉已经在最前面,则不需要更新
|
||
|
|
if (prev.length > 0 && prev[prev.length - 1] === id) {
|
||
|
|
return prev;
|
||
|
|
}
|
||
|
|
const filtered = prev.filter((item) => item !== id);
|
||
|
|
return [...filtered, id];
|
||
|
|
});
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
const getZIndex = useCallback((id: string) => {
|
||
|
|
const index = openOrder.indexOf(id);
|
||
|
|
if (index === -1) return baseZIndex;
|
||
|
|
return baseZIndex + index;
|
||
|
|
}, [openOrder, baseZIndex]);
|
||
|
|
|
||
|
|
const value = useMemo(
|
||
|
|
() => ({ openOrder, registerDrawer, unregisterDrawer, bringToFront, getZIndex }),
|
||
|
|
[openOrder, registerDrawer, unregisterDrawer, bringToFront, getZIndex]
|
||
|
|
);
|
||
|
|
|
||
|
|
return <DrawerLayerContext.Provider value={value}>{children}</DrawerLayerContext.Provider>;
|
||
|
|
};
|
||
|
|
|
||
|
|
const InlineDrawerContext = createContext<{
|
||
|
|
id: string;
|
||
|
|
open: boolean;
|
||
|
|
onOpenChange: (open: boolean) => void;
|
||
|
|
}>({
|
||
|
|
id: "",
|
||
|
|
open: false,
|
||
|
|
onOpenChange: () => {},
|
||
|
|
});
|
||
|
|
|
||
|
|
export const InlineDrawer = ({
|
||
|
|
id,
|
||
|
|
open,
|
||
|
|
onOpenChange,
|
||
|
|
timestamp,
|
||
|
|
children,
|
||
|
|
}: {
|
||
|
|
id: string;
|
||
|
|
open: boolean;
|
||
|
|
onOpenChange: (open: boolean) => void;
|
||
|
|
timestamp?: number;
|
||
|
|
children: React.ReactNode;
|
||
|
|
}) => {
|
||
|
|
const { registerDrawer, unregisterDrawer, bringToFront } = useContext(DrawerLayerContext);
|
||
|
|
|
||
|
|
// 当抽屉打开时注册并置顶;当关闭或卸载时移除
|
||
|
|
// 监听 timestamp 变化,确保每次重新打开时都会置顶
|
||
|
|
useEffect(() => {
|
||
|
|
if (open) {
|
||
|
|
registerDrawer(id);
|
||
|
|
bringToFront(id);
|
||
|
|
}
|
||
|
|
}, [open, timestamp, id, registerDrawer, bringToFront]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
return () => {
|
||
|
|
unregisterDrawer(id);
|
||
|
|
};
|
||
|
|
}, [id, unregisterDrawer, open]);
|
||
|
|
|
||
|
|
// 当抽屉关闭时不渲染任何内容
|
||
|
|
if (!open) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<InlineDrawerContext.Provider value={{ id, open, onOpenChange }}>
|
||
|
|
{children}
|
||
|
|
</InlineDrawerContext.Provider>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const InlineDrawerContent = ({
|
||
|
|
children,
|
||
|
|
className,
|
||
|
|
}: {
|
||
|
|
children: React.ReactNode;
|
||
|
|
className?: string;
|
||
|
|
}) => {
|
||
|
|
const { id } = useContext(InlineDrawerContext);
|
||
|
|
const { getZIndex, bringToFront } = useContext(DrawerLayerContext);
|
||
|
|
const zIndex = getZIndex(id);
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
"bg-background-default absolute inset-0 flex flex-col w-[400px] border-l border-solid border-outline-normal",
|
||
|
|
className
|
||
|
|
)}
|
||
|
|
style={{ zIndex }}
|
||
|
|
onPointerDownCapture={() => bringToFront(id)}
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const InlineDrawerHeader = ({
|
||
|
|
children,
|
||
|
|
}: {
|
||
|
|
children: React.ReactNode;
|
||
|
|
}) => {
|
||
|
|
const { onOpenChange } = useContext(InlineDrawerContext);
|
||
|
|
return (
|
||
|
|
<div className="flex items-center gap-2 p-6">
|
||
|
|
<IconButton iconfont="icon-arrow-right" variant="ghost" size="small" onClick={() => onOpenChange(false)} />
|
||
|
|
<div className="txt-title-m flex-1 min-w-0">
|
||
|
|
{children}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export const InlineDrawerDescription = ({
|
||
|
|
children,
|
||
|
|
className,
|
||
|
|
}: {
|
||
|
|
children: React.ReactNode;
|
||
|
|
className?: string;
|
||
|
|
}) => {
|
||
|
|
return <div className={cn("flex-1 px-6 overflow-y-auto", className)}>{children}</div>;
|
||
|
|
};
|
||
|
|
|
||
|
|
export const InlineDrawerFooter = ({
|
||
|
|
children,
|
||
|
|
className,
|
||
|
|
}: {
|
||
|
|
children: React.ReactNode;
|
||
|
|
className?: string;
|
||
|
|
}) => {
|
||
|
|
return <div className={cn("flex items-center justify-end gap-4 p-6", className)}>{children}</div>;
|
||
|
|
};
|