From 88e48b859901bcac1456901d42d049f9fc06d85d Mon Sep 17 00:00:00 2001 From: katelya Date: Thu, 4 Sep 2025 22:32:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=95=B4=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=88=90=E4=BA=BA=E5=86=85=E5=AE=B9=E8=BF=87=E6=BB=A4=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E7=9A=84=E5=89=8D=E7=AB=AF=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改用户菜单设置按钮导航到/settings页面 - 增强搜索页面支持用户认证和内容过滤 - 添加分组结果显示:常规内容和成人内容分标签显示 - 在搜索API调用中包含用户认证信息 - 支持成人内容分组展示和警告提示 - 保持原有聚合搜索功能的兼容性 现在用户可以: 1. 在设置页面控制成人内容过滤开关 2. 在搜索结果中看到内容分组(当存在成人内容时) 3. 获得个性化的搜索体验 --- src/app/search/page.tsx | 202 +++++++++++++++++++++++------------- src/components/UserMenu.tsx | 2 +- 2 files changed, 131 insertions(+), 73 deletions(-) diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 36dba7d..7f78837 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -3,8 +3,9 @@ import { ChevronUp, Search, X } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; -import { Suspense, useEffect, useMemo, useState } from 'react'; +import { Suspense, useEffect, useState } from 'react'; +import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import { addSearchHistory, clearSearchHistory, @@ -29,6 +30,15 @@ function SearchPageClient() { const [isLoading, setIsLoading] = useState(false); const [showResults, setShowResults] = useState(false); const [searchResults, setSearchResults] = useState([]); + + // 分组结果状态 + const [groupedResults, setGroupedResults] = useState<{ + regular: SearchResult[]; + adult: SearchResult[]; + } | null>(null); + + // 分组标签页状态 + const [activeTab, setActiveTab] = useState<'regular' | 'adult'>('regular'); // 获取默认聚合设置:只读取用户本地设置,默认为 true const getDefaultAggregate = () => { @@ -45,11 +55,11 @@ function SearchPageClient() { return getDefaultAggregate() ? 'agg' : 'all'; }); - // 聚合后的结果(按标题和年份分组) - const aggregatedResults = useMemo(() => { + // 聚合函数 + const aggregateResults = (results: SearchResult[]) => { const map = new Map(); - searchResults.forEach((item) => { - // 使用 title + year + type 作为键,year 必然存在,但依然兜底 'unknown' + results.forEach((item) => { + // 使用 title + year + type 作为键 const key = `${item.title.replaceAll(' ', '')}-${ item.year || 'unknown' }-${item.episodes.length === 1 ? 'movie' : 'tv'}`; @@ -73,23 +83,21 @@ function SearchPageClient() { if (a[1][0].year === b[1][0].year) { return a[0].localeCompare(b[0]); } else { - // 处理 unknown 的情况 const aYear = a[1][0].year; const bYear = b[1][0].year; if (aYear === 'unknown' && bYear === 'unknown') { return 0; } else if (aYear === 'unknown') { - return 1; // a 排在后面 + return 1; } else if (bYear === 'unknown') { - return -1; // b 排在后面 + return -1; } else { - // 都是数字年份,按数字大小排序(大的在前面) return aYear > bYear ? -1 : 1; } } }); - }, [searchResults]); + }; useEffect(() => { // 无搜索参数时聚焦搜索框 @@ -161,39 +169,39 @@ function SearchPageClient() { const fetchSearchResults = async (query: string) => { try { setIsLoading(true); + + // 获取用户认证信息 + const authInfo = getAuthInfoFromBrowserCookie(); + + // 构建请求头 + const headers: HeadersInit = {}; + if (authInfo?.username) { + headers['Authorization'] = `Bearer ${authInfo.username}`; + } + const response = await fetch( - `/api/search?q=${encodeURIComponent(query.trim())}` + `/api/search?q=${encodeURIComponent(query.trim())}`, + { headers } ); const data = await response.json(); - setSearchResults( - data.results.sort((a: SearchResult, b: SearchResult) => { - // 优先排序:标题与搜索词完全一致的排在前面 - const aExactMatch = a.title === query.trim(); - const bExactMatch = b.title === query.trim(); - - if (aExactMatch && !bExactMatch) return -1; - if (!aExactMatch && bExactMatch) return 1; - - // 如果都匹配或都不匹配,则按原来的逻辑排序 - if (a.year === b.year) { - return a.title.localeCompare(b.title); - } else { - // 处理 unknown 的情况 - if (a.year === 'unknown' && b.year === 'unknown') { - return 0; - } else if (a.year === 'unknown') { - return 1; // a 排在后面 - } else if (b.year === 'unknown') { - return -1; // b 排在后面 - } else { - // 都是数字年份,按数字大小排序(大的在前面) - return parseInt(a.year) > parseInt(b.year) ? -1 : 1; - } - } - }) - ); + + // 如果返回了分组结果,我们需要处理这种格式 + if (data.grouped) { + // 处理分组结果 + setGroupedResults({ + regular: data.regular || [], + adult: data.adult || [] + }); + setSearchResults([...(data.regular || []), ...(data.adult || [])]); + } else { + // 处理普通结果 + setGroupedResults(null); + setSearchResults(data.results || []); + } + setShowResults(true); } catch (error) { + setGroupedResults(null); setSearchResults([]); } finally { setIsLoading(false); @@ -284,50 +292,100 @@ function SearchPageClient() { + + {/* 如果有分组结果且有成人内容,显示分组标签 */} + {groupedResults && groupedResults.adult.length > 0 && ( +
+
+
+ + +
+
+ {activeTab === 'adult' && ( +
+

+ ⚠️ 以下内容可能包含成人资源,请确保您已年满18周岁 +

+
+ )} +
+ )}
- {viewMode === 'agg' - ? aggregatedResults.map(([mapKey, group]) => { - return ( -
- -
- ); - }) - : searchResults.map((item) => ( -
+ {(() => { + // 确定要显示的结果 + let displayResults = searchResults; + if (groupedResults && groupedResults.adult.length > 0) { + displayResults = activeTab === 'adult' + ? groupedResults.adult + : groupedResults.regular; + } + + // 聚合显示模式 + if (viewMode === 'agg') { + const aggregated = aggregateResults(displayResults); + return aggregated.map(([mapKey, group]: [string, SearchResult[]]) => ( +
1 ? 'tv' : 'movie'} />
- ))} + )); + } + + // 列表显示模式 + return displayResults.map((item) => ( +
+ 1 ? 'tv' : 'movie'} + /> +
+ )); + })()} {searchResults.length === 0 && (
未找到相关结果 diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx index a60cd92..04d6099 100644 --- a/src/components/UserMenu.tsx +++ b/src/components/UserMenu.tsx @@ -210,7 +210,7 @@ export const UserMenu: React.FC = () => { const handleSettings = () => { setIsOpen(false); - setIsSettingsOpen(true); + router.push('/settings'); }; const handleCloseSettings = () => {