feat: 优化组件层级,优化Drawer组件

This commit is contained in:
liuyonghe0111 2025-10-28 19:43:50 +08:00
parent 199475af53
commit a279653321
8 changed files with 123 additions and 100 deletions

View File

@ -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 (
<div className="h-full w-full bg-[rgba(22,18,29,1)] pt-7.5 pr-5 pl-10">
<div className="flex items-center justify-between">
<span>Chat</span>
<div onClick={() => setHistoryListOpen(false)}>x</div>
</div>
</div>
);
});
export default ChatHistory;

View File

@ -101,7 +101,6 @@ export default function Side() {
);
})}
</div>
<Drawer />
</div>
);
}

View File

@ -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<HTMLDivElement>(null);
return (
<div className="flex h-full w-112">
<div ref={containerRef} className="relative flex h-full w-112">
<Side />
<div className="flex-1">
<Component />
</div>
<Drawer
getContainer={() => containerRef.current}
position="left"
width={448}
open={historyListOpen}
destroyOnClose
>
<ChatHistory />
</Drawer>
</div>
);
});

View File

@ -24,7 +24,7 @@ export default function CharacterChat() {
{/* 设置按钮 */}
<div
className={cn(
'absolute top-8 right-10 z-10 h-10 w-10 select-none hover:cursor-pointer',
'absolute top-8 right-10 z-[1201] h-10 w-10 select-none hover:cursor-pointer',
'text-text-color/10 hover:text-text-color/20'
)}
onClick={() => setSettingOpen(!settingOpen)}
@ -37,7 +37,7 @@ export default function CharacterChat() {
position="left"
width={448}
destroyOnClose
container={container.current}
getContainer={() => container.current}
>
<Left />
</Drawer>
@ -47,7 +47,7 @@ export default function CharacterChat() {
position="right"
width={448}
destroyOnClose
container={container.current}
getContainer={() => container.current}
>
<SettingForm />
</Drawer>

View File

@ -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 {

View File

@ -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<HTMLDivElement>(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(
<div

View File

@ -50,6 +50,7 @@ type SelectProps = {
contentClassName?: string; // 自定义下拉框样式
defaultOpen?: boolean; // 默认是否打开
onOpenChange?: (open: boolean) => void; // 打开/关闭回调
zIndex?: number;
} & React.HTMLAttributes<HTMLDivElement>;
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)',
}}
>
<Viewport className="p-2">

View File

@ -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;