智能化产品发布流水线

51 阅读9分钟
import React, { useState, useEffect } from 'react';
import { 
  ChevronRight, 
  CheckCircle, 
  Circle, 
  AlertCircle, 
  Clock, 
  Play, 
  Pause, 
  Settings, 
  Code, 
  Server, 
  Shield, 
  Database,
  Globe,
  Zap,
  FileText,
  Users,
  GitBranch,
  Package,
  Monitor,
  ChevronDown
} from 'lucide-react';

const DevOpsPipelineSystem = () => {
  const [expandedStep, setExpandedStep] = useState(null);
  const [completedSteps, setCompletedSteps] = useState(new Set());
  const [stepStatuses, setStepStatuses] = useState({});
  const [isProcessing, setIsProcessing] = useState(false);
  const [currentExecutingStep, setCurrentExecutingStep] = useState(null);

  // 主流程定义
  const mainPipeline = [
    {
      id: 'preparation',
      title: '准备阶段',
      icon: FileText,
      color: '#3B82F6',
      description: '项目初始化和环境准备',
      subSteps: [
        { id: 'code-review', title: '代码审查', icon: Code, description: '代码质量检查和同行评审', details: '执行静态代码分析,检查代码规范,确保代码质量符合团队标准。' },
        { id: 'dependency-check', title: '依赖检查', icon: Package, description: '检查项目依赖和版本兼容性', details: '扫描项目依赖,检查版本冲突,确保所有依赖项都是最新且兼容的版本。' },
        { id: 'env-config', title: '环境配置', icon: Settings, description: '配置部署环境参数', details: '设置环境变量、配置文件、数据库连接等部署所需的各项参数。' }
      ]
    },
    {
      id: 'build',
      title: '构建阶段',
      icon: Package,
      color: '#10B981',
      description: '代码编译和打包',
      subSteps: [
        { id: 'compile', title: '代码编译', icon: Code, description: '编译源代码并生成可执行文件', details: '使用构建工具编译源代码,处理TypeScript、Sass等预处理文件。' },
        { id: 'unit-test', title: '单元测试', icon: CheckCircle, description: '执行单元测试确保代码质量', details: '运行所有单元测试用例,确保代码覆盖率达到标准,验证功能正确性。' },
        { id: 'package-build', title: '打包构建', icon: Package, description: '打包应用程序和资源文件', details: '将编译后的代码和资源文件打包成可部署的格式,优化文件大小。' }
      ]
    },
    {
      id: 'security',
      title: '安全检测',
      icon: Shield,
      color: '#F59E0B',
      description: '安全漏洞扫描和检测',
      subSteps: [
        { id: 'vulnerability-scan', title: '漏洞扫描', icon: Shield, description: '扫描代码中的安全漏洞', details: '使用安全扫描工具检测SQL注入、XSS等常见安全漏洞。' },
        { id: 'dependency-security', title: '依赖安全检查', icon: AlertCircle, description: '检查第三方依赖的安全性', details: '扫描第三方依赖包,检查是否存在已知的安全漏洞和风险。' },
        { id: 'compliance-check', title: '合规性检查', icon: FileText, description: '确保符合安全合规要求', details: '验证应用程序是否符合GDPR、SOX等法规要求和公司安全政策。' }
      ]
    },
    {
      id: 'deployment',
      title: '部署阶段',
      icon: Server,
      color: '#8B5CF6',
      description: '应用程序部署和配置',
      subSteps: [
        { id: 'staging-deploy', title: '预发布部署', icon: Server, description: '部署到预发布环境进行测试', details: '将应用部署到预发布环境,进行功能验证和性能测试。' },
        { id: 'integration-test', title: '集成测试', icon: Zap, description: '执行集成测试验证功能', details: '在预发布环境中执行端到端测试,验证各系统间的集成。' },
        { id: 'production-deploy', title: '生产部署', icon: Globe, description: '部署到生产环境', details: '使用蓝绿部署或滚动更新方式,将应用安全地部署到生产环境。' }
      ]
    },
    {
      id: 'monitoring',
      title: '监控验证',
      icon: Monitor,
      color: '#EF4444',
      description: '系统监控和健康检查',
      subSteps: [
        { id: 'health-check', title: '健康检查', icon: Monitor, description: '检查应用程序运行状态', details: '验证应用服务、数据库连接、外部API等关键组件的健康状态。' },
        { id: 'performance-test', title: '性能测试', icon: Zap, description: '验证系统性能指标', details: '测试应用的响应时间、吞吐量、资源使用率等性能指标。' },
        { id: 'user-acceptance', title: '用户验收', icon: Users, description: '用户验收测试', details: '邀请关键用户进行验收测试,确保功能满足业务需求。' }
      ]
    }
  ];

  const executeStep = async (stepId) => {
    setIsProcessing(true);
    setCurrentExecutingStep(stepId);
    setStepStatuses(prev => ({ ...prev, [stepId]: 'running' }));

    // 模拟处理时间
    await new Promise(resolve => setTimeout(resolve, 3000));

    setStepStatuses(prev => ({ ...prev, [stepId]: 'completed' }));
    setCompletedSteps(prev => new Set([...prev, stepId]));
    setIsProcessing(false);
    setCurrentExecutingStep(null);
  };

  const getStepStatus = (stepId) => {
    if (stepStatuses[stepId] === 'running') return 'running';
    if (completedSteps.has(stepId)) return 'completed';
    return 'pending';
  };

  const getMainStepStatus = (step) => {
    const completedSubSteps = step.subSteps.filter(subStep => completedSteps.has(subStep.id)).length;
    if (completedSubSteps === step.subSteps.length) return 'completed';
    if (completedSubSteps > 0) return 'running';
    return 'pending';
  };

  const StepIndicator = ({ status, icon: Icon, color, size = 20 }) => {
    const statusStyles = {
      completed: { 
        background: '#10B981', 
        border: '2px solid #10B981',
        color: 'white'
      },
      running: { 
        background: color, 
        border: `2px solid ${color}`,
        color: 'white',
        animation: 'pulse 2s infinite'
      },
      pending: { 
        background: 'rgba(255, 255, 255, 0.1)', 
        border: '2px solid rgba(255, 255, 255, 0.3)',
        color: 'rgba(255, 255, 255, 0.6)'
      }
    };

    return (
      <div 
        className="step-indicator"
        style={statusStyles[status]}
      >
        {status === 'completed' ? <CheckCircle size={size} /> : <Icon size={size} />}
      </div>
    );
  };

  const toggleExpanded = (stepId) => {
    setExpandedStep(expandedStep === stepId ? null : stepId);
  };

  return (
    <div className="devops-system">
      <div className="system-header">
        <div className="header-content">
          <div className="logo">
            <GitBranch size={32} />
            <div>
              <h1>DevOps 发布系统</h1>
              <p>智能化产品发布流水线</p>
            </div>
          </div>
          
          <div className="system-status">
            <div className="status-item">
              <span className="label">总体进度</span>
              <span className="value">
                {Math.round((completedSteps.size / mainPipeline.reduce((acc, step) => acc + step.subSteps.length, 0)) * 100)}%
              </span>
            </div>
            <div className="status-item">
              <span className="label">已完成步骤</span>
              <span className="value">
                {completedSteps.size} / {mainPipeline.reduce((acc, step) => acc + step.subSteps.length, 0)}
              </span>
            </div>
          </div>
        </div>
      </div>

      <div className="system-content">
        <div className="pipeline-container">
          <div className="pipeline-header">
            <h2>产品发布流水线</h2>
            <p>点击各阶段卡片展开详细步骤,引导您完成完整的产品上线流程</p>
          </div>
          
          {/* 主流程总览 */}
          <div className="main-pipeline">
            {mainPipeline.map((step, index) => (
              <React.Fragment key={step.id}>
                <div 
                  className={`pipeline-step ${expandedStep === step.id ? 'expanded' : ''} ${getMainStepStatus(step)}`}
                  onClick={() => toggleExpanded(step.id)}
                >
                  <StepIndicator 
                    status={getMainStepStatus(step)}
                    icon={step.icon}
                    color={step.color}
                    size={24}
                  />
                  <div className="step-content">
                    <h3>{step.title}</h3>
                    <p>{step.description}</p>
                    <div className="step-progress">
                      <div className="progress-bar">
                        <div 
                          className="progress-fill"
                          style={{
                            width: `${(step.subSteps.filter(subStep => completedSteps.has(subStep.id)).length / step.subSteps.length) * 100}%`,
                            background: step.color
                          }}
                        />
                      </div>
                      <span className="progress-text">
                        {step.subSteps.filter(subStep => completedSteps.has(subStep.id)).length} / {step.subSteps.length}
                      </span>
                    </div>
                  </div>
                  <div className="expand-indicator">
                    <ChevronDown 
                      size={20} 
                      className={`expand-arrow ${expandedStep === step.id ? 'rotated' : ''}`}
                    />
                  </div>
                  {expandedStep === step.id && (
                    <div className="active-border" style={{ background: step.color }} />
                  )}
                </div>
                
                {index < mainPipeline.length - 1 && (
                  <div className="pipeline-connector">
                    <ChevronRight size={24} />
                  </div>
                )}
              </React.Fragment>
            ))}
          </div>

          {/* 详细步骤展开区域 */}
          {expandedStep && (
            <div className="detailed-steps-section">
              <div className="section-header">
                <h3>{mainPipeline.find(s => s.id === expandedStep)?.title} - 详细步骤</h3>
                <p>按顺序执行以下步骤完成 {mainPipeline.find(s => s.id === expandedStep)?.title}</p>
              </div>
              
              <div className="substeps-grid">
                {mainPipeline.find(s => s.id === expandedStep)?.subSteps.map((subStep, index) => (
                  <div 
                    key={subStep.id}
                    className={`substep-card ${getStepStatus(subStep.id)} ${currentExecutingStep === subStep.id ? 'executing' : ''}`}
                  >
                    <div className="card-header">
                      <StepIndicator 
                        status={getStepStatus(subStep.id)}
                        icon={subStep.icon}
                        color={mainPipeline.find(s => s.id === expandedStep)?.color}
                        size={20}
                      />
                      <div className="step-number">步骤 {index + 1}</div>
                    </div>
                    
                    <div className="card-content">
                      <h4>{subStep.title}</h4>
                      <p className="description">{subStep.description}</p>
                      <p className="details">{subStep.details}</p>
                    </div>
                    
                    <div className="card-actions">
                      {getStepStatus(subStep.id) === 'pending' && (
                        <button 
                          className="execute-btn"
                          onClick={() => executeStep(subStep.id)}
                          disabled={isProcessing}
                          style={{ background: mainPipeline.find(s => s.id === expandedStep)?.color }}
                        >
                          {currentExecutingStep === subStep.id ? (
                            <>
                              <Clock size={16} />
                              执行中...
                            </>
                          ) : (
                            <>
                              <Play size={16} />
                              开始执行
                            </>
                          )}
                        </button>
                      )}
                      
                      {getStepStatus(subStep.id) === 'completed' && (
                        <div className="completed-badge">
                          <CheckCircle size={16} />
                          已完成
                        </div>
                      )}
                      
                      {getStepStatus(subStep.id) === 'running' && (
                        <div className="running-badge">
                          <Clock size={16} />
                          执行中
                        </div>
                      )}
                    </div>
                  </div>
                ))}
              </div>
            </div>
          )}
        </div>
      </div>

      <style jsx>{`
        .devops-system {
          min-height: 100vh;
          background: linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #1e3a8a 100%);
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
          color: white;
        }

        .system-header {
          background: rgba(30, 27, 75, 0.8);
          backdrop-filter: blur(20px);
          border-bottom: 1px solid rgba(255, 255, 255, 0.1);
          padding: 24px 40px;
        }

        .header-content {
          display: flex;
          justify-content: space-between;
          align-items: center;
          max-width: 1400px;
          margin: 0 auto;
        }

        .logo {
          display: flex;
          align-items: center;
          gap: 16px;
        }

        .logo h1 {
          margin: 0;
          font-size: 28px;
          font-weight: 700;
          background: linear-gradient(135deg, #60a5fa, #a78bfa);
          -webkit-background-clip: text;
          -webkit-text-fill-color: transparent;
        }

        .logo p {
          margin: 0;
          color: rgba(255, 255, 255, 0.7);
          font-size: 14px;
        }

        .system-status {
          display: flex;
          gap: 32px;
        }

        .status-item {
          text-align: right;
        }

        .status-item .label {
          display: block;
          font-size: 12px;
          color: rgba(255, 255, 255, 0.6);
          margin-bottom: 4px;
        }

        .status-item .value {
          display: block;
          font-size: 18px;
          font-weight: 600;
          color: white;
        }

        .system-content {
          padding: 40px;
          max-width: 1400px;
          margin: 0 auto;
        }

        .pipeline-container {
          background: rgba(255, 255, 255, 0.05);
          backdrop-filter: blur(10px);
          border-radius: 20px;
          border: 1px solid rgba(255, 255, 255, 0.1);
          padding: 40px;
        }

        .pipeline-header {
          text-align: center;
          margin-bottom: 40px;
        }

        .pipeline-header h2 {
          font-size: 32px;
          margin: 0 0 12px 0;
          background: linear-gradient(135deg, #60a5fa, #a78bfa);
          -webkit-background-clip: text;
          -webkit-text-fill-color: transparent;
        }

        .pipeline-header p {
          color: rgba(255, 255, 255, 0.7);
          font-size: 16px;
          margin: 0;
        }

        .main-pipeline {
          display: flex;
          align-items: center;
          gap: 24px;
          margin-bottom: 40px;
          overflow-x: auto;
          padding: 20px 0;
        }

        .pipeline-step {
          background: rgba(255, 255, 255, 0.08);
          border: 2px solid rgba(255, 255, 255, 0.1);
          border-radius: 16px;
          padding: 24px;
          min-width: 280px;
          cursor: pointer;
          transition: all 0.3s ease;
          position: relative;
          display: flex;
          flex-direction: column;
          gap: 16px;
        }

        .pipeline-step:hover {
          transform: translateY(-4px);
          box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2);
          background: rgba(255, 255, 255, 0.12);
        }

        .pipeline-step.expanded {
          border-color: rgba(96, 165, 250, 0.6);
          box-shadow: 0 8px 16px rgba(96, 165, 250, 0.3);
        }

        .pipeline-step.completed {
          border-color: rgba(16, 185, 129, 0.6);
        }

        .pipeline-step.running {
          border-color: rgba(251, 191, 36, 0.6);
          animation: glow 2s ease-in-out infinite alternate;
        }

        @keyframes glow {
          from { box-shadow: 0 0 10px rgba(251, 191, 36, 0.3); }
          to { box-shadow: 0 0 20px rgba(251, 191, 36, 0.6); }
        }

        .active-border {
          position: absolute;
          top: 0;
          left: 0;
          right: 0;
          height: 4px;
          border-radius: 16px 16px 0 0;
        }

        .step-indicator {
          width: 56px;
          height: 56px;
          border-radius: 16px;
          display: flex;
          align-items: center;
          justify-content: center;
          transition: all 0.3s ease;
          align-self: flex-start;
        }

        @keyframes pulse {
          0%, 100% { transform: scale(1); opacity: 1; }
          50% { transform: scale(1.1); opacity: 0.8; }
        }

        .step-content {
          flex: 1;
        }

        .step-content h3 {
          margin: 0 0 8px 0;
          font-size: 20px;
          font-weight: 600;
        }

        .step-content p {
          margin: 0 0 16px 0;
          color: rgba(255, 255, 255, 0.7);
          font-size: 14px;
          line-height: 1.5;
        }

        .step-progress {
          display: flex;
          align-items: center;
          gap: 12px;
        }

        .progress-bar {
          flex: 1;
          height: 6px;
          background: rgba(255, 255, 255, 0.2);
          border-radius: 3px;
          overflow: hidden;
        }

        .progress-fill {
          height: 100%;
          transition: width 0.3s ease;
          border-radius: 3px;
        }

        .progress-text {
          font-size: 12px;
          color: rgba(255, 255, 255, 0.6);
          min-width: 40px;
        }

        .expand-indicator {
          align-self: center;
        }

        .expand-arrow {
          transition: transform 0.3s ease;
          color: rgba(255, 255, 255, 0.6);
        }

        .expand-arrow.rotated {
          transform: rotate(180deg);
        }

        .pipeline-connector {
          color: rgba(255, 255, 255, 0.4);
          flex-shrink: 0;
        }

        .detailed-steps-section {
          border-top: 1px solid rgba(255, 255, 255, 0.1);
          padding-top: 40px;
          animation: slideIn 0.3s ease-out;
        }

        @keyframes slideIn {
          from {
            opacity: 0;
            transform: translateY(20px);
          }
          to {
            opacity: 1;
            transform: translateY(0);
          }
        }

        .section-header {
          margin-bottom: 32px;
        }

        .section-header h3 {
          margin: 0 0 8px 0;
          font-size: 24px;
          font-weight: 700;
          color: white;
        }

        .section-header p {
          margin: 0;
          color: rgba(255, 255, 255, 0.7);
          font-size: 16px;
        }

        .substeps-grid {
          display: grid;
          grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
          gap: 24px;
        }

        .substep-card {
          background: rgba(255, 255, 255, 0.08);
          border: 2px solid rgba(255, 255, 255, 0.1);
          border-radius: 16px;
          padding: 24px;
          transition: all 0.3s ease;
          position: relative;
          overflow: hidden;
        }

        .substep-card:hover {
          transform: translateY(-2px);
          box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
        }

        .substep-card.completed {
          border-color: rgba(16, 185, 129, 0.6);
          background: rgba(16, 185, 129, 0.1);
        }

        .substep-card.running {
          border-color: rgba(59, 130, 246, 0.6);
          background: rgba(59, 130, 246, 0.1);
        }

        .substep-card.executing {
          animation: executeGlow 2s ease-in-out infinite;
        }

        @keyframes executeGlow {
          0%, 100% { box-shadow: 0 0 10px rgba(59, 130, 246, 0.3); }
          50% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.6); }
        }

        .card-header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 16px;
        }

        .card-header .step-indicator {
          width: 48px;
          height: 48px;
        }

        .step-number {
          background: rgba(255, 255, 255, 0.1);
          padding: 6px 12px;
          border-radius: 8px;
          font-size: 12px;
          color: rgba(255, 255, 255, 0.8);
          font-weight: 500;
        }

        .card-content h4 {
          margin: 0 0 12px 0;
          font-size: 18px;
          font-weight: 600;
          color: white;
        }

        .card-content .description {
          margin: 0 0 12px 0;
          color: rgba(255, 255, 255, 0.8);
          font-size: 14px;
          line-height: 1.5;
        }

        .card-content .details {
          margin: 0 0 20px 0;
          color: rgba(255, 255, 255, 0.6);
          font-size: 13px;
          line-height: 1.6;
        }

        .card-actions {
          margin-top: auto;
        }

        .execute-btn {
          display: flex;
          align-items: center;
          gap: 8px;
          background: #3B82F6;
          border: none;
          border-radius: 12px;
          padding: 12px 20px;
          color: white;
          font-weight: 500;
          cursor: pointer;
          transition: all 0.2s ease;
          width: 100%;
          justify-content: center;
        }

        .execute-btn:hover:not(:disabled) {
          transform: translateY(-2px);
          box-shadow: 0 8px 16px rgba(59, 130, 246, 0.3);
        }

        .execute-btn:disabled {
          opacity: 0.6;
          cursor: not-allowed;
        }

        .completed-badge {
          display: flex;
          align-items: center;
          gap: 8px;
          background: rgba(16, 185, 129, 0.2);
          color: #10B981;
          padding: 12px 20px;
          border-radius: 12px;
          justify-content: center;
          font-weight: 500;
        }

        .running-badge {
          display: flex;
          align-items: center;
          gap: 8px;
          background: rgba(59, 130, 246, 0.2);
          color: #3B82F6;
          padding: 12px 20px;
          border-radius: 12px;
          justify-content: center;
          font-weight: 500;
          animation: pulse 2s infinite;
        }

        @media (max-width: 768px) {
          .system-header {
            padding: 20px;
          }

          .header-content {
            flex-direction: column;
            gap: 20px;
            text-align: center;
          }

          .system-status {
            justify-content: center;
          }

          .system-content {
            padding: 20px;
          }

          .pipeline-container {
            padding: 24px;
          }

          .main-pipeline {
            flex-direction: column;
            align-items: stretch;
          }

          .pipeline-step {
            min-width: auto;
          }

          .pipeline-connector {
            transform: rotate(90deg);
            margin: 12px 0;
          }

          .substeps-grid {
            flex-direction: column;
            align-items: stretch;
          }

          .substep-card {
            min-width: auto;
          }

          .substep-connector {
            transform: rotate(90deg);
            margin: 12px 0;
            align-self: center;
          }
        }
      `}</style>
    </div>
  );
};

export default DevOpsPipelineSystem;