先推一版热重载问题修复 且带上了骨架屏效果的一版
This commit is contained in:
parent
14b175f28c
commit
8f8b34763f
@ -15,6 +15,7 @@ import proxy from './proxy';
|
|||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
|
|
||||||
const { REACT_APP_ENV } = process.env;
|
const { REACT_APP_ENV } = process.env;
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
hash: true,
|
hash: true,
|
||||||
@ -36,7 +37,7 @@ export default defineConfig({
|
|||||||
baseNavigator: true,
|
baseNavigator: true,
|
||||||
},
|
},
|
||||||
dynamicImport: {
|
dynamicImport: {
|
||||||
loading: '@/components/PageLoading/index',
|
loading: '@/components/SmartLoading/index',
|
||||||
},
|
},
|
||||||
targets: {
|
targets: {
|
||||||
ie: 11,
|
ie: 11,
|
||||||
@ -45,13 +46,14 @@ export default defineConfig({
|
|||||||
routes,
|
routes,
|
||||||
// Theme for antd: https://ant.design/docs/react/customize-theme-cn
|
// Theme for antd: https://ant.design/docs/react/customize-theme-cn
|
||||||
theme: {
|
theme: {
|
||||||
|
// 'primary-color': defaultSettings.primaryColor,
|
||||||
'primary-color': defaultSettings.primaryColor,
|
'primary-color': defaultSettings.primaryColor,
|
||||||
},
|
},
|
||||||
title: false,
|
title: false,
|
||||||
ignoreMomentLocale: true,
|
ignoreMomentLocale: true,
|
||||||
proxy: proxy[REACT_APP_ENV || 'dev'],
|
proxy: proxy[REACT_APP_ENV || 'dev'],
|
||||||
base: '/cloud',
|
base: '/cloud',
|
||||||
publicPath: '/cloud/',
|
publicPath: process.env.NODE_ENV === 'development' ? '/' : '/cloud/',
|
||||||
// 快速刷新功能 https://umijs.org/config#fastrefresh
|
// 快速刷新功能 https://umijs.org/config#fastrefresh
|
||||||
fastRefresh: {},
|
fastRefresh: {},
|
||||||
esbuild: {},
|
esbuild: {},
|
||||||
@ -59,5 +61,5 @@ export default defineConfig({
|
|||||||
master: {}
|
master: {}
|
||||||
},
|
},
|
||||||
webpack5: {
|
webpack5: {
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -63,7 +63,7 @@
|
|||||||
"@umijs/route-utils": "^1.0.33",
|
"@umijs/route-utils": "^1.0.33",
|
||||||
"ahooks": "^2.10.3",
|
"ahooks": "^2.10.3",
|
||||||
"antd": "^4.17.0",
|
"antd": "^4.17.0",
|
||||||
"aws-sdk": "^2.1692.0",
|
"aws-sdk": "2.1692.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"compression-webpack-plugin": "^11.1.0",
|
"compression-webpack-plugin": "^11.1.0",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
@ -142,4 +142,4 @@
|
|||||||
"config/**/*.js*",
|
"config/**/*.js*",
|
||||||
"scripts/**/*.js"
|
"scripts/**/*.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,9 @@
|
|||||||
* @FilePath: \cloud-platform\src\app.ts
|
* @FilePath: \cloud-platform\src\app.ts
|
||||||
* @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 './patchCssHmr';
|
||||||
|
import './global.less';
|
||||||
import globalState from './globalState';
|
import globalState from './globalState';
|
||||||
|
|
||||||
// import { getMicroAppRouteComponent } from 'umi';
|
// import { getMicroAppRouteComponent } from 'umi';
|
||||||
|
|
||||||
// 将全局state注入子应用
|
// 将全局state注入子应用
|
||||||
|
|||||||
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;
|
||||||
65
src/components/PageTransition/index.less
Normal file
65
src/components/PageTransition/index.less
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// 简化版页面过渡动画
|
||||||
|
.simple-page-transition {
|
||||||
|
|
||||||
|
// 默认不播放动画,只有带animate类时才播放
|
||||||
|
&.animate {
|
||||||
|
animation: pageEnter 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
will-change: opacity, transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pageEnter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px) scale(0.96);
|
||||||
|
filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
filter: blur(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复杂版页面过渡动画
|
||||||
|
.page-transition {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.transition-content {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&.fadeIn {
|
||||||
|
animation: fadeIn 0.3s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fadeOut {
|
||||||
|
animation: fadeOut 0.3s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/components/PageTransition/index.tsx
Normal file
72
src/components/PageTransition/index.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||||
|
import { useLocation } from 'umi';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
export interface PageTransitionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
type?: 'fade' | 'slide' | 'scale';
|
||||||
|
duration?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageTransition: React.FC<PageTransitionProps> = ({
|
||||||
|
children,
|
||||||
|
type = 'fade',
|
||||||
|
duration = 300,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [displayLocation, setDisplayLocation] = useState(location);
|
||||||
|
const [transitionStage, setTransitionStage] = useState('fadeIn');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (location !== displayLocation) setTransitionStage('fadeOut');
|
||||||
|
}, [location, displayLocation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`page-transition ${type}-transition ${className}`}>
|
||||||
|
<div
|
||||||
|
className={`transition-content ${transitionStage}`}
|
||||||
|
onAnimationEnd={() => {
|
||||||
|
if (transitionStage === 'fadeOut') {
|
||||||
|
setDisplayLocation(location);
|
||||||
|
setTransitionStage('fadeIn');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 简化版本 - 仅使用CSS动画
|
||||||
|
export const SimplePageTransition: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
enableAnimation?: boolean;
|
||||||
|
onAnimationEnd?: () => void;
|
||||||
|
}> = ({
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
enableAnimation = true,
|
||||||
|
onAnimationEnd,
|
||||||
|
}) => {
|
||||||
|
const handleAnimationEnd = () => {
|
||||||
|
if (enableAnimation && onAnimationEnd) {
|
||||||
|
onAnimationEnd();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`simple-page-transition ${enableAnimation ? 'animate' : ''} ${className}`}
|
||||||
|
onAnimationEnd={handleAnimationEnd}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageTransition;
|
||||||
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;
|
||||||
107
src/components/SmartLoading/index.tsx
Normal file
107
src/components/SmartLoading/index.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
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 [loadingType, setLoadingType] = useState<'page' | 'table' | 'form' | 'card'>('page');
|
||||||
|
const [shouldShow, setShouldShow] = useState(false);
|
||||||
|
const [location, setLocation] = useState<any>(null);
|
||||||
|
|
||||||
|
// 安全获取location,避免在路由未准备好时出错
|
||||||
|
try {
|
||||||
|
const currentLocation = useLocation();
|
||||||
|
if (!location) {
|
||||||
|
setLocation(currentLocation);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 如果useLocation失败,使用window.location作为fallback
|
||||||
|
if (!location && typeof window !== 'undefined') {
|
||||||
|
setLocation({ pathname: window.location.pathname });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 对于页面切换,立即显示骨架屏,但对于初始加载延迟显示
|
||||||
|
const isInitialLoad = !location?.pathname || location.pathname === '/';
|
||||||
|
|
||||||
|
if (isInitialLoad) {
|
||||||
|
// 初始加载时延迟显示,避免刷新时闪烁
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setShouldShow(true);
|
||||||
|
}, 200);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
} else {
|
||||||
|
// 页面切换时立即显示,保持骨架屏效果
|
||||||
|
setShouldShow(true);
|
||||||
|
}
|
||||||
|
}, [location?.pathname]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!location?.pathname) return;
|
||||||
|
|
||||||
|
// 根据路径判断页面类型,选择合适的骨架屏
|
||||||
|
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) {
|
||||||
|
setTimeout(() => {
|
||||||
|
routePreloader.preloadBasedOnUserBehavior(path);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [location?.pathname, enablePreload]);
|
||||||
|
|
||||||
|
// 如果提供了自定义fallback,使用它
|
||||||
|
if (fallback) {
|
||||||
|
return <>{fallback}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟显示,避免闪烁
|
||||||
|
if (!shouldShow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据页面类型返回对应的骨架屏
|
||||||
|
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;
|
||||||
79
src/components/TabVirtualizer/index.less
Normal file
79
src/components/TabVirtualizer/index.less
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
.tab-virtualizer {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unloaded {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400px;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
.unloaded-placeholder {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
|
||||||
|
.placeholder-icon {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-content {
|
||||||
|
h3 {
|
||||||
|
color: #666;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-desc {
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-access-time {
|
||||||
|
color: #bbb;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reload-btn {
|
||||||
|
min-width: 120px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(24, 144, 255, 0.3);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loaded {
|
||||||
|
// 正常加载状态的样式
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/components/TabVirtualizer/index.tsx
Normal file
96
src/components/TabVirtualizer/index.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { ReloadOutlined, ClockCircleOutlined } from '@ant-design/icons';
|
||||||
|
import PageLoading from '../PageLoading';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
export interface TabVirtualizerProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
tabKey: string;
|
||||||
|
isActive: boolean;
|
||||||
|
isLoaded: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
lastAccessTime?: number;
|
||||||
|
onReload: (tabKey: string) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabVirtualizer: React.FC<TabVirtualizerProps> = ({
|
||||||
|
children,
|
||||||
|
tabKey,
|
||||||
|
isActive,
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
lastAccessTime,
|
||||||
|
onReload,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 格式化最后访问时间
|
||||||
|
const formatLastAccessTime = (timestamp?: number) => {
|
||||||
|
if (!timestamp) return '';
|
||||||
|
const now = Date.now();
|
||||||
|
const diff = now - timestamp;
|
||||||
|
const minutes = Math.floor(diff / 60000);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}小时前访问`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}分钟前访问`;
|
||||||
|
} else {
|
||||||
|
return '刚刚访问';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果正在加载,显示加载状态
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className={`tab-virtualizer loading ${className}`} ref={containerRef}>
|
||||||
|
<PageLoading />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果未加载(被卸载),显示占位符
|
||||||
|
if (!isLoaded) {
|
||||||
|
return (
|
||||||
|
<div className={`tab-virtualizer unloaded ${className}`} ref={containerRef}>
|
||||||
|
<div className="unloaded-placeholder">
|
||||||
|
<div className="placeholder-icon">
|
||||||
|
<ClockCircleOutlined style={{ fontSize: 48, color: '#d9d9d9' }} />
|
||||||
|
</div>
|
||||||
|
<div className="placeholder-content">
|
||||||
|
<h3>页面已休眠</h3>
|
||||||
|
<p className="placeholder-desc">
|
||||||
|
为了优化内存使用,此页面内容已被暂时卸载
|
||||||
|
</p>
|
||||||
|
{lastAccessTime && (
|
||||||
|
<p className="last-access-time">
|
||||||
|
{formatLastAccessTime(lastAccessTime)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={() => onReload(tabKey)}
|
||||||
|
className="reload-btn"
|
||||||
|
>
|
||||||
|
重新加载
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常渲染内容
|
||||||
|
return (
|
||||||
|
<div className={`tab-virtualizer loaded ${className}`} ref={containerRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabVirtualizer;
|
||||||
@ -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;
|
||||||
@ -55,4 +100,18 @@ ol {
|
|||||||
body .ant-design-pro>.ant-layout {
|
body .ant-design-pro>.ant-layout {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面过渡优化
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增强页面切换体验
|
||||||
|
.ant-tabs-content-holder {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tabpane {
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
@import '~antd/es/style/themes/default.less';
|
@import '~antd/es/style/themes/default.less';
|
||||||
|
@import '../components/PageTransition/index.less';
|
||||||
|
|
||||||
.ant-pro-global-header {
|
.ant-pro-global-header {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|||||||
@ -15,68 +15,101 @@ type SecurityLayoutProps = {
|
|||||||
|
|
||||||
type SecurityLayoutState = {
|
type SecurityLayoutState = {
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
|
shouldShowLoading: boolean;
|
||||||
|
initialLoadComplete: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
|
class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
|
||||||
state: SecurityLayoutState = {
|
state: SecurityLayoutState = {
|
||||||
isReady: false,
|
isReady: false,
|
||||||
|
shouldShowLoading: false,
|
||||||
|
initialLoadComplete: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private loadingTimer?: NodeJS.Timeout;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const { location } = history;
|
||||||
const { location } = history
|
|
||||||
|
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
if (dispatch) {
|
|
||||||
|
|
||||||
|
// 检查是否有缓存的用户信息,避免不必要的加载状态
|
||||||
|
const cachedUser = session.get("currentUser");
|
||||||
|
|
||||||
|
// 设置防闪烁定时器,只有加载时间超过300ms才显示loading
|
||||||
|
this.loadingTimer = setTimeout(() => {
|
||||||
|
if (!this.state.isReady && !this.state.initialLoadComplete) {
|
||||||
|
this.setState({
|
||||||
|
shouldShowLoading: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
if (dispatch) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'user/fetch', callback: (user) => {
|
type: 'user/fetch',
|
||||||
|
callback: (user) => {
|
||||||
|
// 清除定时器
|
||||||
|
if (this.loadingTimer) {
|
||||||
|
clearTimeout(this.loadingTimer);
|
||||||
|
}
|
||||||
|
|
||||||
if (user.code && location.pathname !== '/user/login') {
|
if (user.code && location.pathname !== '/user/login') {
|
||||||
history.push('/user/login');
|
history.push('/user/login');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
console.log('secur')
|
|
||||||
dispatch({
|
|
||||||
type: 'global/getMenuData', payload: user.ID, callback: (menu) => {
|
|
||||||
if (menu) {
|
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'global/getMenuData',
|
||||||
|
payload: user.ID,
|
||||||
|
callback: (menu) => {
|
||||||
|
if (menu) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isReady: true,
|
isReady: true,
|
||||||
|
shouldShowLoading: false,
|
||||||
|
initialLoadComplete: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
// 清除定时器
|
||||||
|
if (this.loadingTimer) {
|
||||||
|
clearTimeout(this.loadingTimer);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isReady: true,
|
isReady: true,
|
||||||
|
shouldShowLoading: false,
|
||||||
|
initialLoadComplete: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.loadingTimer) {
|
||||||
|
clearTimeout(this.loadingTimer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isReady } = this.state;
|
const { isReady, shouldShowLoading, initialLoadComplete } = this.state;
|
||||||
const { children, loading, currentUser } = this.props;
|
const { children, currentUser } = this.props;
|
||||||
// const { location } = history;
|
|
||||||
|
|
||||||
|
// 用户认证规则
|
||||||
// You can replace it to your authentication rule (such as check token exists)
|
|
||||||
// You can replace it with your own login authentication rules (such as judging whether the token exists)
|
|
||||||
const isLogin = currentUser && currentUser.ID;
|
const isLogin = currentUser && currentUser.ID;
|
||||||
|
|
||||||
|
// 如果初始化未完成且需要显示加载状态,才显示骨架屏
|
||||||
if ((!isLogin && loading) || !isReady) {
|
if (!isReady && shouldShowLoading) {
|
||||||
return <PageLoading />;
|
return <PageLoading />;
|
||||||
}
|
}
|
||||||
// if (!isLogin && location.pathname !== '/user/login') {
|
|
||||||
// history.push('/user/login');
|
// 如果还在初始化过程中,但不需要显示loading,返回null(避免闪烁)
|
||||||
// }
|
if (!isReady && !shouldShowLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,14 +12,16 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
line-height: 64px;
|
line-height: 64px;
|
||||||
display:flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0px 3px 9px 0px rgba(204,212,230,0.84);
|
box-shadow: 0px 3px 9px 0px rgba(204, 212, 230, 0.84);
|
||||||
|
|
||||||
:global(.ant-dropdown-trigger) {
|
:global(.ant-dropdown-trigger) {
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.headtitle {
|
.headtitle {
|
||||||
color: #191919;
|
color: #191919;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -53,5 +55,5 @@
|
|||||||
// .content {
|
// .content {
|
||||||
// padding: 32px 0 24px;
|
// padding: 32px 0 24px;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -56,18 +56,18 @@ const UserLayout: React.FC<UserLayoutProps> = (props) => {
|
|||||||
<div className={styles.lang}>
|
<div className={styles.lang}>
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<img alt="logo" className={styles.logo} src={logo} />
|
<img alt="logo" className={styles.logo} src={logo} />
|
||||||
<span className={styles.headtitle}>驿商云</span>
|
<span className={styles.headtitle}>云南交投</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<DefaultFooter
|
{/* <DefaultFooter
|
||||||
copyright={`Copyright © 2013-${new Date().getFullYear()} Eshang Cloud. All Rights Reserved. 驿商云 版权所有 V${VERSION || ''}`}
|
copyright={`Copyright © 2013-${new Date().getFullYear()} Eshang Cloud. All Rights Reserved. 驿商云 版权所有 V${VERSION || ''}`}
|
||||||
// copyright={`Copyright © 2013-${new Date().getFullYear()} Eshang Cloud. All Rights Reserved. 驿商云 版权所有 V6.6.6`}
|
// copyright={`Copyright © 2013-${new Date().getFullYear()} Eshang Cloud. All Rights Reserved. 驿商云 版权所有 V6.6.6`}
|
||||||
links={[]}
|
links={[]}
|
||||||
/>
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
95
src/patchCssHmr.ts
Normal file
95
src/patchCssHmr.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// src/hmrCssFix.ts
|
||||||
|
// 仅开发环境执行
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
const getBase = () => {
|
||||||
|
const base = (window as any).routerBase || '/';
|
||||||
|
return (base as string).endsWith('/') ? base.slice(0, -1) : base;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UMI_CSS_RE = /\/umi\.css(\?|#|$)/;
|
||||||
|
|
||||||
|
const createStylesheetLink = (href: string) => {
|
||||||
|
const l = document.createElement('link');
|
||||||
|
l.rel = 'stylesheet';
|
||||||
|
// 加个 cache-bust,避免 HMR/缓存干扰
|
||||||
|
l.href = href + (href.indexOf('?') > -1 ? '&' : '?') + '_hmrfix_=' + Date.now();
|
||||||
|
l.setAttribute('data-hmrfix', 'stylesheet');
|
||||||
|
return l;
|
||||||
|
};
|
||||||
|
|
||||||
|
const injectImportStyle = (href: string) => {
|
||||||
|
if (document.querySelector('style[data-hmrfix="import"]')) return;
|
||||||
|
const s = document.createElement('style');
|
||||||
|
s.setAttribute('data-hmrfix', 'import');
|
||||||
|
s.appendChild(document.createTextNode(`@import url("${href}");`));
|
||||||
|
document.head.appendChild(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hardFix = () => {
|
||||||
|
const base = getBase();
|
||||||
|
const href = `${base}/umi.css`;
|
||||||
|
|
||||||
|
// 1) 先把 preload as=style 全部替换成真正的 stylesheet(克隆+替换,避免被后续还原)
|
||||||
|
const preloads = Array.from(
|
||||||
|
document.querySelectorAll<HTMLLinkElement>('link[rel="preload"][as="style"]'),
|
||||||
|
);
|
||||||
|
preloads.forEach((pl) => {
|
||||||
|
const isUmi = UMI_CSS_RE.test(pl.href);
|
||||||
|
// 无论是不是 umi.css,都改;Umi 的其他 chunk css 被 preload 也会导致样式不生效
|
||||||
|
const newLink = createStylesheetLink(pl.href);
|
||||||
|
pl.replaceWith(newLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2) 如果 umi.css 对应的 stylesheet 还不存在,就强插一个
|
||||||
|
const hasUmiSheet = Array.from(
|
||||||
|
document.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]'),
|
||||||
|
).some((l) => UMI_CSS_RE.test(l.href));
|
||||||
|
|
||||||
|
if (!hasUmiSheet) {
|
||||||
|
document.head.appendChild(createStylesheetLink(href));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 兜底再兜底:若 50ms 后仍然没有 stylesheet,就用 @import 方式强行生效
|
||||||
|
setTimeout(() => {
|
||||||
|
const stillNoSheet = !Array.from(
|
||||||
|
document.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]'),
|
||||||
|
).some((l) => UMI_CSS_RE.test(l.href));
|
||||||
|
if (stillNoSheet) {
|
||||||
|
injectImportStyle(href);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
try {
|
||||||
|
hardFix();
|
||||||
|
} catch { }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初次与 DOM 就绪
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', run);
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 <head> 变化(HMR 注入/替换节点时立即修正)
|
||||||
|
const mo = new MutationObserver(run);
|
||||||
|
mo.observe(document.head, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
// 监听 HMR 状态(apply/idle 后多次修正,压住竞态)
|
||||||
|
// @ts-ignore
|
||||||
|
if (typeof module !== 'undefined' && module?.hot?.addStatusHandler) {
|
||||||
|
// @ts-ignore
|
||||||
|
module.hot.addStatusHandler((status: string) => {
|
||||||
|
if (status === 'apply' || status === 'idle') {
|
||||||
|
setTimeout(run, 0);
|
||||||
|
setTimeout(run, 50);
|
||||||
|
setTimeout(run, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兜底定时器(极端情况下也能拉回)
|
||||||
|
setInterval(run, 300);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user