From a4729f7b5c90343270102569a70aab4e667cac36 Mon Sep 17 00:00:00 2001 From: ylj20011123 Date: Mon, 8 Sep 2025 11:04:50 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=8A=A0=E8=BD=BD=E7=9A=84?= =?UTF-8?q?=E5=B9=B3=E6=BB=91=E6=95=88=E6=9E=9C=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.ts | 2 +- src/app.ts | 17 ++ src/components/LoadingExample/index.tsx | 65 ++++++++ src/components/PageLoading/index.less | 7 + src/components/PageLoading/index.tsx | 16 +- src/components/SkeletonLoading/index.less | 163 +++++++++++++++++++ src/components/SkeletonLoading/index.tsx | 110 +++++++++++++ src/components/SmartLoading/index.tsx | 68 ++++++++ src/global.less | 45 ++++++ src/utils/routePreloader.ts | 188 ++++++++++++++++++++++ 10 files changed, 676 insertions(+), 5 deletions(-) create mode 100644 src/components/LoadingExample/index.tsx create mode 100644 src/components/PageLoading/index.less create mode 100644 src/components/SkeletonLoading/index.less create mode 100644 src/components/SkeletonLoading/index.tsx create mode 100644 src/components/SmartLoading/index.tsx create mode 100644 src/utils/routePreloader.ts diff --git a/config/config.ts b/config/config.ts index 0e795a2..44da989 100644 --- a/config/config.ts +++ b/config/config.ts @@ -36,7 +36,7 @@ export default defineConfig({ baseNavigator: true, }, dynamicImport: { - loading: '@/components/PageLoading/index', + loading: '@/components/SmartLoading/index', }, targets: { ie: 11, diff --git a/src/app.ts b/src/app.ts index 0ebcde4..07c98e6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,6 +7,7 @@ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE */ import globalState from './globalState'; +import { routePreloader } from './utils/routePreloader'; // import { getMicroAppRouteComponent } from 'umi'; @@ -29,6 +30,22 @@ export const qiankun = { } +// 应用启动时的初始化配置 +export async function getInitialState() { + // 预加载关键路由以优化首屏加载 + setTimeout(() => { + routePreloader.preloadCriticalRoutes().then(() => { + console.log('关键路由预加载完成'); + }).catch(error => { + console.warn('关键路由预加载失败:', error); + }); + }, 1000); // 延迟1秒后开始预加载,避免影响首屏渲染 + + return { + preloadEnabled: true, + }; +} + // export const patchRoutes = ({ routes }: any) => { // console.info('routes', routes); // routes[0].routes[1].routes[0].routes.forEach((item: any, index: number) => { diff --git a/src/components/LoadingExample/index.tsx b/src/components/LoadingExample/index.tsx new file mode 100644 index 0000000..6ffb467 --- /dev/null +++ b/src/components/LoadingExample/index.tsx @@ -0,0 +1,65 @@ +import React, { Suspense } from 'react'; +import { Button, Space, Card } from 'antd'; +import SkeletonLoading from '../SkeletonLoading'; +import SmartLoading from '../SmartLoading'; + +/** + * 加载组件使用示例 + * 展示不同场景下的骨架屏效果 + */ +const LoadingExample: React.FC = () => { + return ( +
+

加载组件示例

+ + + {/* 页面级骨架屏 */} + +
+ +
+
+ + {/* 表格骨架屏 */} + +
+ +
+
+ + {/* 表单骨架屏 */} + +
+ +
+
+ + {/* 卡片网格骨架屏 */} + +
+ +
+
+ + {/* 智能加载组件 */} + +
+ +
+
+ + {/* 在Suspense中使用 */} + + }> +
+

这里是延迟加载的内容

+

骨架屏会在内容加载完成后自动消失

+
+
+
+
+
+ ); +}; + +export default LoadingExample; \ No newline at end of file diff --git a/src/components/PageLoading/index.less b/src/components/PageLoading/index.less new file mode 100644 index 0000000..d8adfe4 --- /dev/null +++ b/src/components/PageLoading/index.less @@ -0,0 +1,7 @@ +.page-loading-container { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + width: 100%; +} \ No newline at end of file diff --git a/src/components/PageLoading/index.tsx b/src/components/PageLoading/index.tsx index 096c58f..dbd4884 100644 --- a/src/components/PageLoading/index.tsx +++ b/src/components/PageLoading/index.tsx @@ -1,5 +1,13 @@ -import { PageLoading } from '@ant-design/pro-layout'; +// import { PageLoading } from '@ant-design/pro-layout'; -// loading components from code split -// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport -export default PageLoading; +// // loading components from code split +// // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport +// export default PageLoading; +import React from 'react'; +import SkeletonLoading from '../SkeletonLoading'; + +const PageLoading: React.FC = () => { + return ; +}; + +export default PageLoading; \ No newline at end of file diff --git a/src/components/SkeletonLoading/index.less b/src/components/SkeletonLoading/index.less new file mode 100644 index 0000000..e319965 --- /dev/null +++ b/src/components/SkeletonLoading/index.less @@ -0,0 +1,163 @@ +.skeleton-page-container { + padding: 24px; + min-height: 100vh; + background: #f5f5f5; + + .skeleton-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + padding: 16px 24px; + background: white; + border-radius: 6px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); + + .skeleton-header-actions { + display: flex; + gap: 8px; + } + } + + .skeleton-content { + .skeleton-filter-bar { + display: flex; + align-items: center; + margin-bottom: 16px; + padding: 16px 24px; + background: white; + border-radius: 6px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); + } + + .skeleton-table-card { + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); + } + } +} + +.skeleton-table-container { + padding: 16px; + + .skeleton-table-header { + display: flex; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; + margin-bottom: 16px; + } + + .skeleton-table-row { + display: flex; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f9f9f9; + + &:last-child { + border-bottom: none; + } + } +} + +.skeleton-card-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 16px; + padding: 16px; + + .skeleton-card { + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); + } +} + +.skeleton-form-container { + padding: 24px; + + .skeleton-form-title { + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid #f0f0f0; + } + + .skeleton-form-row { + display: flex; + align-items: center; + margin-bottom: 16px; + + .skeleton-form-label { + width: 120px; + margin-right: 16px; + } + + .skeleton-form-control { + flex: 1; + } + } + + .skeleton-form-actions { + margin-top: 32px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; + text-align: center; + } +} + +// 骨架屏动画优化 +.ant-skeleton-element { + .ant-skeleton-input { + border-radius: 4px; + } + + .ant-skeleton-button { + border-radius: 4px; + } +} + +// 响应式设计 +@media (max-width: 768px) { + .skeleton-page-container { + padding: 12px; + + .skeleton-header { + flex-direction: column; + gap: 12px; + align-items: stretch; + + .skeleton-header-actions { + justify-content: center; + } + } + + .skeleton-content { + .skeleton-filter-bar { + flex-direction: column; + gap: 12px; + align-items: stretch; + + .ant-skeleton-input { + width: 100% !important; + } + } + } + } + + .skeleton-card-container { + grid-template-columns: 1fr; + padding: 12px; + } + + .skeleton-form-container { + padding: 12px; + + .skeleton-form-row { + flex-direction: column; + align-items: stretch; + + .skeleton-form-label { + width: 100%; + margin-right: 0; + margin-bottom: 8px; + } + } + } +} \ No newline at end of file diff --git a/src/components/SkeletonLoading/index.tsx b/src/components/SkeletonLoading/index.tsx new file mode 100644 index 0000000..f7eed92 --- /dev/null +++ b/src/components/SkeletonLoading/index.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Skeleton, Card } from 'antd'; +import './index.less'; + +interface SkeletonLoadingProps { + type?: 'page' | 'table' | 'card' | 'form'; + rows?: number; +} + +const SkeletonLoading: React.FC = ({ + type = 'page', + rows = 3 +}) => { + const renderPageSkeleton = () => ( +
+
+ +
+ + +
+
+ +
+
+ + + +
+ + + + +
+
+ ); + + const renderTableSkeleton = () => ( +
+
+ {Array.from({ length: 5 }).map((_, index) => ( + + ))} +
+ {Array.from({ length: rows }).map((_, index) => ( +
+ {Array.from({ length: 5 }).map((_, cellIndex) => ( + + ))} +
+ ))} +
+ ); + + const renderCardSkeleton = () => ( +
+ {Array.from({ length: rows }).map((_, index) => ( + + + + ))} +
+ ); + + const renderFormSkeleton = () => ( +
+ +
+ +
+ {Array.from({ length: rows }).map((_, index) => ( +
+
+ +
+
+ +
+
+ ))} +
+ + +
+
+
+ ); + + switch (type) { + case 'table': + return renderTableSkeleton(); + case 'card': + return renderCardSkeleton(); + case 'form': + return renderFormSkeleton(); + case 'page': + default: + return renderPageSkeleton(); + } +}; + +export default SkeletonLoading; \ No newline at end of file diff --git a/src/components/SmartLoading/index.tsx b/src/components/SmartLoading/index.tsx new file mode 100644 index 0000000..91fefa4 --- /dev/null +++ b/src/components/SmartLoading/index.tsx @@ -0,0 +1,68 @@ +import React, { useEffect, useState } from 'react'; +import { useLocation } from 'umi'; +import SkeletonLoading from '../SkeletonLoading'; +import { routePreloader } from '@/utils/routePreloader'; + +interface SmartLoadingProps { + fallback?: React.ReactNode; + enablePreload?: boolean; +} + +/** + * 智能加载组件 + * 根据路由类型自动选择合适的骨架屏,并支持路由预加载 + */ +const SmartLoading: React.FC = ({ + fallback, + enablePreload = true +}) => { + const location = useLocation(); + const [loadingType, setLoadingType] = useState<'page' | 'table' | 'form' | 'card'>('page'); + + useEffect(() => { + // 根据路径判断页面类型,选择合适的骨架屏 + const path = location.pathname; + + if (path.includes('list') || path.includes('table')) { + setLoadingType('table'); + } else if (path.includes('form') || path.includes('edit') || path.includes('add')) { + setLoadingType('form'); + } else if (path.includes('card') || path.includes('dashboard')) { + setLoadingType('card'); + } else { + setLoadingType('page'); + } + + // 预加载相关路由 + if (enablePreload) { + routePreloader.preloadBasedOnUserBehavior(path); + } + }, [location.pathname, enablePreload]); + + // 如果提供了自定义fallback,使用它 + if (fallback) { + return <>{fallback}; + } + + // 根据页面类型返回对应的骨架屏 + return ; +}; + +/** + * 根据加载类型获取合适的行数 + */ +function getRowsByType(type: string): number { + switch (type) { + case 'table': + return 8; // 表格通常显示更多行 + case 'form': + return 5; // 表单通常5-6个字段 + case 'card': + return 6; // 卡片网格通常6个 + case 'page': + default: + return 4; // 默认4行 + } +} + +export default SmartLoading; \ No newline at end of file diff --git a/src/global.less b/src/global.less index 09099a3..f572cc0 100644 --- a/src/global.less +++ b/src/global.less @@ -24,6 +24,51 @@ body { -moz-osx-font-smoothing: grayscale; } +// 骨架屏优化样式 +.ant-skeleton { + .ant-skeleton-content { + + .ant-skeleton-title, + .ant-skeleton-paragraph>li { + background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%); + background-size: 200% 100%; + animation: loading 1.4s ease-in-out infinite; + } + } + + .ant-skeleton-avatar { + background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%); + background-size: 200% 100%; + animation: loading 1.4s ease-in-out infinite; + } + + .ant-skeleton-input, + .ant-skeleton-button { + background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%); + background-size: 200% 100%; + animation: loading 1.4s ease-in-out infinite; + } +} + +@keyframes loading { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } +} + +// 页面切换动画优化 +.ant-layout-content { + transition: opacity 0.2s ease-in-out; + + &.loading { + opacity: 0.7; + } +} + ul, ol { list-style: none; diff --git a/src/utils/routePreloader.ts b/src/utils/routePreloader.ts new file mode 100644 index 0000000..52a1a53 --- /dev/null +++ b/src/utils/routePreloader.ts @@ -0,0 +1,188 @@ +/** + * 路由预加载工具 + * 优化首屏加载和用户体验 + */ + +interface PreloadOptions { + delay?: number; // 延迟预加载时间(ms) + priority?: 'high' | 'low' | 'auto'; // 预加载优先级 + onlyOnIdle?: boolean; // 仅在浏览器空闲时预加载 +} + +class RoutePreloader { + private preloadedRoutes = new Set(); + private preloadPromises = new Map>(); + + /** + * 预加载指定路由 + * @param routePath 路由路径 + * @param importFunction 动态导入函数 + * @param options 预加载选项 + */ + async preloadRoute( + routePath: string, + importFunction: () => Promise, + options: PreloadOptions = {} + ) { + const { delay = 0, onlyOnIdle = true } = options; + + // 避免重复预加载 + if (this.preloadedRoutes.has(routePath)) { + return this.preloadPromises.get(routePath); + } + + const preloadPromise = new Promise((resolve) => { + const executePreload = () => { + setTimeout(async () => { + try { + const module = await importFunction(); + this.preloadedRoutes.add(routePath); + resolve(module); + } catch (error) { + console.warn(`预加载路由失败: ${routePath}`, error); + resolve(null); + } + }, delay); + }; + + if (onlyOnIdle && 'requestIdleCallback' in window) { + // 在浏览器空闲时执行预加载 + requestIdleCallback(executePreload, { timeout: 5000 }); + } else { + executePreload(); + } + }); + + this.preloadPromises.set(routePath, preloadPromise); + return preloadPromise; + } + + /** + * 批量预加载路由 + * @param routes 路由配置数组 + */ + async preloadRoutes(routes: Array<{ + path: string; + import: () => Promise; + options?: PreloadOptions; + }>) { + const promises = routes.map(({ path, import: importFn, options }) => + this.preloadRoute(path, importFn, options) + ); + + return Promise.allSettled(promises); + } + + /** + * 预加载首屏关键路由 + */ + async preloadCriticalRoutes() { + const criticalRoutes = [ + { + path: '/dashboard/analysis', + import: () => import('@/pages/busniess/Analysis'), + options: { priority: 'high' as const, delay: 100 } + }, + { + path: '/dashboard/workplace', + import: () => import('@/pages/dashboard/workplace'), + options: { priority: 'high' as const, delay: 200 } + }, + { + path: '/busniessproject/list', + import: () => import('@/pages/BussinessProject/list'), + options: { priority: 'high' as const, delay: 300 } + } + ]; + + return this.preloadRoutes(criticalRoutes); + } + + /** + * 基于用户行为预加载路由 + * @param currentPath 当前路径 + */ + async preloadBasedOnUserBehavior(currentPath: string) { + // 根据当前页面预测用户可能访问的页面 + const predictions = this.getPredictedRoutes(currentPath); + + const routesToPreload = predictions.map(path => ({ + path, + import: () => this.getRouteImport(path), + options: { priority: 'low' as const, delay: 1000, onlyOnIdle: true } + })); + + return this.preloadRoutes(routesToPreload); + } + + /** + * 根据当前路径预测用户可能访问的路由 + */ + private getPredictedRoutes(currentPath: string): string[] { + const routeMap: Record = { + '/dashboard/analysis': [ + '/busniessproject/list', + '/dashboard/workplace', + '/basicManage/brand' + ], + '/busniessproject/list': [ + '/busniessproject/detail', + '/contract/list', + '/busniess/paymentConfirm' + ], + '/basicManage/brand': [ + '/basicManage/serverpart', + '/basicManage/merchants', + '/basicManage/commodity' + ], + // 可以根据实际业务流程继续添加 + }; + + return routeMap[currentPath] || []; + } + + /** + * 获取路由对应的动态导入函数 + */ + private async getRouteImport(path: string) { + // 这里需要根据实际的路由映射来返回对应的import函数 + const routeImportMap: Record Promise> = { + '/dashboard/analysis': () => import('@/pages/busniess/Analysis'), + '/dashboard/workplace': () => import('@/pages/dashboard/workplace'), + '/busniessproject/list': () => import('@/pages/BussinessProject/list'), + '/busniessproject/detail': () => import('@/pages/BussinessProject/detail'), + '/contract/list': () => import('@/pages/contract/list'), + '/basicManage/brand': () => import('@/pages/basicManage/Brand'), + '/basicManage/serverpart': () => import('@/pages/basicManage/Serverpart'), + '/basicManage/merchants': () => import('@/pages/basicManage/Merchats'), + '/basicManage/commodity': () => import('@/pages/basicManage/Commodity/list'), + }; + + const importFn = routeImportMap[path]; + if (importFn) { + return importFn(); + } + + return Promise.resolve(null); + } + + /** + * 清理预加载缓存 + */ + clearCache() { + this.preloadedRoutes.clear(); + this.preloadPromises.clear(); + } +} + +// 创建单例实例 +export const routePreloader = new RoutePreloader(); + +// 导出预加载钩子 +export const useRoutePreloader = () => { + return { + preloadRoute: routePreloader.preloadRoute.bind(routePreloader), + preloadCriticalRoutes: routePreloader.preloadCriticalRoutes.bind(routePreloader), + preloadBasedOnUserBehavior: routePreloader.preloadBasedOnUserBehavior.bind(routePreloader), + }; +}; \ No newline at end of file