diff --git a/frontend/src/composables/performance/useDebounceThrottle.ts b/frontend/src/composables/performance/useDebounceThrottle.ts new file mode 100644 index 0000000..7954476 --- /dev/null +++ b/frontend/src/composables/performance/useDebounceThrottle.ts @@ -0,0 +1,53 @@ +import { ref } from 'vue' + +/** + * 防抖 - 延迟执行,多次触发只执行最后一次 + * @param fn 要执行的函数 + * @param delay 延迟时间(毫秒) + */ +export function useDebounce any>(fn: T, delay = 300) { + const timer = ref | null>(null) + + function debouncedFn(...args: Parameters) { + if (timer.value) { + clearTimeout(timer.value) + } + timer.value = setTimeout(() => { + fn(...args) + timer.value = null + }, delay) + } + + function cancel() { + if (timer.value) { + clearTimeout(timer.value) + timer.value = null + } + } + + return { + run: debouncedFn, + cancel, + } +} + +/** + * 节流 - 固定时间间隔执行 + * @param fn 要执行的函数 + * @param interval 时间间隔(毫秒) + */ +export function useThrottle any>(fn: T, interval = 300) { + const lastTime = ref(0) + + function throttledFn(...args: Parameters) { + const now = Date.now() + if (now - lastTime.value >= interval) { + lastTime.value = now + fn(...args) + } + } + + return { + run: throttledFn, + } +} diff --git a/frontend/src/composables/performance/useLazyImage.ts b/frontend/src/composables/performance/useLazyImage.ts new file mode 100644 index 0000000..52d63db --- /dev/null +++ b/frontend/src/composables/performance/useLazyImage.ts @@ -0,0 +1,51 @@ +import { onMounted, onUnmounted, ref } from 'vue' + +/** + * 图片懒加载 composable + * 使用 Intersection Observer API 实现高性能懒加载 + */ +export function useLazyImage() { + const imageRefs = ref>(new Set()) + let observer: IntersectionObserver | null = null + + onMounted(() => { + observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const img = entry.target as HTMLImageElement + const src = img.dataset.src + if (src) { + img.src = src + img.removeAttribute('data-src') + observer?.unobserve(img) + } + } + }) + }, + { + rootMargin: '50px', // 提前50px加载 + threshold: 0.01, + } + ) + + imageRefs.value.forEach((img) => { + observer?.observe(img) + }) + }) + + onUnmounted(() => { + observer?.disconnect() + }) + + function registerImage(el: HTMLImageElement | null) { + if (el && observer) { + imageRefs.value.add(el) + observer.observe(el) + } + } + + return { + registerImage, + } +} diff --git a/frontend/src/composables/performance/useLazyLoad.ts b/frontend/src/composables/performance/useLazyLoad.ts new file mode 100644 index 0000000..346afbb --- /dev/null +++ b/frontend/src/composables/performance/useLazyLoad.ts @@ -0,0 +1,27 @@ +import { defineAsyncComponent, type Component } from 'vue' + +/** + * 组件懒加载包装器 + * 自动添加 loading 和 error 处理 + */ +export function lazyLoadComponent(importFunc: () => Promise) { + return defineAsyncComponent({ + loader: importFunc, + loadingComponent: { + template: '
加载中...
', + }, + errorComponent: { + template: '
加载失败,请刷新重试
', + }, + delay: 200, // 延迟200ms显示loading + timeout: 10000, // 10秒超时 + }) +} + +/** + * 路由级别懒加载 + * 使用 webpack 魔法注释自动分包 + */ +export function lazyLoadRoute(path: string) { + return () => import(/* webpackChunkName: "[request]" */ `@/views/${path}.vue`) +} diff --git a/frontend/src/styles/performance.css b/frontend/src/styles/performance.css new file mode 100644 index 0000000..dc42702 --- /dev/null +++ b/frontend/src/styles/performance.css @@ -0,0 +1,36 @@ +/* 性能优化相关样式 */ + +.lazy-image { + background: #f1f5f9; + min-height: 180px; +} + +.component-loading, +.component-error { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; + color: #94a3b8; + font-size: 14px; +} + +.component-error { + color: #ef4444; +} + +/* 骨架屏动画 */ +@keyframes skeleton-loading { + 0% { + background-position: -200px 0; + } + 100% { + background-position: calc(200px + 100%) 0; + } +} + +.skeleton { + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200px 100%; + animation: skeleton-loading 1.5s ease-in-out infinite; +}