重构前端逻辑

This commit is contained in:
dengqichen 2025-11-10 16:50:28 +08:00
parent a964dac6b0
commit a682d5718b
6 changed files with 597 additions and 384 deletions

View File

@ -1,4 +1,4 @@
import { useState, useCallback, useEffect } from 'react';
import { useState, useCallback, useEffect, useRef } from 'react';
import { message, Modal } from 'antd';
import type { Page } from '@/types/base/page';
import type { TablePaginationConfig } from 'antd/es/table';
@ -60,16 +60,30 @@ export function useTableData<
loading: false
});
// 加载数据
const loadData = useCallback(async (params?: Partial<Q>) => {
// 使用 ref 存储当前分页参数,避免循环依赖
const paginationRef = useRef({
current: 1,
pageSize: config.defaultPageSize || 10
});
// 同步 ref 和 state
useEffect(() => {
paginationRef.current = {
current: state.pagination.current || 1,
pageSize: state.pagination.pageSize || 10
};
}, [state.pagination.current, state.pagination.pageSize]);
// 加载数据 - 修复:使用 ref 避免依赖 state
const loadData = useCallback(async (params?: Partial<Q> & { pageNum?: number; pageSize?: number }) => {
setState(prev => ({ ...prev, loading: true }));
try {
const pageData = await service.list({
...defaultParams,
...params,
pageNum: state.pagination.current,
pageSize: state.pagination.pageSize
});
pageNum: params?.pageNum ?? paginationRef.current.current,
pageSize: params?.pageSize ?? paginationRef.current.pageSize
} as Q & { pageNum?: number; pageSize?: number });
setState(prev => ({
...prev,
@ -80,11 +94,12 @@ export function useTableData<
},
loading: false
}));
return pageData;
} catch (error) {
setState(prev => ({ ...prev, loading: false }));
throw error;
}
}, [service, defaultParams, state.pagination]);
}, [service, defaultParams]);
// 表格变化处理
const handleTableChange = (

View File

@ -0,0 +1,127 @@
import React from 'react';
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent } from "@/components/ui/tabs";
import { Package, Shield, CheckCircle2 } from "lucide-react";
import { ApplicationCard } from './ApplicationCard';
import type { DeployTeam, DeployEnvironment, ApplicationConfig } from '../types';
interface EnvironmentTabsProps {
team: DeployTeam;
currentEnvId: number | null;
deploying: Set<number>;
onEnvChange: (envId: number) => void;
onDeploy: (app: ApplicationConfig, remark: string) => Promise<void>;
}
/**
*
*
*/
export const EnvironmentTabs: React.FC<EnvironmentTabsProps> = React.memo(({
team,
currentEnvId,
deploying,
onEnvChange,
onDeploy
}) => {
const currentEnv = team.environments.find(e => e.environmentId === currentEnvId);
return (
<Tabs value={currentEnvId?.toString()} onValueChange={(value) => onEnvChange(Number(value))}>
{/* 现代化 TAB 头部 */}
<div className="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b">
<div className="flex items-center justify-between px-6 pt-4 pb-2">
<div className="flex items-center gap-3">
<div className="h-8 w-1 bg-primary rounded-full" />
<h3 className="text-lg font-semibold"></h3>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground min-h-[20px]">
{currentEnv && currentEnv.requiresApproval && currentEnv.approvers.length > 0 && (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800">
<Shield className="h-3.5 w-3.5 text-amber-600" />
<span className="text-amber-900 dark:text-amber-200 font-medium">
: {currentEnv.approvers.map((a) => a.realName).join('、')}
</span>
</div>
)}
</div>
</div>
<div className="px-6 pb-3">
<div className="inline-flex h-11 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground gap-1">
{team.environments.map((env) => (
<button
key={env.environmentId}
onClick={() => onEnvChange(env.environmentId)}
className={`
inline-flex items-center justify-center whitespace-nowrap rounded-md px-4 py-2
text-sm font-medium ring-offset-background transition-all
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
disabled:pointer-events-none disabled:opacity-50
${currentEnvId === env.environmentId
? 'bg-background text-foreground shadow-sm'
: 'hover:bg-background/50 hover:text-foreground'
}
`}
>
<div className="flex items-center gap-2">
{env.requiresApproval ? (
<Shield className={`h-3.5 w-3.5 ${currentEnvId === env.environmentId ? 'text-amber-600' : 'text-amber-500/60'}`} />
) : (
<CheckCircle2 className={`h-3.5 w-3.5 ${currentEnvId === env.environmentId ? 'text-green-600' : 'text-green-500/60'}`} />
)}
<span>{env.environmentName}</span>
<span className={`
ml-1 rounded-full px-2 py-0.5 text-xs font-semibold
${currentEnvId === env.environmentId
? 'bg-primary/10 text-primary'
: 'bg-muted-foreground/10 text-muted-foreground'
}
`}>
{env.applications.length}
</span>
</div>
</button>
))}
</div>
</div>
</div>
{/* 内容区域 */}
{team.environments.map((env) => (
<TabsContent key={env.environmentId} value={env.environmentId.toString()} className="mt-0 focus-visible:ring-0 focus-visible:ring-offset-0">
<div className="p-6">
{env.applications.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-16">
<div className="rounded-full bg-muted p-6 mb-4">
<Package className="h-12 w-12 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-muted-foreground">
{env.environmentName}
</p>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-6 gap-3">
{env.applications.map((app) => (
<ApplicationCard
key={app.teamApplicationId}
app={app}
environment={env}
teamId={team.teamId}
onDeploy={onDeploy}
isDeploying={deploying.has(app.teamApplicationId)}
/>
))}
</div>
)}
</div>
</TabsContent>
))}
</Tabs>
);
});
EnvironmentTabs.displayName = 'EnvironmentTabs';

