From b38b513df03e277dcb4d621de25756c2def09eb0 Mon Sep 17 00:00:00 2001 From: shinya Date: Tue, 17 Jun 2025 21:57:42 +0800 Subject: [PATCH] feat: implement search page ui --- src/app/page.tsx | 8 +- src/app/search/page.tsx | 133 ++++++++++++++++++ .../SearchCard.tsx => card/DemoCard.tsx} | 19 ++- src/components/{video => card}/VideoCard.tsx | 0 src/components/layout/Sidebar.tsx | 73 +++++----- 5 files changed, 191 insertions(+), 42 deletions(-) create mode 100644 src/app/search/page.tsx rename src/components/{video/SearchCard.tsx => card/DemoCard.tsx} (84%) rename src/components/{video => card}/VideoCard.tsx (100%) diff --git a/src/app/page.tsx b/src/app/page.tsx index 8a8f41d..ca9a43c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,9 +3,9 @@ import { useState } from 'react'; import CapsuleSwitch from '@/components/CapsuleSwitch'; +import DemoCard from '@/components/card/DemoCard'; +import VideoCard from '@/components/card/VideoCard'; import Sidebar from '@/components/layout/Sidebar'; -import SearchCard from '@/components/video/SearchCard'; -import VideoCard from '@/components/video/VideoCard'; const defaultPoster = 'https://vip.dytt-img.com/upload/vod/20250326-1/9857e2e8581f231e24747ee32e633a3b.jpg'; @@ -105,7 +105,7 @@ export default function Home() {
{mockData.recentMovies.map((movie) => (
- +
))}
@@ -119,7 +119,7 @@ export default function Home() {
{mockData.recentTvShows.map((show) => (
- +
))}
diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx new file mode 100644 index 0000000..0a020ee --- /dev/null +++ b/src/app/search/page.tsx @@ -0,0 +1,133 @@ +'use client'; + +import { Search } from 'lucide-react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect, useRef, useState } from 'react'; + +import VideoCard from '@/components/card/VideoCard'; +import Sidebar from '@/components/layout/Sidebar'; + +// 模拟搜索历史数据 +const mockSearchHistory = ['流浪地球', '三体', '狂飙', '满江红']; + +// 模拟搜索结果数据 +const mockSearchResults = [ + { + id: 1, + title: '流浪地球2', + poster: + 'https://vip.dytt-img.com/upload/vod/20250326-1/9857e2e8581f231e24747ee32e633a3b.jpg', + type: 'movie' as const, + source: '电影天堂', + }, + { + id: 2, + title: '三体', + poster: + 'https://vip.dytt-img.com/upload/vod/20250326-1/9857e2e8581f231e24747ee32e633a3b.jpg', + type: 'tv' as const, + episodes: 30, + source: '电影天堂', + }, +]; + +export default function SearchPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [searchQuery, setSearchQuery] = useState(searchParams.get('q') || ''); + const [searchResults, setSearchResults] = useState( + [] + ); + const [showResults, setShowResults] = useState(false); + const searchInputRef = useRef(null); + + useEffect(() => { + // 自动聚焦搜索框 + searchInputRef.current?.focus(); + }, []); + + useEffect(() => { + // 当搜索参数变化时更新搜索状态 + const query = searchParams.get('q'); + if (query) { + setSearchQuery(query); + // 这里应该调用实际的搜索 API + setSearchResults(mockSearchResults); + setShowResults(true); + } else { + setShowResults(false); + } + }, [searchParams]); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + if (searchQuery.trim()) { + router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`); + } + }; + + return ( +
+ + +
+ {/* 搜索框 */} +
+
+
+ + setSearchQuery(e.target.value)} + placeholder='搜索电影、电视剧...' + className='w-full h-12 rounded-lg bg-gray-50/80 py-3 pl-10 pr-4 text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-400 focus:bg-white border border-gray-200/50 shadow-sm' + /> +
+
+
+ + {/* 搜索结果或搜索历史 */} +
+ {showResults ? ( + // 搜索结果 +
+ {searchResults.map((item) => ( +
+ +
+ ))} +
+ ) : mockSearchHistory.length > 0 ? ( + // 搜索历史 +
+

+ 搜索历史 +

+
+ {mockSearchHistory.map((item, index) => ( + + ))} +
+
+ ) : null} +
+
+
+ ); +} diff --git a/src/components/video/SearchCard.tsx b/src/components/card/DemoCard.tsx similarity index 84% rename from src/components/video/SearchCard.tsx rename to src/components/card/DemoCard.tsx index 75487fd..f71bade 100644 --- a/src/components/video/SearchCard.tsx +++ b/src/components/card/DemoCard.tsx @@ -1,8 +1,9 @@ import { Search } from 'lucide-react'; import Image from 'next/image'; +import { useRouter } from 'next/navigation'; import React, { useState } from 'react'; -interface SearchCardProps { +interface DemoCardProps { title: string; poster: string; rating?: number; @@ -54,10 +55,19 @@ function SearchCircle({ ); } -const SearchCard = ({ title, poster, episodes }: SearchCardProps) => { +const DemoCard = ({ title, poster, episodes }: DemoCardProps) => { const [hover, setHover] = useState(false); + const router = useRouter(); + + const handleClick = () => { + router.push(`/search?q=${encodeURIComponent(title)}`); + }; + return ( -
+
{/* 海报图片 - 2:3 比例 */}
{title} @@ -70,7 +80,6 @@ const SearchCard = ({ title, poster, episodes }: SearchCardProps) => { className={`transition-all duration-200 ${ hover ? 'scale-110' : '' }`} - style={{ cursor: 'pointer' }} >
@@ -95,4 +104,4 @@ const SearchCard = ({ title, poster, episodes }: SearchCardProps) => { ); }; -export default SearchCard; +export default DemoCard; diff --git a/src/components/video/VideoCard.tsx b/src/components/card/VideoCard.tsx similarity index 100% rename from src/components/video/VideoCard.tsx rename to src/components/card/VideoCard.tsx diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 6a3db17..1242928 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,23 +1,29 @@ import { Film, Folder, Home, Menu, Search, Star, Tv } from 'lucide-react'; import Link from 'next/link'; +import { useRouter } from 'next/navigation'; import { useState } from 'react'; // 可替换为你自己的 logo 图片 const Logo = () => ( -
+ LibreTV -
+ ); interface SidebarProps { onToggle?: (collapsed: boolean) => void; + activePath?: string; } -const Sidebar = ({ onToggle }: SidebarProps) => { +const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => { + const router = useRouter(); const [isCollapsed, setIsCollapsed] = useState(false); - const [active, setActive] = useState('/'); + const [active, setActive] = useState(activePath); const handleToggle = () => { const newCollapsed = !isCollapsed; @@ -25,6 +31,10 @@ const Sidebar = ({ onToggle }: SidebarProps) => { onToggle?.(newCollapsed); }; + const handleSearchClick = () => { + router.push('/search'); + }; + const menuItems = [ { icon: Tv, label: '电视剧', href: '/tv-shows' }, { icon: Film, label: '电影', href: '/movies' }, @@ -65,38 +75,14 @@ const Sidebar = ({ onToggle }: SidebarProps) => {
- {/* 搜索框 */} -
-
- {isCollapsed ? ( - - ) : ( -
- - -
- )} -
-
- - {/* 首页导航 */} -