import { Button, Card, Descriptions, Dropdown, Empty, Form, Input, List, Menu, Message, Modal, Select, Space, Switch, Tabs, Tag, Typography, } from '@arco-design/web-react'; import { IconCode, IconCommand, IconCopy, IconDelete, IconEdit, IconFolder, IconHistory, IconMore, IconPlayArrow, IconPlus, IconRefresh, IconSettings, } from '@arco-design/web-react/icon'; import type { DragEndEvent } from '@dnd-kit/core'; import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; import { useAsyncEffect } from '../../../hooks/useAsyncEffect'; import { formatDateTime } from '../../../utils/time'; import type { Deployment, Pipeline, Project, Step } from '../types'; import DeployModal from './components/DeployModal'; import DeployRecordItem from './components/DeployRecordItem'; import EnvPresetsEditor, { type EnvPreset, } from './components/EnvPresetsEditor'; import PipelineStepItem from './components/PipelineStepItem'; import { detailService } from './service'; interface StepWithEnabled extends Step { enabled: boolean; } interface PipelineWithEnabled extends Pipeline { steps?: StepWithEnabled[]; enabled: boolean; } function ProjectDetailPage() { const [detail, setDetail] = useState(); const navigate = useNavigate(); // 拖拽传感器配置 const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const [selectedRecordId, setSelectedRecordId] = useState(1); const [pipelines, setPipelines] = useState([]); const [editModalVisible, setEditModalVisible] = useState(false); const [selectedPipelineId, setSelectedPipelineId] = useState(0); const [editingStep, setEditingStep] = useState(null); const [editingPipelineId, setEditingPipelineId] = useState( null, ); const [pipelineModalVisible, setPipelineModalVisible] = useState(false); const [editingPipeline, setEditingPipeline] = useState(null); const [form] = Form.useForm(); const [pipelineForm] = Form.useForm(); const [deployRecords, setDeployRecords] = useState([]); const [deployModalVisible, setDeployModalVisible] = useState(false); // 流水线模板相关状态 const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false); const [selectedTemplateId, setSelectedTemplateId] = useState( null, ); const [templates, setTemplates] = useState< Array<{ id: number; name: string; description: string }> >([]); // 项目设置相关状态 const [isEditingProject, setIsEditingProject] = useState(false); const [projectForm] = Form.useForm(); const [envPresets, setEnvPresets] = useState([]); const [envPresetsLoading, setEnvPresetsLoading] = useState(false); const { id } = useParams(); // 获取可用的流水线模板 useAsyncEffect(async () => { try { const templateData = await detailService.getPipelineTemplates(); setTemplates(templateData); } catch (error) { console.error('获取流水线模板失败:', error); Message.error('获取流水线模板失败'); } }, []); useAsyncEffect(async () => { if (id) { const project = await detailService.getProject(id); setDetail(project); // 获取项目的所有流水线 try { const pipelineData = await detailService.getPipelines(Number(id)); // 转换数据结构,添加enabled字段 const transformedPipelines = pipelineData.map((pipeline) => ({ ...pipeline, description: pipeline.description || '', // 确保description不为undefined enabled: pipeline.valid === 1, // 根据valid字段设置enabled steps: pipeline.steps?.map((step) => ({ ...step, enabled: step.valid === 1, // 根据valid字段设置enabled })) || [], })); setPipelines(transformedPipelines); if (transformedPipelines.length > 0) { setSelectedPipelineId(transformedPipelines[0].id); } } catch (error) { console.error('获取流水线数据失败:', error); Message.error('获取流水线数据失败'); } // 获取部署记录 try { const records = await detailService.getDeployments(Number(id)); setDeployRecords(records); if (records.length > 0) { setSelectedRecordId(records[0].id); } } catch (error) { console.error('获取部署记录失败:', error); Message.error('获取部署记录失败'); } } }, []); // 获取构建日志 const getBuildLogs = (recordId: number): string[] => { const record = deployRecords.find((r) => r.id === recordId); if (!record || !record.buildLog) { return ['暂无日志记录']; } return record.buildLog.split('\n'); }; // 定期轮询部署记录以更新状态和日志 useAsyncEffect(async () => { const interval = setInterval(async () => { if (id) { try { const records = await detailService.getDeployments(Number(id)); setDeployRecords(records); // 如果当前选中的记录正在运行,则更新选中记录 const selectedRecord = records.find( (r: Deployment) => r.id === selectedRecordId, ); if ( selectedRecord && (selectedRecord.status === 'running' || selectedRecord.status === 'pending') ) { // 保持当前选中状态,但更新数据 } } catch (error) { console.error('轮询部署记录失败:', error); } } }, 3000); // 每3秒轮询一次 return () => clearInterval(interval); }, [id, selectedRecordId]); // 触发部署 const handleDeploy = () => { setDeployModalVisible(true); }; // 添加新流水线 const handleAddPipeline = () => { setEditingPipeline(null); pipelineForm.resetFields(); setPipelineModalVisible(true); setIsCreatingFromTemplate(false); // 默认不是从模板创建 setSelectedTemplateId(null); }; // 编辑流水线 const handleEditPipeline = (pipeline: PipelineWithEnabled) => { setEditingPipeline(pipeline); pipelineForm.setFieldsValue({ name: pipeline.name, description: pipeline.description, }); setPipelineModalVisible(true); }; // 删除流水线 const handleDeletePipeline = async (pipelineId: number) => { Modal.confirm({ title: '确认删除', content: '确定要删除这个流水线吗?此操作不可撤销,将同时删除该流水线下的所有步骤。', onOk: async () => { try { // 从数据库删除流水线 await detailService.deletePipeline(pipelineId); // 更新本地状态 setPipelines((prev) => { const newPipelines = prev.filter( (pipeline) => pipeline.id !== pipelineId, ); // 如果删除的是当前选中的流水线,选中第一个或清空选择 if (selectedPipelineId === pipelineId) { setSelectedPipelineId( newPipelines.length > 0 ? newPipelines[0].id : 0, ); } return newPipelines; }); Message.success('流水线删除成功'); } catch (error) { console.error('删除流水线失败:', error); Message.error('删除流水线失败'); } }, }); }; // 复制流水线 const handleCopyPipeline = async (pipeline: PipelineWithEnabled) => { Modal.confirm({ title: '确认复制', content: '确定要复制这个流水线吗?', onOk: async () => { try { // 创建新的流水线 const newPipelineData = await detailService.createPipeline({ name: `${pipeline.name} - 副本`, description: pipeline.description || '', projectId: pipeline.projectId, }); // 复制步骤 if (pipeline.steps && pipeline.steps.length > 0) { for (const step of pipeline.steps) { await detailService.createStep({ name: step.name, description: step.description, order: step.order, script: step.script, pipelineId: newPipelineData.id, }); } // 重新获取流水线数据以确保步骤已创建 if (pipeline.projectId) { const pipelineData = await detailService.getPipelines( pipeline.projectId, ); // 转换数据结构,添加enabled字段 const transformedPipelines = pipelineData.map((p) => ({ ...p, description: p.description || '', // 确保description不为undefined enabled: p.valid === 1, // 根据valid字段设置enabled steps: p.steps?.map((step) => ({ ...step, enabled: step.valid === 1, // 根据valid字段设置enabled })) || [], })); setPipelines(transformedPipelines); setSelectedPipelineId(newPipelineData.id); } } else { // 如果没有步骤,直接更新状态 setPipelines((prev) => [ ...prev, { ...newPipelineData, description: newPipelineData.description || '', enabled: newPipelineData.valid === 1, steps: [], }, ]); setSelectedPipelineId(newPipelineData.id); } Message.success('流水线复制成功'); } catch (error) { console.error('复制流水线失败:', error); Message.error('复制流水线失败'); } }, }); }; // 切换流水线启用状态 const handleTogglePipeline = async (pipelineId: number, enabled: boolean) => { // 在数据库中更新流水线状态(这里简化处理,实际可能需要添加enabled字段到数据库) setPipelines((prev) => prev.map((pipeline) => pipeline.id === pipelineId ? { ...pipeline, enabled } : pipeline, ), ); }; // 保存流水线 const handleSavePipeline = async () => { try { const values = await pipelineForm.validate(); if (editingPipeline) { // 更新现有流水线 const updatedPipeline = await detailService.updatePipeline( editingPipeline.id, { name: values.name, description: values.description, }, ); // 更新本地状态 setPipelines((prev) => prev.map((pipeline) => pipeline.id === editingPipeline.id ? { ...updatedPipeline, description: updatedPipeline.description || '', enabled: updatedPipeline.valid === 1, steps: pipeline.steps || [], // 保持步骤不变 } : pipeline, ), ); Message.success('流水线更新成功'); } else if (isCreatingFromTemplate && selectedTemplateId) { // 基于模板创建新流水线 const newPipeline = await detailService.createPipelineFromTemplate( selectedTemplateId, Number(id), values.name, values.description || '', ); // 更新本地状态 - 需要转换步骤数据结构 const transformedSteps = newPipeline.steps?.map((step) => ({ ...step, enabled: step.valid === 1, })) || []; const pipelineWithDefaults = { ...newPipeline, description: newPipeline.description || '', enabled: newPipeline.valid === 1, steps: transformedSteps, }; setPipelines((prev) => [...prev, pipelineWithDefaults]); // 自动选中新创建的流水线 setSelectedPipelineId(newPipeline.id); Message.success('基于模板创建流水线成功'); } else { // 创建新流水线 const newPipeline = await detailService.createPipeline({ name: values.name, description: values.description || '', projectId: Number(id), }); // 更新本地状态 const pipelineWithDefaults = { ...newPipeline, description: newPipeline.description || '', enabled: newPipeline.valid === 1, steps: [], }; setPipelines((prev) => [...prev, pipelineWithDefaults]); // 自动选中新创建的流水线 setSelectedPipelineId(newPipeline.id); Message.success('流水线创建成功'); } setPipelineModalVisible(false); setIsCreatingFromTemplate(false); setSelectedTemplateId(null); } catch (error) { console.error('保存流水线失败:', error); Message.error('保存流水线失败'); } }; // 添加新步骤 const handleAddStep = (pipelineId: number) => { setEditingStep(null); setEditingPipelineId(pipelineId); form.resetFields(); setEditModalVisible(true); }; // 编辑步骤 const handleEditStep = (pipelineId: number, step: StepWithEnabled) => { setEditingStep(step); setEditingPipelineId(pipelineId); form.setFieldsValue({ name: step.name, script: step.script, }); setEditModalVisible(true); }; // 删除步骤 const handleDeleteStep = async (pipelineId: number, stepId: number) => { Modal.confirm({ title: '确认删除', content: '确定要删除这个流水线步骤吗?此操作不可撤销。', onOk: async () => { try { // 从数据库删除步骤 await detailService.deleteStep(stepId); // 更新本地状态 setPipelines((prev) => prev.map((pipeline) => pipeline.id === pipelineId ? { ...pipeline, steps: pipeline.steps?.filter((step) => step.id !== stepId) || [], updatedAt: new Date().toISOString(), } : pipeline, ), ); Message.success('步骤删除成功'); } catch (error) { console.error('删除步骤失败:', error); Message.error('删除步骤失败'); } }, }); }; // 切换步骤启用状态 const handleToggleStep = async ( pipelineId: number, stepId: number, enabled: boolean, ) => { // 在数据库中更新步骤状态(这里简化处理,实际可能需要添加enabled字段到数据库) setPipelines((prev) => prev.map((pipeline) => pipeline.id === pipelineId ? { ...pipeline, steps: pipeline.steps?.map((step) => step.id === stepId ? { ...step, enabled } : step, ) || [], updatedAt: new Date().toISOString(), } : pipeline, ), ); }; // 拖拽结束处理 const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) { return; } if (selectedPipelineId) { // 更新步骤顺序到数据库(简化处理,实际应该更新所有步骤的order字段) setPipelines((prev) => prev.map((pipeline) => { if (pipeline.id === selectedPipelineId) { const oldIndex = pipeline.steps?.findIndex((step) => step.id === active.id) || 0; const newIndex = pipeline.steps?.findIndex((step) => step.id === over.id) || 0; return { ...pipeline, steps: pipeline.steps ? arrayMove(pipeline.steps, oldIndex, newIndex) : [], updatedAt: new Date().toISOString(), }; } return pipeline; }), ); Message.success('步骤顺序调整成功'); } }; // 保存步骤 const handleSaveStep = async () => { try { const values = await form.validate(); if (editingStep && editingPipelineId) { // 更新现有步骤 const updatedStep = await detailService.updateStep(editingStep.id, { name: values.name, script: values.script, }); // 更新本地状态 setPipelines((prev) => prev.map((pipeline) => pipeline.id === editingPipelineId ? { ...pipeline, steps: pipeline.steps?.map((step) => step.id === editingStep.id ? { ...updatedStep, enabled: step.enabled } : step, ) || [], updatedAt: new Date().toISOString(), } : pipeline, ), ); Message.success('步骤更新成功'); } else if (editingPipelineId) { // 创建新步骤 const newStep = await detailService.createStep({ name: values.name, script: values.script, order: pipelines.find((p) => p.id === editingPipelineId)?.steps?.length || 0, pipelineId: editingPipelineId, }); // 更新本地状态 setPipelines((prev) => prev.map((pipeline) => pipeline.id === editingPipelineId ? { ...pipeline, steps: [ ...(pipeline.steps || []), { ...newStep, enabled: true }, ], updatedAt: new Date().toISOString(), } : pipeline, ), ); Message.success('步骤添加成功'); } setEditModalVisible(false); } catch (error) { console.error('保存步骤失败:', error); Message.error('保存步骤失败'); } }; // 添加重新执行部署的函数 const handleRetryDeployment = async (deploymentId: number) => { try { await detailService.retryDeployment(deploymentId); Message.success('重新执行任务已创建'); // 刷新部署记录 if (id) { const records = await detailService.getDeployments(Number(id)); setDeployRecords(records); } } catch (error) { console.error('重新执行部署失败:', error); Message.error('重新执行部署失败'); } }; // 解析环境变量预设 useEffect(() => { if (detail?.envPresets) { try { const presets = JSON.parse(detail.envPresets); setEnvPresets(presets); } catch (error) { console.error('解析环境变量预设失败:', error); setEnvPresets([]); } } else { setEnvPresets([]); } }, [detail]); // 项目设置相关函数 const handleEditProject = () => { if (detail) { projectForm.setFieldsValue({ name: detail.name, description: detail.description, repository: detail.repository, }); setIsEditingProject(true); } }; const handleCancelEditProject = () => { setIsEditingProject(false); projectForm.resetFields(); }; const handleSaveProject = async () => { try { const values = await projectForm.validate(); await detailService.updateProject(Number(id), values); Message.success('项目更新成功'); setIsEditingProject(false); // 刷新项目详情 if (id) { const projectDetail = await detailService.getProjectDetail(Number(id)); setDetail(projectDetail); } } catch (error) { console.error('更新项目失败:', error); Message.error('更新项目失败'); } }; const handleSaveEnvPresets = async () => { try { setEnvPresetsLoading(true); await detailService.updateProject(Number(id), { envPresets: JSON.stringify(envPresets), }); Message.success('环境变量预设保存成功'); // 刷新项目详情 if (id) { const projectDetail = await detailService.getProjectDetail(Number(id)); setDetail(projectDetail); } } catch (error) { console.error('保存环境变量预设失败:', error); Message.error('保存环境变量预设失败'); } finally { setEnvPresetsLoading(false); } }; const handleDeleteProject = () => { Modal.confirm({ title: '删除项目', content: `确定要删除项目 "${detail?.name}" 吗?删除后将无法恢复。`, okButtonProps: { status: 'danger', }, onOk: async () => { try { await detailService.deleteProject(Number(id)); Message.success('项目删除成功'); navigate('/project'); } catch (error) { console.error('删除项目失败:', error); Message.error('删除项目失败'); } }, }); }; const selectedRecord = deployRecords.find( (record) => record.id === selectedRecordId, ); const buildLogs = getBuildLogs(selectedRecordId); // 简单的状态标签渲染函数(仅用于构建日志区域) const renderStatusTag = (status: Deployment['status']) => { const statusMap: Record = { success: { color: 'green', text: '成功' }, running: { color: 'blue', text: '运行中' }, failed: { color: 'red', text: '失败' }, pending: { color: 'orange', text: '等待中' }, }; const config = statusMap[status]; return {config.text}; }; // 渲染部署记录项 const renderDeployRecordItem = (item: Deployment) => ( ); // 获取工作目录状态标签 const getWorkspaceStatusTag = ( status: string, ): { text: string; color: string } => { const statusMap: Record = { not_created: { text: '未创建', color: 'gray' }, empty: { text: '空目录', color: 'orange' }, no_git: { text: '无Git仓库', color: 'orange' }, ready: { text: '就绪', color: 'green' }, }; return statusMap[status] || { text: '未知', color: 'gray' }; }; // 渲染工作目录状态卡片 const renderWorkspaceStatus = () => { if (!detail?.workspaceStatus) return null; const { workspaceStatus } = detail; const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string); return ( 工作目录状态 } > {statusInfo.text}, }, { label: '当前分支', value: workspaceStatus.gitInfo?.branch || '-', }, { label: '最后提交', value: workspaceStatus.gitInfo?.lastCommit ? ( {workspaceStatus.gitInfo.lastCommit} {workspaceStatus.gitInfo.lastCommitMessage} ) : ( '-' ), }, ]} /> {workspaceStatus.error && (
{workspaceStatus.error}
)}
); }; return (
{detail?.name} 自动化地部署项目
部署记录 } key="deployRecords" >
{/* 左侧部署记录列表 */}
共 {deployRecords.length} 条部署记录
{deployRecords.length > 0 ? ( ) : (
)}
{/* 右侧构建日志 */}
构建日志 #{selectedRecordId} {selectedRecord && ( {selectedRecord.branch} {formatDateTime(selectedRecord.createdAt)} )}
{selectedRecord && (
{selectedRecord.status === 'failed' && ( )} {renderStatusTag(selectedRecord.status)}
)}
{buildLogs.map((log: string, index: number) => (
{log}
))}
流水线 } key="pipeline" >
{/* 左侧流水线列表 */}
共 {pipelines.length} 条流水线
{pipelines.map((pipeline) => { const isSelected = pipeline.id === selectedPipelineId; return ( setSelectedPipelineId(pipeline.id)} >
{pipeline.name} { // 阻止事件冒泡 e?.stopPropagation?.(); handleTogglePipeline(pipeline.id, enabled); }} onClick={(e) => e.stopPropagation()} /> {!pipeline.enabled && ( 已禁用 )}
handleEditPipeline(pipeline) } > 编辑流水线 handleCopyPipeline(pipeline) } > 复制流水线 handleDeletePipeline(pipeline.id) } > 删除流水线 } position="br" trigger="click" >
{pipeline.description}
{pipeline.steps?.length || 0} 个步骤 {formatDateTime(pipeline.updatedAt)}
); })}
{/* 右侧流水线步骤详情 */}
{selectedPipelineId && pipelines.find((p) => p.id === selectedPipelineId) ? ( (() => { const selectedPipeline = pipelines.find( (p) => p.id === selectedPipelineId, ); if (!selectedPipeline) return null; return ( <>
{selectedPipeline.name} - 流水线步骤 {selectedPipeline.description} · 共{' '} {selectedPipeline.steps?.length || 0} 个步骤
step.id, ) || [] } strategy={verticalListSortingStrategy} >
{selectedPipeline.steps?.map((step, index) => ( ))} {selectedPipeline.steps?.length === 0 && (
点击上方"添加步骤"按钮开始配置
)}
); })() ) : (
)}
{/* 项目设置标签页 */} 项目设置 } >
{!isEditingProject ? ( <>
) : ( <>
工作目录: {detail?.projectDir || '-'}
创建时间:{' '} {formatDateTime(detail?.createdAt)}
)}
{/* 工作目录状态 */} {renderWorkspaceStatus()}
{/* 环境变量预设标签页 */} 环境变量 } >
保存预设 } >
配置项目的环境变量预设,在部署时可以选择这些预设值。支持单选、多选和输入框类型。
{/* 新建/编辑流水线模态框 */} { setPipelineModalVisible(false); setIsCreatingFromTemplate(false); setSelectedTemplateId(null); }} style={{ width: 500 }} >
{!editingPipeline && templates.length > 0 && (
)} {isCreatingFromTemplate && templates.length > 0 ? ( <> {selectedTemplateId && ( <> )} ) : ( <> )}
{/* 编辑步骤模态框 */} setEditModalVisible(false)} style={{ width: 600 }} >
可用环境变量:
• $PROJECT_NAME - 项目名称
• $BUILD_NUMBER - 构建编号
• $REGISTRY - 镜像仓库地址
setDeployModalVisible(false)} onOk={() => { setDeployModalVisible(false); // 刷新部署记录 if (id) { detailService.getDeployments(Number(id)).then((records) => { setDeployRecords(records); if (records.length > 0) { setSelectedRecordId(records[0].id); } }); } }} pipelines={pipelines} projectId={Number(id)} project={detail} />
); } export default ProjectDetailPage;