mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-15 00:33:44 +08:00
refactor: optimize performance across components; adjust backdrop filters, transitions, and scrolling behavior
This commit is contained in:
+43
-17
@@ -58,11 +58,10 @@ body {
|
||||
background-image: var(--bg-image);
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-family-system);
|
||||
transition: background 0.3s ease;
|
||||
|
||||
/* Optimize scrolling performance */
|
||||
will-change: scroll-position;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
body.dark,
|
||||
@@ -88,8 +87,8 @@ body.dark,
|
||||
}
|
||||
|
||||
html {
|
||||
/* Enable smooth scrolling with performance optimization */
|
||||
scroll-behavior: smooth;
|
||||
/* Disable smooth scrolling for better performance - users expect instant scroll */
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
/* Global focus-visible styles for keyboard navigation accessibility */
|
||||
@@ -310,20 +309,16 @@ select:focus-visible {
|
||||
/* Liquid Glass Components */
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(25px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(25px) saturate(180%);
|
||||
backdrop-filter: blur(12px) saturate(120%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(120%);
|
||||
border-radius: var(--radius-2xl);
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1px solid var(--glass-border);
|
||||
transition: transform var(--transition-fluid), box-shadow 0.2s ease;
|
||||
|
||||
/* Use GPU acceleration for smooth animations */
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
transform: translateY(-5px) scale(1.02) translateZ(0);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px var(--shadow-color);
|
||||
}
|
||||
|
||||
@@ -331,20 +326,18 @@ select:focus-visible {
|
||||
nav:where(.sticky, .fixed),
|
||||
.sticky,
|
||||
.fixed {
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.glass-input {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(10px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(150%);
|
||||
backdrop-filter: blur(8px) saturate(120%);
|
||||
-webkit-backdrop-filter: blur(8px) saturate(120%);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-2xl);
|
||||
color: var(--text-color);
|
||||
transition: all var(--transition-fluid);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.glass-input:focus {
|
||||
@@ -532,5 +525,38 @@ nav:where(.sticky, .fixed),
|
||||
animation: slide-up 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Performance optimizations for scrolling */
|
||||
/* Apply CSS containment to grid items for better scroll performance */
|
||||
[class*="grid"] > * {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 400px;
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
/* Reduce paint complexity on scroll */
|
||||
img {
|
||||
content-visibility: auto;
|
||||
}
|
||||
|
||||
/* Disable pointer events during scroll for better performance */
|
||||
body.scrolling * {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Force hardware acceleration for specific animated elements */
|
||||
.group:hover img,
|
||||
.glass-card,
|
||||
nav {
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
/* Optimize badge rendering */
|
||||
[class*="badge"],
|
||||
[class*="Badge"] {
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -41,6 +41,37 @@ export default function RootLayout({
|
||||
aria-atomic="true"
|
||||
className="sr-only"
|
||||
/>
|
||||
|
||||
{/* Scroll Performance Optimization Script */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
let scrollTimer;
|
||||
const body = document.body;
|
||||
|
||||
function handleScroll() {
|
||||
body.classList.add('scrolling');
|
||||
clearTimeout(scrollTimer);
|
||||
scrollTimer = setTimeout(function() {
|
||||
body.classList.remove('scrolling');
|
||||
}, 150);
|
||||
}
|
||||
|
||||
let ticking = false;
|
||||
window.addEventListener('scroll', function() {
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(function() {
|
||||
handleScroll();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
}, { passive: true });
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+2
-2
@@ -86,9 +86,9 @@ function HomePage() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Glass Navbar */}
|
||||
<nav className="sticky top-0 z-50 pt-4 pb-2" style={{ transform: 'translateZ(0)' }}>
|
||||
<nav className="sticky top-0 z-50 pt-4 pb-2">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] [-webkit-backdrop-filter:blur(25px)_saturate(180%)] border border-[var(--glass-border)] shadow-[var(--shadow-md)] px-6 py-4 transition-all duration-[var(--transition-fluid)] rounded-[var(--radius-2xl)]">
|
||||
<div className="bg-[var(--glass-bg)] backdrop-blur-[12px] saturate-[120%] [-webkit-backdrop-filter:blur(12px)_saturate(120%)] border border-[var(--glass-border)] shadow-[var(--shadow-md)] px-6 py-4 rounded-[var(--radius-2xl)]">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
href="/"
|
||||
|
||||
@@ -84,8 +84,7 @@ export function WatchHistorySidebar() {
|
||||
{/* Toggle Button */}
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="fixed right-6 top-1/2 -translate-y-1/2 z-40 bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-3 hover:scale-105 transition-all will-change-transform"
|
||||
style={{ transform: 'translate(0, -50%) translateZ(0)' }}
|
||||
className="fixed right-6 top-1/2 -translate-y-1/2 z-40 bg-[var(--glass-bg)] backdrop-blur-[12px] saturate-[120%] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-3 hover:scale-105 transition-transform duration-200"
|
||||
aria-label="打开观看历史"
|
||||
>
|
||||
<Icons.History size={24} className="text-[var(--text-color)]" />
|
||||
@@ -94,8 +93,7 @@ export function WatchHistorySidebar() {
|
||||
{/* Backdrop */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-[1999] bg-black/30 backdrop-blur-[5px] opacity-0 animate-[fadeIn_0.3s_ease-out_forwards]"
|
||||
style={{ transform: 'translateZ(0)' }}
|
||||
className="fixed inset-0 z-[1999] bg-black/40 opacity-0 animate-[fadeIn_0.2s_ease-out_forwards]"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
@@ -107,9 +105,9 @@ export function WatchHistorySidebar() {
|
||||
aria-labelledby="history-sidebar-title"
|
||||
aria-hidden={!isOpen}
|
||||
style={{
|
||||
transform: isOpen ? 'translateX(0) translateZ(0)' : 'translateX(100%) translateZ(0)'
|
||||
transform: isOpen ? 'translateX(0)' : 'translateX(100%)'
|
||||
}}
|
||||
className={`fixed top-0 right-0 bottom-0 w-[85%] sm:w-[90%] max-w-[420px] z-[2000] bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] border-l border-[var(--glass-border)] rounded-tl-[var(--radius-2xl)] rounded-bl-[var(--radius-2xl)] p-6 flex flex-col shadow-[0_8px_32px_rgba(0,0,0,0.2)] transition-transform duration-[400ms] cubic-bezier(0.2,0.8,0.2,1)`}
|
||||
className={`fixed top-0 right-0 bottom-0 w-[85%] sm:w-[90%] max-w-[420px] z-[2000] bg-[var(--glass-bg)] backdrop-blur-[12px] saturate-[120%] border-l border-[var(--glass-border)] rounded-tl-[var(--radius-2xl)] rounded-bl-[var(--radius-2xl)] p-6 flex flex-col shadow-[0_8px_32px_rgba(0,0,0,0.2)] transition-transform duration-250 ease-out`}
|
||||
>
|
||||
{/* Header */}
|
||||
<header className="flex items-center justify-between mb-6 pb-4 border-b border-[var(--glass-border)]">
|
||||
|
||||
@@ -37,8 +37,9 @@ export function MovieCard({ movie, onMovieClick }: MovieCardProps) {
|
||||
src={movie.cover}
|
||||
alt={movie.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-110 will-change-transform rounded-[var(--radius-2xl)]"
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-105 rounded-[var(--radius-2xl)]"
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 20vw"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
@@ -54,7 +55,7 @@ export function MovieCard({ movie, onMovieClick }: MovieCardProps) {
|
||||
/>
|
||||
{movie.rate && parseFloat(movie.rate) > 0 && (
|
||||
<div
|
||||
className="absolute top-2 right-2 bg-black/70 backdrop-blur-sm px-2.5 py-1.5 flex items-center gap-1.5 rounded-[var(--radius-full)]"
|
||||
className="absolute top-2 right-2 bg-black/80 px-2.5 py-1.5 flex items-center gap-1.5 rounded-[var(--radius-full)]"
|
||||
>
|
||||
<Icons.Star size={12} className="text-yellow-400 fill-yellow-400" />
|
||||
<span className="text-xs font-bold text-white">
|
||||
|
||||
@@ -43,7 +43,7 @@ export function EpisodeList({ episodes, currentEpisode, onEpisodeClick }: Episod
|
||||
});
|
||||
|
||||
return (
|
||||
<Card hover={false} className="lg:sticky lg:top-32" style={{ transform: 'translateZ(0)' }}>
|
||||
<Card hover={false} className="lg:sticky lg:top-32">
|
||||
<h3 className="text-lg sm:text-xl font-bold text-[var(--text-color)] mb-4 flex items-center gap-2">
|
||||
<Icons.List size={20} className="sm:w-6 sm:h-6" />
|
||||
<span>选集</span>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useState, useRef, useCallback, useMemo, memo } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Icons } from '@/components/ui/Icon';
|
||||
import { useKeyboardNavigation } from '@/lib/hooks/useKeyboardNavigation';
|
||||
|
||||
interface Video {
|
||||
vod_id: string;
|
||||
@@ -26,38 +25,7 @@ interface VideoGridProps {
|
||||
|
||||
export function VideoGrid({ videos, className = '' }: VideoGridProps) {
|
||||
const [activeCardId, setActiveCardId] = useState<string | null>(null);
|
||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
const videoRefs = useRef<(HTMLAnchorElement | null)[]>([]);
|
||||
|
||||
// Calculate columns for grid navigation
|
||||
const getColumns = () => {
|
||||
if (typeof window === 'undefined') return 6;
|
||||
const width = window.innerWidth;
|
||||
if (width >= 1536) return 6; // 2xl
|
||||
if (width >= 1280) return 6; // xl
|
||||
if (width >= 1024) return 5; // lg
|
||||
if (width >= 768) return 4; // md
|
||||
if (width >= 640) return 3; // sm
|
||||
return 2; // default
|
||||
};
|
||||
|
||||
// Keyboard navigation
|
||||
useKeyboardNavigation({
|
||||
enabled: true,
|
||||
containerRef: gridRef,
|
||||
currentIndex: focusedIndex,
|
||||
itemCount: videos.length,
|
||||
orientation: 'grid',
|
||||
columns: getColumns(),
|
||||
onNavigate: useCallback((index: number) => {
|
||||
setFocusedIndex(index);
|
||||
videoRefs.current[index]?.focus();
|
||||
}, []),
|
||||
onSelect: useCallback((index: number) => {
|
||||
videoRefs.current[index]?.click();
|
||||
}, []),
|
||||
});
|
||||
|
||||
if (videos.length === 0) {
|
||||
return null;
|
||||
@@ -97,29 +65,17 @@ export function VideoGrid({ videos, className = '' }: VideoGridProps) {
|
||||
|
||||
const cardId = `${video.vod_id}-${index}`;
|
||||
const isActive = activeCardId === cardId;
|
||||
const isFocused = focusedIndex === index;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={cardId}
|
||||
href={videoUrl}
|
||||
ref={(el) => { videoRefs.current[index] = el; }}
|
||||
onClick={(e) => handleCardClick(e, cardId, videoUrl)}
|
||||
onFocus={() => setFocusedIndex(index)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleCardClick(e as any, cardId, videoUrl);
|
||||
}
|
||||
}}
|
||||
role="listitem"
|
||||
tabIndex={0}
|
||||
aria-label={`${video.vod_name}${video.vod_remarks ? ` - ${video.vod_remarks}` : ''}`}
|
||||
>
|
||||
<Card
|
||||
className={`p-0 overflow-hidden group cursor-pointer flex flex-col h-full ${video.isNew ? 'animate-scale-in' : ''} ${
|
||||
isFocused ? 'ring-2 ring-[var(--accent-color)] ring-offset-2' : ''
|
||||
}`}
|
||||
className={`p-0 overflow-hidden group cursor-pointer flex flex-col h-full ${video.isNew ? 'animate-scale-in' : ''}`}
|
||||
>
|
||||
{/* Poster */}
|
||||
<div className="relative aspect-[2/3] bg-[color-mix(in_srgb,var(--glass-bg)_50%,transparent)] overflow-hidden rounded-[var(--radius-2xl)]">
|
||||
@@ -127,8 +83,9 @@ export function VideoGrid({ videos, className = '' }: VideoGridProps) {
|
||||
<img
|
||||
src={video.vod_pic}
|
||||
alt={video.vod_name}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500 will-change-transform rounded-[var(--radius-2xl)]"
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 rounded-[var(--radius-2xl)]"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = '/placeholder-poster.svg';
|
||||
}}
|
||||
@@ -142,7 +99,7 @@ export function VideoGrid({ videos, className = '' }: VideoGridProps) {
|
||||
{/* Source Badge - Top Left */}
|
||||
{video.sourceName && (
|
||||
<div className="absolute top-2 left-2 z-10">
|
||||
<Badge variant="primary" className="text-xs backdrop-blur-md bg-[var(--accent-color)]/90">
|
||||
<Badge variant="primary" className="text-xs bg-[var(--accent-color)]">
|
||||
{video.sourceName}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -10,14 +10,14 @@ interface CardProps {
|
||||
|
||||
export function Card({ children, className = '', hover = true, onClick, style }: CardProps) {
|
||||
const hoverStyles = hover
|
||||
? "hover:translate-y-[-5px] hover:scale-[1.02] hover:shadow-[0_8px_24px_var(--shadow-color)] cursor-pointer transition-all duration-[var(--transition-fluid)]"
|
||||
: "transition-all duration-[var(--transition-fluid)]";
|
||||
? "hover:translate-y-[-2px] hover:shadow-[0_8px_24px_var(--shadow-color)] cursor-pointer transition-transform duration-200 ease-out"
|
||||
: "";
|
||||
|
||||
const baseClasses = `
|
||||
bg-[var(--glass-bg)]
|
||||
backdrop-blur-[25px]
|
||||
saturate-[180%]
|
||||
[-webkit-backdrop-filter:blur(25px)_saturate(180%)]
|
||||
backdrop-blur-[12px]
|
||||
saturate-[120%]
|
||||
[-webkit-backdrop-filter:blur(12px)_saturate(120%)]
|
||||
rounded-[var(--radius-2xl)]
|
||||
shadow-[0_2px_8px_var(--shadow-color)] md:shadow-[var(--shadow-md)]
|
||||
border
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// Performance optimizations
|
||||
reactStrictMode: true,
|
||||
poweredByHeader: false,
|
||||
|
||||
// Compiler optimizations
|
||||
compiler: {
|
||||
removeConsole: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
// Douban images
|
||||
@@ -38,6 +47,9 @@ const nextConfig: NextConfig = {
|
||||
hostname: '**.cn',
|
||||
},
|
||||
],
|
||||
// Add image optimization for better performance
|
||||
formats: ['image/webp'],
|
||||
minimumCacheTTL: 60,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user