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

@ -181,27 +181,6 @@ const EnvironmentList: React.FC = () => {
{environment.envCode}
</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>
</Card>
);

View File

@ -115,19 +115,6 @@ const ProjectGroupModal: React.FC<ProjectGroupModalProps> = ({
>
<Input.TextArea rows={4} placeholder="请输入项目组描述"/>
</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
name="environments"
label="关联环境"
@ -143,13 +130,16 @@ const ProjectGroupModal: React.FC<ProjectGroupModalProps> = ({
}))}
/>
</Form.Item>
<Form.Item
name="sort"
label="排序"
rules={[{ required: true, message: '请输入排序值' }]}
name="projectGroupStatus"
label="项目组状态"
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>
</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,11 +1,18 @@
import React, {useState, useEffect} from 'react';
import {PageContainer} from '@ant-design/pro-layout';
import { Card, Row, Col, Typography, Button, message, Input, Select, Empty, Tooltip, Tag, Space } from 'antd';
import { PlusOutlined, SearchOutlined, RocketOutlined, TeamOutlined, EnvironmentOutlined, EditOutlined, DeleteOutlined, ClockCircleOutlined, CloudServerOutlined, SettingOutlined } from '@ant-design/icons';
import {Card, Row, Col, Typography, Button, message, Popconfirm, Space, Tag, Input, Select, Tooltip, Statistic, Divider} from 'antd';
import {
PlusOutlined,
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 { BuildTypeEnum, DeployTypeEnum } from '../../Environment/List/types';
const {Title, Text} = Typography;
const {Search} = Input;
@ -83,161 +90,187 @@ const ProjectList: React.FC = () => {
};
const filteredProjects = projects.filter(project => {
const matchesSearch = searchText ?
const matchesSearch = searchText ? (
project.projectGroupName.toLowerCase().includes(searchText.toLowerCase()) ||
project.projectGroupCode.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;
});
const buildTypeMap = {
[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 Icon = typeInfo.icon;
return (
<Card
hoverable
className="project-card"
style={{
borderRadius: '8px',
overflow: 'hidden',
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 className="project-header">
<Tag className="project-type" color={typeInfo.color}>
{typeInfo.icon}
</Tag>
<div className="title-section">
<Title level={4} className="project-name">
<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>
<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}
</div>
</div>
</div>
{/* 项目组描述 */}
<div className="project-desc">
{project.projectGroupDesc || '暂无描述'}
</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>
{/* 项目组统计信息 */}
<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>
<Divider style={{margin: '8px 0'}}/>
{/* 环境列表 */}
{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">
<Space size="middle" style={{justifyContent: 'center'}}>
<Button
type="text"
type="primary"
icon={<EditOutlined/>}
onClick={() => handleEdit(project)}
style={{
borderRadius: '6px',
background: typeInfo.color,
border: 'none'
}}
>
</Button>
<Button
type="text"
icon={<TeamOutlined />}
<Popconfirm
title="确定要删除该项目组吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(project.id)}
>
</Button>
<Button
type="text"
danger
icon={<DeleteOutlined/>}
onClick={() => handleDelete(project.id)}
style={{borderRadius: '6px'}}
>
</Button>
</Popconfirm>
<Button
icon={<TeamOutlined/>}
style={{borderRadius: '6px'}}
>
</Button>
</Space>
</div>
</div>
</Card>
@ -256,10 +289,9 @@ const ProjectList: React.FC = () => {
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',
height: '40px',
padding: '0 24px',
}}
>
@ -280,59 +312,52 @@ const ProjectList: React.FC = () => {
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'}}>
<Row gutter={[24, 24]}>
{filteredProjects.length > 0 ? (
filteredProjects.map((project) => (
<Col key={project.id} xs={24} sm={12} md={8} lg={6}>
{filteredProjects.map(project => (
<Col xs={24} sm={12} md={8} lg={6} key={project.id}>
<ProjectCard project={project}/>
</Col>
))
) : (
<Col span={24}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={searchText ? "未找到匹配的项目组" : "暂无项目组数据"}
))}
<Col xs={24} sm={12} md={8} lg={6}>
<Card
hoverable
onClick={handleAdd}
style={{
background: '#fff',
padding: '40px',
borderRadius: '8px',
height: '100%',
borderRadius: '16px',
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}>
</Button>
</Empty>
<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
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onSuccess={() => {
setModalVisible(false);
fetchProjects();
}}
onSuccess={fetchProjects}
initialValues={currentProject}
/>
</PageContainer>