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

175 lines
4.3 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 border-outline-normal absolute inset-0 flex w-[400px] flex-col border-l border-solid',
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 min-w-0 flex-1">{children}</div>
</div>
)
}
export const InlineDrawerDescription = ({
children,
className,
}: {
children: React.ReactNode
className?: string
}) => {
return <div className={cn('flex-1 overflow-y-auto px-6', 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>
}