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