import { Button, Card, Dropdown, Empty, Form, Input, List, Menu, Message, Modal, Switch, Tabs, Tag, Typography, } from '@arco-design/web-react'; import { IconCopy, IconDelete, IconEdit, IconMore, IconPlayArrow, IconPlus, } 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 { useState } from 'react'; import { useParams } from 'react-router'; import { useAsyncEffect } from '../../../hooks/useAsyncEffect'; import type { Deployment, Pipeline, Project, Step } from '../types'; import DeployModal from './components/DeployModal'; import DeployRecordItem from './components/DeployRecordItem'; 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 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 { id } = useParams(); 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'); }; // 触发部署 const handleDeploy = () => { setDeployModalVisible(true); }; // 添加新流水线 const handleAddPipeline = () => { setEditingPipeline(null); pipelineForm.resetFields(); setPipelineModalVisible(true); }; // 编辑流水线 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 { // 创建新流水线 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); } 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 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, _index: number) => { const isSelected = item.id === selectedRecordId; return ( ); }; return (
{detail?.name} 自动化地部署项目
{/* 左侧部署记录列表 */}
共 {deployRecords.length} 条部署记录
{deployRecords.length > 0 ? ( ) : (
)}
{/* 右侧构建日志 */}
构建日志 #{selectedRecordId} {selectedRecord && ( {selectedRecord.branch} · {selectedRecord.env} ·{' '} {selectedRecord.createdAt} )}
{selectedRecord && (
{renderStatusTag(selectedRecord.status)}
)}
{buildLogs.map((log: string, index: number) => (
{log}
))}
{/* 左侧流水线列表 */}
共 {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="bottom" >
{pipeline.description}
共 {pipeline.steps?.length || 0} 个步骤 {new Date( pipeline.updatedAt, ).toLocaleString()}
); })} {pipelines.length === 0 && (
点击上方"新建流水线"按钮开始创建
)}
{/* 右侧流水线步骤详情 */}
{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 && (
点击上方"添加步骤"按钮开始配置
)}
); })() ) : (
)}
{/* 新建/编辑流水线模态框 */} setPipelineModalVisible(false)} style={{ width: 500 }} >
{/* 编辑步骤模态框 */} 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)} />
); } export default ProjectDetailPage;