feat: play page && other refactor

This commit is contained in:
shinya
2025-06-19 02:40:39 +08:00
parent 3d199261a3
commit d01bc68d75
12 changed files with 1408 additions and 75 deletions

View File

@@ -0,0 +1,40 @@
import { LucideIcon } from 'lucide-react';
import Link from 'next/link';
interface CollectionCardProps {
title: string;
icon: LucideIcon;
href: string;
}
export default function CollectionCard({
title,
icon: Icon,
href,
}: CollectionCardProps) {
return (
<Link href={href} className='group block'>
<div className='relative w-full'>
{/* 长方形容器 - 调整宽高比和背景色 */}
<div className='relative aspect-[5/3] w-full overflow-hidden rounded-xl bg-gray-200 border border-gray-300/50'>
{/* 图标容器 */}
<div className='absolute inset-0 flex items-center justify-center'>
<Icon className='h-14 w-14 text-gray-600' />
</div>
{/* Hover 蒙版效果 - 参考 DemoCard */}
<div className='absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200'></div>
</div>
{/* 标题 - absolute 定位,类似 DemoCard */}
<div className='absolute top-[calc(100%+0.5rem)] left-0 right-0'>
<div className='flex flex-col items-center justify-center'>
<h3 className='text-sm font-medium text-gray-800 truncate w-full text-center'>
{title}
</h3>
</div>
</div>
</div>
</Link>
);
}

View File

