feat: 优化drawer, 完善一下静态样式
This commit is contained in:
parent
ef0bdff317
commit
6177e64684
|
|
@ -6,7 +6,9 @@ import { cn } from '@/lib';
|
||||||
import { Select, Switch, Number, FontSize } from '@/components/ui/inputs';
|
import { Select, Switch, Number, FontSize } from '@/components/ui/inputs';
|
||||||
import Background from './Background';
|
import Background from './Background';
|
||||||
import { AddIcon } from '@/assets/common';
|
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';
|
import IconFont from '@/components/ui/iconFont';
|
||||||
|
|
||||||
const Title: React.FC<
|
const Title: React.FC<
|
||||||
|
|
@ -27,7 +29,6 @@ const Title: React.FC<
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingForm = React.memo(() => {
|
const SettingForm = React.memo(() => {
|
||||||
console.log('SettingForm');
|
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
label: 'Model 1',
|
label: 'Model 1',
|
||||||
|
|
@ -80,12 +81,7 @@ const SettingForm = React.memo(() => {
|
||||||
<Title text="Sound">
|
<Title text="Sound">
|
||||||
<FormItem
|
<FormItem
|
||||||
name="name12"
|
name="name12"
|
||||||
render={({ value, onChange }) => (
|
render={({ value, onChange }) => <VoiceActorSelectDialog />}
|
||||||
<Select.View
|
|
||||||
icon={'/character/voice_actor.svg'}
|
|
||||||
text="Voice Actor"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
name="name12"
|
name="name12"
|
||||||
|
|
@ -131,12 +127,7 @@ const SettingForm = React.memo(() => {
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
name="name12"
|
name="name12"
|
||||||
render={({ value, onChange }) => (
|
render={({ value, onChange }) => <BubbleSelectDialog />}
|
||||||
<Select.View
|
|
||||||
icon={'/character/chat_bubble.svg'}
|
|
||||||
text="Chat Bubble"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
// 是否打开两侧的设置
|
// 是否打开两侧的设置
|
||||||
export const settingOpenAtom = atom(false);
|
export const settingOpenAtom = atom(true);
|
||||||
|
|
||||||
// 是否是立绘模式
|
// 是否是立绘模式
|
||||||
export const isPortraitModeAtom = atom(false);
|
export const isPortraitModeAtom = atom(false);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { ExitFullScreenIcon, FullScreenIcon } from '@/assets/common';
|
||||||
|
|
||||||
export default function CharacterChat() {
|
export default function CharacterChat() {
|
||||||
const [settingOpen, setSettingOpen] = useAtom(settingOpenAtom);
|
const [settingOpen, setSettingOpen] = useAtom(settingOpenAtom);
|
||||||
|
console.log('settingOpen', settingOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ type ModelSelectDialogProps = {
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ModelSelectDialog(props: ModelSelectDialogProps) {
|
export default function ModelSelectDialog(props: ModelSelectDialogProps) {
|
||||||
const [value, onChange] = useControllableValue(props, {
|
const [value, onChange] = useControllableValue(props, {
|
||||||
valuePropName: 'value',
|
valuePropName: 'value',
|
||||||
|
|
@ -26,7 +27,11 @@ export default function ModelSelectDialog(props: ModelSelectDialogProps) {
|
||||||
<Select.View icon={'/character/model_switch.svg'} text="Model 1" />
|
<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>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,5 @@ export { default as Rate } from './ui/rate';
|
||||||
export { default as Form } from './ui/form';
|
export { default as Form } from './ui/form';
|
||||||
export { default as Icon } from './ui/icon';
|
export { default as Icon } from './ui/icon';
|
||||||
export { default as Drawer } from './ui/drawer';
|
export { default as Drawer } from './ui/drawer';
|
||||||
export { default as ModelSelectDialog } from './feature/ModelSelectDialog';
|
|
||||||
export { default as VirtualGrid } from './ui/VirtualGrid';
|
export { default as VirtualGrid } from './ui/VirtualGrid';
|
||||||
export { default as TagSelect } from './ui/TagSelect';
|
export { default as TagSelect } from './ui/TagSelect';
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ export default function Drawer({
|
||||||
zIndex,
|
zIndex,
|
||||||
}: DrawerProps) {
|
}: DrawerProps) {
|
||||||
// shouldRender 控制是否渲染 DOM(用于 destroyOnClose)
|
// shouldRender 控制是否渲染 DOM(用于 destroyOnClose)
|
||||||
const [shouldRender, setShouldRender] = useState(false);
|
// 初始值应该根据 open 的初始值来设置,避免初始 open=true 时组件不渲染
|
||||||
|
const [shouldRender, setShouldRender] = useState(open);
|
||||||
const mounted = useMounted();
|
const mounted = useMounted();
|
||||||
const drawerRef = useRef<HTMLDivElement>(null);
|
const drawerRef = useRef<HTMLDivElement>(null);
|
||||||
const previousState = useRef<{
|
const previousState = useRef<{
|
||||||
|
|
@ -37,21 +38,7 @@ export default function Drawer({
|
||||||
open: false,
|
open: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 当 open 变为 true 时,立即设置 shouldRender 为 true
|
// 计算隐藏时的 transform 值,需要在 useEffect 之前定义
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
setShouldRender(true);
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
// 处理动画结束后的销毁
|
|
||||||
const handleTransitionEnd = useCallback(() => {
|
|
||||||
// 只有在关闭状态且设置了 destroyOnClose 时,才销毁 DOM
|
|
||||||
if (!open && destroyOnClose) {
|
|
||||||
setShouldRender(false);
|
|
||||||
}
|
|
||||||
}, [open, destroyOnClose, setShouldRender]);
|
|
||||||
|
|
||||||
const hiddenTransform = useMemo(() => {
|
const hiddenTransform = useMemo(() => {
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 'left':
|
case 'left':
|
||||||
|
|
@ -67,6 +54,51 @@ export default function Drawer({
|
||||||
}
|
}
|
||||||
}, [position]);
|
}, [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(() => {
|
const positionStyles = useMemo(() => {
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 'left':
|
case 'left':
|
||||||
|
|
@ -110,28 +142,20 @@ export default function Drawer({
|
||||||
}
|
}
|
||||||
|
|
||||||
const positionType = inBody ? 'fixed' : 'absolute';
|
const positionType = inBody ? 'fixed' : 'absolute';
|
||||||
|
|
||||||
const baseStyles: React.CSSProperties = {
|
const baseStyles: React.CSSProperties = {
|
||||||
...positionStyles,
|
...positionStyles,
|
||||||
position: positionType,
|
position: positionType,
|
||||||
zIndex,
|
zIndex,
|
||||||
boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)',
|
boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)',
|
||||||
transition: 'transform 0.3s ease-in-out',
|
transition: 'transform 0.3s ease-in-out',
|
||||||
|
// 初始状态总是先设置为隐藏,然后通过 useEffect 中的动画来显示
|
||||||
|
// 这样即使初始 open=true,也能看到从隐藏到显示的动画效果
|
||||||
transform: hiddenTransform,
|
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 = {
|
previousState.current = {
|
||||||
styles: baseStyles,
|
styles: baseStyles,
|
||||||
open,
|
open,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ type ModalProps = {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
title?: string;
|
title?: string;
|
||||||
classNames?: Record<'content' | 'overlay', string>;
|
classNames?: Partial<Record<'content' | 'overlay', string>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Modal(props: ModalProps) {
|
export default function Modal(props: ModalProps) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue