);
});
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;