This commit is contained in:
asp_ly 2024-12-23 22:11:34 +08:00
parent 3f1240e972
commit b9d559a683
4 changed files with 707 additions and 901 deletions

View File

@ -1,274 +1,253 @@
import React, { useState, useEffect } from 'react'; import React, {useState, useEffect} from 'react';
import { PageContainer } from '@ant-design/pro-layout'; import {PageContainer} from '@ant-design/pro-layout';
import { Card, Row, Col, Typography, Button, message, Empty, Tooltip, Tag } from 'antd'; import {Card, Row, Col, Typography, Button, message, Empty, Tooltip, Tag} from 'antd';
import { PlusOutlined, RocketOutlined, CloudServerOutlined, SettingOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'; import {PlusOutlined, RocketOutlined, CloudServerOutlined, SettingOutlined, DeleteOutlined, EditOutlined} from '@ant-design/icons';
import { getEnvironmentList, deleteEnvironment } from './service'; import {getEnvironmentList, deleteEnvironment} from './service';
import type { Environment } from './types'; import type {Environment} from './types';
import { BuildTypeEnum, DeployTypeEnum } from './types'; import {BuildTypeEnum, DeployTypeEnum} from './types';
import EnvironmentModal from './components/EnvironmentModal'; import EnvironmentModal from './components/EnvironmentModal';
const { Title, Text } = Typography; const {Title, Text} = Typography;
// 构建方式和部署方式的显示映射 // 构建方式和部署方式的显示映射
const buildTypeMap = { const buildTypeMap = {
[BuildTypeEnum.JENKINS]: { [BuildTypeEnum.JENKINS]: {
label: 'Jenkins构建', label: 'Jenkins构建',
color: '#1890ff', color: '#1890ff',
icon: <RocketOutlined />, icon: <RocketOutlined/>,
description: '使用Jenkins进行自动化构建和部署' description: '使用Jenkins进行自动化构建和部署'
}, },
[BuildTypeEnum.GITLAB_RUNNER]: { [BuildTypeEnum.GITLAB_RUNNER]: {
label: 'GitLab Runner构建', label: 'GitLab Runner构建',
color: '#722ed1', color: '#722ed1',
icon: <CloudServerOutlined />, icon: <CloudServerOutlined/>,
description: '使用GitLab Runner进行CI/CD流程' description: '使用GitLab Runner进行CI/CD流程'
}, },
[BuildTypeEnum.GITHUB_ACTION]: { [BuildTypeEnum.GITHUB_ACTION]: {
label: 'GitHub Action构建', label: 'GitHub Action构建',
color: '#52c41a', color: '#52c41a',
icon: <SettingOutlined />, icon: <SettingOutlined/>,
description: '使用GitHub Action进行自动化工作流' description: '使用GitHub Action进行自动化工作流'
}, },
}; };
const deployTypeMap = { const deployTypeMap = {
[DeployTypeEnum.K8S]: { [DeployTypeEnum.K8S]: {
label: 'Kubernetes集群部署', label: 'Kubernetes集群部署',
color: '#1890ff', color: '#1890ff',
icon: <CloudServerOutlined />, icon: <CloudServerOutlined/>,
description: '部署到Kubernetes容器编排平台' description: '部署到Kubernetes容器编排平台'
}, },
[DeployTypeEnum.DOCKER]: { [DeployTypeEnum.DOCKER]: {
label: 'Docker容器部署', label: 'Docker容器部署',
color: '#13c2c2', color: '#13c2c2',
icon: <RocketOutlined />, icon: <RocketOutlined/>,
description: '部署为Docker独立容器' description: '部署为Docker独立容器'
}, },
[DeployTypeEnum.VM]: { [DeployTypeEnum.VM]: {
label: '虚拟机部署', label: '虚拟机部署',
color: '#faad14', color: '#faad14',
icon: <SettingOutlined />, icon: <SettingOutlined/>,
description: '部署到传统虚拟机环境' description: '部署到传统虚拟机环境'
}, },
}; };
const EnvironmentList: React.FC = () => { const EnvironmentList: React.FC = () => {
const [environments, setEnvironments] = useState<Environment[]>([]); const [environments, setEnvironments] = useState<Environment[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [currentEnvironment, setCurrentEnvironment] = useState<Environment>(); const [currentEnvironment, setCurrentEnvironment] = useState<Environment>();
const fetchEnvironments = async () => { const fetchEnvironments = async () => {
setLoading(true); setLoading(true);
try { try {
const data = await getEnvironmentList(); const data = await getEnvironmentList();
setEnvironments(data); setEnvironments(data);
} catch (error) { } catch (error) {
message.error('获取环境列表失败'); message.error('获取环境列表失败');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
useEffect(() => { useEffect(() => {
fetchEnvironments(); fetchEnvironments();
}, []); }, []);
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
try { try {
await deleteEnvironment(id); await deleteEnvironment(id);
message.success('删除成功'); message.success('删除成功');
fetchEnvironments(); fetchEnvironments();
} catch (error) { } catch (error) {
message.error('删除失败'); message.error('删除失败');
} }
}; };
const handleAdd = () => { const handleAdd = () => {
setCurrentEnvironment(undefined); setCurrentEnvironment(undefined);
setModalVisible(true); setModalVisible(true);
}; };
const handleEdit = (environment: Environment) => { const handleEdit = (environment: Environment) => {
setCurrentEnvironment(environment); setCurrentEnvironment(environment);
setModalVisible(true); setModalVisible(true);
}; };
const EnvironmentCard = ({ environment }: { environment: Environment }) => { const EnvironmentCard = ({environment}: { environment: Environment }) => {
const buildType = buildTypeMap[environment.buildType]; const buildType = buildTypeMap[environment.buildType];
const deployType = deployTypeMap[environment.deployType]; const deployType = deployTypeMap[environment.deployType];
return (
<Card
hoverable
className="environment-card"
style={{
borderRadius: '12px',
overflow: 'hidden',
height: '100%',
transition: 'all 0.3s ease',
}}
actions={[
<Tooltip title="编辑环境">
<Button
type="text"
icon={<EditOutlined/>}
onClick={() => handleEdit(environment)}
>
</Button>
</Tooltip>,
<Tooltip title="删除环境">
<Button
type="text"
danger
icon={<DeleteOutlined/>}
onClick={() => handleDelete(environment.id)}
>
</Button>
</Tooltip>
]}
>
<div style={{position: 'relative', padding: '16px'}}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '16px'
}}>
<Title level={4} style={{margin: 0}}>
{environment.envName}
</Title>
<Tag color={buildType.color} style={{borderRadius: '12px', padding: '0 12px'}}>
{buildType.icon} {buildType.label}
</Tag>
</div>
<Text
type="secondary"
style={{
display: 'block',
marginBottom: '24px',
height: '40px',
overflow: 'hidden',
textOverflow: 'ellipsis',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}}
>
{environment.envDesc || '暂无描述'}
</Text>
<div style={{
background: '#f5f5f5',
borderRadius: '8px',
padding: '16px',
marginBottom: '16px'
}}>
<Text type="secondary"></Text>
<div style={{
fontFamily: 'monaco, monospace',
fontSize: '14px',
color: '#262626',
marginTop: '8px',
padding: '8px',
background: '#fff',
borderRadius: '4px',
border: '1px solid #f0f0f0'
}}>
{environment.envCode}
</div>
</div>
</div>
</Card>
);
};
return ( return (
<Card <PageContainer
hoverable header={{
className="environment-card" title: '环境管理',
style={{ extra: [
borderRadius: '12px', <Button
overflow: 'hidden', key="add"
height: '100%', type="primary"
transition: 'all 0.3s ease', icon={<PlusOutlined/>}
}} onClick={handleAdd}
actions={[ style={{
<Tooltip title="编辑环境"> borderRadius: '8px',
<Button padding: '0 24px',
type="text" height: '40px',
icon={<EditOutlined />} boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
onClick={() => handleEdit(environment)} transition: 'all 0.3s ease',
> }}
>
</Button>
</Tooltip>, </Button>
<Tooltip title="删除环境"> ],
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(environment.id)}
>
</Button>
</Tooltip>
]}
>
<div style={{ position: 'relative', padding: '16px' }}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '16px'
}}>
<Title level={4} style={{ margin: 0 }}>
{environment.envName}
</Title>
<Tag color={buildType.color} style={{ borderRadius: '12px', padding: '0 12px' }}>
{buildType.icon} {buildType.label}
</Tag>
</div>
<Text
type="secondary"
style={{
display: 'block',
marginBottom: '24px',
height: '40px',
overflow: 'hidden',
textOverflow: 'ellipsis',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}} }}
> >
{environment.envDesc || '暂无描述'} <div style={{padding: '24px'}}>
</Text> <Row gutter={[24, 24]}>
{environments.length > 0 ? (
<div style={{ environments.map((environment) => (
background: '#f5f5f5', <Col key={environment.id} xs={24} sm={12} md={8} lg={6}>
borderRadius: '8px', <EnvironmentCard environment={environment}/>
padding: '16px', </Col>
marginBottom: '16px' ))
}}> ) : (
<Text type="secondary"></Text> <Col span={24}>
<div style={{ <Empty
fontFamily: 'monaco, monospace', image={Empty.PRESENTED_IMAGE_SIMPLE}
fontSize: '14px', description="暂无环境数据"
color: '#262626', style={{
marginTop: '8px', background: '#fff',
padding: '8px', padding: '40px',
background: '#fff', borderRadius: '8px',
borderRadius: '4px', }}
border: '1px solid #f0f0f0' >
}}> <Button type="primary" onClick={handleAdd}>
{environment.envCode}
</Button>
</Empty>
</Col>
)}
</Row>
</div> </div>
</div>
<div style={{ <EnvironmentModal
display: 'flex', visible={modalVisible}
justifyContent: 'space-between', onCancel={() => setModalVisible(false)}
alignItems: 'center', onSuccess={() => {
marginTop: '16px' setModalVisible(false);
}}> fetchEnvironments();
<Tooltip title={deployType.description}>
<Tag
color={deployType.color}
style={{
padding: '4px 12px',
borderRadius: '12px',
cursor: 'help'
}} }}
> initialValues={currentEnvironment}
{deployType.icon} {deployType.label} />
</Tag> </PageContainer>
</Tooltip>
<Text type="secondary">: {environment.sort}</Text>
</div>
</div>
</Card>
); );
};
return (
<PageContainer
header={{
title: '环境管理',
extra: [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
style={{
borderRadius: '8px',
padding: '0 24px',
height: '40px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
transition: 'all 0.3s ease',
}}
>
</Button>
],
}}
>
<div style={{ padding: '24px' }}>
<Row gutter={[24, 24]}>
{environments.length > 0 ? (
environments.map((environment) => (
<Col key={environment.id} xs={24} sm={12} md={8} lg={6}>
<EnvironmentCard environment={environment} />
</Col>
))
) : (
<Col span={24}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无环境数据"
style={{
background: '#fff',
padding: '40px',
borderRadius: '8px',
}}
>
<Button type="primary" onClick={handleAdd}>
</Button>
</Empty>
</Col>
)}
</Row>
</div>
<EnvironmentModal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onSuccess={() => {
setModalVisible(false);
fetchEnvironments();
}}
initialValues={currentEnvironment}
/>
</PageContainer>
);
}; };
export default EnvironmentList; export default EnvironmentList;