View File

@ -0,0 +1,66 @@
import React from 'react';
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Users, ClipboardCheck } from "lucide-react";
import type { DeployTeam } from '../types';
interface TeamSelectorProps {
teams: DeployTeam[];
currentTeamId: number | null;
pendingApprovalCount: number;
onTeamChange: (teamId: string) => void;
onApprovalClick: () => void;
}
/**
*
*
*/
export const TeamSelector: React.FC<TeamSelectorProps> = React.memo(({
teams,
currentTeamId,
pendingApprovalCount,
onTeamChange,
onApprovalClick
}) => {
return (
<div className="flex items-center gap-4">
{/* 待审批按钮 */}
<Button
variant="outline"
className="border-orange-500 text-orange-600 hover:bg-orange-50 hover:text-orange-700 hover:border-orange-600"
onClick={onApprovalClick}
>
<ClipboardCheck className="h-4 w-4 mr-2" />
{pendingApprovalCount > 0 && (
<span className="ml-2 px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 text-xs font-semibold">
{pendingApprovalCount}
</span>
)}
</Button>
<div className="h-6 w-px bg-border" />
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">:</span>
</div>
<Select value={currentTeamId?.toString()} onValueChange={onTeamChange}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="选择团队" />
</SelectTrigger>
<SelectContent>
{teams.map((team) => (
<SelectItem key={team.teamId} value={team.teamId.toString()}>
{team.teamName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
});
TeamSelector.displayName = 'TeamSelector';

View File

@ -0,0 +1,232 @@
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)
};
}

View File

@ -0,0 +1,86 @@
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { getMyApprovalTasks } from '../service';
import type { DeployTeam } from '../types';
interface UsePendingApprovalOptions {
teams: DeployTeam[];
pollingEnabled?: boolean;
pollingInterval?: number; // 轮询间隔默认30秒
}
/**
* Hook
*
*
*/
export function usePendingApproval({
teams,
pollingEnabled = true,
pollingInterval = 30000 // 默认30秒
}: UsePendingApprovalOptions) {
const [approvalModalOpen, setApprovalModalOpen] = useState(false);
const [pendingApprovalCount, setPendingApprovalCount] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// 提取所有工作流定义键(去重)- 使用 useMemo 避免重复计算
const workflowDefinitionKeys = useMemo(() => {
const workflowKeys = teams.flatMap(team =>
team.environments.flatMap(env =>
env.applications
.map(app => app.workflowDefinitionKey)
.filter((key): key is string => !!key)
)
);
return Array.from(new Set(workflowKeys));
}, [teams]);
// 加载待审批数量 - 使用 useCallback 避免重复创建
const loadPendingApprovalCount = useCallback(async () => {
if (!pollingEnabled || workflowDefinitionKeys.length === 0) {
return;
}
try {
const response = await getMyApprovalTasks(workflowDefinitionKeys);
if (response) {
setPendingApprovalCount(response.length || 0);
}
} catch (error) {
// 静默失败,不影响主页面
console.error('Failed to load pending approval count:', error);
}
}, [workflowDefinitionKeys, pollingEnabled]);
// 轮询待审批数量
useEffect(() => {
if (!pollingEnabled || workflowDefinitionKeys.length === 0) {
return;
}
// 清除旧的定时器
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
// 立即加载一次
loadPendingApprovalCount();
// 设置定时轮询
intervalRef.current = setInterval(loadPendingApprovalCount, pollingInterval);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [loadPendingApprovalCount, pollingEnabled, workflowDefinitionKeys.length, pollingInterval]);
return {
approvalModalOpen,
setApprovalModalOpen,
pendingApprovalCount,
workflowDefinitionKeys,
loadPendingApprovalCount,
refreshApprovalCount: loadPendingApprovalCount
};
}

View File

@ -1,24 +1,12 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useEffect, useCallback } from 'react';
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent } from "@/components/ui/tabs";
import {
Package,
Shield,
Loader2,
Users,
Server,
CheckCircle2,
ClipboardCheck,
} from "lucide-react";
import { useToast } from '@/components/ui/use-toast';
import { useSelector } from 'react-redux';
import type { RootState } from '@/store';
import { getDeployEnvironments, getMyApprovalTasks } from './service';
import { ApplicationCard } from './components/ApplicationCard';
import { Package, Server } from "lucide-react";
import { TeamSelector } from './components/TeamSelector';
import { EnvironmentTabs } from './components/EnvironmentTabs';
import { PendingApprovalModal } from './components/PendingApprovalModal';
import type { DeployTeam, ApplicationConfig } from './types';
import { useDeploymentData } from './hooks/useDeploymentData';
import { usePendingApproval } from './hooks/usePendingApproval';
import type { ApplicationConfig } from './types';
// ✅ 优化:使用骨架屏替代 loading体验更好
const LoadingState = () => (
@ -41,236 +29,50 @@ const LoadingState = () => (
</div>
);
/**
* Dashboard -
*
* :
* 1. TeamSelector, EnvironmentTabs
* 2. 使 hooks useDeploymentData, usePendingApproval
* 3. 使 React.memo useCallback
* 4. -
*/
const Dashboard: React.FC = () => {
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 [approvalModalOpen, setApprovalModalOpen] = useState(false);
const [pendingApprovalCount, setPendingApprovalCount] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// 从 Redux store 中获取当前登录用户信息
const currentUserId = useSelector((state: RootState) => state.user.userInfo?.id);
// 提取所有工作流定义键(去重)
const workflowDefinitionKeys = React.useMemo(() => {
const workflowKeys = teams.flatMap(team =>
team.environments.flatMap(env =>
env.applications
.map(app => app.workflowDefinitionKey)
.filter((key): key is string => !!key)
)
);
return Array.from(new Set(workflowKeys));
}, [teams]);
// 加载待审批数量
const loadPendingApprovalCount = React.useCallback(async () => {
try {
const response = await getMyApprovalTasks(workflowDefinitionKeys);
if (response) {
setPendingApprovalCount(response.length || 0);
}
} catch (error) {
// 静默失败,不影响主页面
console.error('Failed to load pending approval count:', error);
// 使用自定义 hooks 管理状态
const deploymentData = useDeploymentData({
onInitialLoadComplete: () => {
// 初始加载完成后,加载待审批数据
approvalData.loadPendingApprovalCount();
}
}, [workflowDefinitionKeys]);
});
// 加载部署环境数据
const loadData = React.useCallback(async (showLoading = false) => {
try {
if (showLoading) {
setLoading(true);
}
const response = await getDeployEnvironments();
// ✅ 新接口直接返回 teams 数组
if (response && response.length > 0) {
const prevTeamId = currentTeamId;
const prevEnvId = currentEnvId;
setTeams(response);
if (isInitialLoad) {
// 首次加载:默认选中第一个团队和第一个环境
if (response.length > 0) {
setCurrentTeamId(response[0].teamId);
if (response[0].environments.length > 0) {
setCurrentEnvId(response[0].environments[0].environmentId);
}
}
setIsInitialLoad(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 => {
// 如果应用在 deploying 状态中,但最新状态不是 RUNNING则移除
if (newDeploying.has(app.teamApplicationId)) {
const latestStatus = app.deployStatistics?.latestStatus;
if (latestStatus && latestStatus !== 'RUNNING') {
newDeploying.delete(app.teamApplicationId);
hasChanges = true;
}
}
});
});
});
return hasChanges ? newDeploying : prevDeploying;
});
}
}
} catch (error) {
console.error('加载数据失败:', error);
// 只在首次加载失败时显示错误提示
if (isInitialLoad) {
toast({
variant: 'destructive',
title: '加载失败',
description: '无法加载部署环境数据,请稍后重试',
});
}
} finally {
if (showLoading) {
setLoading(false);
}
}
}, [currentTeamId, currentEnvId, isInitialLoad, toast]);
const approvalData = usePendingApproval({
teams: deploymentData.teams,
pollingEnabled: !deploymentData.loading
});
// 首次加载数据
// 监听团队数据变化,自动刷新待审批数量
useEffect(() => {
// ✅ 优化:异步加载数据,不阻塞页面渲染
// 先渲染骨架,然后后台加载数据
const initData = async () => {
await Promise.all([
loadData(false), // 不显示 loading直接显示内容
loadPendingApprovalCount()
]);
setLoading(false);
};
initData();
if (deploymentData.teams.length > 0) {
approvalData.loadPendingApprovalCount();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [deploymentData.teams.length]);
// 定时刷新数据每5秒
useEffect(() => {
// 只在非首次加载后开始轮询
if (isInitialLoad) return;
// 处理部署成功 - 使用 useCallback 避免重复创建
const handleDeploy = useCallback(async (app: ApplicationConfig, remark: string) => {
await deploymentData.handleDeploySuccess();
await approvalData.refreshApprovalCount();
}, [deploymentData.handleDeploySuccess, approvalData.refreshApprovalCount]);
// 启动定时器的函数
const startPolling = () => {
// 清除旧的定时器
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
// 立即刷新一次
loadData(false);
loadPendingApprovalCount();
// 设置新的定时器
intervalRef.current = setInterval(() => {
loadData(false);
loadPendingApprovalCount();
}, 5000);
};
// 停止定时器的函数
const stopPolling = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
// 页面可见性检测:当页面隐藏时暂停轮询,显示时恢复
const handleVisibilityChange = () => {
if (document.hidden) {
stopPolling();
} else {
startPolling();
}
};
// 启动轮询
startPolling();
// 监听页面可见性变化
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
stopPolling();
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [isInitialLoad, loadData]);
// 切换团队时,自动选中第一个环境
const handleTeamChange = (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);
}
};
// 处理部署成功后的回调(刷新数据)
// 注意:实际的部署提交已在 DeploymentFormModal 中完成
const handleDeploy = async (app: ApplicationConfig, remark: string) => {
// 部署成功后,刷新数据以获取最新状态
await loadData(false);
};
// 获取当前团队和环境
const currentTeam = teams.find(t => t.teamId === currentTeamId);
const currentEnv = currentTeam?.environments.find(e => e.environmentId === currentEnvId);
if (loading) {
// 加载状态
if (deploymentData.loading) {
return <LoadingState/>;
}
if (teams.length === 0) {
// 无团队状态
if (deploymentData.teams.length === 0) {
return (
<div className="flex-1 p-8">
<h2 className="text-3xl font-bold tracking-tight mb-6"></h2>
@ -295,171 +97,56 @@ const Dashboard: React.FC = () => {
</p>
</div>
<div className="flex items-center gap-4">
{/* 待审批按钮 */}
<Button
variant="outline"
className="border-orange-500 text-orange-600 hover:bg-orange-50 hover:text-orange-700 hover:border-orange-600"
onClick={() => setApprovalModalOpen(true)}
>
<ClipboardCheck className="h-4 w-4 mr-2" />
{pendingApprovalCount > 0 && (
<span className="ml-2 px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 text-xs font-semibold">
{pendingApprovalCount}
</span>
)}
</Button>
<div className="h-6 w-px bg-border" />
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">:</span>
</div>
<Select value={currentTeamId?.toString()} onValueChange={handleTeamChange}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="选择团队" />
</SelectTrigger>
<SelectContent>
{teams.map((team) => (
<SelectItem key={team.teamId} value={team.teamId.toString()}>
{team.teamName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<TeamSelector
teams={deploymentData.teams}
currentTeamId={deploymentData.currentTeamId}
pendingApprovalCount={approvalData.pendingApprovalCount}
onTeamChange={deploymentData.handleTeamChange}
onApprovalClick={() => approvalData.setApprovalModalOpen(true)}
/>
</div>
{/* 当前团队信息 */}
{currentTeam && (
{deploymentData.currentTeam && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="font-medium text-foreground">{currentTeam.teamName}</span>
{currentTeam.description && (
<span className="font-medium text-foreground">{deploymentData.currentTeam.teamName}</span>
{deploymentData.currentTeam.description && (
<>
<span>·</span>
<span>{currentTeam.description}</span>
<span>{deploymentData.currentTeam.description}</span>
</>
)}
</div>
)}
{/* 环境切换和应用列表 */}
{currentTeam && (
currentTeam.environments.length === 0 ? (
{deploymentData.currentTeam && (
deploymentData.currentTeam.environments.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-16">
<Server className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-muted-foreground">
{currentTeam.teamName}
{deploymentData.currentTeam.teamName}
</p>
</CardContent>
</Card>
) : (
<Tabs value={currentEnvId?.toString()} onValueChange={(value) => setCurrentEnvId(Number(value))}>
{/* 现代化 TAB 头部 - 独立于 Card */}
<div className="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b">
<div className="flex items-center justify-between px-6 pt-4 pb-2">
<div className="flex items-center gap-3">
<div className="h-8 w-1 bg-primary rounded-full" />
<h3 className="text-lg font-semibold"></h3>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground min-h-[20px]">
{currentEnv && currentEnv.requiresApproval && currentEnv.approvers.length > 0 && (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800">
<Shield className="h-3.5 w-3.5 text-amber-600" />
<span className="text-amber-900 dark:text-amber-200 font-medium">
: {currentEnv.approvers.map((a) => a.realName).join('、')}
</span>
</div>
)}
</div>
</div>
<div className="px-6 pb-3">
<div className="inline-flex h-11 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground gap-1">
{currentTeam.environments.map((env) => (
<button
key={env.environmentId}
onClick={() => setCurrentEnvId(env.environmentId)}
className={`
inline-flex items-center justify-center whitespace-nowrap rounded-md px-4 py-2
text-sm font-medium ring-offset-background transition-all
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
disabled:pointer-events-none disabled:opacity-50
${currentEnvId === env.environmentId
? 'bg-background text-foreground shadow-sm'
: 'hover:bg-background/50 hover:text-foreground'
}
`}
>
<div className="flex items-center gap-2">
{env.requiresApproval ? (
<Shield className={`h-3.5 w-3.5 ${currentEnvId === env.environmentId ? 'text-amber-600' : 'text-amber-500/60'}`} />
) : (
<CheckCircle2 className={`h-3.5 w-3.5 ${currentEnvId === env.environmentId ? 'text-green-600' : 'text-green-500/60'}`} />
)}
<span>{env.environmentName}</span>
<span className={`
ml-1 rounded-full px-2 py-0.5 text-xs font-semibold
${currentEnvId === env.environmentId
? 'bg-primary/10 text-primary'
: 'bg-muted-foreground/10 text-muted-foreground'
}
`}>
{env.applications.length}
</span>
</div>
</button>
))}
</div>
</div>
</div>
{/* 内容区域 */}
{currentTeam.environments.map((env) => (
<TabsContent key={env.environmentId} value={env.environmentId.toString()} className="mt-0">
<div className="p-6">
{/* 应用列表 */}
{env.applications.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-16">
<div className="rounded-full bg-muted p-6 mb-4">
<Package className="h-12 w-12 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-muted-foreground">
{env.environmentName}
</p>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-6 gap-3">
{env.applications.map((app) => (
<ApplicationCard
key={app.teamApplicationId}
app={app}
environment={env}
teamId={currentTeam?.teamId || 0}
onDeploy={handleDeploy}
isDeploying={deploying.has(app.teamApplicationId)}
/>
))}
</div>
)}
</div>
</TabsContent>
))}
</Tabs>
<EnvironmentTabs
team={deploymentData.currentTeam}
currentEnvId={deploymentData.currentEnvId}
deploying={deploymentData.deploying}
onEnvChange={deploymentData.setCurrentEnvId}
onDeploy={handleDeploy}
/>
)
)}
{/* 待审批列表弹窗 */}
<PendingApprovalModal
open={approvalModalOpen}
onOpenChange={setApprovalModalOpen}
workflowDefinitionKeys={workflowDefinitionKeys}
open={approvalData.approvalModalOpen}
onOpenChange={approvalData.setApprovalModalOpen}
workflowDefinitionKeys={approvalData.workflowDefinitionKeys}
/>
</div>
);