This commit is contained in:
ylj20011123 2025-09-12 18:08:21 +08:00
parent eae1bd2c18
commit c5500f6170
12 changed files with 100401 additions and 50 deletions

View File

@ -1,7 +1,10 @@
// 简化版页面过渡动画
.simple-page-transition {
animation: pageEnter 0.5s cubic-bezier(0.4, 0, 0.2, 1);
will-change: opacity, transform;
// 默认不播放动画只有带animate类时才播放
&.animate {
animation: pageEnter 0.5s cubic-bezier(0.4, 0, 0.2, 1);
will-change: opacity, transform;
}
}
@keyframes pageEnter {

View File

@ -61,7 +61,7 @@ export const SimplePageTransition: React.FC<{
return (
<div
className={`${enableAnimation ? 'simple-page-transition' : ''} ${className}`}
className={`simple-page-transition ${enableAnimation ? 'animate' : ''} ${className}`}
onAnimationEnd={handleAnimationEnd}
>
{children}

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

@ -29,6 +29,8 @@ import IconFont from '@/components/IconFont';
import type { CurrentUser } from '@/models/user'
import session from '@/utils/session';
import { SimplePageTransition } from '@/components/PageTransition';
import TabVirtualizer from '@/components/TabVirtualizer';
import { tabPerformanceManager, DEFAULT_CONFIG } from '@/utils/tabPerformanceManager';
import upMenu from '../assets/tab/upMenu.png'
import { getFieldEnum, getFieldEnumTravel, getFieldEnumTree, getFieldGetFieEnumList, getTravelFieldEnumTree, handleGetFieldEnumTreeTravel, handleGetNestingFIELDENUMList } from "@/services/options";
import { handleGetServerpartTree } from '@/pages/basicManage/serverpartAssets/service';
@ -94,7 +96,9 @@ const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
const [activeKey, setActiveKey] = useState<string>(location?.pathname || '/')
const [animatedPages, setAnimatedPages] = useState<Set<string>>(new Set());
const [reloadingTabs, setReloadingTabs] = useState<Set<string>>(new Set());
const menuDataRef = useRef<MenuDataItem[]>([]);
const checkTimerRef = useRef<NodeJS.Timeout | null>(null);
const { formatMessage } = useIntl();
useEffect(() => {
@ -142,20 +146,57 @@ const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
// 改变panes
const handleTabsPanes = (payload: any): void => {
if (dispatch) {
// 检查是否为新页面
// 检查是否为新页面(从未打开过的页面才需要动画)
const isNewPage = !tabsPanes.some(tab => tab.path === payload.path);
if (isNewPage) {
// 标记新页面需要播放动画
setAnimatedPages(prev => new Set(prev).add(payload.path));
}
// 更新访问时间
const updatedPayload = {
...payload,
lastAccessTime: Date.now(),
isLoaded: true,
isLoading: false,
};
dispatch({
type: 'global/changeTabsRoutes',
payload: { data: payload, action: 'add' },
payload: { data: updatedPayload, action: 'add' },
});
}
};
// 处理标签页重新加载
const handleTabReload = (tabPath: string): void => {
if (dispatch) {
// 标记为加载中
setReloadingTabs(prev => new Set(prev).add(tabPath));
// 重新加载时也要播放动画
setAnimatedPages(prev => new Set(prev).add(tabPath));
dispatch({
type: 'global/reloadTab',
payload: { tabPath },
});
// 模拟加载过程
setTimeout(() => {
setReloadingTabs(prev => {
const newSet = new Set(prev);
newSet.delete(tabPath);
return newSet;
});
// 切换到该标签页
history.push(tabPath);
setActiveKey(tabPath);
}, 800); // 800ms的加载时间
}
};
// 关闭当前标签
const handleEdit = (targetKey: string | React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element>, action: "add" | "remove"): void => {
@ -886,6 +927,31 @@ const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
})
}
// 启动标签页性能管理
useEffect(() => {
// 启动定时检查
checkTimerRef.current = setInterval(() => {
if (dispatch && tabsPanes.length > 0) {
const tabsToUnload = tabPerformanceManager.getTabsToUnload(tabsPanes, activeKey);
if (tabsToUnload.length > 0) {
// 批量卸载标签页
tabsToUnload.forEach(tabPath => {
dispatch({
type: 'global/unloadTab',
payload: { tabPath },
});
});
}
}
}, DEFAULT_CONFIG.checkIntervalMinutes * 60 * 1000);
return () => {
if (checkTimerRef.current) {
clearInterval(checkTimerRef.current);
}
};
}, [dispatch, tabsPanes, activeKey]);
// 显示就调用
useEffect(() => {
handleGetAllFieldEnum()
@ -998,6 +1064,14 @@ const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
onChange={(value) => {
history.push(value)
setActiveKey(value)
// 更新标签页访问时间
if (dispatch) {
dispatch({
type: 'global/updateTabAccessTime',
payload: { tabPath: value },
});
}
}}
activeKey={activeKey}
onEdit={handleEdit}
@ -1060,6 +1134,9 @@ const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
>
{tabsPanes && tabsPanes.map((item: tabsRoute) => {
const shouldAnimate = animatedPages.has(item.path);
const isActive = activeKey === item.path;
const isLoaded = item.isLoaded !== false; // 默认为已加载状态
const isLoading = reloadingTabs.has(item.path) || item.isLoading === true;
return (
<TabPane
@ -1067,23 +1144,38 @@ const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
key={item?.path}
style={{ padding: 24, paddingTop: 0 }}
>
<SimplePageTransition
enableAnimation={shouldAnimate}
onAnimationEnd={() => {
// 动画播放完成后,移除动画标记
setAnimatedPages(prev => {
const newSet = new Set(prev);
newSet.delete(item.path);
return newSet;
});
}}
>
<Suspense fallback={null}>
<Authorized authority={authorized!.authority} noMatch={noMatch}>
{item.children}
</Authorized>
</Suspense>
</SimplePageTransition>
{/* 如果页面未加载或正在加载显示TabVirtualizer */}
{(!isLoaded || isLoading) ? (
<TabVirtualizer
tabKey={item.path}
isActive={isActive}
isLoaded={isLoaded}
isLoading={isLoading}
lastAccessTime={item.lastAccessTime}
onReload={handleTabReload}
>
{/* 空内容TabVirtualizer会处理显示 */}
</TabVirtualizer>
) : (
/* 页面已加载,直接显示内容,根据需要播放动画 */
<SimplePageTransition
enableAnimation={shouldAnimate}
onAnimationEnd={() => {
// 动画播放完成后,移除动画标记
setAnimatedPages(prev => {
const newSet = new Set(prev);
newSet.delete(item.path);
return newSet;
});
}}
>
<Suspense fallback={null}>
<Authorized authority={authorized!.authority} noMatch={noMatch}>
{item.children}
</Authorized>
</Suspense>
</SimplePageTransition>
)}
</TabPane>
);
})}

View File

@ -20,6 +20,9 @@ export type tabsRoute = {
path: string;
key: string;
children: any;
lastAccessTime?: number; // 最后访问时间
isLoaded?: boolean; // 是否已加载
isLoading?: boolean; // 是否正在加载
}
export type GlobalModelState = {
@ -39,6 +42,9 @@ export type GlobalModelType = {
changeNoticeReadState: Effect;
changeTabsRoutes: Effect;
getMenuData: Effect;
updateTabAccessTime: Effect;
unloadTab: Effect;
reloadTab: Effect;
};
reducers: {
changeLayoutCollapsed: Reducer<GlobalModelState>;
@ -179,10 +185,26 @@ const GlobalModel: GlobalModelType = {
const index = state.global.tabsRoutes.findIndex(n => n.path === data.path)
if (index === -1) { // 没缓存 则添加
return [...state.global.tabsRoutes, { ...data, index: state.global.tabsRoutes.length }]
return [...state.global.tabsRoutes, {
...data,
index: state.global.tabsRoutes.length,
lastAccessTime: data.lastAccessTime || Date.now(),
isLoaded: data.isLoaded !== false,
isLoading: data.isLoading || false,
}]
}
// 否则不操作
return [...state.global.tabsRoutes]
// 否则更新现有标签页信息
return state.global.tabsRoutes.map(tab =>
tab.path === data.path
? {
...tab,
...data,
lastAccessTime: data.lastAccessTime || Date.now(),
isLoaded: data.isLoaded !== false,
isLoading: data.isLoading || false,
}
: tab
);
}
if (payload.action === 'removeAll') {
return []
@ -199,8 +221,57 @@ const GlobalModel: GlobalModelType = {
type: 'saveTabsRoutes',
payload: tabsRoutes,
});
},
// 更新标签页访问时间
* updateTabAccessTime({ payload }, { put, select }) {
const { tabPath } = payload;
const tabsRoutes: tabsRoute[] = yield select((state: ConnectState) =>
state.global.tabsRoutes.map(tab =>
tab.path === tabPath
? { ...tab, lastAccessTime: Date.now(), isLoaded: true, isLoading: false }
: tab
)
);
yield put({
type: 'saveTabsRoutes',
payload: tabsRoutes,
});
},
// 卸载标签页
* unloadTab({ payload }, { put, select }) {
const { tabPath } = payload;
const tabsRoutes: tabsRoute[] = yield select((state: ConnectState) =>
state.global.tabsRoutes.map(tab =>
tab.path === tabPath
? { ...tab, isLoaded: false, isLoading: false }
: tab
)
);
yield put({
type: 'saveTabsRoutes',
payload: tabsRoutes,
});
},
// 重新加载标签页
* reloadTab({ payload }, { put, select }) {
const { tabPath } = payload;
const tabsRoutes: tabsRoute[] = yield select((state: ConnectState) =>
state.global.tabsRoutes.map(tab =>
tab.path === tabPath
? { ...tab, isLoaded: true, isLoading: false, lastAccessTime: Date.now() }
: tab
)
);
yield put({
type: 'saveTabsRoutes',
payload: tabsRoutes,
});
}
},

View File

@ -249,7 +249,7 @@ const ProjectsScore = ({ dataList, onRef, currentRow, BUSINESSTRADEVALUE, server
console.log('brandList', brandList);
// 常规提成比例
suggestionObj.conventionRate = brandList[0].COMMISSION_RATIO
let [minRate, maxRate] = brandList[0].COMMISSION_RATIO.split('-')
let [minRate, maxRate] = brandList[0] && brandList[0].COMMISSION_RATIO ? brandList[0].COMMISSION_RATIO.split('-') : ['', '']
// let conventionRate: any = brandList[0].COMMISSION_RATIO
// 拿到该业态的提成比例的最大值 最小值 平均值 和 商家净利润率

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
// 景区信息配置
import React, { useRef, useState, Suspense } from 'react';
import React, { useRef, useState, Suspense, useEffect } from 'react';
import moment from 'moment'; // 时间相关引用,没有使用可以删除
import numeral from "numeral"; // 数字相关引用,没有使用可以删除
import { connect } from 'umi';
@ -28,18 +28,22 @@ import { deleteAHYDPicture, deletePicture, uploadAHYDPicture, uploadPicture } fr
import ModalFooter from './component/modalFooter';
import { handleSetlogSave } from '@/utils/format';
import { highlightText } from '@/utils/highlightText';
import { compressImage } from '@/utils/imageCompress';
import allData from './data'
const beforeUpload = (file: any) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
message.error('请上传JPEG、jpg、png格式的图片文件!');
return false;
}
const isLt2M = file.size / 1024 / 1024 < 5;
if (!isLt2M) {
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error('图片大小不超过 5MB!');
return false;
}
return isJpgOrPng && isLt2M;
return true;
}
const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (props) => {
@ -317,15 +321,15 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
const handleAddUpdate = async (res: any) => {
let req: any = {}
let imgList: any = []
if (fileList && fileList.length > 0) {
fileList.forEach((item: any) => {
imgList.push({
ImageName: item.name,
ImageUrl: item.url
})
})
}
// let imgList: any = []
// if (fileList && fileList.length > 0) {
// fileList.forEach((item: any) => {
// imgList.push({
// ImageName: item.name,
// ImageUrl: item.url
// })
// })
// }
if (currentRow?.SCENICAREA_ID) {
req = {
@ -347,7 +351,8 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
OPERATE_DATE: moment().format('YYYY-MM-DD HH:mm:ss'),
}
}
handleConfirmLoading(true)
req.SCENICAREA_Image = ''
console.log('reqreqreqreq', req);
const data = await handeSynchroSCENICAREA(req)
console.log('datadatadatadata', data);
@ -358,7 +363,9 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
const formData = new FormData();
res.SCENICAREA_Image.forEach((file: any) => {
if (!file.ImageUrl) {
formData.append('files[]', file.originFileObj);
// 使用压缩后的文件,如果没有则使用原文件
const fileToUpload = file.originFileObj?.compressedFile || file.originFileObj;
formData.append('files[]', fileToUpload);
formData.append('ImageName', typeof file !== 'string' ? file?.name : '');
}
});
@ -385,6 +392,46 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
}
};
useEffect(() => {
let status0: number = 0
let status1: number = 0
let status2: number = 0
let status3: number = 0
let status255: number = 0
allData.data.forEach((item: any) => {
if (item.equipments && item.equipments.length > 0) {
item.equipments.forEach((subItem: any) => {
if (subItem.connectors && subItem.connectors.length > 0) {
subItem.connectors.forEach((thirdItem: any) => {
if (thirdItem.status === 0) {
status0 += 1
} else if (thirdItem.status === 1) {
status1 += 1
} else if (thirdItem.status === 2) {
status2 += 1
} else if (thirdItem.status === 3) {
status3 += 1
} else if (thirdItem.status === 255) {
status255 += 1
}
})
}
})
}
})
let obj: any = {
status0,
status1,
status2,
status3,
status255
}
console.log('objobjobjobjobj', obj);
}, [])
return (
<div style={{
width: "100%",
@ -503,7 +550,7 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
handleModalVisible(false)
setFileList([])
}}
footer={<ModalFooter hideDelete={!currentRow?.SCENICAREA_ID} handleDelete={async () => {
footer={<ModalFooter confirmLoading={confirmLoading} hideDelete={!currentRow?.SCENICAREA_ID} handleDelete={async () => {
await handelDelete(currentRow?.SCENICAREA_ID);
}} handleCancel={() => {
@ -547,6 +594,7 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
SCENICAREA_STATE: 1000
}}
onFinish={async (values) => {
let newValue = { ...values };
if (currentRow) {
// 编辑数据
@ -695,13 +743,29 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
beforeUpload: beforeUpload,
onPreview: handlePreview,
fileList: fileList, // 绑定 fileList
customRequest: ({ file, onSuccess }) => {
// 自定义上传,不实际发送请求
setTimeout(() => {
if (onSuccess) {
onSuccess({});
}
}, 0);
customRequest: ({ file, onSuccess, onError, onProgress }) => {
// 开始处理
onProgress?.({ percent: 10 });
compressImage(file as File, {
width: 300,
height: 300,
maxSize: 200 // KB
})
.then(compressedFile => {
console.log(`压缩成功: ${file.name}${(file.size / 1024).toFixed(1)}KB 压缩到 ${(compressedFile.size / 1024).toFixed(1)}KB`);
// 保存压缩后的文件到原文件对象上
(file as any).compressedFile = compressedFile;
onProgress?.({ percent: 100 });
onSuccess?.(compressedFile);
})
.catch(err => {
console.error('图片压缩失败:', err);
message.error('图片处理失败,请重试');
onError?.(err);
});
},
onChange: async (info: any) => {
console.log('info', info);

273
src/utils/imageCompress.ts Normal file
View File

@ -0,0 +1,273 @@
/**
*
*
*/
export interface CompressOptions {
width?: number;
height?: number;
quality?: number;
maxSize?: number; // KB
}
export const compressImage = (
file: File,
options: CompressOptions = {}
): Promise<File> => {
const {
quality = 0.8
} = options;
return new Promise((resolve, reject) => {
if (!file || !file.type.includes('image')) {
reject(new Error('无效的图片文件'));
return;
}
const fileSizeKB = file.size / 1024;
const targetSizeKB = fileSizeKB / 2;
console.log(`原文件: ${fileSizeKB.toFixed(1)}KB, 目标: ${targetSizeKB.toFixed(1)}KB`);
// 对于小文件,直接尝试转换格式压缩
if (fileSizeKB < 300 && file.type === 'image/png') {
console.log('PNG小文件尝试转换为JPEG格式压缩');
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
compressSmallPngToJpeg(img, file, targetSizeKB, resolve, reject);
};
img.onerror = () => reject(new Error('图片加载失败'));
img.src = e.target?.result as string;
};
reader.onerror = () => reject(new Error('文件读取失败'));
reader.readAsDataURL(file);
return;
}
// 正常压缩流程
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
compressToHalfSize(img, file, quality, resolve, reject);
};
img.onerror = () => reject(new Error('图片加载失败'));
img.src = e.target?.result as string;
};
reader.onerror = () => reject(new Error('文件读取失败'));
reader.readAsDataURL(file);
});
};
// PNG小文件转JPEG压缩
function compressSmallPngToJpeg(
img: HTMLImageElement,
originalFile: File,
targetSizeKB: number,
resolve: (file: File) => void,
reject: (error: Error) => void
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('浏览器不支持Canvas'));
return;
}
console.log(`PNG转JPEG: ${img.width}x${img.height}, 目标: ${targetSizeKB.toFixed(1)}KB`);
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, img.width, img.height);
// 尝试转换为JPEG格式并压缩
const compressAsJpeg = (quality: number): void => {
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error('JPEG转换失败'));
return;
}
const sizeKB = blob.size / 1024;
console.log(`JPEG质量: ${quality.toFixed(2)}, 大小: ${sizeKB.toFixed(1)}KB`);
if (sizeKB <= targetSizeKB || quality <= 0.1) {
// 创建新的JPEG文件
const jpegFileName = originalFile.name.replace(/\.png$/i, '.jpg');
const compressedFile = new File([blob], jpegFileName, {
type: 'image/jpeg',
lastModified: Date.now()
});
const originalSizeKB = originalFile.size / 1024;
console.log(`✅ PNG→JPEG压缩完成: ${originalSizeKB.toFixed(1)}KB → ${sizeKB.toFixed(1)}KB`);
resolve(compressedFile);
} else {
const nextQuality = Math.max(0.1, quality - 0.1);
compressAsJpeg(nextQuality);
}
},
'image/jpeg', // 强制转换为JPEG
quality
);
};
compressAsJpeg(0.7); // 从0.7质量开始
}
// 图片处理函数 - 压缩到原文件一半大小
function compressToHalfSize(
img: HTMLImageElement,
originalFile: File,
initialQuality: number,
resolve: (file: File) => void,
reject: (error: Error) => void
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('浏览器不支持Canvas'));
return;
}
const originalSizeKB = originalFile.size / 1024;
const targetSizeKB = originalSizeKB / 2;
console.log(`原图: ${img.width}x${img.height}, ${originalSizeKB.toFixed(1)}KB`);
console.log(`目标: 压缩到 ${targetSizeKB.toFixed(1)}KB (原大小的一半)`);
// 对于小文件(< 200KB),直接用低质量压缩,不测试
if (originalSizeKB < 200) {
console.log(`小文件直接压缩模式`);
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, img.width, img.height);
// 直接从低质量开始压缩
const compressDirectly = (quality: number): void => {
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error('图片处理失败'));
return;
}
const sizeKB = blob.size / 1024;
console.log(`直接压缩 质量: ${quality.toFixed(2)}, 大小: ${sizeKB.toFixed(1)}KB, 目标: ${targetSizeKB.toFixed(1)}KB`);
if (sizeKB <= targetSizeKB || quality <= 0.1) {
const compressedFile = new File([blob], originalFile.name, {
type: originalFile.type,
lastModified: Date.now()
});
const compressionRatio = (sizeKB / originalSizeKB * 100).toFixed(1);
console.log(`✅ 小文件压缩完成: ${originalSizeKB.toFixed(1)}KB → ${sizeKB.toFixed(1)}KB (${compressionRatio}%)`);
resolve(compressedFile);
} else {
const nextQuality = Math.max(0.1, quality - 0.1);
compressDirectly(nextQuality);
}
}, originalFile.type, quality);
};
compressDirectly(0.5); // 从0.5开始
return;
}
// 大文件才做测试
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, img.width, img.height);
// 先用初始质量测试一下
canvas.toBlob((testBlob) => {
if (!testBlob) {
reject(new Error('图片处理失败'));
return;
}
const testSizeKB = testBlob.size / 1024;
console.log(`=== 详细分析 ===`);
console.log(`原文件: ${originalSizeKB.toFixed(1)}KB`);
console.log(`目标大小: ${targetSizeKB.toFixed(1)}KB (原大小的一半)`);
console.log(`Canvas处理测试: ${testSizeKB.toFixed(1)}KB`);
console.log(`变化: ${testSizeKB > originalSizeKB ? '+' : ''}${(testSizeKB - originalSizeKB).toFixed(1)}KB`);
console.log(`是否需要继续压缩: ${testSizeKB > targetSizeKB ? '是' : '否'}`);
// 如果初始处理后比原文件大很多(超过10%),直接返回原文件
if (testSizeKB > originalSizeKB * 1.1) {
console.log(`⚠️ Canvas处理后变大超过10%,直接返回原文件`);
resolve(originalFile);
return;
}
// 如果Canvas处理后已经达到或接近目标大小直接返回
if (testSizeKB <= targetSizeKB) {
console.log(`✅ Canvas处理后已经达到目标大小直接返回`);
const compressedFile = new File([testBlob], originalFile.name, {
type: originalFile.type,
lastModified: Date.now()
});
resolve(compressedFile);
return;
}
// 开始压缩循环 - 只降低质量,不提高
const compress = (currentQuality: number): void => {
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error('图片处理失败'));
return;
}
const sizeKB = blob.size / 1024;
console.log(`质量: ${currentQuality.toFixed(2)}, 大小: ${sizeKB.toFixed(1)}KB, 目标: ${targetSizeKB.toFixed(1)}KB`);
// 如果达到目标大小,立即返回
if (sizeKB <= targetSizeKB) {
const compressedFile = new File([blob], originalFile.name, {
type: originalFile.type,
lastModified: Date.now()
});
const compressionRatio = (sizeKB / originalSizeKB * 100).toFixed(1);
console.log(`✅ 压缩完成!`);
console.log(`大小: ${originalSizeKB.toFixed(1)}KB → ${sizeKB.toFixed(1)}KB (${compressionRatio}%)`);
resolve(compressedFile);
} else if (currentQuality <= 0.1) {
// 质量已经很低但还没达到目标,返回当前结果
const compressedFile = new File([blob], originalFile.name, {
type: originalFile.type,
lastModified: Date.now()
});
const compressionRatio = (sizeKB / originalSizeKB * 100).toFixed(1);
console.log(`⚠️ 已达到最低质量,无法继续压缩`);
console.log(`大小: ${originalSizeKB.toFixed(1)}KB → ${sizeKB.toFixed(1)}KB (${compressionRatio}%)`);
resolve(compressedFile);
} else {
// 文件还是太大,继续降低质量
const nextQuality = Math.max(0.05, currentQuality - 0.1);
console.log(`文件还是太大(${sizeKB.toFixed(1)}KB > ${targetSizeKB.toFixed(1)}KB),降低质量到 ${nextQuality.toFixed(2)}`);
compress(nextQuality);
}
},
originalFile.type,
currentQuality
);
};
// 从初始质量开始压缩
compress(initialQuality);
}, originalFile.type, initialQuality);
}

