diff --git a/src/app/(main)/character/[id]/chat/Right/index.tsx b/src/app/(main)/character/[id]/chat/Right/index.tsx index 08df540..9f98a58 100644 --- a/src/app/(main)/character/[id]/chat/Right/index.tsx +++ b/src/app/(main)/character/[id]/chat/Right/index.tsx @@ -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(() => { <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 />} /> diff --git a/src/app/(main)/character/[id]/chat/atoms.ts b/src/app/(main)/character/[id]/chat/atoms.ts index fedd2d8..109dac2 100644 --- a/src/app/(main)/character/[id]/chat/atoms.ts +++ b/src/app/(main)/character/[id]/chat/atoms.ts @@ -1,7 +1,7 @@ import { atom } from 'jotai'; // 是否打开两侧的设置 -export const settingOpenAtom = atom(false); +export const settingOpenAtom = atom(true); // 是否是立绘模式 export const isPortraitModeAtom = atom(false); diff --git a/src/app/(main)/character/[id]/chat/page.tsx b/src/app/(main)/character/[id]/chat/page.tsx index e6dc791..ab51d0e 100644 --- a/src/app/(main)/character/[id]/chat/page.tsx +++ b/src/app/(main)/character/[id]/chat/page.tsx @@ -12,6 +12,7 @@ import { ExitFullScreenIcon, FullScreenIcon } from '@/assets/common'; export default function CharacterChat() { const [settingOpen, setSettingOpen] = useAtom(settingOpenAtom); + console.log('settingOpen', settingOpen); return (
+ } + > + BubbleSelectDialog + + ); +} diff --git a/src/components/feature/ModelSelectDialog.tsx b/src/components/feature/ModelSelectDialog.tsx index f52f083..1c42512 100644 --- a/src/components/feature/ModelSelectDialog.tsx +++ b/src/components/feature/ModelSelectDialog.tsx @@ -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) { } > - Content +
+ {options?.map((i) => { + return
{i.label}
; + })} +
); } diff --git a/src/components/feature/VoiceActorSelectDialog.tsx b/src/components/feature/VoiceActorSelectDialog.tsx new file mode 100644 index 0000000..7d81f18 --- /dev/null +++ b/src/components/feature/VoiceActorSelectDialog.tsx @@ -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 ( + + } + > +
+ {options?.map((i) => { + return ( +
+
+
+ avator +
+
+
{'Name 1'}
+
+ {'Anime-Style Girl'} +
+
+
+
+
+
+
+ ); + })} +
+
+ ); +} diff --git a/src/components/index.tsx b/src/components/index.tsx index 5b75915..e6bacc4 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -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'; diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx index 88085cf..f909aa8 100644 --- a/src/components/ui/drawer.tsx +++ b/src/components/ui/drawer.tsx @@ -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(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, diff --git a/src/components/ui/modal/index.tsx b/src/components/ui/modal/index.tsx index 9e9b916..d8389cc 100644 --- a/src/components/ui/modal/index.tsx +++ b/src/components/ui/modal/index.tsx @@ -12,7 +12,7 @@ type ModalProps = { children?: React.ReactNode; trigger?: React.ReactNode; title?: string; - classNames?: Record<'content' | 'overlay', string>; + classNames?: Partial>; }; export default function Modal(props: ModalProps) {