From 6177e646842cb81ce8550db87c844e037b7513bb Mon Sep 17 00:00:00 2001
From: liuyonghe0111 <1763195287@qq.com>
Date: Mon, 3 Nov 2025 19:32:52 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96drawer,=20=E5=AE=8C?=
=?UTF-8?q?=E5=96=84=E4=B8=80=E4=B8=8B=E9=9D=99=E6=80=81=E6=A0=B7=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../character/[id]/chat/Right/index.tsx | 19 ++---
src/app/(main)/character/[id]/chat/atoms.ts | 2 +-
src/app/(main)/character/[id]/chat/page.tsx | 1 +
src/components/feature/BubbleSelectDialog.tsx | 18 ++++
src/components/feature/ModelSelectDialog.tsx | 7 +-
.../feature/VoiceActorSelectDialog.tsx | 66 +++++++++++++++
src/components/index.tsx | 1 -
src/components/ui/drawer.tsx | 82 ++++++++++++-------
src/components/ui/modal/index.tsx | 2 +-
9 files changed, 151 insertions(+), 47 deletions(-)
create mode 100644 src/components/feature/BubbleSelectDialog.tsx
create mode 100644 src/components/feature/VoiceActorSelectDialog.tsx
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(() => {
(
-
- )}
+ render={({ value, onChange }) => }
/>
{
/>
(
-
- )}
+ render={({ value, onChange }) => }
/>
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 (
+
+
+
+
+
+
+
{'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) {