feat: dark mode

This commit is contained in:
shinya
2025-06-26 21:00:11 +08:00
parent 3652bf3e6b
commit d677ca9877
23 changed files with 234 additions and 78 deletions

View File

@@ -143,7 +143,7 @@ function AggregatePageClient() {
className='absolute top-0 left-0 -translate-x-[40%] -translate-y-[30%] sm:-translate-x-[180%] sm:-translate-y-1/2 p-2 rounded transition-colors'
>
<svg
className='h-5 w-5 text-gray-500 hover:text-green-600 transition-colors'
className='h-5 w-5 text-gray-500 hover:text-green-600 dark:text-gray-400 dark:hover:text-green-500 transition-colors'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
@@ -216,7 +216,7 @@ function AggregatePageClient() {
}&title=${encodeURIComponent(src.title)}${
src.year ? `&year=${src.year}` : ''
}&from=aggregate`}
className='relative flex items-center justify-center w-full h-14 bg-gray-500/80 hover:bg-green-500 rounded-lg transition-colors'
className='relative flex items-center justify-center w-full h-14 bg-gray-500/80 hover:bg-green-500 dark:bg-gray-700/80 dark:hover:bg-green-600 rounded-lg transition-colors'
>
{/* 名称 */}
<span className='px-1 text-white text-sm font-medium truncate whitespace-nowrap'>

View File

@@ -137,7 +137,7 @@ function DetailPageClient() {
className='absolute top-0 left-0 -translate-x-[40%] -translate-y-[30%] sm:-translate-x-[180%] sm:-translate-y-1/2 p-2 rounded transition-colors'
>
<svg
className='h-5 w-5 text-gray-500 hover:text-green-600 transition-colors'
className='h-5 w-5 text-gray-500 hover:text-green-600 dark:text-gray-400 dark:hover:text-green-500 transition-colors'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
@@ -221,7 +221,7 @@ function DetailPageClient() {
? `&year=${detail.year || fallbackYear}`
: ''
}`}
className='hidden sm:flex items-center justify-center gap-2 px-6 py-2 bg-gray-500 hover:bg-gray-600 rounded-lg transition-colors text-white'
className='hidden sm:flex items-center justify-center gap-2 px-6 py-2 bg-gray-500 hover:bg-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg transition-colors text-white'
>
<div className='w-0 h-0 border-t-[6px] border-t-transparent border-l-[10px] border-l-white border-b-[6px] border-b-transparent'></div>
<span></span>
@@ -252,10 +252,10 @@ function DetailPageClient() {
{/* 爱心按钮 */}
<button
onClick={handleToggleFavorite}
className={`flex items-center justify-center w-10 h-10 rounded-full transition-colors ${
className={`flex items-center justify-center w-10 h-10 rounded-full transition-colors ${
favorited
? 'bg-gray-300 hover:bg-gray-400'
: 'bg-gray-400 hover:bg-gray-500'
? 'bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500'
: 'bg-gray-400 hover:bg-gray-500 dark:bg-gray-700 dark:hover:bg-gray-600'
}`}
>
<Heart
@@ -281,7 +281,7 @@ function DetailPageClient() {
></div>
</div>
{/* 剩余时间 */}
<span className='text-gray-600/60 text-xs whitespace-nowrap'>
<span className='text-gray-600/60 dark:text-gray-400/60 text-xs whitespace-nowrap'>
{playRecord.total_episodes > 1
? `${playRecord.index}集 剩余 `
: '剩余 '}
@@ -318,12 +318,12 @@ function DetailPageClient() {
'source'
)}&id=${searchParams.get('id')}&index=${
idx + 1
}&title=${encodeURIComponent(detail.title)}${
}&position=0&title=${encodeURIComponent(detail.title)}${
detail.year || fallbackYear
? `&year=${detail.year || fallbackYear}`
: ''
}`}
className='bg-gray-500/80 hover:bg-green-500 text-white px-5 py-2 rounded-lg transition-colors text-base font-medium w-24 text-center'
className='bg-gray-500/80 hover:bg-green-500 dark:bg-gray-700/80 dark:hover:bg-green-600 text-white px-5 py-2 rounded-lg transition-colors text-base font-medium w-24 text-center'
>
{idx + 1}
</a>

View File

@@ -183,10 +183,10 @@ function DoubanPageClient() {
<div className='px-4 sm:px-10 py-4 sm:py-8 overflow-visible'>
{/* 页面标题 */}
<div className='mb-8'>
<h1 className='text-3xl font-bold text-gray-800 mb-2'>
<h1 className='text-3xl font-bold text-gray-800 mb-2 dark:text-gray-200'>
{getPageTitle()}
</h1>
<p className='text-gray-600'></p>
<p className='text-gray-600 dark:text-gray-400'></p>
</div>
{/* 内容展示区域 */}

View File

@@ -27,6 +27,9 @@ body {
body {
color: rgb(var(--foreground-rgb));
}
html:not(.dark) body {
background: linear-gradient(
180deg,
#e6f3fb 0%,
@@ -99,3 +102,47 @@ body {
*::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
/* View Transitions API 动画 */
@keyframes slide-from-top {
from {
clip-path: polygon(0 0, 100% 0, 100% 0, 0 0);
}
to {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
}
@keyframes slide-from-bottom {
from {
clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0 100%);
}
to {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.8s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
animation-fill-mode: both;
}
/*
切换时,旧的视图不应该有动画,它应该在下面,等待被新的视图覆盖。
这可以防止在动画完成前,页面底部提前变色。
*/
::view-transition-old(root) {
animation: none;
}
/* 从浅色到深色:新内容(深色)从顶部滑入 */
html.dark::view-transition-new(root) {
animation-name: slide-from-top;
}
/* 从深色到浅色:新内容(浅色)从底部滑入 */
html:not(.dark)::view-transition-new(root) {
animation-name: slide-from-bottom;
}

View File

@@ -4,6 +4,7 @@ import { Inter } from 'next/font/google';
import './globals.css';
import AuthProvider from '../components/AuthProvider';
import { ThemeProvider } from '../components/ThemeProvider';
const inter = Inter({ subsets: ['latin'] });
@@ -19,12 +20,19 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang='zh-CN'>
<head>
<meta name='theme-color' content='#f9fbfe' />
</head>
<body className={`${inter.className} min-h-screen text-gray-900`}>
<AuthProvider>{children}</AuthProvider>
<html lang='zh-CN' suppressHydrationWarning>
<head />
<body
className={`${inter.className} min-h-screen bg-white text-gray-900 dark:bg-black dark:text-gray-200`}
>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
</body>
</html>
);

View File

@@ -119,10 +119,12 @@ function HomeClient() {
// 收藏夹视图
<section className='mb-8'>
<div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800'></h2>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h2>
{favoriteItems.length > 0 && (
<button
className='text-sm text-gray-500 hover:text-gray-700'
className='text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
onClick={async () => {
await clearAllFavorites();
setFavoriteItems([]);
@@ -139,7 +141,7 @@ function HomeClient() {
</div>
))}
{favoriteItems.length === 0 && (
<div className='col-span-full text-center text-gray-500 py-8'>
<div className='col-span-full text-center text-gray-500 py-8 dark:text-gray-400'>
</div>
)}
@@ -154,10 +156,12 @@ function HomeClient() {
{/* 热门电影 */}
<section className='mb-8'>
<div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800'></h2>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h2>
<Link
href='/douban?type=movie&tag=热门&title=热门电影'
className='flex items-center text-sm text-gray-500 hover:text-gray-700'
className='flex items-center text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
>
<ChevronRight className='w-4 h-4 ml-1' />
@@ -171,10 +175,10 @@ function HomeClient() {
key={index}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<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 className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse dark:bg-gray-800'>
<div className='absolute inset-0 bg-gray-300 dark:bg-gray-700'></div>
</div>
<div className='mt-2 h-4 bg-gray-200 rounded animate-pulse'></div>
<div className='mt-2 h-4 bg-gray-200 rounded animate-pulse dark:bg-gray-800'></div>
</div>
))
: // 显示真实数据
@@ -196,10 +200,12 @@ function HomeClient() {
{/* 热门剧集 */}
<section className='mb-8'>
<div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800'></h2>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h2>
<Link
href='/douban?type=tv&tag=热门&title=热门剧集'
className='flex items-center text-sm text-gray-500 hover:text-gray-700'
className='flex items-center text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
>
<ChevronRight className='w-4 h-4 ml-1' />
@@ -213,10 +219,10 @@ function HomeClient() {
key={index}
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<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 className='relative aspect-[2/3] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse dark:bg-gray-800'>
<div className='absolute inset-0 bg-gray-300 dark:bg-gray-700'></div>
</div>
<div className='mt-2 h-4 bg-gray-200 rounded animate-pulse'></div>
<div className='mt-2 h-4 bg-gray-200 rounded animate-pulse dark:bg-gray-800'></div>
</div>
))
: // 显示真实数据

View File

@@ -135,14 +135,14 @@ function SearchPageClient() {
<div className='mb-8'>
<form onSubmit={handleSearch} className='max-w-2xl mx-auto'>
<div className='relative'>
<Search className='absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-gray-400' />
<Search className='absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-gray-400 dark:text-gray-500' />
<input
ref={searchInputRef}
type='text'
value={searchQuery}
onChange={(e) => 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'
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 dark:bg-gray-800 dark:text-gray-300 dark:placeholder-gray-500 dark:focus:bg-gray-700 dark:border-gray-700'
/>
</div>
</form>
@@ -158,10 +158,14 @@ function SearchPageClient() {
<section className='mb-12'>
{/* 标题 + 聚合开关 */}
<div className='mb-8 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800'></h2>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h2>
{/* 聚合开关 */}
<label className='flex items-center gap-2 cursor-pointer select-none'>
<span className='text-sm text-gray-700'></span>
<span className='text-sm text-gray-700 dark:text-gray-300'>
</span>
<div className='relative'>
<input
type='checkbox'
@@ -171,7 +175,7 @@ function SearchPageClient() {
setViewMode(viewMode === 'agg' ? 'all' : 'agg')
}
/>
<div className='w-9 h-5 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors'></div>
<div className='w-9 h-5 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-4'></div>
</div>
</label>
@@ -209,7 +213,7 @@ function SearchPageClient() {
</div>
))}
{searchResults.length === 0 && (
<div className='col-span-full text-center text-gray-500 py-8'>
<div className='col-span-full text-center text-gray-500 py-8 dark:text-gray-400'>
</div>
)}
@@ -218,7 +222,7 @@ function SearchPageClient() {
) : searchHistory.length > 0 ? (
// 搜索历史
<section className='mb-12'>
<h2 className='mb-4 text-xl font-bold text-gray-800 text-left'>
<h2 className='mb-4 text-xl font-bold text-gray-800 text-left dark:text-gray-200'>
{searchHistory.length > 0 && (
<button
@@ -226,7 +230,7 @@ function SearchPageClient() {
await clearSearchHistory();
setSearchHistory([]);
}}
className='ml-3 text-sm text-gray-500 hover:text-red-500 transition-colors'
className='ml-3 text-sm text-gray-500 hover:text-red-500 transition-colors dark:text-gray-400 dark:hover:text-red-500'
>
</button>
@@ -240,7 +244,7 @@ function SearchPageClient() {
setSearchQuery(item);
router.push(`/search?q=${encodeURIComponent(item)}`);
}}
className='px-4 py-2 bg-gray-500/10 hover:bg-gray-300 rounded-full text-sm text-gray-700 transition-colors duration-200'
className='px-4 py-2 bg-gray-500/10 hover:bg-gray-300 rounded-full text-sm text-gray-700 transition-colors duration-200 dark:bg-gray-700/50 dark:hover:bg-gray-600 dark:text-gray-300'
>
{item}
</button>