diff --git a/docker-compose.yml b/docker-compose.yml index 627b581..a6f7250 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,4 +8,6 @@ services: container_name: ittoview ports: - "11.144.144.9:8035:80" + volumes: + - ./src/data/image:/usr/share/nginx/html/learning-images:ro restart: always diff --git a/nginx.conf b/nginx.conf index c862cb2..f8dd0ee 100644 --- a/nginx.conf +++ b/nginx.conf @@ -9,6 +9,14 @@ server { try_files $uri $uri/ /index.html; } + # 一图流图片目录,由 Docker 挂载提供,支持运行时新增图片 + location /learning-images/ { + alias /usr/share/nginx/html/learning-images/; + autoindex on; + charset utf-8; + add_header Cache-Control "no-store"; + } + # 静态资源缓存 location /assets { expires 1y; diff --git a/src/data/image/05-范围管理找问题.png b/src/data/image/05-范围管理找问题.png new file mode 100644 index 0000000..77c8983 Binary files /dev/null and b/src/data/image/05-范围管理找问题.png differ diff --git a/src/data/image/06-整合与范围管理一图流.png b/src/data/image/06-整合与范围管理一图流.png new file mode 100644 index 0000000..4dac552 Binary files /dev/null and b/src/data/image/06-整合与范围管理一图流.png differ diff --git a/src/data/image/07-进度与成本管理一图流.png b/src/data/image/07-进度与成本管理一图流.png new file mode 100644 index 0000000..79b4766 Binary files /dev/null and b/src/data/image/07-进度与成本管理一图流.png differ diff --git a/src/data/image/08-质量与资源管理一图流.png b/src/data/image/08-质量与资源管理一图流.png new file mode 100644 index 0000000..ac14347 Binary files /dev/null and b/src/data/image/08-质量与资源管理一图流.png differ diff --git a/src/data/image/09-沟通与风险管理一图流.png b/src/data/image/09-沟通与风险管理一图流.png new file mode 100644 index 0000000..88adaf7 Binary files /dev/null and b/src/data/image/09-沟通与风险管理一图流.png differ diff --git a/src/data/image/10-采购与干系人管理一图流.png b/src/data/image/10-采购与干系人管理一图流.png new file mode 100644 index 0000000..78a4ff0 Binary files /dev/null and b/src/data/image/10-采购与干系人管理一图流.png differ diff --git a/src/pages/LearningMapsPage.tsx b/src/pages/LearningMapsPage.tsx index 52b4176..39fcbb6 100644 --- a/src/pages/LearningMapsPage.tsx +++ b/src/pages/LearningMapsPage.tsx @@ -15,19 +15,41 @@ type Point = { y: number } -const imageModules = import.meta.glob('../data/image/*.{png,jpg,jpeg,webp,avif,gif}', { - eager: true, - import: 'default', - query: '?url', -}) as Record +const IMAGE_DIRECTORY = '/learning-images/' +const IMAGE_EXTENSIONS = /\.(png|jpe?g|webp|avif|gif)$/i -const learningMapImages: LearningMapImage[] = Object.entries(imageModules) - .map(([path, src]) => { - const fileName = decodeURIComponent(path.split('/').pop() || 'learning-map') - const title = fileName.replace(/\.[^.]+$/, '') - return { src, fileName, title } - }) - .sort((a, b) => a.fileName.localeCompare(b.fileName, 'zh-CN', { numeric: true })) +function imageHrefToItem(href: string): LearningMapImage | null { + const cleanHref = href.split('?')[0].split('#')[0] + const rawFileName = cleanHref.split('/').pop() + + if (!rawFileName || rawFileName === '..' || !IMAGE_EXTENSIONS.test(rawFileName)) { + return null + } + + const fileName = decodeURIComponent(rawFileName) + const title = fileName.replace(/\.[^.]+$/, '') + + return { + src: `${IMAGE_DIRECTORY}${encodeURIComponent(fileName)}`, + fileName, + title, + } +} + +async function loadLearningMapImages() { + const response = await fetch(IMAGE_DIRECTORY, { cache: 'no-store' }) + if (!response.ok) { + throw new Error('无法读取学习图谱') + } + + const html = await response.text() + const documentHtml = new DOMParser().parseFromString(html, 'text/html') + + return Array.from(documentHtml.querySelectorAll('a')) + .map((link) => imageHrefToItem(link.getAttribute('href') || '')) + .filter((item): item is LearningMapImage => Boolean(item)) + .sort((a, b) => a.fileName.localeCompare(b.fileName, 'zh-CN', { numeric: true })) +} function getDistance(a: Point, b: Point) { return Math.hypot(a.x - b.x, a.y - b.y) @@ -61,11 +83,61 @@ async function blobToPngBlob(blob: Blob) { }) } +function LazyLearningMapImage({ image }: { image: LearningMapImage }) { + const containerRef = useRef(null) + const [shouldLoad, setShouldLoad] = useState(false) + const [isLoaded, setIsLoaded] = useState(false) + + useEffect(() => { + const container = containerRef.current + if (!container || shouldLoad) return + + const observer = new IntersectionObserver( + ([entry]) => { + if (!entry.isIntersecting) return + setShouldLoad(true) + observer.disconnect() + }, + { + rootMargin: '700px 0px', + threshold: 0.01, + } + ) + + observer.observe(container) + + return () => observer.disconnect() + }, [shouldLoad]) + + return ( +
+ {!isLoaded && ( +
+ )} + {shouldLoad && ( + {image.title} setIsLoaded(true)} + className={clsx( + 'w-full object-contain transition duration-300 group-hover:scale-[1.01]', + isLoaded ? 'opacity-100' : 'opacity-0' + )} + /> + )} +
+ ) +} + export function LearningMapsPage() { const [activeIndex, setActiveIndex] = useState(null) const [scale, setScale] = useState(1) const [offset, setOffset] = useState({ x: 0, y: 0 }) const [isDragging, setIsDragging] = useState(false) + const [learningMapImages, setLearningMapImages] = useState([]) + const [isLoadingImages, setIsLoadingImages] = useState(true) const [message, setMessage] = useState('') const pointersRef = useRef(new Map()) const lastDragPointRef = useRef(null) @@ -80,6 +152,29 @@ export function LearningMapsPage() { [offset.x, offset.y, scale] ) + + useEffect(() => { + let isMounted = true + + loadLearningMapImages() + .then((images) => { + if (!isMounted) return + setLearningMapImages(images) + }) + .catch(() => { + if (!isMounted) return + setLearningMapImages([]) + }) + .finally(() => { + if (!isMounted) return + setIsLoadingImages(false) + }) + + return () => { + isMounted = false + } + }, []) + const showMessage = (text: string) => { setMessage(text) window.setTimeout(() => setMessage(''), 1800) @@ -241,7 +336,11 @@ export function LearningMapsPage() {

用一张图看清知识结构。

- {learningMapImages.length > 0 ? ( + {isLoadingImages ? ( +
+ 正在加载学习图谱 +
+ ) : learningMapImages.length > 0 ? ( openViewer(index)} className="group mb-4 block w-full break-inside-avoid overflow-hidden rounded-2xl bg-white text-left shadow-sm ring-1 ring-gray-200 transition duration-200 hover:-translate-y-0.5 hover:shadow-lg hover:ring-indigo-200 dark:bg-gray-800 dark:ring-gray-700 dark:hover:ring-indigo-500/50" > - {image.title} + ))}