crush-level-web/src/app/(main)/chat/[aiId]/components/ChatDrawers/InlineDrawer.tsx

174 lines
4.4 KiB
TypeScript
Raw Normal View History

2025-11-13 08:38:25 +00:00
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>;
};