From a76585c61a2b53e44f450f7b9e9626ef0dfdbef4 Mon Sep 17 00:00:00 2001 From: walle Date: Tue, 18 Nov 2025 17:10:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E5=88=97=E8=A1=A8UI=E5=B8=83=E5=B1=80=E4=B8=8E=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=88=86=E7=A6=BB=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重命名"当前选中音乐"为"当前选中的声音" - 完全分离两个模块为独立UI组件,移除互斥展开逻辑 - 优化音乐列表横向布局:音乐名称、声音名称和按钮在同一行显示 - 实现智能展开逻辑:音乐列表默认展开,超过5项时自动收起 - 增加模块间32px间距,提升视觉层次 - 修复展开按钮样式冲突,优化CSS类名结构 - 改进组件状态管理,确保模块独立性 --- .../language-switcher/language-switcher.tsx | 65 ++++-- .../selected-sounds-display.tsx | 213 +++++++++--------- src/components/sounds/sound/sound.tsx | 5 +- src/components/sounds/sounds.module.css | 82 +++++-- src/hooks/use-sound.ts | 4 +- 5 files changed, 219 insertions(+), 150 deletions(-) diff --git a/src/components/language-switcher/language-switcher.tsx b/src/components/language-switcher/language-switcher.tsx index b9290b8..3fa29fe 100644 --- a/src/components/language-switcher/language-switcher.tsx +++ b/src/components/language-switcher/language-switcher.tsx @@ -14,7 +14,8 @@ interface LanguageSwitcherProps { export function LanguageSwitcher({ className }: LanguageSwitcherProps) { const { currentLang, changeLanguage, t } = useTranslation(); const { isAuthenticated, user, login, register, logout, isLoading, checkAuth, error, clearError } = useAuthStore(); - const [isDarkTheme, setIsDarkTheme] = useState(false); + const [isDarkTheme, setIsDarkTheme] = useState(null); // 使用 null 表示未初始化 + const [isClient, setIsClient] = useState(false); // 跟踪是否在客户端 const [showAuthForm, setShowAuthForm] = useState(false); const [isLogin, setIsLogin] = useState(true); const [showNotification, setShowNotification] = useState(false); @@ -26,10 +27,17 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) { password: '', }); + // 客户端检测 + useEffect(() => { + setIsClient(true); + }, []); + // 认证状态检查 useEffect(() => { - checkAuth(); - }, []); + if (isClient) { + checkAuth(); + } + }, [isClient]); // 点击外部关闭用户菜单 useEffect(() => { @@ -63,8 +71,13 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) { }; }, []); - // 主题切换逻辑 + // 主题切换逻辑 - 确保只在客户端执行 useEffect(() => { + // 避免在 SSR 环境下执行 + if (typeof window === 'undefined' || typeof localStorage === 'undefined') { + return; + } + const savedTheme = localStorage.getItem('theme'); const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const initialDarkTheme = savedTheme ? savedTheme === 'dark' : systemPrefersDark; @@ -114,6 +127,11 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) { }; const toggleTheme = () => { + // 确保主题已初始化且在客户端环境 + if (isDarkTheme === null || typeof window === 'undefined' || typeof localStorage === 'undefined') { + return; + } + const newTheme = !isDarkTheme; setIsDarkTheme(newTheme); applyTheme(newTheme); @@ -203,23 +221,26 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) { @@ -229,10 +250,10 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) { onClick={handleAuthClick} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} - aria-label={isAuthenticated ? `用户: ${user?.username}` : '登录'} + aria-label={isClient ? (isAuthenticated ? `用户: ${user?.username}` : '登录') : '登录'} > - {isAuthenticated && user && ( + {isClient && isAuthenticated && user && ( )} - {isAuthenticated ? (user?.username || '用户') : '登录'} + {isClient ? (isAuthenticated ? (user?.username || '用户') : '登录') : '登录'} diff --git a/src/components/selected-sounds-display/selected-sounds-display.tsx b/src/components/selected-sounds-display/selected-sounds-display.tsx index 091d036..d0fa1f6 100644 --- a/src/components/selected-sounds-display/selected-sounds-display.tsx +++ b/src/components/selected-sounds-display/selected-sounds-display.tsx @@ -37,8 +37,8 @@ export function SelectedSoundsDisplay() { const [editingId, setEditingId] = useState(null); const [editingName, setEditingName] = useState(''); const [expandedMusic, setExpandedMusic] = useState>(new Set()); // 跟踪展开的音乐项 - const [expandedCurrent, setExpandedCurrent] = useState(true); // 跟踪当前选中音乐的展开状态,默认展开 - const [expandedMyMusic, setExpandedMyMusic] = useState(false); // 跟踪我的音乐展开状态,默认收起 + const [expandedCurrent, setExpandedCurrent] = useState(true); // 跟踪当前选中声音的展开状态,默认展开 + const [expandedMyMusic, setExpandedMyMusic] = useState(true); // 跟踪音乐列表展开状态,默认展开 const [error, setError] = useState(null); const [musicName, setMusicName] = useState(''); @@ -50,24 +50,18 @@ export function SelectedSoundsDisplay() { Object.keys(state.sounds).filter(id => state.sounds[id].isSelected) ); - // 互斥展开逻辑:展开当前音乐时收起我的音乐,反之亦然 + // 独立展开逻辑:两个区域可以独立展开/收起 const toggleExpandedCurrent = () => { - if (expandedCurrent) { - setExpandedCurrent(false); - } else { - setExpandedCurrent(true); - setExpandedMyMusic(false); - setExpandedMusic(new Set()); // 收起所有展开的音乐项 + setExpandedCurrent(!expandedCurrent); + if (!expandedCurrent) { + setExpandedMusic(new Set()); // 展开时收起所有展开的音乐项 } }; const toggleExpandedMyMusic = () => { - if (expandedMyMusic) { - setExpandedMyMusic(false); - } else { - setExpandedMyMusic(true); - setExpandedCurrent(false); - setExpandedMusic(new Set()); // 收起所有展开的音乐项 + setExpandedMyMusic(!expandedMyMusic); + if (!expandedMyMusic) { + setExpandedMusic(new Set()); // 展开时收起所有展开的音乐项 } }; @@ -118,13 +112,21 @@ export function SelectedSoundsDisplay() { // 根据选中的声音ID获取声音对象 const selectedSounds = useMemo(() => { return selectedSoundIds.map(id => { - const sound = sounds[id]; + // 从 localizedCategories 中查找对应的声音数据 + const allSounds = localizedCategories + .map(category => category.sounds) + .flat(); + const soundData = allSounds.find(s => s.id === id); + + if (!soundData) return null; + return { id, - ...sound + ...soundData, + ...sounds[id] // 合并状态信息(volume, speed 等) }; }).filter(Boolean); - }, [selectedSoundIds, sounds]); + }, [selectedSoundIds, sounds, localizedCategories]); // 获取音乐列表 const fetchMusicList = async () => { @@ -244,19 +246,28 @@ export function SelectedSoundsDisplay() { } }, [isAuthenticated, user]); + // 监听音乐列表数量,超过5个时默认收起 + useEffect(() => { + if (savedMusicList.length > 5) { + setExpandedMyMusic(false); + } else { + setExpandedMyMusic(true); + } + }, [savedMusicList.length]); + // 如果没有选中的声音,不渲染组件 if (selectedSounds.length === 0) { return null; } return ( -
- {/* 当前选中音乐标题区域 */} - {selectedSounds.length > 0 && ( -
-

+
+ {/* 当前选中声音模块 */} +
+
+

- 当前选中音乐 + 当前选中的声音

- )} - {/* 音乐名称配置区域 */} - {selectedSounds.length > 0 && expandedCurrent && ( -
- setMusicName(e.target.value)} - placeholder="音乐名称" - className={styles.musicNameInput} - maxLength={50} - /> - - -
- )} + {/* 音乐名称配置区域 */} + {expandedCurrent && ( +
+ setMusicName(e.target.value)} + placeholder="音乐名称" + className={styles.musicNameInput} + maxLength={50} + /> + + +
+ )} - {/* 选中的声音展示 */} - {selectedSounds.length > 0 && expandedCurrent && ( -
- - {selectedSounds.map((sound) => ( - -
- )} + {/* 选中的声音展示 */} + {expandedCurrent && ( +
+ + {selectedSounds.map((sound) => ( + +
+ )} +
- {/* 音乐列表区域 - 只有登录用户才显示 */} - {isAuthenticated && ( -
+ {/* 音乐列表模块 - 只有登录用户且有音乐时才显示 */} + {isAuthenticated && savedMusicList.length > 0 && ( +

- 我的音乐 + 音乐列表

- +
+ + +
)} @@ -434,7 +445,7 @@ export function SelectedSoundsDisplay() { {expandedMusic.has(music.id) && (
{/* 播放按钮 */} -
+