Hide optional frontend entries

This commit is contained in:
leowang 2026-05-24 21:04:34 +08:00
parent 12d88d3991
commit 6d1ada87b8
7 changed files with 111 additions and 184 deletions

View File

@ -199,6 +199,45 @@ export const Card = Object.assign(
} }
); );
export const Link = ({
children,
href,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
<a href={href} {...props}>
{children}
</a>
);
export const Tooltip = Object.assign(
({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div {...props}>{children}</div>
),
{
Trigger: ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div {...props}>{children}</div>
),
Content: ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div {...props}>{children}</div>
),
}
);
export const Badge = Object.assign(
({ children, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
<span {...props}>{children}</span>
),
{
Label: ({ children, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
<span {...props}>{children}</span>
),
}
);
export const Separator = (props: React.HTMLAttributes<HTMLHRElement>) => (
<hr {...props} />
);
const ModalRoot = ({ const ModalRoot = ({
children, children,
state, state,

View File

@ -11,10 +11,6 @@ import {
} from '@heroui/react'; } from '@heroui/react';
import { Suspense, useEffect, useState } from 'react'; import { Suspense, useEffect, useState } from 'react';
import {
BangumiCalendarData,
GetBangumiCalendarData,
} from '@/lib/bangumi.client';
// 客户端收藏 API // 客户端收藏 API
import { import {
clearAllFavorites, clearAllFavorites,
@ -38,9 +34,6 @@ function HomeClient() {
const [hotMovies, setHotMovies] = useState<DoubanItem[]>([]); const [hotMovies, setHotMovies] = useState<DoubanItem[]>([]);
const [hotTvShows, setHotTvShows] = useState<DoubanItem[]>([]); const [hotTvShows, setHotTvShows] = useState<DoubanItem[]>([]);
const [hotVarietyShows, setHotVarietyShows] = useState<DoubanItem[]>([]); const [hotVarietyShows, setHotVarietyShows] = useState<DoubanItem[]>([]);
const [bangumiCalendarData, setBangumiCalendarData] = useState<
BangumiCalendarData[]
>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { announcement } = useSite(); const { announcement } = useSite();
@ -79,7 +72,7 @@ function HomeClient() {
setLoading(true); setLoading(true);
// 并行获取热门电影、热门剧集和热门综艺 // 并行获取热门电影、热门剧集和热门综艺
const [moviesData, tvShowsData, varietyShowsData, bangumiCalendarData] = const [moviesData, tvShowsData, varietyShowsData] =
await Promise.all([ await Promise.all([
getDoubanCategories({ getDoubanCategories({
kind: 'movie', kind: 'movie',
@ -88,7 +81,6 @@ function HomeClient() {
}), }),
getDoubanCategories({ kind: 'tv', category: 'tv', type: 'tv' }), getDoubanCategories({ kind: 'tv', category: 'tv', type: 'tv' }),
getDoubanCategories({ kind: 'tv', category: 'show', type: 'show' }), getDoubanCategories({ kind: 'tv', category: 'show', type: 'show' }),
GetBangumiCalendarData(),
]); ]);
if (moviesData.code === 200) { if (moviesData.code === 200) {
@ -102,8 +94,6 @@ function HomeClient() {
if (varietyShowsData.code === 200) { if (varietyShowsData.code === 200) {
setHotVarietyShows(varietyShowsData.list); setHotVarietyShows(varietyShowsData.list);
} }
setBangumiCalendarData(bangumiCalendarData);
} catch (error) { } catch (error) {
console.error('获取推荐数据失败:', error); console.error('获取推荐数据失败:', error);
} finally { } finally {
@ -322,75 +312,6 @@ function HomeClient() {
</ScrollableRow> </ScrollableRow>
</Card> </Card>
{/* 每日新番放送 */}
<Card>
<Card.Header className='flex-row items-end justify-between gap-4'>
<div>
<Card.Description>Bangumi</Card.Description>
<Card.Title></Card.Title>
</div>
<HeroLink href='/douban?type=anime'></HeroLink>
</Card.Header>
<ScrollableRow>
{loading
? // 加载状态显示灰色占位数据
Array.from({ length: 8 }).map((_, index) => (
<div
key={index}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<Skeleton className='aspect-[2/3] w-full' />
<Skeleton className='mt-3 h-4' />
</div>
))
: // 展示当前日期的番剧
(() => {
// 获取当前日期对应的星期
const today = new Date();
const weekdays = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
];
const currentWeekday = weekdays[today.getDay()];
// 找到当前星期对应的番剧数据
const todayAnimes =
bangumiCalendarData.find(
(item) => item.weekday.en === currentWeekday
)?.items || [];
return todayAnimes.map((anime, index) => (
<div
key={`${anime.id || 0}-${index}`}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<VideoCard
from='douban'
title={anime.name_cn || anime.name || '未知标题'}
poster={
anime.images?.large ||
anime.images?.common ||
anime.images?.medium ||
anime.images?.small ||
anime.images?.grid ||
'' // 空字符串,让 VideoCard 组件处理图片加载失败
}
douban_id={anime.id || 0}
rate={anime.rating?.score?.toFixed(1) || ''}
year={anime.air_date?.split('-')?.[0] || ''}
isBangumi={true}
/>
</div>
));
})()}
</ScrollableRow>
</Card>
{/* 热门综艺 */} {/* 热门综艺 */}
<Card> <Card>
<Card.Header className='flex-row items-end justify-between gap-4'> <Card.Header className='flex-row items-end justify-between gap-4'>

View File

@ -1037,7 +1037,7 @@ function SearchPageClient() {
value={searchQuery} value={searchQuery}
onChange={handleInputChange} onChange={handleInputChange}
onFocus={handleInputFocus} onFocus={handleInputFocus}
placeholder='搜索电影、电视剧、短剧...' placeholder='搜索电影、电视剧...'
autoComplete="off" autoComplete="off"
fullWidth fullWidth
className='pl-10 pr-12' className='pl-10 pr-12'

View File

@ -2,7 +2,7 @@
'use client'; 'use client';
import { Cat, Clover, Film, Home, Play, Radio, Star, Tv } from 'lucide-react'; import { Clover, Film, Home, Star, Tv } from 'lucide-react';
import { Button, Card, ScrollShadow } from '@heroui/react'; import { Button, Card, ScrollShadow } from '@heroui/react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -33,26 +33,11 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
label: '剧集', label: '剧集',
href: '/douban?type=tv', href: '/douban?type=tv',
}, },
{
icon: Play,
label: '短剧',
href: '/shortdrama',
},
{
icon: Cat,
label: '动漫',
href: '/douban?type=anime',
},
{ {
icon: Clover, icon: Clover,
label: '综艺', label: '综艺',
href: '/douban?type=show', href: '/douban?type=show',
}, },
{
icon: Radio,
label: '直播',
href: '/live',
},
]); ]);
useEffect(() => { useEffect(() => {
@ -81,11 +66,6 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
return true; return true;
} }
// 短剧页面的特殊处理
if (href === '/shortdrama' && decodedActive.startsWith('/shortdrama')) {
return true;
}
// 豆瓣页面的类型匹配 // 豆瓣页面的类型匹配
if (decodedActive.startsWith('/douban') && typeMatch && if (decodedActive.startsWith('/douban') && typeMatch &&
decodedActive.includes(`type=${typeMatch}`)) { decodedActive.includes(`type=${typeMatch}`)) {

View File

@ -3,14 +3,11 @@
'use client'; 'use client';
import { import {
Cat,
Clover, Clover,
ExternalLink, ExternalLink,
Film, Film,
Home, Home,
Menu, Menu,
PlayCircle,
Radio,
Search, Search,
Star, Star,
Tv, Tv,
@ -176,31 +173,16 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
label: '电影', label: '电影',
href: '/douban?type=movie', href: '/douban?type=movie',
}, },
{
icon: PlayCircle,
label: '短剧',
href: '/shortdrama',
},
{ {
icon: Tv, icon: Tv,
label: '剧集', label: '剧集',
href: '/douban?type=tv', href: '/douban?type=tv',
}, },
{
icon: Cat,
label: '动漫',
href: '/douban?type=anime',
},
{ {
icon: Clover, icon: Clover,
label: '综艺', label: '综艺',
href: '/douban?type=show', href: '/douban?type=show',
}, },
{
icon: Radio,
label: '直播',
href: '/live',
},
]); ]);
useEffect(() => { useEffect(() => {
@ -327,8 +309,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
const isActive = const isActive =
decodedActive === decodedItemHref || decodedActive === decodedItemHref ||
(decodedActive.startsWith('/douban') && (decodedActive.startsWith('/douban') &&
decodedActive.includes(`type=${typeMatch}`)) || decodedActive.includes(`type=${typeMatch}`));
(item.href === '/shortdrama' && decodedActive.startsWith('/shortdrama'));
const Icon = item.icon; const Icon = item.icon;
return ( return (
<div key={item.label}> <div key={item.label}>

View File

@ -1,48 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any,react-hooks/exhaustive-deps */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client'; 'use client';
import { MessageCircle, Moon, Sun } from 'lucide-react'; import { Moon, Sun } from 'lucide-react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState } from 'react';
import { Badge } from '@heroui/react';
import { ChatModal } from './ChatModal';
import { AppIconButton } from './ui/HeroPrimitives'; import { AppIconButton } from './ui/HeroPrimitives';
import { useWebSocket } from '../hooks/useWebSocket';
import { WebSocketMessage } from '../lib/types';
export function ThemeToggle() { export function ThemeToggle() {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [isChatModalOpen, setIsChatModalOpen] = useState(false);
const [messageCount, setMessageCount] = useState(0);
const [chatCount, setChatCount] = useState(0);
const [friendRequestCount, setFriendRequestCount] = useState(0);
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const { setTheme, resolvedTheme } = useTheme(); const { setTheme, resolvedTheme } = useTheme();
const pathname = usePathname(); const pathname = usePathname();
// 不再在ThemeToggle中创建独立的WebSocket连接
// 改为依赖ChatModal传递的消息计数
// 直接使用ChatModal传来的消息计数
const handleMessageCountFromModal = useCallback((totalCount: number) => {
console.log('📊 [ThemeToggle] 收到ChatModal传来的消息计数:', totalCount);
setMessageCount(totalCount);
}, []);
// 处理聊天消息计数重置(当用户查看对话时)
const handleChatCountReset = useCallback((resetCount: number) => {
console.log('💬 [ThemeToggle] 重置聊天计数:', resetCount);
// 这些回调函数现在主要用于同步状态实际计数由ChatModal管理
}, []);
// 处理好友请求计数重置(当用户查看好友请求时)
const handleFriendRequestCountReset = useCallback((resetCount: number) => {
console.log('👥 [ThemeToggle] 重置好友请求计数:', resetCount);
// 这些回调函数现在主要用于同步状态实际计数由ChatModal管理
}, []);
const setThemeColor = (theme?: string) => { const setThemeColor = (theme?: string) => {
const meta = document.querySelector('meta[name="theme-color"]'); const meta = document.querySelector('meta[name="theme-color"]');
if (!meta) { if (!meta) {
@ -96,28 +68,9 @@ export function ThemeToggle() {
}); });
}; };
// 检查是否在登录页面
const isLoginPage = pathname === '/login';
return ( return (
<> <>
<div className={`flex items-center ${isMobile ? 'space-x-1' : 'space-x-2'}`}> <div className={`flex items-center ${isMobile ? 'space-x-1' : 'space-x-2'}`}>
{/* 聊天按钮 - 在登录页面不显示 */}
{!isLoginPage && (
<AppIconButton
onPress={() => setIsChatModalOpen(true)}
size={isMobile ? 'sm' : 'md'}
aria-label='Open chat'
>
{messageCount > 0 && (
<Badge size='sm' color='accent' variant='primary' className='absolute -right-1 -top-1'>
<Badge.Label>{messageCount > 99 ? '99+' : messageCount}</Badge.Label>
</Badge>
)}
<MessageCircle className='h-5 w-5' />
</AppIconButton>
)}
{/* 主题切换按钮 */} {/* 主题切换按钮 */}
<AppIconButton <AppIconButton
onPress={toggleTheme} onPress={toggleTheme}
@ -131,17 +84,6 @@ export function ThemeToggle() {
)} )}
</AppIconButton> </AppIconButton>
</div> </div>
{/* 聊天模态框 - 在登录页面不渲染 */}
{!isLoginPage && (
<ChatModal
isOpen={isChatModalOpen}
onClose={() => setIsChatModalOpen(false)}
onMessageCountChange={handleMessageCountFromModal}
onChatCountReset={handleChatCountReset}
onFriendRequestCountReset={handleFriendRequestCountReset}
/>
)}
</> </>
); );
} }

View File

@ -0,0 +1,64 @@
import { render, screen, waitFor } from '@testing-library/react';
import MobileBottomNav from '../MobileBottomNav';
import Sidebar from '../Sidebar';
import { ThemeToggle } from '../ThemeToggle';
const push = jest.fn();
jest.mock('next/navigation', () => ({
usePathname: () => '/',
useRouter: () => ({ push }),
useSearchParams: () => new URLSearchParams(),
}));
jest.mock('next-themes', () => ({
useTheme: () => ({
resolvedTheme: 'dark',
setTheme: jest.fn(),
}),
}));
jest.mock('../ChatModal', () => ({
ChatModal: () => <div role='dialog'></div>,
}));
describe('hidden front-end options', () => {
beforeEach(() => {
push.mockClear();
localStorage.clear();
});
it('does not render manga, short drama, or live in desktop navigation', () => {
render(<Sidebar activePath='/' />);
expect(screen.queryByRole('button', { name: '动漫' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '短剧' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '直播' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '电影' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '剧集' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '综艺' })).toBeInTheDocument();
});
it('does not render manga, short drama, or live in mobile navigation', () => {
render(<MobileBottomNav activePath='/' />);
expect(screen.queryByRole('button', { name: '动漫' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '短剧' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '直播' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '电影' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '剧集' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '综艺' })).toBeInTheDocument();
});
it('does not render the chat entry point or chat modal', async () => {
render(<ThemeToggle />);
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Toggle theme' })).toBeInTheDocument();
});
expect(screen.queryByRole('button', { name: 'Open chat' })).not.toBeInTheDocument();
expect(screen.queryByRole('dialog', { name: '聊天' })).not.toBeInTheDocument();
});
});