pref: 权限校验中 - 显示占位骨架,不显示登录按钮或用户菜单,/home页面添加骨架屏逻辑

This commit is contained in:
zdl
2025-11-26 09:57:20 +08:00
parent 601b06d79e
commit 84f70f3329
4 changed files with 163 additions and 145 deletions

View File

@@ -2,7 +2,7 @@
// Navbar 右侧功能区组件 // Navbar 右侧功能区组件
import React, { memo } from 'react'; import React, { memo } from 'react';
import { HStack, Spinner, IconButton, Box } from '@chakra-ui/react'; import { HStack, IconButton, Box } from '@chakra-ui/react';
import { HamburgerIcon } from '@chakra-ui/icons'; import { HamburgerIcon } from '@chakra-ui/icons';
// import ThemeToggleButton from '../ThemeToggleButton'; // ❌ 已删除 - 不再支持深色模式切换 // import ThemeToggleButton from '../ThemeToggleButton'; // ❌ 已删除 - 不再支持深色模式切换
import LoginButton from '../LoginButton'; import LoginButton from '../LoginButton';
@@ -41,9 +41,15 @@ const NavbarActions = memo(({
}) => { }) => {
return ( return (
<HStack spacing={{ base: 2, md: 4 }}> <HStack spacing={{ base: 2, md: 4 }}>
{/* 显示加载状态 */} {/* 权限校验中 - 显示占位骨架,不显示登录按钮或用户菜单 */}
{isLoading ? ( {isLoading ? (
<Spinner size="sm" color="blue.500" /> <Box
w={{ base: '80px', md: '120px' }}
h="36px"
borderRadius="md"
bg="whiteAlpha.100"
opacity={0.6}
/>
) : isAuthenticated && user ? ( ) : isAuthenticated && user ? (
// 已登录状态 - 用户菜单 + 功能菜单排列 // 已登录状态 - 用户菜单 + 功能菜单排列
<HStack spacing={{ base: 2, md: 3 }}> <HStack spacing={{ base: 2, md: 3 }}>

View File

@@ -9,43 +9,44 @@ import React from 'react';
*/ */
export const lazyComponents = { export const lazyComponents = {
// Home 模块 // Home 模块
HomePage: React.lazy(() => import('../views/Home/HomePage')), // 首页使用专用入口组件,内置骨架屏 fallback
CenterDashboard: React.lazy(() => import('../views/Dashboard/Center')), HomePage: React.lazy(() => import('@views/Home')),
ProfilePage: React.lazy(() => import('../views/Profile')), CenterDashboard: React.lazy(() => import('@views/Dashboard/Center')),
SettingsPage: React.lazy(() => import('../views/Settings/SettingsPage')), ProfilePage: React.lazy(() => import('@views/Profile')),
Subscription: React.lazy(() => import('../views/Pages/Account/Subscription')), SettingsPage: React.lazy(() => import('@views/Settings/SettingsPage')),
PrivacyPolicy: React.lazy(() => import('../views/Pages/PrivacyPolicy')), Subscription: React.lazy(() => import('@views/Pages/Account/Subscription')),
UserAgreement: React.lazy(() => import('../views/Pages/UserAgreement')), PrivacyPolicy: React.lazy(() => import('@views/Pages/PrivacyPolicy')),
WechatCallback: React.lazy(() => import('../views/Pages/WechatCallback')), UserAgreement: React.lazy(() => import('@views/Pages/UserAgreement')),
WechatCallback: React.lazy(() => import('@views/Pages/WechatCallback')),
// 社区/内容模块 // 社区/内容模块
Community: React.lazy(() => import('../views/Community')), Community: React.lazy(() => import('@views/Community')),
ConceptCenter: React.lazy(() => import('../views/Concept')), ConceptCenter: React.lazy(() => import('@views/Concept')),
StockOverview: React.lazy(() => import('../views/StockOverview')), StockOverview: React.lazy(() => import('@views/StockOverview')),
LimitAnalyse: React.lazy(() => import('../views/LimitAnalyse')), LimitAnalyse: React.lazy(() => import('@views/LimitAnalyse')),
// 交易模块 // 交易模块
TradingSimulation: React.lazy(() => import('../views/TradingSimulation')), TradingSimulation: React.lazy(() => import('@views/TradingSimulation')),
// 事件模块 // 事件模块
EventDetail: React.lazy(() => import('../views/EventDetail')), EventDetail: React.lazy(() => import('@views/EventDetail')),
// 公司相关模块 // 公司相关模块
CompanyIndex: React.lazy(() => import('../views/Company')), CompanyIndex: React.lazy(() => import('@views/Company')),
ForecastReport: React.lazy(() => import('../views/Company/ForecastReport')), ForecastReport: React.lazy(() => import('@views/Company/ForecastReport')),
FinancialPanorama: React.lazy(() => import('../views/Company/FinancialPanorama')), FinancialPanorama: React.lazy(() => import('@views/Company/FinancialPanorama')),
MarketDataView: React.lazy(() => import('../views/Company/MarketDataView')), MarketDataView: React.lazy(() => import('@views/Company/MarketDataView')),
// Agent模块 // Agent模块
AgentChat: React.lazy(() => import('../views/AgentChat')), AgentChat: React.lazy(() => import('@views/AgentChat')),
// 价值论坛模块 // 价值论坛模块
ValueForum: React.lazy(() => import('../views/ValueForum')), ValueForum: React.lazy(() => import('@views/ValueForum')),
ForumPostDetail: React.lazy(() => import('../views/ValueForum/PostDetail')), ForumPostDetail: React.lazy(() => import('@views/ValueForum/PostDetail')),
PredictionTopicDetail: React.lazy(() => import('../views/ValueForum/PredictionTopicDetail')), PredictionTopicDetail: React.lazy(() => import('@views/ValueForum/PredictionTopicDetail')),
// 数据浏览器模块 // 数据浏览器模块
DataBrowser: React.lazy(() => import('../views/DataBrowser')), DataBrowser: React.lazy(() => import('@views/DataBrowser')),
}; };
/** /**

View File

@@ -1,8 +1,6 @@
/** /**
* 首页骨架屏组件 * 首页骨架屏组件
* 模拟首页的 6 个功能卡片布局,减少白屏感知时间 * 匹配首页深色科技风格,减少白屏感知时间
*
* 使用 Chakra UI 的 Skeleton 组件
* *
* @module views/Home/components/HomePageSkeleton * @module views/Home/components/HomePageSkeleton
*/ */
@@ -15,8 +13,6 @@ import {
Skeleton, Skeleton,
SkeletonText, SkeletonText,
VStack, VStack,
HStack,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
// ============================================================ // ============================================================
@@ -24,163 +20,152 @@ import {
// ============================================================ // ============================================================
interface HomePageSkeletonProps { interface HomePageSkeletonProps {
/** 是否显示动画效果 */
isAnimated?: boolean;
/** 骨架屏速度(秒) */ /** 骨架屏速度(秒) */
speed?: number; speed?: number;
} }
// ============================================================ // ============================================================
// 单个卡片骨架 // 单个卡片骨架 - 深色风格
// ============================================================ // ============================================================
const FeatureCardSkeleton: React.FC<{ isFeatured?: boolean }> = ({ isFeatured = false }) => { const FeatureCardSkeleton: React.FC<{ isFeatured?: boolean; speed?: number }> = ({
const bg = useColorModeValue('white', 'gray.800'); isFeatured = false,
const borderColor = useColorModeValue('gray.200', 'gray.700'); speed = 0.8,
}) => {
return ( return (
<Box <Box
bg={bg} bg="rgba(255, 255, 255, 0.03)"
borderRadius="xl" borderRadius="2xl"
borderWidth="1px" borderWidth="1px"
borderColor={borderColor} borderColor="whiteAlpha.100"
p={isFeatured ? 8 : 6} p={{ base: 5, md: isFeatured ? 8 : 6 }}
h={isFeatured ? '350px' : '280px'} h={isFeatured ? { base: '280px', md: '320px' } : { base: '200px', md: '240px' }}
boxShadow={isFeatured ? 'xl' : 'md'} backdropFilter="blur(10px)"
position="relative"
> >
<VStack align="start" spacing={4} h="full"> <VStack align="start" spacing={4} h="full">
{/* 图标骨架 */} {/* 图标骨架 */}
<Skeleton <Skeleton
height={isFeatured ? '60px' : '48px'} height={isFeatured ? '56px' : '44px'}
width={isFeatured ? '60px' : '48px'} width={isFeatured ? '56px' : '44px'}
borderRadius="lg" borderRadius="xl"
startColor={isFeatured ? 'blue.100' : 'gray.100'} startColor="whiteAlpha.100"
endColor={isFeatured ? 'blue.200' : 'gray.200'} endColor="whiteAlpha.200"
speed={speed}
/> />
{/* 标题骨架 */} {/* 标题骨架 */}
<Skeleton height="28px" width="70%" borderRadius="md" /> <Skeleton
height="24px"
width="60%"
borderRadius="md"
startColor="whiteAlpha.100"
endColor="whiteAlpha.200"
speed={speed}
/>
{/* 描述骨架 */} {/* 描述骨架 */}
<SkeletonText <SkeletonText
mt="2" mt="2"
noOfLines={isFeatured ? 4 : 3} noOfLines={isFeatured ? 3 : 2}
spacing="3" spacing="3"
skeletonHeight="2" skeletonHeight="3"
width="100%" width="90%"
/> startColor="whiteAlpha.50"
endColor="whiteAlpha.100"
{/* 按钮骨架 */} speed={speed}
<Skeleton
height="40px"
width={isFeatured ? '140px' : '100px'}
borderRadius="md"
mt="auto"
/> />
</VStack> </VStack>
{/* Featured 徽章骨架 */}
{isFeatured && (
<Skeleton
position="absolute"
top="4"
right="4"
height="24px"
width="80px"
borderRadius="full"
/>
)}
</Box> </Box>
); );
}; };
// ============================================================
// Hero 区域骨架
// ============================================================
const HeroSkeleton: React.FC<{ speed?: number }> = ({ speed = 0.8 }) => {
return (
<VStack spacing={{ base: 4, md: 6 }} align="center" py={{ base: 8, md: 12 }}>
{/* 主标题骨架 */}
<Skeleton
height={{ base: '48px', md: '72px' }}
width={{ base: '280px', md: '400px' }}
borderRadius="lg"
startColor="whiteAlpha.100"
endColor="whiteAlpha.200"
speed={speed}
/>
{/* 副标题骨架 */}
<Skeleton
height={{ base: '20px', md: '24px' }}
width={{ base: '200px', md: '300px' }}
borderRadius="md"
startColor="whiteAlpha.50"
endColor="whiteAlpha.100"
speed={speed}
/>
</VStack>
);
};
// ============================================================ // ============================================================
// 主骨架组件 // 主骨架组件
// ============================================================ // ============================================================
export const HomePageSkeleton: React.FC<HomePageSkeletonProps> = ({ export const HomePageSkeleton: React.FC<HomePageSkeletonProps> = ({
isAnimated = true,
speed = 0.8, speed = 0.8,
}) => { }) => {
const containerBg = useColorModeValue('gray.50', 'gray.900');
return ( return (
<Box <Box
w="full" w="full"
minH="100vh" minH="100vh"
bg={containerBg} bg="linear-gradient(135deg, #0E0C15 0%, #15131D 50%, #252134 100%)"
pt={{ base: '120px', md: '140px' }} position="relative"
pb={{ base: '60px', md: '80px' }} overflow="hidden"
> >
<Container maxW="container.xl"> {/* 背景装饰 - 模拟光晕效果 */}
<VStack spacing={{ base: 8, md: 12 }} align="stretch"> <Box
{/* 顶部标题区域骨架 */} position="absolute"
<VStack spacing={4} textAlign="center"> top="-20%"
{/* 主标题 */} left="50%"
<Skeleton transform="translateX(-50%)"
height={{ base: '40px', md: '56px' }} w="800px"
width={{ base: '80%', md: '500px' }} h="800px"
borderRadius="md" bg="radial-gradient(circle, rgba(99, 102, 241, 0.08) 0%, transparent 70%)"
speed={speed} pointerEvents="none"
/> />
{/* 副标题 */} <Container maxW="7xl" position="relative" zIndex={10} px={{ base: 4, md: 6 }}>
<Skeleton <VStack
height={{ base: '20px', md: '24px' }} spacing={{ base: 8, md: 12, lg: 16 }}
width={{ base: '90%', md: '600px' }} align="stretch"
borderRadius="md" minH="100vh"
speed={speed} justify="center"
/> >
{/* Hero 区域骨架 */}
<HeroSkeleton speed={speed} />
{/* CTA 按钮 */} {/* 功能卡片区域骨架 */}
<HStack spacing={4} mt={4}> <Box pb={{ base: 8, md: 12 }}>
<Skeleton <VStack spacing={{ base: 6, md: 8 }}>
height="48px" {/* 特色功能卡片骨架 */}
width="140px" <Box w="100%">
borderRadius="lg" <FeatureCardSkeleton isFeatured speed={speed} />
speed={speed} </Box>
/>
<Skeleton
height="48px"
width="140px"
borderRadius="lg"
speed={speed}
/>
</HStack>
</VStack>
{/* 功能卡片网格骨架 */} {/* 其他功能卡片骨架 */}
<SimpleGrid <SimpleGrid
columns={{ base: 1, md: 2, lg: 3 }} columns={{ base: 1, md: 2, lg: 3 }}
spacing={{ base: 6, md: 8 }} spacing={{ base: 4, md: 5, lg: 6 }}
mt={8} w="100%"
> >
{/* 第一张卡片 - Featured (新闻中心) */} {[1, 2, 3, 4, 5].map((index) => (
<Box gridColumn={{ base: 'span 1', lg: 'span 2' }}> <FeatureCardSkeleton key={index} speed={speed} />
<FeatureCardSkeleton isFeatured /> ))}
</Box> </SimpleGrid>
</VStack>
{/* 其余 5 张卡片 */} </Box>
{[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> </VStack>
</Container> </Container>
</Box> </Box>

26
src/views/Home/index.tsx Normal file
View File

@@ -0,0 +1,26 @@
/**
* 首页入口组件
* 使用专用骨架屏作为 Suspense fallback优化加载体验
*
* @module views/Home
*/
import React, { Suspense, lazy } from 'react';
import { HomePageSkeleton } from './components/HomePageSkeleton';
// 懒加载实际首页组件
const HomePage = lazy(() => import('./HomePage'));
/**
* 首页入口 - 带骨架屏的懒加载包装
* 使用深色风格骨架屏,与首页视觉风格一致
*/
const HomePageEntry: React.FC = () => {
return (
<Suspense fallback={<HomePageSkeleton />}>
<HomePage />
</Suspense>
);
};
export default HomePageEntry;