恢复到没有加载效果的 除了效果别的还都是新的
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