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> </div>
<Drawer />
</div> </div>
); );
} }

View File

@ -2,21 +2,34 @@
import Side from './Side'; import Side from './Side';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { leftTabActiveKeyAtom } from '../atoms'; import { historyListOpenAtom, leftTabActiveKeyAtom } from '../atoms';
import Info from './info'; import Info from './info';
import ArchiveHistory from './ArchiveHistory'; 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 Left = memo(() => {
const activeKey = useAtomValue(leftTabActiveKeyAtom); const activeKey = useAtomValue(leftTabActiveKeyAtom);
const Component = activeKey === 'info' ? Info : ArchiveHistory; const Component = activeKey === 'info' ? Info : ArchiveHistory;
const historyListOpen = useAtomValue(historyListOpenAtom);
const containerRef = useRef<HTMLDivElement>(null);
return ( return (
<div className="flex h-full w-112"> <div ref={containerRef} className="relative flex h-full w-112">
<Side /> <Side />
<div className="flex-1"> <div className="flex-1">
<Component /> <Component />
</div> </div>
<Drawer
getContainer={() => containerRef.current}
position="left"
width={448}
open={historyListOpen}
destroyOnClose
>
<ChatHistory />
</Drawer>
</div> </div>
); );
}); });

View File

@ -24,7 +24,7 @@ export default function CharacterChat() {
{/* 设置按钮 */} {/* 设置按钮 */}
<div <div
className={cn( 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' 'text-text-color/10 hover:text-text-color/20'
)} )}
onClick={() => setSettingOpen(!settingOpen)} onClick={() => setSettingOpen(!settingOpen)}
@ -37,7 +37,7 @@ export default function CharacterChat() {
position="left" position="left"
width={448} width={448}
destroyOnClose destroyOnClose
container={container.current} getContainer={() => container.current}
> >
<Left /> <Left />
</Drawer> </Drawer>
@ -47,7 +47,7 @@ export default function CharacterChat() {
position="right" position="right"
width={448} width={448}
destroyOnClose destroyOnClose
container={container.current} getContainer={() => container.current}
> >
<SettingForm /> <SettingForm />
</Drawer> </Drawer>

View File

@ -5,6 +5,14 @@
--background: #14132d; --background: #14132d;
--text-color: #fff; --text-color: #fff;
--text-color-1: rgba(174, 196, 223, 1); --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 { @theme inline {

View File

@ -1,37 +1,39 @@
'use client'; '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'; import { createPortal } from 'react-dom';
type DrawerProps = { type DrawerProps = {
open?: boolean; open?: boolean;
onCloseChange?: (open: boolean) => void; getContainer?: () => HTMLElement | null;
children?: React.ReactNode; children?: React.ReactNode;
container?: HTMLElement | null;
position?: 'left' | 'right' | 'top' | 'bottom'; position?: 'left' | 'right' | 'top' | 'bottom';
width?: number; width?: number;
destroyOnClose?: boolean; destroyOnClose?: boolean;
zIndex?: number | string;
}; };
export default function Drawer({ export default function Drawer({
open = false, open = false,
onCloseChange, getContainer,
children, children,
container,
position = 'right', position = 'right',
width = 400, width = 400,
destroyOnClose = false, destroyOnClose = false,
zIndex = 'var(--z-drawer)',
}: DrawerProps) { }: DrawerProps) {
// shouldRender 控制是否渲染 DOM用于 destroyOnClose // shouldRender 控制是否渲染 DOM用于 destroyOnClose
const [shouldRender, setShouldRender] = useState(false); const [shouldRender, setShouldRender] = useState(false);
const mountedRef = useRef(false); const mounted = useMounted();
const drawerRef = useRef<HTMLDivElement>(null); const drawerRef = useRef<HTMLDivElement>(null);
const prevOpenRef = useRef(open); const previousState = useRef<{
styles: React.CSSProperties;
// 确保组件在客户端挂载后再渲染 open: boolean;
useEffect(() => { }>({
mountedRef.current = true; styles: {},
}, []); open: false,
});
// 当 open 变为 true 时,立即设置 shouldRender 为 true // 当 open 变为 true 时,立即设置 shouldRender 为 true
useEffect(() => { useEffect(() => {
@ -40,41 +42,6 @@ export default function Drawer({
} }
}, [open]); }, [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(() => { const handleTransitionEnd = useCallback(() => {
// 只有在关闭状态且设置了 destroyOnClose 时,才销毁 DOM // 只有在关闭状态且设置了 destroyOnClose 时,才销毁 DOM
@ -83,45 +50,25 @@ export default function Drawer({
} }
}, [open, destroyOnClose, setShouldRender]); }, [open, destroyOnClose, setShouldRender]);
// 如果还没挂载,或者设置了销毁且不应该渲染,则不显示 const hiddenTransform = useMemo(() => {
if (!mountedRef.current || (!shouldRender && destroyOnClose)) { switch (position) {
return null; case 'left':
} return 'translateX(-100%)';
case 'right':
// 根据位置计算样式 return 'translateX(100%)';
const getPositionStyles = (): React.CSSProperties => { case 'top':
// 如果有 container使用 absolute 定位,否则使用 fixed 定位 return 'translateY(-100%)';
const positionType = container ? 'absolute' : 'fixed'; case 'bottom':
return 'translateY(100%)';
// 获取隐藏位置的 transform 值 default:
const getHiddenTransform = () => { return 'translateX(100%)';
switch (position) { }
case 'left': }, [position]);
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 positionStyles = useMemo(() => {
switch (position) { switch (position) {
case 'left': case 'left':
return { return {
...baseStyles,
top: 0, top: 0,
left: 0, left: 0,
height: '100%', height: '100%',
@ -129,7 +76,6 @@ export default function Drawer({
}; };
case 'right': case 'right':
return { return {
...baseStyles,
top: 0, top: 0,
right: 0, right: 0,
height: '100%', height: '100%',
@ -137,7 +83,6 @@ export default function Drawer({
}; };
case 'top': case 'top':
return { return {
...baseStyles,
top: 0, top: 0,
left: 0, left: 0,
width: '100%', width: '100%',
@ -145,22 +90,59 @@ export default function Drawer({
}; };
case 'bottom': case 'bottom':
return { return {
...baseStyles,
bottom: 0, bottom: 0,
left: 0, left: 0,
width: '100%', width: '100%',
height: `${width}px`, height: `${width}px`,
}; };
default: 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 if (!mounted) return null;
const targetContainer =
container || (mountedRef.current ? document.body : null);
if (!targetContainer) return null; // 使用 portal 渲染到指定容器,默认是 body
const targetContainer = getContainer?.() || (mounted ? document.body : null);
if (!targetContainer || !shouldRender) return null;
return createPortal( return createPortal(
<div <div

View File

@ -50,6 +50,7 @@ type SelectProps = {
contentClassName?: string; // 自定义下拉框样式 contentClassName?: string; // 自定义下拉框样式
defaultOpen?: boolean; // 默认是否打开 defaultOpen?: boolean; // 默认是否打开
onOpenChange?: (open: boolean) => void; // 打开/关闭回调 onOpenChange?: (open: boolean) => void; // 打开/关闭回调
zIndex?: number;
} & React.HTMLAttributes<HTMLDivElement>; } & React.HTMLAttributes<HTMLDivElement>;
function Select(props: SelectProps) { function Select(props: SelectProps) {
@ -60,6 +61,7 @@ function Select(props: SelectProps) {
icon, icon,
className, className,
contentClassName, contentClassName,
zIndex,
} = props; } = props;
// 使用 useControllableValue 管理状态,支持受控和非受控模式 // 使用 useControllableValue 管理状态,支持受控和非受控模式
@ -100,7 +102,7 @@ function Select(props: SelectProps) {
style={{ style={{
width: 'var(--radix-select-trigger-width)', // 默认跟随 Trigger 宽度 width: 'var(--radix-select-trigger-width)', // 默认跟随 Trigger 宽度
backgroundColor: 'rgba(26, 23, 34, 1)', backgroundColor: 'rgba(26, 23, 34, 1)',
zIndex: 9999, zIndex: zIndex || 'var(--z-select)',
}} }}
> >
<Viewport className="p-2"> <Viewport className="p-2">

View File

@ -1,7 +1,7 @@
.dialog-overlay { .dialog-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 40; z-index: var(--z-modal);
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
} }
@ -11,7 +11,7 @@
left: 50%; left: 50%;
max-height: 100vh; max-height: 100vh;
max-width: 100vw; max-width: 100vw;
z-index: 50; z-index: var(--z-modal);
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
border-radius: 0.75rem; border-radius: 0.75rem;