前端:完善加载动画,和markdown收缩栏

This commit is contained in:
lsy 2024-12-11 17:09:20 +08:00
parent fde98a5032
commit 507595a269
3 changed files with 452 additions and 134 deletions

View File

@ -447,7 +447,7 @@ export const ParticleImage = ({
}, [cleanup]); }, [cleanup]);
// 修改 updateParticles 函数 // 修改 updateParticles 函数
const updateParticles = useCallback((width: number, height: number) => { const updateParticles = useCallback((width: number, height: number, instanceId: string) => {
if (!sceneRef.current || isAnimatingRef.current || !isMountedRef.current) return; if (!sceneRef.current || isAnimatingRef.current || !isMountedRef.current) return;
// 只有当src不为空时才执行cleanup // 只有当src不为空时才执行cleanup
@ -504,16 +504,16 @@ export const ParticleImage = ({
}, },
onComplete: () => { onComplete: () => {
completedAnimations++; completedAnimations++;
// 当所有动画完成时设置标记
if (completedAnimations === totalAnimations && sceneRef.current) { if (completedAnimations === totalAnimations && sceneRef.current) {
sceneRef.current.userData.isSmileComplete = true; sceneRef.current.userData.isSmileComplete = true;
loadingQueue.remove('', instanceId);
} }
} }
}); });
}); });
}, [cleanup, src]); }, [cleanup, src]);
// 将 resize 处理逻辑移到组件顶层 // 将 handleResize 移到 ParticleImage 组件内部
const handleResize = useCallback(() => { const handleResize = useCallback(() => {
if (!containerRef.current || !cameraRef.current || !rendererRef.current || if (!containerRef.current || !cameraRef.current || !rendererRef.current ||
!sceneRef.current || !isMountedRef.current) return; !sceneRef.current || !isMountedRef.current) return;
@ -542,7 +542,7 @@ export const ParticleImage = ({
cleanupResources(sceneRef.current); cleanupResources(sceneRef.current);
} }
sceneRef.current.userData.previousSize = currentSize; sceneRef.current.userData.previousSize = currentSize;
updateParticles(width, height); updateParticles(width, height, ''); // 传入空字符串作为 instanceId
} }
}, [src, updateParticles]); }, [src, updateParticles]);
@ -578,7 +578,7 @@ export const ParticleImage = ({
canvas: document.createElement('canvas') canvas: document.createElement('canvas')
}); });
// 在初始化渲染后立即添加错误检查 // 在初始化渲染后立即添加错误检查
if (!renderer.capabilities.isWebGL2) { if (!renderer.capabilities.isWebGL2) {
console.warn('WebGL2 not supported, falling back...'); console.warn('WebGL2 not supported, falling back...');
renderer.dispose(); renderer.dispose();
@ -742,7 +742,7 @@ export const ParticleImage = ({
}); });
}; };
// 加载 // 加载
const img = new Image(); const img = new Image();
img.crossOrigin = 'anonymous'; img.crossOrigin = 'anonymous';
@ -757,8 +757,8 @@ export const ParticleImage = ({
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (ctx) { if (ctx) {
// 增加一个小的边距以确保完全覆 // 增加一个小的边距以确保<EFBFBD><EFBFBD>
const padding = 2; // 添加2像素的内边 const padding = 2; // 添加2像素的
canvas.width = width + padding * 2; canvas.width = width + padding * 2;
canvas.height = height + padding * 2; canvas.height = height + padding * 2;
@ -823,8 +823,8 @@ export const ParticleImage = ({
delay: normalizedDistance * 0.3 delay: normalizedDistance * 0.3
}); });
// 机初始位置(根据距离调范围) // 机初始位置(根据距离调范围)
const spread = 1 - normalizedDistance * 0.5; // 距离越远,扩散越小 const spread = 1 - normalizedDistance * 0.5; // 距离越远,扩散越小
positionArray.push( positionArray.push(
(Math.random() - 0.5) * width * spread, (Math.random() - 0.5) * width * spread,
(Math.random() - 0.5) * height * spread, (Math.random() - 0.5) * height * spread,
@ -925,17 +925,14 @@ export const ParticleImage = ({
.to(material, { .to(material, {
opacity: 0, opacity: 0,
duration: 0.8 duration: 0.8
}, "-=0.6"); // 前开始消失 }, "-=0.6"); // 前开始消失
} }
return { particles, positionArray, colorArray, particleSize }; return { particles, positionArray, colorArray, particleSize };
} }
}; };
img.onerror = () => {
clearTimeout(timeoutId);
showErrorAnimation();
};
img.src = src || ''; img.src = src || '';
@ -964,7 +961,7 @@ export const ParticleImage = ({
containerRef.current.removeChild(renderer.domElement); containerRef.current.removeChild(renderer.domElement);
renderer.dispose(); renderer.dispose();
} }
// 清所有 GSAP 动 // 清有 GSAP <20><><EFBFBD>
gsap.killTweensOf('*'); gsap.killTweensOf('*');
// 移除 resize 监听 // 移除 resize 监听
@ -978,7 +975,236 @@ export const ParticleImage = ({
return <div ref={containerRef} className="w-full h-full" />; return <div ref={containerRef} className="w-full h-full" />;
}; };
// 图片加载组件 let instanceCounter = 0;
// 添加性能检测函数
const detectDevicePerformance = () => {
// 检查是否在浏览器环境
if (typeof window === 'undefined') {
return 1; // 服务器端返回默认值
}
try {
// 检查是否支持 hardwareConcurrency
const cores = navigator?.hardwareConcurrency || 2;
// 检查设备内 (如果支持)
const memory = (navigator as any)?.deviceMemory || 4;
// 检查是否为移动设备
const isMobile = typeof navigator !== 'undefined'
? /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
: false;
// 基于性能指标计算并发数
let concurrent = 1; // 默认值
if (!isMobile) {
if (cores >= 8 && memory >= 8) {
concurrent = 4; // 高性能设备
} else if (cores >= 4 && memory >= 4) {
concurrent = 3; // 中等性能设备
} else {
concurrent = 2; // 较低性能设备
}
}
return concurrent;
} catch (error) {
console.warn('性能检测失败,使用默认值:', error);
return 1; // 出错时返回最保守的值
}
};
// 修改队列管理
const loadingQueue = {
items: new Map<string, {
isProcessing: boolean;
instanceId: string;
isLongConnection: boolean;
lastActiveTime?: number;
lastProcessState?: boolean;
lastLogTime?: number; // 添加这个字段来控制日志输出频率
}>(),
pendingQueue: new Set<string>(), // 备选队列,存储已加载完成的长连接
maxConcurrent: 1,
currentProcessing: 0,
get availableSlots() {
// 只计算普通请求的槽位,排除错误和长连接
const normalProcessing = Array.from(this.items.values())
.filter(item =>
item.isProcessing &&
!item.isLongConnection
).length;
return this.maxConcurrent - normalProcessing;
},
add(url: string, instanceId: string, isLongConnection = false) {
const key = `${instanceId}:${url}`;
if (!this.items.has(key)) {
console.log('[Queue] Adding:', key,
isLongConnection ? '(long connection)' : '',
'Current processing:', this.currentProcessing,
'Max concurrent:', this.maxConcurrent
);
this.items.set(key, {
isProcessing: false,
instanceId,
isLongConnection,
lastActiveTime: Date.now()
});
// 连接不再直接进入备选队列,而是等待加载完成后再加入
this.processQueue();
return true;
}
return false;
},
processQueue() {
console.log('[Queue] Processing queue:', {
availableSlots: this.availableSlots,
currentProcessing: this.currentProcessing,
pendingQueueSize: this.pendingQueue.size,
totalItems: this.items.size
});
if (this.availableSlots > 0) {
let nextKey: string | undefined;
// 优先从备选队列中获取已加载完成的长连接
if (this.pendingQueue.size > 0) {
nextKey = Array.from(this.pendingQueue)[0];
this.pendingQueue.delete(nextKey);
console.log('[Queue] Processing from pending queue:', nextKey);
} else {
// 如果没有待处理的长连接,处理普通请求
const normalItem = Array.from(this.items.entries())
.find(([_, item]) =>
!item.isProcessing &&
!item.isLongConnection
);
if (normalItem) {
nextKey = normalItem[0];
console.log('[Queue] Processing normal request:', nextKey);
}
}
if (nextKey) {
this.startProcessing(nextKey);
}
}
},
startProcessing(key: string) {
const item = this.items.get(key);
if (item && !item.isProcessing) {
console.log('[Queue] Start processing:', key, {
isLongConnection: item.isLongConnection,
isError: key.includes('error')
});
item.isProcessing = true;
// 只有普通请求且不是错误状态时才增加处理数量
if (!item.isLongConnection && !key.includes('error')) {
this.currentProcessing++;
console.log('[Queue] Increased processing count:', this.currentProcessing);
}
}
},
// 添加新方法:将长连接添加到备选队列
addToPending(url: string, instanceId: string) {
const key = `${instanceId}:${url}`;
const item = this.items.get(key);
if (item?.isLongConnection) {
this.pendingQueue.add(key);
console.log('[Queue] Added to pending queue:', key);
}
},
remove(url: string, instanceId: string) {
const key = `${instanceId}:${url}`;
const item = this.items.get(key);
console.log('[Queue] Removing:', key,
'Is long connection:', item?.isLongConnection,
'Was processing:', item?.isProcessing,
'Has error:', key.includes('error')
);
// 只有普通请求且正在处理时才减少处理数量
if (item?.isProcessing && !item.isLongConnection && !key.includes('error')) {
this.currentProcessing--;
console.log('[Queue] Decreased processing count:', this.currentProcessing);
}
// 确保从队列中移除
this.items.delete(key);
this.pendingQueue.delete(key);
console.log('[Queue] After remove - Processing:', this.currentProcessing,
'Pending queue size:', this.pendingQueue.size,
'Total items:', this.items.size
);
// 如果是错误状态,立即处理下一个请求
if (key.includes('error')) {
this.processQueue();
} else {
// 使用 requestAnimationFrame 来确保状态更新后再处理队列
requestAnimationFrame(() => {
this.processQueue();
});
}
},
canProcess(url: string, instanceId: string): boolean {
const key = `${instanceId}:${url}`;
const item = this.items.get(key);
// 错误状态不占用槽位,只在第一次检查时输出日志
if (key.includes('error')) {
if (!item?.lastLogTime) {
console.log('[Queue] Can process (error):', key, true);
if (item) {
item.lastLogTime = Date.now();
}
}
return true;
}
// 长连接在备选队列中时可以处理
if (item?.isLongConnection && this.pendingQueue.has(key)) {
if (!item.lastLogTime || Date.now() - item.lastLogTime > 1000) {
console.log('[Queue] Can process (pending long):', key, true);
item.lastLogTime = Date.now();
}
return true;
}
const canProcess = item?.isProcessing || false;
// 只在状态发生变化时或者每秒最多输出一次日志
if (item &&
(item.lastProcessState !== canProcess ||
!item.lastLogTime ||
Date.now() - item.lastLogTime > 1000)) {
console.log('[Queue] Can process (normal):', key, canProcess);
item.lastProcessState = canProcess;
item.lastLogTime = Date.now();
}
return canProcess;
}
};
// 在组件挂载时更新 maxConcurrent
if (typeof window !== 'undefined') {
loadingQueue.maxConcurrent = detectDevicePerformance();
}
export const ImageLoader = ({ export const ImageLoader = ({
src, src,
alt, alt,
@ -988,6 +1214,9 @@ export const ImageLoader = ({
alt: string; alt: string;
className: string; className: string;
}) => { }) => {
// 为每个实例创唯一ID
const instanceId = useRef(`img-${++instanceCounter}`);
// 保持现有的状态和引用
const [status, setStatus] = useState<LoaderStatus>({ const [status, setStatus] = useState<LoaderStatus>({
isLoading: true, isLoading: true,
hasError: false, hasError: false,
@ -1000,6 +1229,65 @@ export const ImageLoader = ({
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [animationComplete, setAnimationComplete] = useState(false); const [animationComplete, setAnimationComplete] = useState(false);
// 把 useEffect 移到这里
useEffect(() => {
if (!src) return;
console.log('[Queue] Effect triggered for:', src, 'Instance:', instanceId.current);
setShowImage(false);
setAnimationComplete(false);
setCanShowParticles(false);
loadingQueue.add(src, instanceId.current);
const checkQueue = () => {
if (loadingQueue.canProcess(src, instanceId.current)) {
const now = Date.now();
const timeSinceLastAnimation = now - lastAnimationTime;
if (particleLoadQueue.size === 0) {
console.log('[Queue] Starting immediate animation for:', src, 'Instance:', instanceId.current);
particleLoadQueue.add(src);
setCanShowParticles(true);
lastAnimationTime = now;
return;
}
const delay = Math.max(
MIN_DELAY,
Math.min(ANIMATION_THRESHOLD, timeSinceLastAnimation)
);
console.log('[Queue] Scheduling delayed animation for:', src, 'Instance:', instanceId.current);
const timer = setTimeout(() => {
const key = `${instanceId.current}:${src}`;
if (!loadingQueue.items.has(key)) return;
console.log('[Queue] Starting delayed animation for:', src, 'Instance:', instanceId.current);
particleLoadQueue.add(src);
setCanShowParticles(true);
lastAnimationTime = Date.now();
}, delay);
return () => {
clearTimeout(timer);
particleLoadQueue.delete(src);
};
}
const timer = setTimeout(checkQueue, 100);
return () => clearTimeout(timer);
};
const cleanup = checkQueue();
return () => {
console.log('[Queue] Cleanup effect for:', src, 'Instance:', instanceId.current);
cleanup?.();
loadingQueue.remove(src, instanceId.current);
};
}, [src]);
// 处理图片加载 // 处理图片加载
const preloadImage = useCallback(() => { const preloadImage = useCallback(() => {
if (!src || loadingRef.current) return; if (!src || loadingRef.current) return;
@ -1026,12 +1314,28 @@ export const ImageLoader = ({
const img = new Image(); const img = new Image();
img.crossOrigin = 'anonymous'; img.crossOrigin = 'anonymous';
timeoutRef.current = setTimeout(() => {
loadingRef.current = false;
setStatus({
isLoading: false,
hasError: true,
timeoutError: true
});
// 超时时触发错误动画
setCanShowParticles(true);
if (src) {
loadingQueue.remove(src, instanceId.current);
particleLoadQueue.delete(src);
}
}, 5000);
img.onload = () => { img.onload = () => {
if (timeoutRef.current) { if (timeoutRef.current) {
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
} }
// 在图片加载成功后,立即创建和缓存一个适应容器大小的图片 // 在图片加载成功后,立即创建缓存一个适应容器大小的图片
if (containerRef.current) { if (containerRef.current) {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@ -1075,6 +1379,11 @@ export const ImageLoader = ({
imageRef.current = img; imageRef.current = img;
} }
// 如果是长连接,加载成功后添加到备选队列
if (src && src === src) { // 相同URL判断
loadingQueue.addToPending(src, instanceId.current);
}
loadingRef.current = false; loadingRef.current = false;
setStatus({ setStatus({
isLoading: false, isLoading: false,
@ -1084,6 +1393,7 @@ export const ImageLoader = ({
}; };
img.onerror = () => { img.onerror = () => {
console.log('[Image Loader] Error loading image:', src);
if (timeoutRef.current) { if (timeoutRef.current) {
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
} }
@ -1093,9 +1403,16 @@ export const ImageLoader = ({
hasError: true, hasError: true,
timeoutError: false timeoutError: false
}); });
// 错误时立即触发错误动画
setCanShowParticles(true);
if (src) {
loadingQueue.remove(src, instanceId.current);
particleLoadQueue.delete(src);
}
}; };
// 确保src存在再设置
if (src) { if (src) {
img.src = src; img.src = src;
} }
@ -1115,66 +1432,42 @@ export const ImageLoader = ({
// 添加一个新的状来控制粒子动画 // 添加一个新的状来控制粒子动画
const [canShowParticles, setCanShowParticles] = useState(false); const [canShowParticles, setCanShowParticles] = useState(false);
useEffect(() => { // 添加加载动画组件
if (!src) return; const LoadingSpinner = () => (
<div className="absolute inset-0 flex items-center justify-center">
// 重置状 <div className="w-8 h-8 border-2 border-[--accent-9] border-t-transparent rounded-full animate-spin" />
setShowImage(false); </div>
setAnimationComplete(false);
setCanShowParticles(false);
const now = Date.now();
const timeSinceLastAnimation = now - lastAnimationTime;
if (particleLoadQueue.size === 0) {
particleLoadQueue.add(src);
setCanShowParticles(true);
lastAnimationTime = now;
return;
}
const delay = Math.max(
MIN_DELAY,
Math.min(ANIMATION_THRESHOLD, timeSinceLastAnimation)
); );
const timer = setTimeout(() => {
particleLoadQueue.add(src);
setCanShowParticles(true);
lastAnimationTime = Date.now();
}, delay);
return () => {
clearTimeout(timer);
particleLoadQueue.delete(src);
};
}, [src]);
return ( return (
<div className="relative w-full h-full overflow-hidden"> <div className="relative w-full h-full overflow-hidden">
<div className={`absolute inset-0 ${BG_CONFIG.className} rounded-lg overflow-hidden`}> <div className={`absolute inset-0 ${BG_CONFIG.className} rounded-lg overflow-hidden`}>
{(!src || (!animationComplete && canShowParticles)) && ( {src && (status.isLoading || !canShowParticles) && <LoadingSpinner />}
{(!src || (src && !animationComplete && canShowParticles)) && (
<ParticleImage <ParticleImage
src={src} src={src}
status={status} status={status}
onLoad={() => { onLoad={() => {
console.log('[ParticleImage] onLoad START:', src);
if (imageRef.current) { if (imageRef.current) {
// 保持为空 // 保持为空
} }
console.log('[ParticleImage] onLoad END:', src);
}} }}
onAnimationComplete={() => { onAnimationComplete={() => {
console.log('[ParticleImage] Animation START:', src);
if (imageRef.current && src) { if (imageRef.current && src) {
// 先显示图片,保持透明
setShowImage(true); setShowImage(true);
// 等待一帧确保图片已经渲染
requestAnimationFrame(() => { requestAnimationFrame(() => {
// 标记动画完成,触发粒子消失 console.log('[ParticleImage] Setting animation complete:', src);
setAnimationComplete(true); setAnimationComplete(true);
particleLoadQueue.delete(src); particleLoadQueue.delete(src);
loadingQueue.remove(src, instanceId.current);
// 给图一个短暂延迟再开始淡入
setTimeout(() => { setTimeout(() => {
console.log('[ParticleImage] Fading image:', src);
const img = document.querySelector(`img[src="${imageRef.current?.src}"]`) as HTMLImageElement; const img = document.querySelector(`img[src="${imageRef.current?.src}"]`) as HTMLImageElement;
if (img) { if (img) {
img.style.opacity = '1'; img.style.opacity = '1';
@ -1182,19 +1475,18 @@ export const ImageLoader = ({
}, 50); }, 50);
}); });
} }
console.log('[ParticleImage] Animation END:', src);
}} }}
/> />
)} )}
</div> </div>
{/* 保持现图片渲染部分 */}
{!status.hasError && !status.timeoutError && imageRef.current && ( {!status.hasError && !status.timeoutError && imageRef.current && (
<div className="absolute inset-0 rounded-lg overflow-hidden"> <div className="absolute inset-0 rounded-lg overflow-hidden">
<img <img
src={imageRef.current.src} src={imageRef.current.src}
alt={alt} alt={alt}
className={` className={`w-full h-full object-cover ${className}`}
w-full h-full object-cover
${className}
`}
style={{ style={{
opacity: 0, opacity: 0,
visibility: showImage ? 'visible' : 'hidden', visibility: showImage ? 'visible' : 'hidden',
@ -1202,8 +1494,8 @@ export const ImageLoader = ({
objectPosition: 'center', objectPosition: 'center',
willChange: 'opacity, transform', willChange: 'opacity, transform',
transition: 'opacity 0.5s ease-in-out', transition: 'opacity 0.5s ease-in-out',
transform: 'scale(1.015)', // 增加缩放比例到 1.015 transform: 'scale(1.015)',
transformOrigin: 'center bottom', // 从底部中心开始缩放 transformOrigin: 'center bottom',
}} }}
/> />
</div> </div>

View File

@ -837,7 +837,9 @@ export default new Template({}, ({ http, args }) => {
details: ({ node, ...props }: ComponentPropsWithoutRef<'details'> & { node?: any }) => ( details: ({ node, ...props }: ComponentPropsWithoutRef<'details'> & { node?: any }) => (
<details <details
className="my-4 rounded-lg border border-[--gray-6] bg-[--gray-2] overflow-hidden className="my-4 rounded-lg border border-[--gray-6] bg-[--gray-2] overflow-hidden
marker:text-[--gray-11] [&[open]]:bg-[--gray-1]" marker:text-[--gray-11] [&[open]]:bg-[--gray-1]
[&>*:not(summary)]:px-10 [&>*:not(summary)]:py-3
"
{...props} {...props}
/> />
), ),

View File

@ -19,7 +19,7 @@ const mockArticles: PostDisplay[] = [
content: "在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要...", content: "在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要...",
authorName: "张三", authorName: "张三",
publishedAt: new Date("2024-03-15"), publishedAt: new Date("2024-03-15"),
coverImage: "https://avatars.githubusercontent.com/u/72159?v=4", coverImage: "https://www.helloimg.com/i/2024/12/11/6759312352499.png",
status: "published", status: "published",
isEditor: false, isEditor: false,
createdAt: new Date("2024-03-15"), createdAt: new Date("2024-03-15"),
@ -199,6 +199,30 @@ const mockArticles: PostDisplay[] = [
{ name: "应用通信", slug: "application-communication", type: "tag" } { name: "应用通信", slug: "application-communication", type: "tag" }
] ]
} }
},
{
id: 9,
title: "AI 驱动的前端开发:从概念到实践",
content: "探索如何将人工智能技术融入前端开发流程包括智能代码补全、自动化测试、UI 生成、性能优化建议等实践应用...",
authorName: "陈十一",
publishedAt: new Date("2024-03-08"),
coverImage: "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=500&auto=format",
status: "published",
isEditor: false,
createdAt: new Date("2024-03-08"),
updatedAt: new Date("2024-03-08"),
taxonomies: {
categories: [
{ name: "人工智能", slug: "artificial-intelligence", type: "category" },
{ name: "前端开发", slug: "frontend-development", type: "category" }
],
tags: [
{ name: "AI开发", slug: "ai-development", type: "tag" },
{ name: "智能化", slug: "intelligence", type: "tag" },
{ name: "自动化", slug: "automation", type: "tag" },
{ name: "开发效率", slug: "development-efficiency", type: "tag" }
]
}
} }
]; ];