feat: 优化drawer, 完善一下静态样式

This commit is contained in:
liuyonghe0111 2025-11-03 19:32:52 +08:00
parent ef0bdff317
commit 6177e64684
9 changed files with 151 additions and 47 deletions

View File

@ -6,7 +6,9 @@ import { cn } from '@/lib';
import { Select, Switch, Number, FontSize } from '@/components/ui/inputs';
import Background from './Background';
import { AddIcon } from '@/assets/common';
import { ModelSelectDialog } from '@/components';
import ModelSelectDialog from '@/components/feature/ModelSelectDialog';
import VoiceActorSelectDialog from '@/components/feature/VoiceActorSelectDialog';
import BubbleSelectDialog from '@/components/feature/BubbleSelectDialog';
import IconFont from '@/components/ui/iconFont';
const Title: React.FC<
@ -27,7 +29,6 @@ const Title: React.FC<
};
const SettingForm = React.memo(() => {
console.log('SettingForm');
const options = [
{
label: 'Model 1',
@ -80,12 +81,7 @@ const SettingForm = React.memo(() => {
<Title text="Sound">
<FormItem
name="name12"
render={({ value, onChange }) => (
<Select.View
icon={'/character/voice_actor.svg'}
text="Voice Actor"
/>
)}
render={({ value, onChange }) => <VoiceActorSelectDialog />}
/>
<FormItem
name="name12"
@ -131,12 +127,7 @@ const SettingForm = React.memo(() => {
/>
<FormItem
name="name12"
render={({ value, onChange }) => (
<Select.View
icon={'/character/chat_bubble.svg'}
text="Chat Bubble"
/>
)}
render={({ value, onChange }) => <BubbleSelectDialog />}
/>
</Title>

View File

@ -1,7 +1,7 @@
import { atom } from 'jotai';
// 是否打开两侧的设置
export const settingOpenAtom = atom(false);
export const settingOpenAtom = atom(true);
// 是否是立绘模式
export const isPortraitModeAtom = atom(false);

View File

@ -12,6 +12,7 @@ import { ExitFullScreenIcon, FullScreenIcon } from '@/assets/common';
export default function CharacterChat() {
const [settingOpen, setSettingOpen] = useAtom(settingOpenAtom);
console.log('settingOpen', settingOpen);
return (
<div

View File

@ -0,0 +1,18 @@
'use client';
import { Select } from '../ui/inputs';
import Modal from '../ui/modal';
type BubbleSelectDialogProps = {};
export default function BubbleSelectDialog(props: BubbleSelectDialogProps) {
return (
<Modal
title="Chat Bubble"
trigger={
<Select.View icon={'/character/chat_bubble.svg'} text="Chat Bubble" />
}
>
BubbleSelectDialog
</Modal>
);
}

View File

@ -6,6 +6,7 @@ type ModelSelectDialogProps = {
value?: string;
onChange?: (value: string) => void;
};
export default function ModelSelectDialog(props: ModelSelectDialogProps) {
const [value, onChange] = useControllableValue(props, {
valuePropName: 'value',
@ -26,7 +27,11 @@ export default function ModelSelectDialog(props: ModelSelectDialogProps) {
<Select.View icon={'/character/model_switch.svg'} text="Model 1" />
}
>
Content
<div className="flex flex-col pr-2.5">
{options?.map((i) => {
return <div key={i.value}>{i.label}</div>;
})}
</div>
</Modal>
);
}

View File

@ -0,0 +1,66 @@
'use client';
import { Select } from '../ui/inputs';
import Modal from '../ui/modal';
import Image from 'next/image';
type VoiceActorSelectDialogProps = {};
export default function VoiceActorSelectDialog(
props: VoiceActorSelectDialogProps
) {
const options = [
{ label: 'Voice Actor 1', value: 'voiceActor1', gender: 'male' },
{ label: 'Voice Actor 2', value: 'voiceActor2', gender: 'female' },
{ label: 'Voice Actor 3', value: 'voiceActor3', gender: 'male' },
];
return (
<Modal
classNames={{
content: 'w-200',
}}
title="Voice Actor"
trigger={
<Select.View icon={'/character/voice_actor.svg'} text="Voice Actor" />
}
>
<div className="grid max-h-80 grid-cols-2 gap-5 overflow-auto pr-2.5">
{options?.map((i) => {
return (
<div
style={{
background:
i.gender === 'male'
? 'linear-gradient(90deg, rgba(0, 157, 255, 0.3) 0%, rgba(0, 157, 255, 0.1) 100%)'
: 'rgba(112, 27, 140, 1)',
}}
key={i.value}
className="flex h-20 cursor-pointer items-center justify-between rounded-[10px] p-2 pr-5"
>
<div className="flex flex-1 gap-2.5">
<div>
<Image
src={'/avator.png'}
className="rounded-full"
width={50}
height={50}
alt="avator"
/>
</div>
<div>
<div className="font-bold">{'Name 1'}</div>
<div className="text-sm text-white/60">
{'Anime-Style Girl'}
</div>
</div>
</div>
<div className="flex-center h-4.5 w-4.5 rounded-full bg-black">
<div className="h-3 w-3 rounded-full bg-green-600"></div>
</div>
</div>
);
})}
</div>
</Modal>
);
}

View File

@ -4,6 +4,5 @@ export { default as Rate } from './ui/rate';
export { default as Form } from './ui/form';
export { default as Icon } from './ui/icon';
export { default as Drawer } from './ui/drawer';
export { default as ModelSelectDialog } from './feature/ModelSelectDialog';
export { default as VirtualGrid } from './ui/VirtualGrid';
export { default as TagSelect } from './ui/TagSelect';

View File

@ -26,7 +26,8 @@ export default function Drawer({
zIndex,
}: DrawerProps) {
// shouldRender 控制是否渲染 DOM用于 destroyOnClose
const [shouldRender, setShouldRender] = useState(false);
// 初始值应该根据 open 的初始值来设置,避免初始 open=true 时组件不渲染
const [shouldRender, setShouldRender] = useState(open);
const mounted = useMounted();
const drawerRef = useRef<HTMLDivElement>(null);
const previousState = useRef<{
@ -37,21 +38,7 @@ export default function Drawer({
open: false,
});
// 当 open 变为 true 时,立即设置 shouldRender 为 true
useEffect(() => {
if (open) {
setShouldRender(true);
}
}, [open]);
// 处理动画结束后的销毁
const handleTransitionEnd = useCallback(() => {
// 只有在关闭状态且设置了 destroyOnClose 时,才销毁 DOM
if (!open && destroyOnClose) {
setShouldRender(false);
}
}, [open, destroyOnClose, setShouldRender]);
// 计算隐藏时的 transform 值,需要在 useEffect 之前定义
const hiddenTransform = useMemo(() => {
switch (position) {
case 'left':
@ -67,6 +54,51 @@ export default function Drawer({
}
}, [position]);
// 当 open 变为 true 时,立即设置 shouldRender 为 true
useEffect(() => {
if (open) {
setShouldRender(true);
}
}, [open]);
// 处理打开/关闭动画,确保 DOM 已经挂载
useEffect(() => {
if (!shouldRender || !mounted || !drawerRef.current) return;
// 判断是否是初始状态
const isInitialState =
previousState.current.open === false &&
Object.keys(previousState.current.styles).length === 0;
// 无论是初始状态还是状态变化,都需要触发动画
if (open) {
// 从隐藏状态变为显示状态,触发显示动画
// 使用双重 requestAnimationFrame 确保浏览器已经渲染了初始的隐藏状态
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (drawerRef.current) {
drawerRef.current.style.transform = 'translateX(0) translateY(0)';
}
});
});
} else if (!isInitialState) {
// 从打开状态变为关闭状态,触发隐藏动画(初始状态不需要)
requestAnimationFrame(() => {
if (drawerRef.current) {
drawerRef.current.style.transform = hiddenTransform;
}
});
}
}, [open, shouldRender, mounted, hiddenTransform]);
// 处理动画结束后的销毁
const handleTransitionEnd = useCallback(() => {
// 只有在关闭状态且设置了 destroyOnClose 时,才销毁 DOM
if (!open && destroyOnClose) {
setShouldRender(false);
}
}, [open, destroyOnClose, setShouldRender]);
const positionStyles = useMemo(() => {
switch (position) {
case 'left':
@ -110,28 +142,20 @@ export default function Drawer({
}
const positionType = inBody ? 'fixed' : 'absolute';
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',
// 初始状态总是先设置为隐藏,然后通过 useEffect 中的动画来显示
// 这样即使初始 open=true也能看到从隐藏到显示的动画效果
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;
}
});
}
// 缓存当前状态
// 注意:动画逻辑已经在 useEffect 中处理,这里只需要设置初始样式
previousState.current = {
styles: baseStyles,
open,

View File

@ -12,7 +12,7 @@ type ModalProps = {
children?: React.ReactNode;
trigger?: React.ReactNode;
title?: string;
classNames?: Record<'content' | 'overlay', string>;
classNames?: Partial<Record<'content' | 'overlay', string>>;
};
export default function Modal(props: ModalProps) {