页面加载的平滑效果添加
This commit is contained in:
parent
09478a1b58
commit
a4729f7b5c
@ -36,7 +36,7 @@ export default defineConfig({
|
|||||||
baseNavigator: true,
|
baseNavigator: true,
|
||||||
},
|
},
|
||||||
dynamicImport: {
|
dynamicImport: {
|
||||||
loading: '@/components/PageLoading/index',
|
loading: '@/components/SmartLoading/index',
|
||||||
},
|
},
|
||||||
targets: {
|
targets: {
|
||||||
ie: 11,
|
ie: 11,
|
||||||
|
|||||||
17
src/app.ts
17
src/app.ts
@ -7,6 +7,7 @@
|
|||||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||||
*/
|
*/
|
||||||
import globalState from './globalState';
|
import globalState from './globalState';
|
||||||
|
import { routePreloader } from './utils/routePreloader';
|
||||||
|
|
||||||
// import { getMicroAppRouteComponent } from 'umi';
|
// 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) => {
|
// export const patchRoutes = ({ routes }: any) => {
|
||||||
// console.info('routes', routes);
|
// console.info('routes', routes);
|
||||||
// routes[0].routes[1].routes[0].routes.forEach((item: any, index: number) => {
|
// routes[0].routes[1].routes[0].routes.forEach((item: any, index: number) => {
|
||||||
|
|||||||
65
src/components/LoadingExample/index.tsx
Normal file
65
src/components/LoadingExample/index.tsx
Normal file
@ -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 (
|
||||||
|
<div style={{ padding: 24 }}>
|
||||||
|
<h2>加载组件示例</h2>
|
||||||
|
|
||||||
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
|
{/* 页面级骨架屏 */}
|
||||||
|
<Card title="页面级骨架屏" size="small">
|
||||||
|
<div style={{ height: 400, overflow: 'hidden' }}>
|
||||||
|
<SkeletonLoading type="page" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 表格骨架屏 */}
|
||||||
|
<Card title="表格骨架屏" size="small">
|
||||||
|
<div style={{ height: 300, overflow: 'hidden' }}>
|
||||||
|
<SkeletonLoading type="table" rows={5} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 表单骨架屏 */}
|
||||||
|
<Card title="表单骨架屏" size="small">
|
||||||
|
<div style={{ height: 300, overflow: 'hidden' }}>
|
||||||
|
<SkeletonLoading type="form" rows={4} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 卡片网格骨架屏 */}
|
||||||
|
<Card title="卡片网格骨架屏" size="small">
|
||||||
|
<div style={{ height: 300, overflow: 'hidden' }}>
|
||||||
|
<SkeletonLoading type="card" rows={4} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 智能加载组件 */}
|
||||||
|
<Card title="智能加载组件(根据路由自动选择)" size="small">
|
||||||
|
<div style={{ height: 200, overflow: 'hidden' }}>
|
||||||
|
<SmartLoading />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 在Suspense中使用 */}
|
||||||
|
<Card title="在Suspense中使用" size="small">
|
||||||
|
<Suspense fallback={<SkeletonLoading type="page" />}>
|
||||||
|
<div style={{ padding: 20, textAlign: 'center' }}>
|
||||||
|
<h3>这里是延迟加载的内容</h3>
|
||||||
|
<p>骨架屏会在内容加载完成后自动消失</p>
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingExample;
|
||||||
7
src/components/PageLoading/index.less
Normal file
7
src/components/PageLoading/index.less
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.page-loading-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@ -1,5 +1,13 @@
|
|||||||
import { PageLoading } from '@ant-design/pro-layout';
|
// import { PageLoading } from '@ant-design/pro-layout';
|
||||||
|
|
||||||
// loading components from code split
|
// // loading components from code split
|
||||||
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
|
// // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
|
||||||
export default PageLoading;
|
// export default PageLoading;
|
||||||
|
import React from 'react';
|
||||||
|
import SkeletonLoading from '../SkeletonLoading';
|
||||||
|
|
||||||
|
const PageLoading: React.FC = () => {
|
||||||
|
return <SkeletonLoading type="page" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageLoading;
|
||||||
163
src/components/SkeletonLoading/index.less
Normal file
163
src/components/SkeletonLoading/index.less
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/components/SkeletonLoading/index.tsx
Normal file
110
src/components/SkeletonLoading/index.tsx
Normal file
@ -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<SkeletonLoadingProps> = ({
|
||||||
|
type = 'page',
|
||||||
|
rows = 3
|
||||||
|
}) => {
|
||||||
|
const renderPageSkeleton = () => (
|
||||||
|
<div className="skeleton-page-container">
|
||||||
|
<div className="skeleton-header">
|
||||||
|
<Skeleton.Input style={{ width: 200, height: 32 }} active />
|
||||||
|
<div className="skeleton-header-actions">
|
||||||
|
<Skeleton.Button style={{ width: 80 }} active />
|
||||||
|
<Skeleton.Button style={{ width: 80 }} active />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="skeleton-content">
|
||||||
|
<div className="skeleton-filter-bar">
|
||||||
|
<Skeleton.Input style={{ width: 150, marginRight: 16 }} active />
|
||||||
|
<Skeleton.Input style={{ width: 150, marginRight: 16 }} active />
|
||||||
|
<Skeleton.Button style={{ width: 60 }} active />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="skeleton-table-card">
|
||||||
|
<Skeleton active paragraph={{ rows: 6 }} />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTableSkeleton = () => (
|
||||||
|
<div className="skeleton-table-container">
|
||||||
|
<div className="skeleton-table-header">
|
||||||
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
|
<Skeleton.Input
|
||||||
|
key={index}
|
||||||
|
style={{ width: `${Math.random() * 50 + 80}px`, marginRight: 16 }}
|
||||||
|
active
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{Array.from({ length: rows }).map((_, index) => (
|
||||||
|
<div key={index} className="skeleton-table-row">
|
||||||
|
{Array.from({ length: 5 }).map((_, cellIndex) => (
|
||||||
|
<Skeleton.Input
|
||||||
|
key={cellIndex}
|
||||||
|
style={{ width: `${Math.random() * 60 + 60}px`, marginRight: 16 }}
|
||||||
|
active
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCardSkeleton = () => (
|
||||||
|
<div className="skeleton-card-container">
|
||||||
|
{Array.from({ length: rows }).map((_, index) => (
|
||||||
|
<Card key={index} className="skeleton-card">
|
||||||
|
<Skeleton active avatar paragraph={{ rows: 2 }} />
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderFormSkeleton = () => (
|
||||||
|
<div className="skeleton-form-container">
|
||||||
|
<Card>
|
||||||
|
<div className="skeleton-form-title">
|
||||||
|
<Skeleton.Input style={{ width: 150 }} active />
|
||||||
|
</div>
|
||||||
|
{Array.from({ length: rows }).map((_, index) => (
|
||||||
|
<div key={index} className="skeleton-form-row">
|
||||||
|
<div className="skeleton-form-label">
|
||||||
|
<Skeleton.Input style={{ width: 80 }} active />
|
||||||
|
</div>
|
||||||
|
<div className="skeleton-form-control">
|
||||||
|
<Skeleton.Input style={{ width: '100%' }} active />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="skeleton-form-actions">
|
||||||
|
<Skeleton.Button style={{ width: 80, marginRight: 8 }} active />
|
||||||
|
<Skeleton.Button style={{ width: 80 }} active />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'table':
|
||||||
|
return renderTableSkeleton();
|
||||||
|
case 'card':
|
||||||
|
return renderCardSkeleton();
|
||||||
|
case 'form':
|
||||||
|
return renderFormSkeleton();
|
||||||
|
case 'page':
|
||||||
|
default:
|
||||||
|
return renderPageSkeleton();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SkeletonLoading;
|
||||||
68
src/components/SmartLoading/index.tsx
Normal file
68
src/components/SmartLoading/index.tsx
Normal file
@ -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<SmartLoadingProps> = ({
|
||||||
|
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 <SkeletonLoading type={loadingType} rows={getRowsByType(loadingType)} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据加载类型获取合适的行数
|
||||||
|
*/
|
||||||
|
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;
|
||||||
@ -24,6 +24,51 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-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,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|||||||
188
src/utils/routePreloader.ts
Normal file
188
src/utils/routePreloader.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* 路由预加载工具
|
||||||
|
* 优化首屏加载和用户体验
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface PreloadOptions {
|
||||||
|
delay?: number; // 延迟预加载时间(ms)
|
||||||
|
priority?: 'high' | 'low' | 'auto'; // 预加载优先级
|
||||||
|
onlyOnIdle?: boolean; // 仅在浏览器空闲时预加载
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoutePreloader {
|
||||||
|
private preloadedRoutes = new Set<string>();
|
||||||
|
private preloadPromises = new Map<string, Promise<any>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预加载指定路由
|
||||||
|
* @param routePath 路由路径
|
||||||
|
* @param importFunction 动态导入函数
|
||||||
|
* @param options 预加载选项
|
||||||
|
*/
|
||||||
|
async preloadRoute(
|
||||||
|
routePath: string,
|
||||||
|
importFunction: () => Promise<any>,
|
||||||
|
options: PreloadOptions = {}
|
||||||
|
) {
|
||||||
|
const { delay = 0, onlyOnIdle = true } = options;
|
||||||
|
|
||||||
|
// 避免重复预加载
|
||||||
|
if (this.preloadedRoutes.has(routePath)) {
|
||||||
|
return this.preloadPromises.get(routePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const preloadPromise = new Promise<any>((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<any>;
|
||||||
|
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<string, string[]> = {
|
||||||
|
'/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<string, () => Promise<any>> = {
|
||||||
|
'/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),
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user