feat: 优化组件层级,优化Drawer组件
This commit is contained in:
parent
199475af53
commit
a279653321
|
|
@ -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;
|
||||
|
|
@ -101,7 +101,6 @@ export default function Side() {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
<Drawer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,18 +50,7 @@ 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 = () => {
|
||||
const hiddenTransform = useMemo(() => {
|
||||
switch (position) {
|
||||
case 'left':
|
||||
return 'translateX(-100%)';
|
||||
|
|
@ -107,21 +63,12 @@ export default function Drawer({
|
|||
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(),
|
||||
};
|
||||
}, [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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue