From 5ac63ef39b1562a060e000765cb9766ac119d7c3 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Wed, 18 Mar 2026 16:52:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=92=AD=E6=94=BE=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ContinueWatching.tsx | 33 +++- src/components/PlayRecordsPanel.tsx | 229 ++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 src/components/PlayRecordsPanel.tsx diff --git a/src/components/ContinueWatching.tsx b/src/components/ContinueWatching.tsx index 556e6bc..895dc26 100644 --- a/src/components/ContinueWatching.tsx +++ b/src/components/ContinueWatching.tsx @@ -1,7 +1,7 @@ /* eslint-disable no-console */ 'use client'; -import { AlertTriangle } from 'lucide-react'; +import { AlertTriangle, ChevronRight } from 'lucide-react'; import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; @@ -13,6 +13,7 @@ import { } from '@/lib/db.client'; import VideoCard from '@/components/VideoCard'; +import PlayRecordsPanel from '@/components/PlayRecordsPanel'; import VirtualScrollableRow from '@/components/VirtualScrollableRow'; interface ContinueWatchingProps { @@ -25,6 +26,7 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) { >([]); const [loading, setLoading] = useState(true); const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [showPlayRecordsPanel, setShowPlayRecordsPanel] = useState(false); // 处理播放记录数据更新的函数 const updatePlayRecords = (allRecords: Record, limit?: number) => { @@ -114,12 +116,21 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) { 继续观看 {!loading && playRecords.length > 0 && ( - +
+ + +
)} {loading ? ( @@ -281,6 +292,14 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) { , document.body )} + + {showPlayRecordsPanel && createPortal( + setShowPlayRecordsPanel(false)} + />, + document.body + )} ); } diff --git a/src/components/PlayRecordsPanel.tsx b/src/components/PlayRecordsPanel.tsx new file mode 100644 index 0000000..1090b9e --- /dev/null +++ b/src/components/PlayRecordsPanel.tsx @@ -0,0 +1,229 @@ +'use client'; + +import { AlertTriangle, History, X } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; + +import type { PlayRecord } from '@/lib/db.client'; +import { + clearAllPlayRecords, + getAllPlayRecords, + subscribeToDataUpdates, +} from '@/lib/db.client'; + +import VideoCard from '@/components/VideoCard'; + +type PlayRecordItem = PlayRecord & { + key: string; +}; + +interface PlayRecordsPanelProps { + isOpen: boolean; + onClose: () => void; +} + +const parseKey = (key: string) => { + const [source, id] = key.split('+'); + return { source, id }; +}; + +const getProgress = (record: PlayRecord) => { + if (record.total_time === 0) return 0; + return (record.play_time / record.total_time) * 100; +}; + +export default function PlayRecordsPanel({ + isOpen, + onClose, +}: PlayRecordsPanelProps) { + const [playRecords, setPlayRecords] = useState([]); + const [loading, setLoading] = useState(false); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + + const loadPlayRecords = async () => { + setLoading(true); + try { + const allRecords = await getAllPlayRecords(); + const sorted = Object.entries(allRecords) + .map(([key, record]) => ({ + ...record, + key, + })) + .sort((a, b) => b.save_time - a.save_time); + setPlayRecords(sorted); + } catch (error) { + console.error('加载播放记录失败:', error); + setPlayRecords([]); + } finally { + setLoading(false); + } + }; + + const handleClearAll = async () => { + try { + await clearAllPlayRecords(); + setPlayRecords([]); + setShowConfirmDialog(false); + } catch (error) { + console.error('清空播放记录失败:', error); + } + }; + + useEffect(() => { + if (!isOpen) return; + loadPlayRecords(); + }, [isOpen]); + + useEffect(() => { + const unsubscribe = subscribeToDataUpdates( + 'playRecordsUpdated', + (newRecords: Record) => { + if (!isOpen) return; + const sorted = Object.entries(newRecords) + .map(([key, record]) => ({ + ...record, + key, + })) + .sort((a, b) => b.save_time - a.save_time); + setPlayRecords(sorted); + } + ); + + return () => { + unsubscribe(); + }; + }, [isOpen]); + + return ( + <> +
+ +
+
+
+ +

+ 播放记录 +

+ {playRecords.length > 0 && ( + + {playRecords.length} 项 + + )} +
+
+ {playRecords.length > 0 && ( + + )} + +
+
+ +
+ {loading ? ( +
+
+
+ ) : playRecords.length === 0 ? ( +
+ +

暂无播放记录

+
+ ) : ( +
+ {playRecords.map((record) => { + const { source, id } = parseKey(record.key); + + return ( +
+ + setPlayRecords((prev) => + prev.filter((item) => item.key !== record.key) + ) + } + type={record.total_episodes > 1 ? 'tv' : ''} + origin={record.origin} + playTime={record.play_time} + totalTime={record.total_time} + /> +
+ ); + })} +
+ )} +
+
+ + {showConfirmDialog && + createPortal( +
setShowConfirmDialog(false)} + > +
e.stopPropagation()} + > +
+
+
+ +
+
+

+ 清空播放记录 +

+

+ 确定要清空所有播放记录吗?此操作不可恢复。 +

+
+
+ +
+ + +
+
+
+
, + document.body + )} + + ); +}