页面加载的平滑效果添加

This commit is contained in:
ylj20011123 2025-09-08 11:04:50 +08:00
parent 09478a1b58
commit a4729f7b5c
10 changed files with 676 additions and 5 deletions

View File

@ -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,

View File

@ -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) => {

View 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;

View File

@ -0,0 +1,7 @@
.page-loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100%;
}

View File

@ -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;
import React from 'react';
import SkeletonLoading from '../SkeletonLoading';
const PageLoading: React.FC = () => {
return <SkeletonLoading type="page" />;
};
// loading components from code split
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
export default PageLoading; export default PageLoading;

View 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;
}
}
}
}

View 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;

View 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;

View File

@ -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
View 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),
};
};