update
This commit is contained in:
parent
eae1bd2c18
commit
c5500f6170
@ -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 {
|
||||
|
||||
@ -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}
|
||||
|
||||
79
src/components/TabVirtualizer/index.less
Normal file
79
src/components/TabVirtualizer/index.less
Normal 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);
|
||||
}
|
||||
}
|
||||
96
src/components/TabVirtualizer/index.tsx
Normal file
96
src/components/TabVirtualizer/index.tsx
Normal 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;
|
||||
@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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
|
||||
// 拿到该业态的提成比例的最大值 最小值 平均值 和 商家净利润率
|
||||
|
||||
99494
src/pages/travelMember/scenicSpotConfig/data.js
Normal file
99494
src/pages/travelMember/scenicSpotConfig/data.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
273
src/utils/imageCompress.ts
Normal 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);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
180
src/utils/tabPerformanceManager.ts
Normal file
180
src/utils/tabPerformanceManager.ts
Normal 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;
|
||||
Loading…
x
Reference in New Issue
Block a user