窗中窗
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
} from '@/lib/db.client';
|
} from '@/lib/db.client';
|
||||||
import AddToPlaylistModal from '@/components/AddToPlaylistModal';
|
import AddToPlaylistModal from '@/components/AddToPlaylistModal';
|
||||||
import Toast, { ToastProps } from '@/components/Toast';
|
import Toast, { ToastProps } from '@/components/Toast';
|
||||||
|
import LyricsPiPWindow from '@/components/LyricsPiPWindow';
|
||||||
|
|
||||||
interface Song {
|
interface Song {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -55,6 +56,16 @@ interface DbRecord {
|
|||||||
pic?: string;
|
pic?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 扩展 Window 接口以支持 Document PiP API
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
documentPictureInPicture?: {
|
||||||
|
requestWindow: (options: { width: number; height: number }) => Promise<Window>;
|
||||||
|
window: Window | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function MusicPage() {
|
export default function MusicPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [currentSource, setCurrentSource] = useState<'netease' | 'qq' | 'kuwo'>('netease');
|
const [currentSource, setCurrentSource] = useState<'netease' | 'qq' | 'kuwo'>('netease');
|
||||||
@@ -112,6 +123,11 @@ export default function MusicPage() {
|
|||||||
onCancel: () => {},
|
onCancel: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// PiP 相关状态
|
||||||
|
const [showPiPLyrics, setShowPiPLyrics] = useState(false);
|
||||||
|
const [pipOpacity, setPipOpacity] = useState(0.9);
|
||||||
|
const [pipMinimized, setPipMinimized] = useState(false);
|
||||||
|
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const lyricsContainerRef = useRef<HTMLDivElement>(null);
|
const lyricsContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const lastSaveTimeRef = useRef<number>(0);
|
const lastSaveTimeRef = useRef<number>(0);
|
||||||
@@ -293,6 +309,36 @@ export default function MusicPage() {
|
|||||||
initializePlayState();
|
initializePlayState();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 恢复 PiP 偏好设置
|
||||||
|
useEffect(() => {
|
||||||
|
const savedOpacity = localStorage.getItem('lyricsPiPOpacity');
|
||||||
|
const savedMinimized = localStorage.getItem('lyricsPiPMinimized');
|
||||||
|
if (savedOpacity) setPipOpacity(parseFloat(savedOpacity));
|
||||||
|
if (savedMinimized) setPipMinimized(savedMinimized === 'true');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 监听来自 PiP 窗口的消息
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
switch (event.data.type) {
|
||||||
|
case 'PIP_OPACITY_CHANGE':
|
||||||
|
setPipOpacity(event.data.opacity);
|
||||||
|
localStorage.setItem('lyricsPiPOpacity', event.data.opacity.toString());
|
||||||
|
break;
|
||||||
|
case 'PIP_MINIMIZED_CHANGE':
|
||||||
|
setPipMinimized(event.data.minimized);
|
||||||
|
localStorage.setItem('lyricsPiPMinimized', event.data.minimized.toString());
|
||||||
|
break;
|
||||||
|
case 'PIP_CLOSE':
|
||||||
|
setShowPiPLyrics(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
return () => window.removeEventListener('message', handleMessage);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 监听播放状态变化,自动保存
|
// 监听播放状态变化,自动保存
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentSong) {
|
if (currentSong) {
|
||||||
@@ -1098,6 +1144,29 @@ export default function MusicPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// PiP 窗口管理
|
||||||
|
const togglePiPLyrics = () => {
|
||||||
|
if (!('documentPictureInPicture' in window)) {
|
||||||
|
setToast({
|
||||||
|
message: '您的浏览器不支持画中画功能,请使用 Chrome 116+ 版本',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
// 降级方案:打开全屏歌词
|
||||||
|
setShowLyrics(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSong) {
|
||||||
|
setToast({
|
||||||
|
message: '请先播放歌曲',
|
||||||
|
type: 'info',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowPiPLyrics(!showPiPLyrics);
|
||||||
|
};
|
||||||
|
|
||||||
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
||||||
|
|
||||||
const getQualityLabel = () => {
|
const getQualityLabel = () => {
|
||||||
@@ -1861,6 +1930,24 @@ export default function MusicPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* PiP 歌词按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
togglePiPLyrics();
|
||||||
|
}}
|
||||||
|
className={`transition-colors ${
|
||||||
|
showPiPLyrics
|
||||||
|
? 'text-green-500 hover:text-green-400'
|
||||||
|
: 'text-zinc-500 hover:text-white'
|
||||||
|
}`}
|
||||||
|
title={showPiPLyrics ? '关闭画中画歌词' : '画中画歌词'}
|
||||||
|
disabled={!currentSong}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 md:w-5 md:h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
{/* 添加到歌单按钮 */}
|
{/* 添加到歌单按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -2280,6 +2367,28 @@ export default function MusicPage() {
|
|||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* PiP Lyrics Window */}
|
||||||
|
{showPiPLyrics && (
|
||||||
|
<LyricsPiPWindow
|
||||||
|
currentSong={currentSong}
|
||||||
|
lyrics={lyrics}
|
||||||
|
currentLyricIndex={currentLyricIndex}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
currentTime={currentTime}
|
||||||
|
opacity={pipOpacity}
|
||||||
|
minimized={pipMinimized}
|
||||||
|
onOpacityChange={(opacity) => {
|
||||||
|
setPipOpacity(opacity);
|
||||||
|
localStorage.setItem('lyricsPiPOpacity', opacity.toString());
|
||||||
|
}}
|
||||||
|
onMinimizedChange={(minimized) => {
|
||||||
|
setPipMinimized(minimized);
|
||||||
|
localStorage.setItem('lyricsPiPMinimized', minimized.toString());
|
||||||
|
}}
|
||||||
|
onClose={() => setShowPiPLyrics(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,396 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
|
interface Song {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
artist: string;
|
||||||
|
album?: string;
|
||||||
|
pic?: string;
|
||||||
|
platform: 'netease' | 'qq' | 'kuwo';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LyricLine {
|
||||||
|
time: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LyricsPiPWindowProps {
|
||||||
|
currentSong: Song | null;
|
||||||
|
lyrics: LyricLine[];
|
||||||
|
currentLyricIndex: number;
|
||||||
|
isPlaying: boolean;
|
||||||
|
currentTime: number;
|
||||||
|
opacity: number;
|
||||||
|
minimized: boolean;
|
||||||
|
onOpacityChange: (opacity: number) => void;
|
||||||
|
onMinimizedChange: (minimized: boolean) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PiPLyricsContentProps {
|
||||||
|
currentSong: Song | null;
|
||||||
|
lyrics: LyricLine[];
|
||||||
|
currentLyricIndex: number;
|
||||||
|
opacity: number;
|
||||||
|
minimized: boolean;
|
||||||
|
onOpacityChange: (opacity: number) => void;
|
||||||
|
onMinimizedChange: (minimized: boolean) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PiP 窗口内容组件
|
||||||
|
const PiPLyricsContent = ({
|
||||||
|
currentSong,
|
||||||
|
lyrics,
|
||||||
|
currentLyricIndex,
|
||||||
|
opacity,
|
||||||
|
minimized,
|
||||||
|
onOpacityChange,
|
||||||
|
onMinimizedChange,
|
||||||
|
onClose,
|
||||||
|
}: PiPLyricsContentProps) => {
|
||||||
|
const lyricsContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 自动滚动到当前歌词
|
||||||
|
useEffect(() => {
|
||||||
|
if (lyricsContainerRef.current && currentLyricIndex >= 0 && !minimized) {
|
||||||
|
const container = lyricsContainerRef.current;
|
||||||
|
const currentLine = container.children[currentLyricIndex] as HTMLElement;
|
||||||
|
if (currentLine) {
|
||||||
|
const containerHeight = container.clientHeight;
|
||||||
|
const lineTop = currentLine.offsetTop;
|
||||||
|
const lineHeight = currentLine.clientHeight;
|
||||||
|
const scrollTop = lineTop - containerHeight / 2 + lineHeight / 2;
|
||||||
|
container.scrollTo({ top: scrollTop, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentLyricIndex, minimized]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="pip-container"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `rgba(0, 0, 0, ${opacity})`,
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: '16px',
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 头部:歌曲信息 + 控制按钮 */}
|
||||||
|
<div
|
||||||
|
className="pip-header"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '12px',
|
||||||
|
paddingBottom: '8px',
|
||||||
|
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
gap: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
opacity: 0.7,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentSong ? `${currentSong.name} - ${currentSong.artist}` : '暂无播放'}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexShrink: 0 }}>
|
||||||
|
{/* 透明度滑块 */}
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.3"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
value={opacity}
|
||||||
|
onChange={(e) => onOpacityChange(parseFloat(e.target.value))}
|
||||||
|
style={{ width: '60px', cursor: 'pointer' }}
|
||||||
|
title={`透明度: ${Math.round(opacity * 100)}%`}
|
||||||
|
/>
|
||||||
|
{/* 最小化按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={() => onMinimizedChange(!minimized)}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
color: 'white',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '11px',
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
|
||||||
|
}}
|
||||||
|
title={minimized ? '展开' : '最小化'}
|
||||||
|
>
|
||||||
|
{minimized ? '展开' : '最小化'}
|
||||||
|
</button>
|
||||||
|
{/* 关闭按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(239, 68, 68, 0.8)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
color: 'white',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = 'rgba(239, 68, 68, 1)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.8)';
|
||||||
|
}}
|
||||||
|
title="关闭"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 歌词内容 */}
|
||||||
|
{minimized ? (
|
||||||
|
// 最小化模式:仅显示当前歌词
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '18px',
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '0 16px',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{lyrics.length > 0 && currentLyricIndex >= 0
|
||||||
|
? lyrics[currentLyricIndex]?.text || '♪'
|
||||||
|
: currentSong
|
||||||
|
? '暂无歌词'
|
||||||
|
: '请播放歌曲'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 完整模式:显示所有歌词
|
||||||
|
<div
|
||||||
|
ref={lyricsContainerRef}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
paddingTop: '20px',
|
||||||
|
paddingBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{lyrics.length > 0 ? (
|
||||||
|
lyrics.map((line, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
padding: '8px 0',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: index === currentLyricIndex ? '16px' : '14px',
|
||||||
|
opacity: index === currentLyricIndex ? 1 : 0.5,
|
||||||
|
color: index === currentLyricIndex ? '#22c55e' : 'white',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
fontWeight: index === currentLyricIndex ? 'bold' : 'normal',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{line.text}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '14px',
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentSong ? '暂无歌词' : '请播放歌曲'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 复制样式表到 PiP 窗口
|
||||||
|
const copyStylesToPiPWindow = (pipWin: Window) => {
|
||||||
|
const styleSheets = Array.from(document.styleSheets);
|
||||||
|
styleSheets.forEach((styleSheet) => {
|
||||||
|
try {
|
||||||
|
const cssRules = Array.from(styleSheet.cssRules)
|
||||||
|
.map((rule) => rule.cssText)
|
||||||
|
.join('');
|
||||||
|
const style = pipWin.document.createElement('style');
|
||||||
|
style.textContent = cssRules;
|
||||||
|
pipWin.document.head.appendChild(style);
|
||||||
|
} catch (e) {
|
||||||
|
// 跨域样式表使用 link 标签
|
||||||
|
if ((styleSheet as any).href) {
|
||||||
|
const link = pipWin.document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = (styleSheet as any).href;
|
||||||
|
pipWin.document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主组件:管理 PiP 窗口
|
||||||
|
export default function LyricsPiPWindow({
|
||||||
|
currentSong,
|
||||||
|
lyrics,
|
||||||
|
currentLyricIndex,
|
||||||
|
isPlaying,
|
||||||
|
currentTime,
|
||||||
|
opacity,
|
||||||
|
minimized,
|
||||||
|
onOpacityChange,
|
||||||
|
onMinimizedChange,
|
||||||
|
onClose,
|
||||||
|
}: LyricsPiPWindowProps) {
|
||||||
|
const pipWindowRef = useRef<Window | null>(null);
|
||||||
|
const rootRef = useRef<ReactDOM.Root | null>(null);
|
||||||
|
|
||||||
|
// 渲染 PiP 内容
|
||||||
|
const renderPiPContent = (pipWin: Window) => {
|
||||||
|
const container = pipWin.document.createElement('div');
|
||||||
|
container.id = 'pip-lyrics-root';
|
||||||
|
pipWin.document.body.appendChild(container);
|
||||||
|
|
||||||
|
// 设置 body 样式
|
||||||
|
pipWin.document.body.style.margin = '0';
|
||||||
|
pipWin.document.body.style.padding = '0';
|
||||||
|
pipWin.document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
// 使用 ReactDOM 渲染组件到 PiP 窗口
|
||||||
|
const root = ReactDOM.createRoot(container);
|
||||||
|
rootRef.current = root;
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<PiPLyricsContent
|
||||||
|
currentSong={currentSong}
|
||||||
|
lyrics={lyrics}
|
||||||
|
currentLyricIndex={currentLyricIndex}
|
||||||
|
opacity={opacity}
|
||||||
|
minimized={minimized}
|
||||||
|
onOpacityChange={(newOpacity) => {
|
||||||
|
window.postMessage({ type: 'PIP_OPACITY_CHANGE', opacity: newOpacity }, '*');
|
||||||
|
}}
|
||||||
|
onMinimizedChange={(newMinimized) => {
|
||||||
|
window.postMessage({ type: 'PIP_MINIMIZED_CHANGE', minimized: newMinimized }, '*');
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
window.postMessage({ type: 'PIP_CLOSE' }, '*');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新 PiP 内容
|
||||||
|
useEffect(() => {
|
||||||
|
if (pipWindowRef.current && !pipWindowRef.current.closed && rootRef.current) {
|
||||||
|
rootRef.current.render(
|
||||||
|
<PiPLyricsContent
|
||||||
|
currentSong={currentSong}
|
||||||
|
lyrics={lyrics}
|
||||||
|
currentLyricIndex={currentLyricIndex}
|
||||||
|
opacity={opacity}
|
||||||
|
minimized={minimized}
|
||||||
|
onOpacityChange={(newOpacity) => {
|
||||||
|
window.postMessage({ type: 'PIP_OPACITY_CHANGE', opacity: newOpacity }, '*');
|
||||||
|
}}
|
||||||
|
onMinimizedChange={(newMinimized) => {
|
||||||
|
window.postMessage({ type: 'PIP_MINIMIZED_CHANGE', minimized: newMinimized }, '*');
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
window.postMessage({ type: 'PIP_CLOSE' }, '*');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [currentSong, lyrics, currentLyricIndex, opacity, minimized]);
|
||||||
|
|
||||||
|
// 打开 PiP 窗口
|
||||||
|
useEffect(() => {
|
||||||
|
const openPiPWindow = async () => {
|
||||||
|
if (!('documentPictureInPicture' in window)) {
|
||||||
|
console.error('浏览器不支持 Document Picture-in-Picture API');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pipWin = await (window as any).documentPictureInPicture.requestWindow({
|
||||||
|
width: 400,
|
||||||
|
height: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
pipWindowRef.current = pipWin;
|
||||||
|
|
||||||
|
// 复制样式表到 PiP 窗口
|
||||||
|
copyStylesToPiPWindow(pipWin);
|
||||||
|
|
||||||
|
// 监听窗口关闭
|
||||||
|
pipWin.addEventListener('pagehide', () => {
|
||||||
|
if (rootRef.current) {
|
||||||
|
rootRef.current.unmount();
|
||||||
|
rootRef.current = null;
|
||||||
|
}
|
||||||
|
pipWindowRef.current = null;
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 渲染内容
|
||||||
|
renderPiPContent(pipWin);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开画中画窗口失败:', error);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
openPiPWindow();
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
return () => {
|
||||||
|
if (pipWindowRef.current && !pipWindowRef.current.closed) {
|
||||||
|
pipWindowRef.current.close();
|
||||||
|
}
|
||||||
|
if (rootRef.current) {
|
||||||
|
rootRef.current.unmount();
|
||||||
|
rootRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []); // 只在组件挂载时执行一次
|
||||||
|
|
||||||
|
return null; // 此组件不渲染任何内容到主窗口
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user