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,32 +1,32 @@
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进行自动化工作流'
}, },
}; };
@ -35,19 +35,19 @@ 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: '部署到传统虚拟机环境'
}, },
}; };
@ -94,7 +94,7 @@ const EnvironmentList: React.FC = () => {
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];
@ -112,7 +112,7 @@ const EnvironmentList: React.FC = () => {
<Tooltip title="编辑环境"> <Tooltip title="编辑环境">
<Button <Button
type="text" type="text"
icon={<EditOutlined />} icon={<EditOutlined/>}
onClick={() => handleEdit(environment)} onClick={() => handleEdit(environment)}
> >
@ -122,7 +122,7 @@ const EnvironmentList: React.FC = () => {
<Button <Button
type="text" type="text"
danger danger
icon={<DeleteOutlined />} icon={<DeleteOutlined/>}
onClick={() => handleDelete(environment.id)} onClick={() => handleDelete(environment.id)}
> >
@ -130,17 +130,17 @@ const EnvironmentList: React.FC = () => {
</Tooltip> </Tooltip>
]} ]}
> >
<div style={{ position: 'relative', padding: '16px' }}> <div style={{position: 'relative', padding: '16px'}}>
<div style={{ <div style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: '16px' marginBottom: '16px'
}}> }}>
<Title level={4} style={{ margin: 0 }}> <Title level={4} style={{margin: 0}}>
{environment.envName} {environment.envName}
</Title> </Title>
<Tag color={buildType.color} style={{ borderRadius: '12px', padding: '0 12px' }}> <Tag color={buildType.color} style={{borderRadius: '12px', padding: '0 12px'}}>
{buildType.icon} {buildType.label} {buildType.icon} {buildType.label}
</Tag> </Tag>
</div> </div>
@ -181,27 +181,6 @@ const EnvironmentList: React.FC = () => {
{environment.envCode} {environment.envCode}
</div> </div>
</div> </div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '16px'
}}>
<Tooltip title={deployType.description}>
<Tag
color={deployType.color}
style={{
padding: '4px 12px',
borderRadius: '12px',
cursor: 'help'
}}
>
{deployType.icon} {deployType.label}
</Tag>
</Tooltip>
<Text type="secondary">: {environment.sort}</Text>
</div>
</div> </div>
</Card> </Card>
); );
@ -215,7 +194,7 @@ const EnvironmentList: React.FC = () => {
<Button <Button
key="add" key="add"
type="primary" type="primary"
icon={<PlusOutlined />} icon={<PlusOutlined/>}
onClick={handleAdd} onClick={handleAdd}
style={{ style={{
borderRadius: '8px', borderRadius: '8px',
@ -230,12 +209,12 @@ const EnvironmentList: React.FC = () => {
], ],
}} }}
> >
<div style={{ padding: '24px' }}> <div style={{padding: '24px'}}>
<Row gutter={[24, 24]}> <Row gutter={[24, 24]}>
{environments.length > 0 ? ( {environments.length > 0 ? (
environments.map((environment) => ( environments.map((environment) => (
<Col key={environment.id} xs={24} sm={12} md={8} lg={6}> <Col key={environment.id} xs={24} sm={12} md={8} lg={6}>
<EnvironmentCard environment={environment} /> <EnvironmentCard environment={environment}/>
</Col> </Col>
)) ))
) : ( ) : (

View File

@ -1,9 +1,9 @@
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;
@ -17,7 +17,7 @@ const ProjectGroupModal: React.FC<ProjectGroupModalProps> = ({
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;
@ -50,14 +50,14 @@ const ProjectGroupModal: React.FC<ProjectGroupModalProps> = ({
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
// 转换环境ID数组为对象数组 // 转换环境ID数组为对象数组
const environments = (values.environments || []).map((id: number) => ({ id })); const environments = (values.environments || []).map((id: number) => ({id}));
const submitData = { const submitData = {
...values, ...values,
environments environments
}; };
if (isEdit) { if (isEdit) {
await updateProjectGroup({ ...submitData, id: initialValues.id }); await updateProjectGroup({...submitData, id: initialValues.id});
message.success('更新成功'); message.success('更新成功');
} else { } else {
await createProjectGroup(submitData); await createProjectGroup(submitData);
@ -88,50 +88,37 @@ const ProjectGroupModal: React.FC<ProjectGroupModalProps> = ({
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={{ projectGroupStatus: 'ENABLE', sort: 0 }} initialValues={{projectGroupStatus: 'ENABLE', sort: 0}}
> >
<Form.Item <Form.Item
name="projectGroupCode" name="projectGroupCode"
label="项目组编码" label="项目组编码"
rules={[ rules={[
{ required: true, message: '请输入项目组编码' }, {required: true, message: '请输入项目组编码'},
{ pattern: /^[A-Za-z0-9_-]+$/, message: '项目组编码只能包含字母、数字、下划线和连字符' } {pattern: /^[A-Za-z0-9_-]+$/, message: '项目组编码只能包含字母、数字、下划线和连字符'}
]} ]}
> >
<Input placeholder="请输入项目组编码" disabled={isEdit} /> <Input placeholder="请输入项目组编码" disabled={isEdit}/>
</Form.Item> </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
name="projectGroupStatus"
label="项目组状态"
rules={[{ required: true, message: '请选择项目组状态' }]}
initialValue="ENABLE"
>
<Radio.Group>
<Radio value="ENABLE"></Radio>
<Radio value="DISABLE"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item <Form.Item
name="environments" name="environments"
label="关联环境" label="关联环境"
rules={[{ required: true, message: '请选择关联环境' }]} rules={[{required: true, message: '请选择关联环境'}]}
> >
<Select <Select
mode="multiple" mode="multiple"
@ -143,13 +130,16 @@ const ProjectGroupModal: React.FC<ProjectGroupModalProps> = ({
}))} }))}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="sort" name="projectGroupStatus"
label="排序" label="项目组状态"
rules={[{ required: true, message: '请输入排序值' }]} rules={[{required: true, message: '请选择项目组状态'}]}
initialValue="ENABLE"
> >
<InputNumber min={0} placeholder="请输入排序值" style={{ width: '100%' }} /> <Radio.Group>
<Radio value="ENABLE"></Radio>
<Radio value="DISABLE"></Radio>
</Radio.Group>
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>

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,14 +1,21 @@
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[]>([]);
@ -60,7 +67,7 @@ const ProjectList: React.FC = () => {
type: 'DEMO', type: 'DEMO',
label: '示例项目组', label: '示例项目组',
color: '#1890ff', color: '#1890ff',
icon: <RocketOutlined />, icon: <RocketOutlined/>,
description: '用于演示和测试的项目组' description: '用于演示和测试的项目组'
}; };
} }
@ -69,7 +76,7 @@ const ProjectList: React.FC = () => {
type: 'PLATFORM', type: 'PLATFORM',
label: '平台项目组', label: '平台项目组',
color: '#52c41a', color: '#52c41a',
icon: <EnvironmentOutlined />, icon: <EnvironmentOutlined/>,
description: '平台相关的核心项目组' description: '平台相关的核心项目组'
}; };
} }
@ -77,167 +84,193 @@ const ProjectList: React.FC = () => {
type: 'BUSINESS', type: 'BUSINESS',
label: '业务项目组', label: '业务项目组',
color: '#722ed1', color: '#722ed1',
icon: <TeamOutlined />, icon: <TeamOutlined/>,
description: '具体业务相关的项目组' description: '具体业务相关的项目组'
}; };
}; };
const filteredProjects = projects.filter(project => { const filteredProjects = projects.filter(project => {
const matchesSearch = searchText ? const matchesSearch = searchText ? (
project.projectGroupName.toLowerCase().includes(searchText.toLowerCase()) || project.projectGroupName.toLowerCase().includes(searchText.toLowerCase()) ||
project.projectGroupCode.toLowerCase().includes(searchText.toLowerCase()) || project.projectGroupCode.toLowerCase().includes(searchText.toLowerCase()) ||
project.projectGroupDesc?.toLowerCase().includes(searchText.toLowerCase()) project.projectGroupDesc?.toLowerCase().includes(searchText.toLowerCase())
: true; ) : true;
const matchesType = projectType === 'ALL' ? true : getProjectTypeInfo(project.projectGroupCode).type === projectType; const matchesType = projectType === 'ALL' ? true :
getProjectTypeInfo(project.projectGroupCode).type === projectType;
return matchesSearch && matchesType; return matchesSearch && matchesType;
}); });
const buildTypeMap = { const ProjectCard = ({project}: { project: ProjectGroup }) => {
[BuildTypeEnum.JENKINS]: {
label: 'Jenkins构建',
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 = {
[DeployTypeEnum.K8S]: {
label: 'K8S',
color: '#1890ff',
icon: <CloudServerOutlined />,
},
[DeployTypeEnum.DOCKER]: {
label: 'Docker',
color: '#13c2c2',
icon: <RocketOutlined />,
},
[DeployTypeEnum.VM]: {
label: 'VM',
color: '#faad14',
icon: <SettingOutlined />,
},
};
const ProjectCard = ({ project }: { project: ProjectGroup }) => {
const typeInfo = getProjectTypeInfo(project.projectGroupCode); const typeInfo = getProjectTypeInfo(project.projectGroupCode);
const Icon = typeInfo.icon;
return ( return (
<Card <Card
hoverable hoverable
className="project-card" className="project-card"
style={{ style={{
borderRadius: '8px',
overflow: 'hidden',
height: '100%', height: '100%',
transition: 'all 0.3s ease', 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 className="project-content"> <div style={{flex: 1}}>
{/* 项目组标题和类型 */} <div style={{
<div className="project-header"> display: 'flex',
<Tag className="project-type" color={typeInfo.color}> alignItems: 'flex-start',
{typeInfo.icon} justifyContent: 'space-between',
</Tag> marginBottom: '16px',
<div className="title-section"> position: 'relative'
<Title level={4} className="project-name"> }}>
<div style={{flex: 1}}>
<Title level={4} style={{
margin: 0,
color: '#1f1f1f',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
{project.projectGroupName} {project.projectGroupName}
<Tag
color={typeInfo.color}
style={{
margin: 0,
padding: '0 8px',
borderRadius: '4px',
fontSize: '12px'
}}
>
{typeInfo.label}
</Tag>
</Title> </Title>
<div className="project-code"> <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} {project.projectGroupCode}
</div> </div>
</div> </div>
</div>
{/* 项目组描述 */} <Row gutter={16}>
<div className="project-desc"> <Col span={8}>
{project.projectGroupDesc || '暂无描述'} <Statistic
</div> 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'}}/>
<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>
{/* 环境列表 */} <Space size="middle" style={{justifyContent: 'center'}}>
{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 <Button
type="text" type="primary"
icon={<EditOutlined />} icon={<EditOutlined/>}
onClick={() => handleEdit(project)} onClick={() => handleEdit(project)}
style={{
borderRadius: '6px',
background: typeInfo.color,
border: 'none'
}}
> >
</Button> </Button>
<Button <Popconfirm
type="text" title="确定要删除该项目组吗?"
icon={<TeamOutlined />} description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(project.id)}
> >
</Button>
<Button <Button
type="text"
danger danger
icon={<DeleteOutlined />} icon={<DeleteOutlined/>}
onClick={() => handleDelete(project.id)} style={{borderRadius: '6px'}}
> >
</Button> </Button>
</Popconfirm>
<Button
icon={<TeamOutlined/>}
style={{borderRadius: '6px'}}
>
</Button>
</Space>
</div> </div>
</div> </div>
</Card> </Card>
@ -252,14 +285,13 @@ const ProjectList: React.FC = () => {
<Button <Button
key="add" key="add"
type="primary" type="primary"
icon={<PlusOutlined />} icon={<PlusOutlined/>}
onClick={handleAdd} onClick={handleAdd}
style={{ style={{
borderRadius: '8px', borderRadius: '8px',
padding: '0 24px',
height: '40px',
boxShadow: '0 2px 0 rgba(0,0,0,0.045)', boxShadow: '0 2px 0 rgba(0,0,0,0.045)',
transition: 'all 0.3s ease', height: '40px',
padding: '0 24px',
}} }}
> >
@ -267,72 +299,65 @@ const ProjectList: React.FC = () => {
], ],
}} }}
> >
<div style={{ marginBottom: '24px', padding: '0 24px' }}> <div style={{marginBottom: '24px', padding: '0 24px'}}>
<Row gutter={16} align="middle"> <Row gutter={16} align="middle">
<Col flex="300px"> <Col flex="300px">
<Search <Search
placeholder="搜索项目组名称、编码或描述" placeholder="搜索项目组名称、编码或描述"
allowClear allowClear
enterButton={<><SearchOutlined /> </>} enterButton={<><SearchOutlined/> </>}
size="large" size="large"
value={searchText} value={searchText}
onChange={e => setSearchText(e.target.value)} onChange={e => setSearchText(e.target.value)}
style={{ width: '100%' }} 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> </Col>
</Row> </Row>
</div> </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}>
<Col span={24}> <Card
<Empty hoverable
image={Empty.PRESENTED_IMAGE_SIMPLE} onClick={handleAdd}
description={searchText ? "未找到匹配的项目组" : "暂无项目组数据"}
style={{ style={{
background: '#fff', height: '100%',
padding: '40px', borderRadius: '16px',
borderRadius: '8px', border: '1px dashed #d9d9d9',
background: '#fafafa',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '380px',
transition: 'all 0.3s',
}}
bodyStyle={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}} }}
> >
<Button type="primary" onClick={handleAdd}> <div style={{textAlign: 'center'}}>
<PlusOutlined style={{fontSize: '28px', color: '#8c8c8c'}}/>
</Button> <div style={{marginTop: '12px', color: '#8c8c8c', fontSize: '16px'}}></div>
</Empty> </div>
</Card>
</Col> </Col>
)}
</Row> </Row>
</div> </div>
<ProjectGroupModal <ProjectGroupModal
visible={modalVisible} visible={modalVisible}
onCancel={() => setModalVisible(false)} onCancel={() => setModalVisible(false)}
onSuccess={() => { onSuccess={fetchProjects}
setModalVisible(false);
fetchProjects();
}}
initialValues={currentProject} initialValues={currentProject}
/> />
</PageContainer> </PageContainer>