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

175 lines
4.3 KiB
TypeScript
Raw Normal View History

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