mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-19 18:53:42 +08:00
feat: Optimize component performance with GPU acceleration and add placeholder image for missing content
This commit is contained in:
@@ -56,14 +56,21 @@ export function SearchLoadingAnimation({
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-[var(--accent-color)] transition-all duration-500 ease-out relative"
|
||||
className="h-full bg-[var(--accent-color)] transition-all duration-500 ease-out relative will-change-[width]"
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
borderRadius: 'var(--radius-full)'
|
||||
borderRadius: 'var(--radius-full)',
|
||||
transform: 'translateZ(0)'
|
||||
}}
|
||||
>
|
||||
{/* Shimmer Effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer"></div>
|
||||
{/* Shimmer Effect - Optimized for GPU */}
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer"
|
||||
style={{
|
||||
willChange: 'transform',
|
||||
transform: 'translateZ(0)'
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -102,6 +102,17 @@ export function HistoryItem({
|
||||
alt={title}
|
||||
fill
|
||||
className="object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const parent = target.parentElement;
|
||||
if (parent) {
|
||||
const fallback = document.createElement('div');
|
||||
fallback.className = 'w-full h-full flex items-center justify-center';
|
||||
fallback.innerHTML = '<svg class="text-[var(--text-color-secondary)] opacity-30" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>';
|
||||
parent.appendChild(fallback);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
|
||||
@@ -55,7 +55,8 @@ 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"
|
||||
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)' }}
|
||||
aria-label="打开观看历史"
|
||||
>
|
||||
<Icons.History size={24} className="text-[var(--text-color)]" />
|
||||
@@ -65,6 +66,7 @@ export function WatchHistorySidebar() {
|
||||
{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)' }}
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
@@ -75,9 +77,10 @@ export function WatchHistorySidebar() {
|
||||
role="complementary"
|
||||
aria-labelledby="history-sidebar-title"
|
||||
aria-hidden={!isOpen}
|
||||
className={`fixed top-0 right-0 bottom-0 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) ${
|
||||
isOpen ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
style={{
|
||||
transform: isOpen ? 'translateX(0) translateZ(0)' : 'translateX(100%) translateZ(0)'
|
||||
}}
|
||||
className={`fixed top-0 right-0 bottom-0 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)`}
|
||||
>
|
||||
{/* Header */}
|
||||
<header className="flex items-center justify-between mb-6 pb-4 border-b border-[var(--glass-border)]">
|
||||
|
||||
@@ -37,9 +37,21 @@ export function MovieCard({ movie, onMovieClick }: MovieCardProps) {
|
||||
src={movie.cover}
|
||||
alt={movie.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-110 will-change-transform"
|
||||
style={{ borderRadius: 'var(--radius-2xl)' }}
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 20vw"
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const parent = target.parentElement;
|
||||
if (parent) {
|
||||
const fallback = document.createElement('img');
|
||||
fallback.src = '/placeholder-poster.svg';
|
||||
fallback.alt = movie.title;
|
||||
fallback.style.cssText = 'width: 100%; height: 100%; object-fit: cover; border-radius: var(--radius-2xl);';
|
||||
parent.appendChild(fallback);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{movie.rate && parseFloat(movie.rate) > 0 && (
|
||||
<div
|
||||
|
||||
@@ -91,7 +91,7 @@ export function TagManager({
|
||||
<button
|
||||
onClick={() => onTagSelect(tag.id)}
|
||||
className={`
|
||||
px-6 py-2.5 text-sm font-semibold transition-all whitespace-nowrap
|
||||
px-6 py-2.5 text-sm font-semibold transition-all whitespace-nowrap will-change-transform
|
||||
${selectedTag === tag.id
|
||||
? 'bg-[var(--accent-color)] text-white shadow-md scale-105'
|
||||
: 'bg-[var(--glass-bg)] backdrop-blur-xl text-[var(--text-color)] border border-[var(--glass-border)] hover:border-[var(--accent-color)] hover:scale-105'
|
||||
|
||||
@@ -644,7 +644,7 @@ export function DesktopVideoPlayer({
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<button
|
||||
onClick={togglePlay}
|
||||
className="pointer-events-auto w-20 h-20 rounded-full bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] border border-[var(--glass-border)] flex items-center justify-center transition-all duration-300 hover:scale-110 hover:bg-[var(--accent-color)] shadow-[var(--shadow-md)]"
|
||||
className="pointer-events-auto w-20 h-20 rounded-full bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] border border-[var(--glass-border)] flex items-center justify-center transition-all duration-300 hover:scale-110 hover:bg-[var(--accent-color)] shadow-[var(--shadow-md)] will-change-transform"
|
||||
aria-label="Play"
|
||||
>
|
||||
<Icons.Play size={32} className="text-white ml-1" />
|
||||
|
||||
@@ -43,7 +43,7 @@ export function EpisodeList({ episodes, currentEpisode, onEpisodeClick }: Episod
|
||||
});
|
||||
|
||||
return (
|
||||
<Card hover={false} className="lg:sticky lg:top-32">
|
||||
<Card hover={false} className="lg:sticky lg:top-32" style={{ transform: 'translateZ(0)' }}>
|
||||
<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>
|
||||
|
||||
@@ -707,7 +707,7 @@ export function MobileVideoPlayer({
|
||||
|
||||
{/* Toast Notification */}
|
||||
{showToast && toastMessage && (
|
||||
<div className="fixed bottom-20 left-1/2 -translate-x-1/2 z-[200] animate-slide-up">
|
||||
<div className="fixed bottom-20 left-1/2 -translate-x-1/2 z-[200] animate-slide-up" style={{ transform: 'translate(-50%, 0) translateZ(0)' }}>
|
||||
<div className="bg-[rgba(28,28,30,0.95)] backdrop-blur-[25px] rounded-[var(--radius-2xl)] border border-white/20 shadow-[0_8px_32px_rgba(0,0,0,0.6)] px-6 py-3 flex items-center gap-3 min-w-[200px] max-w-[90vw]">
|
||||
<Icons.Check size={18} className="text-[#34c759] flex-shrink-0" />
|
||||
<span className="text-white text-sm font-medium">{toastMessage}</span>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function TypeBadgeItem({
|
||||
text-xs font-medium whitespace-nowrap
|
||||
transition-all duration-[var(--transition-fluid)]
|
||||
hover:scale-105 hover:shadow-[var(--shadow-sm)]
|
||||
active:scale-95 snap-start
|
||||
active:scale-95 snap-start will-change-transform
|
||||
${isSelected
|
||||
? 'bg-[var(--accent-color)] text-white border-[var(--accent-color)]'
|
||||
: 'bg-[var(--glass-bg)] text-[var(--text-color)] backdrop-blur-[10px]'
|
||||
|
||||
@@ -127,9 +127,12 @@ 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"
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500 will-change-transform"
|
||||
style={{ borderRadius: 'var(--radius-2xl)' }}
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = '/placeholder-poster.svg';
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
|
||||
@@ -5,9 +5,10 @@ interface CardProps {
|
||||
className?: string;
|
||||
hover?: boolean;
|
||||
onClick?: () => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function Card({ children, className = '', hover = true, onClick }: 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)]";
|
||||
@@ -34,6 +35,7 @@ export function Card({ children, className = '', hover = true, onClick }: CardPr
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`${baseClasses} text-left w-full`}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
@@ -42,7 +44,7 @@ export function Card({ children, className = '', hover = true, onClick }: CardPr
|
||||
|
||||
// Use div for non-interactive cards
|
||||
return (
|
||||
<div className={baseClasses}>
|
||||
<div className={baseClasses} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user