feat: douban page

This commit is contained in:
shinya
2025-06-18 13:55:27 +08:00
parent a1bf1839ea
commit 3d199261a3
6 changed files with 432 additions and 64 deletions

View File

@@ -0,0 +1,21 @@
const DoubanCardSkeleton = () => {
return (
<div className='w-44'>
<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 rounded-lg bg-gray-200 animate-pulse'>
<div className='absolute inset-0 bg-gray-300'></div>
</div>
{/* 信息层骨架 */}
<div className='absolute top-[calc(100%+0.5rem)] left-0 right-0'>
<div className='flex flex-col items-center justify-center'>
<div className='h-4 w-32 bg-gray-200 rounded animate-pulse mb-2'></div>
</div>
</div>
</div>
</div>
);
};
export default DoubanCardSkeleton;

View File

@@ -1,20 +0,0 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
type SkeletonProps = React.ComponentPropsWithoutRef<'div'>;
export default function Skeleton({ className, ...rest }: SkeletonProps) {
return (
<div
className={cn('animate-shimmer bg-[#f6f7f8]', className)}
style={{
backgroundImage:
'linear-gradient(to right, #f6f7f8 0%, #edeef1 20%, #f6f7f8 40%, #f6f7f8 100%)',
backgroundSize: '700px 100%',
backgroundRepeat: 'no-repeat',
}}
{...rest}
/>
);
}

View File

@@ -1,6 +1,6 @@
import { Film, Folder, Home, Menu, Search, Star, Tv } from 'lucide-react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import {
createContext,
useCallback,
@@ -26,7 +26,7 @@ const Logo = () => (
className='flex items-center justify-center h-16 select-none hover:opacity-80 transition-opacity duration-200'
>
<span className='text-2xl font-bold text-green-600 tracking-tight'>
LibreTV
MoonTV
</span>
</Link>
);
@@ -39,6 +39,7 @@ interface SidebarProps {
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');
@@ -47,8 +48,19 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
const [active, setActive] = useState(activePath);
useEffect(() => {
setActive(pathname);
}, [pathname]);
// 优先使用传入的 activePath
if (activePath) {
setActive(activePath);
} else {
// 否则使用当前路径
const getCurrentFullPath = () => {
const queryString = searchParams.toString();
return queryString ? `${pathname}?${queryString}` : pathname;
};
const fullPath = getCurrentFullPath();
setActive(fullPath);
}
}, [activePath, pathname, searchParams]);
const handleToggle = useCallback(() => {
const newState = !isCollapsed;
@@ -66,12 +78,25 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
};
const menuItems = [
{ icon: Tv, label: '电视剧', href: '/tv-shows' },
{ icon: Film, label: '电影', href: '/movies' },
{ icon: Star, label: '豆瓣 Top250', href: '/top250' },
{ icon: Folder, label: '合集', href: '/collections' },
{ icon: Star, label: '热门电影', href: '/douban/hot-movies' },
{ icon: Star, label: '热门电视剧', href: '/douban/hot-tv' },
{
icon: Film,
label: '热门电影',
href: '/douban?type=movie&tag=热门&title=热门电影',
},
{
icon: Tv,
label: '热门剧集',
href: '/douban?type=tv&tag=热门&title=热门剧集',
},
{
icon: Star,
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=日本动画' },
];
return (
@@ -158,28 +183,44 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
{/* 菜单项 */}
<div className='flex-1 overflow-y-auto px-2 pt-4'>
<div className='space-y-1'>
{menuItems.map((item) => (
<Link
key={item.href}
href={item.href}
onClick={() => setActive(item.href)}
data-active={active === item.href}
className={`group flex items-center rounded-lg px-2 py-2 pl-4 text-gray-700 hover:bg-gray-100/30 hover:text-green-600 data-[active=true]:bg-green-500/20 data-[active=true]:text-green-700 transition-colors duration-200 min-h-[40px] ${
isCollapsed
? 'w-full max-w-none mx-0'
: 'max-w-[220px] mx-auto'
} gap-3 justify-start`}
>
<div className='w-4 h-4 flex items-center justify-center'>
<item.icon className='h-4 w-4 text-gray-500 group-hover:text-green-600 group-data-[active=true]:text-green-700' />
</div>
{!isCollapsed && (
<span className='whitespace-nowrap transition-opacity duration-200 opacity-100'>
{item.label}
</span>
)}
</Link>
))}
{menuItems.map((item) => {
// 检查当前路径是否匹配这个菜单项
const typeMatch = item.href.match(/type=([^&]+)/)?.[1];
const tagMatch = item.href.match(/tag=([^&]+)/)?.[1];
// 解码URL以进行正确的比较
const decodedActive = decodeURIComponent(active);
const decodedItemHref = decodeURIComponent(item.href);
const isActive =
decodedActive === decodedItemHref ||
(decodedActive.startsWith('/douban') &&
decodedActive.includes(`type=${typeMatch}`) &&
decodedActive.includes(`tag=${tagMatch}`));
return (
<Link
key={item.href}
href={item.href}
onClick={() => setActive(item.href)}
data-active={isActive}
className={`group flex items-center rounded-lg px-2 py-2 pl-4 text-gray-700 hover:bg-gray-100/30 hover:text-green-600 data-[active=true]:bg-green-500/20 data-[active=true]:text-green-700 transition-colors duration-200 min-h-[40px] ${
isCollapsed
? 'w-full max-w-none mx-0'
: 'max-w-[220px] mx-auto'
} gap-3 justify-start`}
>
<div className='w-4 h-4 flex items-center justify-center'>
<item.icon className='h-4 w-4 text-gray-500 group-hover:text-green-600 group-data-[active=true]:text-green-700' />
</div>
{!isCollapsed && (
<span className='whitespace-nowrap transition-opacity duration-200 opacity-100'>
{item.label}
</span>
)}
</Link>
);
})}
</div>
</div>
</div>