mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-14 16:23:43 +08:00
feat: Optimize component performance with GPU acceleration and add placeholder image for missing content
This commit is contained in:
+7
-7
@@ -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
@@ -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
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user