先推一版热重载问题修复 且带上了骨架屏效果的一版

This commit is contained in:
ylj20011123 2025-09-17 18:05:21 +08:00
parent 14b175f28c
commit 8f8b34763f
18 changed files with 944 additions and 44 deletions

View File

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

View File

@ -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"
] ]
} }

View File

@ -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注入子应用

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

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

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

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

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

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

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

View File

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

View File

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

View File

@ -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;
// } // }
} }

View File

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