增加团队管理页面
This commit is contained in:
parent
2ab9ebff70
commit
19a1bc86ff
@ -49,12 +49,14 @@ const sheetVariants = cva(
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
VariantProps<typeof sheetVariants> {
|
||||
showClose?: boolean
|
||||
}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
>(({ side = "right", className, children, showClose = true, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
@ -63,10 +65,12 @@ const SheetContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showClose && (
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
|
||||
@ -163,12 +163,14 @@ const EnvironmentList: React.FC = () => {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<Table minWidth="950px">
|
||||
<Table minWidth="1150px">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead width="120px">环境编码</TableHead>
|
||||
<TableHead width="150px">环境名称</TableHead>
|
||||
<TableHead width="300px">环境描述</TableHead>
|
||||
<TableHead width="250px">环境描述</TableHead>
|
||||
<TableHead width="100px">团队数</TableHead>
|
||||
<TableHead width="100px">应用数</TableHead>
|
||||
<TableHead width="80px">排序</TableHead>
|
||||
<TableHead width="100px">状态</TableHead>
|
||||
<TableHead width="200px" sticky>操作</TableHead>
|
||||
@ -177,7 +179,7 @@ const EnvironmentList: React.FC = () => {
|
||||
<TableBody>
|
||||
{list.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-24 text-center">
|
||||
<TableCell colSpan={8} className="h-24 text-center">
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@ -187,7 +189,21 @@ const EnvironmentList: React.FC = () => {
|
||||
<TableRow key={item.id}>
|
||||
<TableCell width="120px" className="font-medium">{item.envCode}</TableCell>
|
||||
<TableCell width="150px">{item.envName}</TableCell>
|
||||
<TableCell width="300px" className="text-muted-foreground">{item.envDesc || '-'}</TableCell>
|
||||
<TableCell width="250px" className="text-muted-foreground">{item.envDesc || '-'}</TableCell>
|
||||
<TableCell width="100px">
|
||||
<div className="text-center">
|
||||
<Badge variant="outline" className="font-medium">
|
||||
{item.teamCount || 0}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell width="100px">
|
||||
<div className="text-center">
|
||||
<Badge variant="outline" className="font-medium">
|
||||
{item.applicationCount || 0}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell width="80px">{item.sort}</TableCell>
|
||||
<TableCell width="100px">
|
||||
<Badge variant={item.enabled ? "default" : "secondary"} className="inline-flex">
|
||||
|
||||
@ -7,6 +7,8 @@ export interface Environment extends BaseResponse {
|
||||
envName: string;
|
||||
envDesc?: string;
|
||||
sort: number;
|
||||
teamCount?: number;
|
||||
applicationCount?: number;
|
||||
}
|
||||
|
||||
// 创建环境请求参数
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
Clock, Activity, CheckCircle2, XCircle, FolderKanban, PlayCircle, FileText, BarChart3, List, Ban
|
||||
} from 'lucide-react';
|
||||
import {useToast} from '@/components/ui/use-toast';
|
||||
import {getScheduleJobs, getJobCategoryList, startJob, pauseJob, resumeJob, stopJob, triggerJob, disableJob, deleteScheduleJob, enableJob} from './service';
|
||||
import {getScheduleJobs, getJobCategoryList, pauseJob, resumeJob, triggerJob, disableJob, deleteScheduleJob, enableJob} from './service';
|
||||
import type {ScheduleJobResponse, ScheduleJobQuery, JobCategoryResponse, JobStatus} from './types';
|
||||
import type {Page} from '@/types/base';
|
||||
import {DEFAULT_PAGE_SIZE, DEFAULT_CURRENT} from '@/utils/page';
|
||||
@ -141,25 +141,6 @@ const ScheduleJobList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 启动任务
|
||||
const handleStart = async (record: ScheduleJobResponse) => {
|
||||
try {
|
||||
await startJob(record.id);
|
||||
toast({
|
||||
title: '启动成功',
|
||||
description: `任务 "${record.jobName}" 已启动`,
|
||||
});
|
||||
loadData();
|
||||
} catch (error) {
|
||||
console.error('启动失败:', error);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '启动失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 暂停任务
|
||||
const handlePause = async (record: ScheduleJobResponse) => {
|
||||
try {
|
||||
@ -198,25 +179,6 @@ const ScheduleJobList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 停止任务
|
||||
const handleStop = async (record: ScheduleJobResponse) => {
|
||||
try {
|
||||
await stopJob(record.id);
|
||||
toast({
|
||||
title: '停止成功',
|
||||
description: `任务 "${record.jobName}" 已停止`,
|
||||
});
|
||||
loadData();
|
||||
} catch (error) {
|
||||
console.error('停止失败:', error);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '停止失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 立即触发任务
|
||||
const handleTrigger = async (record: ScheduleJobResponse) => {
|
||||
try {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Server,
|
||||
Cpu,
|
||||
HardDrive,
|
||||
MemoryStick,
|
||||
@ -62,7 +61,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({
|
||||
server.status === 'OFFLINE' ? 'bg-red-500/10 text-red-700 border-red-500/30 dark:text-red-400' :
|
||||
'bg-gray-500/10 text-gray-700 border-gray-500/30 dark:text-gray-400'
|
||||
}`}>
|
||||
{ServerStatusLabels[server.status]?.label || server.status}
|
||||
{(server.status && ServerStatusLabels[server.status]?.label) || server.status || '-'}
|
||||
</Badge>
|
||||
</div>
|
||||
{server.categoryName && (
|
||||
@ -94,7 +93,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({
|
||||
)}
|
||||
{server.osType && (
|
||||
<div className="text-xs text-muted-foreground mt-0.5">
|
||||
{OsTypeLabels[server.osType]?.label || server.osType}
|
||||
{(server.osType && OsTypeLabels[server.osType]?.label) || server.osType || '-'}
|
||||
{server.osVersion && ` ${server.osVersion}`}
|
||||
</div>
|
||||
)}
|
||||
@ -175,7 +174,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
size="sm"
|
||||
onClick={() => onTest(server)}
|
||||
disabled={isTesting}
|
||||
className="h-7 px-2"
|
||||
@ -193,7 +192,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
size="sm"
|
||||
onClick={() => onEdit(server)}
|
||||
className="h-7 px-2"
|
||||
>
|
||||
@ -206,7 +205,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
size="sm"
|
||||
onClick={() => onDelete(server)}
|
||||
className="h-7 px-2 text-destructive hover:text-destructive"
|
||||
>
|
||||
|
||||
@ -40,7 +40,6 @@ interface ServerTableProps {
|
||||
|
||||
export const ServerTable: React.FC<ServerTableProps> = ({
|
||||
servers,
|
||||
loading,
|
||||
onTest,
|
||||
onEdit,
|
||||
onDelete,
|
||||
@ -106,14 +105,14 @@ export const ServerTable: React.FC<ServerTableProps> = ({
|
||||
server.status === 'OFFLINE' ? 'bg-red-500/10 text-red-700 border-red-500/30 dark:text-red-400' :
|
||||
'bg-gray-500/10 text-gray-700 border-gray-500/30 dark:text-gray-400'
|
||||
}>
|
||||
{ServerStatusLabels[server.status]?.label || server.status}
|
||||
{(server.status && ServerStatusLabels[server.status]?.label) || server.status || '-'}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{getOsIcon(server.osType)}
|
||||
<span className="text-sm">{OsTypeLabels[server.osType]?.label || server.osType}</span>
|
||||
<span className="text-sm">{(server.osType && OsTypeLabels[server.osType]?.label) || server.osType || '-'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
Grid3x3,
|
||||
List,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
@ -0,0 +1,957 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetFooter,
|
||||
} from '@/components/ui/sheet';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { Plus, Trash2, Loader2, AlertCircle } from 'lucide-react';
|
||||
import type {
|
||||
TeamConfig,
|
||||
TeamConfigRequest,
|
||||
TeamApplication,
|
||||
TeamApplicationRequest,
|
||||
Environment,
|
||||
Application,
|
||||
} from '../types';
|
||||
import type { UserResponse } from '@/pages/System/User/types';
|
||||
import {
|
||||
getTeamConfig,
|
||||
createTeamConfig,
|
||||
updateTeamConfig,
|
||||
getTeamApplications,
|
||||
createTeamApplication,
|
||||
deleteTeamApplication,
|
||||
} from '../service';
|
||||
|
||||
interface TeamConfigSheetProps {
|
||||
open: boolean;
|
||||
teamId: number;
|
||||
teamName: string;
|
||||
users: UserResponse[];
|
||||
environments: Environment[];
|
||||
applications: Application[];
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const TeamConfigSheet: React.FC<TeamConfigSheetProps> = ({
|
||||
open,
|
||||
teamId,
|
||||
teamName,
|
||||
users,
|
||||
environments,
|
||||
applications,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
// 状态管理
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [teamConfig, setTeamConfig] = useState<TeamConfig | null>(null);
|
||||
const [teamApplications, setTeamApplications] = useState<TeamApplication[]>([]);
|
||||
|
||||
// 步骤管理
|
||||
const [currentStep, setCurrentStep] = useState(1); // 1: 环境选择, 2: 环境配置, 3: 配置预览
|
||||
const [currentConfigEnvId, setCurrentConfigEnvId] = useState<number | null>(null); // 当前正在配置的环境ID
|
||||
|
||||
// 表单状态 - 改为以环境ID为key的对象结构
|
||||
const [selectedEnvs, setSelectedEnvs] = useState<number[]>([]);
|
||||
const [envConfigs, setEnvConfigs] = useState<Record<number, {
|
||||
approvalRequired: boolean;
|
||||
approverUserIds: number[];
|
||||
}>>({}); // 每个环境的审批配置
|
||||
const [configuredEnvs, setConfiguredEnvs] = useState<number[]>([]); // 已配置的环境列表
|
||||
|
||||
// 每个环境的添加表单状态
|
||||
const [addForms, setAddForms] = useState<Record<number, { appId: number | null; branch: string }>>({});
|
||||
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
if (open && teamId) {
|
||||
loadData();
|
||||
setCurrentStep(1); // 重置为第一步
|
||||
}
|
||||
}, [open, teamId]);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await Promise.all([loadTeamConfig(), loadTeamApplications()]);
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadTeamConfig = async () => {
|
||||
try {
|
||||
const config = await getTeamConfig(teamId);
|
||||
if (config) {
|
||||
setTeamConfig(config);
|
||||
setSelectedEnvs(config.allowedEnvironmentIds || []);
|
||||
|
||||
// 转换为新的数据结构
|
||||
const configs: Record<number, { approvalRequired: boolean; approverUserIds: number[] }> = {};
|
||||
config.allowedEnvironmentIds.forEach((envId, index) => {
|
||||
configs[envId] = {
|
||||
approvalRequired: config.environmentApprovalRequired[index] || false,
|
||||
approverUserIds: config.approverUserIds[index] || [],
|
||||
};
|
||||
});
|
||||
setEnvConfigs(configs);
|
||||
setConfiguredEnvs(config.allowedEnvironmentIds);
|
||||
} else {
|
||||
// 没有配置数据,重置为空
|
||||
setSelectedEnvs([]);
|
||||
setEnvConfigs({});
|
||||
setConfiguredEnvs([]);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 404 表示配置不存在,这是正常的,重置为空
|
||||
if (error.response?.status === 404) {
|
||||
setTeamConfig(null);
|
||||
setSelectedEnvs([]);
|
||||
setEnvConfigs({});
|
||||
setConfiguredEnvs([]);
|
||||
} else {
|
||||
console.error('加载团队配置失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadTeamApplications = async () => {
|
||||
try {
|
||||
const response = await getTeamApplications({ teamId, pageNum: 0, size: 1000 });
|
||||
if (response) {
|
||||
setTeamApplications(response.content || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载团队应用失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理环境选择
|
||||
const handleEnvToggle = (envId: number, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedEnvs([...selectedEnvs, envId]);
|
||||
// 初始化该环境的配置
|
||||
if (!envConfigs[envId]) {
|
||||
setEnvConfigs({
|
||||
...envConfigs,
|
||||
[envId]: { approvalRequired: false, approverUserIds: [] },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setSelectedEnvs(selectedEnvs.filter((id) => id !== envId));
|
||||
// 移除该环境的配置
|
||||
const newConfigs = { ...envConfigs };
|
||||
delete newConfigs[envId];
|
||||
setEnvConfigs(newConfigs);
|
||||
setConfiguredEnvs(configuredEnvs.filter((id) => id !== envId));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理当前环境的审批开关
|
||||
const handleCurrentEnvApprovalToggle = (checked: boolean) => {
|
||||
if (!currentConfigEnvId) return;
|
||||
|
||||
setEnvConfigs({
|
||||
...envConfigs,
|
||||
[currentConfigEnvId]: {
|
||||
...envConfigs[currentConfigEnvId],
|
||||
approvalRequired: checked,
|
||||
approverUserIds: checked ? envConfigs[currentConfigEnvId]?.approverUserIds || [] : [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 处理当前环境的审批人选择
|
||||
const handleCurrentEnvApproverToggle = (userId: number, checked: boolean) => {
|
||||
if (!currentConfigEnvId) return;
|
||||
|
||||
const currentApprovers = envConfigs[currentConfigEnvId]?.approverUserIds || [];
|
||||
const newApprovers = checked
|
||||
? [...currentApprovers, userId]
|
||||
: currentApprovers.filter((id) => id !== userId);
|
||||
|
||||
setEnvConfigs({
|
||||
...envConfigs,
|
||||
[currentConfigEnvId]: {
|
||||
...envConfigs[currentConfigEnvId],
|
||||
approverUserIds: newApprovers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 步骤导航
|
||||
const handleNext = () => {
|
||||
// Step 1: 环境选择 → Step 2: 环境配置 或 直接保存
|
||||
if (currentStep === 1) {
|
||||
if (selectedEnvs.length === 0) {
|
||||
// 没有选择环境,直接保存空配置
|
||||
handleSaveConfig();
|
||||
return;
|
||||
}
|
||||
// 进入 Step 2,默认选择第一个环境
|
||||
setCurrentConfigEnvId(selectedEnvs[0]);
|
||||
setCurrentStep(2);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: 环境配置 → Step 3: 配置预览
|
||||
if (currentStep === 2) {
|
||||
// 校验当前环境的配置
|
||||
if (currentConfigEnvId) {
|
||||
const config = envConfigs[currentConfigEnvId];
|
||||
if (config?.approvalRequired && (!config.approverUserIds || config.approverUserIds.length === 0)) {
|
||||
const env = environments.find((e) => e.id === currentConfigEnvId);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: `环境"${env?.envName}"需要审批,请至少选择一个审批人`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记当前环境已配置
|
||||
if (!configuredEnvs.includes(currentConfigEnvId)) {
|
||||
setConfiguredEnvs([...configuredEnvs, currentConfigEnvId]);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否所有环境都已配置
|
||||
const unconfiguredEnvs = selectedEnvs.filter((id) => !configuredEnvs.includes(id) && id !== currentConfigEnvId);
|
||||
|
||||
if (unconfiguredEnvs.length > 0) {
|
||||
// 还有未配置的环境,切换到下一个
|
||||
setCurrentConfigEnvId(unconfiguredEnvs[0]);
|
||||
toast({
|
||||
title: '当前环境配置已保存',
|
||||
description: `请继续配置 "${environments.find((e) => e.id === unconfiguredEnvs[0])?.envName}"`,
|
||||
});
|
||||
} else {
|
||||
// 所有环境都配置完了,进入预览
|
||||
setCurrentStep(3);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换配置的环境
|
||||
const handleSwitchConfigEnv = (envId: number) => {
|
||||
// 先保存当前环境的配置状态
|
||||
if (currentConfigEnvId && !configuredEnvs.includes(currentConfigEnvId)) {
|
||||
const config = envConfigs[currentConfigEnvId];
|
||||
// 如果已经有配置内容,标记为已配置
|
||||
if (config && (config.approvalRequired || (envApps(currentConfigEnvId).length > 0))) {
|
||||
setConfiguredEnvs([...configuredEnvs, currentConfigEnvId]);
|
||||
}
|
||||
}
|
||||
setCurrentConfigEnvId(envId);
|
||||
};
|
||||
|
||||
// 跳过配置(直接进入预览)
|
||||
const handleSkipToPreview = () => {
|
||||
if (currentConfigEnvId && !configuredEnvs.includes(currentConfigEnvId)) {
|
||||
setConfiguredEnvs([...configuredEnvs, currentConfigEnvId]);
|
||||
}
|
||||
setCurrentStep(3);
|
||||
};
|
||||
|
||||
// 保存最终配置
|
||||
const handleSaveConfig = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// 如果没有选择任何环境,清空所有配置
|
||||
if (selectedEnvs.length === 0) {
|
||||
const configData: TeamConfigRequest = {
|
||||
teamId,
|
||||
allowedEnvironmentIds: [],
|
||||
environmentApprovalRequired: [],
|
||||
approverUserIds: [],
|
||||
};
|
||||
|
||||
if (teamConfig) {
|
||||
await updateTeamConfig(teamConfig.id, configData);
|
||||
} else {
|
||||
const newConfig = await createTeamConfig(configData);
|
||||
setTeamConfig(newConfig);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: '保存成功',
|
||||
description: '已清空团队配置',
|
||||
});
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 只保存已勾选环境的配置
|
||||
const allowedEnvironmentIds = selectedEnvs;
|
||||
const environmentApprovalRequired = selectedEnvs.map(
|
||||
(envId) => envConfigs[envId]?.approvalRequired || false
|
||||
);
|
||||
const approverUserIds = selectedEnvs.map(
|
||||
(envId) => envConfigs[envId]?.approverUserIds || null
|
||||
);
|
||||
|
||||
const configData: TeamConfigRequest = {
|
||||
teamId,
|
||||
allowedEnvironmentIds,
|
||||
environmentApprovalRequired,
|
||||
approverUserIds,
|
||||
};
|
||||
|
||||
if (teamConfig) {
|
||||
// 更新
|
||||
await updateTeamConfig(teamConfig.id, configData);
|
||||
} else {
|
||||
// 创建
|
||||
const newConfig = await createTeamConfig(configData);
|
||||
setTeamConfig(newConfig);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: '保存成功',
|
||||
description: '团队配置已保存',
|
||||
});
|
||||
onSuccess();
|
||||
onOpenChange(false); // 关闭抽屉
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '保存失败',
|
||||
description: error.response?.data?.message || '保存配置失败',
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取某个环境的应用列表(辅助函数)
|
||||
const envApps = (envId: number) => {
|
||||
return teamApplications.filter((app) => app.environmentId === envId);
|
||||
};
|
||||
|
||||
// 添加应用到环境
|
||||
const handleAddApplication = async (envId: number) => {
|
||||
const form = addForms[envId];
|
||||
if (!form?.appId) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '请选择应用',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
const exists = teamApplications.some(
|
||||
(app) => app.environmentId === envId && app.applicationId === form.appId
|
||||
);
|
||||
if (exists) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '该应用已添加到此环境',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data: TeamApplicationRequest = {
|
||||
teamId,
|
||||
applicationId: form.appId,
|
||||
environmentId: envId,
|
||||
branch: form.branch || undefined,
|
||||
};
|
||||
|
||||
await createTeamApplication(data);
|
||||
|
||||
toast({
|
||||
title: '添加成功',
|
||||
});
|
||||
|
||||
// 清空表单
|
||||
setAddForms({
|
||||
...addForms,
|
||||
[envId]: { appId: null, branch: '' },
|
||||
});
|
||||
|
||||
// 重新加载
|
||||
loadTeamApplications();
|
||||
onSuccess();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '添加失败',
|
||||
description: error.response?.data?.message || '添加应用失败',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 删除应用
|
||||
const handleDeleteApplication = async (id: number) => {
|
||||
try {
|
||||
await deleteTeamApplication(id);
|
||||
|
||||
toast({
|
||||
title: '删除成功',
|
||||
});
|
||||
|
||||
// 重新加载
|
||||
loadTeamApplications();
|
||||
onSuccess();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '删除失败',
|
||||
description: error.response?.data?.message || '删除应用失败',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取某个环境下的应用列表
|
||||
const getEnvApplications = (envId: number) => {
|
||||
return teamApplications.filter((app) => app.environmentId === envId);
|
||||
};
|
||||
|
||||
|
||||
// 获取环境图标
|
||||
const getEnvIcon = (envCode?: string) => {
|
||||
const code = envCode?.toUpperCase();
|
||||
if (code?.includes('DEV')) return '🟢';
|
||||
if (code?.includes('TEST') || code?.includes('QA')) return '🟡';
|
||||
if (code?.includes('UAT') || code?.includes('PRE')) return '🟠';
|
||||
if (code?.includes('PROD')) return '🔴';
|
||||
return '⚪';
|
||||
};
|
||||
|
||||
// 获取可添加的应用列表(排除已添加的)
|
||||
const getAvailableApplications = (envId: number) => {
|
||||
const addedAppIds = getEnvApplications(envId).map((app) => app.applicationId);
|
||||
return applications.filter((app) => !addedAppIds.includes(app.id));
|
||||
};
|
||||
|
||||
// 渲染步骤指示器
|
||||
const renderStepIndicator = () => (
|
||||
<div className="flex items-center justify-center gap-4 py-4 border-b">
|
||||
{[
|
||||
{ step: 1, label: '环境选择' },
|
||||
{ step: 2, label: '环境配置' },
|
||||
{ step: 3, label: '配置预览' },
|
||||
].map((item, index) => (
|
||||
<React.Fragment key={item.step}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium ${
|
||||
currentStep === item.step
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: currentStep > item.step
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{item.step}
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
currentStep === item.step
|
||||
? 'text-foreground'
|
||||
: currentStep > item.step
|
||||
? 'text-primary'
|
||||
: 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
{index < 2 && (
|
||||
<div
|
||||
className={`h-[2px] w-12 ${
|
||||
currentStep > item.step ? 'bg-primary' : 'bg-muted'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Sheet open={open}>
|
||||
<SheetContent side="right" className="w-[700px] sm:max-w-[700px]" showClose={false}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>团队配置:{teamName}</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-[400px]">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 步骤指示器 */}
|
||||
{renderStepIndicator()}
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-240px)] mt-6 pr-4">
|
||||
<div className="space-y-6">
|
||||
{/* Step 1: 环境选择 */}
|
||||
{currentStep === 1 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">选择可访问的环境</CardTitle>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
选择该团队可以访问哪些环境
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{environments.length === 0 ? (
|
||||
<div className="p-8 border border-dashed rounded-md text-center">
|
||||
<AlertCircle className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
||||
<p className="text-sm text-muted-foreground">暂无可用环境</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
请先前往"环境管理"创建环境
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{environments.map((env) => (
|
||||
<div
|
||||
key={env.id}
|
||||
className={`flex items-center space-x-3 p-4 border rounded-lg cursor-pointer transition-colors ${
|
||||
selectedEnvs.includes(env.id)
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-primary/50'
|
||||
}`}
|
||||
onClick={() => handleEnvToggle(env.id, !selectedEnvs.includes(env.id))}
|
||||
>
|
||||
<Checkbox
|
||||
id={`env-${env.id}`}
|
||||
checked={selectedEnvs.includes(env.id)}
|
||||
onCheckedChange={(checked) =>
|
||||
handleEnvToggle(env.id, checked as boolean)
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{getEnvIcon(env.envCode)}</span>
|
||||
<label
|
||||
htmlFor={`env-${env.id}`}
|
||||
className="font-medium cursor-pointer"
|
||||
>
|
||||
{env.envName}
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{env.envCode}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{selectedEnvs.length > 0 ? (
|
||||
<div className="mt-4 p-3 bg-muted/50 rounded-md">
|
||||
<p className="text-sm">
|
||||
已选择 <span className="font-medium text-primary">{selectedEnvs.length}</span> 个环境
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 p-3 bg-yellow-50 dark:bg-yellow-950/20 border border-yellow-200 dark:border-yellow-800 rounded-md">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
💡 未选择任何环境,点击"下一步"将清空该团队的环境配置
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Step 2: 环境配置(侧边导航式) */}
|
||||
{currentStep === 2 && currentConfigEnvId && (
|
||||
<div className="grid grid-cols-[200px_1fr] gap-4">
|
||||
{/* 左侧:环境列表 */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm">环境列表</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{selectedEnvs.map((envId) => {
|
||||
const env = environments.find((e) => e.id === envId);
|
||||
if (!env) return null;
|
||||
const isConfigured = configuredEnvs.includes(envId);
|
||||
const isCurrent = envId === currentConfigEnvId;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={envId}
|
||||
className={`flex items-center gap-2 p-2 rounded-md cursor-pointer transition-colors ${
|
||||
isCurrent
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-muted'
|
||||
}`}
|
||||
onClick={() => handleSwitchConfigEnv(envId)}
|
||||
>
|
||||
<span className="text-sm">{getEnvIcon(env.envCode)}</span>
|
||||
<span className="flex-1 text-sm font-medium truncate">
|
||||
{env.envName}
|
||||
</span>
|
||||
{isConfigured && !isCurrent && (
|
||||
<span className="text-xs text-primary">✓</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 右侧:当前环境配置 */}
|
||||
<div className="space-y-4">
|
||||
{(() => {
|
||||
const currentEnv = environments.find((e) => e.id === currentConfigEnvId);
|
||||
const currentConfig = envConfigs[currentConfigEnvId] || { approvalRequired: false, approverUserIds: [] };
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 环境审批配置 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{getEnvIcon(currentEnv?.envCode)}</span>
|
||||
<CardTitle className="text-base">{currentEnv?.envName}</CardTitle>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
配置审批规则和应用分支
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* 审批开关 */}
|
||||
<div className="flex items-center justify-between p-3 border rounded-md">
|
||||
<Label className="text-sm">需要审批</Label>
|
||||
<Checkbox
|
||||
checked={currentConfig.approvalRequired}
|
||||
onCheckedChange={handleCurrentEnvApprovalToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 审批人选择 */}
|
||||
{currentConfig.approvalRequired && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm">审批人(至少选择一个)</Label>
|
||||
<div className="grid grid-cols-2 gap-2 max-h-[200px] overflow-y-auto p-3 border rounded-md bg-muted/30">
|
||||
{users.map((user) => (
|
||||
<div key={user.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`approver-${user.id}`}
|
||||
checked={currentConfig.approverUserIds.includes(user.id)}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCurrentEnvApproverToggle(user.id, checked as boolean)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`approver-${user.id}`}
|
||||
className="text-xs cursor-pointer"
|
||||
>
|
||||
{user.nickname || user.username}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 应用分支配置 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">应用分支</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{applications.length === 0 ? (
|
||||
<div className="p-4 border border-dashed rounded-md text-center">
|
||||
<AlertCircle className="h-6 w-6 mx-auto text-muted-foreground mb-2" />
|
||||
<p className="text-sm text-muted-foreground">暂无可用应用</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>应用名称</TableHead>
|
||||
<TableHead>分支</TableHead>
|
||||
<TableHead className="w-[80px] text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{envApps(currentConfigEnvId).map((app) => (
|
||||
<TableRow key={app.id}>
|
||||
<TableCell className="font-medium">
|
||||
{app.applicationName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs bg-muted px-2 py-1 rounded">
|
||||
{app.branch || '-'}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteApplication(app.id)}
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
{/* 添加行 */}
|
||||
{getAvailableApplications(currentConfigEnvId).length > 0 && (
|
||||
<TableRow className="bg-muted/20">
|
||||
<TableCell>
|
||||
<Select
|
||||
value={addForms[currentConfigEnvId]?.appId?.toString() || ''}
|
||||
onValueChange={(value) => {
|
||||
setAddForms({
|
||||
...addForms,
|
||||
[currentConfigEnvId]: {
|
||||
...addForms[currentConfigEnvId],
|
||||
appId: Number(value),
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue placeholder="选择应用" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getAvailableApplications(currentConfigEnvId).map((app) => (
|
||||
<SelectItem key={app.id} value={app.id.toString()}>
|
||||
{app.appName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
placeholder="分支名"
|
||||
className="h-8"
|
||||
value={addForms[currentConfigEnvId]?.branch || ''}
|
||||
onChange={(e) => {
|
||||
setAddForms({
|
||||
...addForms,
|
||||
[currentConfigEnvId]: {
|
||||
...addForms[currentConfigEnvId],
|
||||
branch: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={() => handleAddApplication(currentConfigEnvId)}
|
||||
disabled={!addForms[currentConfigEnvId]?.appId}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{envApps(currentConfigEnvId).length === 0 && getAvailableApplications(currentConfigEnvId).length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center text-sm text-muted-foreground py-4">
|
||||
所有应用已添加
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: 配置预览 */}
|
||||
{currentStep === 3 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">配置预览</CardTitle>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
请检查所有配置,确认无误后点击"完成配置"
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{selectedEnvs.map((envId) => {
|
||||
const env = environments.find((e) => e.id === envId);
|
||||
if (!env) return null;
|
||||
const config = envConfigs[envId] || { approvalRequired: false, approverUserIds: [] };
|
||||
const apps = envApps(envId);
|
||||
|
||||
return (
|
||||
<Card key={envId} className="border-muted">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{getEnvIcon(env.envCode)}</span>
|
||||
<CardTitle className="text-base">{env.envName}</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setCurrentConfigEnvId(envId);
|
||||
setCurrentStep(2);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* 审批配置信息 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">审批配置</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
{config.approvalRequired ? (
|
||||
<>
|
||||
<Badge variant="default" className="text-xs">需要审批</Badge>
|
||||
{config.approverUserIds.length > 0 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
审批人:
|
||||
{config.approverUserIds
|
||||
.map((userId) => {
|
||||
const user = users.find((u) => u.id === userId);
|
||||
return user?.nickname || user?.username;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('、')}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Badge variant="secondary" className="text-xs">无需审批</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 应用配置信息 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">应用配置</Label>
|
||||
{apps.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{apps.map((app) => (
|
||||
<div key={app.id} className="flex items-center justify-between text-xs p-2 bg-muted/30 rounded">
|
||||
<span>{app.applicationName}</span>
|
||||
<code className="text-xs bg-background px-2 py-0.5 rounded">
|
||||
{app.branch || '-'}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">暂无应用配置</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* 底部导航按钮 */}
|
||||
<SheetFooter className="mt-4 flex justify-between items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handlePrev}
|
||||
disabled={currentStep === 1 || saving}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{currentStep === 2 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSkipToPreview}
|
||||
disabled={saving}
|
||||
>
|
||||
跳过预览
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < 3 ? (
|
||||
<Button onClick={handleNext}>
|
||||
{currentStep === 1 && selectedEnvs.length === 0
|
||||
? '保存(清空配置)'
|
||||
: currentStep === 2 && selectedEnvs.filter((id) => !configuredEnvs.includes(id) && id !== currentConfigEnvId).length > 0
|
||||
? '配置下一个环境'
|
||||
: '下一步'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleSaveConfig} disabled={saving}>
|
||||
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
完成配置
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamConfigSheet;
|
||||
|
||||
@ -5,11 +5,11 @@ import type { TeamResponse, TeamQuery } from './types';
|
||||
import TeamModal from './components/TeamModal';
|
||||
import DeleteDialog from './components/DeleteDialog';
|
||||
import MemberManageDialog from './components/MemberManageDialog';
|
||||
import ApplicationManageDialog from './components/ApplicationManageDialog';
|
||||
import { getUserList } from '@/pages/System/User/service';
|
||||
import type { UserResponse } from '@/pages/System/User/types';
|
||||
import { getApplicationList } from '@/pages/Deploy/Application/List/service';
|
||||
import type { Application } from '@/pages/Deploy/Application/List/types';
|
||||
import { getEnvironmentList, getApplicationList } from './service';
|
||||
import type { Environment, Application } from './types';
|
||||
import TeamConfigSheet from './components/TeamConfigSheet';
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
@ -50,7 +50,7 @@ import {
|
||||
Activity,
|
||||
Server,
|
||||
Database,
|
||||
Package,
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
|
||||
type Column = {
|
||||
@ -73,8 +73,9 @@ const TeamList: React.FC = () => {
|
||||
const [currentTeam, setCurrentTeam] = useState<TeamResponse | undefined>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [memberDialogOpen, setMemberDialogOpen] = useState(false);
|
||||
const [applicationDialogOpen, setApplicationDialogOpen] = useState(false);
|
||||
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
||||
const [users, setUsers] = useState<UserResponse[]>([]);
|
||||
const [environments, setEnvironments] = useState<Environment[]>([]);
|
||||
const [applications, setApplications] = useState<Application[]>([]);
|
||||
|
||||
const form = useForm<SearchFormValues>({
|
||||
@ -119,9 +120,10 @@ const TeamList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 加载用户列表和应用列表(仅一次)
|
||||
// 加载基础数据(仅一次)
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
loadEnvironments();
|
||||
loadApplications();
|
||||
}, []);
|
||||
|
||||
@ -136,6 +138,17 @@ const TeamList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadEnvironments = async () => {
|
||||
try {
|
||||
const response = await getEnvironmentList();
|
||||
if (response) {
|
||||
setEnvironments(response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载环境列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadApplications = async () => {
|
||||
try {
|
||||
const response = await getApplicationList();
|
||||
@ -175,9 +188,9 @@ const TeamList: React.FC = () => {
|
||||
setMemberDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleManageApplications = (record: TeamResponse) => {
|
||||
const handleConfig = (record: TeamResponse) => {
|
||||
setCurrentTeam(record);
|
||||
setApplicationDialogOpen(true);
|
||||
setConfigDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
@ -214,7 +227,7 @@ const TeamList: React.FC = () => {
|
||||
{
|
||||
id: 'memberCount',
|
||||
header: '成员数量',
|
||||
size: 120,
|
||||
size: 100,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center">
|
||||
<span className="font-medium">{row.original.memberCount || 0}</span>
|
||||
@ -222,12 +235,26 @@ const TeamList: React.FC = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'applicationCount',
|
||||
header: '应用数量',
|
||||
size: 120,
|
||||
id: 'environmentCount',
|
||||
header: '环境数量',
|
||||
size: 100,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center">
|
||||
<span className="font-medium">{row.original.applicationCount || 0}</span>
|
||||
<Badge variant="outline" className="font-medium">
|
||||
{row.original.environmentCount || 0}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'applicationCount',
|
||||
header: '应用数量',
|
||||
size: 100,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center">
|
||||
<Badge variant="outline" className="font-medium">
|
||||
{row.original.applicationCount || 0}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@ -256,9 +283,9 @@ const TeamList: React.FC = () => {
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
成员
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleManageApplications(row.original)}>
|
||||
<Package className="h-4 w-4 mr-1" />
|
||||
应用
|
||||
<Button variant="ghost" size="sm" onClick={() => handleConfig(row.original)}>
|
||||
<Settings className="h-4 w-4 mr-1" />
|
||||
配置
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(row.original)}>
|
||||
<Edit className="h-4 w-4 mr-1" />
|
||||
@ -368,7 +395,7 @@ const TeamList: React.FC = () => {
|
||||
|
||||
{/* 数据表格 */}
|
||||
<div className="rounded-md border">
|
||||
<Table minWidth="1460px">
|
||||
<Table minWidth="1440px">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
@ -455,12 +482,14 @@ const TeamList: React.FC = () => {
|
||||
onOpenChange={setMemberDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
<ApplicationManageDialog
|
||||
open={applicationDialogOpen}
|
||||
<TeamConfigSheet
|
||||
open={configDialogOpen}
|
||||
teamId={currentTeam.id}
|
||||
teamName={currentTeam.teamName}
|
||||
users={users}
|
||||
environments={environments}
|
||||
applications={applications}
|
||||
onOpenChange={setApplicationDialogOpen}
|
||||
onOpenChange={setConfigDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -4,9 +4,15 @@ import type {
|
||||
TeamQuery,
|
||||
TeamResponse,
|
||||
TeamRequest,
|
||||
TeamConfig,
|
||||
TeamConfigRequest,
|
||||
TeamApplication,
|
||||
TeamApplicationRequest,
|
||||
} from './types';
|
||||
|
||||
const BASE_URL = '/api/v1/teams';
|
||||
const TEAM_CONFIG_URL = '/api/v1/team-configs';
|
||||
const TEAM_APPLICATION_URL = '/api/v1/team-applications';
|
||||
|
||||
/**
|
||||
* 分页查询团队
|
||||
@ -44,3 +50,61 @@ export const updateTeam = (id: number, data: TeamRequest) =>
|
||||
export const deleteTeam = (id: number) =>
|
||||
request.delete(`${BASE_URL}/${id}`);
|
||||
|
||||
// ==================== 环境和应用(复用已有接口) ====================
|
||||
|
||||
/**
|
||||
* 获取环境列表
|
||||
*/
|
||||
export { getEnvironmentList } from '@/pages/Deploy/Environment/List/service';
|
||||
|
||||
/**
|
||||
* 获取应用列表
|
||||
*/
|
||||
export { getApplicationListByCondition as getApplicationList } from '@/pages/Deploy/Application/List/service';
|
||||
|
||||
// ==================== 团队配置相关 ====================
|
||||
|
||||
/**
|
||||
* 获取团队配置
|
||||
*/
|
||||
export const getTeamConfig = (teamId: number) =>
|
||||
request.get<TeamConfig>(`${TEAM_CONFIG_URL}/team/${teamId}`);
|
||||
|
||||
/**
|
||||
* 创建团队配置
|
||||
*/
|
||||
export const createTeamConfig = (data: TeamConfigRequest) =>
|
||||
request.post<TeamConfig>(TEAM_CONFIG_URL, data);
|
||||
|
||||
/**
|
||||
* 更新团队配置
|
||||
*/
|
||||
export const updateTeamConfig = (id: number, data: TeamConfigRequest) =>
|
||||
request.put<TeamConfig>(`${TEAM_CONFIG_URL}/${id}`, data);
|
||||
|
||||
// ==================== 团队应用关联相关 ====================
|
||||
|
||||
/**
|
||||
* 获取团队应用关联列表(分页)
|
||||
*/
|
||||
export const getTeamApplications = (params?: { teamId?: number; environmentId?: number; pageNum?: number; size?: number }) =>
|
||||
request.get<Page<TeamApplication>>(`${TEAM_APPLICATION_URL}/page`, { params });
|
||||
|
||||
/**
|
||||
* 创建团队应用关联
|
||||
*/
|
||||
export const createTeamApplication = (data: TeamApplicationRequest) =>
|
||||
request.post<TeamApplication>(TEAM_APPLICATION_URL, data);
|
||||
|
||||
/**
|
||||
* 更新团队应用关联
|
||||
*/
|
||||
export const updateTeamApplication = (id: number, data: TeamApplicationRequest) =>
|
||||
request.put<TeamApplication>(`${TEAM_APPLICATION_URL}/${id}`, data);
|
||||
|
||||
/**
|
||||
* 删除团队应用关联
|
||||
*/
|
||||
export const deleteTeamApplication = (id: number) =>
|
||||
request.delete(`${TEAM_APPLICATION_URL}/${id}`);
|
||||
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import type { BaseQuery, BaseResponse } from '@/types/base';
|
||||
import type { Environment } from '@/pages/Deploy/Environment/List/types';
|
||||
import type { Application } from '@/pages/Deploy/Application/List/types';
|
||||
|
||||
// 导出环境和应用类型供使用
|
||||
export type { Environment, Application };
|
||||
|
||||
/**
|
||||
* 团队查询参数
|
||||
@ -21,6 +26,7 @@ export interface TeamResponse extends BaseResponse {
|
||||
enabled: boolean;
|
||||
sort: number;
|
||||
memberCount?: number;
|
||||
environmentCount?: number;
|
||||
applicationCount?: number;
|
||||
}
|
||||
|
||||
@ -37,3 +43,51 @@ export interface TeamRequest {
|
||||
sort?: number;
|
||||
}
|
||||
|
||||
// ==================== 团队配置相关 ====================
|
||||
|
||||
/**
|
||||
* 团队配置响应
|
||||
*/
|
||||
export interface TeamConfig extends BaseResponse {
|
||||
teamId: number;
|
||||
allowedEnvironmentIds: number[]; // [1, 2, 3]
|
||||
environmentApprovalRequired: boolean[]; // [false, false, true]
|
||||
approverUserIds: (number[] | null)[]; // [null, null, [1, 4]]
|
||||
}
|
||||
|
||||
/**
|
||||
* 团队配置请求
|
||||
*/
|
||||
export interface TeamConfigRequest {
|
||||
teamId: number;
|
||||
allowedEnvironmentIds: number[]; // [1, 2, 3]
|
||||
environmentApprovalRequired: boolean[]; // [false, false, true]
|
||||
approverUserIds: (number[] | null)[]; // [null, null, [1, 4]]
|
||||
}
|
||||
|
||||
// ==================== 团队应用关联相关 ====================
|
||||
|
||||
/**
|
||||
* 团队应用关联响应
|
||||
*/
|
||||
export interface TeamApplication extends BaseResponse {
|
||||
teamId: number;
|
||||
applicationId: number;
|
||||
environmentId: number;
|
||||
branch?: string;
|
||||
teamName?: string;
|
||||
applicationName?: string;
|
||||
applicationCode?: string;
|
||||
environmentName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 团队应用关联请求
|
||||
*/
|
||||
export interface TeamApplicationRequest {
|
||||
teamId: number;
|
||||
applicationId: number;
|
||||
environmentId: number;
|
||||
branch?: string;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user