View File

@ -32,7 +32,6 @@ const codeMessage: Record<number, string> = {
const errorHandler = (error: { response: Response }): Response => {
const { response } = error;
if (response && response.status) {
const errorText = codeMessage[response.status] || response.statusText;
const { status, url } = response;

View File

@ -0,0 +1,180 @@
import type { tabsRoute } from '@/models/global';
// 标签页性能配置
export interface TabPerformanceConfig {
unloadAfterMinutes: number; // 多少分钟后卸载默认10分钟
checkIntervalMinutes: number; // 检查间隔默认2分钟
enableAutoUnload: boolean; // 是否启用自动卸载默认true
excludePaths: string[]; // 排除路径,这些路径永不卸载
}
// 默认配置
export const DEFAULT_CONFIG: TabPerformanceConfig = {
unloadAfterMinutes: 10,
checkIntervalMinutes: 2,
enableAutoUnload: true,
excludePaths: ['/dashboard', '/'], // 仪表板页面永不卸载
};
class TabPerformanceManager {
private config: TabPerformanceConfig = DEFAULT_CONFIG;
private checkTimer: NodeJS.Timeout | null = null;
private onUnloadCallback: ((tabPath: string) => void) | null = null;
constructor(config?: Partial<TabPerformanceConfig>) {
this.updateConfig(config);
}
// 更新配置
updateConfig(newConfig?: Partial<TabPerformanceConfig>) {
this.config = { ...this.config, ...newConfig };
this.restartTimer();
}
// 设置卸载回调
setUnloadCallback(callback: (tabPath: string) => void) {
this.onUnloadCallback = callback;
}
// 启动定时检查
start() {
if (!this.config.enableAutoUnload) return;
this.checkTimer = setInterval(() => {
this.checkAndUnloadTabs();
}, this.config.checkIntervalMinutes * 60 * 1000);
}
// 停止定时检查
stop() {
if (this.checkTimer) {
clearInterval(this.checkTimer);
this.checkTimer = null;
}
}
// 重启定时器
private restartTimer() {
this.stop();
this.start();
}
// 检查并卸载标签页
private checkAndUnloadTabs() {
if (!this.onUnloadCallback) return;
const now = Date.now();
const unloadThreshold = this.config.unloadAfterMinutes * 60 * 1000;
// 这里需要从外部获取标签页列表,暂时留空
// 实际使用时会通过回调函数获取当前标签页状态
}
// 检查单个标签页是否应该卸载
shouldUnloadTab(tab: tabsRoute, currentActiveKey: string): boolean {
// 当前激活的标签页不卸载
if (tab.path === currentActiveKey) return false;
// 排除路径不卸载
if (this.config.excludePaths.includes(tab.path)) return false;
// 没有访问时间记录的不卸载(可能是新标签)
if (!tab.lastAccessTime) return false;
// 已经卸载的不需要再次卸载
if (tab.isLoaded === false) return false;
// 检查时间阈值
const now = Date.now();
const unloadThreshold = this.config.unloadAfterMinutes * 60 * 1000;
return (now - tab.lastAccessTime) > unloadThreshold;
}
// 获取需要卸载的标签页路径列表
getTabsToUnload(tabs: tabsRoute[], currentActiveKey: string): string[] {
if (!this.config.enableAutoUnload) return [];
return tabs
.filter(tab => this.shouldUnloadTab(tab, currentActiveKey))
.map(tab => tab.path);
}
// 更新标签页访问时间
updateTabAccessTime(tabs: tabsRoute[], tabPath: string): tabsRoute[] {
return tabs.map(tab => {
if (tab.path === tabPath) {
return {
...tab,
lastAccessTime: Date.now(),
isLoaded: true,
isLoading: false,
};
}
return tab;
});
}
// 标记标签页为卸载状态
markTabUnloaded(tabs: tabsRoute[], tabPath: string): tabsRoute[] {
return tabs.map(tab => {
if (tab.path === tabPath) {
return {
...tab,
isLoaded: false,
isLoading: false,
};
}
return tab;
});
}
// 标记标签页为加载中状态
markTabLoading(tabs: tabsRoute[], tabPath: string): tabsRoute[] {
return tabs.map(tab => {
if (tab.path === tabPath) {
return {
...tab,
isLoaded: false,
isLoading: true,
};
}
return tab;
});
}
// 标记标签页为已加载状态
markTabLoaded(tabs: tabsRoute[], tabPath: string): tabsRoute[] {
return tabs.map(tab => {
if (tab.path === tabPath) {
return {
...tab,
isLoaded: true,
isLoading: false,
lastAccessTime: Date.now(),
};
}
return tab;
});
}
// 获取内存使用情况统计
getMemoryStats(tabs: tabsRoute[]): {
totalTabs: number;
loadedTabs: number;
unloadedTabs: number;
loadingTabs: number;
} {
return {
totalTabs: tabs.length,
loadedTabs: tabs.filter(tab => tab.isLoaded === true).length,
unloadedTabs: tabs.filter(tab => tab.isLoaded === false && !tab.isLoading).length,
loadingTabs: tabs.filter(tab => tab.isLoading === true).length,
};
}
}
// 创建单例实例
export const tabPerformanceManager = new TabPerformanceManager();
export default TabPerformanceManager;