feat: Optimize component performance with GPU acceleration and add placeholder image for missing content

This commit is contained in:
kuekhaoyang
2025-11-18 15:15:45 +08:00
parent 44cae03731
commit 79ac2c349e
15 changed files with 106 additions and 26 deletions
+7 -7
View File
@@ -663,8 +663,8 @@ export const announceToScreenReader = (message: string) => {
- ✅ 焦点项自动滚动到视图内,优化用户体验
#### 7. **图片加载优化** (预计 2 小时)
- [ ] 创建 `public/placeholder-poster.svg` 占位图
- [ ] **VideoGrid.tsx** - 添加 `onError` 处理
- [x] 创建 `public/placeholder-poster.svg` 占位图
- [x] **VideoGrid.tsx** - 添加 `onError` 处理
```tsx
<img
src={video.vod_pic}
@@ -673,13 +673,13 @@ export const announceToScreenReader = (message: string) => {
}}
/>
```
- [ ] **WatchHistorySidebar.tsx** - 同样添加 `onError`
- [ ] **PopularFeatures.tsx** - 添加 `onError`
- [x] **WatchHistorySidebar.tsx** - 同样添加 `onError`
- [x] **PopularFeatures.tsx** - 添加 `onError`
#### 8. **性能优化 - GPU 加速** (预计 1 小时)
- [ ] 给所有 hover scale 动画添加 `will-change-transform`
- [ ] 给 fixed/sticky 元素添加 `transform: translateZ(0)`
- [ ] 优化 `SearchLoadingAnimation` 的 shimmer 动画
- [x] 给所有 hover scale 动画添加 `will-change-transform`
- [x] 给 fixed/sticky 元素添加 `transform: translateZ(0)`
- [x] 优化 `SearchLoadingAnimation` 的 shimmer 动画
---
+1 -1
View File
@@ -87,7 +87,7 @@ function HomePage() {
return (
<div className="min-h-screen">
{/* Glass Navbar */}
<nav className="sticky top-0 z-50 pt-4 pb-2">
<nav className="sticky top-0 z-50 pt-4 pb-2" style={{ transform: 'translateZ(0)' }}>
<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)]" style={{ borderRadius: 'var(--radius-2xl)' }}>
<div className="flex items-center justify-between">
+1 -1
View File
@@ -80,7 +80,7 @@ function PlayerContent() {
return (
<div className="min-h-screen bg-[var(--bg-color)]">
{/* Glass Navbar */}
<nav className="sticky top-0 z-50 pt-4 pb-2 px-4">
<nav className="sticky top-0 z-50 pt-4 pb-2 px-4" style={{ transform: 'translateZ(0)' }}>
<div className="max-w-7xl mx-auto bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[0_4px_12px_color-mix(in_srgb,var(--shadow-color)_40%,transparent)] px-4 sm:px-6 py-4">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 sm:gap-4 min-w-0">
+11 -4
View File
@@ -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>
+11
View File
@@ -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">
+7 -4
View File
@@ -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)]">
+13 -1
View File
@@ -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
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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" />
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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]'
+4 -1
View File
@@ -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">
+4 -2
View File
@@ -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>
);
+42
View File
@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 450" fill="none">
<defs>
<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f0f2f5;stop-opacity:1" />
<stop offset="100%" style="stop-color:#e0e4e8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="300" height="450" fill="url(#bg-gradient)" rx="24"/>
<!-- Glass effect overlay -->
<rect width="300" height="450" fill="rgba(255, 255, 255, 0.5)" rx="24"/>
<!-- Film icon -->
<g transform="translate(150, 225)">
<!-- Film reel -->
<rect x="-60" y="-80" width="120" height="160" rx="8" fill="none" stroke="#6e6e73" stroke-width="4"/>
<!-- Film strip holes -->
<rect x="-60" y="-70" width="15" height="15" rx="3" fill="#6e6e73"/>
<rect x="-60" y="-40" width="15" height="15" rx="3" fill="#6e6e73"/>
<rect x="-60" y="-10" width="15" height="15" rx="3" fill="#6e6e73"/>
<rect x="-60" y="20" width="15" height="15" rx="3" fill="#6e6e73"/>
<rect x="-60" y="50" width="15" height="15" rx="3" fill="#6e6e73"/>
<rect x="45" y="-70" width="15" height="15" rx="3" fill="#6e6e73"/>
<rect x="45" y="-40" width="15" height="15" rx="3" fill="#6e6e73"/>
<rect x="45" y="-10" width="15" height="15" rx="3" fill="#6e6e73"/>
<rect x="45" y="20" width="15" height="15" rx="3" fill="#6e6e73"/>
<rect x="45" y="50" width="15" height="15" rx="3" fill="#6e6e73"/>
<!-- Play icon in center -->
<circle cx="0" cy="0" r="35" fill="#007aff" opacity="0.9"/>
<path d="M -10,-15 L -10,15 L 15,0 Z" fill="white"/>
</g>
<!-- "Image Not Available" text -->
<text x="150" y="330" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="16" font-weight="600" fill="#6e6e73" text-anchor="middle">
Image Not Available
</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB