diff --git a/config/config.ts b/config/config.ts index 0e795a2..71593f5 100644 --- a/config/config.ts +++ b/config/config.ts @@ -15,6 +15,7 @@ import proxy from './proxy'; import routes from './routes'; const { REACT_APP_ENV } = process.env; +const isDev = process.env.NODE_ENV === 'development'; export default defineConfig({ hash: true, @@ -36,7 +37,7 @@ export default defineConfig({ baseNavigator: true, }, dynamicImport: { - loading: '@/components/PageLoading/index', + loading: '@/components/SmartLoading/index', }, targets: { ie: 11, @@ -45,13 +46,14 @@ export default defineConfig({ routes, // Theme for antd: https://ant.design/docs/react/customize-theme-cn theme: { + // 'primary-color': defaultSettings.primaryColor, 'primary-color': defaultSettings.primaryColor, }, title: false, ignoreMomentLocale: true, proxy: proxy[REACT_APP_ENV || 'dev'], base: '/cloud', - publicPath: '/cloud/', + publicPath: process.env.NODE_ENV === 'development' ? '/' : '/cloud/', // 快速刷新功能 https://umijs.org/config#fastrefresh fastRefresh: {}, esbuild: {}, @@ -59,5 +61,5 @@ export default defineConfig({ master: {} }, webpack5: { - }, + } }); diff --git a/package.json b/package.json index cfc56f4..47bc806 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@umijs/route-utils": "^1.0.33", "ahooks": "^2.10.3", "antd": "^4.17.0", - "aws-sdk": "^2.1692.0", + "aws-sdk": "2.1692.0", "classnames": "^2.2.6", "compression-webpack-plugin": "^11.1.0", "crypto-js": "^4.1.1", @@ -142,4 +142,4 @@ "config/**/*.js*", "scripts/**/*.js" ] -} \ No newline at end of file +} diff --git a/src/app.ts b/src/app.ts index 96a4c39..2d0c10f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,8 +6,9 @@ * @FilePath: \cloud-platform\src\app.ts * @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 { getMicroAppRouteComponent } from 'umi'; // 将全局state注入子应用 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/PageTransition/index.less b/src/components/PageTransition/index.less new file mode 100644 index 0000000..165733f --- /dev/null +++ b/src/components/PageTransition/index.less @@ -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); + } +} \ No newline at end of file diff --git a/src/components/PageTransition/index.tsx b/src/components/PageTransition/index.tsx new file mode 100644 index 0000000..a8c684f --- /dev/null +++ b/src/components/PageTransition/index.tsx @@ -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 = ({ + 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 ( + + { + if (transitionStage === 'fadeOut') { + setDisplayLocation(location); + setTransitionStage('fadeIn'); + } + }} + > + {children} + + + ); +}; + +// 简化版本 - 仅使用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 ( + + {children} + + ); + }; + +export default PageTransition; \ 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..d5756bf --- /dev/null +++ b/src/components/SmartLoading/index.tsx @@ -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 = ({ + fallback, + enablePreload = true +}) => { + const [loadingType, setLoadingType] = useState<'page' | 'table' | 'form' | 'card'>('page'); + const [shouldShow, setShouldShow] = useState(false); + const [location, setLocation] = useState(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 ; +}; + +/** + * 根据加载类型获取合适的行数 + */ +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/components/TabVirtualizer/index.less b/src/components/TabVirtualizer/index.less new file mode 100644 index 0000000..7f8785c --- /dev/null +++ b/src/components/TabVirtualizer/index.less @@ -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); + } +} \ No newline at end of file diff --git a/src/components/TabVirtualizer/index.tsx b/src/components/TabVirtualizer/index.tsx new file mode 100644 index 0000000..971da65 --- /dev/null +++ b/src/components/TabVirtualizer/index.tsx @@ -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 = ({ + children, + tabKey, + isActive, + isLoaded, + isLoading, + lastAccessTime, + onReload, + className = '', +}) => { + const containerRef = useRef(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 ( + + + + ); + } + + // 如果未加载(被卸载),显示占位符 + if (!isLoaded) { + return ( + + + + + + + 页面已休眠 + + 为了优化内存使用,此页面内容已被暂时卸载 + + {lastAccessTime && ( + + {formatLastAccessTime(lastAccessTime)} + + )} + } + onClick={() => onReload(tabKey)} + className="reload-btn" + > + 重新加载 + + + + + ); + } + + // 正常渲染内容 + return ( + + {children} + + ); +}; + +export default TabVirtualizer; \ No newline at end of file diff --git a/src/global.less b/src/global.less index 0b57e94..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; @@ -55,4 +100,18 @@ ol { body .ant-design-pro>.ant-layout { min-height: 100vh; } +} + +// 页面过渡优化 +* { + box-sizing: border-box; +} + +// 增强页面切换体验 +.ant-tabs-content-holder { + overflow: hidden; +} + +.ant-tabs-tabpane { + outline: none; } \ No newline at end of file diff --git a/src/layouts/BasicLayout.less b/src/layouts/BasicLayout.less index 8383d42..d9491bd 100644 --- a/src/layouts/BasicLayout.less +++ b/src/layouts/BasicLayout.less @@ -1,4 +1,5 @@ @import '~antd/es/style/themes/default.less'; +@import '../components/PageTransition/index.less'; .ant-pro-global-header { box-shadow: none !important; diff --git a/src/layouts/SecurityLayout.tsx b/src/layouts/SecurityLayout.tsx index e208fb0..89cd62e 100644 --- a/src/layouts/SecurityLayout.tsx +++ b/src/layouts/SecurityLayout.tsx @@ -15,68 +15,101 @@ type SecurityLayoutProps = { type SecurityLayoutState = { isReady: boolean; + shouldShowLoading: boolean; + initialLoadComplete: boolean; }; class SecurityLayout extends React.Component { state: SecurityLayoutState = { isReady: false, - + shouldShowLoading: false, + initialLoadComplete: false, }; + private loadingTimer?: NodeJS.Timeout; + componentDidMount() { - - const { location } = history - + const { location } = history; 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({ - type: 'user/fetch', callback: (user) => { + type: 'user/fetch', + callback: (user) => { + // 清除定时器 + if (this.loadingTimer) { + clearTimeout(this.loadingTimer); + } if (user.code && location.pathname !== '/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({ isReady: true, + shouldShowLoading: false, + initialLoadComplete: true, }); } } - }) - + }); } - }) - + }); } else { + // 清除定时器 + if (this.loadingTimer) { + clearTimeout(this.loadingTimer); + } + this.setState({ isReady: true, + shouldShowLoading: false, + initialLoadComplete: true, }); } + } + componentWillUnmount() { + if (this.loadingTimer) { + clearTimeout(this.loadingTimer); + } } render() { - const { isReady } = this.state; - const { children, loading, currentUser } = this.props; - // const { location } = history; + const { isReady, shouldShowLoading, initialLoadComplete } = this.state; + const { children, currentUser } = this.props; - - // 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; - - if ((!isLogin && loading) || !isReady) { + // 如果初始化未完成且需要显示加载状态,才显示骨架屏 + if (!isReady && shouldShowLoading) { return ; } - // if (!isLogin && location.pathname !== '/user/login') { - // history.push('/user/login'); - // } + + // 如果还在初始化过程中,但不需要显示loading,返回null(避免闪烁) + if (!isReady && !shouldShowLoading) { + return null; + } + return children; } } diff --git a/src/layouts/UserLayout.less b/src/layouts/UserLayout.less index cdef461..49495bd 100644 --- a/src/layouts/UserLayout.less +++ b/src/layouts/UserLayout.less @@ -12,14 +12,16 @@ width: 100%; height: 64px; line-height: 64px; - display:flex; + display: flex; align-items: center; 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) { margin-right: 24px; } } + .headtitle { color: #191919; font-weight: 700; @@ -53,5 +55,5 @@ // .content { // padding: 32px 0 24px; // } - -} + +} \ No newline at end of file diff --git a/src/layouts/UserLayout.tsx b/src/layouts/UserLayout.tsx index 7ce4964..4aa5975 100644 --- a/src/layouts/UserLayout.tsx +++ b/src/layouts/UserLayout.tsx @@ -56,18 +56,18 @@ const UserLayout: React.FC = (props) => { - 驿商云 + 云南交投 - {children} + {children} - + /> */} ); diff --git a/src/patchCssHmr.ts b/src/patchCssHmr.ts new file mode 100644 index 0000000..a59d800 --- /dev/null +++ b/src/patchCssHmr.ts @@ -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('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('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('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(); + } + + // 监听 变化(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); +}
+ 为了优化内存使用,此页面内容已被暂时卸载 +
+ {formatLastAccessTime(lastAccessTime)} +