Merge branch 'feature_2025/251117_pref' of https://git.valuefrontier.cn/vf/vf_react into feature_2025/251117_pref
This commit is contained in:
194
src/views/Home/components/HomePageSkeleton.tsx
Normal file
194
src/views/Home/components/HomePageSkeleton.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 首页骨架屏组件
|
||||
* 模拟首页的 6 个功能卡片布局,减少白屏感知时间
|
||||
*
|
||||
* 使用 Chakra UI 的 Skeleton 组件
|
||||
*
|
||||
* @module views/Home/components/HomePageSkeleton
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
SkeletonText,
|
||||
VStack,
|
||||
HStack,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
// ============================================================
|
||||
// 类型定义
|
||||
// ============================================================
|
||||
|
||||
interface HomePageSkeletonProps {
|
||||
/** 是否显示动画效果 */
|
||||
isAnimated?: boolean;
|
||||
/** 骨架屏速度(秒) */
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 单个卡片骨架
|
||||
// ============================================================
|
||||
|
||||
const FeatureCardSkeleton: React.FC<{ isFeatured?: boolean }> = ({ isFeatured = false }) => {
|
||||
const bg = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg={bg}
|
||||
borderRadius="xl"
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
p={isFeatured ? 8 : 6}
|
||||
h={isFeatured ? '350px' : '280px'}
|
||||
boxShadow={isFeatured ? 'xl' : 'md'}
|
||||
position="relative"
|
||||
>
|
||||
<VStack align="start" spacing={4} h="full">
|
||||
{/* 图标骨架 */}
|
||||
<Skeleton
|
||||
height={isFeatured ? '60px' : '48px'}
|
||||
width={isFeatured ? '60px' : '48px'}
|
||||
borderRadius="lg"
|
||||
startColor={isFeatured ? 'blue.100' : 'gray.100'}
|
||||
endColor={isFeatured ? 'blue.200' : 'gray.200'}
|
||||
/>
|
||||
|
||||
{/* 标题骨架 */}
|
||||
<Skeleton height="28px" width="70%" borderRadius="md" />
|
||||
|
||||
{/* 描述骨架 */}
|
||||
<SkeletonText
|
||||
mt="2"
|
||||
noOfLines={isFeatured ? 4 : 3}
|
||||
spacing="3"
|
||||
skeletonHeight="2"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
{/* 按钮骨架 */}
|
||||
<Skeleton
|
||||
height="40px"
|
||||
width={isFeatured ? '140px' : '100px'}
|
||||
borderRadius="md"
|
||||
mt="auto"
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
{/* Featured 徽章骨架 */}
|
||||
{isFeatured && (
|
||||
<Skeleton
|
||||
position="absolute"
|
||||
top="4"
|
||||
right="4"
|
||||
height="24px"
|
||||
width="80px"
|
||||
borderRadius="full"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 主骨架组件
|
||||
// ============================================================
|
||||
|
||||
export const HomePageSkeleton: React.FC<HomePageSkeletonProps> = ({
|
||||
isAnimated = true,
|
||||
speed = 0.8,
|
||||
}) => {
|
||||
const containerBg = useColorModeValue('gray.50', 'gray.900');
|
||||
|
||||
return (
|
||||
<Box
|
||||
w="full"
|
||||
minH="100vh"
|
||||
bg={containerBg}
|
||||
pt={{ base: '120px', md: '140px' }}
|
||||
pb={{ base: '60px', md: '80px' }}
|
||||
>
|
||||
<Container maxW="container.xl">
|
||||
<VStack spacing={{ base: 8, md: 12 }} align="stretch">
|
||||
{/* 顶部标题区域骨架 */}
|
||||
<VStack spacing={4} textAlign="center">
|
||||
{/* 主标题 */}
|
||||
<Skeleton
|
||||
height={{ base: '40px', md: '56px' }}
|
||||
width={{ base: '80%', md: '500px' }}
|
||||
borderRadius="md"
|
||||
speed={speed}
|
||||
/>
|
||||
|
||||
{/* 副标题 */}
|
||||
<Skeleton
|
||||
height={{ base: '20px', md: '24px' }}
|
||||
width={{ base: '90%', md: '600px' }}
|
||||
borderRadius="md"
|
||||
speed={speed}
|
||||
/>
|
||||
|
||||
{/* CTA 按钮 */}
|
||||
<HStack spacing={4} mt={4}>
|
||||
<Skeleton
|
||||
height="48px"
|
||||
width="140px"
|
||||
borderRadius="lg"
|
||||
speed={speed}
|
||||
/>
|
||||
<Skeleton
|
||||
height="48px"
|
||||
width="140px"
|
||||
borderRadius="lg"
|
||||
speed={speed}
|
||||
/>
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
{/* 功能卡片网格骨架 */}
|
||||
<SimpleGrid
|
||||
columns={{ base: 1, md: 2, lg: 3 }}
|
||||
spacing={{ base: 6, md: 8 }}
|
||||
mt={8}
|
||||
>
|
||||
{/* 第一张卡片 - Featured (新闻中心) */}
|
||||
<Box gridColumn={{ base: 'span 1', lg: 'span 2' }}>
|
||||
<FeatureCardSkeleton isFeatured />
|
||||
</Box>
|
||||
|
||||
{/* 其余 5 张卡片 */}
|
||||
{[1, 2, 3, 4, 5].map((index) => (
|
||||
<FeatureCardSkeleton key={index} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
{/* 底部装饰元素骨架 */}
|
||||
<HStack justify="center" spacing={8} mt={12}>
|
||||
{[1, 2, 3].map((index) => (
|
||||
<VStack key={index} spacing={2} align="center">
|
||||
<Skeleton
|
||||
height="40px"
|
||||
width="40px"
|
||||
borderRadius="full"
|
||||
speed={speed}
|
||||
/>
|
||||
<Skeleton height="16px" width="60px" borderRadius="md" speed={speed} />
|
||||
</VStack>
|
||||
))}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 默认导出
|
||||
// ============================================================
|
||||
|
||||
export default HomePageSkeleton;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import React, { useMemo, useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
import { getFormattedTextProps } from '../../../utils/textUtils';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import RiskDisclaimer from '../../../components/RiskDisclaimer';
|
||||
import './WordCloud.css';
|
||||
import {
|
||||
BarChart, Bar,
|
||||
PieChart, Pie, Cell,
|
||||
@@ -51,6 +50,10 @@ import {
|
||||
Treemap,
|
||||
Area, AreaChart,
|
||||
} from 'recharts';
|
||||
// 词云库 - 支持两种实现
|
||||
import { Wordcloud } from '@visx/wordcloud';
|
||||
import { scaleLog } from '@visx/scale';
|
||||
import { Text as VisxText } from '@visx/text';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import 'echarts-wordcloud';
|
||||
// 颜色配置
|
||||
@@ -59,8 +62,101 @@ const CHART_COLORS = [
|
||||
'#D4A5A5', '#9B6B6B', '#E9967A', '#B19CD9', '#87CEEB'
|
||||
];
|
||||
|
||||
// 词云图组件(使用 ECharts Wordcloud)
|
||||
const WordCloud = ({ data }) => {
|
||||
// 词云颜色常量
|
||||
const WORDCLOUD_COLORS = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD'];
|
||||
|
||||
// ==================== 词云组件实现 1: @visx/wordcloud ====================
|
||||
// 使用 SVG 渲染,React 18 原生支持,配置灵活
|
||||
const VisxWordCloud = ({ data }) => {
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 400 });
|
||||
const containerRef = useRef(null);
|
||||
|
||||
// 监听容器尺寸变化
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const updateDimensions = () => {
|
||||
if (containerRef.current) {
|
||||
setDimensions({
|
||||
width: containerRef.current.offsetWidth,
|
||||
height: 400
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
const resizeObserver = new ResizeObserver(updateDimensions);
|
||||
resizeObserver.observe(containerRef.current);
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, []);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Center h="400px">
|
||||
<VStack>
|
||||
<Text color="gray.500">暂无词云数据</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
const words = data.slice(0, 100).map(item => ({
|
||||
name: item.name || item.text,
|
||||
value: item.value || item.count || 1
|
||||
}));
|
||||
|
||||
// 计算字体大小比例
|
||||
const fontScale = scaleLog({
|
||||
domain: [
|
||||
Math.min(...words.map(w => w.value)),
|
||||
Math.max(...words.map(w => w.value))
|
||||
],
|
||||
range: [16, 80],
|
||||
});
|
||||
|
||||
const fontSizeSetter = (datum) => fontScale(datum.value);
|
||||
|
||||
return (
|
||||
<Box ref={containerRef} h="400px" w="100%">
|
||||
{dimensions.width > 0 && (
|
||||
<svg width={dimensions.width} height={dimensions.height}>
|
||||
<Wordcloud
|
||||
words={words}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
fontSize={fontSizeSetter}
|
||||
font="Microsoft YaHei, sans-serif"
|
||||
padding={3}
|
||||
spiral="archimedean"
|
||||
rotate={0}
|
||||
random={() => 0.5}
|
||||
>
|
||||
{(cloudWords) =>
|
||||
cloudWords.map((w, i) => (
|
||||
<VisxText
|
||||
key={w.text}
|
||||
fill={WORDCLOUD_COLORS[i % WORDCLOUD_COLORS.length]}
|
||||
textAnchor="middle"
|
||||
transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
|
||||
fontSize={w.size}
|
||||
fontFamily={w.font}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{w.text}
|
||||
</VisxText>
|
||||
))
|
||||
}
|
||||
</Wordcloud>
|
||||
</svg>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== 词云组件实现 2: ECharts Wordcloud ====================
|
||||
// 使用 Canvas 渲染,内置交互效果(tooltip、emphasis),配置简单
|
||||
const EChartsWordCloud = ({ data }) => {
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Center h="400px">
|
||||
@@ -97,8 +193,7 @@ const WordCloud = ({ data }) => {
|
||||
fontFamily: 'Microsoft YaHei, sans-serif',
|
||||
fontWeight: 'bold',
|
||||
color: function () {
|
||||
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD'];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
return WORDCLOUD_COLORS[Math.floor(Math.random() * WORDCLOUD_COLORS.length)];
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
@@ -121,6 +216,23 @@ const WordCloud = ({ data }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== 词云组件包装器 ====================
|
||||
// 统一接口,支持切换两种实现方式
|
||||
const WordCloud = ({ data, engine = 'echarts' }) => {
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Center h="400px">
|
||||
<VStack>
|
||||
<Text color="gray.500">暂无词云数据</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
// 根据 engine 参数选择实现方式
|
||||
return engine === 'visx' ? <VisxWordCloud data={data} /> : <EChartsWordCloud data={data} />;
|
||||
};
|
||||
|
||||
// 板块热力图组件
|
||||
const SectorHeatMap = ({ data }) => {
|
||||
if (!data) return null;
|
||||
|
||||
Reference in New Issue
Block a user