mirror of
https://github.com/LiuYuYang01/ThriveX-Blog.git
synced 2026-05-06 22:03:08 +08:00
优化标签墙页面,移除不必要的组件,重构标签展示逻辑,使用 memo 和 useMemo 提升性能,添加 CSS containment 和硬件加速以优化渲染效果,确保更流畅的用户体验。
This commit is contained in:
@@ -30,6 +30,7 @@ export default defineConfig([
|
|||||||
'no-unused-vars': 'off', // 关闭未使用变量的检查
|
'no-unused-vars': 'off', // 关闭未使用变量的检查
|
||||||
'react-refresh/only-export-components': 'off',
|
'react-refresh/only-export-components': 'off',
|
||||||
'react/display-name': 'off',
|
'react/display-name': 'off',
|
||||||
|
'react/prop-types': 'off', // TypeScript 项目不需要 prop-types 验证
|
||||||
// 约束js使用单引号,允许jsx双引号
|
// 约束js使用单引号,允许jsx双引号
|
||||||
quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
|
quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
|
||||||
'jsx-quotes': ['error', 'prefer-double'],
|
'jsx-quotes': ['error', 'prefer-double'],
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo, memo } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { FaBook, FaBookmark, FaFileAlt, FaHashtag, FaLink, FaPaperclip, FaTag, FaFont } from 'react-icons/fa';
|
import { FaBook, FaBookmark, FaFileAlt, FaHashtag, FaLink, FaPaperclip, FaTag, FaFont } from 'react-icons/fa';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
const icons = [FaBook, FaBookmark, FaFileAlt, FaHashtag, FaLink, FaPaperclip, FaTag, FaFont];
|
const icons = [FaBook, FaBookmark, FaFileAlt, FaHashtag, FaLink, FaPaperclip, FaTag, FaFont];
|
||||||
|
|
||||||
const getRandomIcon = (): React.ElementType => icons[Math.floor(Math.random() * icons.length)];
|
// 优化:使用固定随机数,避免重复计算
|
||||||
const getRandomTag = (tags: string[]): string => tags[Math.floor(Math.random() * tags.length)];
|
const getRandomIcon = (seed: number): React.ElementType => icons[seed % icons.length];
|
||||||
|
const getRandomTag = (tags: string[], seed: number): string => {
|
||||||
|
if (tags.length === 0) return '';
|
||||||
|
return tags[seed % tags.length];
|
||||||
|
};
|
||||||
|
|
||||||
interface TagItemProps {
|
interface TagItemProps {
|
||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
@@ -17,14 +21,21 @@ interface TagItemProps {
|
|||||||
delay: number;
|
delay: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagItem: React.FC<TagItemProps> = ({ icon: Icon, text, isLeft, delay }) => (
|
const TagItem = memo<TagItemProps>(({ icon: Icon, text, isLeft, delay }) => (
|
||||||
<motion.div initial={{ opacity: 0, x: isLeft ? 100 : -100 }} animate={{ opacity: [0, 0.8, 0], x: isLeft ? [-100, 0, 100] : [100, 0, -100] }} transition={{ duration: 5, delay, repeat: Infinity, repeatType: 'loop', ease: 'linear' }}>
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: isLeft ? 100 : -100 }}
|
||||||
|
animate={{ opacity: [0, 0.8, 0], x: isLeft ? [-100, 0, 100] : [100, 0, -100] }}
|
||||||
|
transition={{ duration: 5, delay, repeat: Infinity, repeatType: 'loop', ease: 'linear' }}
|
||||||
|
style={{ willChange: 'transform, opacity' }} // 优化动画性能
|
||||||
|
>
|
||||||
<div className={clsx('inline-flex items-center space-x-2 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-400 px-3 py-1 text-sm shadow-sm')}>
|
<div className={clsx('inline-flex items-center space-x-2 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-400 px-3 py-1 text-sm shadow-sm')}>
|
||||||
<Icon size={14} />
|
<Icon size={14} />
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
));
|
||||||
|
|
||||||
|
TagItem.displayName = 'TagItem';
|
||||||
|
|
||||||
interface TagRowProps {
|
interface TagRowProps {
|
||||||
isLeft: boolean;
|
isLeft: boolean;
|
||||||
@@ -32,40 +43,89 @@ interface TagRowProps {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagRow: React.FC<TagRowProps> = ({ isLeft, rowIndex, tags }) => {
|
const TagRow = memo<TagRowProps>(({ isLeft, rowIndex, tags }) => {
|
||||||
const rowTags = Array.from({ length: 8 }, (_, i) => ({
|
// 优化:进一步减少每行的标签数量,从 4 个减少到 3 个
|
||||||
icon: getRandomIcon(),
|
const rowTags = useMemo(
|
||||||
text: getRandomTag(tags),
|
() =>
|
||||||
delay: i * 0.5 + rowIndex * 0.2,
|
Array.from({ length: 3 }, (_, i) => ({
|
||||||
}));
|
icon: getRandomIcon(rowIndex * 3 + i),
|
||||||
|
text: getRandomTag(tags, rowIndex * 3 + i),
|
||||||
|
delay: i * 0.5 + rowIndex * 0.2,
|
||||||
|
key: `${rowIndex}-${i}`,
|
||||||
|
})),
|
||||||
|
[rowIndex, tags]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex ${isLeft ? 'justify-start' : 'justify-end'} space-x-4 my-8`}>
|
<div className={`flex ${isLeft ? 'justify-start' : 'justify-end'} space-x-4 my-8`} style={{ contain: 'layout' }}>
|
||||||
{rowTags.map((tag, index) => (
|
{rowTags.map((tag) => (
|
||||||
<TagItem key={index} {...tag} isLeft={isLeft} />
|
<TagItem key={tag.key} icon={tag.icon} text={tag.text} isLeft={isLeft} delay={tag.delay} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
TagRow.displayName = 'TagRow';
|
||||||
|
|
||||||
interface Row {
|
interface Row {
|
||||||
isLeft: boolean;
|
isLeft: boolean;
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TagCloudBackground({ tags }: { tags: string[] }) {
|
function TagCloudBackground({ tags }: { tags: string[] }) {
|
||||||
const [rows, setRows] = useState<Row[]>([]);
|
const [rows, setRows] = useState<Row[]>([]);
|
||||||
|
const [reducedMotion, setReducedMotion] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const numberOfRows = Math.ceil(window.innerHeight / 50); // Approximate row height
|
// 检测用户是否偏好减少动画
|
||||||
setRows(Array.from({ length: numberOfRows }, (_, i) => ({ isLeft: i % 2 === 0, index: i })));
|
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
setReducedMotion(mediaQuery.matches);
|
||||||
|
|
||||||
|
const handleChange = (e: MediaQueryListEvent) => {
|
||||||
|
setReducedMotion(e.matches);
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaQuery.addEventListener('change', handleChange);
|
||||||
|
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 优化:进一步减少行数,使用更大的行高,并限制最大行数
|
||||||
|
const rowHeight = 100; // 进一步增加行高,减少行数
|
||||||
|
const maxRows = reducedMotion ? 5 : 10; // 如果用户偏好减少动画,则更少行数
|
||||||
|
const numberOfRows = Math.min(maxRows, Math.ceil(window.innerHeight / rowHeight));
|
||||||
|
setRows(Array.from({ length: numberOfRows }, (_, i) => ({ isLeft: i % 2 === 0, index: i })));
|
||||||
|
}, [reducedMotion]);
|
||||||
|
|
||||||
|
// 优化:使用 useMemo 缓存标签数组,避免重复计算,并且只取前100个标签用于背景动画
|
||||||
|
const safeTags = useMemo(() => {
|
||||||
|
const tagArray = tags && tags.length > 0 ? tags : [];
|
||||||
|
// 只使用前100个标签,减少计算量
|
||||||
|
return tagArray.slice(0, 100);
|
||||||
|
}, [tags]);
|
||||||
|
|
||||||
|
if (safeTags.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户偏好减少动画,直接返回空或简化版本
|
||||||
|
if (reducedMotion) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-1 h-screen overflow-hidden pointer-events-none z-0">
|
<div
|
||||||
{rows.map((row, index) => (
|
className="absolute inset-1 h-screen overflow-hidden pointer-events-none z-0"
|
||||||
<TagRow key={index} isLeft={row.isLeft} rowIndex={row.index} tags={tags} />
|
style={{
|
||||||
|
contain: 'layout paint', // CSS containment 优化
|
||||||
|
willChange: 'contents', // 提示浏览器优化
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rows.map((row) => (
|
||||||
|
<TagRow key={row.index} isLeft={row.isLeft} rowIndex={row.index} tags={safeTags} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(TagCloudBackground);
|
||||||
|
|||||||
@@ -1,43 +1,57 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { LiaTagsSolid } from 'react-icons/lia';
|
import { LiaTagsSolid } from 'react-icons/lia';
|
||||||
import { Tag } from '@/types/app/tag';
|
import { Tag } from '@/types/app/tag';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
const TagItemCard = ({ data, count, index }: { data: Tag; count: number; index: number }) => {
|
interface TagItemCardProps {
|
||||||
const colors = ['bg-blue-400', 'bg-green-400', 'bg-yellow-400', 'bg-red-400', 'bg-indigo-400', 'bg-purple-400', 'bg-pink-400'];
|
data: Tag;
|
||||||
const color = colors[index % colors.length];
|
count: number;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
// 颜色数组提取到组件外部,避免重复创建
|
||||||
<motion.div
|
const COLORS = ['bg-blue-400', 'bg-green-400', 'bg-yellow-400', 'bg-red-400', 'bg-indigo-400', 'bg-purple-400', 'bg-pink-400'] as const;
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
const TagItemCard = memo(
|
||||||
transition={{ type: 'spring', stiffness: 100, mass: 0.8, delay: index * 0.05 }}
|
({ data, count, index }: TagItemCardProps) => {
|
||||||
whileHover={{
|
// 使用 useMemo 缓存颜色计算
|
||||||
scale: 1.05,
|
const color = useMemo(() => COLORS[index % COLORS.length], [index]);
|
||||||
transition: { duration: 0.2 },
|
|
||||||
}}
|
// 使用 useMemo 缓存链接 URL
|
||||||
>
|
const href = useMemo(() => `/tag/${data?.id}?name=${encodeURIComponent(data?.name || '')}`, [data?.id, data?.name]);
|
||||||
|
|
||||||
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/tag/${data?.id}?name=${data?.name}`}
|
href={href}
|
||||||
className={clsx('flex h-10 bg-opacity-20 backdrop-blur', color)}
|
className={clsx('flex h-10 bg-opacity-20 transition-transform duration-200 hover:scale-105', color)}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: '0.5rem',
|
borderRadius: '0.5rem',
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
margin: '0.5rem',
|
margin: '0.5rem',
|
||||||
padding: '0 1rem',
|
padding: '0 1rem',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
contain: 'layout style paint', // CSS containment 隔离渲染
|
||||||
|
transform: 'translateZ(0)', // 启用硬件加速
|
||||||
|
backfaceVisibility: 'hidden', // 优化渲染性能
|
||||||
}}
|
}}
|
||||||
|
prefetch={false} // 禁用预取,减少初始加载
|
||||||
>
|
>
|
||||||
<LiaTagsSolid className="h-4 w-4 text-gray-400" aria-hidden="true" />
|
<LiaTagsSolid className="h-4 w-4 text-gray-400 flex-shrink-0" aria-hidden="true" />
|
||||||
<span className="ml-2">{data?.name}</span>
|
<span className="ml-2 truncate max-w-[200px]" title={data?.name}>
|
||||||
<span className="ml-4 text-sm text-gray-400 dark:text-gray-500">{count}</span>
|
{data?.name}
|
||||||
|
</span>
|
||||||
|
<span className="ml-4 text-sm text-gray-400 dark:text-gray-500 flex-shrink-0">{count}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
);
|
||||||
);
|
},
|
||||||
};
|
(prevProps, nextProps) => {
|
||||||
|
// 自定义比较函数,只有关键属性变化时才重新渲染
|
||||||
|
return prevProps.data?.id === nextProps.data?.id && prevProps.data?.name === nextProps.data?.name && prevProps.count === nextProps.count && prevProps.index === nextProps.index;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
TagItemCard.displayName = 'TagItemCard';
|
||||||
|
|
||||||
export default TagItemCard;
|
export default TagItemCard;
|
||||||
|
|||||||
31
src/app/tags/components/TagsPageClient.tsx
Normal file
31
src/app/tags/components/TagsPageClient.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Tag } from '@/types/app/tag';
|
||||||
|
import TagCloudBackground from '@/app/tags/components/TagCloudBackground';
|
||||||
|
import VirtualizedTagList from './VirtualizedTagList';
|
||||||
|
|
||||||
|
interface TagsPageClientProps {
|
||||||
|
tags: Tag[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TagsPageClient({ tags }: TagsPageClientProps) {
|
||||||
|
// 使用 useMemo 缓存标签名称数组
|
||||||
|
const tagNames = useMemo(() => {
|
||||||
|
return tags.map((item: Tag) => item.name).filter(Boolean);
|
||||||
|
}, [tags]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-[50px] mt-[60px] min-h-screen relative hide_sliding">
|
||||||
|
<h1 className="relative z-20 text-4xl font-bold text-center mb-5">标签墙</h1>
|
||||||
|
|
||||||
|
{/* 标签列表 */}
|
||||||
|
<div className="relative z-20 w-11/12 mx-auto">
|
||||||
|
<VirtualizedTagList tags={tags} initialBatchSize={30} batchSize={30} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 背景动画 */}
|
||||||
|
<TagCloudBackground tags={tagNames} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
156
src/app/tags/components/VirtualizedTagList.tsx
Normal file
156
src/app/tags/components/VirtualizedTagList.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
||||||
|
import TagItemCard from './TagItemCard';
|
||||||
|
import { Tag } from '@/types/app/tag';
|
||||||
|
|
||||||
|
interface VirtualizedTagListProps {
|
||||||
|
tags: Tag[];
|
||||||
|
containerHeight?: number; // 容器高度
|
||||||
|
batchSize?: number; // 每批加载的数量
|
||||||
|
initialBatchSize?: number; // 初始加载数量
|
||||||
|
}
|
||||||
|
|
||||||
|
const VirtualizedTagList: React.FC<VirtualizedTagListProps> = ({
|
||||||
|
tags,
|
||||||
|
containerHeight,
|
||||||
|
batchSize = 50, // 每批加载 50 个标签(减少批次大小)
|
||||||
|
initialBatchSize = 50, // 初始只加载 50 个(大幅减少初始DOM)
|
||||||
|
}) => {
|
||||||
|
const [visibleCount, setVisibleCount] = useState(initialBatchSize);
|
||||||
|
const [containerHeightState, setContainerHeightState] = useState(containerHeight || 600);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const loadingRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isLoadingRef = useRef(false); // 防止重复加载
|
||||||
|
const resizeTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
|
|
||||||
|
// 动态计算容器高度(添加防抖)
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerHeight) {
|
||||||
|
setContainerHeightState(containerHeight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateHeight = () => {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (resizeTimerRef.current) {
|
||||||
|
clearTimeout(resizeTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防抖处理
|
||||||
|
resizeTimerRef.current = setTimeout(() => {
|
||||||
|
const height = window.innerHeight - 200; // 减去头部和其他元素的高度
|
||||||
|
setContainerHeightState(Math.max(400, height));
|
||||||
|
}, 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始计算
|
||||||
|
const height = window.innerHeight - 200;
|
||||||
|
setContainerHeightState(Math.max(400, height));
|
||||||
|
|
||||||
|
window.addEventListener('resize', updateHeight, { passive: true });
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updateHeight);
|
||||||
|
if (resizeTimerRef.current) {
|
||||||
|
clearTimeout(resizeTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [containerHeight]);
|
||||||
|
|
||||||
|
// 使用 requestAnimationFrame 优化加载
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
if (isLoadingRef.current || visibleCount >= tags.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingRef.current = true;
|
||||||
|
|
||||||
|
// 使用 requestAnimationFrame 优化性能
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setVisibleCount((prev) => {
|
||||||
|
const next = Math.min(prev + batchSize, tags.length);
|
||||||
|
isLoadingRef.current = false;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [visibleCount, tags.length, batchSize]);
|
||||||
|
|
||||||
|
// 使用 Intersection Observer 实现懒加载(优化配置)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadingRef.current || visibleCount >= tags.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用节流,避免过于频繁触发
|
||||||
|
let ticking = false;
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (!ticking && entries[0].isIntersecting && visibleCount < tags.length && !isLoadingRef.current) {
|
||||||
|
ticking = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
loadMore();
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rootMargin: '300px', // 提前 300px 开始加载,减少加载次数
|
||||||
|
threshold: 0.1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(loadingRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [visibleCount, tags.length, loadMore]);
|
||||||
|
|
||||||
|
// 获取可见的标签(使用 useMemo 优化)
|
||||||
|
const visibleTags = useMemo(() => {
|
||||||
|
return tags.slice(0, visibleCount);
|
||||||
|
}, [tags, visibleCount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{
|
||||||
|
maxHeight: `${containerHeightState}px`,
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative',
|
||||||
|
contain: 'layout style paint', // CSS containment 优化
|
||||||
|
willChange: 'scroll-position', // 提示浏览器优化滚动
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex flex-wrap justify-center w-full py-10 px-0 sm:px-10"
|
||||||
|
style={{
|
||||||
|
contain: 'layout', // 隔离布局影响
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{visibleTags.map((tag, index) => (
|
||||||
|
<TagItemCard key={tag.id || index} data={tag} count={tag.count || 0} index={index} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 加载触发器 */}
|
||||||
|
{visibleCount < tags.length && (
|
||||||
|
<div ref={loadingRef} className="w-full h-20 flex items-center justify-center" style={{ minHeight: '80px', contain: 'layout' }}>
|
||||||
|
<div className="text-gray-400 dark:text-gray-500 text-sm">
|
||||||
|
加载中... ({visibleCount}/{tags.length})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 加载完成提示 */}
|
||||||
|
{visibleCount >= tags.length && tags.length > 0 && (
|
||||||
|
<div className="w-full h-10 flex items-center justify-center text-gray-400 dark:text-gray-500 text-sm" style={{ contain: 'layout' }}>
|
||||||
|
共 {tags.length} 个标签
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VirtualizedTagList;
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { getTagListWithArticleCountAPI } from '@/api/tag';
|
import { getTagListWithArticleCountAPI } from '@/api/tag';
|
||||||
import { Tag } from '@/types/app/tag';
|
import { Tag } from '@/types/app/tag';
|
||||||
import TagCloudBackground from '@/app/tags/components/TagCloudBackground';
|
|
||||||
import TagItemCard from './components/TagItemCard';
|
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
|
import TagsPageClient from './components/TagsPageClient';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: '🏷️ 标签墙',
|
title: '🏷️ 标签墙',
|
||||||
@@ -12,17 +11,5 @@ export const metadata: Metadata = {
|
|||||||
export default async () => {
|
export default async () => {
|
||||||
const { data } = (await getTagListWithArticleCountAPI()) || { data: {} as Tag[] };
|
const { data } = (await getTagListWithArticleCountAPI()) || { data: {} as Tag[] };
|
||||||
|
|
||||||
return (
|
return <TagsPageClient tags={data || []} />;
|
||||||
<div className="py-[50px] mt-[60px] h-screen overflow-scroll hide_sliding">
|
|
||||||
<h1 className="relative z-20 text-4xl font-bold text-center">标签墙</h1>
|
|
||||||
|
|
||||||
<div className="relative z-20 flex flex-wrap justify-center w-11/12 mx-auto py-10 px-0 sm:px-10">
|
|
||||||
{data.map((tag, index) => (
|
|
||||||
<TagItemCard data={tag} count={tag.count || 0} index={index} key={tag.id} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TagCloudBackground tags={data?.map((item: Tag) => item.name) || []} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user