View File

@ -1,159 +1,149 @@
import React, { useEffect, useState } from 'react'; import React, {useEffect, useState} from 'react';
import { Modal, Form, Input, InputNumber, Radio, message, Select } from 'antd'; import {Modal, Form, Input, InputNumber, Radio, message, Select} from 'antd';
import type { ProjectGroup } from '../types'; import type {ProjectGroup} from '../types';
import type { Environment } from '../../../Environment/List/types'; import type {Environment} from '../../../Environment/List/types';
import { createProjectGroup, updateProjectGroup } from '../service'; import {createProjectGroup, updateProjectGroup} from '../service';
import { getEnvironmentList } from '../../../Environment/List/service'; import {getEnvironmentList} from '../../../Environment/List/service';
interface ProjectGroupModalProps { interface ProjectGroupModalProps {
visible: boolean; visible: boolean;
onCancel: () => void; onCancel: () => void;
onSuccess: () => void; onSuccess: () => void;
initialValues?: ProjectGroup; initialValues?: ProjectGroup;
} }
const ProjectGroupModal: React.FC<ProjectGroupModalProps> = ({ const ProjectGroupModal: React.FC<ProjectGroupModalProps> = ({
visible, visible,
onCancel, onCancel,
onSuccess, onSuccess,
initialValues, initialValues,
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [environments, setEnvironments] = useState<Environment[]>([]); const [environments, setEnvironments] = useState<Environment[]>([]);
const isEdit = !!initialValues; const isEdit = !!initialValues;
// 获取环境列表 // 获取环境列表
useEffect(() => { useEffect(() => {
const fetchEnvironments = async () => { const fetchEnvironments = async () => {
try { try {
const data = await getEnvironmentList(); const data = await getEnvironmentList();
setEnvironments(data); setEnvironments(data);
} catch (error) { } catch (error) {
message.error('获取环境列表失败'); message.error('获取环境列表失败');
} }
};
fetchEnvironments();
}, []);
useEffect(() => {
if (visible && initialValues) {
// 设置初始值包括环境ID列表
const envIds = initialValues.environments?.map(env => env.id) || [];
form.setFieldsValue({
...initialValues,
environments: envIds
});
}
}, [visible, initialValues, form]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
// 转换环境ID数组为对象数组
const environments = (values.environments || []).map((id: number) => ({id}));
const submitData = {
...values,
environments
};
if (isEdit) {
await updateProjectGroup({...submitData, id: initialValues.id});
message.success('更新成功');
} else {
await createProjectGroup(submitData);
message.success('创建成功');
}
onSuccess();
onCancel();
form.resetFields();
} catch (error) {
message.error(isEdit ? '更新失败' : '创建失败');
}
}; };
fetchEnvironments();
}, []);
useEffect(() => { const handleCancel = () => {
if (visible && initialValues) { form.resetFields();
// 设置初始值包括环境ID列表 onCancel();
const envIds = initialValues.environments?.map(env => env.id) || []; };
form.setFieldsValue({
...initialValues,
environments: envIds
});
}
}, [visible, initialValues, form]);
const handleSubmit = async () => { return (
try { <Modal
const values = await form.validateFields(); title={isEdit ? '编辑项目组' : '新建项目组'}
// 转换环境ID数组为对象数组 open={visible}
const environments = (values.environments || []).map((id: number) => ({ id })); onOk={handleSubmit}
const submitData = { onCancel={handleCancel}
...values, destroyOnClose
environments width={560}
};
if (isEdit) {
await updateProjectGroup({ ...submitData, id: initialValues.id });
message.success('更新成功');
} else {
await createProjectGroup(submitData);
message.success('创建成功');
}
onSuccess();
onCancel();
form.resetFields();
} catch (error) {
message.error(isEdit ? '更新失败' : '创建失败');
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title={isEdit ? '编辑项目组' : '新建项目组'}
open={visible}
onOk={handleSubmit}
onCancel={handleCancel}
destroyOnClose
width={560}
>
<Form
form={form}
layout="vertical"
initialValues={{ projectGroupStatus: 'ENABLE', sort: 0 }}
>
<Form.Item
name="projectGroupCode"
label="项目组编码"
rules={[
{ required: true, message: '请输入项目组编码' },
{ pattern: /^[A-Za-z0-9_-]+$/, message: '项目组编码只能包含字母、数字、下划线和连字符' }
]}
> >
<Input placeholder="请输入项目组编码" disabled={isEdit} /> <Form
</Form.Item> form={form}
layout="vertical"
initialValues={{projectGroupStatus: 'ENABLE', sort: 0}}
>
<Form.Item
name="projectGroupCode"
label="项目组编码"
rules={[
{required: true, message: '请输入项目组编码'},
{pattern: /^[A-Za-z0-9_-]+$/, message: '项目组编码只能包含字母、数字、下划线和连字符'}
]}
>
<Input placeholder="请输入项目组编码" disabled={isEdit}/>
</Form.Item>
<Form.Item <Form.Item
name="projectGroupName" name="projectGroupName"
label="项目组名称" label="项目组名称"
rules={[{ required: true, message: '请输入项目组名称' }]} rules={[{required: true, message: '请输入项目组名称'}]}
> >
<Input placeholder="请输入项目组名称" /> <Input placeholder="请输入项目组名称"/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="projectGroupDesc" name="projectGroupDesc"
label="项目组描述" label="项目组描述"
> >
<Input.TextArea rows={4} placeholder="请输入项目组描述" /> <Input.TextArea rows={4} placeholder="请输入项目组描述"/>
</Form.Item> </Form.Item>
<Form.Item
<Form.Item name="environments"
name="projectGroupStatus" label="关联环境"
label="项目组状态" rules={[{required: true, message: '请选择关联环境'}]}
rules={[{ required: true, message: '请选择项目组状态' }]} >
initialValue="ENABLE" <Select
> mode="multiple"
<Radio.Group> placeholder="请选择关联环境"
<Radio value="ENABLE"></Radio> optionFilterProp="label"
<Radio value="DISABLE"></Radio> options={environments.map(env => ({
</Radio.Group> label: env.envName,
</Form.Item> value: env.id,
}))}
<Form.Item />
name="environments" </Form.Item>
label="关联环境" <Form.Item
rules={[{ required: true, message: '请选择关联环境' }]} name="projectGroupStatus"
> label="项目组状态"
<Select rules={[{required: true, message: '请选择项目组状态'}]}
mode="multiple" initialValue="ENABLE"
placeholder="请选择关联环境" >
optionFilterProp="label" <Radio.Group>
options={environments.map(env => ({ <Radio value="ENABLE"></Radio>
label: env.envName, <Radio value="DISABLE"></Radio>
value: env.id, </Radio.Group>
}))} </Form.Item>
/> </Form>
</Form.Item> </Modal>
);
<Form.Item
name="sort"
label="排序"
rules={[{ required: true, message: '请输入排序值' }]}
>
<InputNumber min={0} placeholder="请输入排序值" style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
);
}; };
export default ProjectGroupModal; export default ProjectGroupModal;

View File

@ -1,188 +0,0 @@
.project-card {
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.ant-card-body {
padding: 0;
}
.project-content {
padding: 20px;
}
// 项目组头部
.project-header {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 16px;
.project-type {
margin: 0;
padding: 6px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.title-section {
flex: 1;
min-width: 0;
.project-name {
margin: 0;
font-size: 16px;
line-height: 1.4;
color: #1f1f1f;
}
.project-code {
margin-top: 4px;
font-size: 13px;
color: #666;
font-family: 'monaco, monospace';
}
}
}
// 项目组描述
.project-desc {
color: #666;
font-size: 13px;
line-height: 1.6;
margin-bottom: 16px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
// 项目组统计
.project-stats {
display: flex;
gap: 16px;
margin-bottom: 20px;
padding: 12px;
background: #fafafa;
border-radius: 6px;
.stat-item {
display: flex;
align-items: center;
gap: 6px;
color: #666;
font-size: 13px;
.anticon {
font-size: 14px;
color: #8c8c8c;
}
}
}
// 环境列表
.environments-section {
margin-bottom: 20px;
.env-list {
display: flex;
flex-direction: column;
gap: 8px;
.env-item {
padding: 12px;
background: #fafafa;
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
background: #f0f0f0;
}
.env-main {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
.env-info {
flex: 1;
min-width: 0;
.env-name {
display: block;
font-size: 14px;
font-weight: 500;
color: #262626;
margin-bottom: 4px;
}
.env-code {
display: block;
font-size: 12px;
color: #666;
font-family: 'monaco, monospace';
}
}
.env-tags {
display: flex;
gap: 4px;
flex-shrink: 0;
.ant-tag {
margin: 0;
padding: 4px;
border: none;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
.anticon {
font-size: 12px;
}
}
}
}
.env-desc {
margin-top: 8px;
font-size: 12px;
color: #8c8c8c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
// 操作按钮
.project-actions {
display: flex;
gap: 8px;
padding-top: 16px;
border-top: 1px dashed #f0f0f0;
.ant-btn {
padding: 4px 12px;
height: 32px;
border-radius: 4px;
&:hover {
transform: translateY(-1px);
background: #fafafa;
}
&.ant-btn-dangerous:hover {
background: #fff1f0;
}
}
}
}

View File

@ -1,342 +1,367 @@
import React, { useState, useEffect } from 'react'; import React, {useState, useEffect} from 'react';
import { PageContainer } from '@ant-design/pro-layout'; import {PageContainer} from '@ant-design/pro-layout';
import { Card, Row, Col, Typography, Button, message, Input, Select, Empty, Tooltip, Tag, Space } from 'antd'; import {Card, Row, Col, Typography, Button, message, Popconfirm, Space, Tag, Input, Select, Tooltip, Statistic, Divider} from 'antd';
import { PlusOutlined, SearchOutlined, RocketOutlined, TeamOutlined, EnvironmentOutlined, EditOutlined, DeleteOutlined, ClockCircleOutlined, CloudServerOutlined, SettingOutlined } from '@ant-design/icons'; import {
import { getProjectGroupList, deleteProjectGroup } from './service'; PlusOutlined,
import type { ProjectGroup } from './types'; EditOutlined,
DeleteOutlined,
TeamOutlined,
SearchOutlined,
EnvironmentOutlined,
RocketOutlined
} from '@ant-design/icons';
import {getProjectGroupList, deleteProjectGroup} from './service';
import type {ProjectGroup} from './types';
import ProjectGroupModal from './components/ProjectGroupModal'; import ProjectGroupModal from './components/ProjectGroupModal';
import { BuildTypeEnum, DeployTypeEnum } from '../../Environment/List/types';
const { Title, Text } = Typography; const {Title, Text} = Typography;
const { Search } = Input; const {Search} = Input;
const ProjectList: React.FC = () => { const ProjectList: React.FC = () => {
const [projects, setProjects] = useState<ProjectGroup[]>([]); const [projects, setProjects] = useState<ProjectGroup[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [currentProject, setCurrentProject] = useState<ProjectGroup>(); const [currentProject, setCurrentProject] = useState<ProjectGroup>();
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [projectType, setProjectType] = useState<string>('ALL'); const [projectType, setProjectType] = useState<string>('ALL');
const fetchProjects = async () => { const fetchProjects = async () => {
setLoading(true); setLoading(true);
try { try {
const data = await getProjectGroupList(); const data = await getProjectGroupList();
setProjects(data); setProjects(data);
} catch (error) { } catch (error) {
message.error('获取项目组列表失败'); message.error('获取项目组列表失败');
} finally { } finally {
setLoading(false); 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: <RocketOutlined />,
description: '用于演示和测试的项目组'
};
}
if (code.toUpperCase().includes('PLATFORM')) {
return {
type: 'PLATFORM',
label: '平台项目组',
color: '#52c41a',
icon: <EnvironmentOutlined />,
description: '平台相关的核心项目组'
};
}
return {
type: 'BUSINESS',
label: '业务项目组',
color: '#722ed1',
icon: <TeamOutlined />,
description: '具体业务相关的项目组'
}; };
};
const filteredProjects = projects.filter(project => { useEffect(() => {
const matchesSearch = searchText ? fetchProjects();
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; const handleDelete = async (id: number) => {
try {
await deleteProjectGroup(id);
message.success('删除成功');
fetchProjects();
} catch (error) {
message.error('删除失败');
}
};
return matchesSearch && matchesType; const handleAdd = () => {
}); setCurrentProject(undefined);
setModalVisible(true);
};
const buildTypeMap = { const handleEdit = (project: ProjectGroup) => {
[BuildTypeEnum.JENKINS]: { setCurrentProject(project);
label: 'Jenkins构建', setModalVisible(true);
color: '#1890ff', };
icon: <RocketOutlined />,
},
[BuildTypeEnum.GITLAB_RUNNER]: {
label: 'GitLab Runner构建',
color: '#722ed1',
icon: <CloudServerOutlined />,
},
[BuildTypeEnum.GITHUB_ACTION]: {
label: 'GitHub Action构建',
color: '#52c41a',
icon: <SettingOutlined />,
},
};
const deployTypeMap = { const getProjectTypeInfo = (code: string) => {
[DeployTypeEnum.K8S]: { if (code.toUpperCase().includes('DEMO')) {
label: 'K8S', return {
color: '#1890ff', type: 'DEMO',
icon: <CloudServerOutlined />, label: '示例项目组',
}, color: '#1890ff',
[DeployTypeEnum.DOCKER]: { icon: <RocketOutlined/>,
label: 'Docker', description: '用于演示和测试的项目组'
color: '#13c2c2', };
icon: <RocketOutlined />, }
}, if (code.toUpperCase().includes('PLATFORM')) {
[DeployTypeEnum.VM]: { return {
label: 'VM', type: 'PLATFORM',
color: '#faad14', label: '平台项目组',
icon: <SettingOutlined />, color: '#52c41a',
}, icon: <EnvironmentOutlined/>,
}; description: '平台相关的核心项目组'
};
}
return {
type: 'BUSINESS',
label: '业务项目组',
color: '#722ed1',
icon: <TeamOutlined/>,
description: '具体业务相关的项目组'
};
};
const ProjectCard = ({ project }: { project: ProjectGroup }) => { const filteredProjects = projects.filter(project => {
const typeInfo = getProjectTypeInfo(project.projectGroupCode); 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 (
<Card
hoverable
className="project-card"
style={{
height: '100%',
borderRadius: '16px',
overflow: 'hidden',
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)',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
},
}}
bodyStyle={{
padding: '24px',
height: '100%',
display: 'flex',
flexDirection: 'column',
background: `linear-gradient(145deg, #ffffff 0%, ${typeInfo.color}08 100%)`,
}}
>
<div style={{flex: 1}}>
<div style={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
marginBottom: '16px',
position: 'relative'
}}>
<div style={{flex: 1}}>
<Title level={4} style={{
margin: 0,
color: '#1f1f1f',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
{project.projectGroupName}
<Tag
color={typeInfo.color}
style={{
margin: 0,
padding: '0 8px',
borderRadius: '4px',
fontSize: '12px'
}}
>
{typeInfo.label}
</Tag>
</Title>
<Text
type="secondary"
style={{
display: 'block',
marginTop: '8px',
height: '40px',
overflow: 'hidden',
textOverflow: 'ellipsis',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}}
>
{project.projectGroupDesc || '暂无描述'}
</Text>
</div>
<div style={{
color: typeInfo.color,
fontSize: '24px',
opacity: 0.8,
marginLeft: '16px'
}}>
{Icon}
</div>
</div>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
padding: '20px',
background: '#fff',
borderRadius: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
}}>
<div>
<Text type="secondary" style={{fontSize: '13px', display: 'block', marginBottom: '8px'}}></Text>
<div style={{
color: '#262626',
fontFamily: 'monaco, monospace',
fontSize: '14px',
padding: '8px 12px',
background: '#f5f5f5',
borderRadius: '6px',
letterSpacing: '0.5px',
}}>
{project.projectGroupCode}
</div>
</div>
<Row gutter={16}>
<Col span={8}>
<Statistic
title={<Text type="secondary" style={{fontSize: '13px'}}></Text>}
value={2}
prefix={<EnvironmentOutlined/>}
valueStyle={{fontSize: '16px', color: typeInfo.color}}
/>
</Col>
<Col span={8}>
<Statistic
title={<Text type="secondary" style={{fontSize: '13px'}}></Text>}
value={5}
prefix={<TeamOutlined/>}
valueStyle={{fontSize: '16px', color: typeInfo.color}}
/>
</Col>
<Col span={8}>
<Statistic
title={<Text type="secondary" style={{fontSize: '13px'}}></Text>}
value={5}
prefix={<TeamOutlined/>}
valueStyle={{fontSize: '16px', color: typeInfo.color}}
/>
</Col>
</Row>
<Divider style={{margin: '8px 0'}}/>
<Space size="middle" style={{justifyContent: 'center'}}>
<Button
type="primary"
icon={<EditOutlined/>}
onClick={() => handleEdit(project)}
style={{
borderRadius: '6px',
background: typeInfo.color,
border: 'none'
}}
>
</Button>
<Popconfirm
title="确定要删除该项目组吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(project.id)}
>
<Button
danger
icon={<DeleteOutlined/>}
style={{borderRadius: '6px'}}
>
</Button>
</Popconfirm>
<Button
icon={<TeamOutlined/>}
style={{borderRadius: '6px'}}
>
</Button>
</Space>
</div>
</div>
</Card>
);
};
return ( return (
<Card <PageContainer
hoverable header={{
className="project-card" title: '项目组管理',
style={{ extra: [
borderRadius: '8px', <Button
overflow: 'hidden', key="add"
height: '100%', type="primary"
transition: 'all 0.3s ease', icon={<PlusOutlined/>}
}} onClick={handleAdd}
> style={{
<div className="project-content"> borderRadius: '8px',
{/* 项目组标题和类型 */} boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
<div className="project-header"> height: '40px',
<Tag className="project-type" color={typeInfo.color}> padding: '0 24px',
{typeInfo.icon} }}
</Tag> >
<div className="title-section">
<Title level={4} className="project-name"> </Button>
{project.projectGroupName} ],
</Title>
<div className="project-code">
{project.projectGroupCode}
</div>
</div>
</div>
{/* 项目组描述 */}
<div className="project-desc">
{project.projectGroupDesc || '暂无描述'}
</div>
{/* 项目组统计信息 */}
<div className="project-stats">
<div className="stat-item">
<EnvironmentOutlined />
<span>{project.environments?.length || 0} </span>
</div>
<div className="stat-item">
<TeamOutlined />
<span>5 </span>
</div>
<div className="stat-item">
<ClockCircleOutlined />
<span>2</span>
</div>
</div>
{/* 环境列表 */}
{project.environments && project.environments.length > 0 && (
<div className="environments-section">
<div className="env-list">
{project.environments.map(env => {
const buildType = buildTypeMap[env.buildType];
const deployType = deployTypeMap[env.deployType];
return (
<div key={env.id} className="env-item">
<div className="env-main">
<div className="env-info">
<span className="env-name">{env.envName}</span>
<span className="env-code">{env.envCode}</span>
</div>
<div className="env-tags">
<Tooltip title={buildType.label}>
<Tag color={buildType.color}>{buildType.icon}</Tag>
</Tooltip>
<Tooltip title={deployType.label}>
<Tag color={deployType.color}>{deployType.icon}</Tag>
</Tooltip>
</div>
</div>
{env.envDesc && (
<div className="env-desc">{env.envDesc}</div>
)}
</div>
);
})}
</div>
</div>
)}
{/* 操作按钮 */}
<div className="project-actions">
<Button
type="text"
icon={<EditOutlined />}
onClick={() => handleEdit(project)}
>
</Button>
<Button
type="text"
icon={<TeamOutlined />}
>
</Button>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(project.id)}
>
</Button>
</div>
</div>
</Card>
);
};
return (
<PageContainer
header={{
title: '项目组管理',
extra: [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
style={{
borderRadius: '8px',
padding: '0 24px',
height: '40px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
transition: 'all 0.3s ease',
}} }}
> >
<div style={{marginBottom: '24px', padding: '0 24px'}}>
</Button> <Row gutter={16} align="middle">
], <Col flex="300px">
}} <Search
> placeholder="搜索项目组名称、编码或描述"
<div style={{ marginBottom: '24px', padding: '0 24px' }}> allowClear
<Row gutter={16} align="middle"> enterButton={<><SearchOutlined/> </>}
<Col flex="300px"> size="large"
<Search value={searchText}
placeholder="搜索项目组名称、编码或描述" onChange={e => setSearchText(e.target.value)}
allowClear style={{width: '100%'}}
enterButton={<><SearchOutlined /> </>} />
size="large" </Col>
value={searchText} </Row>
onChange={e => setSearchText(e.target.value)} </div>
style={{ width: '100%' }}
/>
</Col>
<Col>
<Select
placeholder="项目组类型"
style={{ width: 200 }}
value={projectType}
onChange={setProjectType}
options={[
{ label: '全部类型', value: 'ALL' },
{ label: '示例项目组', value: 'DEMO' },
{ label: '平台项目组', value: 'PLATFORM' },
{ label: '业务项目组', value: 'BUSINESS' },
]}
size="large"
/>
</Col>
</Row>
</div>
<div style={{ padding: '0 24px 24px' }}> <div style={{padding: '0 24px 24px'}}>
<Row gutter={[24, 24]}> <Row gutter={[24, 24]}>
{filteredProjects.length > 0 ? ( {filteredProjects.map(project => (
filteredProjects.map((project) => ( <Col xs={24} sm={12} md={8} lg={6} key={project.id}>
<Col key={project.id} xs={24} sm={12} md={8} lg={6}> <ProjectCard project={project}/>
<ProjectCard project={project} /> </Col>
</Col> ))}
)) <Col xs={24} sm={12} md={8} lg={6}>
) : ( <Card
<Col span={24}> hoverable
<Empty onClick={handleAdd}
image={Empty.PRESENTED_IMAGE_SIMPLE} style={{
description={searchText ? "未找到匹配的项目组" : "暂无项目组数据"} height: '100%',
style={{ borderRadius: '16px',
background: '#fff', border: '1px dashed #d9d9d9',
padding: '40px', background: '#fafafa',
borderRadius: '8px', display: 'flex',
}} alignItems: 'center',
> justifyContent: 'center',
<Button type="primary" onClick={handleAdd}> minHeight: '380px',
transition: 'all 0.3s',
</Button> }}
</Empty> bodyStyle={{
</Col> width: '100%',
)} height: '100%',
</Row> display: 'flex',
</div> alignItems: 'center',
justifyContent: 'center',
}}
>
<div style={{textAlign: 'center'}}>
<PlusOutlined style={{fontSize: '28px', color: '#8c8c8c'}}/>
<div style={{marginTop: '12px', color: '#8c8c8c', fontSize: '16px'}}></div>
</div>
</Card>
</Col>
</Row>
</div>
<ProjectGroupModal <ProjectGroupModal
visible={modalVisible} visible={modalVisible}
onCancel={() => setModalVisible(false)} onCancel={() => setModalVisible(false)}
onSuccess={() => { onSuccess={fetchProjects}
setModalVisible(false); initialValues={currentProject}
fetchProjects(); />
}} </PageContainer>
initialValues={currentProject} );
/>
</PageContainer>
);
}; };
export default ProjectList; export default ProjectList;