恢复到没有加载效果的 除了效果别的还都是新的
This commit is contained in:
parent
b07d192d20
commit
14b175f28c
@ -1,67 +0,0 @@
|
||||
// 简化版页面过渡动画
|
||||
.simple-page-transition {
|
||||
// 默认不播放动画,只有带animate类时才播放
|
||||
&.animate {
|
||||
animation: pageEnter 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
// 移除will-change避免HMR时样式冲突
|
||||
// will-change: opacity, transform;
|
||||
}
|
||||
|
||||
// 动画结束后重置will-change,避免持续占用GPU资源
|
||||
&.animate:not(:hover):not(:focus) {
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pageEnter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.96);
|
||||
// filter: blur(4px); // 注释掉filter属性,可能导致HMR问题
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
// filter: blur(0); // 注释掉filter属性
|
||||
}
|
||||
}
|
||||
|
||||
// 复杂版页面过渡动画
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
// 简化版本 - 禁用所有动画
|
||||
export const SimplePageTransition: React.FC<{
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
enableAnimation?: boolean;
|
||||
onAnimationEnd?: () => void;
|
||||
}> = ({
|
||||
children,
|
||||
className = '',
|
||||
enableAnimation = false, // 强制禁用动画
|
||||
onAnimationEnd,
|
||||
}) => {
|
||||
// 立即执行回调,不等待动画
|
||||
const handleAnimationEnd = () => {
|
||||
if (onAnimationEnd) {
|
||||
onAnimationEnd();
|
||||
}
|
||||
};
|
||||
|
||||
// 立即执行动画结束回调
|
||||
React.useEffect(() => {
|
||||
if (onAnimationEnd) {
|
||||
onAnimationEnd();
|
||||
}
|
||||
}, [onAnimationEnd]);
|
||||
|
||||
return (
|
||||
<div className={`simple-page-transition ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageTransition;
|
||||
@ -1,163 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
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;
|
||||
@ -1,107 +0,0 @@
|
||||
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;
|
||||
@ -1,79 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
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;
|
||||
Loading…
x
Reference in New Issue
Block a user