diff --git a/src/app/music/MusicClient.tsx b/src/app/music/MusicClient.tsx index 605db54..459e785 100644 --- a/src/app/music/MusicClient.tsx +++ b/src/app/music/MusicClient.tsx @@ -238,6 +238,11 @@ export default function MusicClient({ children: _children }: { children?: React. const [showPlaylist, setShowPlaylist] = useState(false); const [playlistIndex, setPlaylistIndex] = useState(-1); // 当前在播放列表中的索引 const [showQualityMenu, setShowQualityMenu] = useState(false); // 音质选择菜单 + const [showSleepTimerMenu, setShowSleepTimerMenu] = useState(false); // 睡眠定时菜单 + const [sleepTimerEndAt, setSleepTimerEndAt] = useState(null); // 睡眠定时结束时间 + const [sleepTimerRemaining, setSleepTimerRemaining] = useState(0); // 睡眠定时剩余秒数 + const [customSleepHours, setCustomSleepHours] = useState(0); // 自定义睡眠定时小时 + const [customSleepMinutes, setCustomSleepMinutes] = useState(30); // 自定义睡眠定时分钟 const [showSidebarDrawer, setShowSidebarDrawer] = useState(false); // 左侧抽屉菜单 const [showVolumeSlider, setShowVolumeSlider] = useState(false); // 音量滑块显示状态 const [pendingSongToPlay, setPendingSongToPlay] = useState<{ platform: string; id: string } | null>(null); // 待播放的歌曲信息 @@ -281,6 +286,8 @@ export default function MusicClient({ children: _children }: { children?: React. const audioRef = useRef(null); const lyricsContainerRef = useRef(null); + const sleepHoursPickerRef = useRef(null); + const sleepMinutesPickerRef = useRef(null); const lastSaveTimeRef = useRef(0); const restoredTimeRef = useRef(0); const songStartTimeRef = useRef(0); // 歌曲开始播放的时间戳 @@ -1457,6 +1464,116 @@ export default function MusicClient({ children: _children }: { children?: React. return `${mins}:${secs.toString().padStart(2, '0')}`; }; + const sleepPickerItemHeight = 40; + const sleepPickerVisibleCount = 5; + const sleepPickerHeight = sleepPickerItemHeight * sleepPickerVisibleCount; + const clampSleepPickerValue = (value: number, max: number) => Math.max(0, Math.min(max, value)); + + const formatSleepTimer = (seconds: number) => { + if (!Number.isFinite(seconds) || seconds <= 0) return '已关闭'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + if (mins >= 60) { + const hours = Math.floor(mins / 60); + const restMins = mins % 60; + return `${hours}:${restMins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + + const setSleepTimer = (minutes: number) => { + const endAt = Date.now() + minutes * 60 * 1000; + setSleepTimerEndAt(endAt); + setSleepTimerRemaining(minutes * 60); + setShowSleepTimerMenu(false); + setToast({ + message: `已设置 ${minutes} 分钟后暂停播放`, + type: 'success', + onClose: () => setToast(null), + }); + }; + + const setCustomSleepTimer = () => { + const totalMinutes = customSleepHours * 60 + customSleepMinutes; + if (totalMinutes <= 0) { + setToast({ + message: '请选择大于 0 的定时时长', + type: 'info', + onClose: () => setToast(null), + }); + return; + } + setSleepTimer(totalMinutes); + }; + + const cancelSleepTimer = () => { + setSleepTimerEndAt(null); + setSleepTimerRemaining(0); + setShowSleepTimerMenu(false); + setToast({ + message: '已关闭睡眠定时', + type: 'info', + onClose: () => setToast(null), + }); + }; + + const handleSleepHourScroll = (e: React.UIEvent) => { + const nextValue = clampSleepPickerValue(Math.round(e.currentTarget.scrollTop / sleepPickerItemHeight), 12); + if (nextValue !== customSleepHours) setCustomSleepHours(nextValue); + }; + + const handleSleepMinuteScroll = (e: React.UIEvent) => { + const nextValue = clampSleepPickerValue(Math.round(e.currentTarget.scrollTop / sleepPickerItemHeight), 59); + if (nextValue !== customSleepMinutes) setCustomSleepMinutes(nextValue); + }; + + useEffect(() => { + if (!sleepTimerEndAt) return; + + const updateSleepTimer = () => { + const remaining = Math.max(0, Math.ceil((sleepTimerEndAt - Date.now()) / 1000)); + setSleepTimerRemaining(remaining); + + if (remaining > 0) return; + + setSleepTimerEndAt(null); + setShowSleepTimerMenu(false); + + if (audioRef.current && !audioRef.current.paused) { + audioRef.current.pause(); + setIsPlaying(false); + savePlayState(); + } + + setToast({ + message: '睡眠定时结束,已暂停播放', + type: 'info', + onClose: () => setToast(null), + }); + }; + + updateSleepTimer(); + const timerId = window.setInterval(updateSleepTimer, 1000); + return () => window.clearInterval(timerId); + }, [sleepTimerEndAt]); + + useEffect(() => { + if (!showSleepTimerMenu) return; + + const scrollToSelected = (el: HTMLDivElement | null, value: number) => { + if (!el) return; + window.requestAnimationFrame(() => { + el.scrollTop = value * sleepPickerItemHeight; + }); + }; + + // 只在弹窗打开时定位一次。不要把 customSleepHours/customSleepMinutes + // 放进依赖,否则滚动触发 setState 后又反向改 scrollTop,会造成频闪。 + scrollToSelected(sleepHoursPickerRef.current, customSleepHours); + scrollToSelected(sleepMinutesPickerRef.current, customSleepMinutes); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showSleepTimerMenu]); + useEffect(() => { currentTimeRef.current = currentTime; }, [currentTime]); @@ -2213,6 +2330,25 @@ export default function MusicClient({ children: _children }: { children?: React. > {getQualityLabel()} + +

+ {sleepTimerEndAt ? `剩余 ${formatSleepTimer(sleepTimerRemaining)} 后暂停播放` : '选择倒计时时长,到点后自动暂停'} +

+ + +
+ {[15, 30, 45, 60, 90, 120].map((minutes) => ( + + ))} +
+ +
+
自定义时间
+
+ {[ + { + label: '小时', + value: customSleepHours, + ref: sleepHoursPickerRef, + onScroll: handleSleepHourScroll, + items: Array.from({ length: 13 }, (_, hour) => hour), + }, + { + label: '分钟', + value: customSleepMinutes, + ref: sleepMinutesPickerRef, + onScroll: handleSleepMinuteScroll, + items: Array.from({ length: 60 }, (_, minute) => minute), + }, + ].map((picker) => ( +
+ {picker.label} +
+
+ {picker.items.map((item) => { + const isActive = item === picker.value; + return ( + + ); + })} +
+
+
+
+
+
+ ))} +
+ +
+ +
+ + +
+
+
+ )} + {/* Quality Selection Menu */} {showQualityMenu && (