diff --git a/src/app/(main)/character/[id]/chat/Left/ChatHisory.tsx b/src/app/(main)/character/[id]/chat/Left/ChatHisory.tsx new file mode 100644 index 0000000..837a822 --- /dev/null +++ b/src/app/(main)/character/[id]/chat/Left/ChatHisory.tsx @@ -0,0 +1,19 @@ +'use client'; + +import React from 'react'; +import { useSetAtom } from 'jotai'; +import { historyListOpenAtom } from '../atoms'; + +const ChatHistory = React.memo(() => { + const setHistoryListOpen = useSetAtom(historyListOpenAtom); + + return ( +
+
+ Chat +
setHistoryListOpen(false)}>x
+
+
+ ); +}); +export default ChatHistory; diff --git a/src/app/(main)/character/[id]/chat/Left/Side.tsx b/src/app/(main)/character/[id]/chat/Left/Side.tsx index 554e960..94b9e98 100644 --- a/src/app/(main)/character/[id]/chat/Left/Side.tsx +++ b/src/app/(main)/character/[id]/chat/Left/Side.tsx @@ -101,7 +101,6 @@ export default function Side() { ); })} - ); } diff --git a/src/app/(main)/character/[id]/chat/Left/index.tsx b/src/app/(main)/character/[id]/chat/Left/index.tsx index 12852e8..47059be 100644 --- a/src/app/(main)/character/[id]/chat/Left/index.tsx +++ b/src/app/(main)/character/[id]/chat/Left/index.tsx @@ -2,21 +2,34 @@ import Side from './Side'; import { useAtomValue } from 'jotai'; -import { leftTabActiveKeyAtom } from '../atoms'; +import { historyListOpenAtom, leftTabActiveKeyAtom } from '../atoms'; import Info from './info'; import ArchiveHistory from './ArchiveHistory'; -import { memo } from 'react'; +import { memo, useRef } from 'react'; +import { Drawer } from '@/components'; +import ChatHistory from './ChatHisory'; const Left = memo(() => { const activeKey = useAtomValue(leftTabActiveKeyAtom); const Component = activeKey === 'info' ? Info : ArchiveHistory; + const historyListOpen = useAtomValue(historyListOpenAtom); + const containerRef = useRef(null); return ( -
+
+ containerRef.current} + position="left" + width={448} + open={historyListOpen} + destroyOnClose + > + +
); }); diff --git a/src/app/(main)/character/[id]/chat/page.tsx b/src/app/(main)/character/[id]/chat/page.tsx index c234262..b765fb7 100644 --- a/src/app/(main)/character/[id]/chat/page.tsx +++ b/src/app/(main)/character/[id]/chat/page.tsx @@ -24,7 +24,7 @@ export default function CharacterChat() { {/* 设置按钮 */}
setSettingOpen(!settingOpen)} @@ -37,7 +37,7 @@ export default function CharacterChat() { position="left" width={448} destroyOnClose - container={container.current} + getContainer={() => container.current} > @@ -47,7 +47,7 @@ export default function CharacterChat() { position="right" width={448} destroyOnClose - container={container.current} + getContainer={() => container.current} > diff --git a/src/app/globals.css b/src/app/globals.css index 3b81f8d..860037c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -5,6 +5,14 @@ --background: #14132d; --text-color: #fff; --text-color-1: rgba(174, 196, 223, 1); + + /* UI 组件层级 */ + --z-tooltip: 1000; + --z-dropdown: 1100; + --z-drawer: 1200; + --z-modal: 1300; + --z-toast: 1400; + --z-overlay: 1500; } @theme inline { diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx index d0d7b1b..705d5c8 100644 --- a/src/components/ui/drawer.tsx +++ b/src/components/ui/drawer.tsx @@ -1,37 +1,39 @@ 'use client'; -import { useEffect, useCallback, useRef, useState } from 'react'; +import { useMounted } from '@/hooks'; +import { useEffect, useCallback, useRef, useState, useMemo } from 'react'; import { createPortal } from 'react-dom'; type DrawerProps = { open?: boolean; - onCloseChange?: (open: boolean) => void; + getContainer?: () => HTMLElement | null; children?: React.ReactNode; - container?: HTMLElement | null; position?: 'left' | 'right' | 'top' | 'bottom'; width?: number; destroyOnClose?: boolean; + zIndex?: number | string; }; export default function Drawer({ open = false, - onCloseChange, + getContainer, children, - container, position = 'right', width = 400, destroyOnClose = false, + zIndex = 'var(--z-drawer)', }: DrawerProps) { // shouldRender 控制是否渲染 DOM(用于 destroyOnClose) const [shouldRender, setShouldRender] = useState(false); - const mountedRef = useRef(false); + const mounted = useMounted(); const drawerRef = useRef(null); - const prevOpenRef = useRef(open); - - // 确保组件在客户端挂载后再渲染 - useEffect(() => { - mountedRef.current = true; - }, []); + const previousState = useRef<{ + styles: React.CSSProperties; + open: boolean; + }>({ + styles: {}, + open: false, + }); // 当 open 变为 true 时,立即设置 shouldRender 为 true useEffect(() => { @@ -40,41 +42,6 @@ export default function Drawer({ } }, [open]); - // 控制渲染时机,用于动画 - useEffect(() => { - // 获取隐藏位置的 transform 值 - const getHiddenTransform = () => { - switch (position) { - case 'left': - return 'translateX(-100%)'; - case 'right': - return 'translateX(100%)'; - case 'top': - return 'translateY(-100%)'; - case 'bottom': - return 'translateY(100%)'; - default: - return 'translateX(100%)'; - } - }; - - if (open) { - // 打开:等待下一帧,确保 DOM 已经渲染,然后直接操作 style 触发动画 - requestAnimationFrame(() => { - if (drawerRef.current) { - drawerRef.current.style.transform = 'translateX(0) translateY(0)'; - } - }); - } else if (prevOpenRef.current) { - // 关闭:直接操作 style 开始关闭动画 - if (drawerRef.current) { - drawerRef.current.style.transform = getHiddenTransform(); - } - } - - prevOpenRef.current = open; - }, [open, position]); - // 处理动画结束后的销毁 const handleTransitionEnd = useCallback(() => { // 只有在关闭状态且设置了 destroyOnClose 时,才销毁 DOM @@ -83,45 +50,25 @@ export default function Drawer({ } }, [open, destroyOnClose, setShouldRender]); - // 如果还没挂载,或者设置了销毁且不应该渲染,则不显示 - if (!mountedRef.current || (!shouldRender && destroyOnClose)) { - return null; - } - - // 根据位置计算样式 - const getPositionStyles = (): React.CSSProperties => { - // 如果有 container,使用 absolute 定位,否则使用 fixed 定位 - const positionType = container ? 'absolute' : 'fixed'; - - // 获取隐藏位置的 transform 值 - const getHiddenTransform = () => { - switch (position) { - case 'left': - return 'translateX(-100%)'; - case 'right': - return 'translateX(100%)'; - case 'top': - return 'translateY(-100%)'; - case 'bottom': - return 'translateY(100%)'; - default: - return 'translateX(100%)'; - } - }; - - const baseStyles: React.CSSProperties = { - position: positionType, - zIndex: 5, - boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)', - transition: 'transform 0.3s ease-in-out', - // 初始状态:隐藏在屏幕外 - transform: getHiddenTransform(), - }; + const hiddenTransform = useMemo(() => { + switch (position) { + case 'left': + return 'translateX(-100%)'; + case 'right': + return 'translateX(100%)'; + case 'top': + return 'translateY(-100%)'; + case 'bottom': + return 'translateY(100%)'; + default: + return 'translateX(100%)'; + } + }, [position]); + const positionStyles = useMemo(() => { switch (position) { case 'left': return { - ...baseStyles, top: 0, left: 0, height: '100%', @@ -129,7 +76,6 @@ export default function Drawer({ }; case 'right': return { - ...baseStyles, top: 0, right: 0, height: '100%', @@ -137,7 +83,6 @@ export default function Drawer({ }; case 'top': return { - ...baseStyles, top: 0, left: 0, width: '100%', @@ -145,22 +90,59 @@ export default function Drawer({ }; case 'bottom': return { - ...baseStyles, bottom: 0, left: 0, width: '100%', height: `${width}px`, }; default: - return baseStyles; + return {}; } + }, [position, width]); + + // 根据位置计算样式 + const getPositionStyles = (): React.CSSProperties => { + // 如果状态未改变,直接返回缓存的样式 + if (previousState.current.open === open) { + return previousState.current.styles; + } + + // 如果有 container,使用 absolute 定位,否则使用 fixed 定位 + const positionType = getContainer ? 'absolute' : 'fixed'; + const baseStyles: React.CSSProperties = { + ...positionStyles, + position: positionType, + zIndex, + boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)', + transition: 'transform 0.3s ease-in-out', + transform: hiddenTransform, + }; + if (open) { + requestAnimationFrame(() => { + if (drawerRef.current) { + drawerRef.current.style.transform = 'translateX(0) translateY(0)'; + } + }); + } else { + requestAnimationFrame(() => { + if (drawerRef.current) { + drawerRef.current.style.transform = hiddenTransform; + } + }); + } + // 缓存当前状态 + previousState.current = { + styles: baseStyles, + open, + }; + return baseStyles; }; - // 使用 portal 渲染到指定容器,默认是 body - const targetContainer = - container || (mountedRef.current ? document.body : null); + if (!mounted) return null; - if (!targetContainer) return null; + // 使用 portal 渲染到指定容器,默认是 body + const targetContainer = getContainer?.() || (mounted ? document.body : null); + if (!targetContainer || !shouldRender) return null; return createPortal(
void; // 打开/关闭回调 + zIndex?: number; } & React.HTMLAttributes; function Select(props: SelectProps) { @@ -60,6 +61,7 @@ function Select(props: SelectProps) { icon, className, contentClassName, + zIndex, } = props; // 使用 useControllableValue 管理状态,支持受控和非受控模式 @@ -100,7 +102,7 @@ function Select(props: SelectProps) { style={{ width: 'var(--radix-select-trigger-width)', // 默认跟随 Trigger 宽度 backgroundColor: 'rgba(26, 23, 34, 1)', - zIndex: 9999, + zIndex: zIndex || 'var(--z-select)', }} > diff --git a/src/components/ui/modal/index.css b/src/components/ui/modal/index.css index 9d385a2..48a60a7 100644 --- a/src/components/ui/modal/index.css +++ b/src/components/ui/modal/index.css @@ -1,7 +1,7 @@ .dialog-overlay { position: fixed; inset: 0; - z-index: 40; + z-index: var(--z-modal); background: rgba(0, 0, 0, 0.7); } @@ -11,7 +11,7 @@ left: 50%; max-height: 100vh; max-width: 100vw; - z-index: 50; + z-index: var(--z-modal); transform: translate(-50%, -50%); border-radius: 0.75rem;