diff --git a/frontend/src/pages/Deploy/Application/List/components/ApplicationModal.tsx b/frontend/src/pages/Deploy/Application/List/components/ApplicationModal.tsx index 85657eb0..b90cfee9 100644 --- a/frontend/src/pages/Deploy/Application/List/components/ApplicationModal.tsx +++ b/frontend/src/pages/Deploy/Application/List/components/ApplicationModal.tsx @@ -8,7 +8,7 @@ interface ApplicationModalProps { onCancel: () => void; onSuccess: () => void; initialValues?: Application; - projectId: number; + projectGroupId: number; } const ApplicationModal: React.FC = ({ @@ -16,7 +16,7 @@ const ApplicationModal: React.FC = ({ onCancel, onSuccess, initialValues, - projectId, + projectGroupId, }) => { const [form] = Form.useForm(); const isEdit = !!initialValues; @@ -26,19 +26,28 @@ const ApplicationModal: React.FC = ({ if (initialValues) { form.setFieldsValue(initialValues); } else { - form.setFieldsValue({ projectId, appStatus: 'ENABLE', sort: 0 }); + form.setFieldsValue({ + appStatus: 'ENABLE', + sort: 0, + projectGroupId: projectGroupId // 设置项目组ID的初始值 + }); } } - }, [visible, initialValues, form, projectId]); + }, [visible, initialValues, form, projectGroupId]); const handleSubmit = async () => { try { const values = await form.validateFields(); + const submitData = { + ...values, + projectGroupId: projectGroupId // 确保提交时包含项目组ID + }; + if (isEdit) { - await updateApplication({ ...values, id: initialValues.id }); + await updateApplication({ ...submitData, id: initialValues?.id }); message.success('更新成功'); } else { - await createApplication(values); + await createApplication(submitData); message.success('创建成功'); } onSuccess(); @@ -67,6 +76,11 @@ const ApplicationModal: React.FC = ({ layout="vertical" initialValues={{ appStatus: 'ENABLE', sort: 0 }} > + {/* 添加一个隐藏的表单项来存储项目组ID */} + + = ({ { pattern: /^[A-Za-z0-9_-]+$/, message: '应用编码只能包含字母、数字、下划线和连字符' } ]} > - + = ({ > - - ); diff --git a/frontend/src/pages/Deploy/Application/List/index.tsx b/frontend/src/pages/Deploy/Application/List/index.tsx index a372c87d..315010cb 100644 --- a/frontend/src/pages/Deploy/Application/List/index.tsx +++ b/frontend/src/pages/Deploy/Application/List/index.tsx @@ -1,27 +1,46 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { PageContainer } from '@ant-design/pro-layout'; -import { Card, Row, Col, Typography, Button, message, Popconfirm, Tag, Space, Tooltip } from 'antd'; +import { Card, Row, Col, Typography, Button, message, Popconfirm, Tag, Space, Tooltip, Select } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, CodeOutlined, CloudUploadOutlined, ApiOutlined } from '@ant-design/icons'; import { getApplicationList, deleteApplication } from './service'; +import { getProjectGroupList } from '../../ProjectGroup/List/service'; import type { Application } from './types'; +import type { ProjectGroup } from '../../ProjectGroup/List/types'; import ApplicationModal from './components/ApplicationModal'; const { Title, Text } = Typography; +const { Option } = Select; const ApplicationList: React.FC = () => { const [applications, setApplications] = useState([]); + const [projectGroups, setProjects] = useState([]); + const [selectedProjectGroupId, setSelectedProjectGroupId] = useState(); const [loading, setLoading] = useState(false); const [modalVisible, setModalVisible] = useState(false); const [currentApplication, setCurrentApplication] = useState(); - // TODO: 这里需要从上下文或者URL参数获取projectId - const projectId = 1; + // 获取项目列表 + const fetchProjects = async () => { + try { + const data = await getProjectGroupList(); + setProjects(data); + if (data.length > 0 && !selectedProjectGroupId) { + setSelectedProjectGroupId(data[0].id); + } + } catch (error) { + message.error('获取项目组列表失败'); + } + }; + // 获取应用列表 const fetchApplications = async () => { + if (!selectedProjectGroupId) return; + setLoading(true); try { const data = await getApplicationList(); - setApplications(data); + // TODO: 这里需要根据selectedProjectId筛选应用 + setApplications(data.filter(app => app.projectId === selectedProjectGroupId)); } catch (error) { message.error('获取应用列表失败'); } finally { @@ -29,10 +48,14 @@ const ApplicationList: React.FC = () => { } }; - React.useEffect(() => { - fetchApplications(); + useEffect(() => { + fetchProjects(); }, []); + useEffect(() => { + fetchApplications(); + }, [selectedProjectGroupId]); + const handleDelete = async (id: number) => { try { await deleteApplication(id); @@ -53,6 +76,10 @@ const ApplicationList: React.FC = () => { setModalVisible(true); }; + const handleProjectChange = (value: number) => { + setSelectedProjectGroupId(value); + }; + // 根据应用编码推测应用类型 const getAppType = (appCode: string) => { if (appCode.toLowerCase().includes('web')) return { icon: , name: '前端应用', color: '#1890ff' }; @@ -201,10 +228,31 @@ const ApplicationList: React.FC = () => { ); }; + const currentProject = projectGroups.find(p => p.id === selectedProjectGroupId); + return ( + 当前项目组: + + + ), extra: [ @@ -228,34 +277,36 @@ const ApplicationList: React.FC = () => { ))} - - -
- -
新建应用
-
-
- + {selectedProjectGroupId && ( + + +
+ +
新建应用
+
+
+ + )} @@ -264,7 +315,7 @@ const ApplicationList: React.FC = () => { onCancel={() => setModalVisible(false)} onSuccess={fetchApplications} initialValues={currentApplication} - projectId={projectId} + projectGroupId={selectedProjectGroupId!} />
); diff --git a/frontend/src/pages/Deploy/Environment/List/components/EnvironmentModal.tsx b/frontend/src/pages/Deploy/Environment/List/components/EnvironmentModal.tsx index 42fe7669..11e43a0a 100644 --- a/frontend/src/pages/Deploy/Environment/List/components/EnvironmentModal.tsx +++ b/frontend/src/pages/Deploy/Environment/List/components/EnvironmentModal.tsx @@ -1,118 +1,158 @@ -import React, { useEffect } from 'react'; -import { Modal, Form, Input, InputNumber, message } from 'antd'; -import type { Environment } from '../types'; -import { createEnvironment, updateEnvironment } from '../service'; +import React, {useEffect, useState} from 'react'; +import {Modal, Form, Input, Select, message} from 'antd'; +import type {Environment} from '../types'; +import {BuildTypeEnum, DeployTypeEnum} from '../types'; +import {createEnvironment, updateEnvironment} from '../service'; interface EnvironmentModalProps { - visible: boolean; - onCancel: () => void; - onSuccess: () => void; - initialValues?: Environment; - projectId: number; - tenantCode: string; + visible: boolean; + onCancel: () => void; + onSuccess: () => void; + initialValues?: Environment; } +const {Option} = Select; + +const buildTypeOptions = [ + {label: 'Jenkins构建', value: BuildTypeEnum.JENKINS}, + {label: 'GitLab Runner构建', value: BuildTypeEnum.GITLAB_RUNNER}, + {label: 'GitHub Action构建', value: BuildTypeEnum.GITHUB_ACTION}, +]; + +const deployTypeOptions = [ + {label: 'Kubernetes集群部署', value: DeployTypeEnum.K8S}, + {label: 'Docker容器部署', value: DeployTypeEnum.DOCKER}, + {label: '虚拟机部署', value: DeployTypeEnum.VM}, +]; + const EnvironmentModal: React.FC = ({ - visible, - onCancel, - onSuccess, - initialValues, - projectId, - tenantCode, -}) => { - const [form] = Form.useForm(); - const isEdit = !!initialValues; + visible, + onCancel, + onSuccess, + initialValues, + }) => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); - useEffect(() => { - if (visible) { - if (initialValues) { - form.setFieldsValue(initialValues); - } else { - form.setFieldsValue({ projectId, tenantCode, sort: 0 }); - } - } - }, [visible, initialValues, form, projectId, tenantCode]); - const handleSubmit = async () => { - try { - const values = await form.validateFields(); - if (isEdit) { - await updateEnvironment({ ...values, id: initialValues.id }); - message.success('更新成功'); - } else { - await createEnvironment(values); - message.success('创建成功'); - } - onSuccess(); - onCancel(); - form.resetFields(); - } catch (error) { - message.error(isEdit ? '更新失败' : '创建失败'); - } - }; + useEffect(() => { + if (visible) { + form.setFieldsValue(initialValues || {}); + } + }, [visible, initialValues]); - const handleCancel = () => { - form.resetFields(); - onCancel(); - }; + const handleOk = async () => { + try { + const values = await form.validateFields(); + setLoading(true); + if (initialValues?.id) { + await updateEnvironment({ + ...values, + id: initialValues.id, + }); + message.success('更新成功'); + } else { + await createEnvironment(values); + message.success('创建成功'); + } + onSuccess(); + onCancel(); + } catch (error) { + console.error('Submit failed:', error); + } finally { + setLoading(false); + } + }; - return ( - -
- - - + + + + - - - + + + - - - + + + - + + + - -
-
- ); + + + + + + ); }; export default EnvironmentModal; diff --git a/frontend/src/pages/Deploy/Environment/List/index.tsx b/frontend/src/pages/Deploy/Environment/List/index.tsx index d21857ef..69febb9d 100644 --- a/frontend/src/pages/Deploy/Environment/List/index.tsx +++ b/frontend/src/pages/Deploy/Environment/List/index.tsx @@ -1,30 +1,57 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { PageContainer } from '@ant-design/pro-layout'; -import { Card, Row, Col, Typography, Button, message, Popconfirm, Tag, Space, Progress } from 'antd'; -import { - PlusOutlined, - EditOutlined, - DeleteOutlined, - CloudServerOutlined, - DatabaseOutlined, - ClusterOutlined, - DesktopOutlined -} from '@ant-design/icons'; +import { Card, Row, Col, Typography, Button, message, Popconfirm, Space, Tag, Tooltip } from 'antd'; +import { PlusOutlined, EditOutlined, DeleteOutlined, RocketOutlined } from '@ant-design/icons'; import { getEnvironmentList, deleteEnvironment } from './service'; import type { Environment } from './types'; +import { BuildTypeEnum, DeployTypeEnum } from './types'; import EnvironmentModal from './components/EnvironmentModal'; const { Title, Text } = Typography; +// 构建方式和部署方式的显示映射 +const buildTypeMap = { + [BuildTypeEnum.JENKINS]: { + label: 'Jenkins构建', + color: '#1890ff', + description: '使用Jenkins进行自动化构建和部署' + }, + [BuildTypeEnum.GITLAB_RUNNER]: { + label: 'GitLab Runner构建', + color: '#722ed1', + description: '使用GitLab Runner进行CI/CD流程' + }, + [BuildTypeEnum.GITHUB_ACTION]: { + label: 'GitHub Action构建', + color: '#52c41a', + description: '使用GitHub Action进行自动化工作流' + }, +}; + +const deployTypeMap = { + [DeployTypeEnum.K8S]: { + label: 'Kubernetes集群部署', + color: '#1890ff', + description: '部署到Kubernetes容器编排平台' + }, + [DeployTypeEnum.DOCKER]: { + label: 'Docker容器部署', + color: '#13c2c2', + description: '部署为Docker独立容器' + }, + [DeployTypeEnum.VM]: { + label: '虚拟机部署', + color: '#faad14', + description: '部署到传统虚拟机环境' + }, +}; + const EnvironmentList: React.FC = () => { const [environments, setEnvironments] = useState([]); const [loading, setLoading] = useState(false); const [modalVisible, setModalVisible] = useState(false); const [currentEnvironment, setCurrentEnvironment] = useState(); - // TODO: 这里需要从上下文或者URL参数获取projectId - const projectId = 1; - const fetchEnvironments = async () => { setLoading(true); try { @@ -37,7 +64,7 @@ const EnvironmentList: React.FC = () => { } }; - React.useEffect(() => { + useEffect(() => { fetchEnvironments(); }, []); @@ -61,49 +88,9 @@ const EnvironmentList: React.FC = () => { setModalVisible(true); }; - // 根据环境编码获取环境信息 - const getEnvInfo = (envCode: string) => { - const envTypes = { - dev: { - icon: , - name: '开发环境', - color: '#1890ff', - usage: 45 - }, - test: { - icon: , - name: '测试环境', - color: '#52c41a', - usage: 65 - }, - staging: { - icon: , - name: '预发环境', - color: '#faad14', - usage: 85 - }, - prod: { - icon: , - name: '生产环境', - color: '#f5222d', - usage: 92 - } - }; - - const envType = Object.entries(envTypes).find(([key]) => - envCode.toLowerCase().includes(key) - ); - - return envType ? envType[1] : { - icon: , - name: '其他环境', - color: '#8c8c8c', - usage: 50 - }; - }; - const EnvironmentCard = ({ environment }: { environment: Environment }) => { - const envInfo = getEnvInfo(environment.envCode); + const buildType = buildTypeMap[environment.buildType]; + const deployType = deployTypeMap[environment.deployType]; return ( { className="environment-card" style={{ height: '100%', - borderRadius: '12px', + borderRadius: '16px', overflow: 'hidden', - border: '1px solid #f0f0f0', + border: 'none', transition: 'all 0.3s', + boxShadow: '0 1px 2px 0 rgba(0,0,0,0.03), 0 1px 6px -1px rgba(0,0,0,0.02), 0 2px 4px 0 rgba(0,0,0,0.02)', }} bodyStyle={{ padding: '24px', - background: `linear-gradient(145deg, #ffffff 0%, ${envInfo.color}0A 100%)`, + height: '100%', + display: 'flex', + flexDirection: 'column', + background: `linear-gradient(145deg, #ffffff 0%, ${buildType.color}08 100%)`, }} actions={[ - , + + + , handleDelete(environment.id)} > , - handleDelete(project.id)} - > - - , - ]} - > -
- - {project.projectName} - <Tag - color={project.projectStatus === 'ENABLE' ? '#52c41a' : '#d9d9d9'} - style={{ - marginLeft: '8px', - borderRadius: '4px', - fontSize: '12px', - padding: '0 8px', - }} - > - {project.projectStatus === 'ENABLE' ? '启用' : '禁用'} - </Tag> - - - {project.projectDesc || '暂无描述'} - -
-
- 项目编码 -
- {project.projectCode} -
-
-
- 排序 -
- {project.sort} -
-
-
-
-
- ); - - return ( - } - onClick={handleAdd} - style={{ - borderRadius: '6px', - boxShadow: '0 2px 0 rgba(0,0,0,0.045)', - }} - > - 新建项目 - - ], - }} - > -
- - {projects.map(project => ( - - - - ))} - - -
- -
新建项目
-
-
- -
-
-
- ); -}; - -export default ProjectList; diff --git a/frontend/src/pages/Deploy/Project/List/service.ts b/frontend/src/pages/Deploy/Project/List/service.ts deleted file mode 100644 index fbf1020f..00000000 --- a/frontend/src/pages/Deploy/Project/List/service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import request from '@/utils/request'; -import type { CreateProjectRequest, UpdateProjectRequest, Project, ProjectQueryParams } from './types'; -import type { Page } from '@/types/base'; - -const BASE_URL = '/api/v1/projects'; - -// 创建项目 -export const createProject = (data: CreateProjectRequest) => - request.post(BASE_URL, data); - -// 更新项目 -export const updateProject = (data: UpdateProjectRequest) => - request.put(`${BASE_URL}/${data.id}`, data); - -// 删除项目 -export const deleteProject = (id: number) => - request.delete(`${BASE_URL}/${id}`); - -// 获取项目详情 -export const getProject = (id: number) => - request.get(`${BASE_URL}/${id}`); - -// 分页查询项目列表 -export const getProjectPage = (params?: ProjectQueryParams) => - request.get>(`${BASE_URL}/page`, { params }); - -// 获取所有项目列表 -export const getProjectList = () => - request.get(BASE_URL); - -// 条件查询项目列表 -export const getProjectListByCondition = (params?: ProjectQueryParams) => - request.get(`${BASE_URL}/list`, { params }); - -// 批量处理项目 -export const batchProcessProject = (data: any) => - request.post(`${BASE_URL}/batch`, data); - -// 导出项目数据 -export const exportProject = () => - request.get(`${BASE_URL}/export`, { responseType: 'blob' }); diff --git a/frontend/src/pages/Deploy/Project/List/types.ts b/frontend/src/pages/Deploy/Project/List/types.ts deleted file mode 100644 index 33b6c5f7..00000000 --- a/frontend/src/pages/Deploy/Project/List/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { BaseResponse, BaseRequest, BaseQuery } from '@/types/base'; - -// 项目基础信息 -export interface Project extends BaseResponse { - tenantId: number; - projectCode: string; - projectName: string; - projectDesc?: string; - projectStatus: string; - sort: number; -} - -// 创建项目请求参数 -export interface CreateProjectRequest extends BaseRequest { - tenantId: number; - projectCode: string; - projectName: string; - projectDesc?: string; - projectStatus: string; - sort: number; -} - -// 更新项目请求参数 -export interface UpdateProjectRequest extends CreateProjectRequest { - id: number; -} - -// 分页查询参数 -export interface ProjectQueryParams extends BaseQuery { - projectName?: string; - projectCode?: string; - projectStatus?: string; -} diff --git a/frontend/src/pages/Deploy/Project/List/components/ProjectModal.tsx b/frontend/src/pages/Deploy/ProjectGroup/List/components/ProjectModal.tsx similarity index 60% rename from frontend/src/pages/Deploy/Project/List/components/ProjectModal.tsx rename to frontend/src/pages/Deploy/ProjectGroup/List/components/ProjectModal.tsx index 21d27aa6..c700ea22 100644 --- a/frontend/src/pages/Deploy/Project/List/components/ProjectModal.tsx +++ b/frontend/src/pages/Deploy/ProjectGroup/List/components/ProjectModal.tsx @@ -1,13 +1,13 @@ import React, { useEffect } from 'react'; import { Modal, Form, Input, InputNumber, Radio, message } from 'antd'; -import type { Project } from '../types'; -import { createProject, updateProject } from '../service'; +import type { ProjectGroup } from '../types'; +import { createProjectGroup, updateProjectGroup } from '../service'; interface ProjectModalProps { visible: boolean; onCancel: () => void; onSuccess: () => void; - initialValues?: Project; + initialValues?: ProjectGroup; } const ProjectModal: React.FC = ({ @@ -29,10 +29,10 @@ const ProjectModal: React.FC = ({ try { const values = await form.validateFields(); if (isEdit) { - await updateProject({ ...values, id: initialValues.id }); + await updateProjectGroup({ ...values, id: initialValues.id }); message.success('更新成功'); } else { - await createProject(values); + await createProjectGroup(values); message.success('创建成功'); } onSuccess(); @@ -50,7 +50,7 @@ const ProjectModal: React.FC = ({ return ( = ({
- + - + - + 启用 diff --git a/frontend/src/pages/Deploy/ProjectGroup/List/index.tsx b/frontend/src/pages/Deploy/ProjectGroup/List/index.tsx new file mode 100644 index 00000000..e87d3cc7 --- /dev/null +++ b/frontend/src/pages/Deploy/ProjectGroup/List/index.tsx @@ -0,0 +1,400 @@ +import React, { useState, useEffect } from 'react'; +import { PageContainer } from '@ant-design/pro-layout'; +import { Card, Row, Col, Typography, Button, message, Popconfirm, Space, Tag, Input, Select, Tooltip, Statistic, Divider } from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + TeamOutlined, + SearchOutlined, + EnvironmentOutlined, + ClockCircleOutlined, + RocketOutlined +} from '@ant-design/icons'; +import { getProjectGroupList, deleteProjectGroup } from './service'; +import type { ProjectGroup } from './types'; +import ProjectModal from './components/ProjectModal'; + +const { Title, Text } = Typography; +const { Search } = Input; + +const ProjectList: React.FC = () => { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [currentProject, setCurrentProject] = useState(); + const [searchText, setSearchText] = useState(''); + const [projectType, setProjectType] = useState('ALL'); + + const fetchProjects = async () => { + setLoading(true); + try { + const data = await getProjectGroupList(); + setProjects(data); + } catch (error) { + message.error('获取项目组列表失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchProjects(); + }, []); + + const handleDelete = async (id: number) => { + try { + await deleteProjectGroup(id); + message.success('删除成功'); + fetchProjects(); + } catch (error) { + message.error('删除失败'); + } + }; + + const handleAdd = () => { + setCurrentProject(undefined); + setModalVisible(true); + }; + + const handleEdit = (project: ProjectGroup) => { + setCurrentProject(project); + setModalVisible(true); + }; + + const getProjectTypeInfo = (code: string) => { + if (code.toUpperCase().includes('DEMO')) { + return { + type: 'DEMO', + label: '示例项目组', + color: '#1890ff', + icon: , + description: '用于演示和测试的项目组' + }; + } + if (code.toUpperCase().includes('PLATFORM')) { + return { + type: 'PLATFORM', + label: '平台项目组', + color: '#52c41a', + icon: , + description: '平台相关的核心项目组' + }; + } + return { + type: 'BUSINESS', + label: '业务项目组', + color: '#722ed1', + icon: , + description: '具体业务相关的项目组' + }; + }; + + const filteredProjects = projects.filter(project => { + const matchesSearch = searchText ? ( + project.projectGroupName.toLowerCase().includes(searchText.toLowerCase()) || + project.projectGroupCode.toLowerCase().includes(searchText.toLowerCase()) || + project.projectGroupDesc?.toLowerCase().includes(searchText.toLowerCase()) + ) : true; + + const matchesType = projectType === 'ALL' ? true : + getProjectTypeInfo(project.projectGroupCode).type === projectType; + + return matchesSearch && matchesType; + }); + + const ProjectCard = ({ project }: { project: ProjectGroup }) => { + const typeInfo = getProjectTypeInfo(project.projectGroupCode); + const Icon = typeInfo.icon; + + return ( + +
+
+
+ + {project.projectGroupName} + <Tag + color={typeInfo.color} + style={{ + margin: 0, + padding: '0 8px', + borderRadius: '4px', + fontSize: '12px' + }} + > + {typeInfo.label} + </Tag> + + + {project.projectGroupDesc || '暂无描述'} + +
+
+ {Icon} +
+
+ +
+
+ 项目组编码 +
+ {project.projectGroupCode} +
+
+ + + + 环境数量} + value={2} + prefix={} + valueStyle={{ fontSize: '16px', color: typeInfo.color }} + /> + + + 成员数量} + value={5} + prefix={} + valueStyle={{ fontSize: '16px', color: typeInfo.color }} + /> + + + +
+ 最近活动 +
+ + 2小时前 +
+
+ +
+ 排序 +
+ {project.sort} +
+
+ + + + + + handleDelete(project.id)} + > + + + + +
+
+
+ ); + }; + + return ( + } + onClick={handleAdd} + style={{ + borderRadius: '8px', + boxShadow: '0 2px 0 rgba(0,0,0,0.045)', + height: '40px', + padding: '0 24px', + }} + > + 新建项目组 + + ], + }} + > +
+ + + 搜索} + size="large" + value={searchText} + onChange={e => setSearchText(e.target.value)} + style={{ width: '100%' }} + /> + + +