diff --git a/package.json b/package.json index 29f2cda..0d2d47c 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "next-themes": "^0.4.6", "node-fetch": "^2.7.0", "nprogress": "^0.2.0", + "opencc-js": "^1.0.5", "parse-torrent-name": "^0.5.4", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc08a03..917ed6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: nprogress: specifier: ^0.2.0 version: 0.2.0 + opencc-js: + specifier: ^1.0.5 + version: 1.0.5 parse-torrent-name: specifier: ^0.5.4 version: 0.5.4 @@ -1235,24 +1238,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@14.2.33': resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@14.2.33': resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@14.2.33': resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@14.2.33': resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} @@ -1821,41 +1828,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -4558,6 +4573,9 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + opencc-js@1.0.5: + resolution: {integrity: sha512-LD+1SoNnZdlRwtYTjnQdFrSVCAaYpuDqL5CkmOaHOkKoKh7mFxUicLTRVNLU5C+Jmi1vXQ3QL4jWdgSaa4sKjg==} + option-validator@2.0.6: resolution: {integrity: sha512-tmZDan2LRIRQyhUGvkff68/O0R8UmF+Btmiiz0SmSw2ng3CfPZB9wJlIjHpe/MKUZqyIZkVIXCrwr1tIN+0Dzg==} @@ -11545,6 +11563,8 @@ snapshots: dependencies: mimic-fn: 2.1.0 + opencc-js@1.0.5: {} + option-validator@2.0.6: dependencies: kind-of: 6.0.3 diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index ca0df40..c686929 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -36,6 +36,10 @@ function SearchPageClient() { const [triggerAcgSearch, setTriggerAcgSearch] = useState(false); // 用户权限 const [userRole, setUserRole] = useState<'owner' | 'admin' | 'user' | null>(null); + // 繁体转简体转换器 + const converterRef = useRef<((text: string) => string) | null>(null); + // 转换器是否已初始化 + const [converterReady, setConverterReady] = useState(false); const router = useRouter(); const searchParams = useSearchParams(); @@ -538,6 +542,26 @@ function SearchPageClient() { const authInfo = getAuthInfoFromBrowserCookie(); setUserRole(authInfo?.role || null); + // 初始化繁体转简体转换器 + if (typeof window !== 'undefined') { + import('opencc-js').then((module) => { + try { + const OpenCC = module.default || module; + const converter = OpenCC.Converter({ from: 'hk', to: 'cn' }); + converterRef.current = converter; + setConverterReady(true); + } catch (error) { + console.error('初始化繁体转简体转换器失败:', error); + setConverterReady(true); // 即使失败也设置为 true,避免阻塞 + } + }).catch((error) => { + console.error('加载 opencc-js 失败:', error); + setConverterReady(true); // 即使失败也设置为 true,避免阻塞 + }); + } else { + setConverterReady(true); + } + // 初始加载搜索历史 getSearchHistory().then(setSearchHistory); @@ -600,13 +624,41 @@ function SearchPageClient() { }, []); useEffect(() => { + // 等待转换器初始化完成 + if (!converterReady) { + return; + } + // 当搜索参数变化时更新搜索状态 - const query = searchParams.get('q') || ''; + let query = searchParams.get('q') || ''; + + // 如果开启了繁体转简体,进行转换 + if (query && typeof window !== 'undefined') { + const searchTraditionalToSimplified = localStorage.getItem('searchTraditionalToSimplified'); + + if (searchTraditionalToSimplified === 'true' && converterRef.current) { + try { + const originalQuery = query; + query = converterRef.current(query); + + // 如果转换后的文本与原文本不同,更新 URL + if (originalQuery !== query) { + const trimmedConverted = query.trim(); + // 使用 replace 而不是 push,避免在历史记录中留下繁体版本 + router.replace(`/search?q=${encodeURIComponent(trimmedConverted)}${searchParams.get('type') ? `&type=${searchParams.get('type')}` : ''}`); + return; // 等待 URL 更新后重新触发此 effect + } + } catch (error) { + console.error('[URL参数监听] 繁体转简体转换失败:', error); + } + } + } + currentQueryRef.current = query.trim(); if (query) { setSearchQuery(query); - + const trimmed = query.trim(); // 检查是否有缓存且不是强制刷新 @@ -799,7 +851,7 @@ function SearchPageClient() { setShowResults(false); setShowSuggestions(false); } - }, [searchParams, forceRefresh]); + }, [searchParams, forceRefresh, converterReady]); // 组件卸载时,关闭可能存在的连接 useEffect(() => { @@ -838,9 +890,21 @@ function SearchPageClient() { // 搜索表单提交时触发,处理搜索逻辑 const handleSearch = (e: React.FormEvent) => { e.preventDefault(); - const trimmed = searchQuery.trim().replace(/\s+/g, ' '); + let trimmed = searchQuery.trim().replace(/\s+/g, ' '); if (!trimmed) return; + // 如果开启了繁体转简体,进行转换 + if (typeof window !== 'undefined') { + const searchTraditionalToSimplified = localStorage.getItem('searchTraditionalToSimplified'); + if (searchTraditionalToSimplified === 'true' && converterRef.current) { + try { + trimmed = converterRef.current(trimmed); + } catch (error) { + console.error('繁体转简体转换失败:', error); + } + } + } + // 回显搜索框 setSearchQuery(trimmed); setShowResults(true); @@ -863,7 +927,21 @@ function SearchPageClient() { }; const handleSuggestionSelect = (suggestion: string) => { - setSearchQuery(suggestion); + let processedSuggestion = suggestion; + + // 如果开启了繁体转简体,进行转换 + if (typeof window !== 'undefined') { + const searchTraditionalToSimplified = localStorage.getItem('searchTraditionalToSimplified'); + if (searchTraditionalToSimplified === 'true' && converterRef.current) { + try { + processedSuggestion = converterRef.current(suggestion); + } catch (error) { + console.error('繁体转简体转换失败:', error); + } + } + } + + setSearchQuery(processedSuggestion); setShowSuggestions(false); // 自动执行搜索 @@ -872,15 +950,15 @@ function SearchPageClient() { // 根据当前选项卡执行不同的搜索 if (activeTab === 'video') { // 影视搜索 - router.push(`/search?q=${encodeURIComponent(suggestion)}&type=video`); + router.push(`/search?q=${encodeURIComponent(processedSuggestion)}&type=video`); // 其余由 searchParams 变化的 effect 处理 } else if (activeTab === 'pansou') { // 网盘搜索 - 触发搜索 - router.push(`/search?q=${encodeURIComponent(suggestion)}&type=pansou`); + router.push(`/search?q=${encodeURIComponent(processedSuggestion)}&type=pansou`); setTriggerPansouSearch(prev => !prev); } else if (activeTab === 'acg') { // ACG 磁力搜索 - 触发搜索 - router.push(`/search?q=${encodeURIComponent(suggestion)}&type=acg`); + router.push(`/search?q=${encodeURIComponent(processedSuggestion)}&type=acg`); setTriggerAcgSearch(prev => !prev); } }; diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx index 8cd7ba6..1f7f507 100644 --- a/src/components/UserMenu.tsx +++ b/src/components/UserMenu.tsx @@ -106,6 +106,7 @@ export const UserMenu: React.FC = () => { const [bufferStrategy, setBufferStrategy] = useState('medium'); const [nextEpisodePreCache, setNextEpisodePreCache] = useState(true); const [isBufferStrategyDropdownOpen, setIsBufferStrategyDropdownOpen] = useState(false); + const [searchTraditionalToSimplified, setSearchTraditionalToSimplified] = useState(false); // 折叠面板状态 const [isDoubanSectionOpen, setIsDoubanSectionOpen] = useState(true); @@ -378,6 +379,12 @@ export const UserMenu: React.FC = () => { console.error('解析首页模块配置失败:', error); } } + + // 加载搜索繁体转简体设置 + const savedSearchTraditionalToSimplified = localStorage.getItem('searchTraditionalToSimplified'); + if (savedSearchTraditionalToSimplified !== null) { + setSearchTraditionalToSimplified(savedSearchTraditionalToSimplified === 'true'); + } } }, []); @@ -641,6 +648,13 @@ export const UserMenu: React.FC = () => { } }; + const handleSearchTraditionalToSimplifiedToggle = (value: boolean) => { + setSearchTraditionalToSimplified(value); + if (typeof window !== 'undefined') { + localStorage.setItem('searchTraditionalToSimplified', String(value)); + } + }; + // 首页模块配置处理函数 const handleHomeModuleToggle = (id: string, enabled: boolean) => { const updatedModules = homeModules.map(module => @@ -732,6 +746,7 @@ export const UserMenu: React.FC = () => { setBufferStrategy('medium'); setNextEpisodePreCache(true); setHomeModules(defaultHomeModules); + setSearchTraditionalToSimplified(false); if (typeof window !== 'undefined') { localStorage.setItem('defaultAggregateSearch', JSON.stringify(true)); @@ -747,6 +762,7 @@ export const UserMenu: React.FC = () => { localStorage.setItem('bufferStrategy', 'medium'); localStorage.setItem('nextEpisodePreCache', 'true'); localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules)); + localStorage.setItem('searchTraditionalToSimplified', 'false'); window.dispatchEvent(new CustomEvent('homeModulesUpdated')); } }; @@ -1423,6 +1439,30 @@ export const UserMenu: React.FC = () => { + + {/* 搜索繁体转简体 */} +
+
+

+ 搜索繁体转简体 +

+

+ 搜索时自动将繁体中文转换为简体中文 +

+
+ +
)} diff --git a/src/types/opencc-js.d.ts b/src/types/opencc-js.d.ts new file mode 100644 index 0000000..07dff2a --- /dev/null +++ b/src/types/opencc-js.d.ts @@ -0,0 +1,8 @@ +declare module 'opencc-js' { + interface ConverterOptions { + from: string; + to: string; + } + + export function Converter(options: ConverterOptions): (text: string) => string; +}