233 lines
7.5 KiB
TypeScript
233 lines
7.5 KiB
TypeScript
import { useState, useCallback, useEffect, useRef } from 'react';
|
||
import { useToast } from '@/components/ui/use-toast';
|
||
import { getDeployEnvironments } from '../service';
|
||
import type { DeployTeam } from '../types';
|
||
|
||
interface UseDeploymentDataOptions {
|
||
onInitialLoadComplete?: () => void;
|
||
}
|
||
|
||
/**
|
||
* 部署数据管理 Hook
|
||
* 优化点:
|
||
* 1. 智能轮询 - 根据部署状态动态调整轮询频率
|
||
* 2. 页面可见性检测 - 页面隐藏时暂停轮询
|
||
* 3. 状态持久化 - 刷新时保持选中的团队和环境
|
||
*/
|
||
export function useDeploymentData(options: UseDeploymentDataOptions = {}) {
|
||
const { toast } = useToast();
|
||
const [loading, setLoading] = useState(true);
|
||
const [teams, setTeams] = useState<DeployTeam[]>([]);
|
||
const [currentTeamId, setCurrentTeamId] = useState<number | null>(null);
|
||
const [currentEnvId, setCurrentEnvId] = useState<number | null>(null);
|
||
const [deploying, setDeploying] = useState<Set<number>>(new Set());
|
||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||
|
||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||
const optionsRef = useRef(options);
|
||
const toastRef = useRef(toast);
|
||
|
||
// 同步 options 和 toast 到 ref
|
||
useEffect(() => {
|
||
optionsRef.current = options;
|
||
toastRef.current = toast;
|
||
}, [options, toast]);
|
||
|
||
// 使用 ref 存储当前选中的团队和环境,避免 loadData 依赖变化
|
||
const currentSelectionRef = useRef({ teamId: currentTeamId, envId: currentEnvId });
|
||
|
||
useEffect(() => {
|
||
currentSelectionRef.current = { teamId: currentTeamId, envId: currentEnvId };
|
||
}, [currentTeamId, currentEnvId]);
|
||
|
||
// 加载部署环境数据 - 移除状态依赖,使用 ref
|
||
const loadData = useCallback(async (showLoading = false) => {
|
||
try {
|
||
if (showLoading) {
|
||
setLoading(true);
|
||
}
|
||
|
||
const response = await getDeployEnvironments();
|
||
|
||
if (response && response.length > 0) {
|
||
const prevTeamId = currentSelectionRef.current.teamId;
|
||
const prevEnvId = currentSelectionRef.current.envId;
|
||
|
||
setTeams(response);
|
||
|
||
setIsInitialLoad(prev => {
|
||
if (prev) {
|
||
// 首次加载:默认选中第一个团队和第一个环境
|
||
if (response.length > 0) {
|
||
setCurrentTeamId(response[0].teamId);
|
||
|
||
if (response[0].environments.length > 0) {
|
||
setCurrentEnvId(response[0].environments[0].environmentId);
|
||
}
|
||
}
|
||
optionsRef.current.onInitialLoadComplete?.();
|
||
return false;
|
||
} else {
|
||
// 后续刷新:保持当前选中的团队和环境
|
||
if (prevTeamId !== null) {
|
||
const teamExists = response.some(t => t.teamId === prevTeamId);
|
||
if (teamExists) {
|
||
setCurrentTeamId(prevTeamId);
|
||
|
||
if (prevEnvId !== null) {
|
||
const team = response.find(t => t.teamId === prevTeamId);
|
||
const envExists = team?.environments.some(e => e.environmentId === prevEnvId);
|
||
if (envExists) {
|
||
setCurrentEnvId(prevEnvId);
|
||
} else if (team && team.environments.length > 0) {
|
||
setCurrentEnvId(team.environments[0].environmentId);
|
||
}
|
||
}
|
||
} else if (response.length > 0) {
|
||
setCurrentTeamId(response[0].teamId);
|
||
if (response[0].environments.length > 0) {
|
||
setCurrentEnvId(response[0].environments[0].environmentId);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查部署状态,如果应用不再是 RUNNING 状态,从 deploying 状态中移除
|
||
setDeploying((prevDeploying) => {
|
||
const newDeploying = new Set(prevDeploying);
|
||
let hasChanges = false;
|
||
|
||
response.forEach(team => {
|
||
team.environments.forEach(env => {
|
||
env.applications.forEach(app => {
|
||
if (newDeploying.has(app.teamApplicationId)) {
|
||
const latestStatus = app.deployStatistics?.latestStatus;
|
||
if (latestStatus && latestStatus !== 'RUNNING') {
|
||
newDeploying.delete(app.teamApplicationId);
|
||
hasChanges = true;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
return hasChanges ? newDeploying : prevDeploying;
|
||
});
|
||
}
|
||
return prev;
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('加载数据失败:', error);
|
||
setIsInitialLoad(prev => {
|
||
if (prev) {
|
||
toastRef.current({
|
||
variant: 'destructive',
|
||
title: '加载失败',
|
||
description: '无法加载部署环境数据,请稍后重试',
|
||
});
|
||
}
|
||
return prev;
|
||
});
|
||
} finally {
|
||
if (showLoading) {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
}, []);
|
||
|
||
// 首次加载数据
|
||
useEffect(() => {
|
||
const initData = async () => {
|
||
await loadData(false);
|
||
setLoading(false);
|
||
};
|
||
|
||
initData();
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []);
|
||
|
||
// 定时刷新数据(智能轮询) - 简化版
|
||
useEffect(() => {
|
||
// 初始加载时不启动轮询
|
||
if (isInitialLoad) return;
|
||
|
||
// 清除旧的定时器
|
||
if (intervalRef.current) {
|
||
clearInterval(intervalRef.current);
|
||
}
|
||
|
||
// 页面可见性检测
|
||
const handleVisibilityChange = () => {
|
||
if (document.hidden) {
|
||
// 页面隐藏时停止轮询
|
||
if (intervalRef.current) {
|
||
clearInterval(intervalRef.current);
|
||
intervalRef.current = null;
|
||
}
|
||
} else {
|
||
// 页面显示时恢复轮询
|
||
if (intervalRef.current) {
|
||
clearInterval(intervalRef.current);
|
||
}
|
||
// 智能间隔:有部署时5秒,无部署时30秒
|
||
const interval = deploying.size > 0 ? 5000 : 30000;
|
||
intervalRef.current = setInterval(() => {
|
||
loadData(false);
|
||
}, interval);
|
||
}
|
||
};
|
||
|
||
// 启动轮询 - 智能间隔
|
||
const interval = deploying.size > 0 ? 5000 : 30000;
|
||
intervalRef.current = setInterval(() => {
|
||
loadData(false);
|
||
}, interval);
|
||
|
||
// 监听页面可见性变化
|
||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||
|
||
return () => {
|
||
if (intervalRef.current) {
|
||
clearInterval(intervalRef.current);
|
||
}
|
||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||
};
|
||
}, [isInitialLoad, deploying.size, loadData]);
|
||
|
||
// 切换团队
|
||
const handleTeamChange = useCallback((teamId: string) => {
|
||
const newTeamId = Number(teamId);
|
||
setCurrentTeamId(newTeamId);
|
||
|
||
const team = teams.find(t => t.teamId === newTeamId);
|
||
if (team && team.environments.length > 0) {
|
||
setCurrentEnvId(team.environments[0].environmentId);
|
||
} else {
|
||
setCurrentEnvId(null);
|
||
}
|
||
}, [teams]);
|
||
|
||
// 处理部署成功
|
||
const handleDeploySuccess = useCallback(async () => {
|
||
await loadData(false);
|
||
}, [loadData]);
|
||
|
||
// 获取当前团队和环境
|
||
const currentTeam = teams.find(t => t.teamId === currentTeamId);
|
||
const currentEnv = currentTeam?.environments.find(e => e.environmentId === currentEnvId);
|
||
|
||
return {
|
||
loading,
|
||
teams,
|
||
currentTeamId,
|
||
currentEnvId,
|
||
deploying,
|
||
currentTeam,
|
||
currentEnv,
|
||
setCurrentEnvId,
|
||
handleTeamChange,
|
||
handleDeploySuccess,
|
||
refreshData: () => loadData(false)
|
||
};
|
||
}
|