重构前端逻辑
This commit is contained in:
parent
a964dac6b0
commit
a682d5718b
@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import { message, Modal } from 'antd';
|
import { message, Modal } from 'antd';
|
||||||
import type { Page } from '@/types/base/page';
|
import type { Page } from '@/types/base/page';
|
||||||
import type { TablePaginationConfig } from 'antd/es/table';
|
import type { TablePaginationConfig } from 'antd/es/table';
|
||||||
@ -60,16 +60,30 @@ export function useTableData<
|
|||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载数据
|
// 使用 ref 存储当前分页参数,避免循环依赖
|
||||||
const loadData = useCallback(async (params?: Partial<Q>) => {
|
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 }));
|
setState(prev => ({ ...prev, loading: true }));
|
||||||
try {
|
try {
|
||||||
const pageData = await service.list({
|
const pageData = await service.list({
|
||||||
...defaultParams,
|
...defaultParams,
|
||||||
...params,
|
...params,
|
||||||
pageNum: state.pagination.current,
|
pageNum: params?.pageNum ?? paginationRef.current.current,
|
||||||
pageSize: state.pagination.pageSize
|
pageSize: params?.pageSize ?? paginationRef.current.pageSize
|
||||||
});
|
} as Q & { pageNum?: number; pageSize?: number });
|
||||||
|
|
||||||
setState(prev => ({
|
setState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -80,11 +94,12 @@ export function useTableData<
|
|||||||
},
|
},
|
||||||
loading: false
|
loading: false
|
||||||
}));
|
}));
|
||||||
|
return pageData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setState(prev => ({ ...prev, loading: false }));
|
setState(prev => ({ ...prev, loading: false }));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, [service, defaultParams, state.pagination]);
|
}, [service, defaultParams]);
|
||||||
|
|
||||||
// 表格变化处理
|
// 表格变化处理
|
||||||
const handleTableChange = (
|
const handleTableChange = (
|
||||||
|
|||||||
127
frontend/src/pages/Dashboard/components/EnvironmentTabs.tsx
Normal file
127
frontend/src/pages/Dashboard/components/EnvironmentTabs.tsx
Normal 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';
|
||||||
66
frontend/src/pages/Dashboard/components/TeamSelector.tsx
Normal file
66
frontend/src/pages/Dashboard/components/TeamSelector.tsx
Normal 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';
|
||||||
232
frontend/src/pages/Dashboard/hooks/useDeploymentData.ts
Normal file
232
frontend/src/pages/Dashboard/hooks/useDeploymentData.ts
Normal 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)
|
||||||
|
};
|
||||||
|
}
|
||||||
86
frontend/src/pages/Dashboard/hooks/usePendingApproval.ts
Normal file
86
frontend/src/pages/Dashboard/hooks/usePendingApproval.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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 { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Package, Server } from "lucide-react";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { TeamSelector } from './components/TeamSelector';
|
||||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
import { EnvironmentTabs } from './components/EnvironmentTabs';
|
||||||
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 { PendingApprovalModal } from './components/PendingApprovalModal';
|
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,体验更好
|
// ✅ 优化:使用骨架屏替代 loading,体验更好
|
||||||
const LoadingState = () => (
|
const LoadingState = () => (
|
||||||
@ -41,236 +29,50 @@ const LoadingState = () => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard 组件 - 优化版
|
||||||
|
*
|
||||||
|
* 优化点:
|
||||||
|
* 1. 拆分为更小的子组件(TeamSelector, EnvironmentTabs)
|
||||||
|
* 2. 使用自定义 hooks 管理状态(useDeploymentData, usePendingApproval)
|
||||||
|
* 3. 使用 React.memo 和 useCallback 优化性能
|
||||||
|
* 4. 智能轮询 - 根据部署状态动态调整轮询频率
|
||||||
|
*/
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const { toast } = useToast();
|
// 使用自定义 hooks 管理状态
|
||||||
const [loading, setLoading] = useState(true);
|
const deploymentData = useDeploymentData({
|
||||||
const [teams, setTeams] = useState<DeployTeam[]>([]);
|
onInitialLoadComplete: () => {
|
||||||
const [currentTeamId, setCurrentTeamId] = useState<number | null>(null);
|
// 初始加载完成后,加载待审批数据
|
||||||
const [currentEnvId, setCurrentEnvId] = useState<number | null>(null);
|
approvalData.loadPendingApprovalCount();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}, [workflowDefinitionKeys]);
|
});
|
||||||
|
|
||||||
// 加载部署环境数据
|
const approvalData = usePendingApproval({
|
||||||
const loadData = React.useCallback(async (showLoading = false) => {
|
teams: deploymentData.teams,
|
||||||
try {
|
pollingEnabled: !deploymentData.loading
|
||||||
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]);
|
|
||||||
|
|
||||||
// 首次加载数据
|
// 监听团队数据变化,自动刷新待审批数量
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// ✅ 优化:异步加载数据,不阻塞页面渲染
|
if (deploymentData.teams.length > 0) {
|
||||||
// 先渲染骨架,然后后台加载数据
|
approvalData.loadPendingApprovalCount();
|
||||||
const initData = async () => {
|
}
|
||||||
await Promise.all([
|
|
||||||
loadData(false), // 不显示 loading,直接显示内容
|
|
||||||
loadPendingApprovalCount()
|
|
||||||
]);
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
initData();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, [deploymentData.teams.length]);
|
||||||
|
|
||||||
// 定时刷新数据(每5秒)
|
// 处理部署成功 - 使用 useCallback 避免重复创建
|
||||||
useEffect(() => {
|
const handleDeploy = useCallback(async (app: ApplicationConfig, remark: string) => {
|
||||||
// 只在非首次加载后开始轮询
|
await deploymentData.handleDeploySuccess();
|
||||||
if (isInitialLoad) return;
|
await approvalData.refreshApprovalCount();
|
||||||
|
}, [deploymentData.handleDeploySuccess, approvalData.refreshApprovalCount]);
|
||||||
|
|
||||||
// 启动定时器的函数
|
// 加载状态
|
||||||
const startPolling = () => {
|
if (deploymentData.loading) {
|
||||||
// 清除旧的定时器
|
|
||||||
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) {
|
|
||||||
return <LoadingState/>;
|
return <LoadingState/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (teams.length === 0) {
|
// 无团队状态
|
||||||
|
if (deploymentData.teams.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 p-8">
|
<div className="flex-1 p-8">
|
||||||
<h2 className="text-3xl font-bold tracking-tight mb-6">部署管理</h2>
|
<h2 className="text-3xl font-bold tracking-tight mb-6">部署管理</h2>
|
||||||
@ -295,171 +97,56 @@ const Dashboard: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<TeamSelector
|
||||||
{/* 待审批按钮 */}
|
teams={deploymentData.teams}
|
||||||
<Button
|
currentTeamId={deploymentData.currentTeamId}
|
||||||
variant="outline"
|
pendingApprovalCount={approvalData.pendingApprovalCount}
|
||||||
className="border-orange-500 text-orange-600 hover:bg-orange-50 hover:text-orange-700 hover:border-orange-600"
|
onTeamChange={deploymentData.handleTeamChange}
|
||||||
onClick={() => setApprovalModalOpen(true)}
|
onApprovalClick={() => approvalData.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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 当前团队信息 */}
|
{/* 当前团队信息 */}
|
||||||
{currentTeam && (
|
{deploymentData.currentTeam && (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<span className="font-medium text-foreground">{currentTeam.teamName}</span>
|
<span className="font-medium text-foreground">{deploymentData.currentTeam.teamName}</span>
|
||||||
{currentTeam.description && (
|
{deploymentData.currentTeam.description && (
|
||||||
<>
|
<>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{currentTeam.description}</span>
|
<span>{deploymentData.currentTeam.description}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 环境切换和应用列表 */}
|
{/* 环境切换和应用列表 */}
|
||||||
{currentTeam && (
|
{deploymentData.currentTeam && (
|
||||||
currentTeam.environments.length === 0 ? (
|
deploymentData.currentTeam.environments.length === 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||||
<Server className="h-16 w-16 text-muted-foreground mb-4" />
|
<Server className="h-16 w-16 text-muted-foreground mb-4" />
|
||||||
<h3 className="text-lg font-semibold mb-2">暂无部署环境</h3>
|
<h3 className="text-lg font-semibold mb-2">暂无部署环境</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
当前团队「{currentTeam.teamName}」还没有配置任何部署环境
|
当前团队「{deploymentData.currentTeam.teamName}」还没有配置任何部署环境
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Tabs value={currentEnvId?.toString()} onValueChange={(value) => setCurrentEnvId(Number(value))}>
|
<EnvironmentTabs
|
||||||
{/* 现代化 TAB 头部 - 独立于 Card */}
|
team={deploymentData.currentTeam}
|
||||||
<div className="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b">
|
currentEnvId={deploymentData.currentEnvId}
|
||||||
<div className="flex items-center justify-between px-6 pt-4 pb-2">
|
deploying={deploymentData.deploying}
|
||||||
<div className="flex items-center gap-3">
|
onEnvChange={deploymentData.setCurrentEnvId}
|
||||||
<div className="h-8 w-1 bg-primary rounded-full" />
|
onDeploy={handleDeploy}
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 待审批列表弹窗 */}
|
{/* 待审批列表弹窗 */}
|
||||||
<PendingApprovalModal
|
<PendingApprovalModal
|
||||||
open={approvalModalOpen}
|
open={approvalData.approvalModalOpen}
|
||||||
onOpenChange={setApprovalModalOpen}
|
onOpenChange={approvalData.setApprovalModalOpen}
|
||||||
workflowDefinitionKeys={workflowDefinitionKeys}
|
workflowDefinitionKeys={approvalData.workflowDefinitionKeys}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user