@@ -3,9 +3,13 @@ import { useEffect, useRef, useState } from 'react';
interface ScrollableRowProps {
children: React.ReactNode;
scrollDistance?: number;
}
export default function ScrollableRow({ children }: ScrollableRowProps) {
export default function ScrollableRow({
children,
scrollDistance = 1000,
}: ScrollableRowProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [showLeftScroll, setShowLeftScroll] = useState(false);
const [showRightScroll, setShowRightScroll] = useState(false);
@@ -70,13 +74,19 @@ export default function ScrollableRow({ children }: ScrollableRowProps) {
const handleScrollRightClick = () => {
if (containerRef.current) {
containerRef.current.scrollBy({ left: 1000, behavior: 'smooth' });
containerRef.current.scrollBy({
left: scrollDistance,
behavior: 'smooth',
});
}
};
const handleScrollLeftClick = () => {
if (containerRef.current) {
containerRef.current.scrollBy({ left: -1000, behavior: 'smooth' });
containerRef.current.scrollBy({
left: -scrollDistance,
behavior: 'smooth',
});
}
};

View File

@@ -1,6 +1,7 @@
import { Heart } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
interface VideoCardProps {
@@ -11,6 +12,7 @@ interface VideoCardProps {
episodes?: number;
source_name: string;
progress?: number;
from?: string;
}
function CheckCircleCustom() {
@@ -73,11 +75,15 @@ export default function VideoCard({
source,
source_name,
progress,
from,
}: VideoCardProps) {
const [playHover, setPlayHover] = useState(false);
const router = useRouter();
return (
<Link href={`/detail?source=${source}&id=${id}`}>
<Link
href={`/detail?source=${source}&id=${id}${from ? `&from=${from}` : ''}`}
>
<div className='group relative w-full rounded-lg bg-transparent shadow-none flex flex-col'>
{/* 海报图片 - 2:3 比例 */}
<div className='relative aspect-[2/3] w-full overflow-hidden'>
@@ -87,12 +93,17 @@ export default function VideoCard({
<div className='absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center group'>
<div className='absolute inset-0 flex items-center justify-center'>
<div
onMouseEnter={() => setPlayHover(true)}
onMouseLeave={() => setPlayHover(false)}
className={`transition-all duration-200 ${
playHover ? 'scale-110' : ''
}`}
style={{ cursor: 'pointer' }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
router.push(`/play?source=${source}&id=${id}`);
}}
onMouseEnter={() => setPlayHover(true)}
onMouseLeave={() => setPlayHover(false)}
>
<PlayCircleSolid fillColor={playHover ? '#22c55e' : 'none'} />
</div>

View File

@@ -1,4 +1,15 @@
import { Film, Folder, Home, Menu, Search, Star, Tv } from 'lucide-react';
import {
Film,
Home,
Menu,
MessageCircleHeart,
MountainSnow,
Search,
Star,
Swords,
Tv,
VenetianMask,
} from 'lucide-react';
import Link from 'next/link';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import {
@@ -6,6 +17,7 @@ import {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useState,
} from 'react';
@@ -36,15 +48,49 @@ interface SidebarProps {
activePath?: string;
}
// 在浏览器环境下通过全局变量缓存折叠状态,避免组件重新挂载时出现初始值闪烁
declare global {
interface Window {
__sidebarCollapsed?: boolean;
}
}
const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [isCollapsed, setIsCollapsed] = useState(() => {
if (typeof window === 'undefined') return false;
const saved = localStorage.getItem('sidebarCollapsed');
return saved !== null ? JSON.parse(saved) : false;
// 若同一次 SPA 会话中已经读取过折叠状态,则直接复用,避免闪烁
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
if (
typeof window !== 'undefined' &&
typeof window.__sidebarCollapsed === 'boolean'
) {
return window.__sidebarCollapsed;
}
return false; // 默认展开
});
// 首次挂载时读取 localStorage以便刷新后仍保持上次的折叠状态
useLayoutEffect(() => {
const saved = localStorage.getItem('sidebarCollapsed');
if (saved !== null) {
const val = JSON.parse(saved);
setIsCollapsed(val);
window.__sidebarCollapsed = val;
}
}, []);
// 当折叠状态变化时,同步到 <html> data 属性,供首屏 CSS 使用
useLayoutEffect(() => {
if (typeof document !== 'undefined') {
if (isCollapsed) {
document.documentElement.dataset.sidebarCollapsed = 'true';
} else {
delete document.documentElement.dataset.sidebarCollapsed;
}
}
}, [isCollapsed]);
const [active, setActive] = useState(activePath);
useEffect(() => {
@@ -66,6 +112,9 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
const newState = !isCollapsed;
setIsCollapsed(newState);
localStorage.setItem('sidebarCollapsed', JSON.stringify(newState));
if (typeof window !== 'undefined') {
window.__sidebarCollapsed = newState;
}
onToggle?.(newState);
}, [isCollapsed, onToggle]);
@@ -93,16 +142,21 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
label: '豆瓣 Top250',
href: '/douban?type=movie&tag=top250&title=豆瓣 Top250',
},
{ icon: Folder, label: '美剧', href: '/douban?type=tv&tag=美剧' },
{ icon: Folder, label: '韩剧', href: '/douban?type=tv&tag=韩剧' },
{ icon: Folder, label: '日剧', href: '/douban?type=tv&tag=日剧' },
{ icon: Folder, label: '日漫', href: '/douban?type=tv&tag=日本动画' },
{ icon: Swords, label: '美剧', href: '/douban?type=tv&tag=美剧' },
{
icon: MessageCircleHeart,
label: '韩剧',
href: '/douban?type=tv&tag=韩剧',
},
{ icon: MountainSnow, label: '日剧', href: '/douban?type=tv&tag=日剧' },
{ icon: VenetianMask, label: '日漫', href: '/douban?type=tv&tag=日本动画' },
];
return (
<SidebarContext.Provider value={contextValue}>
<div className='flex'>
<aside
data-sidebar
className={`fixed top-0 left-0 h-screen bg-white/40 backdrop-blur-xl transition-all duration-300 border-r border-gray-200/50 z-10 shadow-lg ${
isCollapsed ? 'w-16' : 'w-64'
}`}
@@ -226,7 +280,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
</div>
</aside>
<div
className={`transition-all duration-300 ${
className={`transition-all duration-300 sidebar-offset ${
isCollapsed ? 'w-16' : 'w-64'
}`}
></div>