update
This commit is contained in:
parent
eae1bd2c18
commit
c5500f6170
@ -1,7 +1,10 @@
|
|||||||
// 简化版页面过渡动画
|
// 简化版页面过渡动画
|
||||||
.simple-page-transition {
|
.simple-page-transition {
|
||||||
animation: pageEnter 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
// 默认不播放动画,只有带animate类时才播放
|
||||||
will-change: opacity, transform;
|
&.animate {
|
||||||
|
animation: pageEnter 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
will-change: opacity, transform;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pageEnter {
|
@keyframes pageEnter {
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export const SimplePageTransition: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${enableAnimation ? 'simple-page-transition' : ''} ${className}`}
|
className={`simple-page-transition ${enableAnimation ? 'animate' : ''} ${className}`}
|
||||||
onAnimationEnd={handleAnimationEnd}
|
onAnimationEnd={handleAnimationEnd}
|
||||||
>
|
>
|
||||||
{children}
|
{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 type { CurrentUser } from '@/models/user'
|
||||||
import session from '@/utils/session';
|
import session from '@/utils/session';
|
||||||
import { SimplePageTransition } from '@/components/PageTransition';
|
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 upMenu from '../assets/tab/upMenu.png'
|
||||||
import { getFieldEnum, getFieldEnumTravel, getFieldEnumTree, getFieldGetFieEnumList, getTravelFieldEnumTree, handleGetFieldEnumTreeTravel, handleGetNestingFIELDENUMList } from "@/services/options";
|
import { getFieldEnum, getFieldEnumTravel, getFieldEnumTree, getFieldGetFieEnumList, getTravelFieldEnumTree, handleGetFieldEnumTreeTravel, handleGetNestingFIELDENUMList } from "@/services/options";
|
||||||
import { handleGetServerpartTree } from '@/pages/basicManage/serverpartAssets/service';
|
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 [activeKey, setActiveKey] = useState<string>(location?.pathname || '/')
|
||||||
const [animatedPages, setAnimatedPages] = useState<Set<string>>(new Set());
|
const [animatedPages, setAnimatedPages] = useState<Set<string>>(new Set());
|
||||||
|
const [reloadingTabs, setReloadingTabs] = useState<Set<string>>(new Set());
|
||||||
const menuDataRef = useRef<MenuDataItem[]>([]);
|
const menuDataRef = useRef<MenuDataItem[]>([]);
|
||||||
|
const checkTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -142,20 +146,57 @@ const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
|
|||||||
// 改变panes
|
// 改变panes
|
||||||
const handleTabsPanes = (payload: any): void => {
|
const handleTabsPanes = (payload: any): void => {
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
// 检查是否为新页面
|
// 检查是否为新页面(从未打开过的页面才需要动画)
|
||||||
const isNewPage = !tabsPanes.some(tab => tab.path === payload.path);
|
const isNewPage = !tabsPanes.some(tab => tab.path === payload.path);
|
||||||
if (isNewPage) {
|
if (isNewPage) {
|
||||||
// 标记新页面需要播放动画
|
// 标记新页面需要播放动画
|
||||||
setAnimatedPages(prev => new Set(prev).add(payload.path));
|
setAnimatedPages(prev => new Set(prev).add(payload.path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新访问时间
|
||||||
|
const updatedPayload = {
|
||||||
|
...payload,
|
||||||
|
lastAccessTime: Date.now(),
|
||||||
|
isLoaded: true,
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'global/changeTabsRoutes',
|
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 => {
|
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(() => {
|
useEffect(() => {
|
||||||
handleGetAllFieldEnum()
|
handleGetAllFieldEnum()
|
||||||
@ -998,6 +1064,14 @@ const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
|
|||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
history.push(value)
|
history.push(value)
|
||||||
setActiveKey(value)
|
setActiveKey(value)
|
||||||
|
|
||||||
|
// 更新标签页访问时间
|
||||||
|
if (dispatch) {
|
||||||
|
dispatch({
|
||||||
|
type: 'global/updateTabAccessTime',
|
||||||
|
payload: { tabPath: value },
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
@ -1060,6 +1134,9 @@ const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
|
|||||||
>
|
>
|
||||||
{tabsPanes && tabsPanes.map((item: tabsRoute) => {
|
{tabsPanes && tabsPanes.map((item: tabsRoute) => {
|
||||||
const shouldAnimate = animatedPages.has(item.path);
|
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 (
|
return (
|
||||||
<TabPane
|
<TabPane
|
||||||
@ -1067,23 +1144,38 @@ const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
|
|||||||
key={item?.path}
|
key={item?.path}
|
||||||
style={{ padding: 24, paddingTop: 0 }}
|
style={{ padding: 24, paddingTop: 0 }}
|
||||||
>
|
>
|
||||||
<SimplePageTransition
|
{/* 如果页面未加载或正在加载,显示TabVirtualizer */}
|
||||||
enableAnimation={shouldAnimate}
|
{(!isLoaded || isLoading) ? (
|
||||||
onAnimationEnd={() => {
|
<TabVirtualizer
|
||||||
// 动画播放完成后,移除动画标记
|
tabKey={item.path}
|
||||||
setAnimatedPages(prev => {
|
isActive={isActive}
|
||||||
const newSet = new Set(prev);
|
isLoaded={isLoaded}
|
||||||
newSet.delete(item.path);
|
isLoading={isLoading}
|
||||||
return newSet;
|
lastAccessTime={item.lastAccessTime}
|
||||||
});
|
onReload={handleTabReload}
|
||||||
}}
|
>
|
||||||
>
|
{/* 空内容,TabVirtualizer会处理显示 */}
|
||||||
<Suspense fallback={null}>
|
</TabVirtualizer>
|
||||||
<Authorized authority={authorized!.authority} noMatch={noMatch}>
|
) : (
|
||||||
{item.children}
|
/* 页面已加载,直接显示内容,根据需要播放动画 */
|
||||||
</Authorized>
|
<SimplePageTransition
|
||||||
</Suspense>
|
enableAnimation={shouldAnimate}
|
||||||
</SimplePageTransition>
|
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>
|
</TabPane>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -20,6 +20,9 @@ export type tabsRoute = {
|
|||||||
path: string;
|
path: string;
|
||||||
key: string;
|
key: string;
|
||||||
children: any;
|
children: any;
|
||||||
|
lastAccessTime?: number; // 最后访问时间
|
||||||
|
isLoaded?: boolean; // 是否已加载
|
||||||
|
isLoading?: boolean; // 是否正在加载
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GlobalModelState = {
|
export type GlobalModelState = {
|
||||||
@ -39,6 +42,9 @@ export type GlobalModelType = {
|
|||||||
changeNoticeReadState: Effect;
|
changeNoticeReadState: Effect;
|
||||||
changeTabsRoutes: Effect;
|
changeTabsRoutes: Effect;
|
||||||
getMenuData: Effect;
|
getMenuData: Effect;
|
||||||
|
updateTabAccessTime: Effect;
|
||||||
|
unloadTab: Effect;
|
||||||
|
reloadTab: Effect;
|
||||||
};
|
};
|
||||||
reducers: {
|
reducers: {
|
||||||
changeLayoutCollapsed: Reducer<GlobalModelState>;
|
changeLayoutCollapsed: Reducer<GlobalModelState>;
|
||||||
@ -179,10 +185,26 @@ const GlobalModel: GlobalModelType = {
|
|||||||
const index = state.global.tabsRoutes.findIndex(n => n.path === data.path)
|
const index = state.global.tabsRoutes.findIndex(n => n.path === data.path)
|
||||||
|
|
||||||
if (index === -1) { // 没缓存 则添加
|
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') {
|
if (payload.action === 'removeAll') {
|
||||||
return []
|
return []
|
||||||
@ -199,8 +221,57 @@ const GlobalModel: GlobalModelType = {
|
|||||||
type: 'saveTabsRoutes',
|
type: 'saveTabsRoutes',
|
||||||
payload: tabsRoutes,
|
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);
|
console.log('brandList', brandList);
|
||||||
// 常规提成比例
|
// 常规提成比例
|
||||||
suggestionObj.conventionRate = brandList[0].COMMISSION_RATIO
|
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
|
// 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 moment from 'moment'; // 时间相关引用,没有使用可以删除
|
||||||
import numeral from "numeral"; // 数字相关引用,没有使用可以删除
|
import numeral from "numeral"; // 数字相关引用,没有使用可以删除
|
||||||
import { connect } from 'umi';
|
import { connect } from 'umi';
|
||||||
@ -28,18 +28,22 @@ import { deleteAHYDPicture, deletePicture, uploadAHYDPicture, uploadPicture } fr
|
|||||||
import ModalFooter from './component/modalFooter';
|
import ModalFooter from './component/modalFooter';
|
||||||
import { handleSetlogSave } from '@/utils/format';
|
import { handleSetlogSave } from '@/utils/format';
|
||||||
import { highlightText } from '@/utils/highlightText';
|
import { highlightText } from '@/utils/highlightText';
|
||||||
|
import { compressImage } from '@/utils/imageCompress';
|
||||||
|
import allData from './data'
|
||||||
|
|
||||||
|
|
||||||
const beforeUpload = (file: any) => {
|
const beforeUpload = (file: any) => {
|
||||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
|
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
|
||||||
if (!isJpgOrPng) {
|
if (!isJpgOrPng) {
|
||||||
message.error('请上传JPEG、jpg、png格式的图片文件!');
|
message.error('请上传JPEG、jpg、png格式的图片文件!');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
const isLt2M = file.size / 1024 / 1024 < 5;
|
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||||
if (!isLt2M) {
|
if (!isLt5M) {
|
||||||
message.error('图片大小不超过 5MB!');
|
message.error('图片大小不超过 5MB!');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return isJpgOrPng && isLt2M;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (props) => {
|
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) => {
|
const handleAddUpdate = async (res: any) => {
|
||||||
|
|
||||||
let req: any = {}
|
let req: any = {}
|
||||||
let imgList: any = []
|
// let imgList: any = []
|
||||||
if (fileList && fileList.length > 0) {
|
// if (fileList && fileList.length > 0) {
|
||||||
fileList.forEach((item: any) => {
|
// fileList.forEach((item: any) => {
|
||||||
imgList.push({
|
// imgList.push({
|
||||||
ImageName: item.name,
|
// ImageName: item.name,
|
||||||
ImageUrl: item.url
|
// ImageUrl: item.url
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (currentRow?.SCENICAREA_ID) {
|
if (currentRow?.SCENICAREA_ID) {
|
||||||
req = {
|
req = {
|
||||||
@ -347,7 +351,8 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
|
|||||||
OPERATE_DATE: moment().format('YYYY-MM-DD HH:mm:ss'),
|
OPERATE_DATE: moment().format('YYYY-MM-DD HH:mm:ss'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleConfirmLoading(true)
|
req.SCENICAREA_Image = ''
|
||||||
|
console.log('reqreqreqreq', req);
|
||||||
|
|
||||||
const data = await handeSynchroSCENICAREA(req)
|
const data = await handeSynchroSCENICAREA(req)
|
||||||
console.log('datadatadatadata', data);
|
console.log('datadatadatadata', data);
|
||||||
@ -358,7 +363,9 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
res.SCENICAREA_Image.forEach((file: any) => {
|
res.SCENICAREA_Image.forEach((file: any) => {
|
||||||
if (!file.ImageUrl) {
|
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 : '');
|
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 (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -503,7 +550,7 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
|
|||||||
handleModalVisible(false)
|
handleModalVisible(false)
|
||||||
setFileList([])
|
setFileList([])
|
||||||
}}
|
}}
|
||||||
footer={<ModalFooter hideDelete={!currentRow?.SCENICAREA_ID} handleDelete={async () => {
|
footer={<ModalFooter confirmLoading={confirmLoading} hideDelete={!currentRow?.SCENICAREA_ID} handleDelete={async () => {
|
||||||
await handelDelete(currentRow?.SCENICAREA_ID);
|
await handelDelete(currentRow?.SCENICAREA_ID);
|
||||||
|
|
||||||
}} handleCancel={() => {
|
}} handleCancel={() => {
|
||||||
@ -547,6 +594,7 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
|
|||||||
SCENICAREA_STATE: 1000
|
SCENICAREA_STATE: 1000
|
||||||
}}
|
}}
|
||||||
onFinish={async (values) => {
|
onFinish={async (values) => {
|
||||||
|
|
||||||
let newValue = { ...values };
|
let newValue = { ...values };
|
||||||
if (currentRow) {
|
if (currentRow) {
|
||||||
// 编辑数据
|
// 编辑数据
|
||||||
@ -695,13 +743,29 @@ const scenicSpotConfig: React.FC<{ currentUser: CurrentUser | undefined }> = (pr
|
|||||||
beforeUpload: beforeUpload,
|
beforeUpload: beforeUpload,
|
||||||
onPreview: handlePreview,
|
onPreview: handlePreview,
|
||||||
fileList: fileList, // 绑定 fileList
|
fileList: fileList, // 绑定 fileList
|
||||||
customRequest: ({ file, onSuccess }) => {
|
customRequest: ({ file, onSuccess, onError, onProgress }) => {
|
||||||
// 自定义上传,不实际发送请求
|
// 开始处理
|
||||||
setTimeout(() => {
|
onProgress?.({ percent: 10 });
|
||||||
if (onSuccess) {
|
|
||||||
onSuccess({});
|
compressImage(file as File, {
|
||||||
}
|
width: 300,
|
||||||
}, 0);
|
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) => {
|
onChange: async (info: any) => {
|
||||||
console.log('info', info);
|
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 errorHandler = (error: { response: Response }): Response => {
|
||||||
const { response } = error;
|
const { response } = error;
|
||||||
|
|
||||||
|
|
||||||
if (response && response.status) {
|
if (response && response.status) {
|
||||||
const errorText = codeMessage[response.status] || response.statusText;
|
const errorText = codeMessage[response.status] || response.statusText;
|
||||||
const { status, url } = response;
|
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